Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
3 min 7 s
Cost
$0.022
Output tokens
37,193
Spent thinking
23,650 tokens
Speed
199.7 tok/s
Console errors
0
First token after
742 ms
First visible text after
2 min 27 s
Prompt tokens
664
Code size
46.1 KB
Finish reason
stop
Served by
DigitalOcean
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"max"}
Max output
64,000 tokens
Generated
Nov 11, 2026

What it was asked

The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
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 · 1680 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="theme-color" content="#081214">
  <title>Snake — Refined Arcade</title>
  <style>
    :root {
      color-scheme: dark;
      --bg: #071013;
      --panel: rgba(15, 28, 30, 0.86);
      --panel-light: rgba(255, 255, 255, 0.055);
      --line: rgba(203, 255, 223, 0.12);
      --text: #eff8ef;
      --muted: #8da6a3;
      --lime: #c9f66d;
      --mint: #65dba5;
      --coral: #ff8066;
    }

    * {
      box-sizing: border-box;
    }

    [hidden] {
      display: none !important;
    }

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

    body {
      margin: 0;
      min-height: 100vh;
      min-height: 100dvh;
      overflow-x: hidden;
      display: grid;
      place-items: center;
      padding: 14px;
      color: var(--text);
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background:
        radial-gradient(circle at 18% 12%, rgba(73, 193, 148, 0.14), transparent 35%),
        radial-gradient(circle at 84% 82%, rgba(201, 246, 109, 0.09), transparent 31%),
        linear-gradient(145deg, #091518 0%, #050c0f 100%);
    }

    body::before {
      content: "";
      position: fixed;
      inset: 0;
      pointer-events: none;
      opacity: 0.32;
      background-image:
        linear-gradient(rgba(170, 230, 205, 0.035) 1px, transparent 1px),
        linear-gradient(90deg, rgba(170, 230, 205, 0.035) 1px, transparent 1px);
      background-size: 32px 32px;
      -webkit-mask-image: radial-gradient(ellipse at center, black, transparent 72%);
      mask-image: radial-gradient(ellipse at center, black, transparent 72%);
    }

    .app {
      position: relative;
      z-index: 1;
      width: min(94vw, 620px);
      width: min(94vw, 620px, calc(100dvh - 146px));
    }

    .topbar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      min-height: 44px;
      margin-bottom: 10px;
    }

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

    .logo-mark {
      width: 42px;
      height: 42px;
      flex: 0 0 auto;
      display: grid;
      place-items: center;
      border: 1px solid rgba(201, 246, 109, 0.22);
      border-radius: 14px;
      background:
        linear-gradient(145deg, rgba(201, 246, 109, 0.17), rgba(101, 219, 165, 0.07)),
        rgba(255, 255, 255, 0.035);
      box-shadow: inset 0 1px rgba(255, 255, 255, 0.08), 0 8px 24px rgba(0, 0, 0, 0.18);
    }

    .logo-mark svg {
      width: 32px;
      height: 32px;
    }

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

    .brand h1 {
      margin: 0;
      font-size: 27px;
      line-height: 1;
      letter-spacing: -1.2px;
      font-weight: 850;
    }

    .brand h1 span {
      color: var(--lime);
    }

    .brand p {
      margin: 5px 0 0;
      color: var(--muted);
      font-size: 10px;
      font-weight: 650;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    .stats {
      display: flex;
      gap: 7px;
      flex: 0 0 auto;
    }

    .stat {
      min-width: 68px;
      padding: 7px 11px 8px;
      border: 1px solid var(--line);
      border-radius: 13px;
      background: rgba(255, 255, 255, 0.04);
      box-shadow: inset 0 1px rgba(255, 255, 255, 0.045);
    }

    .stat-label {
      display: flex;
      align-items: center;
      gap: 4px;
      color: var(--muted);
      font-size: 8px;
      line-height: 1;
      font-weight: 800;
      letter-spacing: 0.14em;
    }

    .stat.best .stat-label::before {
      content: "★";
      color: var(--lime);
      font-size: 10px;
      line-height: 8px;
    }

    .stat-value {
      display: block;
      margin-top: 5px;
      font-size: 20px;
      line-height: 1;
      font-weight: 800;
      letter-spacing: -0.6px;
      font-variant-numeric: tabular-nums;
    }

    .stat-value.bump {
      animation: score-bump 260ms cubic-bezier(0.2, 1.5, 0.4, 1);
      color: var(--lime);
    }

    .game-card {
      position: relative;
      padding: 10px;
      border: 1px solid rgba(206, 255, 228, 0.13);
      border-radius: 24px;
      background:
        linear-gradient(160deg, rgba(24, 41, 42, 0.88), rgba(9, 19, 22, 0.92));
      box-shadow:
        0 30px 80px rgba(0, 0, 0, 0.42),
        0 4px 14px rgba(0, 0, 0, 0.28),
        inset 0 1px rgba(255, 255, 255, 0.065);
    }

    .toolbar {
      height: 28px;
      margin-bottom: 7px;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .mode-badge,
    .length-chip {
      height: 25px;
      display: inline-flex;
      align-items: center;
      gap: 7px;
      padding: 0 9px;
      border: 1px solid rgba(255, 255, 255, 0.07);
      border-radius: 999px;
      color: #b8cbc6;
      background: rgba(255, 255, 255, 0.035);
      font-size: 9px;
      line-height: 1;
      font-weight: 800;
      letter-spacing: 0.09em;
      text-transform: uppercase;
    }

    .status-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--mint);
      box-shadow: 0 0 0 4px rgba(101, 219, 165, 0.1);
    }

    .mode-badge[data-state="demo"] .status-dot {
      background: var(--coral);
      box-shadow: 0 0 0 4px rgba(255, 128, 102, 0.1);
      animation: dot-pulse 1.5s ease-in-out infinite;
    }

    .mode-badge[data-state="paused"] .status-dot {
      background: #ffd279;
      box-shadow: 0 0 0 4px rgba(255, 210, 121, 0.1);
    }

    .mode-badge[data-state="over"] .status-dot {
      background: #ff6177;
      box-shadow: 0 0 0 4px rgba(255, 97, 119, 0.1);
    }

    .length-chip {
      margin-left: auto;
      color: var(--muted);
      letter-spacing: 0.04em;
    }

    .length-chip strong {
      color: var(--text);
      font-size: 10px;
      font-variant-numeric: tabular-nums;
    }

    .pause-button {
      height: 27px;
      display: inline-flex;
      align-items: center;
      gap: 7px;
      padding: 0 8px 0 10px;
      border: 1px solid rgba(255, 255, 255, 0.09);
      border-radius: 9px;
      color: #d5e2de;
      background: rgba(255, 255, 255, 0.045);
      cursor: pointer;
      font: inherit;
      font-size: 10px;
      font-weight: 700;
      transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
    }

    .pause-button:hover {
      background: rgba(255, 255, 255, 0.085);
      border-color: rgba(201, 246, 109, 0.25);
    }

    .pause-button:active {
      transform: translateY(1px);
    }

    kbd {
      min-width: 19px;
      height: 19px;
      padding: 0 5px;
      display: inline-grid;
      place-items: center;
      border: 1px solid rgba(255, 255, 255, 0.13);
      border-bottom-color: rgba(255, 255, 255, 0.22);
      border-radius: 5px;
      color: #9fb2ae;
      background: rgba(0, 0, 0, 0.2);
      box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04);
      font-family: inherit;
      font-size: 8px;
      font-weight: 800;
      line-height: 1;
    }

    .canvas-wrap {
      position: relative;
      width: 100%;
      aspect-ratio: 1 / 1;
      overflow: hidden;
      border: 1px solid rgba(185, 242, 214, 0.1);
      border-radius: 16px;
      background: #081316;
      box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.35),
        inset 0 18px 38px rgba(0, 0, 0, 0.22);
    }

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

    .demo-hint {
      position: absolute;
      z-index: 3;
      left: 50%;
      bottom: 14px;
      transform: translateX(-50%);
      width: max-content;
      max-width: calc(100% - 24px);
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 9px 13px;
      border: 1px solid rgba(218, 255, 231, 0.18);
      border-radius: 13px;
      color: #edf9ef;
      background: rgba(8, 21, 23, 0.8);
      box-shadow: 0 12px 30px rgba(0, 0, 0, 0.28);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      pointer-events: none;
      animation: hint-float 2.4s ease-in-out infinite;
    }

    .demo-hint .hint-icon {
      width: 28px;
      height: 28px;
      flex: 0 0 auto;
      display: grid;
      place-items: center;
      border-radius: 9px;
      color: #102017;
      background: var(--lime);
      font-size: 14px;
      font-weight: 900;
      box-shadow: 0 0 20px rgba(201, 246, 109, 0.2);
    }

    .demo-hint strong {
      display: block;
      font-size: 11px;
      line-height: 1.2;
      font-weight: 800;
      white-space: nowrap;
    }

    .demo-hint small {
      display: block;
      margin-top: 3px;
      color: #9cb3ad;
      font-size: 9px;
      line-height: 1.2;
      white-space: nowrap;
    }

    .overlay {
      position: absolute;
      z-index: 6;
      inset: 0;
      display: grid;
      place-items: center;
      padding: 18px;
      background: rgba(3, 10, 12, 0.64);
      backdrop-filter: blur(5px);
      -webkit-backdrop-filter: blur(5px);
      animation: overlay-in 180ms ease-out;
    }

    .modal {
      width: min(100%, 330px);
      padding: 24px 23px 21px;
      border: 1px solid rgba(218, 255, 231, 0.16);
      border-radius: 22px;
      text-align: center;
      background:
        linear-gradient(160deg, rgba(27, 47, 47, 0.98), rgba(10, 22, 25, 0.98));
      box-shadow: 0 26px 70px rgba(0, 0, 0, 0.48), inset 0 1px rgba(255, 255, 255, 0.07);
      animation: modal-in 260ms cubic-bezier(0.2, 0.9, 0.25, 1.12);
    }

    .overlay-symbol {
      width: 44px;
      height: 44px;
      margin: 0 auto 13px;
      display: grid;
      place-items: center;
      border-radius: 14px;
      color: #101d19;
      background: linear-gradient(145deg, #dcff8c, #a8e969);
      box-shadow: 0 8px 26px rgba(201, 246, 109, 0.19);
      font-size: 23px;
      font-weight: 900;
      line-height: 1;
    }

    .overlay-eyebrow {
      margin: 0 0 5px;
      color: var(--lime);
      font-size: 9px;
      font-weight: 850;
      letter-spacing: 0.18em;
      text-transform: uppercase;
    }

    .modal h2 {
      margin: 0;
      font-size: 28px;
      line-height: 1.08;
      letter-spacing: -1px;
    }

    .modal-copy {
      margin: 10px auto 17px;
      max-width: 250px;
      color: #9db2ad;
      font-size: 12px;
      line-height: 1.55;
    }

    .result-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
      margin-bottom: 15px;
    }

    .result-grid > div {
      padding: 10px;
      border: 1px solid rgba(255, 255, 255, 0.075);
      border-radius: 12px;
      background: rgba(255, 255, 255, 0.035);
    }

    .result-grid span {
      display: block;
      color: var(--muted);
      font-size: 8px;
      font-weight: 800;
      letter-spacing: 0.13em;
      text-transform: uppercase;
    }

    .result-grid strong {
      display: block;
      margin-top: 4px;
      font-size: 23px;
      font-variant-numeric: tabular-nums;
      letter-spacing: -0.5px;
    }

    .primary-button {
      width: 100%;
      height: 42px;
      border: 0;
      border-radius: 12px;
      color: #112016;
      background: linear-gradient(135deg, #d8ff83, #b6ed65);
      box-shadow: 0 9px 24px rgba(159, 211, 86, 0.16);
      cursor: pointer;
      font: inherit;
      font-size: 12px;
      font-weight: 850;
      transition: transform 160ms ease, filter 160ms ease;
    }

    .primary-button:hover {
      filter: brightness(1.06);
      transform: translateY(-1px);
    }

    .primary-button:active {
      transform: translateY(1px);
    }

    .overlay-shortcut {
      display: block;
      margin-top: 10px;
      color: #728a85;
      font-size: 9px;
    }

    .controls {
      min-height: 18px;
      margin-top: 10px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 9px;
      color: #7f9691;
      font-size: 10px;
      font-weight: 600;
    }

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

    .control .keyset {
      color: #b5c7c2;
      font-weight: 750;
      letter-spacing: 0.09em;
    }

    .control .divider {
      color: #526863;
    }

    @keyframes score-bump {
      0% { transform: scale(1); }
      45% { transform: scale(1.25); }
      100% { transform: scale(1); }
    }

    @keyframes dot-pulse {
      0%, 100% { opacity: 0.65; transform: scale(0.9); }
      50% { opacity: 1; transform: scale(1.15); }
    }

    @keyframes hint-float {
      0%, 100% { transform: translate(-50%, 0); }
      50% { transform: translate(-50%, -4px); }
    }

    @keyframes overlay-in {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    @keyframes modal-in {
      from { opacity: 0; transform: translateY(12px) scale(0.96); }
      to { opacity: 1; transform: translateY(0) scale(1); }
    }

    @media (max-width: 480px) {
      body {
        padding: 8px;
      }

      .app {
        width: min(94vw, 620px);
      }

      .topbar {
        min-height: 39px;
        margin-bottom: 8px;
      }

      .logo-mark {
        width: 36px;
        height: 36px;
        border-radius: 12px;
      }

      .logo-mark svg {
        width: 28px;
        height: 28px;
      }

      .brand {
        gap: 8px;
      }

      .brand h1 {
        font-size: 23px;
      }

      .brand p {
        display: none;
      }

      .stat {
        min-width: 59px;
        padding: 6px 9px 7px;
        border-radius: 11px;
      }

      .stat-value {
        font-size: 17px;
      }

      .game-card {
        padding: 7px;
        border-radius: 19px;
      }

      .toolbar {
        height: 25px;
        margin-bottom: 6px;
      }

      .mode-badge,
      .length-chip {
        height: 23px;
        padding: 0 8px;
        font-size: 8px;
      }

      .pause-button {
        height: 24px;
        padding-left: 8px;
        font-size: 9px;
      }

      .pause-button kbd {
        display: none;
      }

      .canvas-wrap {
        border-radius: 13px;
      }

      .demo-hint {
        bottom: 8px;
        gap: 8px;
        padding: 7px 10px;
        border-radius: 11px;
      }

      .demo-hint .hint-icon {
        width: 24px;
        height: 24px;
        border-radius: 8px;
        font-size: 12px;
      }

      .demo-hint strong {
        font-size: 9px;
      }

      .demo-hint small {
        font-size: 7px;
      }

      .controls {
        margin-top: 8px;
        font-size: 8px;
        gap: 5px;
      }

      .control {
        gap: 4px;
      }

      .control .optional {
        display: none;
      }

      .modal {
        padding: 19px 18px 17px;
        border-radius: 18px;
      }

      .modal h2 {
        font-size: 24px;
      }

      .modal-copy {
        margin: 8px auto 13px;
        font-size: 11px;
      }
    }

    @media (max-height: 600px) {
      .topbar {
        min-height: 34px;
        margin-bottom: 6px;
      }

      .logo-mark {
        width: 34px;
        height: 34px;
      }

      .brand p {
        display: none;
      }

      .controls {
        margin-top: 6px;
      }
    }
  </style>
</head>
<body>
  <main class="app">
    <header class="topbar">
      <div class="brand">
        <div class="logo-mark" aria-hidden="true">
          <svg viewBox="0 0 48 48" fill="none">
            <path d="M12 31c0 6 4.8 10 10.8 10H31c4.3 0 7.8-3.5 7.8-7.8S35.3 25.4 31 25.4H21c-5 0-9-4-9-9s4-9 9-9h8"
              stroke="#C9F66D" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
            <circle cx="31.5" cy="10.4" r="1.8" fill="#102017"/>
          </svg>
        </div>
        <div class="brand-copy">
          <h1>SNAKE<span>.</span></h1>
          <p>Classic, refined</p>
        </div>
      </div>

      <div class="stats" aria-label="Game scores">
        <div class="stat">
          <span class="stat-label">SCORE</span>
          <strong id="scoreValue" class="stat-value" role="status" aria-live="polite">000</strong>
        </div>
        <div class="stat best">
          <span class="stat-label">BEST</span>
          <strong id="bestValue" class="stat-value">000</strong>
        </div>
      </div>
    </header>

    <section class="game-card" aria-label="Snake game">
      <div class="toolbar">
        <div id="modeBadge" class="mode-badge" data-state="demo">
          <span class="status-dot" aria-hidden="true"></span>
          <span id="modeText">AI DEMO</span>
        </div>
        <div class="length-chip">Length <strong id="lengthValue">4</strong></div>
        <button id="pauseButton" class="pause-button" type="button" aria-label="Take over from the AI">
          <span id="pauseButtonText">Take over</span>
          <kbd>P</kbd>
        </button>
      </div>

      <div class="canvas-wrap">
        <canvas id="gameCanvas" width="600" height="600" tabindex="0" role="img"
          aria-label="A 24 by 24 Snake game board"></canvas>

        <div id="demoHint" class="demo-hint">
          <span class="hint-icon" aria-hidden="true">↗</span>
          <span>
            <strong>Click or press a key to start</strong>
            <small>Watch the AI, then take the wheel</small>
          </span>
        </div>

        <div id="overlay" class="overlay" hidden>
          <div class="modal" role="dialog" aria-modal="true" aria-labelledby="overlayTitle">
            <div id="overlaySymbol" class="overlay-symbol" aria-hidden="true">×</div>
            <p id="overlayEyebrow" class="overlay-eyebrow">Run complete</p>
            <h2 id="overlayTitle">Game over</h2>
            <p id="overlayText" class="modal-copy">The wall caught up. Your next run starts fresh.</p>
            <div class="result-grid">
              <div>
                <span>Score</span>
                <strong id="overlayScore">000</strong>
              </div>
              <div>
                <span>Best</span>
                <strong id="overlayBest">000</strong>
              </div>
            </div>
            <button id="overlayButton" class="primary-button" type="button">Play again</button>
            <span id="overlayShortcut" class="overlay-shortcut">Press Space or Enter to restart</span>
          </div>
        </div>
      </div>
    </section>

    <footer class="controls" aria-label="Controls">
      <div class="control">
        <span class="keyset">↑ ↓ ← →</span>
        <span class="divider">/</span>
        <span class="optional">WASD</span>
        <span>Move</span>
      </div>
      <div class="control"><kbd>P</kbd><span>Pause</span></div>
      <div class="control"><kbd>␣</kbd><span class="optional">Restart</span></div>
    </footer>
  </main>

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

      const COLS = 24;
      const ROWS = 24;
      const canvas = document.getElementById("gameCanvas");
      const ctx = canvas.getContext("2d");

      const ui = {
        score: document.getElementById("scoreValue"),
        best: document.getElementById("bestValue"),
        length: document.getElementById("lengthValue"),
        modeBadge: document.getElementById("modeBadge"),
        modeText: document.getElementById("modeText"),
        pauseButton: document.getElementById("pauseButton"),
        pauseButtonText: document.getElementById("pauseButtonText"),
        demoHint: document.getElementById("demoHint"),
        overlay: document.getElementById("overlay"),
        overlaySymbol: document.getElementById("overlaySymbol"),
        overlayEyebrow: document.getElementById("overlayEyebrow"),
        overlayTitle: document.getElementById("overlayTitle"),
        overlayText: document.getElementById("overlayText"),
        overlayScore: document.getElementById("overlayScore"),
        overlayBest: document.getElementById("overlayBest"),
        overlayButton: document.getElementById("overlayButton"),
        overlayShortcut: document.getElementById("overlayShortcut")
      };

      const DIRECTIONS = [
        { x: 1, y: 0 },
        { x: 0, y: 1 },
        { x: -1, y: 0 },
        { x: 0, y: -1 }
      ];

      const KEY_DIRECTIONS = {
        arrowright: { x: 1, y: 0 },
        arrowdown: { x: 0, y: 1 },
        arrowleft: { x: -1, y: 0 },
        arrowup: { x: 0, y: -1 },
        d: { x: 1, y: 0 },
        s: { x: 0, y: 1 },
        a: { x: -1, y: 0 },
        w: { x: 0, y: -1 }
      };

      let size = 600;
      let cell = size / COLS;
      let dpr = Math.min(window.devicePixelRatio || 1, 2);

      let snake = [];
      let previousSnake = [];
      let direction = { x: 1, y: 0 };
      let inputQueue = [];
      let food = null;
      let score = 0;
      let best = 0;
      let state = "demo";
      let tickInterval = 140;
      let accumulator = 0;
      let lastTime = performance.now();
      let particles = [];
      let floaters = [];

      const dust = Array.from({ length: 34 }, () => ({
        x: Math.random(),
        y: Math.random(),
        r: 0.35 + Math.random() * 0.9,
        a: 0.035 + Math.random() * 0.055,
        phase: Math.random() * Math.PI * 2
      }));

      function cellKey(x, y) {
        return x + "," + y;
      }

      function sameDirection(a, b) {
        return a.x === b.x && a.y === b.y;
      }

      function oppositeDirection(a, b) {
        return a.x === -b.x && a.y === -b.y;
      }

      function padScore(value) {
        return String(value).padStart(3, "0");
      }

      function resizeCanvas() {
        const rect = canvas.getBoundingClientRect();
        const nextSize = Math.max(120, Math.round(rect.width));
        size = nextSize;
        cell = size / COLS;
        dpr = Math.min(window.devicePixelRatio || 1, 2);
        canvas.width = Math.round(size * dpr);
        canvas.height = Math.round(size * dpr);
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.imageSmoothingEnabled = true;
      }

      function spawnFood() {
        const occupied = new Set(snake.map(segment => cellKey(segment.x, segment.y)));
        const free = [];

        for (let y = 0; y < ROWS; y++) {
          for (let x = 0; x < COLS; x++) {
            if (!occupied.has(cellKey(x, y))) free.push({ x, y });
          }
        }

        if (!free.length) return null;

        const head = snake[0];
        const preferred = free.filter(point =>
          Math.abs(point.x - head.x) + Math.abs(point.y - head.y) >= 3
        );
        const pool = preferred.length ? preferred : free;
        return pool[Math.floor(Math.random() * pool.length)];
      }

      function resetGame(mode = "demo", initialDirection = { x: 1, y: 0 }) {
        const centerX = Math.floor(COLS / 2);
        const centerY = Math.floor(ROWS / 2);

        snake = [];
        for (let i = 0; i < 4; i++) {
          snake.push({
            x: centerX - i * initialDirection.x,
            y: centerY - i * initialDirection.y
          });
        }

        direction = { x: initialDirection.x, y: initialDirection.y };
        previousSnake = snake.map(segment => ({ ...segment }));
        inputQueue = [];
        particles = [];
        floaters = [];
        score = 0;
        food = spawnFood();
        state = mode;
        tickInterval = 140;
        accumulator = 0;
        lastTime = performance.now();

        ui.overlay.hidden = true;
        ui.demoHint.hidden = mode !== "demo";

        updateStats(false);
        updateUI();
      }

      function startManual(initialDirection = { x: 1, y: 0 }) {
        resetGame("playing", initialDirection);
      }

      function updateStats(animateScore) {
        ui.score.textContent = padScore(score);
        ui.best.textContent = padScore(best);

        if (animateScore) {
          ui.score.classList.remove("bump");
          void ui.score.offsetWidth;
          ui.score.classList.add("bump");
        }
      }

      function updateUI() {
        const labels = {
          demo: "AI DEMO",
          playing: "PLAYING",
          paused: "PAUSED",
          over: "GAME OVER"
        };

        ui.modeText.textContent = labels[state];
        ui.modeBadge.dataset.state = state;
        ui.length.textContent = snake.length;

        if (state === "demo") {
          ui.pauseButtonText.textContent = "Take over";
          ui.pauseButton.setAttribute("aria-label", "Take over from the AI");
        } else if (state === "paused") {
          ui.pauseButtonText.textContent = "Resume";
          ui.pauseButton.setAttribute("aria-label", "Resume the game");
        } else if (state === "over") {
          ui.pauseButtonText.textContent = "Restart";
          ui.pauseButton.setAttribute("aria-label", "Restart the game");
        } else {
          ui.pauseButtonText.textContent = "Pause";
          ui.pauseButton.setAttribute("aria-label", "Pause the game");
        }
      }

      function queueDirection(nextDirection) {
        const reference = inputQueue.length
          ? inputQueue[inputQueue.length - 1]
          : direction;

        // Compare against the final queued turn, not only the current direction.
        // This preserves rapid sequences such as Right -> Up -> Left.
        if (oppositeDirection(nextDirection, reference)) return false;
        if (sameDirection(nextDirection, reference)) return true;
        if (inputQueue.length < 6) inputQueue.push({ ...nextDirection });
        return true;
      }

      function takeQueuedDirection() {
        if (inputQueue.length) return inputQueue.shift();
        return { ...direction };
      }

      function floodCount(start, blocked) {
        const queue = [start];
        const visited = new Set([cellKey(start.x, start.y)]);
        let count = 0;

        for (let index = 0; index < queue.length; index++) {
          const current = queue[index];
          count++;

          for (const delta of DIRECTIONS) {
            const x = current.x + delta.x;
            const y = current.y + delta.y;
            const key = cellKey(x, y);

            if (x < 0 || x >= COLS || y < 0 || y >= ROWS) continue;
            if (blocked.has(key) || visited.has(key)) continue;

            visited.add(key);
            queue.push({ x, y });
          }
        }

        return count;
      }

      function findDistance(start, target, blocked) {
        if (!target) return -1;
        if (start.x === target.x && start.y === target.y) return 0;

        const queue = [{ ...start, distance: 0 }];
        const visited = new Set([cellKey(start.x, start.y)]);

        for (let index = 0; index < queue.length; index++) {
          const current = queue[index];

          for (const delta of DIRECTIONS) {
            const x = current.x + delta.x;
            const y = current.y + delta.y;
            const key = cellKey(x, y);

            if (x < 0 || x >= COLS || y < 0 || y >= ROWS) continue;
            if (blocked.has(key) || visited.has(key)) continue;

            if (x === target.x && y === target.y) return current.distance + 1;
            visited.add(key);
            queue.push({ x, y, distance: current.distance + 1 });
          }
        }

        return -1;
      }

      function chooseAIDirection() {
        const head = snake[0];
        const occupied = new Set(
          snake.slice(0, -1).map(segment => cellKey(segment.x, segment.y))
        );

        let bestDirection = null;
        let bestRank = -Infinity;

        for (const candidate of DIRECTIONS) {
          if (oppositeDirection(candidate, direction)) continue;

          const x = head.x + candidate.x;
          const y = head.y + candidate.y;

          if (x < 0 || x >= COLS || y < 0 || y >= ROWS) continue;
          if (occupied.has(cellKey(x, y))) continue;

          const eating = food && x === food.x && y === food.y;
          const simulated = [{ x, y }, ...snake];
          if (!eating) simulated.pop();

          const blocked = new Set(
            simulated.slice(1).map(segment => cellKey(segment.x, segment.y))
          );
          const area = floodCount({ x, y }, blocked);
          const distance = findDistance({ x, y }, food, blocked);
          const spaceNeeded = Math.min(10, Math.max(1, simulated.length - 1));

          let rank = area * 4;
          rank += distance >= 0 ? 800 - distance * 6 : -500;
          if (eating) rank += 120;
          if (area < spaceNeeded) rank -= 1400;
          if (sameDirection(candidate, direction)) rank += 0.5;

          if (rank > bestRank) {
            bestRank = rank;
            bestDirection = candidate;
          }
        }

        return bestDirection || { ...direction };
      }

      function spawnBurst(gridX, gridY, crash = false) {
        const colors = crash
          ? ["#ff8066", "#ffb17d", "#f7f1da", "#ff6177"]
          : ["#ff8066", "#ffc080", "#c9f66d", "#fff1d1", "#65dba5"];

        for (let i = 0; i < (crash ? 28 : 20); i++) {
          const angle = Math.random() * Math.PI * 2;
          const speed = (crash ? 2.2 : 1.6) + Math.random() * (crash ? 4.5 : 3.5);
          const life = 0.4 + Math.random() * 0.38;

          particles.push({
            x: (gridX + 0.5) / COLS,
            y: (gridY + 0.5) / ROWS,
            vx: Math.cos(angle) * speed / COLS,
            vy: Math.sin(angle) * speed / ROWS,
            life,
            maxLife: life,
            size: 0.055 + Math.random() * 0.09,
            color: colors[Math.floor(Math.random() * colors.length)]
          });
        }
      }

      function stepGame() {
        const oldSnake = snake.map(segment => ({ ...segment }));
        const chosenDirection = state === "demo"
          ? chooseAIDirection()
          : takeQueuedDirection();

        if (!chosenDirection) return;

        const nextHead = {
          x: snake[0].x + chosenDirection.x,
          y: snake[0].y + chosenDirection.y
        };

        const eating = food && nextHead.x === food.x && nextHead.y === food.y;
        const wallCollision = nextHead.x < 0 || nextHead.x >= COLS ||
          nextHead.y < 0 || nextHead.y >= ROWS;

        // The tail vacates its cell on a normal move, so it is not a collision.
        const bodyToCheck = eating ? snake : snake.slice(0, -1);
        const selfCollision = bodyToCheck.some(segment =>
          segment.x === nextHead.x && segment.y === nextHead.y
        );

        direction = { ...chosenDirection };
        previousSnake = oldSnake;

        if (wallCollision || selfCollision) {
          snake = [nextHead, ...snake];
          spawnBurst(nextHead.x, nextHead.y, true);
          endGame(wallCollision ? "wall" : "self");
          return;
        }

        snake.unshift(nextHead);

        if (eating) {
          const eatenFood = { ...food };
          score += 10;
          best = Math.max(best, score);
          updateStats(true);

          spawnBurst(eatenFood.x, eatenFood.y, false);
          floaters.push({
            x: eatenFood.x + 0.5,
            y: eatenFood.y + 0.2,
            life: 0.9,
            maxLife: 0.9
          });

          food = spawnFood();
          if (!food) {
            endGame("win");
            return;
          }
        } else {
          snake.pop();
        }

        tickInterval = Math.max(86, 142 - (snake.length - 4) * 2.15);
        updateUI();
      }

      function endGame(reason) {
        state = "over";
        best = Math.max(best, score);
        accumulator = 0;
        updateStats(false);
        updateUI();
        ui.demoHint.hidden = true;

        if (reason === "win") {
          ui.overlaySymbol.textContent = "★";
          ui.overlayEyebrow.textContent = "Perfect run";
          ui.overlayTitle.textContent = "Board cleared!";
          ui.overlayText.textContent = "Every square is yours. That was a flawless run.";
          ui.overlayButton.textContent = "Play again";
          ui.overlayShortcut.textContent = "Press Space or Enter to restart";
        } else {
          ui.overlaySymbol.textContent = "×";
          ui.overlayEyebrow.textContent = "Run complete";
          ui.overlayTitle.textContent = "Game over";
          ui.overlayText.textContent = reason === "wall"
            ? "The wall caught up. Give the next run a little more room."
            : "Your tail found the finish line. Try turning before the gap closes.";
          ui.overlayButton.textContent = "Play again";
          ui.overlayShortcut.textContent = "Press Space or Enter to restart";
        }

        ui.overlayScore.textContent = padScore(score);
        ui.overlayBest.textContent = padScore(best);
        ui.overlay.hidden = false;
      }

      function pauseGame() {
        if (state !== "playing") return;
        state = "paused";
        accumulator = 0;
        updateUI();

        ui.overlaySymbol.textContent = "Ⅱ";
        ui.overlayEyebrow.textContent = "Paused";
        ui.overlayTitle.textContent = "Take a breath";
        ui.overlayText.textContent = "Your run is right where you left it.";
        ui.overlayScore.textContent = padScore(score);
        ui.overlayBest.textContent = padScore(best);
        ui.overlayButton.textContent = "Resume game";
        ui.overlayShortcut.textContent = "Press P or Space to resume";
        ui.overlay.hidden = false;
      }

      function resumeGame() {
        if (state !== "paused") return;
        state = "playing";
        accumulator = 0;
        lastTime = performance.now();
        ui.overlay.hidden = true;
        updateUI();
      }

      function updateEffects(dt) {
        for (let i = particles.length - 1; i >= 0; i--) {
          const particle = particles[i];
          particle.life -= dt;
          particle.x += particle.vx * dt;
          particle.y += particle.vy * dt;
          particle.vy += dt * 0.42 / ROWS;
          if (particle.life <= 0) particles.splice(i, 1);
        }

        for (let i = floaters.length - 1; i >= 0; i--) {
          const floater = floaters[i];
          floater.life -= dt;
          floater.y -= dt * 0.8;
          if (floater.life <= 0) floaters.splice(i, 1);
        }
      }

      function mixHex(from, to, amount) {
        const a = parseInt(from.slice(1), 16);
        const b = parseInt(to.slice(1), 16);
        const ar = (a >> 16) & 255;
        const ag = (a >> 8) & 255;
        const ab = a & 255;
        const br = (b >> 16) & 255;
        const bg = (b >> 8) & 255;
        const bb = b & 255;
        const r = Math.round(ar + (br - ar) * amount);
        const g = Math.round(ag + (bg - ag) * amount);
        const blue = Math.round(ab + (bb - ab) * amount);
        return `rgb(${r}, ${g}, ${blue})`;
      }

      function roundedRectPath(x, y, width, height, radius) {
        const r = Math.min(radius, width / 2, height / 2);
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.arcTo(x + width, y, x + width, y + height, r);
        ctx.arcTo(x + width, y + height, x, y + height, r);
        ctx.arcTo(x, y + height, x, y, r);
        ctx.arcTo(x, y, x + width, y, r);
        ctx.closePath();
      }

      function drawBackground(time) {
        const background = ctx.createLinearGradient(0, 0, size, size);
        background.addColorStop(0, "#0d1d20");
        background.addColorStop(0.55, "#091619");
        background.addColorStop(1, "#061013");
        ctx.fillStyle = background;
        ctx.fillRect(0, 0, size, size);

        for (const speck of dust) {
          ctx.globalAlpha = speck.a * (0.7 + Math.sin(time * 0.8 + speck.phase) * 0.3);
          ctx.fillStyle = "#b7efd0";
          ctx.beginPath();
          ctx.arc(speck.x * size, speck.y * size, speck.r, 0, Math.PI * 2);
          ctx.fill();
        }
        ctx.globalAlpha = 1;

        ctx.strokeStyle = "rgba(158, 224, 197, 0.055)";
        ctx.lineWidth = 0.65;
        ctx.beginPath();
        for (let x = 1; x < COLS; x++) {
          ctx.moveTo(x * cell, 0);
          ctx.lineTo(x * cell, size);
        }
        for (let y = 1; y < ROWS; y++) {
          ctx.moveTo(0, y * cell);
          ctx.lineTo(size, y * cell);
        }
        ctx.stroke();

        ctx.fillStyle = "rgba(192, 239, 218, 0.075)";
        for (let x = 1; x < COLS; x++) {
          for (let y = 1; y < ROWS; y++) {
            ctx.fillRect(x * cell - 0.65, y * cell - 0.65, 1.3, 1.3);
          }
        }

        const vignette = ctx.createRadialGradient(
          size * 0.5, size * 0.44, size * 0.08,
          size * 0.5, size * 0.5, size * 0.73
        );
        vignette.addColorStop(0, "rgba(0,0,0,0)");
        vignette.addColorStop(1, "rgba(0,0,0,0.48)");
        ctx.fillStyle = vignette;
        ctx.fillRect(0, 0, size, size);

        const inset = cell * 0.48;
        const corner = cell * 0.52;
        ctx.strokeStyle = "rgba(201, 246, 109, 0.32)";
        ctx.lineWidth = 1.4;
        ctx.beginPath();
        ctx.moveTo(inset, inset + corner);
        ctx.lineTo(inset, inset);
        ctx.lineTo(inset + corner, inset);
        ctx.moveTo(size - inset - corner, inset);
        ctx.lineTo(size - inset, inset);
        ctx.lineTo(size - inset, inset + corner);
        ctx.moveTo(inset, size - inset - corner);
        ctx.lineTo(inset, size - inset);
        ctx.lineTo(inset + corner, size - inset);
        ctx.moveTo(size - inset - corner, size - inset);
        ctx.lineTo(size - inset, size - inset);
        ctx.lineTo(size - inset, size - inset - corner);
        ctx.stroke();
      }

      function drawFood(time) {
        if (!food) return;

        const pulse = 1 + Math.sin(time * 4.8) * 0.055;
        const x = (food.x + 0.5) * cell;
        const y = (food.y + 0.5) * cell;
        const radius = cell * 0.255 * pulse;

        ctx.save();
        ctx.translate(x, y);

        ctx.strokeStyle = "rgba(255, 147, 104, 0.2)";
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(0, 0, radius * 1.42, 0, Math.PI * 2);
        ctx.stroke();

        ctx.shadowColor = "rgba(255, 105, 76, 0.72)";
        ctx.shadowBlur = cell * 0.62;
        const berry = ctx.createRadialGradient(
          -radius * 0.3, -radius * 0.34, radius * 0.05,
          0, 0, radius * 1.18
        );
        berry.addColorStop(0, "#ffd2a5");
        berry.addColorStop(0.34, "#ff946b");
        berry.addColorStop(1, "#f05d54");
        ctx.fillStyle = berry;
        ctx.beginPath();
        ctx.arc(0, 0, radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.shadowBlur = 0;

        ctx.strokeStyle = "#a9dc79";
        ctx.lineWidth = Math.max(1.1, cell * 0.06);
        ctx.lineCap = "round";
        ctx.beginPath();
        ctx.moveTo(0, -radius * 0.8);
        ctx.quadraticCurveTo(-radius * 0.12, -radius * 1.08, -radius * 0.42, -radius * 1.1);
        ctx.stroke();

        ctx.fillStyle = "#c2e982";
        ctx.beginPath();
        ctx.ellipse(-radius * 0.42, -radius * 1.08, radius * 0.27, radius * 0.115, -0.45, 0, Math.PI * 2);
        ctx.fill();

        ctx.fillStyle = "rgba(255,255,255,0.72)";
        ctx.beginPath();
        ctx.arc(-radius * 0.28, -radius * 0.33, radius * 0.105, 0, Math.PI * 2);
        ctx.fill();

        ctx.restore();
      }

      function drawSnake(alpha) {
        if (!snake.length) return;

        const points = snake.map((segment, index) => {
          const from = previousSnake[index] || segment;
          return {
            x: (from.x + (segment.x - from.x) * alpha + 0.5) * cell,
            y: (from.y + (segment.y - from.y) * alpha + 0.5) * cell
          };
        });

        ctx.save();
        ctx.lineCap = "round";
        ctx.lineJoin = "round";
        ctx.shadowColor = "rgba(79, 220, 150, 0.22)";
        ctx.shadowBlur = cell * 0.42;

        for (let i = snake.length - 1; i >= 1; i--) {
          const tailPoint = points[i];
          const frontPoint = points[i - 1];
          const blend = 1 - (i - 1) / Math.max(1, snake.length - 1);
          let width = cell * 0.78;

          if (i === snake.length - 1) width = cell * 0.56;
          else if (i === snake.length - 2) width = cell * 0.7;

          ctx.strokeStyle = mixHex("#218f75", "#8cdf7f", blend);
          ctx.lineWidth = width;
          ctx.beginPath();
          ctx.moveTo(tailPoint.x, tailPoint.y);
          ctx.lineTo(frontPoint.x, frontPoint.y);
          ctx.stroke();
        }

        ctx.shadowBlur = 0;
        ctx.fillStyle = "rgba(238, 255, 224, 0.15)";
        for (let i = 1; i < snake.length - 1; i++) {
          ctx.beginPath();
          ctx.arc(points[i].x - cell * 0.07, points[i].y - cell * 0.08, cell * 0.065, 0, Math.PI * 2);
          ctx.fill();
        }

        const head = points[0];
        const headSize = cell * 0.94;

        ctx.save();
        ctx.translate(head.x, head.y);
        ctx.shadowColor = "rgba(201, 246, 109, 0.38)";
        ctx.shadowBlur = cell * 0.62;

        const headGradient = ctx.createLinearGradient(
          -headSize / 2, -headSize / 2,
          headSize / 2, headSize / 2
        );
        headGradient.addColorStop(0, "#e2ff8f");
        headGradient.addColorStop(0.48, "#c9f66d");
        headGradient.addColorStop(1, "#a8e760");

        ctx.fillStyle = headGradient;
        roundedRectPath(
          -headSize / 2,
          -headSize / 2,
          headSize,
          headSize,
          cell * 0.27
        );
        ctx.fill();

        ctx.shadowBlur = 0;
        ctx.strokeStyle = "rgba(244, 255, 211, 0.8)";
        ctx.lineWidth = Math.max(1, cell * 0.035);
        ctx.stroke();

        const forward = direction;
        const side = { x: -forward.y, y: forward.x };

        for (const sign of [-1, 1]) {
          const eyeX = forward.x * cell * 0.18 + side.x * cell * 0.18 * sign;
          const eyeY = forward.y * cell * 0.18 + side.y * cell * 0.18 * sign;

          ctx.fillStyle = "#fbfff4";
          ctx.beginPath();
          ctx.arc(eyeX, eyeY, cell * 0.12, 0, Math.PI * 2);
          ctx.fill();

          ctx.fillStyle = "#10221a";
          ctx.beginPath();
          ctx.arc(
            eyeX + forward.x * cell * 0.045,
            eyeY + forward.y * cell * 0.045,
            cell * 0.061,
            0,
            Math.PI * 2
          );
          ctx.fill();

          ctx.fillStyle = "rgba(255,255,255,0.85)";
          ctx.beginPath();
          ctx.arc(
            eyeX + forward.x * cell * 0.066 - side.x * cell * 0.016 * sign,
            eyeY + forward.y * cell * 0.066 - side.y * cell * 0.016 * sign,
            cell * 0.019,
            0,
            Math.PI * 2
          );
          ctx.fill();
        }

        ctx.restore();
        ctx.restore();
      }

      function drawEffects() {
        ctx.save();
        ctx.globalCompositeOperation = "lighter";

        for (const particle of particles) {
          const alpha = Math.max(0, particle.life / particle.maxLife);
          ctx.globalAlpha = alpha;
          ctx.fillStyle = particle.color;
          ctx.beginPath();
          ctx.arc(
            particle.x * size,
            particle.y * size,
            particle.size * cell * (0.65 + alpha * 0.55),
            0,
            Math.PI * 2
          );
          ctx.fill();
        }

        ctx.restore();

        for (const floater of floaters) {
          const alpha = Math.max(0, floater.life / floater.maxLife);
          ctx.save();
          ctx.globalAlpha = alpha;
          ctx.font = `800 ${Math.max(11, cell * 0.55)}px ui-rounded, system-ui, sans-serif`;
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.lineWidth = 3;
          ctx.strokeStyle = "rgba(6, 18, 18, 0.7)";
          ctx.strokeText("+10", floater.x * cell, floater.y * cell);
          ctx.fillStyle = "#e8ff9c";
          ctx.fillText("+10", floater.x * cell, floater.y * cell);
          ctx.restore();
        }
      }

      function draw(time) {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, size, size);

        drawBackground(time);
        drawFood(time);

        const alpha = state === "over"
          ? 1
          : Math.max(0, Math.min(1, accumulator / tickInterval));

        drawSnake(alpha);
        drawEffects();
      }

      function loop(now) {
        const dt = Math.min((now - lastTime) / 1000, 0.1);
        lastTime = now;

        if (state === "playing" || state === "demo") {
          accumulator += dt * 1000;
          let steps = 0;

          while (accumulator >= tickInterval && steps < 4) {
            accumulator -= tickInterval;
            stepGame();
            steps++;

            if (state === "over") {
              accumulator = 0;
              break;
            }
          }
        }

        updateEffects(dt);
        draw(now / 1000);
        requestAnimationFrame(loop);
      }

      ui.pauseButton.addEventListener("click", event => {
        event.stopPropagation();

        if (state === "demo") startManual();
        else if (state === "playing") pauseGame();
        else if (state === "paused") resumeGame();
        else resetGame("playing");
      });

      ui.overlayButton.addEventListener("click", event => {
        event.stopPropagation();
        if (state === "paused") resumeGame();
        else resetGame("playing");
      });

      window.addEventListener("pointerdown", event => {
        if (state !== "demo") return;
        const target = event.target;
        const isButton = target && target.closest && target.closest("button");
        if (!isButton) startManual();
      });

      window.addEventListener("keydown", event => {
        if (event.metaKey || event.ctrlKey || event.altKey) return;

        const key = event.key.toLowerCase();
        const movement = KEY_DIRECTIONS[key];

        if (movement) {
          event.preventDefault();
          if (state === "demo") startManual(movement);
          else if (state === "playing" && !event.repeat) queueDirection(movement);
          return;
        }

        if (key === "p") {
          event.preventDefault();
          if (event.repeat) return;

          if (state === "demo") {
            startManual();
            pauseGame();
          } else if (state === "playing") {
            pauseGame();
          } else if (state === "paused") {
            resumeGame();
          } else {
            resetGame("playing");
          }
          return;
        }

        if (key === " " || key === "enter") {
          event.preventDefault();
          if (event.repeat) return;

          if (state === "over") resetGame("playing");
          else if (state === "demo") startManual();
          else if (state === "paused") resumeGame();
          else resetGame("playing");
          return;
        }

        if (
          state === "demo" &&
          !["shift", "control", "alt", "meta", "tab"].includes(key)
        ) {
          startManual();
        }
      });

      window.addEventListener("resize", resizeCanvas);
      if ("ResizeObserver" in window) {
        const observer = new ResizeObserver(resizeCanvas);
        observer.observe(canvas.parentElement);
      }

      resizeCanvas();
      resetGame("demo");
      requestAnimationFrame(loop);
    })();
  </script>
