Models 44

GPT-6 Luna

by OpenAIReasoning1 console error
Open in new window

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

How it went

Total time
5 min 57 s
Cost
$0.015
Output tokens
30,073
Spent thinking
16,056 tokens
Speed
86.1 tok/s
Console errors
1
First token after
7.4 s
First visible text after
3 min 48 s
Prompt tokens
508
Code size
41.7 KB
Finish reason
stop
Served by
OpenAI
Model id
openai/gpt-6-luna
Reasoning sent
{"effort":"xhigh"}
Max output
64,000 tokens
Generated
Nov 10, 2026

Console errors

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

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

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

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout — Neon Circuit</title>
<style>
  :root {
    color-scheme: dark;
    --ink: #edf5ff;
    --muted: #8293ad;
    --cyan: #51e3db;
    --panel: #101a2a;
  }

  * { box-sizing: border-box; }

  html, body {
    width: 100%;
    min-height: 100%;
    margin: 0;
  }

  body {
    min-height: 100vh;
    overflow-x: hidden;
    color: var(--ink);
    background:
      radial-gradient(ellipse at 50% -28%, rgba(48, 93, 150, .27), transparent 58%),
      radial-gradient(ellipse at 0% 80%, rgba(28, 111, 128, .09), transparent 34%),
      #080d17;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  }

  .app {
    width: min(1220px, 100%);
    min-height: 100vh;
    margin: 0 auto;
    padding: 17px 24px 12px;
    display: flex;
    flex-direction: column;
  }

  .topbar {
    height: 58px;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
  }

  .brand {
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .brand-mark {
    width: 37px;
    height: 37px;
    padding: 7px;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
    gap: 2px;
    flex: 0 0 auto;
    border: 1px solid rgba(103, 229, 223, .32);
    border-radius: 11px;
    background: linear-gradient(145deg, rgba(49, 220, 207, .16), rgba(85, 117, 255, .13));
    box-shadow: 0 0 22px rgba(58, 221, 213, .12), inset 0 1px rgba(255,255,255,.09);
  }

  .brand-mark i {
    display: block;
    border-radius: 2px;
    background: #60e6dc;
    box-shadow: 0 0 7px rgba(79, 230, 220, .42);
  }

  .brand-mark i:nth-child(2),
  .brand-mark i:nth-child(4),
  .brand-mark i:nth-child(8) { opacity: .45; }
  .brand-mark i:nth-child(3),
  .brand-mark i:nth-child(7) { opacity: .72; }
  .brand-mark i:nth-child(5) { background: #c4fff8; }

  .brand-copy { min-width: 0; }

  .eyebrow {
    margin-bottom: 2px;
    color: #7f94ad;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: .19em;
    line-height: 1;
    text-transform: uppercase;
  }

  .brand-title {
    margin: 0;
    font-size: 20px;
    font-weight: 850;
    letter-spacing: .105em;
    line-height: 1.12;
  }

  .brand-title span {
    color: #61ddd5;
    text-shadow: 0 0 16px rgba(81, 227, 219, .3);
  }

  .run-badge {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    padding: 9px 13px;
    border: 1px solid rgba(128, 156, 190, .18);
    border-radius: 999px;
    background: rgba(16, 27, 43, .78);
    color: #c5d1df;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: .13em;
    white-space: nowrap;
  }

  .run-badge .dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #55e2ce;
    box-shadow: 0 0 10px rgba(85, 226, 206, .8);
    animation: pulse 1.7s ease-in-out infinite;
  }

  @keyframes pulse {
    50% { opacity: .48; box-shadow: 0 0 3px rgba(85,226,206,.3); }
  }

  .game-panel {
    flex: 1 1 auto;
    min-height: 500px;
    display: flex;
    flex-direction: column;
    padding: 12px;
    overflow: hidden;
    border: 1px solid rgba(133, 168, 207, .17);
    border-radius: 22px;
    background:
      linear-gradient(150deg, rgba(21, 34, 53, .96), rgba(12, 20, 33, .96) 65%),
      #101a2a;
    box-shadow:
      0 24px 75px rgba(0, 0, 0, .34),
      inset 0 1px 0 rgba(255,255,255,.045);
    position: relative;
  }

  .hud {
    height: 56px;
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: 1fr 1.2fr 1fr;
    align-items: center;
    padding: 0 11px 9px;
  }

  .score-block,
  .lives-block {
    display: flex;
    flex-direction: column;
    gap: 3px;
  }

  .hud-label {
    color: #7c90aa;
    font-size: 8px;
    font-weight: 850;
    letter-spacing: .18em;
    line-height: 1;
    text-transform: uppercase;
  }

  .score-number {
    color: #eff8ff;
    font-size: 18px;
    font-variant-numeric: tabular-nums;
    font-weight: 800;
    letter-spacing: .055em;
    line-height: 1;
    text-shadow: 0 0 16px rgba(105, 183, 255, .16);
  }

  .level-block {
    width: min(220px, 100%);
    justify-self: center;
    text-align: center;
  }

  .level-line {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    color: #aebbd0;
    font-size: 9px;
    font-weight: 750;
    letter-spacing: .11em;
    white-space: nowrap;
  }

  .level-line b {
    color: #f4f8ff;
    font-size: 11px;
    letter-spacing: .12em;
  }

  .level-line .divider { color: #4a607c; }

  .progress-track {
    height: 3px;
    margin-top: 7px;
    overflow: hidden;
    border-radius: 9px;
    background: rgba(109, 138, 170, .17);
  }

  .progress-fill {
    width: 0%;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, #40d8cf, #8d8bff);
    box-shadow: 0 0 9px rgba(89, 221, 221, .6);
    transition: width .22s ease;
  }

  .lives-block { align-items: flex-end; }

  .life-dots {
    height: 16px;
    display: flex;
    align-items: center;
    gap: 5px;
  }

  .life-dot {
    width: 12px;
    height: 12px;
    border: 1px solid rgba(115, 234, 226, .26);
    border-radius: 4px 4px 6px 6px;
    background: rgba(58, 209, 200, .12);
    transform: rotate(45deg) scale(.82);
  }

  .life-dot.on {
    border-color: #66e4da;
    background: linear-gradient(135deg, #b7fff3, #37c8c5);
    box-shadow: 0 0 10px rgba(72, 221, 212, .44);
  }

  .arena-wrap {
    position: relative;
    flex: 1 1 auto;
    min-height: 420px;
    overflow: hidden;
    border: 1px solid rgba(115, 152, 195, .16);
    border-radius: 14px;
    background: #0a1220;
    box-shadow: inset 0 0 30px rgba(2, 9, 18, .36);
  }

  canvas {
    display: block;
    width: 100%;
    height: 100%;
    outline: none;
    touch-action: none;
  }

  .demo-prompt {
    position: absolute;
    top: 12px;
    left: 50%;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 9px;
    max-width: calc(100% - 20px);
    padding: 7px 12px;
    border: 1px solid rgba(111, 208, 210, .25);
    border-radius: 11px;
    background: rgba(10, 19, 32, .88);
    box-shadow: 0 8px 24px rgba(0,0,0,.18), inset 0 1px rgba(255,255,255,.045);
    backdrop-filter: blur(8px);
    transform: translateX(-50%);
    transition: opacity .25s ease, visibility .25s ease, transform .25s ease;
    pointer-events: none;
  }

  .demo-prompt.hidden {
    visibility: hidden;
    opacity: 0;
    transform: translate(-50%, -5px);
  }

  .prompt-symbol {
    width: 23px;
    height: 23px;
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    border: 1px solid rgba(92, 229, 217, .26);
    border-radius: 7px;
    color: #78f1e2;
    background: rgba(51, 209, 195, .12);
    font-size: 13px;
  }

  .prompt-copy { min-width: 0; }

  .prompt-copy strong {
    display: block;
    color: #e6fbf7;
    font-size: 10px;
    font-weight: 850;
    letter-spacing: .12em;
    line-height: 1.15;
    white-space: nowrap;
  }

  .prompt-copy span {
    display: block;
    margin-top: 2px;
    color: #9aaec2;
    font-size: 9px;
    line-height: 1.1;
    white-space: nowrap;
  }

  .controls {
    height: 31px;
    flex: 0 0 auto;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 10px;
    padding: 0 4px 1px;
    color: #8395ab;
    font-size: 9px;
    font-weight: 750;
    letter-spacing: .1em;
    text-transform: uppercase;
  }

  .control-hints {
    display: flex;
    align-items: center;
    gap: 17px;
    min-width: 0;
  }

  .hint {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
  }

  .keys {
    display: inline-flex;
    gap: 2px;
  }

  kbd {
    min-width: 16px;
    height: 17px;
    display: inline-grid;
    place-items: center;
    padding: 0 4px;
    border: 1px solid rgba(147, 171, 199, .25);
    border-bottom-color: rgba(147, 171, 199, .43);
    border-radius: 4px;
    background: rgba(104, 129, 158, .1);
    color: #d7e5f5;
    font: inherit;
    font-size: 9px;
    line-height: 1;
  }

  .power-state {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: #9babc1;
    white-space: nowrap;
  }

  .power-state .spark {
    color: #76e5db;
    font-size: 13px;
    text-shadow: 0 0 8px rgba(97,229,220,.65);
  }

  .end-screen {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: none;
    place-items: center;
    padding: 18px;
    background: rgba(5, 10, 19, .72);
    backdrop-filter: blur(5px);
  }

  .end-screen.show { display: grid; }

  .end-card {
    width: min(350px, 100%);
    padding: 27px 25px 21px;
    border: 1px solid rgba(128, 192, 215, .27);
    border-radius: 19px;
    background:
      radial-gradient(circle at 50% 0%, rgba(70, 205, 204, .12), transparent 52%),
      linear-gradient(150deg, rgba(22, 37, 57, .98), rgba(12, 20, 34, .99));
    box-shadow: 0 24px 70px rgba(0,0,0,.48), inset 0 1px rgba(255,255,255,.07);
    text-align: center;
    animation: pop-in .3s ease both;
  }

  @keyframes pop-in {
    from { opacity: 0; transform: translateY(8px) scale(.97); }
    to { opacity: 1; transform: translateY(0) scale(1); }
  }

  .end-icon {
    width: 43px;
    height: 43px;
    margin: 0 auto 12px;
    display: grid;
    place-items: center;
    border: 1px solid rgba(100, 224, 216, .28);
    border-radius: 14px;
    color: #77f0df;
    background: rgba(66, 214, 202, .12);
    box-shadow: 0 0 25px rgba(76, 217, 208, .12);
    font-size: 21px;
  }

  .end-kicker {
    margin: 0 0 6px;
    color: #7cded8;
    font-size: 9px;
    font-weight: 850;
    letter-spacing: .2em;
    text-transform: uppercase;
  }

  .end-card h2 {
    margin: 0;
    color: #f4f8ff;
    font-size: 27px;
    font-weight: 850;
    letter-spacing: .075em;
  }

  .end-copy {
    margin: 8px 0 19px;
    color: #a7b6c9;
    font-size: 12px;
    line-height: 1.5;
  }

  .restart-button {
    width: 100%;
    min-height: 43px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    border: 1px solid rgba(132, 255, 242, .34);
    border-radius: 10px;
    background: linear-gradient(100deg, #39c9c6, #4b9fe1);
    box-shadow: 0 8px 22px rgba(46, 182, 193, .17), inset 0 1px rgba(255,255,255,.25);
    color: #071824;
    cursor: pointer;
    font: inherit;
    font-size: 11px;
    font-weight: 900;
    letter-spacing: .13em;
    transition: filter .18s ease, transform .18s ease;
  }

  .restart-button:hover { filter: brightness(1.12); transform: translateY(-1px); }
  .restart-button:active { transform: translateY(1px); }
  .restart-button span { font-size: 17px; font-weight: 500; }

  .end-card small {
    display: block;
    margin-top: 12px;
    color: #70839c;
    font-size: 8px;
    font-weight: 750;
    letter-spacing: .15em;
  }

  @media (max-width: 560px) {
    .app { padding: 12px 10px 10px; }
    .topbar { height: 52px; margin-bottom: 9px; }
    .brand { gap: 9px; }
    .brand-mark { width: 33px; height: 33px; padding: 6px; border-radius: 9px; }
    .eyebrow { font-size: 8px; }
    .brand-title { font-size: 18px; }
    .run-badge { gap: 7px; padding: 8px 9px; font-size: 8px; letter-spacing: .09em; }
    .game-panel { min-height: 490px; padding: 8px; border-radius: 17px; }
    .hud { height: 52px; padding: 0 6px 8px; grid-template-columns: 1fr 1fr 1fr; }
    .score-number { font-size: 15px; }
    .hud-label { font-size: 7px; }
    .level-line { gap: 5px; font-size: 8px; letter-spacing: .07em; }
    .level-line b { font-size: 10px; }
    .level-block { width: 100%; }
    .progress-track { margin-top: 6px; }
    .arena-wrap { min-height: 400px; border-radius: 11px; }
    .controls { height: 29px; font-size: 8px; }
    .control-hints { gap: 10px; }
    .hint { gap: 4px; }
    kbd { min-width: 14px; height: 15px; padding: 0 3px; font-size: 8px; }
    .power-state { gap: 4px; font-size: 7px; letter-spacing: .055em; }
    .power-state .spark { font-size: 11px; }
  }

  @media (max-width: 390px) {
    .run-badge { padding: 7px 8px; }
    .control-hints { gap: 8px; }
    .control-hints .hint:nth-child(2) { display: none; }
    .controls { font-size: 7px; }
    .power-state { font-size: 7px; }
  }

  @media (max-height: 650px) and (min-width: 561px) {
    .app { padding-top: 10px; padding-bottom: 8px; }
    .topbar { height: 49px; margin-bottom: 7px; }
    .game-panel { min-height: 440px; padding: 9px; }
    .hud { height: 49px; }
    .arena-wrap { min-height: 365px; }
    .controls { height: 27px; }
  }
</style>
</head>
<body>
<div class="app">
  <header class="topbar">
    <div class="brand">
      <div class="brand-mark" aria-hidden="true">
        <i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
      </div>
      <div class="brand-copy">
        <div class="eyebrow">PIXEL LAB &nbsp; / &nbsp; ARCADE 01</div>
        <h1 class="brand-title">BREAK<span>OUT</span></h1>
      </div>
    </div>
    <div class="run-badge"><span class="dot"></span><span id="modeText">DEMO RUNNING</span></div>
  </header>

  <main class="game-panel">
    <section class="hud" aria-label="Game status">
      <div class="score-block">
        <span class="hud-label">Score</span>
        <strong class="score-number" id="score">000000</strong>
      </div>

      <div class="level-block">
        <div class="level-line">
          <span>LEVEL</span><b>01</b><span class="divider">/</span>
          <span><span id="remaining">60</span> BRICKS LEFT</span>
        </div>
        <div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
      </div>

      <div class="lives-block">
        <span class="hud-label">Lives</span>
        <div class="life-dots" id="lifeDots" aria-label="3 lives"></div>
      </div>
    </section>

    <div class="arena-wrap">
      <canvas id="game" tabindex="0" aria-label="Breakout game. Move the paddle with your mouse or the left and right arrow keys."></canvas>

      <div class="demo-prompt" id="demoPrompt">
        <div class="prompt-symbol">↗</div>
        <div class="prompt-copy">
          <strong>AUTO PILOT · LIVE DEMO</strong>
          <span>Click or press a key to start</span>
        </div>
      </div>

      <div class="end-screen" id="endScreen">
        <div class="end-card">
          <div class="end-icon" id="endIcon">✦</div>
          <p class="end-kicker" id="endKicker">ARCADE RESULT</p>
          <h2 id="endTitle">LEVEL CLEAR</h2>
          <p class="end-copy" id="endCopy">The grid is yours.</p>
          <button class="restart-button" id="restartButton" type="button">PLAY AGAIN <span>↻</span></button>
          <small>OR PRESS ENTER</small>
        </div>
      </div>
    </div>

    <footer class="controls">
      <div class="control-hints">
        <span class="hint"><span class="keys"><kbd>←</kbd><kbd>→</kbd></span> MOVE</span>
        <span class="hint">MOUSE TO STEER</span>
        <span class="hint">CATCH THE DROPS</span>
      </div>
      <div class="power-state"><span class="spark">✦</span><span id="powerText">NO ACTIVE BOOST</span></div>
    </footer>
  </main>
</div>

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

  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const scoreEl = document.getElementById("score");
  const remainingEl = document.getElementById("remaining");
  const progressFill = document.getElementById("progressFill");
  const lifeDots = document.getElementById("lifeDots");
  const modeText = document.getElementById("modeText");
  const demoPrompt = document.getElementById("demoPrompt");
  const endScreen = document.getElementById("endScreen");
  const endTitle = document.getElementById("endTitle");
  const endCopy = document.getElementById("endCopy");
  const endKicker = document.getElementById("endKicker");
  const endIcon = document.getElementById("endIcon");
  const restartButton = document.getElementById("restartButton");
  const powerText = document.getElementById("powerText");

  let W = 0;
  let H = 0;
  let dpr = 1;
  let initialized = false;
  let state = "playing";
  let demoMode = true;
  let controlMode = "ai";
  let mouseTarget = 0;
  let keys = new Set();
  let score = 0;
  let lives = 3;
  let gameTime = 0;
  let wideUntil = 0;
  let serveTimer = 0;
  let bricks = [];
  let bricksLeft = 0;
  let totalBricks = 0;
  let balls = [];
  let drops = [];
  let particles = [];
  let stars = [];
  let hudTimer = 0;
  let paddle = { x: 0, y: 0, w: 0, h: 12, baseW: 0, prevX: 0 };

  const palettes = [
    ["#56e3d5", "#128986"],
    ["#63c7ff", "#286cae"],
    ["#a99aff", "#6256af"],
    ["#f18bdd", "#a14c9a"],
    ["#ffad83", "#ad5d52"],
    ["#f8d17a", "#a87a33"]
  ];

  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  const random = (lo, hi) => lo + Math.random() * (hi - lo);

  function makeBall(x, y, vx, vy, radius) {
    return { x, y, vx, vy, r: radius, trail: [], dead: false };
  }

  function effectivePaddleWidth() {
    return paddle.baseW * (wideUntil > gameTime ? 1.55 : 1);
  }

  function makeStars() {
    stars = [];
    const count = Math.max(32, Math.floor(W * H / 8000));
    for (let i = 0; i < count; i++) {
      stars.push({
        x: random(14, Math.max(15, W - 14)),
        y: random(10, Math.max(11, H - 10)),
        r: random(.45, 1.35),
        phase: random(0, Math.PI * 2),
        alpha: random(.12, .48)
      });
    }
  }

  function createBricks() {
    const cols = 10;
    const gap = W < 500 ? 5 : 8;
    const boardW = Math.min(W - 38, 1000);
    const brickW = (boardW - (cols - 1) * gap) / cols;
    const brickH = W < 500 ? 20 : 23;
    const startX = (W - boardW) / 2;
    const startY = Math.max(78, Math.min(98, H * .15));
    const rowGap = W < 500 ? 8 : 9;
    const powerMap = {
      "0-1": "wide",
      "0-8": "multi",
      "2-3": "multi",
      "2-6": "wide",
      "4-1": "wide",
      "4-8": "multi",
      "5-5": "wide"
    };
    const list = [];

    for (let r = 0; r < 6; r++) {
      for (let c = 0; c < cols; c++) {
        const tough = (r === 1 && c !== 4) ||
                      (r === 3 && c % 2 === 0) ||
                      (r === 5 && (c === 2 || c === 7));
        list.push({
          x: startX + c * (brickW + gap),
          y: startY + r * (brickH + rowGap),
          w: brickW,
          h: brickH,
          row: r,
          hp: tough ? 2 : 1,
          maxHp: tough ? 2 : 1,
          alive: true,
          power: powerMap[`${r}-${c}`] || null
        });
      }
    }
    return list;
  }

  function fitCanvas() {
    const rect = canvas.getBoundingClientRect();
    if (!rect.width || !rect.height) return;

    const newW = rect.width;
    const newH = rect.height;
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(newW * dpr);
    canvas.height = Math.round(newH * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

    if (initialized && W && H) {
      const sx = newW / W;
      const sy = newH / H;
      paddle.x *= sx;
      paddle.prevX *= sx;
      paddle.y *= sy;
      paddle.baseW *= sx;
      paddle.w *= sx;
      paddle.h *= sy;
      mouseTarget *= sx;

      for (const b of balls) {
        b.x *= sx; b.y *= sy; b.vx *= sx; b.vy *= sy; b.r *= Math.sqrt(sx * sy);
        for (const t of b.trail) { t.x *= sx; t.y *= sy; }
      }
      for (const brick of bricks) {
        brick.x *= sx; brick.y *= sy; brick.w *= sx; brick.h *= sy;
      }
      for (const drop of drops) {
        drop.x *= sx; drop.y *= sy; drop.vx *= sx; drop.vy *= sy; drop.r *= Math.sqrt(sx * sy);
      }
      for (const p of particles) {
        p.x *= sx; p.y *= sy; p.vx *= sx; p.vy *= sy; p.size *= Math.sqrt(sx * sy);
      }
    }

    W = newW;
    H = newH;
    makeStars();
  }

  function resetGame() {
    score = 0;
    lives = 3;
    gameTime = 0;
    wideUntil = 0;
    serveTimer = 0;
    hudTimer = 0;
    drops = [];
    particles = [];
    keys.clear();
    state = "playing";
    demoMode = true;
    controlMode = "ai";

    paddle.baseW = Math.max(90, Math.min(136, W * .13));
    paddle.w = paddle.baseW;
    paddle.h = W < 500 ? 11 : 13;
    paddle.x = W / 2;
    paddle.prevX = paddle.x;
    paddle.y = H - (W < 500 ? 38 : 43);

    const radius = W < 500 ? 6.2 : 7.2;
    const speed = clamp(W * .34, 310, 390);
    balls = [makeBall(paddle.x, paddle.y - 25, speed * .20, -speed * .98, radius)];

    bricks = createBricks();
    bricksLeft = bricks.length;
    totalBricks = bricks.length;

    demoPrompt.classList.remove("hidden");
    endScreen.classList.remove("show");
    modeText.textContent = "DEMO RUNNING";
    updateHud();
  }

  function updateHud() {
    scoreEl.textContent = String(score).padStart(6, "0");
    remainingEl.textContent = String(bricksLeft);
    progressFill.style.width = `${totalBricks ? (100 * (totalBricks - bricksLeft) / totalBricks) : 100}%`;

    let dots = "";
    for (let i = 0; i < 3; i++) {
      dots += `<span class="life-dot ${i < lives ? "on" : ""}"></span>`;
    }
    lifeDots.innerHTML = dots;
    lifeDots.setAttribute("aria-label", `${lives} ${lives === 1 ? "life" : "lives"}`);

    if (wideUntil > gameTime) {
      powerText.textContent = `WIDE PADDLE · ${Math.ceil(wideUntil - gameTime)}S`;
    } else if (balls.length > 1) {
      powerText.textContent = `MULTI-BALL · ${balls.length} BALLS`;
    } else {
      powerText.textContent = "NO ACTIVE BOOST";
    }
  }

  function takeControl(mode, x) {
    if (state !== "playing") return;
    demoMode = false;
    controlMode = mode;
    if (typeof x === "number") mouseTarget = x;
    demoPrompt.classList.add("hidden");
    modeText.textContent = "PLAYER CONTROL";
    updateHud();
  }

  function finishGame(result) {
    state = result;
    demoPrompt.classList.add("hidden");
    endKicker.textContent = result === "won" ? "GRID DEFRAGMENTED" : "RUN COMPLETE";
    endTitle.textContent = result === "won" ? "LEVEL CLEAR" : "GAME OVER";
    endCopy.textContent = result === "won"
      ? `Beautiful run. You scored ${score.toLocaleString()} points.`
      : `You scored ${score.toLocaleString()} points. Ready for another run?`;
    endIcon.textContent = result === "won" ? "✦" : "↻";
    endScreen.classList.add("show");
    updateHud();
  }

  function spawnParticles(x, y, color, count, power = 1) {
    for (let i = 0; i < count; i++) {
      const angle = random(0, Math.PI * 2);
      const speed = random(45, 190) * power;
      const life = random(.28, .68);
      particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed,
        life,
        maxLife: life,
        size: random(1.1, 3.1) * power,
        color
      });
    }
  }

  function hitBrick(brick, ball) {
    const colors = palettes[brick.row];
    if (brick.hp > 1) {
      brick.hp--;
      score += 12;
      spawnParticles(ball.x, ball.y, colors[0], 7, .58);
    } else {
      brick.alive = false;
      bricksLeft--;
      score += 50 + (5 - brick.row) * 5;
      spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, colors[0], 16, .85);

      if (brick.power) {
        drops.push({
          x: brick.x + brick.w / 2,
          y: brick.y + brick.h / 2,
          vx: random(-13, 13),
          vy: 102,
          r: W < 500 ? 9 : 10,
          type: brick.power,
          phase: random(0, Math.PI * 2)
        });
      }
    }
  }

  function activatePower(type) {
    score += 100;
    if (type === "wide") {
      wideUntil = gameTime + 10;
      spawnParticles(paddle.x, paddle.y, "#67f1df", 13, .7);
    } else {
      if (!balls.length) {
        const s = clamp(W * .34, 310, 390);
        balls.push(makeBall(paddle.x, paddle.y - 22, s * .14, -s * .99, W < 500 ? 6.2 : 7.2));
      }
      const source = balls[0];
      const baseAngle = Math.atan2(source.vy, source.vx);
      const speed = Math.hypot(source.vx, source.vy);
      const radius = source.r;
      const additions = Math.max(0, 3 - balls.length);
      for (let i = 0; i < additions; i++) {
        const angle = baseAngle + (i === 0 ? -.48 : .48);
        const clone = makeBall(source.x, source.y, Math.cos(angle) * speed, Math.sin(angle) * speed, radius);
        clone.trail = [];
        balls.push(clone);
      }
      spawnParticles(paddle.x, paddle.y, "#b692ff", 16, .85);
    }
    updateHud();
  }

  function circleRectCollision(ball, rect, previousX, previousY) {
    const closestX = clamp(ball.x, rect.x, rect.x + rect.w);
    const closestY = clamp(ball.y, rect.y, rect.y + rect.h);
    let dx = ball.x - closestX;
    let dy = ball.y - closestY;
    let d2 = dx * dx + dy * dy;
    const rr = ball.r * ball.r;
    if (d2 >= rr) return false;

    let nx, ny, distance, correction;
    if (d2 > .0001) {
      distance = Math.sqrt(d2);
      nx = dx / distance;
      ny = dy / distance;
      correction = ball.r - distance + .18;
    } else {
      const edges = [
        { d: Math.abs(ball.x - rect.x), x: -1, y: 0 },
        { d: Math.abs(rect.x + rect.w - ball.x), x: 1, y: 0 },
        { d: Math.abs(ball.y - rect.y), x: 0, y: -1 },
        { d: Math.abs(rect.y + rect.h - ball.y), x: 0, y: 1 }
      ].sort((a, b) => a.d - b.d);
      nx = edges[0].x;
      ny = edges[0].y;
      correction = ball.r + edges[0].d + .18;
    }

    const approach = ball.vx * nx + ball.vy * ny;
    ball.x += nx * correction;
    ball.y += ny * correction;

    if (approach < 0) {
      ball.vx -= 2 * approach * nx;
      ball.vy -= 2 * approach * ny;
      hitBrick(rect, ball);
    } else if (previousX !== ball.x || previousY !== ball.y) {
      // Position correction keeps a ball that has entered a surface from sticking.
    }
    return true;
  }

  function updateBall(ball, dt) {
    ball.trail.unshift({ x: ball.x, y: ball.y });
    if (ball.trail.length > 7) ball.trail.length = 7;

    const distance = Math.max(Math.abs(ball.vx * dt), Math.abs(ball.vy * dt));
    const steps = clamp(Math.ceil(distance / Math.max(2.4, ball.r * .48)), 1, 14);
    const slice = dt / steps;

    for (let s = 0; s < steps; s++) {
      const previousX = ball.x;
      const previousY = ball.y;
      ball.x += ball.vx * slice;
      ball.y += ball.vy * slice;

      const left = 11 + ball.r;
      const right = W - 11 - ball.r;
      const top = 11 + ball.r;

      if (ball.x < left) {
        ball.x = left;
        if (ball.vx < 0) ball.vx = -ball.vx;
      } else if (ball.x > right) {
        ball.x = right;
        if (ball.vx > 0) ball.vx = -ball.vx;
      }
      if (ball.y < top) {
        ball.y = top;
        if (ball.vy < 0) ball.vy = -ball.vy;
      }

      let struck = false;
      for (const brick of bricks) {
        if (brick.alive && circleRectCollision(ball, brick, previousX, previousY)) {
          struck = true;
          break;
        }
      }

      const pW = effectivePaddleWidth();
      if (!struck && ball.vy > 0 &&
          ball.y + ball.r >= paddle.y &&
          ball.y - ball.r <= paddle.y + paddle.h &&
          ball.x + ball.r >= paddle.x - pW / 2 &&
          ball.x - ball.r <= paddle.x + pW / 2) {
        const impact = clamp((ball.x - paddle.x) / (pW / 2), -1, 1);
        const paddleMotion = clamp((paddle.x - paddle.prevX) / Math.max(dt, .001), -500, 500);
        let angle = impact * 1.12 + paddleMotion * .00016;
        angle = clamp(angle, -1.22, 1.22);
        const speed = clamp(Math.hypot(ball.vx, ball.vy), 300, 405);
        ball.vx = Math.sin(angle) * speed;
        ball.vy = -Math.cos(angle) * speed;
        ball.y = paddle.y - ball.r - .35;
        spawnParticles(ball.x, paddle.y, "#72e8df", 5, .48);
      }

      if (ball.y > H + ball.r + 4) {
        ball.dead = true;
        break;
      }
    }
  }

  function reflectX(x, radius) {
    const min = 11 + radius;
    const max = W - 11 - radius;
    const span = Math.max(1, max - min);
    let u = (x - min) % (2 * span);
    if (u < 0) u += 2 * span;
    return min + (u <= span ? u : 2 * span - u);
  }

  function aiTarget() {
    let target = paddle.x;
    let lowest = null;
    for (const ball of balls) {
      if (ball.vy > 0 && (!lowest || ball.y > lowest.y)) lowest = ball;
    }

    if (lowest) {
      const t = Math.max(0, (paddle.y - lowest.r - lowest.y) / lowest.vy);
      target = reflectX(lowest.x + lowest.vx * t, lowest.r);
    }

    const worthwhileDrop = drops
      .filter(d => d.y > H * .52 && d.y < paddle.y && d.vy > 0)
      .sort((a, b) => b.y - a.y)[0];

    if (worthwhileDrop) {
      const closeToBallPath = !lowest || Math.abs(reflectX(worthwhileDrop.x, worthwhileDrop.r) - target) < W * .28;
      const secondsToPaddle = (paddle.y - worthwhileDrop.y) / worthwhileDrop.vy;
      if (closeToBallPath && secondsToPaddle < 2.1) target = reflectX(worthwhileDrop.x, worthwhileDrop.r);
    }
    return target;
  }

  function updateDrops(dt) {
    const pW = effectivePaddleWidth();
    for (let i = drops.length - 1; i >= 0; i--) {
      const d = drops[i];
      d.phase += dt * 3;
      d.x += d.vx * dt;
      d.y += d.vy * dt;
      if (d.x < d.r + 13 || d.x > W - d.r - 13) d.vx *= -1;

      if (d.y + d.r >= paddle.y &&
          d.y - d.r <= paddle.y + paddle.h &&
          d.x + d.r >= paddle.x - pW / 2 &&
          d.x - d.r <= paddle.x + pW / 2) {
        const caught = drops.splice(i, 1)[0];
        activatePower(caught.type);
        continue;
      }
      if (d.y > H + 22) drops.splice(i, 1);
    }
  }

  function update(dt) {
    if (state !== "playing") return;

    gameTime += dt;
    paddle.prevX = paddle.x;

    const pW = effectivePaddleWidth();
    if (controlMode === "ai") {
      const target = aiTarget();
      const maxMove = Math.max(500, W * .92) * dt;
      paddle.x += clamp(target - paddle.x, -maxMove, maxMove);
    } else if (controlMode === "keys") {
      const dir = (keys.has("ArrowRight") ? 1 : 0) - (keys.has("ArrowLeft") ? 1 : 0);
      paddle.x += dir * Math.max(450, W * .85) * dt;
    } else if (controlMode === "mouse") {
      paddle.x += clamp(mouseTarget - paddle.x, -W * 2.4 * dt, W * 2.4 * dt);
    }
    paddle.x = clamp(paddle.x, pW / 2 + 10, W - pW / 2 - 10);
    paddle.w = pW;

    updateDrops(dt);

    if (balls.length) {
      const survivors = [];
      for (const ball of balls) {
        updateBall(ball, dt);
        if (!ball.dead) survivors.push(ball);
      }
      const hadBalls = balls.length > 0;
      balls = survivors;

      if (hadBalls && balls.length === 0) {
        lives--;
        serveTimer = lives > 0 ? .82 : 0;
        updateHud();
        if (lives <= 0) finishGame("lost");
      }
    }

    if (state === "playing" && balls.length === 0 && lives > 0) {
      if (serveTimer > 0) {
        serveTimer -= dt;
        if (serveTimer <= 0) {
          const speed = clamp(W * .34, 310, 390);
          const radius = W < 500 ? 6.2 : 7.2;
          balls.push(makeBall(paddle.x, paddle.y - 24, speed * .17, -speed * .985, radius));
        }
      }
    }

    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life -= dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vx *= Math.pow(.985, dt * 60);
      p.vy *= Math.pow(.985, dt * 60);
      p.vy += 22 * dt;
      if (p.life <= 0) particles.splice(i, 1);
    }

    if (bricksLeft <= 0) finishGame("won");

    hudTimer += dt;
    if (hudTimer >= .16) {
      hudTimer = 0;
      updateHud();
    }
  }

  function drawBackground() {
    const bg = ctx.createLinearGradient(0, 0, 0, H);
    bg.addColorStop(0, "#0d1728");
    bg.addColorStop(.52, "#0a1422");
    bg.addColorStop(1, "#09111e");
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, W, H);

    const glow = ctx.createRadialGradient(W * .5, 15, 8, W * .5, 15, W * .72);
    glow.addColorStop(0, "rgba(68, 129, 176, .13)");
    glow.addColorStop(1, "rgba(34, 74, 113, 0)");
    ctx.fillStyle = glow;
    ctx.fillRect(0, 0, W, H * .72);

    ctx.lineWidth = 1;
    ctx.strokeStyle = "rgba(120, 161, 199, .035)";
    const grid = W < 500 ? 32 : 39;
    for (let x = 18; x < W; x += grid) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, H);
      ctx.stroke();
    }
    for (let y = 20; y < H; y += grid) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(W, y);
      ctx.stroke();
    }

    for (const star of stars) {
      const flicker = .66 + Math.sin(gameTime * 1.4 + star.phase) * .34;
      ctx.globalAlpha = star.alpha * flicker;
      ctx.fillStyle = "#b9e4ff";
      ctx.beginPath();
      ctx.arc(star.x, star.y, star.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.strokeStyle = "rgba(132, 177, 216, .13)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.roundRect(.5, .5, W - 1, H - 1, 12);
    ctx.stroke();

    ctx.fillStyle = "rgba(164, 199, 229, .34)";
    ctx.font = "800 8px system-ui, sans-serif";
    ctx.textAlign = "left";
    ctx.fillText("SECTOR 01", 19, 29);
    ctx.textAlign = "right";
    ctx.fillText("NEON CIRCUIT", W - 19, 29);
    ctx.textAlign = "left";

    ctx.strokeStyle = "rgba(95, 151, 188, .13)";
    ctx.setLineDash([2, 8]);
    ctx.beginPath();
    ctx.moveTo(18, H - 18);
    ctx.lineTo(W - 18, H - 18);
    ctx.stroke();
    ctx.setLineDash([]);
  }

  function drawPowerIcon(x, y, r, type, color) {
    ctx.save();
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.lineWidth = Math.max(1.2, r * .2);
    ctx.strokeStyle = color;
    ctx.fillStyle = color;

    if (type === "wide") {
      const half = r * .57;
      ctx.beginPath();
      ctx.moveTo(x - half, y);
      ctx.lineTo(x + half, y);
      ctx.moveTo(x - half, y);
      ctx.lineTo(x - half + r * .3, y - r * .29);
      ctx.moveTo(x - half, y);
      ctx.lineTo(x - half + r * .3, y + r * .29);
      ctx.moveTo(x + half, y);
      ctx.lineTo(x + half - r * .3, y - r * .29);
      ctx.moveTo(x + half, y);
      ctx.lineTo(x + half - r * .3, y + r * .29);
      ctx.stroke();
    } else {
      const dot = Math.max(1, r * .18);
      ctx.beginPath();
      ctx.arc(x - r * .34, y, dot, 0, Math.PI * 2);
      ctx.arc(x + r * .34, y, dot, 0, Math.PI * 2);
      ctx.arc(x, y - r * .28, dot, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }

  function drawBricks() {
    for (const b of bricks) {
      if (!b.alive) continue;
      const [bright, dark] = palettes[b.row];
      const radius = Math.min(6, b.h * .26);
      const isCracked = b.maxHp === 2 && b.hp === 1;
      const fill = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);

      if (isCracked) {
        fill.addColorStop(0, "#394252");
        fill.addColorStop(1, "#202b3b");
      } else {
        fill.addColorStop(0, bright);
        fill.addColorStop(1, dark);
      }

      ctx.save();
      ctx.shadowColor = isCracked ? "rgba(78, 104, 137, .11)" : bright;
      ctx.shadowBlur = isCracked ? 2 : 9;
      ctx.beginPath();
      ctx.roundRect(b.x, b.y, b.w, b.h, radius);
      ctx.fillStyle = fill;
      ctx.fill();
      ctx.restore();

      ctx.beginPath();
      ctx.roundRect(b.x + .5, b.y + .5, b.w - 1, b.h - 1, radius);
      ctx.strokeStyle = isCracked ? "rgba(177, 194, 215, .32)" : "rgba(235, 255, 255, .36)";
      ctx.lineWidth = 1;
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(b.x + radius, b.y + 2);
      ctx.lineTo(b.x + b.w - radius, b.y + 2);
      ctx.strokeStyle = isCracked ? "rgba(222, 234, 246, .08)" : "rgba(255,255,255,.24)";
      ctx.lineWidth = 1;
      ctx.stroke();

      if (b.maxHp === 2 && b.hp === 2) {
        ctx.fillStyle = "rgba(246, 251, 255, .8)";
        ctx.beginPath();
        ctx.arc(b.x + b.w - 7, b.y + 6, 1.35, 0, Math.PI * 2);
        ctx.arc(b.x + b.w - 12, b.y + 6, 1.35, 0, Math.PI * 2);
        ctx.fill();
      }

      if (isCracked) {
        ctx.save();
        ctx.lineCap = "round";
        ctx.lineJoin = "round";
        ctx.lineWidth = 1.35;
        ctx.strokeStyle = "rgba(235, 245, 255, .87)";
        ctx.shadowColor = "rgba(189, 222, 255, .4)";
        ctx.shadowBlur = 4;
        ctx.beginPath();
        ctx.moveTo(b.x + b.w * .47, b.y + 1);
        ctx.lineTo(b.x + b.w * .39, b.y + b.h * .35);
        ctx.lineTo(b.x + b.w * .54, b.y + b.h * .52);
        ctx.lineTo(b.x + b.w * .43, b.y + b.h - 1);
        ctx.moveTo(b.x + b.w * .54, b.y + b.h * .52);
        ctx.lineTo(b.x + b.w * .73, b.y + b.h * .37);
        ctx.stroke();
        ctx.restore();
      }

      if (b.power) {
        const color = b.power === "wide" ? "#e7fffb" : "#fff1ff";
        const badgeR = Math.min(7.3, b.w * .23);
        ctx.save();
        ctx.shadowColor = b.power === "wide" ? "#68fff0" : "#db9dff";
        ctx.shadowBlur = 9;
        ctx.beginPath();
        ctx.arc(b.x + b.w / 2, b.y + b.h / 2, badgeR, 0, Math.PI * 2);
        ctx.fillStyle = b.power === "wide" ? "rgba(19, 67, 73, .88)" : "rgba(64, 42, 92, .9)";
        ctx.fill();
        ctx.strokeStyle = b.power === "wide" ? "rgba(166,255,245,.8)" : "rgba(235,190,255,.8)";
        ctx.lineWidth = 1;
        ctx.stroke();
        drawPowerIcon(b.x + b.w / 2, b.y + b.h / 2, badgeR * .77, b.power, color);
        ctx.restore();
      }
    }
  }

  function drawDrops() {
    for (const d of drops) {
      const tint = d.type === "wide" ? "#62f0de" : "#d59bff";
      const pulse = 1 + Math.sin(d.phase) * .09;

      ctx.save();
      ctx.strokeStyle = tint;
      ctx.globalAlpha = .25;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(d.x, d.y - d.r * 2.2);
      ctx.lineTo(d.x, d.y - d.r * 1.15);
      ctx.stroke();
      ctx.globalAlpha = 1;
      ctx.shadowColor = tint;
      ctx.shadowBlur = 17;
      ctx.beginPath();
      ctx.arc(d.x, d.y, d.r * pulse, 0, Math.PI * 2);
      ctx.fillStyle = d.type === "wide" ? "rgba(23, 106, 107, .96)" : "rgba(94, 57, 130, .96)";
      ctx.fill();
      ctx.lineWidth = 1.3;
      ctx.strokeStyle = tint;
      ctx.stroke();
      drawPowerIcon(d.x, d.y, d.r * .76, d.type, "#f4ffff");
      ctx.restore();
    }
  }

  function drawParticles() {
    for (const p of particles) {
      const alpha = clamp(p.life / p.maxLife, 0, 1);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 7;
      ctx.beginPath();
      ctx.arc(p.x, p.y, Math.max(.4, p.size * alpha), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.shadowBlur = 0;
  }

  function drawPaddle() {
    const w = effectivePaddleWidth();
    const h = paddle.h;
    const x = paddle.x - w / 2;
    const y = paddle.y;
    const wide = wideUntil > gameTime;

    ctx.save();
    ctx.shadowColor = wide ? "#75f3e4" : "#58d8e8";
    ctx.shadowBlur = wide ? 24 : 17;
    const grad = ctx.createLinearGradient(x, y, x + w, y + h);
    grad.addColorStop(0, wide ? "#a3fff2" : "#72f5e6");
    grad.addColorStop(.48, "#63cdeb");
    grad.addColorStop(1, "#898aff");
    ctx.beginPath();
    ctx.roundRect(x, y, w, h, h / 2);
    ctx.fillStyle = grad;
    ctx.fill();
    ctx.restore();

    ctx.save();
    ctx.beginPath();
    ctx.roundRect(x + 3, y + 2, w - 6, Math.max(2, h * .24), 3);
    ctx.fillStyle = "rgba(255,255,255,.48)";
    ctx.fill();
    ctx.restore();

    for (const side of [-1, 1]) {
      ctx.fillStyle = "rgba(239,255,255,.9)";
      ctx.beginPath();
      ctx.arc(paddle.x + side * (w / 2 - 8), y + h / 2, 1.1, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawBall(ball) {
    for (let i = ball.trail.length - 1; i >= 0; i--) {
      const t = ball.trail[i];
      const alpha = .08 + (1 - i / ball.trail.length) * .16;
      ctx.globalAlpha = alpha;
      ctx.beginPath();
      ctx.arc(t.x, t.y, ball.r * (.48 + .48 * (1 - i / ball.trail.length)), 0, Math.PI * 2);
      ctx.fillStyle = "#73eaff";
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.save();
    ctx.shadowColor = "#80f5f1";
    ctx.shadowBlur = 19;
    const grad = ctx.createRadialGradient(
      ball.x - ball.r * .27, ball.y - ball.r * .35, ball.r * .08,
      ball.x, ball.y, ball.r
    );
    grad.addColorStop(0, "#ffffff");
    grad.addColorStop(.42, "#d7fffc");
    grad.addColorStop(1, "#6ad9f4");
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
    ctx.fillStyle = grad;
    ctx.fill();
    ctx.restore();
  }

  function drawServeBall() {
    if (balls.length || state !== "playing") return;
    const y = paddle.y - 24 + Math.sin(gameTime * 4) * 2;
    ctx.save();
    ctx.globalAlpha = .8;
    ctx.shadowColor = "#80f5f1";
    ctx.shadowBlur = 14;
    ctx.beginPath();
    ctx.arc(paddle.x, y, W < 500 ? 6 : 7, 0, Math.PI * 2);
    ctx.fillStyle = "#e6fffd";
    ctx.fill();
    ctx.restore();
  }

  function draw() {
    if (!W || !H) return;
    drawBackground();
    drawBricks();
    drawDrops();
    drawParticles();
    drawServeBall();
    for (const ball of balls) drawBall(ball);
    drawPaddle();
  }

  canvas.addEventListener("pointermove", event => {
    if (controlMode !== "mouse" || state !== "playing") return;
    const rect = canvas.getBoundingClientRect();
    mouseTarget = event.clientX - rect.left;
  });

  canvas.addEventListener("click", event => {
    if (state !== "playing") return;
    const rect = canvas.getBoundingClientRect();
    takeControl("mouse", event.clientX - rect.left);
    canvas.focus({ preventScroll: true });
  });

  window.addEventListener("keydown", event => {
    if (state === "lost" || state === "won") {
      if (event.key === "Enter" || event.key.toLowerCase() === "r") {
        event.preventDefault();
        resetGame();
      }
      return;
    }

    if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
      event.preventDefault();
      keys.add(event.key);
      takeControl("keys");
      return;
    }

    if (!event.repeat && state === "playing") {
      takeControl("keys");
      if (event.code === "Space") event.preventDefault();
    }
  });

  window.addEventListener("keyup", event => {
    if (event.key === "ArrowLeft" || event.key === "ArrowRight") keys.delete(event.key);
  });

  restartButton.addEventListener("click", event => {
    event.stopPropagation();
    resetGame();
    canvas.focus({ preventScroll: true });
  });

  window.addEventListener("resize", () => {
    fitCanvas();
  });

  fitCanvas();
  resetGame();
  initialized = true;

  let lastTime = performance.now();
  function frame(now) {
    const dt = Math.min(.035, Math.max(0, (now - lastTime) / 1000));
    lastTime = now;
    update(dt);
    draw();
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (16,056 tokens)
**Mapping canvas dimensions**

I’m working on a visual composition with a centered cabinet card against a dark background, possibly for a gaming arcade. I need to determine the canvas dimensions based on the viewport—maybe 1280x800 with a header. I want to ensure the inner canvas area scales correctly. For mobile, I’ll adapt dimensions like a 360x550 canvas aspect ratio. Using CSS, I plan to set the canvas width to 100% and explore how to manage responsive design with JavaScript.**Determining game frame dimensions**

I’m planning the layout for my game interface with a minimum width of 1050px centered using auto margins. The header will be included, and for the game frame, I want to set the height to adjust dynamically. I’m considering using a clamp function with a minimum of 500px, a maximum of 650px, and calculating based on the viewport height minus 174px. This gives me a flexible design that should work well across different screen sizes!**Designing a game board**

I’m thinking about designing a game board layout that considers both mobile and desktop screens. For mobile, I’m envisioning a size around 350x590 with 6 rows and possibly 9 columns. Each cell would be 36 units wide. For desktops, I want a size of 960x580 with 10 to 12 columns. I need to ensure the board's coordinates are dynamic, with the ball starting at the center-bottom and paddles tracking its movement efficiently. The game should be user-friendly and avoid a sudden game over!**Implementing game physics**

I'm working on the game physics for user and demo modes. In user mode, the paddle is controlled with mouse or keyboard. The demo mode will have an AI that tracks trajectory and projected landing. I need to consider reflections to aim effectively when the ball is descending. 

Collision detection involves checking circle-rectangle intersections, ensuring the ball doesn’t pass through bricks or the paddle. I'm thinking about using a fixed timestep of 1/120 and a maximum speed of around 460 px/s, plus implementing substeps for accurate collision resolution.**Refining collision mechanics**

I'm focusing on the collision mechanics between the ball and the rectangles. If there's an overlap, I'll determine the normal vector and adjust the position to the nearest separation. I want to ensure that collision happens only once with a cooldown to avoid repeated hits. During each substep, overlaps will be corrected, and the direction will be reflected accordingly.

For tougher bricks, the first hit will display cracks, and once a brick’s health reaches zero, it won’t register any more collisions. I'll also handle multiple balls and check for collisions with the paddle when the ball is moving downward.**Implementing timer and power-ups**

I need to implement a timer for a special type of brick set that includes power-ups like 'wide' and 'multi'. I plan to draw a compact icon in a glowing circle, which will be positioned either in the upper corner or the center. Normal bricks can carry these icons, and upon destruction, they should drop the power-up 100% of the time, falling at around 95px/s for the user to catch. For the multi ball power-up, I'll create two additional balls at different angles, ensuring all are active.**Exploring collision detection methods**

I’m considering how to approach a collision detection problem. It seems like using previous positions could simplify things. Maybe employing a swept method with circles and finding the closest point could work? I'm thinking about using a normal vector to determine penetration, where the penetration could be calculated as the radius minus distance. Shifting along the normal is another idea. There’s a lot of detail to work through, but it feels manageable!**Adjusting paddle mechanics**

I'm working on smoothing the paddle's movement by adjusting its position with a formula. It looks like I might want to cap the speed at 700, especially for mobile. When selecting which balls to track, I should focus on the lowest descending ones. If there's none moving down, I could use either the current ball's position or make a target guess. Important to note that the AI in demo mode should not be active while the user is playing!**Optimizing game mechanics**

I'm considering how to manage direct click position updates for mouse interactions. I need to implement power-up effects, including a power timer lasting 10 seconds and specific multi-ball mechanics that allow for exactly three balls. When one ball is caught, I'll add two more until there are three total. If there are already three, I'll ensure no additional balls are added. I want to clone objects carefully and handle drawing power-ups and HUD digits while maintaining canvas dimensions for retina displays.**Clarifying game controls**

I want to clarify the phrase "to take control," but the message should be exact: something like "Click or press a key to start." I need to ensure that control over the mouse and canvas is smooth, including tracking x-coordinates when the user clicks. It’s important to add listeners for key events like arrow keys and the Enter key for restarting the game. I also need to make sure the canvas area is properly set up with a tabindex for focus.**Designing the canvas**

I’m considering using `ctx.roundRect` for drawing, and checking if a path fallback is necessary—Chrome supports it, so that's good! I want to create a procedural starfield by generating static positions with deterministic pseudo-random numbers rather than randomizing every frame. I'll also add subtle vertical and horizontal grid lines, maybe with side rails as borders. The canvas background should have a dark gradient with a glow at the top, and I plan to ensure the grid has lines every 36 pixels.**Designing HUD and Bricks**

I'm working on the HUD for "LEVEL 01" which will show the count of remaining bricks and maybe include a progress bar. I think it could display the power chip status too, with "WIDE 0:10". The title would be "PIXEL LAB / ARCADE 01" with a main heading "BREAKOUT" featuring a neon dot and the description "NEON CIRCUIT". I want to give a polished look with a mint and purple/coral color scheme. For the bricks, I’ll use a colorful gradient and indicate tougher bricks with unique markings.**Reflecting on Power Icon Design**

I'm thinking about placing the power icon at the top right, centered near the upper area. I envision it as an overlay on a brick, which should usually work without any issues. I want to show custom W arrows by drawing a line from one side of the icon to the other, with arrows at both endpoints. I could add small dots around for extra detail. Making sure that the power icon badges are distinct will also be important.**Looking into game mechanics**

I'm working through a few details regarding the game mechanics. There's an issue with the screen overlay and how it handles click events. I'm looking at the canvas size and using window pointer coordinates for mouse movement within the canvas. The power-up timer is connected to gameClock and updates when activated. I also need to ensure paddle width adjustment and timer display functions correctly. In addition, I want to add visual status updates, lives, and score count adjustments.**Planning layout scaling**

I'm thinking about how to implement layout scaling in my project. I need to adjust the old width and height to the new values, scaling x and y accordingly. The brick width and height will multiply by the scaling factors, and I have to ensure the paddle and margins scale as well. It seems like the brick grid might compress during this process. I should also handle the resize event before the initial frame, calling resize on the canvas. After resizing, I'll define setupGame at the script's start.**Clarifying mouse control requirements**

I’m trying to understand the requirements for controlling the game. It suggests using a click or key press, but can I start just with the mouse movement? If the mouse crosses the canvas, should that trigger control? It seems the game should only begin moving on an actual click or key press. I think I'll need to set coordinates after the initial click, then allow pointer movement to work for control after that.**Thinking through collision mechanics**

I'm focusing on ensuring the ball doesn’t pass through the brick if it's moving too quickly. I’m calculating maximum movement and setting limits for dt based on the scheduler. I'm checking for collisions between the ball and paddle, especially considering paddle angles. If a collision occurs from the top, it has specific conditions. 

Also, I need to manage potential collisions appropriately, especially for horizontal and vertical resolutions. These details will help create a smoother gameplay experience.**Clarifying collision detection logic**

I’m working on the function for collision detection, specifically calculating distances from the ball to the rectangle. If the distance squared is zero, I'll be checking distances on each side—left, right, top, and bottom—to find the minimum distance and determine the corresponding normal. 

If the distance is less than the radius and the dot product is negative, it suggests a reflection. I also need to manage the ball's position if it intersects with the rectangle's boundaries.**Evaluating ball dynamics**

I’m thinking about the parameters: 900 for desktop and 600 for mobile—good numbers. I’m considering the ball radius and speed calculations, with some values potentially being affected by weight. I’ll use formulas to determine speeds based on input like `speed=Math.min(395,Math.max(310,W*.34))`. It seems important to track the ball and predict its trajectory for paddle movement, especially when dealing with multiple balls. I need to refine these calculations accordingly to ensure proper gameplay mechanics.**Updating status display**

I’m working on updating the status in the HUD with `powerDesc`. The display should be compact, and I want to use a blue particle for power collection. I’m also considering displaying the initial prompt while the paddle and ball are visibly moving. There's a need for a canvas overlay on top, and I'll adjust the CSS color as necessary. Making all these elements work together is so important for clarity!**Refining canvas layout**

I'm figuring out the desktop canvas dimensions: it's 1100x580 with bricks about 95 wide. I can fit 10 columns and the grid width is around 88%, which translates to a board width of 1010 at W1190. For mobile, the board is 351 width, yielding a cell size of 27, and everything looks okay there too.

The ball area needs a wrap, with instructions visible and a footer for controls. Control layouts are in flex. On mobile, I might hide some text at 375 width, adjusting content and group sizes accordingly, while ensuring the app has fixed text and no overflow.**Refining particle behavior**

I'm focusing on how to handle particle updates with the serve timer. I think particles should spawn only when they hit something. It might make sense to keep active particles updated, even when the state has ended, but only if the game is still playing—there's no need for them to update otherwise.

I also need to decide how to structure the `particles` update. If the scene ends, I could consider freezing them. I see the `updateBall` function relies on `paddle.width` for ball collisions, which is important to keep in mind.**Analyzing game mechanics**

I'm focused on setting up the ball angle and aiming the paddle to follow the mouse pointer. When the size changes, I need to adjust the pointer event coordinates accordingly. I'll ensure the paddle can move anywhere on the x-axis while clamping it and linking it to mouse events. When a click happens, I'll check if the game state is playing; if not, possibly restart. I'll also consider how end-screen clicks behave and make sure to handle events appropriately. Additionally, I'll generate stars based on the current dimensions.**Rendering game graphics**

I'm setting up the drawing and rendering process. First, I'll clear the canvas and draw the background. Then, I’ll add the bricks, drops, and particles to enhance the scene. I'll draw the paddle and balls with a trail effect, and maybe introduce a score overlay to keep the user informed. If no balls are active, I'll draw a serving ring. I think a decorative dashed line could work for the floor, along with a radial gradient for a faint glow and twinkling stars for ambiance. I'll also consider a soft border to frame the game.