</body>
</html>
Reasoning trace (23,650 tokens)
We need craft final exactly one fenced code block complete HTML. Need build polished Snake vanilla JS canvas requirements. Need plan high-quality visual with responsiveness 1280x800, mobile 375. Need all inline no external. Need meaningful first paint screenshot auto ~3 seconds, game auto-play AI behind "Click or press a key to start" message. Need implement self playing demo mode simple AI plays. Need distinct head eyes direction, smooth movement particles food, score best session, overlay game over score Space/Enter restart P pauses. Arrow/WASD queue quick successive honored no reverse. Grid at least 20x20 centered. We can pick 24x24 or 26. Canvas responsive square. Smooth movement using interpolation. Need produce polished UI maybe dark warm gradient lime/coral modern. Need ensure auto-playing visible and non-trivial interesting first 3 sec. Should game begin immediately in demo AI but overlay message "Click or press a key to start" maybe visible slim banner while demo in action. Instructions "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message" so use translucent/banner "Click or press a key to start" top? Demo active, AI moves toward food safe. User input switches to manual and dismisses message. Need maybe central small prompt overlay doesn't obscure game entirely. We can show bottom of canvas center pill.

Need implement game accurate 26x24? Grid at least 20x20. We choose 24 columns x 24 rows square 576px on 1280 screen. Side layout maybe large centered card, canvas left or center plus side panel? "grid ... centered on the page." Center overall game canvas. We can build app max width 1120 with header and play area square around 600? At 1280x800 can canvas 600px plus side panel 260, center? Requirement grid centered on page, maybe centered canvas inside centered card with panels top/bottom not sides. Could use centered game frame width min(86vw, 620px), canvas square ~560 because height. Header above. On 800h header 60+top 20 + game 600+footer 70 =750. 24x24 with cell 24 =576. Good. Could create canvas 576x576 CSS display block width 100%. Frame max-width 620; padding 14; canvas 576. At 1280 800 => top bar header 76, game 604, footer maybe 50 => ~754. Center nicely. Could instead background decorative grid and centered gameplay shell width 640, scoreboard top inside shell, canvas 576; footer. Fine.

Visual polish: dark midnight background radial glows; card translucent with border; title "SNAKE" monospace? no external font so system fonts including ui-rounded. Accent acid green (#c7f46a) and coral orange (#ff795f). Pixel-ish / rounded snake cells with glowy outline, gradient body hue transitioning head bright. Grid subtle dots/lines. Food orange glowing with floating pulse and small sparkles. Snake head distinct with snout? head rounded square slightly bigger, eyes whites with pupils oriented facing direction. Smooth interpolation at logical grid movement. Body cells drawn radius, connecting segments to avoid gaps? Could draw rounded rect each cell with slight overlap; smooth interpolation gives continuity. Add gradient along body? Individual rounded squares gradient with tone; tail taper. We can draw smooth center interpolation head and body positions, segments continuous; at intermediate movement draw rounded rects size cell*.88 with gap? Snake standard look should be connected. Use stroked line with round joins and gradient? Need body distinct from head and eyes. Can render path connecting body with lineWidth cell*.76 round joins/caps then overlay scale? This yields connected body, body transitions. Head circle rounded at interpolation. For segments, draw path of positions head -> tail line with stroke gradient across path impossible gradient along path but can draw segments with color interpolate green shades. A path with stroke uniform gradient vertical maybe nice. Could draw each segment capsule between previous and current, lineWidth cell*.78 with color varying. Head draw rounded square ~.92 with lime. Eyes facing direction. Need avoid reverse and collision during interpolation.

Game mechanics: logical grid 24x24. tick maybe 140ms demo (fast enough 3 sec food? not required). manual 125ms. Use requestAnimationFrame with accumulator and interpolation based on tick progress. AI path chooses toward food with flood fill safety. Need robust self playing mode several seconds without dying quickly. Implement safe AI: BFS from head to food excluding snake; if path exists choose first step, ensure not dead-end and future safe. Standard algorithm. Could also simple greedy with safe check flood fill after candidate. At each AI step calculate BFS from head ignoring snake occupied to food; first direction along path if safe; if no path, pick reachable neighbor maximizing free space (flood fill area). Avoid reverse? AI can reverse if not into neck? reverse onto neck forbidden. no reversals regardless. Must not go reverse onto itself, so exclude opposite direction (unless length 1? still no reverse). manual queue up to 2 or 3 to honor quick presses. Important quick successive both honored: if user presses up then left before tick, one up tick then left. Need queue directions with max maybe 4; avoid reversals against last committed direction and queued? Standard queue input: on press, compare to last queued or current dir; prevent opposite of current effective direction. If user rapidly right->up->left: at right initial, up queued; left should be allowed relative to up, so compare to queued tail. Queue max 4. On tick shift. But if dir right, press up then left quickly, both honored. If right, up, down quick, down should be ignored because opposite queued up. Great.

However AI and manual input handling. We start in demo. Any arrow/WASD key maybe activates manual with that key and should queue relative to AI direction. If AI currently e.g up and user presses down, that is reverse onto body? snake can never reverse. Could ignore direction or instead allowed? Requirement never reverse. Need ensure if first manual key opposite current committed dir it's ignored? Then user may expect down. We could change? No. Better keep queue only non-reverse and show "Turn ignored" maybe unnecessary. Input activation if key valid, apply queue with current dir, if opposite ignore. User can press correct later. On click anywhere could activate manual but direction AI remains. Message dismiss. "Click or press a key to start" clearly. Space/Enter on game over restarts manual or demo? Space/Enter restarts game, likely manual. If click starts manual. Space/Enter while active could pause? requirement only over restarts. We can perhaps Space toggles pause also but game over overlay. We can make Enter/Space start manual if demo active? The message says click or press a key to start; if any key starts manual, space maybe starts and pause? Better user presses arrow/WASD start manual. Space/Enter in demo could start manual too. But requirement "Game over ... Space or Enter restarts, P pauses." Ensure.

Score best of session no localStorage. Best in JS memory; update when score > best (or when game over). Display best chip crown. Add "+10" floating label on food with particle burst. Food always not on snake. Score numeric. Progress? not needed. Length maybe display. High score. Add game status pill "DEMO" and "PAUSED" etc.

Overlay game over: card centered with "Game over", reason (wall/self), final score, best, "Play again" button maybe Space / Enter. Button visible accessible. Need user interaction not mandatory screenshot game over but possible. Could demonstrate end state? no, keep active. The screenshot likely first 3 sec demo. Need demo visible action. Banner top maybe "AI DEMO • Click or press a key to play" and canvas prompt. Instruction "behind a 'Click or press a key to start' message". Use exact text. Demo overlay as bottom centered pill. Good.

Pause P: state paused, overlay with "Paused", resume via P or button. Show overlay no score? okay. Manual switch starts unpaused.

At 375 width adapt: page body min-height 100vh, game width calc 100vw-24px. Canvas aspect 1, header smaller, footer hide extra or wrap. 24x24 cells at 351 px -> 14.6 px, still good. Frame padding maybe 8. Header title, score. At 800h, need game fit. Could use CSS main width min(92vw, 640px); game-shell; canvas wrapper width 100%. Top header title and badges. To ensure 1280x800 screenshot everything fits: body padding 16; app width 650; header 56+12; frame canvas 610 + score maybe? Let's design:

body flex center, overflow hidden? height 100dvh. main.app width min(94vw, 660px); display flex column; gap 12.
header top: brand left, stats right? width 640.
brand small eyebrow "ARCADE / 01", h1 SNAKE with small snake icon.
stats pills Score 000, Best 000. At mobile header wrap maybe title + stats row.
game-card padding 12; canvas wrapper aspect-ratio 1; canvas width 100%; border radius 18.
Under canvas footer: controls hint left and status right, but could reduce.

At 1280x800 main total: header 52 + gap 12 + card (canvas ~600+24+2) =626 + footer 32+gap => 734; good. max width 640 => canvas 612? card inner width 616; total ~650. Height header 60 + 12+646 + 12+30=760. fits body 768 if padding 12. Great. At narrower heights use .app transform? Responsive use width min(94vw, calc(100dvh - 140px), 640px) so canvas fits. CSS: --game-size: min(94vw, calc(100dvh - 148px), 620px); .app { width: min(94vw, 640px);} .playfield { width:100%; aspect ratio } But if height small, game should fit. main width: min(94vw, 640px, calc(100dvh - 150px)); CSS min supports 3. Could set width:min(94vw, 640px); max? for 800 -> 640, canvas 612, total maybe 746. at 375x maybe 667 -> width 353, good.

Canvas resolution devicePixelRatio. Logical drawing coordinate in CSS px? set canvas.width = size*dpr, height; ctx.setTransform(dpr,0...). Then resize cell = canvas.clientWidth/cols. Since canvas uses 24x24. Drawing coordinates CSS. Need detect resize and recalc. Canvas element CSS width 100%, aspect-ratio 1; width attr 600 etc. use ResizeObserver if available, fallback resize event. Need render at 60 fps.

Game representation snake array head first, each cell {x,y}. dir {x,y}. food. tick interval manual 130ms, demo 145? Smooth. Interpolation: previousSnake positions captured at each step; during frame t = min(1, accumulator/tickInterval) maybe draw head interpolation from previous head to current head. Important render representation at tick time. At initialization previous same. On tick:
- prevSnake = snake.map copy (before movement).
- determine next direction from input queue or AI.
- newHead = head+dir.
- collision.
- if wall/self: set gameOver, finalScore, cause; maybe effect crash particles; leave snake last.
- else unshift newHead; if food then score+10, grow; spawn food; particles; else pop.
- prevSnake = old positions. current snake.
Then interpolation each segment: current snake[i] and previousSnake[i]. When growth, previous has length n-1; tail previous may undefined. For body segment last if growth we can use current previous neighbor? More nuanced. We can store previous positions keyed identity? Simpler prevSnake array before growth, current length n+1. for segment i, target current[i], source prevSnake[i] if exists else current[i+1] (new tail at previous tail position), t interpolation. Head source prevSnake[0]. Body mapping typical old head->current head, old segment1->current segment1, etc. New tail extends from old tail to old tail at same? prev tail source current tail previous? Current snake after unshift and no pop: [newHead, oldHead, oldSeg1,...oldTail]; prev=[oldHead,oldSeg1,...oldTail]. For segment i current target and prev source i: segment1 current oldHead source oldSeg1 => moves to oldHead. good. Growth current [newHead,oldHead,...oldTail], prev [oldHead,...oldTail], current index n (tail) source absent => source=current[n-1]? current[n-1]=oldTail (since no pop) so stays. Good source current[i+1]? current index n-1 oldTail, yes source current[i+1]. Use.

At render t eased maybe linear smooth. t = accumulator / tick. But rAF accumulation occurs after tick. Use t maybe clamp, no easing needed linear. For smooth visual, interpolation.

Need game loop and transitions. Variables:
const COLS=24, ROWS=24;
let snake, prevSnake, direction, queuedDirs=[], food, score, best=0, state='demo' | 'playing' | 'paused' | 'over'; demo boolean.
let lastTime=performance.now(), accumulator=0, tickInterval= maybe 140.
function frame(time) { dt=Math.min(time-lastTime,100); lastTime=time; if (state==='playing'||state==='demo') { accumulator += dt; while(accumulator>=tickInterval){accumulator-=tickInterval; step(); if state over break;} } updateParticles(dt); draw(time); requestAnimationFrame(frame);}
Need if state changes to demo? initial reset(true). Demo always steps. Manual click calls activateManual maybe state playing, reset? If demo already running, should we keep current snake/food and just switch direction? Good. But message dismissal. Maybe reset manual for clean game? "Click or press a key to start" usually demo then player takes over. Could keep board and convert, but direction may lead immediate collision based on input; AI chose safe direction. On manual key opposite current ignored. Better reset with new snake and food on any manual input, giving score 0 and fresh game. But then screenshot may transition if user. Fine. click starts manual resetting. Press arrow starts manual resetting and queues desired direction? Need orientation. snake initially direction right at middle; if arrow up, queue up, valid. If arrow left opposite right, ignoring would mean snake keeps right—not ideal. Could initial dir chosen? We can on activation choose requested direction and reset snake orientation along requested? But "snake can never reverse onto itself" initial no body before it so can orient any direction. But if pressing left, start left. Reset snake in that direction with head at center and body behind, all safe. Then queue none. So function startManual(initialDir): resetGame(false, initialDir), state playing. Manual arrow after active queued.

Could let click restart manual with right. Press key on demo: WASD/arrow starts manual oriented desired. Space/Enter maybe starts manual right. P could maybe pause demo? user presses P after click? keydown receives if page clicked; event attached. If P while demo, maybe start manual and paused? no. Requirement P pauses when playing. Could P while demo activate manual and pause? weird. Better P if demo starts manual and paused? not necessary. We can state manual on P and toggles pause perhaps start game. But we can handle arrow only start, and P can start and pause.

State enum 'demo', 'playing', 'paused', 'over'. Overlay function.

AI path:
function chooseAIDirection() {
 const head=snake[0], bodySet snake.slice(0,-1?) all;
 BFS from head grid excluding snake cells maybe tail can move. Find path to food. Neighbors valid, not body, not reverse. BFS with directions order. If food reachable choose first path direction. But BFS path may lead trap later; check safety using reachable area from candidate while excluding body and perhaps candidate not body. Need robust.
Simpler safe step algorithm:
for each neighbor valid non-body and not opposite:
 simulate new snake after move (unshift neighbor, pop tail unless eating)
 compute reachable free cells from new head using flood fill excluding simulated body; choose move maximizing area and BFS distance to food if safe.
Can compute safe area including head. If neighbor is food safe maybe area enough.
Implementation BFS path excluding body; path first. Then safety area from resulting head; if area >= snake.length+? choose. Because path towards food perhaps leads dead-end. Alternative evaluate every valid candidate:
- simulate snake.
- flood fill from candidate head over cells not in sim snake (allow head start), count reachable cells. Tail might move, but exclude all sim body conservative.
- if candidate equals food, safe if reachable area >= maybe 5.
- if area >= sim.length? But reachable area from head excludes body including body itself, count must at least length-1 perhaps to not trap. Since body segments behind block corridors. Criterion area >= length maybe too strict but safe. If none choose max area. choose candidate with shortest Manhattan/BFS to food.
BFS distance to food excluding body from candidate. If no path maybe choose max reachable. AI direction.
We can make standard "look ahead with flood fill": for each valid neighbor:
 simulate newSnake; if eats don't pop.
 flood fill accessible from newHead excluding snake body, count; if newhead food then score huge but area count.
 if area >= newSnake.length - 1 (body behind occupies maybe) choose. Then tie by Manhattan distance. But free area excludes body length, so requiring >= length may too strict. require area >= Math.min(6, newSnake.length-1) or >0; then tie food closeness. At length ~6+ demo 3 sec length 3. Fine.
Need prevent AI reversing (direction opposite).
Maybe AI can run into tail if moving into tail as it vacates? Self collision standard checking snake before pop: moving into current tail is usually allowed if not growing because tail moves. Our collision check snake.some((seg,index)=> index < snake.length - (grows?1:0)). Most games allow moving into tail. But requirement "hits itself"; technically tail vacates. Could implement allow tail if not eating. Need not complicate. Use snake.slice(0,-1) for non-grow collision. NewHead equals tail allowed. AI bodySet excluding tail perhaps.

Manual quick queue:
let dir = {x:1,y:0};
function enqueue(dirCandidate) {
 const last = queue.length ? queue[queue.length-1] : direction;
 if candidate opposite(last) return false;
 if same(last) return true;
 if queue.length < 4 queue.push(candidate);
 return true;
}
But issue queue existing e.g dir right, queue up; then press left -> opposite of queued up allowed => queue [up,left]. Next tick shifts up, then queued left valid (not opposite of up). Great. If press left then up quickly when dir right: left is opposite right ignored, then up relative to last direction right queue up. If initial manual orientation perhaps left then no issue.

Should queued input be cleared on each manual activation/reset. AI no queue.

Collision:
function step(){
 const oldSnake=snake;
 const nextDir = state==='demo'? chooseAI(): shiftQueue();
 // demo maybe choose reverse? exclude.
 let nextHead...
 let eating nextHead==food
 const bodyToCheck = eating ? snake : snake.slice(0,-1)
 if wall or body => gameOver; crash effect; update best; show overlay; return.
 prevSnake=oldSnake.map...
 direction=nextDir;
 snake.unshift...
 if eat score +=10, best=max; particles; floatText; food=spawnFood; tick maybe interval reduce min 85, 138-(length-3)*2; else pop.
}
At gameover prevSnake should maybe old, snake remains current head at wall? Could set snake unshift wall? For visual crash, maybe include head at wall and particles. We'll implement game over after movement: set snake = [nextHead,...snake] even if collision? That shows head into wall. prev old. But self collision weird. okay. state='over'; overlay.

Best score session persistent variable across restarts. On score update best immediately.

Spawn food: random free cells excluding snake. Need if board full -> win state maybe "Perfect run!" overlay; handle. Use list all cells not occupied; random. There are 576 plenty. Maybe ensure spawn away from head at least 2 cells to avoid instant. Random free.

Particles:
array {x,y,vx,vy,life,maxLife,color,size,gravity}. Food logical cell center or canvas pixels. spawn 20 particles at cell center in canvas coordinate based on current cell size. On resize positions in cell coords to stay? Store grid cell x,y and velocities in pixels per second maybe based on cell. Better particle positions in normalized board coordinates (0..1) and velocity in cell/sec. spawn at food.x+0.5 etc. update dt normalized. draw in canvas coords.
Use {x:gridX, y:gridY, vx:(rand-.5)*8 cells/sec, vy..., life .6, maxLife}. Update x += vx*dt etc. draw circle radius size * cell. Food burst.
Floating text {x,y,text:'+10', life:1}. update y -= dt*0.7 cell; draw.

Food draw: cell center; pulse sin time. Glow shadowBlur. Maybe draw small rounded diamond/apple? food as coral orb with highlight, orbit ring, little leaf? Distinct. Snake food can be a glowing berry. Draw circle radius .28 cell, gradient coral to orange, shadow; small top highlight and green stem maybe. Use roundRect. Draw two tiny sparkles cross. Food pulses.

Snake draw:
Need use interpolation positions. Build renderSegments:
function interpolateSegment(i) {
 curr=snake[i]; prev=prevSnake[i] || (i===0?curr:snake[i+1]||curr);
 // careful prev positions correspond old segment indices, current index i:
 const from = prevSnake[i] || snake[i] || curr;
 const to = curr;
 return {x:from.x+(to.x-from.x)*t, y:...};
}
For i=1 old segment1 => prev[1]. yes.
But if prevSnake length old n, current n+1 after growth: prev missing for tail index n => fallback snake[i+1] doesn't exist, fallback current -> tail static, okay. Yet growth tail should at old tail; current tail is old tail indeed fallback current correct.
For current segment i after no growth: prev[i] exists except tail? old length n, current n, prev indices0..n-1 all. tail curr old last-1? At movement, current after pop [new, old0,old1,...oldn-2], prev [old0,...oldn-1]. Tail current=oldn-2, prev=oldn-1; interpolation tail moves backwards toward oldn-2; correct.
First source head prev[0]=oldhead -> current newhead.
Interpolate all.

Draw connected body. Option 1 each capsule between render segment centers:
for i=1..n-1:
 const a=segments[i], b=segments[i-1] (head->tail)
 ctx.strokeStyle color; ctx.lineWidth=cell*.78; lineCap round; beginPath move a to b stroke.
But at bend round join if separate segments caps overlap. Colors gradient per segment. Each capsule is line from a to b with round cap, width .78; at joints nice. Draw tail-to-head to overlay body and head last. Tail segment line from center i to i-1; tail cap circle. Use per-segment color based on i: tail darker teal, head bright lime. We can create color mix. Colors maybe body gradient from teal #35c89b tail to lime #b6f45a head. Draw each segment line with round cap and subtle shadow. Add scales: small translucent spots/dashes on body. Could draw each segment as rounded rectangle at interpolated center with width .78 cell and height .78; capsule overlap. For body consistent width and tail taper.
We can draw one path with gradient? Let's do individual capsule:
lineWidth = cell * (i===last ? .56 : .76)
stroke style color mix tail to head.
At every joint, draw maybe circle at segment center same color to fill gaps.
Add highlight line offset -0.12 cell? Each capsule center.

Head:
at segments[0], size .92 cell, rounded square rotated? Snake head distinct. Could use roundedRect centered with radius .28 cell, fill gradient lime. Draw subtle dark outline and bright border. Eyes facing direction. Need direction where head moving next. Draw eyes positions relative to dir. If moving horizontal, eyes at x +/-? local coordinate transform with forward vector. Use coordinates local: forward f=(dir.x,dir.y), side=(-f.y,f.x). Head eyes placed forward*cell*.17 + side*cell*.17 and forward*cell*.17 - side*cell*.17. Eye radius .115 cell, white. Pupils offset forward .035 cell and maybe dark. Head shape maybe rounded square with snout? Add a small darker lime snout at forward side? Can draw head as rounded rect oriented always axis-aligned, eye positions adapt. Eyes visible. Distinct eyes.
At head direction up/down, eye locations. White circles and dark pupils.
Could add head border stroke #e5ffb0. shadowBlur 18.

Body might overlap eyes? Draw all body then head.

Grid: draw background in canvas or CSS. canvas background #0c1519. Draw grid lines each cell with rgba(255,255,255,.025) and corner dot at intersections. At 24*24 okay. Outer vignette. Draw decorative corners? Canvas frame.

Canvas resolution:
const DPR=Math.min(devicePixelRatio||1,2);
function resize() { const rect=canvas.getBoundingClientRect(); size=Math.round(rect.width); canvas.width=size*dpr; canvas.height=size*dpr; ctx.setTransform(dpr,0,0,dpr,0,0); cell=size/COLS; }
But if CSS canvas width 100%; canvas height auto based attributes ratio. Set aspect-ratio:1 / 1; display block. width and height attributes 600. CSS height:auto maybe aspect. Use style width:100%; height:auto; aspect-ratio:1. Canvas intrinsic width may height. resize sets both. Fine.
DPR backing size should exact Math.round(size*dpr). ctx setTransform(dpr...). If rounded mismatch tiny but okay.

Potential initial flash before JS? body dark, meaningful canvas might blank for <3 sec but JS loads immediate. We can include static loading? no need.

Visual page:
Background CSS radial gradients. maybe decorative floating blurred orbs. body::before grid pattern. Main.
Header:
<div class=topbar>
 <div class=brand><div class=mark> maybe custom inline svg snake icon? We can create CSS icon 3 squares or inline svg. No external. h1 "SNAKE<span>.</span>" small subtitle "Classic, refined."
 <div class=scoreboard> score block and best.
</div>
Game card with top status bar inside? We need status and demo message. Could have .game-toolbar at top of canvas area: status "DEMO MODE" indicator, level? The canvas itself. Use .canvas-wrap position relative.
Top in game card maybe status row:
<span id=modeBadge class=badge><i></i> AI DEMO</span>
<span id=lengthLabel>Length 4</span>
then canvas.
Canvas overlay absolute:
- demo prompt div id=demoPrompt class=prompt "Click or press a key to start" + "The snake is playing for you".
- gameover overlay id=gameOverlay hidden with panel.
- pause overlay same perhaps.
Could use one overlay layer with dynamic content.
Maybe separate start prompt bottom center, overlay full for over/pause.
Status row outside canvas top, controls footer bottom.

Score header use tabular numerals. Score starts 000? Show 000 with 3 digits. best 000.
Score bump animation on eat (class pop). +10 label.

Game card size maybe header topbar width same. Let's calculate CSS.

html,body height:100%; body margin0; overflow:hidden? On 375x? app maybe 650 tall > viewport 667? width 353 -> canvas 337, total 500, fits. 1280x800: app width 640; topbar 64; gap12; card padding10: toolbar30+8+canvas612+footer? Actually canvas width = card inner 620 if app 640, card border+padding 22 ->618. total card 30+8+618+footer maybe footer outside card? Put controls row separate below. App total topbar56+14+ card(30+8+616+22 padding)=676 +14+footer30=790 >800 slightly. Set app width 620 max -> inner596; total card30+8+596+20=654; total 56+12+654+12+28=762 fits. Great. canvas 596. App width min(94vw,620px, calc(100dvh - 150px))? At 800 height calc 650, min620. At 700 ->550; total ~642. Good. CSS width:min(94vw, 620px, calc(100dvh - 150px)); CSS min with calc valid. At mobile width375 height667 -> min(352.5,620,517)=352.5. total canvas330+ etc=520. At 375x600 -> calc450 => width352? min352.5,450 =352.5; canvas330 total 510 fits 600. At landscape height375 -> calc225 width225, tiny but graceful. okay.
Header topbar h1 30, subtitle hidden mobile. score boxes. At mobile width352 topbar brand maybe 100 and scores 200 total fits. topbar flex.
Game shell border radius 24, padding10, bg rgba, box shadow. Toolbar h28.
Canvas border radius16.
Footer hints .key etc. On mobile hide some.

Add accessible live region for score maybe.
Buttons maybe overlay play again.
Need no external images/fonts. Use inline SVG icon for logo maybe CSS.

Sound? no need; WebAudio possible but not required. Avoid extra.

Controls display:
footer:
<div class=controls>
 <div><span class=key-group>↑↓←→</span> / <span>WASD</span> move</div>
 <div><span class=key>P</span> pause</div>
 <div><span class=key>SPACE</span> restart</div>
</div>
Maybe at 375 hide move text? Keep wrap.
Footer inside app after card. At mobile font 10.

Overlay:
<div id=overlay class=overlay hidden>
 <div class=modal>
  <div id=overlayIcon>...</div>
  <p id=overlayEyebrow>GAME OVER</p>
  <h2 id=overlayTitle>...</h2>
  <p id=overlayText></p>
  <div class=result-grid>score/best</div>
  <button id=overlayButton>Play again</button>
  <span class=shortcut>Space / Enter</span>
 </div>
</div>
For pause overlay h2 Paused etc. Button "Resume". For gameover score. Need hidden attribute CSS: [hidden]{display:none!important}. Overlay backdrop blur 4px, dark. Demo prompt separate behind? when state over hide prompt.

Could dynamically set overlay via setState. State transitions:
resetGame(mode='demo', initialDir=right):
 snake start length maybe 4 with head at (12,12), body behind left: head x=12, then 11,10,9. direction right.
 food spawn random avoiding near head maybe.
 score=0; queue=[]; prev=snake copy; particles clear; tickInterval=140; accumulator=0; lastTime=performance.now? Keep rAF.
 state=mode;
 updateUI; overlay hidden; demo prompt shown mode demo; toolbar badge.
 best retained.
 For manual after demo, reset with orientation based on requested. But if reset, score resets, best retained. Start screen prompt disappears.
 If game over and Space/Enter -> resetGame('playing') (manual). If click gameover maybe button.
 If P while playing -> pause: state paused; overlay display; preserve accumulator. On resume state playing and lastTime reset to avoid dt.
 if P while paused -> resume.
 if P while demo: maybe activateManual() and then pause? I'd handle P simply if state playing/paused; in demo, startManual() (manual), then set paused maybe but prompt disappears and overlay says Paused. User can P resume. Fine perhaps.
 Keydown global:
 const key=e.key.toLowerCase();
 if arrows/wasd: e.preventDefault(); if state demo startManual(dirMap[key], maybe queue?), if over? maybe start manual dir; if paused? resume? Should pressing direction while paused perhaps resume and enqueue? implement if paused -> state playing then enqueue after orientation? But direction opposite current concern. Better no resume on direction. Standard P resume. If demo: startManual(dirCandidate) orient. If playing: enqueue. If over: maybe startManual(dirCandidate) but Space/Enter required restart; could let arrow restart too, but requirement says Space/Enter restarts, doesn't forbid arrow. But overlay button. To avoid overwriting? Could arrow restart as well but okay. We can keep over arrow ignored except maybe not.
 if key==='p': prevent; if demo startManual() then pause? Hmm "P pauses" if no game active maybe start then pause. Maybe start manual and state playing, no pause? That would not pause. Better if demo, startManual(); togglePause() => paused. But initial demo P then overlay. Fine.
 if space/enter:
   prevent;
   if state==='over' reset playing;
   else if state==='demo' startManual();
   else if state==='paused' resume (convenience);
   else maybe no action or restart? Footer says SPACE restart maybe inaccurate while playing. Could use Space also restart while playing, but accidental? Requirement Space or Enter restarts after game over only. Footer could say "Space / Enter restart" general. We can make Space restart when over only; while playing no-op. The footer just says "Space restart" maybe users expect. Could restart anytime; but quick key handling? Maybe make Space/Enter toggles pause while playing? Not required and conflict. Better footer "P pause · Space/Enter restart" and allow restart anytime? "Game over ... Space or Enter restarts" yes. We can allow restart from playing too. But then pressing space might reset and queue lost. Fine.
 Mouse click: if state demo start manual; if over clicking overlay button; clicking canvas while playing maybe pause? no. First click anywhere should activate. Add pointerdown on document? If overlay hidden. On canvas pointerdown if demo startManual. On overlay maybe button.
 Any key? "Click or press a key to start" perhaps pressing arbitrary key like Shift should start? We can on keydown if demo and key not modifier: if direction recognized orient; else startManual() (except P handled). For exact message, any key starts. But key repeats may switch/reset repeatedly if not careful. demo only first key. For manual key repeat, should avoid queue duplicate. e.repeat handling? Rapid presses. If hold key, repeated direction same no issue. We can ignore repeats for queue to avoid queue same. For movement, handle even repeat? not needed.

Important demo AI can collect food and score, update best. The AI prompt remains. Food maybe start in distant cell. AI path safe. Need ensure demo does not game over within 3 sec. Let's reason AI algorithm.

Start snake head (12,12), dir right, body x11,10,9. Food random. AI candidate BFS to food excluding snake. choose safe neighbor. Safety flood area from candidate excluding new snake. On open board plenty. likely eats eventually. Manual never reverse.

AI choose algorithm robust:
function aiDirection() {
 const head=snake[0], occupied = new Set(snake.slice(0,-1).map key); // exclude tail to allow
 const validDirs=DIRS.filter(not opposite direction && !occupied neighbor && within)
 For each:
  const eating = neighbor==food
  const sim = [neighbor,...snake]; if(!eating)sim.pop();
  const safe = floodArea(neighbor, sim.slice(1)) // exclude body excluding new head
  const dist = bfsDistance(neighbor, food, sim.slice(1)) maybe
  const danger = sim.length-1 - safe (trapped behind)
  score = safe*100 - dist*2 - danger...
 choose max.
BFS path direct can ignore disconnected due tail. flood area.
If safe area small but candidate food maybe still choose? food opens cell.
Require safe >= snake.length-1 maybe too strict early but open area ~500. choose.
At later length, open area still maybe >length. In corridor created by body safe may <length but choose max.
Use distance to food based BFS with sim body, maybe -1 if unreachable -> 1000.
score = safe * 10 - (dist>=0?dist:500)*2. If candidate is food bonus 50.
Also tie to avoid reversal.
This greedy can loop if path to food blocked. BFS distance each candidate gives shortest route. Good.
floodArea from new head excluding sim body; but if head is enclosed with only body behind, area=0. safe.
BFS distance from candidate to food excluding sim body. Candidate may be food dist0.
Potential candidate enters cell current tail allowed; sim body excludes tail if not eating because popped, okay. occupied excludes tail.

Need decide whether manual queue direction logic based on direction last committed. Suppose dir right, quick up then left. queue [up,left]. At tick choose up. If game tick occurs between up and left, queue [left], current up. good. If quick left after up but before tick and current direction right, queue tail up, left allowed. Good. If direction right and press up then down before tick: up queued; down opposite queued ignored. Then up executes. Great. If queue currently up,left, then press down: down opposite last left ignored. fine.
But when queue full and new direction appended beyond length4 not honored; requirement "Quick successive key presses must both be honored" likely 2; max4 enough. Could not cap at 4? Cap at 8. We can use queue max 6. Need ensure no reversal compare to last queued. Use input buffer 4. Typical.

Collision with queued turn at tick. Good.

Game over overlay cause:
if wall "You hit the wall." if self "You crossed your own path."
score display padded maybe 000.
best updated.
Overlay background canvas blur? CSS backdrop-filter supported. Modal.
Animation CSS.

Demo message:
<div class=demo-hint id=demoHint>
 <span class=pulse-dot></span>
 <span><strong>Click or press a key to start</strong><small>Watch the AI — then take control</small></span>
</div>
position absolute bottom:14px left50 transform. Ensure snake area lower? Prompt may overlap snake, but initial snake middle. fine. Exact phrase visible.
Could show top toolbar badge "AI DEMO" with animated dot. At screenshot, demo mode active. Good.

Maybe prompt should be "behind" message, we satisfy.

Score bump:
scoreEl.textContent=String(score).padStart(3,'0'); scoreEl.classList.remove('bump'); void offsetWidth; add. best.
Length label update every step.

Need ensure first paint shows filled snake food. reset called before rAF.

Canvas draw details and CSS:
- board background CSS? canvas context fill with gradient. Canvas draw:
ctx.clearRect
gradient radial? create each frame maybe okay. fill #0a1215.
ctx.fillStyle; fillRect.
grid lines.
vignette via radial gradient.
Maybe tiny stars/dust fixed random positions normalized, draw opacity.
Use precomputed dust 35 positions. Deterministic seeded pseudo random? can use Math.random initial.
Grid draw line every cell with rgba. At 24, line 1.
Draw board border inside.

Food draw with shadowBlur; context save/restore. Gradient createRadialGradient center-radius.
Leaf maybe small green oval at top-left? Food circle enough. Distinct orange.

Snake body color mixing:
function mixColor(c1,c2,t) parse rgb hex. Define tail #249a7a, mid #69d878, head #c8f56a. But if each segment line color interpolation with head maybe:
t = 1 - i/(n-1) maybe head 1. color = tail->lime.
ctx.shadowColor='rgba(113,235,135,.22)'; shadowBlur=10.
for i=n-1 down to 1:
 line from segments[i] to segments[i-1].
 if tail last lineWidth cell*.56 else .78.
 maybe scale widths taper last 3.
 stroke color.
Then draw small circles at centers? Round cap does.
Scales: For each segment i>0, draw a small circle or line at center with rgba(255,255,255,.12), radius cell*.09. But on connector, center overlap. Draw after all body, maybe small glossy dash along center from segment to previous. Could make body visually segmented. Perhaps draw each segment as rounded rectangle using ctx.roundRect if supported Chrome yes. But capsules adequate.

Head rounded square:
function roundedRectPath(ctx,x,y,w,h,r) use ctx.roundRect (latest chrome supports). Use custom to be safe.
ctx.translate(hx,hy)
let s=cell*.94
ctx.beginPath(); roundRect(-s/2,-s/2,s,s,cell*.27)
gradient linear from -s/2 to s/2 with #dcff83->#a7e957
fill; stroke rgba(238,255,205,.85) lineWidth 1.5
Shadow.
Maybe snout: a small rounded shape forward, but eyes enough.
Eyes:
for sideSign [-1,1]:
 ex=dir.x*cell*.19 + (-dir.y)*cell*.19*sideSign
 ey=dir.y*cell*.19 + dir.x*cell*.19*sideSign
 white circle radius cell*.115; dark pupil radius cell*.057 at ex+dir.x*cell*.035, ey+...
 tiny glint.
At head direction right, eyes positions x+.19, y±.19; right side eye pupil to right. Good.
Head rotates? no.

Tail perhaps small dot.

Particles draw:
positions normalized times COLS? Store board units. x in 0..1, update. convert px=x*size. radius cell*(0.06+...).
ctx.globalCompositeOperation='lighter'; alpha life/max; fill.
Floating +10 font 700 `${cell*.55}px` system; stroke dark maybe.

Food glow and pulse. Use time param. Draw orbit tiny points.

UI update:
modeBadge text:
demo: "AI DEMO"
playing: "PLAYING"
paused: "PAUSED"
over: "GAME OVER"
dot class.
length.
pause button maybe top toolbar button accessible with icon? Add clickable button id pauseButton with "Pause" and key P. This helps mobile. Use CSS button. At mobile label maybe icon. Button pointer. Top row: badge and <button id=pauseButton>Pause <kbd>P</kbd></button>. But requirement grid centered. okay.
In demo pause button says "Take over" maybe click to manual. We can set text based state. Click starts manual.
Length chip maybe right. Need fit 596.
Toolbar: left modeBadge, center length? right pause.
At mobile: font 9.

Footer key caps.

Session best no storage.

Need maybe "food adds 10" score update. score starts 000. Floating +10.

Collision timing: Direction set after collision? For gameover show head facing wall direction maybe. step:
const old = snake; let chosen...
const next...
if out: direction=chosen; prevSnake=old copies; snake=[next,...old]; gameover. Self same. This means wall head outside grid by one, draw partly at border, crash effect. Great.
If collision with snake, newHead duplicate body. Gameover.
crash particles at head center even out. overlay blur covers.

For AI movement queue no.

State transitions and overlay:
function showOverlay(kind):
 overlay.hidden=false
 if over: eyebrow "RUN COMPLETE", title "Game over", text cause, score etc, button "Play again", shortcut "Space / Enter"
 if paused: eyebrow "TAKE A BREATHER", title "Paused", text "Your run is safe...", result maybe hide? Could still display score and best, button "Resume", shortcut "P / Space"
Use DOM elements.
Gameover and paused.
On demo no overlay.
On startManual hide overlay; demoHint hidden.
On reset demo show hint.

Buttons:
overlayButton.addEventListener click: if paused resume else reset playing.
pauseButton click:
 if demo startManual()
 if playing pause
 if paused resume
 if over reset playing
Need event.stopPropagation? document click handler maybe only canvas click. Button overlay click no global.

Canvas pointerdown if demo -> startManual. If playing maybe no action. If over maybe reset? Could let.
document keydown enough.

At initial demo, game loop begins. AI prompt "Click or press a key to start". If user clicks anywhere outside canvas, maybe document pointerdown handler if state demo starts manual. Requirement click. Add document.addEventListener('pointerdown',(e)=> { if(state==='demo' && !e.target.closest('#pauseButton')) { startManual(); }}) But if click pauseButton, button handler also. Could just canvas and overlay; message says click, presumably game. Add window pointerdown once demo, but pauseButton handler triggers then window also maybe reset twice. Event order target then window bubble, button starts manual state changed, window sees state playing so no. Fine. But any click including overlay button hidden no.

Key handling:
const dirKeys = {arrowup:{x:0,y:-1},w:..., etc}
window.addEventListener('keydown', e=>{
 const k=e.key.toLowerCase();
 if (dirKeys[k]) {
  e.preventDefault();
  if (state==='demo'){startManual(dirKeys[k]);}
  else if (state==='playing'){ if(!e.repeat) enqueueDirection(dirKeys[k]); }
  return;
 }
 if(k==='p'){ e.preventDefault(); if(state==='demo'){startManual(); pauseGame();} else if(state==='playing') pauseGame(); else if(state==='paused') resumeGame(); return;}
 if(k===' '||k==='enter'){e.preventDefault(); if(state==='over')resetGame('playing'); else if(state==='demo')startManual(); else if(state==='paused')resumeGame(); /* playing maybe restart?*/ }
});
Requirement Space/Enter restarts game over. Good. Footer says "Space / Enter restart" maybe only when over; could label "Restart on game over" not necessary. Use controls "P pause", "Space / Enter restart". To make accurate, allow space/enter restart from playing too. But then if accidentally, okay. Implement if playing resetGame('playing')? That means user can restart. Requirement doesn't preclude. Footer accurate. Yet keyboard quick repeated Space could restart. ignore repeat. We'll do:
if playing and !e.repeat -> resetGame('playing') maybe surprising but explicit footer. Fine.
if paused -> resume perhaps footer? Could reset? Typical Space resumes. overlay says P / Space.
Enter likewise.
Could reset manual direction right. Good.

AI demo best: score updates; best maybe current score. Score header.
Potential issue startManual resets game while keydown arrow; orientation:
function resetGame(mode, dir={x:1,y:0}) {
 const cx=Math.floor(COLS/2), cy=...
 snake=[]
 for i=0..3: {x:cx-i*dir.x,y:cy-i*dir.y}
 direction=dir
 ...
}
If dir up {0,-1}: body behind down: head cy, body cy+1 etc. good.
No food on snake.

Manual queue starts empty, dir chosen.
First move after 135ms. fine.

Need "quick successive key presses both honored" maybe tests simulate rapid keys. Our queue handles. But potential same direction compared to last queued. Good.
Test scenario initial right, press up and left synchronously before tick: enqueue up then left (left opposite up? opposite of up is down, so allowed). Tick1 up, tick2 left. Great.
Could queued left after up be invalid if current snake body orientation? no.
No reverse onto itself: manual input queue candidate compared to tail of queue. But consider queue [up,left], direction right. At tick shift up. Fine. If queue [up], direction right, user presses down before tick: down opposite up -> ignored. Good.
Potential candidate opposite current committed but queue has unrelated? compare last queued, so sequence right->up->down: down opposite up ignored. Sequence right->up->left->right: right opposite left ignored. Good.

AI excludes reverse direction; could reverse onto tail if snake length? no.

Food spawn never snake. use free array.

Smooth movement: rAF interpolation. Need interpolate relative to direction after turn. It will slide old head to new head and body. If direction queued changes at tick, previous head to new head diagonal? new head moves one cell direction. body mapping okay.

Interpolation t issue rAF accumulator. Suppose state over triggered within while; break; accumulator maybe. render snake positions with t perhaps 0? okay.
When reset set prev=snake and accumulator0.

Drawing text labels etc.

Canvas accessibility: role img aria-label. Score live.

CSS details:
:root --bg #070d10; --panel rgba(16,27,30,.85); --lime #c8f56a; --mint #65ddad; --coral #ff7b5e.
body background:
radial-gradient(circle at 20% 10%, rgba(84,189,142,.12), transparent 35%), radial-gradient(circle at 85% 80%, rgba(199,244,105,.07), transparent 30%), #071014;
display grid place-items:center; padding:16px; min-height:100dvh; box-sizing border-box; color.
body::before fixed inset0 background-image linear gradients size 32px; mask-image radial gradient; pointer events none; opacity .35.
.app position relative z-index1.
.topbar display flex align center justify-between margin-bottom 12.
.brand display flex.
.logo-mark 42x42 rounded 14 background linear gradient; CSS with pseudo squares or inline svg. Use inline SVG with path. It can be inside. e.g <svg viewBox 0 0 40 40><path d="..."> fill none stroke lime stroke-width 5 linecap round; circle eye dark. Fine. Inline.
.brand h1 letter spacing -1; small.
stats display flex gap8.
.stat background rgba border padding 8 12 min-width68; .value font 20.
best icon tiny svg maybe ★.
At mobile subtitle hidden, logo 36, h1 24, stat padding6, values16.
.game-card background linear; border 1px solid rgba; border-radius 24; padding 10px; box-shadow 0 24px 80px rgba, inset.
.toolbar display flex align center gap8 height30 margin-bottom8; font uppercase.
.mode-badge dot.
.pause-button margin-left:auto; border etc.
.length maybe.
.canvas-wrap position relative; border radius 16; overflow hidden; background #081115; box-shadow inset.
canvas width100%; display:block; aspect ratio.
.demo-hint position absolute bottom:14 left50; backdrop blur; border; animation float; pointer-events:none; text. At mobile bottom8 padding.
Maybe hide small line mobile.

.overlay position absolute inset0; display grid place-items center; background rgba(3,9,11,.62); backdrop-filter blur(5px); z-index5.
.modal width min(78%,330px), border etc, text center; padding24; animation modalIn.
.eyebrow color coral; h2 30; result-grid.
button primary lime black.
controls display flex justify-between gap; margin-top10; font 11; color.
.key kbd.
At 375 footer maybe wrap, controls justify center; .control-secondary hide? Keep all but tiny.
Media max-width480:
body padding8
app width calc(100vw - 16px) maybe CSS current min gives 94vw=352.
topbar margin-bottom8, subtitle hidden.
stat minwidth 58; value 17.
game-card padding7 border-radius18
toolbar margin bottom6 height26
demo-hint small.
footer .control-help maybe.
Also @media max-height:700 reduce topbar etc.

Use no horizontal scroll: html,body overflow-x:hidden, max width.
Canvas width.

Do we need center grid exactly? main centered both axes via body grid. yes.

The game is centered on page with card.

Potential screenshot comparison expects obvious snake/food; our canvas 596, grid 24 -> cell24.8, snake 4 cells ~99 px. Visible. food glowing. Good.

Maybe title/logo and stats polished. Great.

Need include code no comments? can include concise comments.

Need make canvas board square, grid 24. Cell at 596/24 24.8. Great.

Let's work through JS carefully no errors.

HTML structure:
<!doctype html>
<html lang=en>
<head>...
</head>
<body>
<main class=app>
<header...>
...
<section class=game-card aria-label...>
 <div class=toolbar>
  <span id=modeBadge ...><span class=status-dot></span><span id=modeText>AI DEMO</span></span>
  <span class=length-chip>Length <strong id=lengthValue>4</strong></span>
  <button id=pauseButton ...><span id=pauseButtonText>Pause</span><kbd>P</kbd></button>
 </div>
 <div class=canvas-wrap>
  <canvas id=gameCanvas width=600 height=600 ...></canvas>
  <div id=demoHint...>
  <div id=overlay class=overlay hidden ...>
...
 </div>
</section>
<footer class=controls>...
</main>
<script>
(() => {
'use strict';
const canvas=document.getElementById...
const ctx=canvas.getContext('2d');
const COLS=24, ROWS=24;
const DIR = Object.freeze...
const DIRS = ...
let size=600, cell=25, dpr=...
let snake=[], previousSnake=[], direction={...}, inputQueue=[], food=null, score=0,best=0,state='demo';
let tickInterval=140, accumulator=0,lastTime=performance.now();
let particles=[], floaters=[], dust=[];
...
resize();
resetGame('demo');
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Need resize before reset? reset uses cell maybe food not. fine. Resize observer callback can call resize while running. In resize, if snake etc no impact.

State UI functions:
function setModeUI() {
 modeText.textContent = state === ...;
 modeBadge.dataset.state=state;
 lengthValue.textContent=snake.length;
 pauseButtonText.textContent = state==='demo' ? 'Take over' : state==='paused' ? 'Resume' : 'Pause';
 pauseButton.disabled = state==='over'? maybe false to restart. Keep.
}
function updateStats(animateScore=false) { scoreValue... best; }
At reset call updateStats.
At score eat call.
Best display session. Could animate.

function resetGame(mode='demo', initialDirection={x:1,y:0}) {
 const cx=Math.floor(COLS/2), cy=Math.floor(ROWS/2);
 snake=[];
 for(let i=0;i<4;i++) snake.push({x:cx-i*initialDirection.x,y:cy-i*initialDirection.y});
 direction={...}; inputQueue=[];
 food=spawnFood();
 score=0; particles=[]; floaters=[];
 previousSnake=snake.map(copy);
 state=mode;
 accumulator=0;
 lastTime=performance.now();
 tickInterval=140;
 overlay.hidden=true;
 demoHint.hidden= mode!=='demo';
 updateStats();
 updateUI();
}
One concern reset called while frame callback and resets lastTime, fine.
Food spawn uses snake and maybe tries not near head:
let candidates free; then choose with min distance? random. To avoid food adjacent to head maybe filter Manhattan >=3, if list. AI reaches later. Fine. Food visible.
function spawnFood() {
 const occupied=new Set(snake.map(key));
 const free=[];
 for y... for x if !occupied.has
 const preferred=free.filter(distance >=3)
 const pool=preferred.length?preferred:free;
 return pool[Math.floor(Math.random()*pool.length)] || null;
}
No food on snake guaranteed.
If null board full -> win. handle in step eating spawn null: set state='over', title maybe "Board cleared!" but requirement overlay game over? Could win. We can set win=true. Rare. Implement state over with overlayTitle "Board cleared!" eyebrow "PERFECT RUN", text. But score. Fine.

startManual(initialDir={x:1,y:0}) {
 resetGame('playing',initialDir);
 demoHint.hidden=true;
}
If start from demo on arrow left, initial orientation left and body to right. Good.
If start from demo any key e.g Space, right.

pauseGame:
if state !==playing return
state='paused'
accumulator=0
showPauseOverlay()
updateUI()
demoHint hidden.
resume: state playing; overlay.hidden=true; lastTime=performance.now(); accumulator=0; update.
showPauseOverlay populate result and button.
gameover:
state='over'; best=Math.max; update stats; overlay.
Need if game over while demo, overlay. AI could die, then screenshot after 3 sec perhaps not likely. We want maybe game continues indefinitely safe. Could if AI dies accidentally, maybe restart demo after 1.5 sec? Requirement game over overlay. Don't auto restart. But screenshot ~3 sec maybe AI could hit? Safe algorithm should avoid. Could demo mode on gameover show game over instead of restart, but user wants demo visible. AI robust.

AI flood:
function simulateMove(nx,ny) {
 const eating = food && nx===food.x...
 const body=[{x:nx,y:ny},...snake];
 if(!eating) body.pop();
 return body;
}
floodCount(start, blockedSet):
 queue arr of {x,y}, seen. Start added regardless blocked (not in).
 neighbors within and !blocked and !seen.
blocked includes body.slice(1) to include tail if eating, exclude tail if popped.
BFS distance to food similarly. We can combine shortest path and reachable area. For candidate, blocked = sim.slice(1). BFS from candidate to food excluding blocked. flood count same blocked. distance maybe number steps.
Valid dirs filter not opposite of direction, not wall, not occupied. Occupied should snake.slice(0,-1) because tail moves unless eating; but candidate into tail when not eating is allowed. If food equals tail impossible. Use blocked full snake except tail; if moving into tail, sim [head,tail,...], pop tail => okay. candidate equals tail not in sim? [tail, oldhead,...oldsecondlast], good. Set occupied = snake.slice(0,-1) excludes tail. If snake length4, head included. valid excludes head.
Also exclude reverse.
scoreCandidate:
const sim = ...
const blocked Set sim.slice(1)
const area=floodCount(candidate, blocked)
const dist=bfsDistance(candidate, food, blocked)
const canReach=dist>=0
const spaceScore=Math.min(area,200)
let rank=spaceScore*4;
if(canReach) rank += 2000 - dist*4; else rank -=500;
if(eating) rank+=250;
rank += area - (sim.length-1) * maybe;
We want choose candidate that can reach food with good area. If any canReach, choose max; area tie. But candidate path might trap after several moves. Yet each step rechecks. rank = (canReach ? 10000:0) - dist*10 + area. If no reachable, max area. This greedily follows shortest. Candidate into food dist0. Good.
Potential candidate's BFS excludes tail because sim popped; could path through tail cell now free (tail moved into previous segment? Wait sim after move pop tail, old tail cell becomes free, yes).
Area exclude sim body. Since body can form walls, area count.
If all candidates unsafe, pick largest area.
Add slight directional bias to current dir .2.
AI can't reverse even if no safe. fine.
Let's consider start dir right, body left. Candidate up/down/right. food random. BFS to food can route. choose.
Because blocked includes snake body, area huge.
At each move safe.

Manual collision should allow moving into tail. bodyToCheck = eating?snake:snake.slice(0,-1). If newHead tail, allowed. Requirement hits itself could interpret tail collision, but standard okay. Could disallow tail to be safe? If tests check reverse only. Hitting tail rare. "hits itself" physically tail moves. fine.
Reverse onto itself explicitly prohibited, input queue prevents 180.

Score update:
score+=10;
best=Math.max(best,score);
updateStats(true)
floaters.push({x:food.x+.5,y:food.y+.15,life:1,max:1});
spawn burst.
food=spawnFood() after snake updated. We need save eaten position before.
particles positions normalized:
for 18: angle random, speed 1.8+3 random cells/sec, x=(food.x+.5)/COLS, y=(food.y+.5)/ROWS, vx=cos*speed/COLS? If x normalized 0..1 and want cells/sec, vx=cos*speed/COLS, vy likewise. update.
Particle radius cell*(.07+rand*.07), life .45+.35.
Colors ['#ff8a68','#ffd18c','#c8f56a','#fff2cf'].

Update floaters dt x,y in grid units? Store x grid coordinate, y grid coordinate; update y -= dt*.7 (cells/sec). Draw at x*cell etc.
food burst location drawn maybe under food.

Frame:
function frame(now) {
 const dt=Math.min((now-lastTime)/1000,0.1); lastTime=now;
 if(state==='playing'||state==='demo'){
  accumulator+=dt*1000;
  let guard=0;
  while(accumulator>=tickInterval && guard<4){
    accumulator-=tickInterval;
    step();
    guard++;
    if(state==='over'){accumulator=0;break;}
  }
 }
 updateEffects(dt);
 draw(now/1000);
 requestAnimationFrame(frame);
}
tickInterval changes in step maybe accumulator. good.
If document hidden dt clamped.
Interpolation alpha = state over?1? At gameover, want final snake visible; if prev old and t maybe accumulator; when over set accumulator0 yields t0 so draws old snake not crash head! In step gameover we could previousSnake=old and snake with new head, alpha computed? if state over set alpha=1. For normal alpha=accumulator/tick. For over alpha=1. For paused alpha=accumulator/tick static.
After step normal, accumulator remainder e.g 50ms -> t=.35. draw snake moving.
At initial prev=same t0.

Potential body render at t based all segments. Great.

draw(timestamp):
ctx.setTransform(dpr,0,0,dpr,0,0); clearRect 0,0,size,size.
background gradient:
const bg=ctx.createLinearGradient(0,0,size,size); stops #0c181b #071014.
fill.
drawDust timestamp subtle.
grid:
ctx.strokeStyle rgba(149,213,184,.06), lineWidth1; for x=1..COLS-1 path all vertical maybe one path. horizontal.
center? dots.
Maybe draw subtle large "24" etc no.
Vignette radial gradient center transparent to rgba black .35.
Draw food before snake so snake over food if overlap no.
Draw particles maybe after snake for glow.
Draw floaters.
Board border.
Need roundRect function:
function roundRect(x,y,w,h,r){ const rr=Math.min(...); ctx.beginPath(); ctx.moveTo... quadraticCurveTo...}
No reliance on ctx.roundRect.

Drawing body:
const alpha = state==='over'?1:Math.min(1,accumulator/tickInterval);
const pts = snake.map((seg,i)=>{
 let from = previousSnake[i];
 if(!from) from = snake[i]; // tail growth
 return {x:(from.x+(seg.x-from.x)*alpha +.5)*cell, ...}
});
Potential previousSnake mapping for i after growth: index i missing only last, fallback current. good.
When snake length1 no.
ctx.save();
ctx.lineCap='round'; ctx.lineJoin='round'; ctx.shadowColor...
for i=n-1 down to1:
 const p=pts[i], q=pts[i-1];
 let t = 1 - (i-1)/Math.max(1,n-1); // i1 t=1 head lime, tail0 teal
 color=mix('#1f9b78','#84df77',t) maybe head adjacent still green. Head lime separate.
 width = cell*(i===n-1? .58 : i===n-2?.70:.78)
 ctx.strokeStyle=color; lineWidth=width; beginPath...
 also maybe draw small scale dot at p with alpha.
ctx.shadowBlur=0.
Then tail maybe draw circle.
Need body continuity at turns: each segment connector from p_i to p_{i-1}, at each p_i two connectors overlap. Round.
But connectors might be diagonally oriented and line width .78cell, leaving a slight bulge; fine.
Scale detail:
for i=1..n-2:
 ctx.fillStyle='rgba(235,255,220,.15)';
 ctx.beginPath(); ctx.arc(p.x-cell*.08,p.y-cell*.08,cell*.065,0,..)
But highlights offset same global direction, not body local. fine. Maybe not draw on all.

Head:
ctx.save translate p0.
s=cell*.94
ctx.shadowColor rgba(200,245,106,.38); shadowBlur=18;
gradient...
roundRect fill, stroke.
ctx.shadowBlur=0.
Maybe draw subtle forehead stripe.
Eyes. Use white #f8fff0, pupils #102019.
At direction up eyes x±, y-. Add glint.
Maybe draw nostrils? no.
Head clear distinct.

Food:
const fx=(food.x+.5)*cell + Math.sin(timestamp*2)*cell*.035; fy...
radius cell*(.25 + sin*time*.035)
ctx.shadowBlur=cell*.65.
radial gradient.
circle.
shadow reset.
draw stem: small green line/ellipse top (for food always same direction) perhaps berry with leaf:
ctx.save translate; rotate? draw ctx.strokeStyle #bde979 lineWidth cell*.07 from(0,-r*.75) to(-r*.4,-r*1.1); ellipse leaf. But stem makes food orange with green leaf, distinct. shadow.
highlighter white small circle.
orbit tiny dot maybe.
Food pulse no distracting.
Particles behind or front. draw after food perhaps burst.

Dust:
const dust array normalized {x,y,r,a,phase}. draw tiny circle with alpha .05+.03 sin. behind grid.

Board decorative corner marks maybe within canvas: draw L-shaped corners at 12px from edges in lime alpha .3. Nice. Use ctx stroke.

Overlay prompt lives DOM.

CSS animations:
@keyframes floatHint {0,100% transform translate(-50%,0);50% translate(-50%,-4px)}
@keyframes pulse { ...}
@keyframes scoreBump.
@keyframes statusPulse.
score animate class via JS.

Need be wary CSS `min(94vw, 620px, calc(100dvh - 150px))` support yes modern Chrome. If width computed 650 at height800 due calc650 and 620 ->620. Canvas inner maybe app620 game padding10 border2 ->598. topbar height? brand 44, card toolbar 30+margin8+598+padding20+border=658; total app header44+12+658+12+footer18 =744 + body padding32=776. fits 800. Good.
At height800 app centered with 12 top/bottom.
At mobile height667: app width min352.5,620,517 =352.5. card canvas width app - padding20 -border2=330.5. card height toolbar30+8+330+22=390; header40+8+390+10+footer32=480. fits. Nice.
At height375: app width min(94vw maybe1182,620,225)=225. canvas203, header44, card243, footer18 total329; fits 375. tiny but okay.

At 375 width topbar: app352. brand logo36 + gap9 + title ~95 =140. stats score two boxes each60 +gap7=127, total267 fits.
Header subtitle hidden.
Toolbar mode badge maybe86, length60, pause button70 =>216 fits.
Footer controls maybe 3 items; width352. Move group "Arrows / WASD" ~100, pause50, restart85 + gaps =250. good.
At very narrow 320 still width300.

Need exact prompt phrase perhaps h strong. It says "Click or press a key to start". Use exactly.

No network requests.

Maybe "best score of session" means updates across restart. yes.

We should maybe show score points label. best.

Could include initial score 000. AI maybe eats by 3 sec and score 010. good.

Need perhaps demo mode is "behind" message; prompt overlay only bottom, game continues. good.

Let's consider game AI visibility: AI starts moving right. Food random. At 3 sec ~20 ticks; length maybe5, score10-30. good. Smooth.

Could AI head get trapped due choose shortest path that cuts itself? safety area. Let's formalize rank:
for candidate:
 simBody excludes candidate? sim includes new head and remaining body.
 blocked = sim.slice(1)
 area=flood
 distance=bfs to food excluding blocked
 rank = (distance>=0 ? 1000 - distance*8 : -1000) + Math.min(area,120)*2 + (eating?50:0) + direction same .1
If candidate path to food exists but area low, another with no path area huge: path candidate score ~1000 + small vs no path -1000+240, still path chosen. Might be trap. Could require area >= min(sim.length-1,8) for canReach; if not, penalize -2000. Better:
const trapped = area < Math.min(8, sim.length-1);
rank = (dist>=0?500:0) + area*3 - dist*4 - (trapped?1500:0).
Path candidate if not trapped wins. If all trapped choose max area.
For length4 sim.length-1=3, need area>=3. open.
Use `spaceNeeded=Math.min(10, sim.length-1)` maybe area>=spaceNeeded. At long snake10, require10 free accessible. okay.
rank = area*4 - (dist>=0 ? dist*5 : 800) + (eating?100:0) - (area<needed?1500:0).
Path candidate dist perhaps 20 area500 =>1960; no path area500=>2000-800=1200. path wins.
Trap path area1 dist5=>4-25-1500=-1521 vs no path area100=400-800=-400, choose no path.
Fine.

BFS distance must avoid candidate? start.
flood area excludes body, but can move through tail free if popped. Good.
At decision current direction reverse excluded. If snake length4, reverse candidate is body anyway.

AI could choose path candidate but later after moving path uses former tail? safe.

Manual P overlay. Overlay result grid always.

DOM update:
`modeBadge.dataset.state = state;` CSS status dot colors based data.
`pauseButtonText` etc.
`overlay.classList` maybe.

function showGameOver(cause) {
 state='over';
 best=Math.max...
 overlayEyebrow.textContent='RUN COMPLETE';
 overlayTitle.textContent='Game over';
 overlayText.textContent=cause;
 overlayScore...
 overlay.hidden=false;
 demoHint.hidden=true;
 updateStats();
 updateUI();
}
Cause based wall/self. If win null food.
Crash particles at nextHead coordinates maybe outside; normalized.
`spawnCrash(x,y)` colors coral, white.

Game over overlay score required. yes.

Score best update while eating. If game over no change.

`overlayButton` focus? don't auto focus maybe no.
`overlay.setAttribute('aria-hidden'...)` hidden.
live region `aria-live=polite`.

Need maybe pause button state "Take over" in demo. User clicks -> manual.
Footer says "Click / any key take over" perhaps.

Input keys:
`e.key` for space is " "; toLowerCase remains " ".
`w` etc.
Prevent default for arrows/space to avoid page scroll.
if ctrl/meta/alt ignore maybe. Add if(e.metaKey||e.ctrlKey||e.altKey)return; but alt+? no.
`e.repeat`: For movement, holding key should maybe continue same; queue same doesn't matter. We can if e.repeat return for movement to avoid repeated but direction same no effect. If key down then up? event separate.
We must honor quick successive even if repeat? fine.
For any key in demo start manual. Direction keys handled; for other e.g "x" start manual right (except P,space).
"press a key" exact.
If state paused and direction key, maybe don't do anything; perhaps resume? user may expect controls still. Could resume and queue but current orientation. Could set state playing and enqueue direction. Requirement P pauses, likely P resumes. no.
Space in paused resume.
P in demo: we could start manual and pause overlay. But phrase any key start; P starts then immediately paused, not ideal. Instead if demo P: startManual(); then pauseGame() maybe user intentionally pressed pause. Fine. Overlay explains. However screenshot automated no P.
Any key in demo other than recognized: startManual.
Space demo: startManual.
Enter demo: startManual.
P demo: startManual then pause.

Quick key presses "both honored": We should perhaps not reset manual on each key. Once state playing, queue. yes.

Click on canvas while demo resets to right. Clicking canvas while playing could focus no action.
When user presses arrow after demo, reset and orient to arrow, so no queue needed. fine.

Potential `window.addEventListener('keydown')` fires only after iframe click per sandbox note. We show prompt. good.

Could use focus? no.

Need "smooth movement": interpolation linear. Also food pulse.

Let's refine visual color.
Canvas background dark teal, grid subtle. Body green gradient. Head chartreuse. Food coral. UI accent lime. Polished.

Add score label with tiny plus sign on eat? floating +10.

Let's build final HTML carefully. Need one fenced code block exactly and no text. User didn't ask explanation. We comply developer.

Before final, let's simulate mentally JS and CSS syntax.

CSS:
`:root { color-scheme: dark; ...}`
* box-sizing border-box.
html,body { width:100%; min-height:100%; }
body { margin:0; min-height:100vh; min-height:100dvh; overflow-x:hidden; display:grid; place-items:center; padding:14px; ...}
body::before fixed.
.app { width:min(94vw, 620px, calc(100dvh - 146px)); position:relative; isolation:isolate; }
Potential width at 800=620. At viewport height800. At mobile width 375 height800, width min352.5,620,654=352.5.
topbar.

SVG logo stroke. Use aria-hidden.

stat `aria-label`.
score live.

game-card.
toolbar.
mode-badge data-state demo etc.
`.mode-badge[data-state="playing"] .status-dot { background: var(--lime); }`.
status dot box-shadow.
length-chip.
pause-button.

Canvas wrap aspect-ratio:1 / 1; canvas display:block;width:100%;height:100%; But canvas height 100% of parent with aspect-ratio defined by width? .canvas-wrap {aspect-ratio:1;} height auto from aspect. canvas height:100% works if parent height resolved. yes.
canvas `touch-action: manipulation; outline:none;`.
Demo hint.
Overlay hidden rule.

Modal.
`[hidden] { display:none !important; }` global.

Controls.
kbd.

Responsive.

JS:
const ui = { ... };
Variables.

Dust generation:
const dust=[]; for i<34 dust.push({x:Math.random(), y..., r:.4+Math.random()*.8, a:.06+..., phase...});
coordinates in px? r in cell maybe. draw radius d.r*cell*.08? We'll define r=.4..1.1 px direct maybe high dpr CSS. use d.r.

Resize:
function resizeCanvas() {
 const rect=canvas.getBoundingClientRect();
 const next=Math.max(120,Math.round(rect.width));
 if (Math.abs(next-size)<1 && canvas.width) {size=next; cell=size/COLS;return;}
 size=next; cell=size/COLS;
 dpr=Math.min(window.devicePixelRatio||1,2);
 canvas.width=Math.round(size*dpr); canvas.height=...
 ctx.setTransform(dpr,0,0,dpr,0,0);
}
But if no rect width due? app loaded. ResizeObserver observe canvas-wrap perhaps. Observing canvas itself can trigger because setting width attr may affect CSS? CSS width100 height100 no. Use `new ResizeObserver(resizeCanvas).observe(canvas.parentElement);`.
At initial call after DOM.
ctx.imageSmoothingEnabled=true.

`canvas.getBoundingClientRect().width` fractional; size=Math.round, CSS width maybe 597.7, canvas backing 598 and CSS stretches slight no issue. cell=size/COLS but coordinate board ends at size; CSS width598. fine.

mixColor hex:
function hexToRgb(hex){...}
function mixHex(a,b,t){... return `rgb(...)`}
We call per frame per segment maybe ~50, okay.

roundRect helper uses ctx:
function roundedRectPath(x,y,w,h,r){r=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);...closePath();}
arcTo supported.

draw:
ctx.setTransform(dpr,...). `ctx.clearRect(0,0,size,size)`.
Gradient.
Dust.
Grid path. If ctx lineWidth=1/dpr? coordinate CSS, use .6.
Vignette.
Draw food if.
Draw snake.
Effects.
Corners.

Background gradient createLinear each frame okay.

Food:
function drawFood(time) {
 if(!food)return;
 const pulse=1+Math.sin(time*4.5)*.06;
 const x=(food.x+.5)*cell, y=(food.y+.5)*cell;
 const r=cell*.255*pulse;
 ctx.save();
 ctx.translate(x,y);
 ctx.shadowColor='rgba(255,111,82,.65)';ctx.shadowBlur=cell*.55;
 const grad=ctx.createRadialGradient(-r*.28,-r*.3,r*.05,0,0,r*1.15);
 grad.addColorStop(0,'#ffd0a1'); .35 '#ff946b';1 '#f35e54';
 ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(0,0,r,...);fill;
 ctx.shadowBlur=0;
 // leaf
 ctx.strokeStyle='#9ddc75';ctx.lineWidth=Math.max(1.2,cell*.065);ctx.lineCap='round';beginPath moveTo(0,-r*.82) quadraticCurveTo(-r*.2,-r*1.18,-r*.45,-r*1.12); stroke
 // leaf ellipse maybe
 ctx.fillStyle='#bce77d'; ctx.beginPath();ctx.ellipse(-r*.42,-r*1.08,r*.28,r*.12,-.5,0,2pi);fill
 // highlight
 ctx.fillStyle='rgba(255,255,255,.75)'; circle(-r*.28,-r*.32,r*.1)
 // orbit ring
 ctx.strokeStyle rgba(...,.22); lineWidth1; arc(0,0,r*1.35,... maybe)
 ctx.restore
}
Leaf above food maybe visible.

Snake:
function drawSnake(alpha,time) {
 const pts...
 // shadow under? Draw path under with thick rgba.
 // body connectors.
 for i=n-1;i>=1;i-- {
  const p=pts[i], q=pts[i-1];
  const t=1-(i-1)/Math.max(1,n-1)
  let color=mixHex('#228f74','#78d982',t)
  // tail maybe '#1f8271'
  let w=cell*(.78 - (i===n-1?.19:0) - (i===n-2?.05:0))
  ctx.strokeStyle=color;ctx.lineWidth=w;ctx.shadow...
  path p to q
 }
 // tail rounded cap naturally.
 // scales maybe loop
 for i=1..n-2 { ctx.fillStyle rgba...; circle pts[i] radius cell*.08 maybe creates spotted look}
 // head
 ...
}
Issue connector at head to segment1 ends under head. good.
Color t for i=1: 1-(0)/(n-1)=1 -> green, head body lime-green. tail dark.
Could use `mixHex('#1f8b76','#8bdf7d',t)`.
Shadow each stroke maybe expensive but fine.

Scales: offset? Draw tiny elongated shine at p:
ctx.fillStyle='rgba(244,255,228,.14)';
ctx.beginPath();ctx.arc(p.x-cell*.07,p.y-cell*.08,cell*.07,0,2π);fill.
Might look spots.

Head eyes:
const f=direction; const side={x:-f.y,y:f.x};
for sign:
 const ex=f.x*cell*.18+side.x*cell*.18*sign;
 ...
white radius cell*.12
pupil at ex+f.x*cell*.045 etc radius cell*.06.
glint.
If direction up, both eyes near top. Good.
Draw subtle nose maybe line at forward:
ctx.fillStyle='rgba(36,68,34,.25)' small circle? no.
Maybe head shape includes snout bump forward? Rounded square enough.

Crash effects:
spawnBurst normalized center. For out-of-bounds x could negative; particles drawn off canvas clipped. fine.

updateEffects:
for particles backwards, p.life-=dt; p.x += p.vx*dt; ...
particles=filter
floaters life.
Need if paused, particles should freeze? Frame updateEffects currently always. For paused maybe effects can finish; okay. Smooth.
At gameover burst needs animate, yes.

step:
function stepGame() {
 const oldSnake=snake.map(p=>({...p}));
 const chosen=state==='demo'?chooseAIDirection():takeQueuedDirection();
 if(!chosen)return;
 const next...
 const eating...
 if wall:
  direction=chosen; previousSnake=oldSnake; snake=[nextHead,...snake]; crash...
  endGame(...); return.
 if self:
 similarly.
 direction=chosen; previousSnake=oldSnake;
 snake.unshift(nextHead);
 if(eating) {
  score +=10; best...
  spawnFoodBurst(food.x,...)
  floaters...
  food=spawnFood();
  if(!food){ endGame('win'); return;}
 } else snake.pop();
 update length ui.
 tickInterval = Math.max(84, 142 - (snake.length-4)*2.2); demo maybe min90.
}
Important if eating and spawnFood uses new snake. yes.
For self collision bodyToCheck: if eating full snake else snake.slice(0,-1). `some`.
At wall direction and snake outside; previous old. endGame. `endGame` uses state.
`spawnCrash` maybe use nextHead normalized; if wall, x maybe -1. fine.
`direction=chosen` before.
Score if eating and then food null (board full) endGame after score update. previous snake includes growth but no pop; okay.
`updateUI` every step.
endGame:
function endGame(reason) {
 state='over';
 best=Math.max(best,score);
 updateStats(false); updateUI();
 overlayEyebrow text reason==='win'...
 overlay...
 overlayScore...
 overlay.hidden=false;
 demoHint.hidden=true;
}
Cause string.
Maybe gameover self if move into tail allowed so no.
`overlayReason` no.

takeQueuedDirection:
if inputQueue.length return shift
return direction.
But direction object references; clone.
When queue directions ignored? okay.

enqueue:
function enqueueDirection(next) {
 const reference=inputQueue.length?inputQueue[inputQueue.length-1]:direction;
 if(next.x===-reference.x&&next.y===-reference.y) return false;
 if(next.x===reference.x&&next.y===reference.y) return true;
 if(inputQueue.length<6) inputQueue.push({...next});
 return true;
}
If queue full, maybe drop new; quick two honored.
What if queue [up,left] and current direction right; new down opposite up? Reference last left, down is perpendicular, enqueue down. Sequence up,left,down: after up then left then down valid (down opposite up but two steps later, and from left down is valid). Queue [up,left,down]. Good. This honors. Great.
If queue [up] and new down: ref up opposite -> ignore.
No reverse at tick? queued [up,left,down], each adjacent valid. But current right->up valid, up->left valid, left->down valid. no opposite. Good.

AI direction returns one of DIRS not reverse.
`const occupied=new Set(snake.slice(0,-1).map(cellKey));` head included. candidate neighbors.
`blocked` in flood includes sim.slice(1), which may include current head? sim=[candidate,...snake], slice1 old head etc. yes.
floodCount:
const visited=new Set([key(start)]); const queue=[start]; let count=0; while...
Check blocked. Could count start even if blocked (not).
bfsDistance:
if start==target return0; queue with distance. blocked excludes food if food in snake no.
food always outside.
`sim` if eating does not pop tail, so blocked includes tail. good.

Rank:
const area=floodCount(candidate, blocked)
const distance=findDistance(candidate,food,blocked)
const needed=Math.min(10,Math.max(1,sim.length-1));
let rank=area*4 + (distance>=0?800-distance*6:-500)+(eating?120:0);
if(area<needed) rank-=1400;
if(candidate same direction)rank+=.5;
Return max.
Potential if candidate food but area0, rank 120-1400+800=-480; another no path area100 rank -100? -500+400=-100, might avoid food. Fine.
If all areas 0, choose max.

`findDistance` can traverse free huge each candidate (4*576) okay.
We might prefer path distance Manhattan if BFS disconnected. no.

Demo AI may oscillate due no path? If target unreachable because snake body creates wall but open board always reachable around body unless body forms full loop; no.
BFS distance excludes body and candidate; on open board.
No reverse.

UI:
function updateUI() {
 const labels...
 ui.modeText.textContent...
 ui.modeBadge.dataset.state=state;
 ui.lengthValue.textContent=snake.length;
 ui.pauseButtonText.textContent=state==='demo'?'Take over':state==='paused'?'Resume':state==='over'?'Restart':'Pause';
 ui.pauseButton.setAttribute('aria-label',...);
}
If state over pause button says Restart.
Maybe mode badge over text "GAME OVER".
When paused overlay.

Stats:
function updateStats(animate=false) {
 ui.scoreValue.textContent=String(score).padStart(3,'0');
 ui.bestValue.textContent=String(best).padStart(3,'0');
 if animate { class remove; void ui.scoreValue.offsetWidth; add;}
}
best score session if score currently less than best remains.

`scoreValue` inside span; class bump.
ARIA live maybe entire score. Animate only text.

Overlay populate:
function showOverlay(kind,message) {
 ui.overlay.hidden=false;
 if kind pause...
}
end game sets.
Pause:
state='paused'; accumulator=0; overlay...
title Paused; eyebrow "PAUSED"; text "Take a breath. Your run is right where you left it."; score etc; button Resume; shortcut "Press P or Space to resume".
Resume hides.

When reset, overlay.hidden=true.

Game over restart via Space:
if state==='over' resetGame('playing')
if playing space reset? if we choose. Need footer. Let's allow only over and paused; footer maybe "Space / Enter restart" but if playing no effect. Could include text "restart after game over" maybe. Controls footer:
- "Arrows / WASD Move"
- "P Pause"
- "Space / Enter Restart"
If playing space no effect not obvious. Let's implement restart anytime but ignore repeat. It satisfies. However pressing Space in playing is a quick key maybe reset. explicit footer. fine.
`if (state==='playing') resetGame('playing');`
Could accidental while holding space repeat ignored. good.
Enter likewise.
If state paused -> resume (not restart), overlay says resume. footer says restart but context. fine.

Any key in demo:
`if (state==='demo') { if movement startManual(dir); else if k==='p' {startManual(); pauseGame();} else startManual(); return; }`
We should process P separately before generic.

Keydown order:
const movement=DIR_KEYS[k];
if(movement) {prevent; if demo startManual(movement); else if playing && !repeat enqueue; return;}
if(k==='p')...
if(k===' '||k==='enter')...
else if(state==='demo'){startManual();}
This means clicking works.
When state over and arrow, maybe ignore. okay.
When state over and P, pause button? Our P handler if over maybe reset? implement:
if p:
 if demo startManual + pause
 else if playing pause
 else if paused resume
 else if over resetGame('playing')
Good.
Space:
 if over reset playing
 else if demo start
 else if paused resume
 else if playing reset playing (perhaps)
Enter same.

`e.repeat` for space ignore. For p ignore repeat.

Pointer:
canvas.addEventListener('pointerdown', ()=> { if(state==='demo') startManual(); else if(state==='over') resetGame('playing'); });
Maybe clicking canvas in playing shouldn't restart. fine.
document pointerdown? We can window add:
window.addEventListener('pointerdown', (e)=> { if(state==='demo' && !e.target.closest('button')) startManual(); }, {once? not});
But canvas handler plus window would reset twice: canvas event target starts manual, bubbles window sees state playing => no. okay. Any click on page starts. Add on `window` pointerdown:
if state demo and target not button? if button pause handler starts manual; window sees state changed. no issue. Use just `window.addEventListener('pointerdown',()=>{if(state==='demo')startManual();});`. Clicking pauseButton: button click handler runs after pointerdown? pointerdown occurs before click, window listener bubbles after target pointerdown but before click; starts manual, then button click sees state playing and pauses. If user clicked "Take over", it would start then immediately pause! Bad. Exclude buttons:
if(state==='demo' && !e.target.closest('button')) startManual();
Then pause button click handler starts manual no pause. Canvas click triggers window start; no separate canvas listener needed. Overlay hidden. Good.
Clicking score starts manual, fine.
Could pointerdown happen on iframe itself? yes.

`pauseButton.addEventListener('click', e=> { e.stopPropagation(); if demo startManual(); else if playing pause; else if paused resume; else reset; });`
Window pointerdown excluded button.
Overlay button click: if over reset; if paused resume.
Overlay itself maybe pointer no.

`document.addEventListener('visibilitychange', ...)` auto pause when hidden? Could pause manual if hidden. But automated screenshot maybe no. Not needed; if hidden dt clamped.

First call `resizeCanvas(); resetGame('demo'); requestAnimationFrame(loop);`.
ResizeObserver callback after reset no issue.

Potential canvas CSS width at app width calculation depends dvh; size read.
If observer not available fallback.
`window.addEventListener('resize', resizeCanvas);`.

At first canvas maybe width 600 attr and CSS, no issue.

Overlay score result grid:
<div class=result-grid><div><span>Score</span><strong id=overlayScore>000</strong></div> ...</div>
Styles.

SVG logo can use path of snake. Example:
<svg viewBox="0 0 48 48"><rect x=3 y=3 width42 height42 rx14 fill=url...> but gradient IDs. Simpler CSS logo and inline svg path:
<div class=logo-mark><svg ...><path d="M12 30c0 6 5 10 11 10h9c4 0 7-3 7-7s-3-7-7-7H21c-5 0-9-4-9-9s4-9 9-9h8" .../><circle cx=31 cy=10 r=2 fill=#102018/></svg></div>
This path head maybe at top right, eye. nice.
logo-mark background rgba lime.

Header h1 "SNAKE" maybe accent dot.

Stats best with star inline? use `<span class=stat-label>BEST</span>`. star pseudo `::before content:"★"` maybe.

Prompt small. `aria-live=polite`.
Mode badge.

Control footer:
<div class=control><span class=keyset>↑</span>... might clutter. Use:
<span class="keys">↑ ↓ ← →</span><span>or WASD</span>
Could style.
`kbd` tags.

At mobile, footer text hide "or". CSS.

Need code block only.

Let's examine requirement "Grid of at least 20×20 cells, centered on the page." Our app overall centered, canvas centered. yes.
"Arrow keys and WASD control the snake." yes.
"Quick successive key presses must both be honored, but the snake can never reverse onto itself." queue.
"Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake." yes.
"Game over when snake hits wall or itself, with overlay showing score; Space or Enter restarts, P pauses." yes.
"Visual polish smooth movement, distinct head and eyes facing direction, small particle burst." yes.
"current score and best score session." yes.
Auto demo with prompt. yes.
375 responsive no horizontal.

One potential concern auto AI will choose food and score. Great.

Let's consider score initial. best session. On reset manual score0, best retains. If user reloads session resets, as expected.

Let's maybe add subtle status line "AI is playing" and prompt. Screenshot visible.

Let's calculate draw interpolation after tick:
At frame time, accumulator e.g 10ms after step. `previousSnake` old, snake current. alpha=.07. Draw head at old+.07 toward new. Nice.
At reset alpha0 segments same.
During pause accumulator maybe remainder; static.

When step queued direction and previous snake old. Body mapping as earlier. For segment i, previousSnake[i] old segment i. At no growth current len n:
current index1=oldhead, source prev index1=oldseg1. good.
current index2=oldseg1, source prev index2=oldseg2. good.
tail current oldseg(n-2), source prev tail oldseg(n-1). moves. correct.
Head current new, source oldhead. correct.
At growth current len n+1:
current index1=oldhead, source prev index1=oldseg1.
...
current index n-1=oldseg(n-2), source prev index n-1=oldtail.
current index n=oldtail, source missing fallback current (oldtail) stays. correct. Body connector tail stays. Great.
At gameover self current len n+1 [dup,...], previous old len n. index last current oldseg(n-2)? source prev tail oldseg(n-1); moves. Alpha=1. okay.

At collision wall current length n+1 out. alpha1.

Need possible previousSnake and snake length mismatch after reset etc. okay.

Round body segment coordinates include +.5.

Draw head eyes direction chosen even if collision.

Particle draw order maybe after snake. burst over. Fine.

Use context.globalCompositeOperation='lighter' only within save restore.

Floating text:
ctx.font=`800 ${Math.max(11,cell*.55)}px ui-rounded,...`;
ctx.textAlign center; textBaseline middle; strokeStyle rgba dark .5 lineWidth3; strokeText then fill.
At cell25 font13.75.

Dust maybe draw before grid.

Grid lines can use one path. At 24.

Corners:
const m=cell*.55, l=cell*.55; ctx.strokeStyle rgba(200,245,106,.32); lineWidth1.5; four L. Nice.

Vignette radial: createRadialGradient(size*.5,size*.45,size*.1,size*.5,size*.5,size*.72); add 0 rgba transparent,1 rgba(0,0,0,.45).

Board border CSS.

Focus accessibility: canvas tabindex=0 maybe. On click focus. But global key anyway. Add tabindex="0". outline none. Buttons.

Potential browser `ctx.ellipse` latest yes.
`ctx.arcTo` yes.
`ResizeObserver` yes.
`CSS backdrop-filter` supported.

No error from `ctx.roundRect` avoided.
No external.

Let's write final code. Need perhaps avoid JS error `canvas.getContext('2d')` null improbable.
DOM IDs all.

Detailed code.

Let's decide overlay update function:
function showPause() {
 ui.overlayEyebrow.textContent='PAUSED';
 ui.overlayTitle.textContent='Take a break';
 ui.overlayText.textContent='Your run is right where you left it.';
 ui.overlayScore.textContent=pad(score);
 ui.overlayBest.textContent=pad(best);
 ui.overlayButton.textContent='Resume game';
 ui.overlayShortcut.textContent='Press P or Space to resume';
 ui.overlay.hidden=false;
}
Gameover:
eyebrow "RUN COMPLETE"; title "Game over"; text reason; button "Play again"; shortcut "Press Space or Enter to restart".
Win title "Board cleared!".
End state over.
`pauseButton` over says "Restart" and click resets.

Overlay CSS modal width, at mobile.
`aria-hidden` maybe set true/false in helper. We can set ui.overlay.setAttribute.
When hidden true attribute enough.

Stats update `ui.scoreValue.classList` animation. Use `ui.scoreValue.parentElement`? CSS `.stat-value.bump`. fine.
`void ui.scoreValue.offsetWidth;` okay.

Mode badge class? data state.

AI choose if no valid returns direction maybe could wall. But if direction leads wall and no alternatives due body, game over. Fine.
Valid includes current direction if not occupied.
Reverse excluded.
`sameDir(c,d)`.

Flood `blocked` Set includes keys. `floodCount`:
let count=0; queue; visited start.
while idx<queue.length:
 count++ maybe count start. For each neighbors. `blocked.has(key(n))`.
Start may be blocked? new head candidate could equal old tail which popped and not in sim.slice1? If sim [tail, oldhead,...], sim.slice1 doesn't include old tail; candidate start not blocked. okay.
BFS distance similarly.

AI ranking path and area. We should make food path BFS perhaps uses blocked including current candidate? blocked sim.slice1 excludes candidate. good.

Could use `food` null if board full but step only if food. guard.

Manual movement next direction may be null no.
`takeQueuedDirection` returns queued shift or `{...direction}`.
We must ensure queue shift direction relative to committed. yes.

Food never spawn on snake. `spawnFood` uses current snake after growth.

`tickInterval` update in step can set demo and manual same. demo min90. At 3 sec ~21 moves.

Score float text coordinate food at cell center. Burst.

`startManual(initialDirection)` reset and maybe initial direction object. `resetGame` clones.
When manual starts, queue empty.
Prompt hidden.
Status "PLAYING".
Overlay hidden.

Any key in demo e.g "Tab" should not start? We can ignore Tab for accessibility. Keydown handler if k==='tab' return. Also modifier keys only? "press a key" but shift not. Use if(['shift','control','alt','meta','tab'].includes(k)) return.
Need e.ctrlKey etc.
When demo and key 'p' branch starts+pause.
When demo and key direction starts oriented.
When demo and space starts.
Good.

Click page starts. Great.

Could hide prompt once clicked.

Let's include a tiny `aria-live` hidden for state changes? score has role status.

Now style result. Use `font-variant-numeric: tabular-nums`.

Let's consider height at 1280x800. body min-height 800 with padding14 -> content area772. app width620. Topbar max44. margin10. Game card:
padding10 top bottom =20
border2
toolbar height28 + margin7 =35
canvas width: app620 - card content? box-sizing border-box app width620, game card width100% border-box padding10 border1 => inner598. canvas-wrap width598, height598.
card total 20+2+35+598=655.
topbar44+10+655=709. controls margin? footer separate margin-top10 height18 =737. body content 772 so fit with 35 extra. body centers app 737 -> top31.5. Great.
At 375x667: app352.5, topbar40, margin8, card inner330.5, total20+2+29+330=381, footer~31 =>460. plenty.
Good.

At 375 width, canvas cell13.8, snake length4 55 px. visible but small. Grid 24. Fine.
Maybe 20x20 grid would bigger cells but requirement at least; 24 nice. Mobile 24 cells 13.7 each.

Could choose 22x22 to larger, but 24 polished. At desktop cell24.9. fine.

At 375x667 canvas scaled, snake is 55px, acceptable.

Need ensure body has no vertical scrollbar. `overflow:hidden` prevents. Better `overflow-x:hidden`, vertical allowed but no. At tiny heights app fits formula except header + controls: width based height-146 yields total maybe height? width225 -> canvas203 + card~257 + header44+footer28+margin=349, viewport375. okay. At 300 height width154 -> total278 maybe fit. Fine.

CSS min width? none.

Let's draft final code now mentally ensure no stray text outside.

One question: developer says "Output rules Reply with exactly ONE fenced code block containing complete file: ```html ...". So final only code block.

Let's maybe include title and meta viewport.

Let's write full code:

HTML:
main app
header
section
...
</main>

CSS detailed.

JS order: define UI, variables, functions, event listeners, init. Functions hoisted. Dust creation before resize.
Potential `best` score updates from reset? reset doesn't reset best.
`updateStats` called.
`resetGame` uses spawnFood which uses snake.
`state` set before updateUI.
`spawnFood` might return null; initial no.

`resetGame` sets lastTime=performance.now(), but frame loop already running; if reset in event between frames, next dt small. good.
`accumulator=0`.

`showPauseOverlay` state already paused.
`resumeGame`: state playing, accumulator0,lastTime; overlay hidden; updateUI.
`endGame`: state over; if reason win maybe. set `overlay.hidden=false`.
For gameover cause text:
wall: "The wall caught up. Keep your corners this time."
self: "Your tail found the finish line. Give the next run more room."
Could be polished.
Score visible.
`ui.overlayIcon` maybe emoji? Avoid emoji style inconsistency. Use CSS icon circle with text? In modal we can have `<div class=overlay-symbol id=overlaySymbol>×</div>`. For pause show "Ⅱ"; gameover "×"; win "★". JS set textContent. CSS.
Symbol is not just overlay score. Nice.
Use aria-hidden.

When pause overlay symbol "Ⅱ". Great.

`endGame` if reason 'wall'/'self'/'win'.

`updateUI` length snake length.
`modeText` labels.
`pauseButtonText`.

Status badge has nested status-dot, text.
`modeBadge.dataset.state=state`.

Maybe state 'demo' prompt remains; clicking button starts. Good.

Event key:
```
window.addEventListener('keydown', e => {
 if (e.metaKey||e.ctrlKey||e.altKey) return;
 const k=e.key.toLowerCase();
 const dir=KEY_DIRS[k];
 if(dir){ e.preventDefault(); if(state==='demo') startManual(dir); else if(state==='playing'&&!e.repeat) queueDirection(dir); return;}
 if(k==='p'){...}
 if(k===' '||k==='enter'){...}
 if(state==='demo' && !['shift',...].includes(k)) startManual();
});
```
Issue space key `e.key` = " " lower " " okay. Enter "enter".
If state over and generic key no.
For P demo: startManual then pauseGame; `pauseGame` overlay. Fine.
For P over: reset.
For Space playing: reset. Could maybe surprising but footer. e.repeat guard.
For Enter paused resume. playing reset.
For other key demo starts.
If direction in paused ignored; okay.
If direction in over ignored.
Manual queue only not repeat. Quick separate presses not repeat honored.

Potential e.key for uppercase W with shift? shift blocked? e.shiftKey not blocked, k='w', direction. good.

Window pointerdown:
```
window.addEventListener('pointerdown', e => {
 if(state==='demo' && !(e.target instanceof Element) ||...) ...
});
```
`e.target.closest` only if Element. Use `const target=e.target; if(state==='demo' && !(target && target.closest && target.closest('button'))) startManual();`
If click body starts.
Pause button click handler.
`canvas.focus({preventScroll:true})` maybe when start via click? not necessary. Focus could ring hidden outline. CSS outline none. In startManual from pointer we could not.
Key events window irrespective focus.

ResizeObserver:
```
if('ResizeObserver' in window) new ResizeObserver(resizeCanvas).observe(canvas.parentElement);
else window.addEventListener('resize',resizeCanvas);
```
Always add resize too maybe.
`resizeCanvas` sets dpr each time. On observer initial.
When canvas width attr set, parent fixed width.
`cell=size/COLS`.

Canvas context set transform. `ctx.clearRect` after set transform clear CSS region maps backing correct.

Drawing dust uses size normalized.

Mix hex function:
```
const a=parseInt(a.slice(1),16)...` all 6-digit.
```
All hex values six.
Return `rgb(${...})`.

draw body shadow:
ctx.shadowBlur=cell*.45 maybe per segment. shadowOffset 0.
ctx.strokeStyle.
Tail cap.

Maybe body connectors don't show individual square cells; snake looks like smooth tube, polished.

Head shape gradient orientation:
ctx.createLinearGradient(-s/2,-s/2,s/2,s/2)
stops.
Eye pupils.

Head outline lineWidth=Math.max(1,cell*.035).

Maybe draw head forehead small highlight.

Particles:
`p.size` normalized relative cell . etc.
`drawEffects` loops particles; if effects behind maybe after snake. use lighter.
Floaters.

Food burst particles should appear immediately at food old. good.

No need `requestAnimationFrame` for animation pause? still draws.

At first paint after 3 sec, score maybe. Prompt floats.

Let's ensure no body overflow from pseudo. Fine.

One issue `width:min(94vw, 620px, calc(100dvh - 146px));` At viewport 1280x800 gives min1203,620,654=620. At browser doesn't support dvh? latest Chrome supports. Fallback could width:min(94vw,620px); then override with @supports. Fine. We can set `width: min(94vw, 620px); width:min(94vw,620px,calc(100dvh - 146px));` older uses first.
At mobile 375x667 first width352, second min352,620,521=352.
Good.

Use body padding14, mobile8.

Overlay backdrop filter perhaps no error.

Let's write final.