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
1 min 54 s
Cost
$0.022
Output tokens
36,574
Spent thinking
25,814 tokens
Speed
321.1 tok/s
Console errors
0
First token after
526 ms
First visible text after
1 min 34 s
Prompt tokens
655
Code size
32.3 KB
Finish reason
stop
Served by
DigitalOcean
Model id
minimax/minimax-m3.1-flash
Reasoning sent
model default
Max output
524,288 tokens
Generated
Nov 13, 2026

What it was asked

The prompt
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 · 926 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Snake — Neon Arcade</title>
<style>
  :root{
    --bg:#070b14;
    --text:#e8f1ff;
    --muted:rgba(232,241,255,.58);
    --line:rgba(255,255,255,.10);
    --green:#4ade80;
    --cyan:#38bdf8;
    --pink:#ff5d8f;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;
    min-height:100vh;
    padding:16px;
    display:flex;
    align-items:center;
    justify-content:center;
    overflow-x:hidden;
    color:var(--text);
    background:
      radial-gradient(1100px 640px at 50% -14%, rgba(56,189,248,.16), transparent 62%),
      radial-gradient(900px 540px at 92% 110%, rgba(74,222,128,.13), transparent 60%),
      radial-gradient(760px 500px at 4% 84%, rgba(255,93,143,.10), transparent 60%),
      var(--bg);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing:antialiased;
  }
  .app{
    width:min(680px,100%);
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:12px;
    user-select:none;
    -webkit-user-select:none;
  }

  /* ---------- header ---------- */
  .topbar{width:100%;display:flex;align-items:center;justify-content:space-between;gap:14px}
  .brand{display:flex;align-items:center;gap:12px;min-width:0}
  .mark{
    width:44px;height:44px;flex:0 0 auto;border-radius:14px;display:grid;place-items:center;
    background:linear-gradient(140deg,rgba(74,222,128,.28),rgba(56,189,248,.18));
    border:1px solid rgba(255,255,255,.14);
    box-shadow:0 12px 28px -14px rgba(74,222,128,.95), inset 0 1px 0 rgba(255,255,255,.16);
  }
  .mark svg{display:block}
  h1{
    margin:0;
    font-size:clamp(23px,4.4vw,34px);
    font-weight:800;
    letter-spacing:.17em;
    line-height:1;
    background:linear-gradient(100deg,#f2fff9,#7df7bd 42%,#38bdf8);
    -webkit-background-clip:text;background-clip:text;color:transparent;
  }
  .subtitle{margin:4px 0 0;font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--muted)}
  .stats{display:flex;gap:10px;flex:0 0 auto}
  .stat{
    min-width:84px;padding:7px 13px 8px;border-radius:15px;text-align:center;
    background:linear-gradient(180deg,rgba(255,255,255,.075),rgba(255,255,255,.032));
    border:1px solid var(--line);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 12px 26px -20px #000;
    backdrop-filter:blur(7px);
  }
  .stat .label{display:block;font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--muted)}
  .stat .value{display:block;margin-top:1px;font-size:23px;font-weight:800;font-variant-numeric:tabular-nums;letter-spacing:.02em}
  #score{color:#9df7c6}
  #best{color:#93d8ff}
  .bump{animation:bump .38s cubic-bezier(.2,1.5,.35,1)}
  @keyframes bump{0%{transform:scale(1)}32%{transform:scale(1.26)}100%{transform:scale(1)}}

  /* ---------- board ---------- */
  .board{
    position:relative;
    width:min(600px, calc(100vw - 30px), calc(100vh - 214px));
    aspect-ratio:1/1;
    border-radius:24px;
    overflow:hidden;
    background:#0a1120;
    border:1px solid rgba(255,255,255,.11);
    box-shadow:
      0 42px 84px -32px rgba(0,0,0,.9),
      0 0 0 1px rgba(0,0,0,.55),
      0 0 74px -34px rgba(74,222,128,.75),
      inset 0 0 70px -34px rgba(56,189,248,.45);
  }
  .board::after{
    content:"";position:absolute;inset:0;pointer-events:none;z-index:4;
    background:radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.05), transparent 46%);
    mix-blend-mode:screen;
  }
  canvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;z-index:1}
  .board.shake{animation:shake .42s cubic-bezier(.36,.07,.19,.97)}
  @keyframes shake{
    10%,90%{transform:translate(-2px,1px)}
    20%,80%{transform:translate(4px,-2px)}
    30%,50%,70%{transform:translate(-6px,2px)}
    40%,60%{transform:translate(6px,-1px)}
  }

  /* ---------- overlay ---------- */
  .overlay{
    position:absolute;inset:0;z-index:3;display:flex;justify-content:center;align-items:center;
    padding:16px;transition:opacity .26s ease, visibility .26s ease;
  }
  .overlay[data-mode="demo"]{align-items:flex-end;padding-bottom:16px}
  .overlay.hidden{opacity:0;visibility:hidden;pointer-events:none}
  .card{
    width:100%;max-width:400px;text-align:center;padding:18px 20px 20px;border-radius:19px;
    background:rgba(8,13,25,.80);border:1px solid rgba(255,255,255,.14);
    box-shadow:0 28px 64px -26px rgba(0,0,0,.95), inset 0 1px 0 rgba(255,255,255,.13);
    backdrop-filter:blur(13px) saturate(1.25);
    animation:pop .34s cubic-bezier(.2,1.2,.3,1);
  }
  @keyframes pop{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:none}}
  .overlay[data-mode="demo"] .card{max-width:430px;padding:13px 16px 15px;border-radius:16px}
  .overlay[data-mode="demo"] .card h2{font-size:clamp(15px,2.5vw,19px);margin-top:8px}
  .overlay[data-mode="demo"] .card p{font-size:12px;margin-top:4px}
  .overlay[data-mode="demo"] .card .btn{margin-top:10px;padding:8px 16px;font-size:13px}
  .badge{
    display:inline-flex;align-items:center;gap:7px;font-size:9.5px;letter-spacing:.19em;
    text-transform:uppercase;font-weight:700;padding:4px 10px;border-radius:999px;
    background:rgba(56,189,248,.14);border:1px solid rgba(56,189,248,.38);color:#bfe6ff;
  }
  .badge::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--cyan);box-shadow:0 0 9px var(--cyan);animation:blink 1.5s infinite}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
  .badge.danger{background:rgba(255,93,143,.14);border-color:rgba(255,93,143,.42);color:#ffc3d4}
  .badge.danger::before{background:var(--pink);box-shadow:0 0 9px var(--pink);animation:none}
  .badge.warn{background:rgba(255,209,102,.14);border-color:rgba(255,209,102,.42);color:#ffe9b5}
  .badge.warn::before{background:#ffd166;box-shadow:0 0 9px #ffd166;animation:none}
  .card h2{margin:11px 0 0;font-size:clamp(17px,2.6vw,21px);font-weight:800;letter-spacing:.01em;line-height:1.25}
  .card h2 .big{font-size:1.55em;color:#9df7c6;margin-left:2px}
  .card p{margin:7px 0 0;font-size:13px;line-height:1.5;color:var(--muted)}
  .card .hint{margin-top:9px;font-size:11px;color:rgba(232,241,255,.42)}
  .btn{
    margin-top:13px;display:inline-block;appearance:none;border:0;cursor:pointer;font:inherit;
    font-weight:800;font-size:14px;color:#04150e;padding:10px 20px;border-radius:12px;
    background:linear-gradient(135deg,#8df3b3,#22d3ee);
    box-shadow:0 14px 28px -14px rgba(34,211,238,.95), inset 0 1px 0 rgba(255,255,255,.55);
    transition:transform .13s ease, filter .13s ease;
  }
  .btn:hover{transform:translateY(-1px);filter:brightness(1.07)}
  .btn:active{transform:translateY(0)}

  /* ---------- footer ---------- */
  .foot{width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--muted)}
  .keys{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
  .klabel{margin-left:6px;opacity:.75}
  .or{opacity:.5;margin:0 1px}
  kbd{
    display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;
    font:600 11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#dbe7ff;
    background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);
    border-bottom-width:2px;border-radius:6px;
  }
  .status{
    display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:999px;
    background:rgba(255,255,255,.05);border:1px solid var(--line);white-space:nowrap;
  }
  .status .dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green)}
  .status[data-state="demo"] .dot{background:var(--cyan);box-shadow:0 0 10px var(--cyan)}
  .status[data-state="paused"] .dot{background:#ffd166;box-shadow:0 0 10px #ffd166}
  .status[data-state="over"] .dot{background:var(--pink);box-shadow:0 0 10px var(--pink)}

  @media (max-width:520px){
    body{padding:10px}
    .app{gap:9px}
    .subtitle{display:none}
    .mark{width:38px;height:38px;border-radius:12px}
    .stat{min-width:70px;padding:6px 10px 7px}
    .stat .value{font-size:19px}
    .foot{font-size:11px;justify-content:center}
  }
</style>
</head>
<body>
<main class="app">
  <header class="topbar">
    <div class="brand">
      <div class="mark" aria-hidden="true">
        <svg width="24" height="24" viewBox="0 0 24 24">
          <defs>
            <linearGradient id="snakeG" x1="0" y1="0" x2="1" y2="1">
              <stop offset="0" stop-color="#b9ffd8"/><stop offset="1" stop-color="#22d3ee"/>
            </linearGradient>
          </defs>
          <path d="M4.5 17.5c0-2.6 2.1-4.7 4.7-4.7s4.7-2.1 4.7-4.7S11.8 3.4 9.2 3.4"
                fill="none" stroke="url(#snakeG)" stroke-width="2.7" stroke-linecap="round"/>
          <circle cx="9.6" cy="3.6" r="1.15" fill="#062018"/>
        </svg>
      </div>
      <div>
        <h1>SNAKE</h1>
        <p class="subtitle">neon arcade</p>
      </div>
    </div>
    <div class="stats">
      <div class="stat"><span class="label">Score</span><span class="value" id="score">0</span></div>
      <div class="stat"><span class="label">Best</span><span class="value" id="best">0</span></div>
    </div>
  </header>

  <div class="board" id="board">
    <canvas id="game" width="600" height="600" aria-label="Snake game board"></canvas>
    <div class="overlay" id="overlay" data-mode="demo">
      <div class="card">
        <span class="badge" id="badge">AI demo</span>
        <h2 id="ovTitle">Click or press a key to start</h2>
        <p id="ovText">The snake is playing by itself — grab the keys to take over.</p>
        <p class="hint" id="ovHint"></p>
        <button class="btn" id="ovBtn" type="button">Play now</button>
      </div>
    </div>
  </div>

  <footer class="foot">
    <div class="keys">
      <span class="klabel">Move</span><kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd>
      <span class="or">or</span><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>
      <span class="klabel">Pause</span><kbd>P</kbd>
      <span class="klabel">Restart</span><kbd>Space</kbd>
    </div>
    <div class="status" id="statusPill" data-state="demo">
      <span class="dot"></span><span id="statusText">AI demo</span>
    </div>
  </footer>
</main>

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

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d', { alpha: false });
  const COLS = 24, ROWS = 24, CELL = 25, SIZE = COLS * CELL;
  const BASE_STEP = 130, MIN_STEP = 80;
  const DPR = Math.min(window.devicePixelRatio || 1, 2);

  canvas.width = SIZE * DPR;
  canvas.height = SIZE * DPR;
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';

  const el = {
    score: document.getElementById('score'),
    best: document.getElementById('best'),
    overlay: document.getElementById('overlay'),
    badge: document.getElementById('badge'),
    title: document.getElementById('ovTitle'),
    text: document.getElementById('ovText'),
    hint: document.getElementById('ovHint'),
    btn: document.getElementById('ovBtn'),
    status: document.getElementById('statusText'),
    statusPill: document.getElementById('statusPill'),
    board: document.getElementById('board')
  };

  const DIRS = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
  const KEYMAP = {
    ArrowUp:{x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
    w:{x:0,y:-1}, W:{x:0,y:-1}, a:{x:-1,y:0}, A:{x:-1,y:0},
    s:{x:0,y:1},  S:{x:0,y:1},  d:{x:1,y:0},  D:{x:1,y:0}
  };

  let state = 'demo';            // demo | playing | paused | over
  let snake = [], dir = {x:1,y:0}, queue = [], food = null;
  let score = 0, best = 0, acc = 0, elapsed = 0, dead = false, flash = 0;
  let particles = [], ripples = [], popups = [];
  let last = performance.now(), demoTimer = null;

  /* ------------------------------------------------------------------ */
  /* helpers                                                             */
  /* ------------------------------------------------------------------ */
  const clamp = (v,a,b) => v < a ? a : (v > b ? b : v);
  function roundRect(c,x,y,w,h,r){
    r = Math.min(r, w/2, h/2);
    c.beginPath();
    c.moveTo(x+r,y);
    c.arcTo(x+w,y,x+w,y+h,r);
    c.arcTo(x+w,y+h,x,y+h,r);
    c.arcTo(x,y+h,x,y,r);
    c.arcTo(x,y,x+w,y,r);
    c.closePath();
  }
  function bump(node){
    node.classList.remove('bump');
    void node.offsetWidth;
    node.classList.add('bump');
  }
  function setStatus(text, kind){
    el.status.textContent = text;
    el.statusPill.dataset.state = kind;
  }

  /* ------------------------------------------------------------------ */
  /* game setup                                                          */
  /* ------------------------------------------------------------------ */
  function resetGame(){
    const y = Math.floor(ROWS / 2);
    const x0 = Math.floor(COLS * 0.24);
    snake = [];
    for (let i = 0; i < 6; i++){
      snake.push({ x: x0 - i, y, px: x0 - i, py: y });
    }
    dir = {x:1,y:0};
    queue = [];
    score = 0;
    acc = 0;
    dead = false;
    flash = 0;
    particles = []; ripples = []; popups = [];
    food = spawnFood();
    updateHUD();
  }

  function spawnFood(){
    const taken = new Set(snake.map(c => c.x + ',' + c.y));
    const free = [];
    for (let y = 0; y < ROWS; y++){
      for (let x = 0; x < COLS; x++){
        if (!taken.has(x + ',' + y)) free.push({x,y});
      }
    }
    if (!free.length) return null;
    return free[(Math.random() * free.length) | 0];
  }

  function updateHUD(pop){
    el.score.textContent = score;
    el.best.textContent = best;
    if (pop) bump(el.score);
  }

  function startDemo(){
    if (demoTimer){ clearTimeout(demoTimer); demoTimer = null; }
    resetGame();
    state = 'demo';
    setOverlay('demo');
    setStatus('AI demo running', 'demo');
  }

  function startPlayer(){
    if (demoTimer){ clearTimeout(demoTimer); demoTimer = null; }
    resetGame();
    state = 'playing';
    el.overlay.classList.add('hidden');
    setStatus('You are in control', 'playing');
  }

  function pauseGame(){
    if (state !== 'playing') return;
    state = 'paused';
    setOverlay('paused');
    setStatus('Paused', 'paused');
  }
  function resumeGame(){
    if (state !== 'paused') return;
    state = 'playing';
    el.overlay.classList.add('hidden');
    setStatus('You are in control', 'playing');
  }

  function setOverlay(mode){
    el.overlay.classList.remove('hidden');
    el.overlay.dataset.mode = mode;
    el.badge.className = 'badge';
    if (mode === 'demo'){
      el.badge.textContent = 'AI demo';
      el.title.textContent = 'Click or press a key to start';
      el.text.textContent = 'The snake is playing by itself — grab the keys to take over.';
      el.hint.innerHTML = '<kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> to steer &nbsp;·&nbsp; <kbd>P</kbd> pause';
      el.btn.textContent = 'Play now';
    } else if (mode === 'over'){
      el.badge.textContent = 'Game over';
      el.badge.classList.add('danger');
      el.title.innerHTML = 'Score <span class="big">' + score + '</span>';
      el.text.innerHTML = 'Best <b style="color:#93d8ff">' + best + '</b> &nbsp;·&nbsp; Press <kbd>Space</kbd> or <kbd>Enter</kbd> to play again';
      el.hint.textContent = 'Wall or tail? The snake never got the chance.';
      el.btn.textContent = 'Play again';
    } else if (mode === 'paused'){
      el.badge.textContent = 'Paused';
      el.badge.classList.add('warn');
      el.title.textContent = 'Take a breather';
      el.text.innerHTML = 'Press <kbd>P</kbd> or <kbd>Enter</kbd> to resume';
      el.hint.textContent = 'Your run is safe while paused.';
      el.btn.textContent = 'Resume';
    }
  }

  function gameOver(){
    const wasDemo = (state === 'demo');
    dead = true;
    state = 'over';
    if (score > best) best = score;
    updateHUD();
    el.board.classList.remove('shake');
    void el.board.offsetWidth;
    el.board.classList.add('shake');
    if (wasDemo){
      setStatus('AI demo restarting…', 'demo');
      demoTimer = setTimeout(() => { if (state === 'over') startDemo(); }, 1100);
    } else {
      setOverlay('over');
      setStatus('Game over — press Space', 'over');
    }
  }

  /* ------------------------------------------------------------------ */
  /* input                                                               */
  /* ------------------------------------------------------------------ */
  function queueDir(d){
    const lastDir = queue.length ? queue[queue.length - 1] : dir;
    if (d.x === lastDir.x && d.y === lastDir.y) return;      // same direction
    if (d.x === -lastDir.x && d.y === -lastDir.y) return;    // no reversals
    if (queue.length < 6) queue.push({x:d.x, y:d.y});
  }

  window.addEventListener('keydown', (e) => {
    const k = e.key;
    if (k === ' ' || k === 'Spacebar' || k === 'Enter'){
      e.preventDefault();
      if (state === 'demo' || state === 'over') startPlayer();
      else if (state === 'paused') resumeGame();
      return;
    }
    if (k === 'p' || k === 'P'){
      e.preventDefault();
      if (state === 'playing') pauseGame();
      else if (state === 'paused') resumeGame();
      return;
    }
    const d = KEYMAP[k];
    if (!d) return;
    e.preventDefault();
    if (state === 'demo'){ startPlayer(); queueDir(d); return; }
    if (state === 'playing'){ queueDir(d); }
  }, { passive: false });

  el.btn.addEventListener('click', (e) => {
    e.stopPropagation();
    if (state === 'paused') resumeGame(); else startPlayer();
  });
  el.overlay.addEventListener('pointerdown', (e) => {
    if (e.target === el.overlay && (state === 'demo' || state === 'over')) startPlayer();
  });
  canvas.addEventListener('pointerdown', () => {
    if (state === 'demo' || state === 'over') startPlayer();
  });

  /* touch / swipe support */
  let touchStart = null;
  canvas.addEventListener('touchstart', (e) => {
    const t = e.changedTouches[0];
    touchStart = { x:t.clientX, y:t.clientY };
  }, { passive:true });
  canvas.addEventListener('touchend', (e) => {
    if (!touchStart) return;
    const t = e.changedTouches[0];
    const dx = t.clientX - touchStart.x, dy = t.clientY - touchStart.y;
    touchStart = null;
    if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return;
    const d = Math.abs(dx) > Math.abs(dy)
      ? (dx > 0 ? {x:1,y:0} : {x:-1,y:0})
      : (dy > 0 ? {x:0,y:1} : {x:0,y:-1});
    if (state === 'demo') startPlayer();
    if (state === 'playing') queueDir(d);
  }, { passive:true });

  /* ------------------------------------------------------------------ */
  /* AI demo (BFS to the food, with a safe-space fallback)               */
  /* ------------------------------------------------------------------ */
  function aiDirection(){
    if (!food) return null;
    const blocked = new Set();
    for (let i = 0; i < snake.length - 1; i++) blocked.add(snake[i].x + ',' + snake[i].y);
    const head = snake[0];
    const seen = new Set([head.x + ',' + head.y]);
    const prev = new Map();
    const q = [head];
    let found = false;

    while (q.length){
      const cur = q.shift();
      if (cur.x === food.x && cur.y === food.y){ found = true; break; }
      for (const d of DIRS){
        const nx = cur.x + d.x, ny = cur.y + d.y;
        if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) continue;
        const k = nx + ',' + ny;
        if (seen.has(k) || blocked.has(k)) continue;
        seen.add(k);
        prev.set(k, { px:cur.x, py:cur.y, d:{x:d.x, y:d.y} });
        q.push({x:nx, y:ny});
      }
    }

    if (found){
      let k = food.x + ',' + food.y;
      let guard = 0;
      while (prev.has(k) && guard++ < COLS * ROWS){
        const p = prev.get(k);
        if (p.px === head.x && p.py === head.y) return p.d;
        k = p.px + ',' + p.py;
      }
    }
    return fallbackDirection();
  }

  function fallbackDirection(){
    const head = snake[0];
    let bestDir = null, bestSpace = -1;
    for (const d of DIRS){
      if (d.x === -dir.x && d.y === -dir.y) continue;
      const nx = head.x + d.x, ny = head.y + d.y;
      if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) continue;
      if (snake.slice(0, -1).some(c => c.x === nx && c.y === ny)) continue;
      const space = floodSpace(nx, ny);
      if (space > bestSpace){ bestSpace = space; bestDir = d; }
    }
    return bestDir;
  }

  function floodSpace(sx, sy){
    const blocked = new Set(snake.map(c => c.x + ',' + c.y));
    const seen = new Set([sx + ',' + sy]);
    const stack = [[sx, sy]];
    let count = 0;
    while (stack.length){
      const [x, y] = stack.pop();
      count++;
      for (const d of DIRS){
        const nx = x + d.x, ny = y + d.y;
        if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) continue;
        const k = nx + ',' + ny;
        if (seen.has(k) || blocked.has(k)) continue;
        seen.add(k);
        stack.push([nx, ny]);
      }
    }
    return count;
  }

  /* ------------------------------------------------------------------ */
  /* simulation                                                          */
  /* ------------------------------------------------------------------ */
  function stepDuration(){
    return Math.max(MIN_STEP, BASE_STEP - score * 0.85);
  }

  function tick(){
    if (state === 'demo'){
      const d = aiDirection();
      if (d) dir = d;
    } else if (state === 'playing'){
      if (queue.length){
        const nd = queue.shift();
        if (!(nd.x === -dir.x && nd.y === -dir.y)) dir = nd;
      }
    }

    const head = snake[0];
    const nx = head.x + dir.x, ny = head.y + dir.y;

    if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS){ gameOver(); return; }

    const len = snake.length;
    const ate = !!food && nx === food.x && ny === food.y;
    const checkLen = ate ? len : len - 1;              // tail moves away unless growing
    for (let i = 0; i < checkLen; i++){
      if (snake[i].x === nx && snake[i].y === ny){ gameOver(); return; }
    }

    const keep = ate ? len : len - 1;
    const cells = [{x:nx, y:ny}];
    for (let i = 0; i < keep; i++) cells.push({x:snake[i].x, y:snake[i].y});
    const prev = snake.map(c => ({x:c.x, y:c.y}));
    if (ate) prev.push({x:snake[len-1].x, y:snake[len-1].y});
    snake = cells.map((c, i) => ({ x:c.x, y:c.y, px:prev[i].x, py:prev[i].y }));

    if (ate){
      score += 10;
      if (score > best) best = score;
      burst(food.x, food.y);
      food = spawnFood();
      if (!food){ gameOver(); return; }
      updateHUD(true);
      setStatus('Playing · ' + (BASE_STEP / stepDuration()).toFixed(1) + '×', 'playing');
    }
  }

  /* ------------------------------------------------------------------ */
  /* effects                                                             */
  /* ------------------------------------------------------------------ */
  function burst(gx, gy){
    const cx = gx * CELL + CELL / 2, cy = gy * CELL + CELL / 2;
    const palette = ['#ff5d8f','#ffd166','#8affc1','#ffffff','#ff9ecb','#67e8f9'];
    for (let i = 0; i < 20; i++){
      const a = Math.random() * Math.PI * 2;
      const sp = 70 + Math.random() * 210;
      const life = 0.42 + Math.random() * 0.45;
      particles.push({
        x:cx, y:cy,
        vx:Math.cos(a) * sp,
        vy:Math.sin(a) * sp - 55,
        life, max:life,
        size: 1.8 + Math.random() * 3.4,
        color: palette[(Math.random() * palette.length) | 0]
      });
    }
    ripples.push({x:cx, y:cy, life:0.5, max:0.5});
    popups.push({x:cx, y:cy - 4, text:'+10', life:0.95, max:0.95});
    flash = 0.12;
  }

  function updateEffects(dt){
    flash = Math.max(0, flash - dt * 1.1);
    for (let i = particles.length - 1; i >= 0; i--){
      const p = particles[i];
      p.life -= dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vy += 340 * dt;
      p.vx *= (1 - 1.4 * dt);
      if (p.life <= 0) particles.splice(i, 1);
    }
    for (let i = ripples.length - 1; i >= 0; i--){
      ripples[i].life -= dt;
      if (ripples[i].life <= 0) ripples.splice(i, 1);
    }
    for (let i = popups.length - 1; i >= 0; i--){
      popups[i].life -= dt;
      if (popups[i].life <= 0) popups.splice(i, 1);
    }
  }

  /* ------------------------------------------------------------------ */
  /* rendering                                                           */
  /* ------------------------------------------------------------------ */
  function drawBoard(t){
    const g = ctx.createLinearGradient(0, 0, SIZE, SIZE);
    g.addColorStop(0, '#0e1829');
    g.addColorStop(0.5, '#0a1220');
    g.addColorStop(1, '#060b16');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, SIZE, SIZE);

    ctx.fillStyle = 'rgba(160,210,255,0.022)';
    for (let y = 0; y < ROWS; y++){
      for (let x = 0; x < COLS; x++){
        if ((x + y) & 1) ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
      }
    }

    ctx.strokeStyle = 'rgba(140,190,255,0.055)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let i = 1; i < COLS; i++){
      const p = i * CELL + 0.5;
      ctx.moveTo(p, 0); ctx.lineTo(p, SIZE);
      ctx.moveTo(0, p); ctx.lineTo(SIZE, p);
    }
    ctx.stroke();

    if (food){
      const fx = food.x * CELL + CELL / 2, fy = food.y * CELL + CELL / 2;
      const gg = ctx.createRadialGradient(fx, fy, 4, fx, fy, CELL * 3.4);
      gg.addColorStop(0, 'rgba(255,93,143,0.10)');
      gg.addColorStop(1, 'rgba(255,93,143,0)');
      ctx.fillStyle = gg;
      ctx.fillRect(fx - CELL * 3.4, fy - CELL * 3.4, CELL * 6.8, CELL * 6.8);
    }

    const vg = ctx.createRadialGradient(SIZE/2, SIZE/2, SIZE * 0.22, SIZE/2, SIZE/2, SIZE * 0.78);
    vg.addColorStop(0, 'rgba(0,0,0,0)');
    vg.addColorStop(1, 'rgba(0,0,0,0.48)');
    ctx.fillStyle = vg;
    ctx.fillRect(0, 0, SIZE, SIZE);
  }

  function drawFood(t){
    if (!food) return;
    const cx = food.x * CELL + CELL / 2, cy = food.y * CELL + CELL / 2;
    const pulse = 1 + Math.sin(t / 260) * 0.06;

    const glow = ctx.createRadialGradient(cx, cy, 2, cx, cy, CELL * 1.15);
    glow.addColorStop(0, 'rgba(255,93,143,0.34)');
    glow.addColorStop(1, 'rgba(255,93,143,0)');
    ctx.fillStyle = glow;
    ctx.beginPath(); ctx.arc(cx, cy, CELL * 1.15, 0, Math.PI * 2); ctx.fill();

    ctx.save();
    ctx.shadowColor = 'rgba(255,80,130,0.85)';
    ctx.shadowBlur = 15;
    const bg = ctx.createRadialGradient(cx - CELL * 0.1, cy - CELL * 0.12, 1, cx, cy, CELL * 0.33 * pulse);
    bg.addColorStop(0, '#ffb3c8');
    bg.addColorStop(0.45, '#ff5d8f');
    bg.addColorStop(1, '#d81f60');
    ctx.fillStyle = bg;
    ctx.beginPath(); ctx.arc(cx, cy, CELL * 0.3 * pulse, 0, Math.PI * 2); ctx.fill();
    ctx.restore();

    ctx.fillStyle = 'rgba(255,255,255,0.72)';
    ctx.beginPath();
    ctx.ellipse(cx - CELL * 0.1, cy - CELL * 0.12, CELL * 0.085, CELL * 0.05, -0.6, 0, Math.PI * 2);
    ctx.fill();

    ctx.save();
    ctx.translate(cx + CELL * 0.05, cy - CELL * 0.25);
    ctx.rotate(-0.55);
    ctx.fillStyle = '#4ade80';
    ctx.beginPath();
    ctx.ellipse(0, 0, CELL * 0.13, CELL * 0.055, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function strokePath(pts){
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
    ctx.stroke();
  }

  function drawSnake(t){
    if (!snake.length) return;
    const alpha = dead ? 1 : clamp(acc / stepDuration(), 0, 1);
    const pts = snake.map(s => ({
      x: (s.px + (s.x - s.px) * alpha + 0.5) * CELL,
      y: (s.py + (s.y - s.py) * alpha + 0.5) * CELL
    }));
    const head = pts[0], tail = pts[pts.length - 1];

    /* soft shadow */
    ctx.save();
    ctx.translate(2, 5);
    ctx.strokeStyle = 'rgba(0,0,0,0.38)';
    ctx.lineWidth = CELL * 0.72;
    strokePath(pts);
    ctx.restore();

    /* body */
    const g = ctx.createLinearGradient(tail.x, tail.y, head.x, head.y);
    if (dead){
      g.addColorStop(0, '#7f1d1d');
      g.addColorStop(0.6, '#dc2626');
      g.addColorStop(1, '#fca5a5');
    } else {
      g.addColorStop(0, '#12805f');
      g.addColorStop(0.45, '#23b981');
      g.addColorStop(1, '#7ef0b6');
    }
    ctx.strokeStyle = g;
    ctx.lineWidth = CELL * 0.70;
    strokePath(pts);

    /* glossy ridge */
    ctx.save();
    ctx.translate(-1.4, -2.4);
    ctx.globalAlpha = 0.17;
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = CELL * 0.16;
    strokePath(pts);
    ctx.restore();

    /* scales */
    ctx.save();
    ctx.globalAlpha = 0.10;
    ctx.fillStyle = '#062018';
    for (let i = 2; i < pts.length - 1; i += 2){
      ctx.beginPath();
      ctx.arc(pts[i].x, pts[i].y, CELL * 0.075, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();

    drawHead(head, t);
  }

  function drawHead(p, t){
    const h = CELL * 0.47;
    const dx = dir.x, dy = dir.y;
    const px = -dy, py = dx;

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

    ctx.shadowColor = dead ? 'rgba(255,70,70,0.8)' : 'rgba(74,222,128,0.55)';
    ctx.shadowBlur = 17;
    roundRect(ctx, -h, -h, h * 2, h * 2, h * 0.5);
    const hg = ctx.createLinearGradient(-h, -h, h, h);
    if (dead){
      hg.addColorStop(0, '#ffc2c2');
      hg.addColorStop(0.5, '#ef4444');
      hg.addColorStop(1, '#991b1b');
    } else {
      hg.addColorStop(0, '#e4fff1');
      hg.addColorStop(0.45, '#6ee7b7');
      hg.addColorStop(1, '#12a870');
    }
    ctx.fillStyle = hg;
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,0.42)';
    ctx.lineWidth = 1.4;
    ctx.stroke();

    /* eyes */
    for (const s of [1, -1]){
      const ex = px * h * 0.42 * s + dx * h * 0.16;
      const ey = py * h * 0.42 * s + dy * h * 0.16;
      ctx.fillStyle = '#fbfffd';
      ctx.beginPath();
      ctx.ellipse(ex, ey, h * 0.27, h * 0.30, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#062018';
      ctx.beginPath();
      ctx.arc(ex + dx * h * 0.12, ey + dy * h * 0.12, h * 0.135, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = 'rgba(255,255,255,0.92)';
      ctx.beginPath();
      ctx.arc(ex + dx * h * 0.16 - px * h * 0.04, ey + dy * h * 0.16 - py * h * 0.04, h * 0.05, 0, Math.PI * 2);
      ctx.fill();
    }

    /* little tongue flick */
    if (!dead && Math.sin(t / 150) > 0.62){
      const bx = dx * h * 0.9, by = dy * h * 0.9;
      const ex2 = bx + dx * h * 0.42, ey2 = by + dy * h * 0.42;
      ctx.strokeStyle = '#ff5d8f';
      ctx.lineWidth = 2.1;
      ctx.beginPath();
      ctx.moveTo(bx, by);
      ctx.lineTo(ex2, ey2);
      ctx.moveTo(ex2, ey2);
      ctx.lineTo(ex2 + dx * h * 0.16 - dy * h * 0.16, ey2 + dy * h * 0.16 + dx * h * 0.16);
      ctx.moveTo(ex2, ey2);
      ctx.lineTo(ex2 + dx * h * 0.16 + dy * h * 0.16, ey2 + dy * h * 0.16 - dx * h * 0.16);
      ctx.stroke();
    }
    ctx.restore();
  }

  function drawEffects(){
    for (const r of ripples){
      const k = 1 - r.life / r.max;
      ctx.strokeStyle = 'rgba(255,150,185,' + ((1 - k) * 0.55).toFixed(3) + ')';
      ctx.lineWidth = 3 * (1 - k) + 0.6;
      ctx.beginPath();
      ctx.arc(r.x, r.y, CELL * 0.25 + k * CELL * 1.1, 0, Math.PI * 2);
      ctx.stroke();
    }
    for (const p of particles){
      const a = clamp(p.life / p.max, 0, 1);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * (0.35 + 0.65 * a), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    for (const p of popups){
      const a = clamp(p.life / p.max, 0, 1);
      ctx.globalAlpha = a;
      ctx.font = '800 19px ui-sans-serif, system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#ffd166';
      ctx.shadowColor = 'rgba(0,0,0,0.65)';
      ctx.shadowBlur = 7;
      ctx.fillText(p.text, p.x, p.y - (1 - a) * 28);
      ctx.shadowBlur = 0;
    }
    ctx.globalAlpha = 1;
  }

  function render(t){
    ctx.clearRect(0, 0, SIZE, SIZE);
    drawBoard(t);
    drawFood(t);
    drawSnake(t);
    drawEffects();
    if (flash > 0){
      ctx.fillStyle = 'rgba(255,255,255,' + flash.toFixed(3) + ')';
      ctx.fillRect(0, 0, SIZE, SIZE);
    }
  }

  /* ------------------------------------------------------------------ */
  /* main loop                                                           */
  /* ------------------------------------------------------------------ */
  function frame(now){
    const dt = Math.min((now - last) / 1000, 0.05);
    last = now;
    elapsed += dt;

    if (state === 'playing' || state === 'demo'){
      acc += dt * 1000;
      let dur = stepDuration();
      let guard = 0;
      while (acc >= dur && guard++ < 5){
        acc -= dur;
        tick();
        dur = stepDuration();
        if (state === 'over'){ acc = 0; break; }
      }
    }

    updateEffects(dt);
    render(elapsed);
    requestAnimationFrame(frame);
  }

  startDemo();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (25,814 tokens)
We need produce exactly one fenced code block with full HTML file. Need build polished snake game canvas, vanilla JS, requirements. Need carefully implement.

Let's plan thoroughly to make impressive visual. Single HTML file, inline CSS/JS, no external resources. Canvas grid at least 20x20, centered page. Handle rapid keys queue, no reversal. Food +10 grows one cell. Food never on snake. Game over overlay score; Space/Enter restarts, P pause. Smooth movement, distinct head + eyes facing direction, particle burst. Score and best of session (session best variable, no storage). Self-playing demo mode behind "Click or press a key to start" message — instruction says games must start in self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so judge can see action without playing. We'll implement demo AI that plays automatically (greedy toward food with safety checks), with overlay/instruction panel "Click or press a key to start". After a click or key press, actual player takes control. Demo mode should be visible and the snake moves. Canvas on first paint showing demo game in action, with a start banner.

Need handle keyboard: Arrow keys and WASD control. Quick successive key presses both honored — implement an input queue of directions; process one per movement tick so quick presses both honored, and never allow reversal onto itself. The direction queue: when key pressed, push direction if not equal to last queued direction and not opposite of last queued direction (compare against last queued). That honors successive turns (e.g., up then left before tick). But a subtlety: if queue has [up, left] and we press down — reverse of current direction up? The snake's head currently moving up; down would be reversal but it's after up in the queue which has not been applied yet. We compare against last queued direction (up), so down rejected. Good. But we must also ensure that the direction applied at tick is legal relative to actual current direction (no reverse). Since queue validation ensures first queued direction was validated against current dir, subsequent validated against previous queued. However the actual current dir changes when applying. We should also check at pop time: if candidate is opposite of current dir, skip it. And maintain queue max length maybe 3? "Quick successive key presses must both be honored" so keep queue maybe up to 4-6. We need not lose presses. Use unbounded-ish cap of 6? Better allow queue of, e.g., 8; but successive key presses both honored. Let's use queue array, and push if not duplicate and not reverse of last queued OR if queue empty compare to current dir. Cap at like 4? Maybe don't cap but avoid same-direction spam: only push if different from last queued. That honors turns.

Smooth movement: interpolate head/body positions between grid steps. Use continuous time-based grid movement with a fixed step (e.g., 130ms) and interpolation factor alpha = accumulator/step. Render snake body segments interpolated. Approach: keep snake as array of grid cells {x,y} with head at index 0. Maintain prevPositions? For smooth movement, store for each segment its previous grid cell. When moving, new snake = [newHead, ...snake]; each segment's previous position is its old cell. Render with interpolation between previous position and current position. For interpolation, the head travels from old head cell to new head cell; the body follows. So maintain segments with px,py (previous) and x,y current. On each move: newHead = head + dir; create array: for i in 0..snake.length-1: newCell = (i===0? newHead : old[i-1]); prev = old[i]. Then render each cell at lerp(prev, current, t). But note when snake grows, we add tail duplicate: old snake length L, new length L+1: new cells = [newHead, ...oldSnake] length L+1; prev positions: head prev = old[0], body i prev = old[i-1] for i<=L-1, tail i=L prev = old[L-1]. Good — the tail doesn't move on growth; its current == prev so it stays put while growing, which visually looks right (tail stays while new segment extends). Actually with that scheme the body shifts forward and the last segment (old tail) remains, and new length has both old tail and one more? Let's compute: old = [h0,h1,...,hL-1]. new = [h1', ...]. Wait, new array: [newHead, old[0], old[1], ..., old[L-1]] → length L+1. prev for index 0 = old[0]; index 1 = old[0]; index 2 = old[1]; ... index L = old[L-1]. Current index 1 = old[0] (body), index L = old[L-1] (tail). So we have current cells [newHead, h0, h1, ..., hL-2]? No wait old[L-1] is last. new = [newHead, h0, h1, ..., hL-2, hL-1] length L+1. Right, index L current = hL-1, and prev = hL-1 (since prev index L = old[L-1]). Hmm then the new segment is not a new position but the old tail retained, and the body segment hL-2 moved to hL-1? Let's re-derive standard: moving without growth: new snake = [newHead, ...oldSnake.slice(0, L-1)] (drop tail). With growth: new snake = [newHead, ...oldSnake] (keep tail). Then prev positions for interpolation: for i=0: prev = old[0] (old head); for i>=1: prev = old[i-1]. So tail at index L (last) has prev = old[L-1] and current = old[L-1] — it doesn't move, correct for growth (tail stays in place while head extends). And index L-1 has prev old[L-2], current old[L-1] — moves forward. Good. So body visually shifts and tail stays, which is standard growth look.

For rendering smooth: t = accumulator/step, each segment pos = lerp(prev, current, t). Head at lerp(oldHead, newHead, t). The snake's cells are cell-aligned at the start of a tick? Actually at t=0 head is at old head cell, and at t→1 it reaches new head cell. Fine.

But better: fixed step with accumulator, render at requestAnimationFrame. Render positions computed continuously.

Distinct head with eyes facing direction: draw head as rounded rect bigger, brighter color, eyes offset in direction, with pupils. Body segments rounded with gradient, maybe scale taper. Add subtle glow/shadow.

Particles: on eating, burst of particles at food cell center with colors, fading. Use array of particles with velocity, life.

Grid: 24x24 cells perhaps. Canvas logical size 24*cell with cell 24 => 576px. Centered. Responsive: CSS canvas width min(88vmin, 620px), aspect ratio 1. Use canvas internal resolution scaled by devicePixelRatio for crispness. Simpler: set canvas width/height attributes to grid * cell (e.g., 24*26=624) and let CSS scale; but retina blurry. Better compute internal size = CSS size * dpr and scale context. But grid drawing uses fixed units. We can set up: const COLS=24, ROWS=24, CELL=26; logical canvas 624x624. Then CSS style width: min(624px, calc(100vw - 32px), calc(100vh - 210px)) maybe. To keep it responsive and centered with header. On 1280x800, canvas around 560-600px. Let's use CSS: canvas { width: min(92vmin, 600px, calc(100vw - 28px)); } height auto. Hmm vmin at 800 height = 736, 92vmin = 677 → min 600. At 375 wide: 92vmin where vmin=375 → 345; min(345,600,347)=345. Good. But we also need height constraint for small screens with header; use `width: min(600px, calc(100vw - 24px), calc(100vh - 200px));` At 1280x800: min(600, 1256, 600)=600. At 375x667: min(600,351,467)=351. Good. At 375x600: min(600,351,400)=351.

Actually use aspect-ratio 1 and display block.

Canvas internal: set width = 600 * dpr? But CSS size may vary; the canvas drawing coordinate system can be set to logical 600x600 with ctx.setTransform(scale...). Let's define LOGICAL = 600 (600/24 = 25 px per cell). Set canvas.width = 600*dpr, canvas.height = 600*dpr, CSS width responsive; then set ctx.setTransform(dpr,0,0,dpr,0,0). But if CSS width is 351, the canvas internal 600*dpr scaled down — fine, still crisp if dpr 2 (1200 internal). Actually the internal resolution independent of CSS size; 600 logical units mapped to 351 CSS pixels means more logical pixels per CSS px → very crisp. Fine.

Alternatively recompute on resize to match CSS size for pixel-perfect. Simpler approach: set canvas width/height = 600*dpr once (and re-evaluate dpr maybe). Use ctx.setTransform(dpr,...). Grid lines drawn in logical units.

Design: Dark modern theme, deep navy gradient background, subtle animated background (CSS radial gradients). Panel with title "SNAKE" styled with gradient text, score cards. Canvas in a "device" frame with rounded corners, border, box-shadow, inner glow. Overlay on canvas for start (frosted panel) and game over / pause.

Overlay: Use a div absolutely positioned over canvas (inside .board). HTML overlay with title, text, button maybe. Requirement: overlay showing score on game over; Space or Enter restarts, P pauses. Start message "Click or press a key to start". We can make overlay with a button "Start game" that triggers start. Clicking the canvas also starts.

Game states: 'demo', 'playing', 'paused', 'over'.

Demo AI: moves snake toward food but safely. Implement simple greedy with safety check: candidate directions that aren't reverse; compute resulting head position; safe if in bounds and not in body; prefer direction minimizing distance to food; tie-break with path safety (flood fill reachability). If no safe move, choose any non-reverse; if trapped → game over in demo? Better make demo robust: use BFS pathfinding from head to food on grid avoiding body, follow the first step of the shortest path. That's a solid AI and will play well; it will survive long. Implement BFS each tick (24x24 = 576 cells, cheap). Body length grows so BFS avoids body. It can still trap itself but with BFS shortest path it's usually fine. Add fallback: if BFS fails, choose safe direction maximizing free space (flood fill count).

But the demo would eat a lot and the score would climb, making "best score" high before start — maybe keep demo score separate? The requirement says display current score and best score of the session. If demo inflates the score, that's odd but "session" best. Hmm. Maybe demo mode plays but doesn't count toward player score? Then best score starts at 0 when player starts. I think nicer: demo has its own visible score maybe labeled "DEMO". But requirement says display current score and best of session. I'll keep the score HUD reflecting the player's game; during demo show "—" for score? That may look less polished. Alternative: demo increments score and best score naturally; on start, reset current score to 0 and keep best (session best includes demo). Many judge screenshots like seeing a score. Let's make demo count too: score increments in demo and best updates. When player starts, current score resets to 0 but best remains (session best across demo+player). Hmm, "current score" resets on restart anyway. I'll label the HUD "SCORE" and "BEST". During demo, score shows demo score; when player takes over, reset to 0 and best stays. That's reasonable.

Actually careful: the judge might press keys and see demo score reset; fine.

Pause: P toggles pause while playing; overlay "Paused" with "Press P to resume". Also pause on window blur? Optional, but avoid weirdness. I'll pause automatically when the tab loses focus, showing paused overlay, but that might interfere with screenshot? No, screenshot happens in active page. I'll skip auto-pause on blur or do it—it could cause confusion in sandbox where iframe loses focus. Better to not auto-pause.

Game over: when head hits wall or body. Show overlay with final score, best, "Press Space or Enter to play again". Also handle the snake's body collision: moving into the tail cell is allowed if the tail moves away (standard). Since growth only when eating, compute new body occupancy = snake minus tail (if not growing) plus newHead; collision if newHead equals any of snake[0..len-2] (when not growing) or entire snake when growing. Simpler: check against snake.slice(0, snake.length - (growing?0:1)).

Input queue honoring rapid presses: implement queue of direction objects; at each tick, shift one and apply. Also, if the player presses a key during demo → start the game and apply that direction.

Movement speed: maybe increase slightly as score grows? Keep base step ~125ms, min 78ms as score increases. Smooth.

Particles: spawn ~14 particles on eating with random angles, speed, life, colors (food color + white). Also a subtle "pop" ring. And floating "+10" text.

Food rendering: apple-like? A glowing orb/berry with highlight and pulsing. Let's do a small circle with gradient, glow, leaf. Could draw a stylized fruit: circle radius ~0.32 cell in coral/red with highlight and tiny green leaf. But "food" generic is fine — glowing pink orb.

Grid: subtle lines, and maybe a vignette. Background of board: dark gradient, subtle checkerboard.

Head: rounded square in a brighter green/teal, with eyes. Eyes: two white ellipses positioned based on direction, pupils dark offset in direction. Need head orientation: draw eyes relative to direction vector. Body color gradient from bright (near head) to darker (tail). Also maybe draw a slight scale taper: body segment radius decreasing along length.

Snake rendering with rounded rects: use roundRect (supported in Chrome). Provide fallback path helper in case? Chrome latest supports ctx.roundRect. Fine, but I'll write my own roundRect helper for safety.

Visual polish: glow using shadowBlur on head and food. Add a subtle motion trail? Not needed.

Overlay styling: .overlay with backdrop-filter blur, semi-transparent, centered card. Start overlay shows "SNAKE" title? Actually the header has the title. The overlay shows "READY?" / "Click or press a key to start", plus a small hint about controls, and an animated "press" cue. In demo mode behind the overlay, the AI snake is playing — so overlay should be not too opaque so you can see the demo. Use a translucent panel in the center with blur but board visible around. Perhaps the overlay card sits at center with rgba(8,12,24,0.72) and the board dimmed by 20%.

Hmm, the demo snake moving behind a centered card is partially hidden. Alternative: put the start prompt as a banner at the top of the board (not full center), so the demo is visible. But the spec says "behind a 'Click or press a key to start' message" — a banner works. But polished approach: show a compact pill near the top of the board: "Click or press a key to start" with a glowing border, plus a small "DEMO" badge in the corner. I'll implement overlay as a centered card but with a smaller footprint and the demo snake visible around; or position the overlay card at bottom center? Let me do: overlay panel centered but sized ~ 320px wide, board 600 → the snake can still be seen around the edges. Actually to maximize visibility of the demo, I'll place the start prompt in the lower portion of the board (like a floating glass pill at bottom), and a small "AI DEMO" badge in the top-left corner of the board. Then when the game starts, the pill fades out.

But game-over overlay should be a centered card showing score — that's fine.

I'll implement a single overlay element whose content changes by state, with CSS classes for positioning: for 'demo' state, the overlay is a bottom bar (align-items: flex-end) with padding-bottom; for 'over'/'paused', centered card.

Let me design the overlay HTML:

```html
<div class="overlay" id="overlay" data-mode="demo">
  <div class="card" id="card">
    <div class="badge" id="badge">AI DEMO</div>
    <h2 id="ovTitle">Snake is playing…</h2>
    <p id="ovText">Click or press a key to start</p>
    <div class="hint" id="ovHint">↑ ↓ ← → / W A S D to steer · P to pause</div>
    <button id="ovBtn">Start game</button>
  </div>
</div>
```

For demo mode, we might use a compact layout: hide title? Keep badge "AI DEMO", text "Click or press a key to start", button "Take control". Position bottom.

Hmm, but "Click or press a key to start" should be prominent. Let's make the demo card: badge "AI DEMO", big line "Click or press a key to start", button "Play now →", hint keys.

Position bottom center of the board for demo.

When the user clicks the button, start the game (and stop propagation).

Also clicking anywhere on the canvas in demo mode starts the game.

When playing: overlay hidden (opacity 0, pointer-events none).

Game over overlay: centered card with "GAME OVER", "Score 120 · Best 240", "Press Space or Enter to restart", button "Play again".

Paused overlay: "PAUSED", "Press P to resume", button "Resume".

Score HUD above the board: two stat cards: SCORE (value) and BEST (value). On the right maybe a "demo/playing" status pill and speed indicator. Let's arrange header: left title "SNAKE" with subtitle "classic arcade"; right: score/best cards. Below: board. Below board: footer hints (controls). Keep everything within 800px height.

Layout math: 1280x800. Header ~70px, board 600, footer ~36, gaps ~30 → 736. Fits.

Use CSS variables, nice fonts: system font stack (no external fonts). Use 'Segoe UI', system-ui etc. Title with letter-spacing and gradient.

Let me now write the code.

Canvas drawing details:

Constants:
```js
const COLS = 24, ROWS = 24;
const CELL = 25;
const SIZE = COLS * CELL; // 600
```

State:
```js
let snake, dir, nextDirs, food, score, best, state, acc, lastTime, stepMs, particles, popups, shake, demoTimer...
```

Snake array of {x,y,px,py}. Initialize for demo: snake = cells along the middle row going right, length 5, head at right? Let's place head at (7,12) moving right with body trailing left: [{x:7,y:12}, {x:6,y:12},...{x:3,y:12}]. Direction right.

Food placed randomly on free cells.

Demo AI BFS:
```js
function demoDirection(){
  // BFS from head to food avoiding body cells (excluding tail which will move)
  const blocked = new Set(snake.slice(0, snake.length - 1).map(c=>c.x+','+c.y));
  // also treat tail as blocked? tail moves away so it's fine to pass; but for path simplicity block all body except tail
  ...
}
```
BFS: from head cell, explore neighbors not blocked and in bounds, find path to food; return first step direction. Since snake occupies grid, head's neighbors may be limited.

If BFS finds food: dir = first step. Else choose a safe move: among non-reverse dirs, in-bounds and not colliding, pick one that maximizes flood-fill reachable area (to avoid trapping) — simpler: pick one that moves toward the center / away from walls. Implement simple heuristic: choose the candidate with the largest free-space count via BFS from the candidate.

Implementation of BFS over 24x24 with a queue array of ints.

To ensure the demo keeps eating, the BFS path around its body works well.

Demo speed: same step as the game.

Also, ensure food never spawns on snake: pick random free cell; if none free (snake fills board) → win state; handle by showing "PERFECT!" game over-ish. Edge case; handle gracefully.

Now input handling:

```js
const KEYMAP = {
  ArrowUp: {x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
  w:{x:0,y:-1}, W:..., a/s/d
};
window.addEventListener('keydown', e => {
  const k = e.key;
  if (k === ' ' || k === 'Enter') { e.preventDefault(); if (state==='over'||state==='demo') startGame(); else if (state==='paused') resume? } 
  ...
});
```

Spec: Space or Enter restarts on game over. Space/Enter during play maybe also restart? I'd say during play, Space/Enter could pause? Not required. Let's make Space/Enter during playing do nothing (or pause toggle). Keep it simple: Space/Enter: if over → restart; if demo → start; if playing → ignore (prevent page scroll).

P toggles pause (only when playing/paused). Also 'p'/'P'.

Arrow/WASD during demo → start the game and enqueue direction. During playing → enqueue.

Key repeat: ignore e.repeat for the same direction? Actually holding a key generates repeats; since we skip duplicates of the last queued direction, repeats are harmless.

Queue push logic:
```js
function queueDir(d){
  const last = nextDirs.length ? nextDirs[nextDirs.length-1] : dir;
  if (d.x === last.x && d.y === last.y) return;      // duplicate
  if (d.x === -last.x && d.y === -last.y) return;    // reversal of queued/current
  nextDirs.push(d);
}
```
But careful: If nextDirs empty and dir = right, and player presses up then left quickly: up pushed (not opposite of right) ✓; left pushed: last=up, left is not opposite of up ✓ → both honored. Then at tick 1: pop up → dir=up; tick2 pop left → dir=left. 

Another subtlety: if the player presses up then right while moving right: up pushed; then right: last=up, right isn't opposite of up → queued. So snake goes up then right — honoring both. Fine.

At pop time, verify candidate is not opposite of current dir (shouldn't happen given validation, but if dir changed due to something). Also avoid popping two opposite in the same tick — only one per tick.

Potential issue: queue grows unbounded with alternating keys; cap at maybe 5 and drop? Requirement says quick successive presses both honored. I'll cap at 6 (drop oldest? no—drop newest?). I'll allow up to 8 and just ignore beyond; fine.

Also we need to make sure the very first key press starts the game with that direction applied immediately (queue it, and the first tick uses it).

Movement tick:

```js
function step(){
  if (nextDirs.length){
    const nd = nextDirs.shift();
    if (!(nd.x === -dir.x && nd.y === -dir.y)) dir = nd;
  }
  const head = snake[0];
  const nx = head.x + dir.x, ny = head.y + dir.y;
  if (nx<0||nx>=COLS||ny<0||ny>=ROWS) return gameOver();
  const ate = (nx===food.x && ny===food.y);
  const bodyToCheck = ate ? snake : snake.slice(0, snake.length-1);
  for (const c of bodyToCheck) if (c.x===nx && c.y===ny) return gameOver();
  // build new snake
  const newSnake = [{x:nx,y:ny,px:head.x,py:head.y}];
  const keep = ate ? snake.length : snake.length - 1;
  for (let i=0;i<keep;i++){
    newSnake.push({x:snake[i].x, y:snake[i].y, px:snake[i].x, py:snake[i].y});
  }
  // wait: interpolation mapping
}
```

Hmm, let me redo the prev mapping: new current cells = [newHead, snake[0], snake[1], ..., snake[keep-1]] where keep = snake.length (ate) or snake.length-1 (not ate). Previous positions: newCell[0].prev = snake[0] (old head); newCell[i].prev = snake[i-1] for i>=1. So build:

```js
const newSnake = [{x:nx,y:ny,px:head.x,py:head.y}];
for (let i=0;i<keep;i++){
  newSnake.push({x:snake[i].x, y:snake[i].y, px:snake[i].x, py:snake[i].y});
}
```
Here for i-th new body cell (index i+1), its current = snake[i], and we want prev = snake[i-1]. But the code sets px=snake[i].x → prev equals current, meaning no interpolation for body! That's wrong.

Let me set prev explicitly: newSnake[1] (index 1) current = snake[0], prev should = snake[0].prev? Hmm. Let's think about motion: The head moves from old head cell h0 to h1. The segment that was the head (old head cell h0) becomes body segment index 1; it should be located at h0 at t=1 and at its previous location at t=0. Its previous location was h0's own previous cell (snake[0].px, snake[0].py), because before the step the head was interpolating from its own prev to h0. Hmm, careful with the interpolation model.

Model: Each segment object has a current cell and the cell it came from (prev). At time t within a step, position = lerp(prev, current, t). After a step, the head moves to newHead: its prev = old head's current cell (h0)? Or old head's prev?

Consider: at the end of step N (t=1), head is exactly at h0. During step N+1, the head travels from h0 to h1. So head's prev = h0 and current = h1. At the start of step N+1 (t=0), head is drawn at h0 — matching the end of step N. Good.

Now body: index 1 during step N+1 is the old head segment; it travels from where it was at the end of step N to where the head was at the end of step N (h0). At end of step N, index1 current = old index0's current = h0? Wait at end of step N, the array is [h0, h1?]... I'm confusing myself.

Let me define it cleanly with a queue of positions: The snake occupies a list of grid cells. Each animation step, we insert a new head cell and possibly remove the tail. The rendering should show the whole snake translating forward smoothly.

Simplest robust approach: keep the snake as a list of grid cells (head first) plus a parallel "prevCells" list representing the positions at the start of the current interpolation (t=0). At each step:
- newCells = [newHead, ...oldCells.slice(0, keep)]
- newPrev = oldCells (the entire old list) — because at t=0 the body should be at old cells, and at t=1 at newCells.

Check lengths: newCells.length = 1 + keep. oldCells.length = L. If keep = L (ate): newCells length L+1, newPrev length L → mismatch. We need prev for the extra segment: the extra (last) segment is the old tail which doesn't move → prev = its current = oldCells[L-1]. So newPrev = [...oldCells, oldCells[L-1]] length L+1 ✓.

If keep = L-1 (not ate): newCells length L, newPrev = oldCells length L ✓. Body segment i (index i in newCells) moves from oldCells[i] to newCells[i] = oldCells[i-1] (for i>=1). Wait newCells[i] = oldCells[i-1] for i=1..keep. So segment i goes from oldCells[i] to oldCells[i-1] ✓ — moves one forward. Head goes from oldCells[0] to newHead ✓.

For the ate case: newCells = [newHead, old0, old1, ..., oldL-1]; newPrev = [old0, old1, ..., oldL-1, oldL-1]. Segment i (i>=1): from newPrev[i] = old[i-1] to newCells[i] = old[i-1]?? Hmm: newCells[1]=old0, newPrev[1]=old1 → segment 1 moves from old1 to old0 ✓. newCells[i]=old[i-1], newPrev[i]=old[i] (for i <= L-1) ✓. newCells[L]=old[L-1], newPrev[L]=old[L-1] → the last segment stays ✓.

So the rule: newPrev = [...oldCells]; if ate, push a copy of old tail. And newCells = [newHead, ...oldCells.slice(0, keep)] with keep = L if ate else L-1.

So represent snake as array of {x,y} current cells and prev array of {x,y}. I'll store each cell as {x,y,px,py} where px,py is the interpolation start.

Implementation:
```js
const old = snake;
const keep = ate ? old.length : old.length - 1;
const newCells = [{x:nx,y:ny}];
for (let i=0;i<keep;i++) newCells.push({x:old[i].x,y:old[i].y});
const newPrev = old.map(c=>({x:c.x,y:c.y}));
if (ate) newPrev.push({x:old[old.length-1].x,y:old[old.length-1].y});
snake = newCells.map((c,i)=>({x:c.x,y:c.y,px:newPrev[i].x,py:newPrev[i].y}));
```
Good.

Note: at t=0 the head is drawn at old head cell (px=old[0]) and at t=1 at new head. But wait — at the end of the previous step, the head was drawn at old[0] exactly, so continuity ✓.

Rendering: pos = prev + (current - prev) * ease(t). Use linear or slight ease-out? Linear with smooth time is fine; use easeInOutQuad? Linear is the classic smooth snake. I'll use a slight easing (t) maybe linear to keep the movement consistent with collision timing. Use linear.

But there's a subtlety: the head's eye direction should be based on current dir, which is fine.

Collision: computed at step boundaries in grid cells. With interpolation, the head visually crosses the wall slightly before the step? At t=1 the head is at the wall cell; the game-over triggers at the step, so it's fine.

Timing: use requestAnimationFrame loop with accumulator:
```js
let last = performance.now();
function frame(now){
  const dt = Math.min(now-last, 100); last = now;
  if (state==='playing' || state==='demo'){
    acc += dt;
    const stepDur = currentStep();
    while (acc >= stepDur){ acc -= stepDur; tick(); if (state==='over') {acc=0; break;} }
  }
  render(now);
  requestAnimationFrame(frame);
}
```
Note: step duration changes with score; the accumulator handles it.

Rendering interpolation t = acc / stepDur.

Also on game over, maybe flash the snake red and shake the board slightly.

Board shake: apply a CSS transform translate on the canvas wrapper for a brief shake on game over. Implement via class toggling and CSS animation.

Score HUD: update text.

Particles: array of {x,y,vx,vy,life,maxLife,size,color}. Update per frame with dt (in seconds). Draw as circles with alpha.

Floating text popups: {x,y,text,life}.

Food draw: 
```js
function drawFood(time){
  const cx = food.x*CELL+CELL/2, cy = ...;
  const pulse = 1 + Math.sin(time/300)*0.06;
  // glow
  ctx.shadowColor = '#ff5d8f'; ctx.shadowBlur = 18;
  gradient circle radius CELL*0.3*pulse
  // highlight small white circle
  // little leaf
}
```
Make the food a strawberry-ish berry: red circle + green leaf at top + seeds? Cute. Let's draw a glowing berry: main circle red-orange gradient, white highlight, small green leaf rotated. Good.

Snake body draw: iterate from tail to head so head is on top. For each segment i: 
- pos = interpolated cell center.
- radius/inset: body inset = CELL*0.10 + (i/len)*CELL*0.06? Let's taper: inset grows with index so tail segments are smaller.
- color: lerp between head color (#7CFFB2? ) and tail (#1f9e6f). Use HSL interpolation: head hue 145, tail hue 165? Let's use colors: head '#8affc1', mid '#3ddc97', tail '#1f9d74'.
- rounded rect of size (CELL - 2*inset).
- add a subtle inner highlight (lighter top-left) and a stroke.

Draw connecting segments? Since segments move one cell per step and are adjacent, rounded squares of size ~CELL*0.86 will slightly overlap at corners only; along straight lines they'd have small gaps if inset too large. Use body square size = CELL*0.86 (inset 0.07 cell) so adjacent rounded squares touch with rounded corners creating gaps at corners. Classic look: use inset 0.06 and corner radius 0.28*size. Adjacent squares will touch along edges (size 0.88 → gap 0.12 between centers edge... wait, adjacent centers are CELL apart; each square spans 0.88*CELL → gap of 0.12*CELL between them → visible gaps!). To make a continuous snake, draw the body as a thick rounded polyline path instead. That's nicer but more complex with the interpolation (path through segment centers works fine — draw a stroke with round joins and round caps!). 

Great idea: draw body as a stroked path through the interpolated centers with lineWidth = CELL*0.72, lineJoin/lineCap round, then draw head on top as a rounded square bigger (CELL*0.9). But the tail end cap is round which looks good. However, the stroke path through all segments with round joins looks like a continuous snake ✓. Then add scale/detail: maybe draw a second thinner lighter stroke on top for a highlight (lineWidth CELL*0.28 with low alpha, offset? can't offset easily). Could draw the highlight path with slightly shifted coordinates? A simpler approach: draw the thick path, then draw small circle "scales" on some segments. Or draw the path with a gradient stroke (createLinearGradient along the path bbox) from head color to tail color ✓.

Then the head: rounded square with the eyes. The body path includes the head center; draw the head square on top to make it distinct.

But there's an issue: when the snake is moving, the path between segment centers is a polyline — good.

Also draw a subtle drop shadow under the snake: same path with a dark color offset by (2,4) and alpha.

Implementation:
```js
function segmentPos(seg,t){ return {x:(seg.px+(seg.x-seg.px)*t)*CELL+CELL/2, y:...}; }
```

Path: moveTo first point (tail) then lineTo each. Stroke with gradient from tail color to head color.

Draw order:
1. board background (gradient, grid, vignette, subtle checker)
2. food glow
3. snake shadow path
4. snake body path (gradient)
5. body scale dots: small lighter dots along the path at every other segment for texture
6. head shape (rounded rect) with gradient + glow
7. eyes (white sclera + dark pupil) oriented by dir
8. particles + popups

Eyes orientation: For direction right (1,0): eyes at head center + (0.16*size, ±0.2*size), pupils offset +x. For up: eyes at (±0.2*size, -0.16*size), pupils offset -y. For left/right similar.

Let's compute head half size h = CELL*0.45. Eye offsets: perpendicular = (-dir.y, dir.x) * h*0.42 + dir * h*0.18. Eye radius h*0.26 (white circle) with pupil radius h*0.13 at eye center + dir*h*0.10.

Add eye shine: small white dot.

Head shape: rounded square with size h*2, radius h*0.45, fill with a gradient (light top-left), plus a darker outline, plus a subtle glow (shadowColor).

Also maybe draw a small "tongue" when the head moves? Cute but optional. Add a tiny tongue flick for right/left directions? Let's add a subtle red tongue that extends and retracts periodically — nice polish. Keep it simple: if direction is horizontal, draw a small red line from the head front, animated with sin. Actually the head's orientation is axis-aligned only (4 directions), so it's easy.

Let me include a tongue that appears briefly every ~1.2s: length = h*0.35.

Particles on eating: burst at food position with colors from a palette [#ff5d8f, #ffd166, #8affc1, #ffffff]. Also add a ripple ring.

Floating "+10" popup.

Background grid: draw lines every CELL with rgba(255,255,255,0.035), and alternating cell tint for a checkerboard at very low alpha.

Board frame: CSS.

Now the HUD and layout. Let's write the HTML:

```html
<div class="app">
  <header class="topbar">
    <div class="brand">
      <div class="logo">🐍? </div>  <!-- emoji may render as color font; maybe use a CSS-drawn snake icon instead -->
      <div>
        <h1>SNAKE</h1>
        <p>Classic arcade · smooth</p>
      </div>
    </div>
    <div class="stats">
      <div class="stat"><span class="label">Score</span><span class="value" id="score">0</span></div>
      <div class="stat"><span class="label">Best</span><span class="value" id="best">0</span></div>
    </div>
  </header>
  <div class="board-wrap">
    <canvas id="game" width="600" height="600"></canvas>
    <div class="overlay" id="overlay">...</div>
  </div>
  <footer class="footbar">
    <div class="keys">...</div>
    <div class="status" id="status">AI demo running</div>
  </footer>
</div>
```

Keyboard hint chips: ↑ ← ↓ → and W A S D, P pause, Space restart. Show as small kbd elements.

Status pill on the footer right shows the current mode (Demo / Playing / Paused / Game over) with a colored dot.

Let me write CSS carefully for a polished look:

- body: background: radial-gradient(1200px 600px at 50% -10%, #1b2a4a 0%, transparent 60%), radial-gradient(900px 500px at 90% 110%, #10283a 0%, transparent 60%), #070b14; min-height 100vh; display flex; align-items center; justify-content center; padding: 18px; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color #e6f0ff; overflow: hidden? Better allow but ensure no horizontal scrollbar.

- .app: width: min(680px, 100%); display flex; flex-direction column; align-items center; gap: 14px.

- .topbar: width: 100%; display flex; justify-content space-between; align-items center; gap 12px; flex-wrap wrap.

- Title: font-size clamp(26px, 5vw, 38px); font-weight 800; letter-spacing .18em; background gradient text; text-shadow glow.

- .stats: display flex; gap 10px. .stat: background rgba(255,255,255,.05); border 1px solid rgba(255,255,255,.09); border-radius 14px; padding 8px 14px; min-width 92px; text-align center; backdrop-filter blur(8px). .label small uppercase letter-spacing .16em color rgba(230,240,255,.55) font-size 10px. .value font-size 24px font-weight 800 font-variant-numeric tabular-nums.

Add a pop animation on score change: class "bump" then remove.

- .board-wrap: position relative; width: min(600px, calc(100vw - 28px), calc(100vh - 210px)); aspect-ratio 1; border-radius 22px; overflow hidden; background #0a1120; border 1px solid rgba(255,255,255,.10); box-shadow: 0 30px 60px -20px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.03) inset, 0 0 60px -30px #35e0a1; 

Hmm, with `aspect-ratio: 1` and width set by the min(), the height follows. But at 1280x800: width = min(600, 1252, 590) = 590. Height 590. Total page height: header ~64 + gap 14 + board 590 + gap + footer ~30 + padding 36 = ~750 ✓.

At 375x667: width = min(600, 347, 457) = 347 ✓.

At 375x600: min(600,347,390)=347 ✓.

- canvas: display block; width 100%; height 100%.

- .overlay: position absolute; inset 0; display flex; align-items center; justify-content center; padding 18px; transition opacity .28s, visibility; pointer-events none when hidden. Use data-mode attribute: demo → align-items: flex-end; over/paused → center.

- .card: background rgba(10,16,30,.78); backdrop-filter: blur(10px); border 1px solid rgba(255,255,255,.12); border-radius 18px; padding 18px 20px; text-align center; box-shadow big; max-width 340px.

For demo mode, maybe the card should be more compact: less padding, horizontal layout. Use CSS to adjust per mode.

- .overlay[hidden-ish] class "is-hidden" {opacity:0; pointer-events:none; visibility:hidden}

- Buttons: .btn {background linear-gradient(135deg,#4ade80,#22d3ee); color:#05121a; font-weight 800; border-radius 12px; padding 10px 18px; border:none; cursor:pointer; box-shadow...} hover transform translateY(-1px).

- kbd style: small rounded squares with border.

- .footbar: display flex; justify-content space-between; align-items center; font-size 12px; color rgba(230,240,255,.55); width 100%; flex-wrap wrap; gap 8px.

Now the "start" prompt needs to be prominent: In demo mode card: badge "AI DEMO", h2 "Click or press a key to start", p "The snake is playing by itself — take over!", btn "Play now".

Wait requirement wording: '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 message must literally be "Click or press a key to start" ✓.

Now JS structure.

```js
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const COLS=24, ROWS=24, CELL=25, SIZE=600;
const DPR = Math.min(window.devicePixelRatio||1, 2);
canvas.width = SIZE*DPR; canvas.height = SIZE*DPR;
ctx.setTransform(DPR,0,0,DPR,0,0);
```
Also handle resize? The CSS size changes but internal stays 600 logical — fine. But for very large displays, 600 CSS px at dpr 2 = 1200 internal ✓.

Hmm: if the CSS width is 347 (mobile), the internal 1200 px is downscaled → super crisp ✓.

Wait — setting canvas.width resets the context transform, so set the transform after ✓.

State variables and functions:

```js
let state='demo'; // demo | playing | paused | over
let snake=[], dir={x:1,y:0}, queue=[], food={x:0,y:0}, score=0, best=0, acc=0, stepMs=125, particles=[], popups=[], ripples=[], flash=0, shakeUntil=0;
```

initDemo(): snake setup, dir right, queue [], score 0? I'd like demo to accumulate score and best. Reset score to 0 for demo and let it grow; best updated.

But when the player starts, do we reset score to 0 and keep best? Yes.

Let me define:
```js
function resetGame(auto){
  snake = [];
  const startX = Math.floor(COLS*0.25), y = Math.floor(ROWS/2);
  for (let i=0;i<5;i++) snake.push({x:startX-i, y, px:startX-i, py:y});
  dir = {x:1,y:0}; queue=[]; food = spawnFood(); score=0; acc=0; stepMs=BASE_STEP;
  updateHUD();
}
```
Careful: snake[0] is the head at startX with body to the left (startX-i) ✓ moving right.

startPlayerGame(): resetGame(); state='playing'; hide overlay; status update; focus canvas? The canvas can get focus for key handling but we use window keydown ✓.

spawnFood(): collect all free cells: iterate all cells not occupied by snake → random pick. For 576 cells, fine each time.

```js
function spawnFood(){
  const occupied = new Set(snake.map(c=>c.x+','+c.y));
  const free=[];
  for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++) if(!occupied.has(x+','+y)) free.push({x,y});
  if(!free.length) return null;
  return free[Math.floor(Math.random()*free.length)];
}
```

tick():
```js
function tick(){
  if (state==='demo') { const d = aiDirection(); if(d) dir=d; }
  else if (state==='playing') { if(queue.length){ const nd=queue.shift(); if(!(nd.x===-dir.x&&nd.y===-dir.y)) dir=nd; } }
  ... move ...
}
```

AI direction via BFS:

```js
function aiDirection(){
  const blocked = new Set();
  for(let i=0;i<snake.length-1;i++) blocked.add(snake[i].x+','+snake[i].y);
  // BFS from head
  const start = snake[0];
  const goal = food;
  const prev = new Map(); // key -> {x,y,dir}
  const q=[start]; const seen=new Set([start.x+','+start.y]);
  let found=false;
  while(q.length){
    const cur=q.shift();
    if(cur.x===goal.x&&cur.y===goal.y){found=true;break;}
    for(const d of DIRS){
      const nx=cur.x+d.x, ny=cur.y+d.y;
      if(nx<0||nx>=COLS||ny<0||ny>=ROWS) continue;
      const k=nx+','+ny;
      if(seen.has(k)||blocked.has(k)) continue;
      seen.add(k);
      prev.set(k,{x:cur.x,y:cur.y,d});
      q.push({x:nx,y:ny});
    }
  }
  if(found){
    // walk back from goal
    let k=goal.x+','+goal.y;
    while(prev.has(k)){
      const p=prev.get(k);
      if(p.x===start.x&&p.y===start.y) break;
      k=p.x+','+p.y;
    }
    // k is the first step cell
    const p=prev.get(k);
    return p.d;
  }
  ...
}
```
Wait, walking back: prev maps cell → its predecessor. Starting at goal, follow prev until we reach a cell whose predecessor is start → that cell is the first step. The loop above: k=goal; while prev.has(k) { p=prev.get(k); if (p is start) break; k=p.key }. After the loop, k = first step cell (the child of start). Then p = prev.get(k) gives the direction from start to k ✓ (since prev stores the direction used to reach that cell). Let me store in prev: {px,py,d}. So prev.get(k).d is the direction from predecessor to k ✓.

Note: the head's own cell is in `seen` initially; the first step cell is a neighbor of start with a prev entry ✓.

But careful: BFS blocked set excludes the tail (last element) since it moves away — but that only matters when the head reaches the tail cell in the same tick; the tail will move away, so it's safe to path through. However, following the path may require stepping into the tail cell later; the tail moves each tick so it's fine. But the path is computed assuming obstacles static; slight risk of collision. Acceptable for a demo. Alternatively, block the tail too (safer but may prevent reaching food). I'll block all body cells including the tail to be safe — but then if the food is right behind the tail... rare. Hmm, blocking the tail might make BFS fail in tight spaces, then fallback heuristic kicks in. I'll block all body (including tail) for safety. Actually blocking the tail could be overly conservative in corridors of width 2. Let's block all except the tail — and when actually moving, the collision check uses the same rule (tail removed when not eating) → consistent with the game's rules. If the AI steps into the tail cell, the tail moves away → no collision ✓. So blocking excludes the tail ✓ consistent.

But there's a subtlety: the BFS path might route through the tail cell and then the next cell is occupied by the body that has since moved... minor. Fine.

Fallback when no path: choose a safe direction that maximizes free space:
```js
function safeFallback(){
  const candidates = DIRS.filter(d => !(d.x===-dir.x&&d.y===-dir.y));
  let best=null,bestScore=-1;
  for(const d of candidates){
    const nx=snake[0].x+d.x, ny=snake[0].y+d.y;
    if(nx<0||nx>=COLS||ny<0||ny>=ROWS) continue;
    if(snake.slice(0,-1).some(c=>c.x===nx&&c.y===ny)) continue;
    const space = floodFill(nx,ny);
    if(space>bestScore){bestScore=space;best=d;}
  }
  return best;
}
```
floodFill counts reachable free cells from a cell (treating body as obstacles). Implement with a stack.

Also, if the AI has no safe move → it will die → game over in demo mode. Then we should restart the demo after a short delay (so the demo keeps playing forever). Implement: on game over while in demo mode, show a brief "demo crashed" then reset after ~900ms. Since the overlay is hidden during demo (except the start prompt), the reset just happens. Let's implement: in gameOver(), if state was 'demo', set state='over' but with demoMode flag; after 1000ms, reset to demo. Use setTimeout.

Actually simpler: keep a `demo` flag separate from state. Let me define states: 'demo' (AI playing with prompt), 'playing', 'paused', 'over'. In demo, if the AI dies → set a timeout of 1200ms → resetGame(); state='demo'; show prompt again? The prompt overlay is visible during demo anyway. Maybe after the crash, show the start prompt again (it is already shown). Fine — reset the demo snake.

Let's also make the AI start from a decent length (5) and grow as it eats → the demo gets impressive quickly (score climbs in a few seconds). With a step of ~120ms, the AI reaches food in a few seconds.

Also increase the demo speed slightly? Keep the same.

Game over (player):
- state='over'; overlay shows final score and best; status "Game over".
- Snake flashes red: set a flag `deadFlash=1` and render the head in red for a moment.

Restart: Space/Enter or the button → startPlayerGame().

Pause: 'p' toggles; overlay shows "Paused"; status "Paused".

HUD update: score text, best text with a bump animation.

Also display "×N" speed? Not needed.

Now the score bump animation: add class 'bump' to the score element and remove after animation via setTimeout or animationend. Use CSS keyframes and `el.classList.remove('bump'); void el.offsetWidth; el.classList.add('bump');`.

Now overlay content management function:

```js
function setOverlay(mode){
  // mode: 'demo' | 'over' | 'paused' | 'hidden'
}
```
Update the card's inner HTML per mode. I'll have static elements and set text content.

For the demo card: badge "AI DEMO"; title "Click or press a key to start"; text "Watch the AI — then take over with the arrow keys."; button "Play now"; hint "↑ ↓ ← → · W A S D · P pause".

For over: badge "GAME OVER" (red), title "Score <n>", text "Best <b> · Press Space or Enter to restart", button "Play again".

For paused: badge "PAUSED", title "Take a breather", text "Press P to resume", button "Resume".

Overlay positioning: demo → bottom; over/paused → center. Use data-mode on the overlay element and CSS rules.

Also the demo card should be compact so the board is visible. Add class .compact for demo.

Now the status pill in the footer: `<span class="dot"></span><span id="statusText">AI demo</span>` with color variations via data-state.

Footer hints: "Move <kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>" and "Pause <kbd>P</kbd>" and "Restart <kbd>Space</kbd>".

On small screens, the footer may wrap — that's fine (ensure no horizontal scroll).

Let me also add a subtle animated background "grid" in CSS? Maybe a faint radial pattern. Keep it tasteful.

Now, writing the rendering code in detail.

```js
function render(t){
  // t = time ms
  ctx.clearRect(0,0,SIZE,SIZE);
  drawBoard(t);
  if (food) drawFood(t);
  drawSnake(t);
  drawParticles(dt);
  drawPopups();
  if (flash>0) { ctx.fillStyle = `rgba(255,255,255,${flash})`; ctx.fillRect(...); flash-=...; }
}
```

drawBoard:
```js
const g = ctx.createLinearGradient(0,0,SIZE,SIZE);
g.addColorStop(0,'#0d1626'); g.addColorStop(0.5,'#0a1220'); g.addColorStop(1,'#070d18');
ctx.fillStyle=g; ctx.fillRect(0,0,SIZE,SIZE);
// checkerboard
ctx.fillStyle='rgba(255,255,255,0.018)';
for(y..)for(x..) if((x+y)%2===0) ctx.fillRect(x*CELL,y*CELL,CELL,CELL);
// grid lines
ctx.strokeStyle='rgba(140,190,255,0.055)'; ctx.lineWidth=1;
for(i=1;i<COLS;i++){ line }
// vignette
const vg = ctx.createRadialGradient(SIZE/2,SIZE/2,SIZE*0.25,SIZE/2,SIZE/2,SIZE*0.78);
vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,0.45)');
ctx.fillStyle=vg; ctx.fillRect(0,0,SIZE,SIZE);
```
Also a subtle animated glow that follows the food? Nice: a large radial gradient at the food position with low alpha. Let's add.

drawFood(t):
```js
const cx=food.x*CELL+CELL/2, cy=food.y*CELL+CELL/2;
const pulse = 1+0.05*Math.sin(t/260);
// outer glow
const glow=ctx.createRadialGradient(cx,cy,2,cx,cy,CELL*1.1);
glow.addColorStop(0,'rgba(255,93,143,0.35)'); glow.addColorStop(1,'rgba(255,93,143,0)');
ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(cx,cy,CELL*1.1,0,Math.PI*2); ctx.fill();
// berry body
ctx.save();
ctx.shadowColor='rgba(255,80,130,0.8)'; ctx.shadowBlur=14;
const bg=ctx.createRadialGradient(cx-CELL*0.1,cy-CELL*0.12,1,cx,cy,CELL*0.33*pulse);
bg.addColorStop(0,'#ff9ab5'); bg.addColorStop(0.5,'#ff5d8f'); bg.addColorStop(1,'#e0306b');
ctx.fillStyle=bg; ctx.beginPath(); ctx.arc(cx,cy,CELL*0.3*pulse,0,Math.PI*2); ctx.fill();
ctx.restore();
// highlight
ctx.fillStyle='rgba(255,255,255,0.75)';
ctx.beginPath(); ctx.ellipse(cx-CELL*0.1, cy-CELL*0.12, CELL*0.08, CELL*0.05, -0.6,0,Math.PI*2); ctx.fill();
// leaf
ctx.save(); ctx.translate(cx+CELL*0.06, cy-CELL*0.26); ctx.rotate(-0.5);
ctx.fillStyle='#4ade80'; ctx.beginPath(); ctx.ellipse(0,0,CELL*0.13,CELL*0.06,0,0,Math.PI*2); ctx.fill();
ctx.restore();
```
Maybe add tiny seeds on the berry. Keep it.

drawSnake(t):
```js
const alpha = clamp(acc/stepDur,0,1);
const pts = snake.map(seg => ({x:(seg.px+(seg.x-seg.px)*alpha+0.5)*CELL, y:(seg.py+(seg.y-seg.py)*alpha+0.5)*CELL}));
```
Note the interpolation: the head's prev is the old head cell, so at alpha=0 the head is at the old head cell and at alpha=1 at the new cell ✓.

Shadow path: stroke with rgba(0,0,0,0.35), lineWidth CELL*0.78, offset by translating (0, 3) — use ctx.save(); ctx.translate(2,4); stroke path; restore.

Body gradient: createLinearGradient from tail point to head point: tail color → head color.

```js
const grad = ctx.createLinearGradient(pts[pts.length-1].x, pts[pts.length-1].y, pts[0].x, pts[0].y);
grad.addColorStop(0,'#1f9d74'); grad.addColorStop(0.55,'#34d399'); grad.addColorStop(1,'#7df7bd');
```
Head color bright.

Stroke the path: lineWidth = CELL*0.7, lineCap='round', lineJoin='round'.

Then an inner highlight: stroke the same path with lineWidth CELL*0.26 and a lighter translucent color (rgba(255,255,255,0.12)), but it would be centered on the path, making a stripe down the middle — that looks like a glossy ridge. Good.

Then scales: for every other segment, draw a small circle with rgba(255,255,255,0.08) radius CELL*0.08 at the segment position, but skip near the head. Hmm, might look odd. Alternative: skip scales. I'll add small darker "scute" dots at alternating segments — subtle. Let's include with low alpha (0.06) and radius 0.07*CELL.

Head:
```js
const hp = pts[0];
const h = CELL*0.46; // half-size
ctx.save();
ctx.translate(hp.x, hp.y);
// glow
ctx.shadowColor = dead? 'rgba(255,80,80,.9)':'rgba(80,255,180,0.55)'; ctx.shadowBlur=18;
// rounded square
const rr = h*0.46;
roundRect(ctx, -h,-h, h*2,h*2, rr);
const hg = ctx.createLinearGradient(-h,-h,h,h);
hg.addColorStop(0, dead?'#ff8a8a':'#b6ffd9'); hg.addColorStop(0.55, dead?'#ef4444':'#5ee6a8'); hg.addColorStop(1, dead?'#b91c1c':'#22b981');
ctx.fillStyle=hg; ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.35)'; ctx.lineWidth=1.5; ctx.stroke();
// eyes
...
ctx.restore();
```

Eyes: perpendicular vector p = (-dir.y, dir.x). Eye center = p*(h*0.40) + dir*(h*0.14). White ellipse radius h*0.27 (rx) and h*0.30 (ry). Pupil = eye center + dir*(h*0.13), radius h*0.13, color '#062018'. Add a tiny white glint.

Tongue: if dir is horizontal (or any), draw at the head's front: from (dir.x*h, dir.y*h) extend h*0.4 * flick, with a fork. Animate with Math.sin(t/140). Let's implement a simple flick: const flick = Math.max(0, Math.sin(t/180)); if (flick>0.55) draw tongue. Might be distracting; keep subtle: draw when Math.sin(t/150) > 0.6.

Draw particles:
```js
for(const p of particles){
  const a = p.life/p.max;
  ctx.globalAlpha = a;
  ctx.fillStyle = p.color;
  ctx.beginPath(); ctx.arc(p.x,p.y,p.size*a,0,Math.PI*2); ctx.fill();
}
```
Positions in canvas coords.

Ripples: expanding rings at the food location, stroke with alpha fading.

Popups: "+10" text rising, fading.

Now the main loop with dt for particles: use frame delta in seconds.

Let me write the update for particles:
```js
function updateEffects(dt){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.life-=dt;
    p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=260*dt; p.vx*=0.99;
    if(p.life<=0) particles.splice(i,1);
  }
  ...
}
```

Eating burst:
```js
function burst(x,y){
  const cx=x*CELL+CELL/2, cy=y*CELL+CELL/2;
  const colors=['#ff5d8f','#ffd166','#8affc1','#ffffff','#ff9ecb'];
  for(let i=0;i<18;i++){
    const a=Math.random()*Math.PI*2, sp=60+Math.random()*180;
    particles.push({x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-40,life:0.45+Math.random()*0.4,max:0.85,size:2+Math.random()*3.5,color:colors[i%colors.length]});
  }
  ripples.push({x:cx,y:cy,r:CELL*0.2,life:0.45,max:0.45});
  popups.push({x:cx,y:cy-6,text:'+10',life:0.9,max:0.9});
  flash = 0.12; // white flash
}
```
Note flash decay: flash -= dt*1.2.

HUD score bump when eating.

Now the game over detection and the "dead" flag.

Let me write the whole file now.

Additional: Prevent arrow keys scrolling the page: e.preventDefault() for arrows/space.

Also handle the case where the user clicks the canvas → start the game (if demo). Add a click handler on canvas: if state==='demo' → startPlayerGame(); else if state==='over' → restart? Maybe clicking the board restarts on game over. Fine.

Also 'touch' support: swipe on canvas for mobile. Nice-to-have: implement touchstart/touchend to detect swipes → queue direction. Good for mobile polish.

Also pointerdown on the board in demo → start.

Let's also add a small "AI DEMO" badge drawn on the canvas? The overlay badge covers it. Fine.

One more: the overlay must not block the demo visually; the demo card at the bottom center with a max-width of 380px.

Hmm, but the "Click or press a key to start" message should be clearly visible on the first paint. The card at the bottom of the board is visible ✓.

Let me reconsider the layout to also show that the game is in action. Maybe position the demo card at the bottom with a slim design: badge + text + button in a row.

CSS for overlay[data-mode="demo"]: align-items: flex-end; padding-bottom: 18px; and .card {padding: 12px 16px; display:flex; flex-direction:column; gap:6px; max-width: 420px; width: calc(100% - 20px)}.

Hmm, on a 347px board, the card would be ~327 wide; the text "Click or press a key to start" at 18px bold might wrap to 2 lines — fine.

Let me now also handle the initial paint before JS runs: the canvas shows nothing until the first frame; that's immediate (<16ms) ✓.

Let me write the final code.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Snake — Neon Arcade</title>
<style> ... </style>
</head>
<body>
 <main class="app"> ... </main>
 <script> ... </script>
</body>
</html>
```

Let me write the CSS in full.

```css
:root{
  --bg:#070b14;
  --panel:rgba(255,255,255,.055);
  --line:rgba(255,255,255,.10);
  --text:#e8f1ff;
  --muted:rgba(232,241,255,.58);
  --green:#4ade80;
  --cyan:#38bdf8;
  --pink:#ff5d8f;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;
  background:
    radial-gradient(1100px 620px at 50% -12%, rgba(56,189,248,.16), transparent 62%),
    radial-gradient(900px 520px at 88% 108%, rgba(74,222,128,.13), transparent 60%),
    radial-gradient(700px 480px at 6% 82%, rgba(255,93,143,.10), transparent 60%),
    var(--bg);
  color:var(--text);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  display:flex; align-items:center; justify-content:center;
  padding:16px;
  min-height:100vh;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
.app{width:min(680px,100%); display:flex; flex-direction:column; align-items:center; gap:12px;}
```

Also add a subtle noise/grid? Skip.

Header:
```css
.topbar{width:100%; display:flex; align-items:center; justify-content:space-between; gap:14px;}
.brand{display:flex; align-items:center; gap:12px;}
.brand .mark{width:42px;height:42px;border-radius:13px; display:grid;place-items:center; background:linear-gradient(140deg,rgba(74,222,128,.25),rgba(56,189,248,.18)); border:1px solid rgba(255,255,255,.14); box-shadow:0 10px 26px -12px rgba(74,222,128,.8), inset 0 1px 0 rgba(255,255,255,.14);}
```
The mark could contain a small SVG snake? Inline SVG is allowed (it's inline). Let's draw a simple snake glyph with SVG paths — a little S-shaped snake in green with an eye. Inline SVG is fine ("no external files/images"). I'll create a simple SVG:

```html
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <path d="M5 16c0-2.2 1.8-4 4-4s4-1.8 4-4-1.8-4-4-4" fill="none" stroke="url(#g)" stroke-width="2.6" stroke-linecap="round"/>
  <circle cx="13" cy="7" r="1.1" fill="#062"/>
  <defs>...gradient...</defs>
</svg>
```
Fine. Simpler: use a CSS-drawn dot grid? I'll do the SVG snake.

Title:
```css
h1{margin:0; font-size:clamp(24px,4.4vw,34px); font-weight:800; letter-spacing:.16em; background:linear-gradient(100deg,#eafff4,#7df7bd 40%,#38bdf8); -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 30px rgba(74,222,128,.18);}
.subtitle{margin:2px 0 0; font-size:11px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted)}
```

Stats:
```css
.stats{display:flex; gap:10px}
.stat{min-width:86px; padding:7px 13px 8px; border-radius:14px; background:linear-gradient(180deg,rgba(255,255,255,.07),rgba(255,255,255,.035)); border:1px solid var(--line); text-align:center; box-shadow:inset 0 1px 0 rgba(255,255,255,.08), 0 10px 24px -18px #000; backdrop-filter:blur(6px)}
.stat .label{display:block; font-size:9.5px; letter-spacing:.2em; text-transform:uppercase; color:var(--muted)}
.stat .value{display:block; margin-top:2px; font-size:23px; font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:.02em}
#score{color:#9df7c6} #best{color:#8fd6ff}
.bump{animation:bump .35s cubic-bezier(.2,1.4,.4,1)}
@keyframes bump{0%{transform:scale(1)}35%{transform:scale(1.28)}100%{transform:scale(1)}}
```

Board:
```css
.board{position:relative; width:min(600px, calc(100vw - 30px), calc(100vh - 215px)); aspect-ratio:1/1; border-radius:24px; overflow:hidden; background:#0a1120; border:1px solid rgba(255,255,255,.11); box-shadow:0 40px 80px -30px rgba(0,0,0,.85), 0 0 0 1px rgba(0,0,0,.5), 0 0 70px -34px rgba(74,222,128,.7), inset 0 0 60px -30px rgba(56,189,248,.35);}
canvas{display:block;width:100%;height:100%; touch-action:none;}
```

Careful: `calc(100vh - 215px)` at 800 → 585; min with 600 → 585. Good. At 667 → 452 vs 347 → 347 ✓.

Add a scanline/glow overlay on the board via ::after with pointer-events none:
```css
.board::after{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.05), transparent 45%);mix-blend-mode:screen}
```

Overlay:
```css
.overlay{position:absolute;inset:0;display:flex;justify-content:center;align-items:center;padding:16px;transition:opacity .25s ease, visibility .25s;z-index:3}
.overlay[data-mode="demo"]{align-items:flex-end;padding-bottom:16px}
.overlay.hidden{opacity:0;visibility:hidden;pointer-events:none}
.card{width:100%;max-width:400px;text-align:center;padding:18px 20px;border-radius:18px;background:rgba(8,13,25,.78);border:1px solid rgba(255,255,255,.14);box-shadow:0 24px 60px -24px rgba(0,0,0,.9), inset 0 1px 0 rgba(255,255,255,.12);backdrop-filter:blur(12px) saturate(1.2);animation:pop .35s cubic-bezier(.2,1.2,.3,1)}
```
For demo, compact card: `.overlay[data-mode="demo"] .card{padding:12px 16px;max-width:430px}` and the title font-size smaller.

Badge:
```css
.badge{display:inline-flex;align-items:center;gap:6px;font-size:10px;letter-spacing:.18em;text-transform:uppercase;font-weight:700;padding:4px 9px;border-radius:999px;background:rgba(74,222,128,.14);border:1px solid rgba(74,222,128,.35);color:#a7f3c8}
.badge::before{content:"";width:6px;height:6px;border-radius:50%;background:#4ade80;box-shadow:0 0 8px #4ade80;animation:blink 1.4s infinite}
```
For game over the badge should be red: add class .badge.danger.

Buttons:
```css
.btn{margin-top:10px;appearance:none;border:0;cursor:pointer;font:inherit;font-weight:800;font-size:14px;letter-spacing:.02em;color:#04150e;padding:10px 18px;border-radius:12px;background:linear-gradient(135deg,#86efac,#22d3ee);box-shadow:0 12px 26px -12px rgba(34,211,238,.9), inset 0 1px 0 rgba(255,255,255,.5);transition:transform .12s ease, filter .12s}
.btn:hover{transform:translateY(-1px);filter:brightness(1.06)}
.btn:active{transform:translateY(0)}
```

Footer:
```css
.foot{width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--muted)}
.keys{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
kbd{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;font:600 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#dbe7ff;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);border-bottom-width:2px;border-radius:6px}
.status{display:flex;align-items:center;gap:8px;padding:6px 11px;border-radius:999px;background:rgba(255,255,255,.05);border:1px solid var(--line)}
.status .dot{width:7px;height:7px;border-radius:50%;background:#4ade80;box-shadow:0 0 10px #4ade80}
```

Media query for small widths: reduce gaps, hide the subtitle, etc.

Now the JS. Let me write it completely and carefully.

```js
(() => {
  'use strict';
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d', { alpha: false });
```
alpha:false means the canvas is opaque — we fill the whole area anyway ✓ (faster).

```js
  const COLS = 24, ROWS = 24, CELL = 25, SIZE = COLS * CELL;
  const BASE_STEP = 128, MIN_STEP = 78;
  const DPR = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = SIZE * DPR; canvas.height = SIZE * DPR;
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
  ctx.lineJoin = 'round'; ctx.lineCap = 'round';
```

Elements:
```js
  const el = {
    score: document.getElementById('score'),
    best: document.getElementById('best'),
    overlay: document.getElementById('overlay'),
    badge: document.getElementById('badge'),
    title: document.getElementById('ovTitle'),
    text: document.getElementById('ovText'),
    hint: document.getElementById('ovHint'),
    btn: document.getElementById('ovBtn'),
    status: document.getElementById('statusText'),
    statusPill: document.getElementById('statusPill')
  };
```

Game state:
```js
  let state = 'demo';
  let snake = [], dir = {x:1,y:0}, queue = [], food = null;
  let score = 0, best = 0, acc = 0, elapsed = 0;
  let particles = [], ripples = [], popups = [];
  let flash = 0, dead = false, last = performance.now(), demoResetTimer = null;
  const DIRS = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
```

reset():
```js
  function resetGame(){
    const y = Math.floor(ROWS/2);
    const x0 = Math.floor(COLS*0.22);
    snake = [];
    for (let i=0;i<6;i++) snake.push({x:x0-i, y, px:x0-i, py:y});
    dir = {x:1,y:0}; queue = []; score = 0; acc = 0; dead=false;
    food = spawnFood();
    particles=[]; ripples=[]; popups=[]; flash=0;
    updateHUD();
  }
```
Careful: x0-i might be negative? x0 = floor(24*0.22)=5 → 5-5=0 ✓ (length 6 → i up to 5 → x=0) ok.

spawnFood as described.

updateHUD:
```js
  function updateHUD(bumpScore){
    el.score.textContent = score;
    el.best.textContent = best;
    if (bumpScore) bump(el.score);
  }
```

Start functions:
```js
  function startDemo(){
    resetGame();
    state='demo';
    setOverlay('demo');
    setStatus('AI demo running','demo');
  }
  function startPlayer(){
    if (demoResetTimer) {clearTimeout(demoResetTimer); demoResetTimer=null;}
    resetGame();
    state='playing';
    hideOverlay();
    setStatus('You are in control','playing');
  }
```

But careful: resetGame sets score=0 — in demo mode, the demo score should accumulate and the best should update. resetGame is called at the demo start; then the AI eats and the score increases ✓.

On startPlayer, score resets to 0 but best keeps the max ✓.

setOverlay(mode): updates the overlay content and classes.

```js
  function setOverlay(mode){
    el.overlay.classList.remove('hidden');
    el.overlay.dataset.mode = mode;
    if (mode==='demo'){
      el.badge.textContent='AI demo';
      el.badge.classList.remove('danger');
      el.title.textContent='Click or press a key to start';
      el.text.textContent='The snake is playing by itself — grab the keys to take over.';
      el.hint.innerHTML = '<kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> · <kbd>P</kbd> pause';
      el.btn.textContent='Play now';
      el.btn.classList.remove('hidden');
    } else if (mode==='over'){
      el.badge.textContent='Game over';
      el.badge.classList.add('danger');
      el.title.innerHTML = `Score <span class="big">${score}</span>`;
      el.text.innerHTML = `Best <b>${best}</b> · Press <kbd>Space</kbd> or <kbd>Enter</kbd> to play again`;
      el.hint.textContent='';
      el.btn.textContent='Play again';
    } else if (mode==='paused'){
      ...
    }
  }
```
Careful with `el.hint.innerHTML` and kbd styling.

The title innerHTML with a span class big — style `.card h2 .big{font-size:1.5em;color:#9df7c6}`.

hideOverlay(): el.overlay.classList.add('hidden').

Status pill: set the dot color per state via data-state on the pill.

Key handling:

```js
  const KEYMAP = {
    ArrowUp:{x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
    w:{x:0,y:-1},W:{x:0,y:-1},a:{x:-1,y:0},A:{x:-1,y:0},s:{x:0,y:1},S:{x:0,y:1},d:{x:1,y:0},D:{x:1,y:0}
  };
  window.addEventListener('keydown', (e)=>{
    const k = e.key;
    if (k===' ' || k==='Spacebar' || k==='Enter'){
      e.preventDefault();
      if (state==='over' || state==='demo') startPlayer();
      else if (state==='paused'){ /* maybe resume */ }
      return;
    }
    if (k==='p'||k==='P'){
      e.preventDefault();
      if (state==='playing') pauseGame(); else if (state==='paused') resumeGame();
      return;
    }
    const d = KEYMAP[k];
    if (!d) return;
    e.preventDefault();
    if (state==='demo'){ startPlayer(); queueDir(d); return; }
    if (state==='playing'){ queueDir(d); return; }
  });
```
Hmm: pressing Space during 'demo' should start the player game ✓.

queueDir:
```js
  function queueDir(d){
    const last = queue.length ? queue[queue.length-1] : dir;
    if (d.x===last.x && d.y===last.y) return;
    if (d.x===-last.x && d.y===-last.y) return;
    if (queue.length<6) queue.push({x:d.x,y:d.y});
  }
```
Wait — a subtle bug: if the queue is empty, last = dir (current). If the player presses 'up' while moving right → queue [up]. Then presses 'down' → last=up → down is opposite of up → rejected ✓ (prevents reversal). Then presses 'left' → last=up, left is not opposite of up → queued → queue [up, left]. At ticks: dir=right → up → left. Good, both honored.

But another scenario: moving right, the player presses up then left then down quickly. Queue: [up], then left (ok), then down: last=left; down is not opposite of left → queued → [up,left,down]. At ticks: up, left, down — down is not a reversal of left ✓ (down is fine relative to left). ✓

Reverse prevention at tick time also double-checks.

Edge: the queue cap of 6. Fine.

Now the tick:

```js
  function tick(){
    if (state==='demo'){
      const d = aiDirection();
      if (d) dir = d;
    } else if (state==='playing'){
      if (queue.length){
        const nd = queue.shift();
        if (!(nd.x===-dir.x && nd.y===-dir.y)) dir = nd;
      }
    }
    const head = snake[0];
    const nx = head.x + dir.x, ny = head.y + dir.y;
    if (nx<0||nx>=COLS||ny<0||ny>=ROWS){ gameOver(); return; }
    const ate = food && nx===food.x && ny===food.y;
    const len = snake.length;
    const limit = ate ? len : len-1;
    for (let i=0;i<limit;i++){
      if (snake[i].x===nx && snake[i].y===ny){ gameOver(); return; }
    }
    // build the new snake
    const keep = ate ? len : len-1;
    const cells = [{x:nx,y:ny}];
    for (let i=0;i<keep;i++) cells.push({x:snake[i].x,y:snake[i].y});
    const prev = snake.map(c=>({x:c.x,y:c.y}));
    if (ate) prev.push({x:snake[len-1].x,y:snake[len-1].y});
    snake = cells.map((c,i)=>({x:c.x,y:c.y,px:prev[i].x,py:prev[i].y}));
    if (ate){
      score += 10;
      if (score>best) best=score;
      burst(food.x, food.y);
      food = spawnFood();
      if (!food){ /* board cleared → win */ gameOver(true); return; }
      updateHUD(true);
    }
  }
```
Wait: the "ate" case — prev has length len+1 = cells length ✓. The non-ate case: prev length len, cells length 1+(len-1)=len ✓.

Hmm, but in the ate case the collision check limit = len (the entire old snake) ✓ — correct, since the tail doesn't move.

Game over:
```js
  function gameOver(win){
    dead = true;
    state='over';
    if (score>best){best=score;}
    updateHUD();
    if (wasDemo){ ... } else { setOverlay('over'); setStatus('Game over','over'); }
  }
```
Need to know if we were in demo: capture `const wasDemo = state==='demo'` at the start of gameOver before changing state.

For the demo crash: after a short delay, restart the demo:
```js
  function gameOver(win){
    const wasDemo = (state==='demo');
    dead=true;
    state='over';
    if (score>best) best=score;
    updateHUD();
    shake();
    if (wasDemo){
      setStatus('AI demo restarting…','demo');
      demoResetTimer = setTimeout(()=>{ if (state==='over' && wasDemo){ startDemo(); } }, 1100);
    } else {
      setOverlay('over'); setStatus('Game over — press Space','over');
    }
  }
```
Careful with the closure capturing wasDemo ✓.

Hmm, but in the demo the overlay is hidden? No — in demo mode the overlay shows the start prompt. If the demo crashes, the overlay is still visible showing the prompt ✓. So for the demo crash we keep the overlay as is.

startDemo() calls setOverlay('demo') ✓.

Pause:
```js
  function pauseGame(){ state='paused'; setOverlay('paused'); setStatus('Paused','paused'); }
  function resumeGame(){ state='playing'; hideOverlay(); setStatus('You are in control','playing'); }
```
Note: when resuming, we should keep the accumulator so the snake doesn't jump ✓ (acc is preserved).

Overlay button behavior:
```js
  el.btn.addEventListener('click', (e)=>{
    e.stopPropagation();
    if (state==='paused') resumeGame(); else startPlayer();
  });
```
Canvas click:
```js
  canvas.addEventListener('pointerdown', ()=>{ if (state==='demo'||state==='over') startPlayer(); });
```
But when the overlay is visible in 'over' mode, clicking the button works; clicking elsewhere on the board (the overlay covers the board with pointer-events... the overlay has pointer-events auto by default in the visible state; clicking the overlay background doesn't start — let's add a click handler on the overlay itself: if the click target is the overlay (not the card), start the player). That covers "Click to start" ✓.

```js
  el.overlay.addEventListener('pointerdown', (e)=>{ if (e.target===el.overlay){ if(state==='demo'||state==='over') startPlayer(); } });
```

Touch swipe:
```js
  let touchStart=null;
  canvas.addEventListener('touchstart', e=>{ const t=e.changedTouches[0]; touchStart={x:t.clientX,y:t.clientY}; }, {passive:true});
  canvas.addEventListener('touchend', e=>{ if(!touchStart) return; const t=e.changedTouches[0]; const dx=t.clientX-touchStart.x, dy=t.clientY-touchStart.y; touchStart=null; if(Math.abs(dx)<18&&Math.abs(dy)<18) return; const d = Math.abs(dx)>Math.abs(dy)? (dx>0?{x:1,y:0}:{x:-1,y:0}) : (dy>0?{x:0,y:1}:{x:0,y:-1}); if(state==='demo'){startPlayer();} if(state==='playing') queueDir(d); }, {passive:true});
```
But pointerdown on the canvas would also fire for touch, starting the game — that's fine, but then the swipe direction would be applied after startPlayer. Since pointerdown fires on touch before touchend, the game starts on touch-down; then the swipe queues a direction ✓. Good.

Hmm, careful: the canvas pointerdown handler starts the player; but the overlay covers the canvas in demo mode, so the pointerdown on the canvas won't fire — the overlay handler handles it ✓.

Now the AI:

```js
  function aiDirection(){
    if (!food) return null;
    const blocked = new Set();
    for (let i=0;i<snake.length-1;i++) blocked.add(snake[i].x+','+snake[i].y);
    const head = snake[0];
    const seen = new Set([head.x+','+head.y]);
    const prev = new Map();
    const q = [head];
    let found = false;
    while (q.length){
      const cur = q.shift();
      if (cur.x===food.x && cur.y===food.y){ found=true; break; }
      for (const d of DIRS){
        const nx=cur.x+d.x, ny=cur.y+d.y;
        if (nx<0||nx>=COLS||ny<0||ny>=ROWS) continue;
        const k=nx+','+ny;
        if (seen.has(k) || blocked.has(k)) continue;
        seen.add(k);
        prev.set(k, {px:cur.x, py:cur.y, d:{x:d.x,y:d.y}});
        q.push({x:nx,y:ny});
      }
    }
    if (found){
      let k = food.x+','+food.y;
      let steps=0;
      while (prev.has(k) && steps<COLS*ROWS){
        const p = prev.get(k);
        if (p.px===head.x && p.py===head.y){ return p.d; }
        k = p.px+','+p.py;
        steps++;
      }
    }
    return fallbackDirection();
  }
```
Wait, the loop: prev maps cell → predecessor + the direction from the predecessor to the cell. Start at the food cell; p = prev.get(food) = the predecessor cell and the direction from that predecessor to the food. If the predecessor is the head → the first step direction ✓ (the direction from the head to the food cell). Otherwise move k to the predecessor and repeat. Eventually we find the entry whose predecessor is the head → return that direction ✓. But careful: the returned direction p.d is the direction from the predecessor to the cell (the food or the next cell) — which, when the predecessor is the head, is the direction from the head to the first step ✓ correct.

fallbackDirection: as described with flood fill.

```js
  function fallbackDirection(){
    const head=snake[0];
    let best=null, bestScore=-1;
    for (const d of DIRS){
      if (d.x===-dir.x && d.y===-dir.y) continue;
      const nx=head.x+d.x, ny=head.y+d.y;
      if (nx<0||nx>=COLS||ny<0||ny>=ROWS) continue;
      if (snake.slice(0,-1).some(c=>c.x===nx&&c.y===ny)) continue;
      const space = flood(nx,ny);
      if (space>bestScore){bestScore=space;best=d;}
    }
    return best;
  }
  function flood(sx,sy){
    const blocked=new Set(snake.map(c=>c.x+','+c.y));
    const seen=new Set([sx+','+sy]); const stack=[[sx,sy]]; let count=0;
    while(stack.length){
      const [x,y]=stack.pop(); count++;
      for(const d of DIRS){
        const nx=x+d.x, ny=y+d.y;
        if(nx<0||nx>=COLS||ny<0||ny>=ROWS) continue;
        const k=nx+','+ny;
        if(seen.has(k)||blocked.has(k)) continue;
        seen.add(k); stack.push([nx,ny]);
      }
    }
    return count;
  }
```

Now the rendering loop:

```js
  function frame(now){
    const dt = Math.min((now-last)/1000, 0.05);
    last = now;
    elapsed += dt;
    if (state==='playing'||state==='demo'){
      acc += dt*1000;
      let dur = stepDuration();
      let guard=0;
      while (acc>=dur && guard++<4){
        acc -= dur;
        tick();
        dur = stepDuration();
        if (state==='over'){acc=0;break;}
      }
    }
    updateEffects(dt);
    render(elapsed);
    requestAnimationFrame(frame);
  }
```

stepDuration: `Math.max(MIN_STEP, BASE_STEP - score*0.8)` → at score 0: 128ms; at score 500: 128-400 → clamped 78. Also for the demo, use a slightly slower base? Fine.

render(elapsed):

```js
  function render(t){
    ctx.clearRect(0,0,SIZE,SIZE);
    drawBoard(t);
    if (food) drawFood(t);
    drawSnake(t);
    drawEffects();
    if (flash>0){
      ctx.fillStyle=`rgba(255,255,255,${flash})`;
      ctx.fillRect(0,0,SIZE,SIZE);
    }
  }
```

Interpolation alpha: `const a = state==='over' ? 1 : clamp(acc/stepDuration(),0,1)`. Actually after game over, acc=0 → alpha 0, so the snake would jump back to the previous positions. Better: when game over happens, set a flag to keep alpha at 1 (the final position). Let's store `const alpha = dead ? 1 : clamp(...)`. On game over, dead=true → alpha=1 → the snake renders at the final positions ✓.

But dead is also set true at the start of gameOver; and resetGame sets dead=false ✓.

Hmm, but there's an issue: the snake at game over is at the position before the fatal move (the head didn't move into the wall). The head is drawn at its last valid cell ✓, which is fine — we could show the head "hitting" the wall. To convey the crash, flash red and shake ✓.

drawSnake:

```js
  function drawSnake(t){
    if (!snake.length) return;
    const alpha = dead ? 1 : Math.min(1, Math.max(0, acc/stepDuration()));
    const pts = snake.map(s=>({
      x:(s.px+(s.x-s.px)*alpha+0.5)*CELL,
      y:(s.py+(s.y-s.py)*alpha+0.5)*CELL
    }));
    const head=pts[0], tail=pts[pts.length-1];
    // shadow
    ctx.save();
    ctx.translate(2,5);
    ctx.strokeStyle='rgba(0,0,0,0.35)';
    ctx.lineWidth=CELL*0.72;
    strokePath(pts);
    ctx.restore();
    // body gradient
    const g=ctx.createLinearGradient(tail.x,tail.y,head.x,head.y);
    ...
    ctx.strokeStyle=g; ctx.lineWidth=CELL*0.70; strokePath(pts);
    // highlight
    ctx.save(); ctx.globalAlpha=.16; ctx.strokeStyle='#ffffff'; ctx.lineWidth=CELL*0.18; strokePath(pts); ctx.restore();
```
Hmm, a centered white stripe might look odd but usually looks like a glossy ridge. Let's offset the highlight slightly? We can translate the context by (-1,-2) for the highlight to simulate a top-left light. Let's do ctx.translate(-1.5,-2.5) before stroking the highlight.

Then scales dots (skip maybe). Then the head.

strokePath(pts): 
```js
  function strokePath(pts){
    ctx.beginPath();
    ctx.moveTo(pts[0].x,pts[0].y);
    for(let i=1;i<pts.length;i++) ctx.lineTo(pts[i].x,pts[i].y);
    ctx.stroke();
  }
```
Note pts[0] is the head; drawing from head to tail is the same path ✓.

Wait: pts order is head→tail, so the gradient from tail to head is correct ✓.

Head drawing with eyes.

Let me write drawHead(head, t):
```js
    const h = CELL*0.47;
    ctx.save();
    ctx.translate(head.x, head.y);
    // glow
    ctx.shadowColor = dead? 'rgba(255,70,70,.75)':'rgba(74,222,128,.55)';
    ctx.shadowBlur = 16;
    roundRect(ctx,-h,-h,h*2,h*2,h*0.5);
    const hg=ctx.createLinearGradient(-h,-h,h,h);
    if (dead){ hg.addColorStop(0,'#ffb4b4'); hg.addColorStop(.5,'#ef4444'); hg.addColorStop(1,'#991b1b'); }
    else { hg.addColorStop(0,'#d9ffe9'); hg.addColorStop(.45,'#6ee7b7'); hg.addColorStop(1,'#14b87a'); }
    ctx.fillStyle=hg; ctx.fill();
    ctx.shadowBlur=0;
    ctx.strokeStyle='rgba(255,255,255,.45)'; ctx.lineWidth=1.4; ctx.stroke();
    // eyes
    const dx=dir.x, dy=dir.y;
    const pxv=-dy, pyv=dx;
    for (const s of [1,-1]){
      const ex = pxv*h*0.42*s + dx*h*0.16;
      const ey = pyv*h*0.42*s + dy*h*0.16;
      ctx.fillStyle='#f8fffb';
      ctx.beginPath(); ctx.ellipse(ex,ey,h*0.27,h*0.30,0,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#062018';
      ctx.beginPath(); ctx.arc(ex+dx*h*0.11, ey+dy*h*0.11, h*0.135,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='rgba(255,255,255,.9)';
      ctx.beginPath(); ctx.arc(ex+dx*h*0.15 - pyv*0.02, ey+dy*h*0.15 - pxv*0.02, h*0.05,0,Math.PI*2); ctx.fill();
    }
    // tongue
    const flick = Math.sin(t/130);
    if (flick > 0.55){
      ctx.strokeStyle='#ff5d8f'; ctx.lineWidth=2.2;
      const bx=dx*h*0.92, by=dy*h*0.92;
      ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(bx+dx*h*0.42, by+dy*h*0.42); ctx.stroke();
      // fork
      ...
    }
    ctx.restore();
```

roundRect helper:
```js
  function roundRect(c,x,y,w,h,r){
    c.beginPath();
    c.moveTo(x+r,y);
    c.arcTo(x+w,y,x+w,y+h,r);
    c.arcTo(x+w,y+h,x,y+h,r);
    c.arcTo(x,y+h,x,y,r);
    c.arcTo(x,y,x+w,y,r);
    c.closePath();
  }
```
Note: if r > h/2 it breaks; use r = min(r, w/2, h/2) ✓.

drawEffects: particles, ripples, popups.

```js
  function drawEffects(){
    for (const r of ripples){
      const k=1-r.life/r.max;
      ctx.strokeStyle=`rgba(255,140,180,${(1-k)*0.55})`;
      ctx.lineWidth=3*(1-k)+0.5;
      ctx.beginPath(); ctx.arc(r.x,r.y,CELL*0.25+k*CELL*1.1,0,Math.PI*2); ctx.stroke();
    }
    for (const p of particles){
      const a=Math.max(0,p.life/p.max);
      ctx.globalAlpha=a;
      ctx.fillStyle=p.color;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(0.4+0.6*a),0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha=1;
    for (const p of popups){
      const a=Math.max(0,p.life/p.max);
      ctx.globalAlpha=a;
      ctx.font='800 18px ui-sans-serif, system-ui, sans-serif';
      ctx.textAlign='center';
      ctx.fillStyle='#ffd166';
      ctx.shadowColor='rgba(0,0,0,.6)'; ctx.shadowBlur=6;
      ctx.fillText(p.text, p.x, p.y - (1-a)*26);
      ctx.shadowBlur=0;
    }
    ctx.globalAlpha=1;
  }
```

Board shake: CSS class on .board: 'shake' → animation. Add/remove with a timeout.

Let me also make the board have a subtle animated border glow on eating? Skip.

One more important consideration: the demo must be running at first paint and visibly moving. The overlay is visible in demo mode. Good.

Also: "Games must start in a self-playing demo mode behind a 'Click or press a key to start' message" ✓.

Potential issue: the user might click the "Play now" button → startPlayer → the demo's score resets to 0 and best remains. ✓

Let me also handle the case where the AI reaches the board limit (unlikely).

Let me also add: when the player starts, maybe show a brief "GO!" flash? Not necessary.

Let me now write the final HTML.

Also, I should double check the HUD "best score of the session": best is a variable that persists across restarts within the page session ✓ (no localStorage).

One more: the score display should update during the demo — the demo's score counts. But when the player starts, score resets to 0. Some might think the score should keep counting in the demo. It's fine.

Hmm, actually maybe better: don't reset the score when the player takes over? That would give the player a free head start of 10-50 points, which seems wrong. Reset is standard.

Let me write out the final code now, being careful with syntax.

Also I want to add a "level/speed" indicator maybe in the footer: "Speed 1.0x". Nice polish. Let's include in the status pill: "Playing · 1.4×". Compute speed = BASE_STEP/stepDuration.

Let's finalize the footer:
```html
<footer class="foot">
  <div class="keys">
    <span class="klabel">Move</span> <kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd> <span class="or">or</span> <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>
    <span class="klabel">Pause</span> <kbd>P</kbd>
    <span class="klabel">Restart</span> <kbd>Space</kbd>
  </div>
  <div class="status" id="statusPill" data-state="demo"><span class="dot"></span><span id="statusText">AI demo</span></div>
</footer>
```

Now writing the final code. Let me make sure the HTML structure for the overlay card:

```html
<div class="overlay" id="overlay" data-mode="demo">
  <div class="card" role="dialog" aria-live="polite">
    <span class="badge" id="badge">AI demo</span>
    <h2 id="ovTitle">Click or press a key to start</h2>
    <p id="ovText">The snake is playing by itself — grab the keys to take over.</p>
    <p class="hint" id="ovHint"></p>
    <button class="btn" id="ovBtn" type="button">Play now</button>
  </div>
</div>
```

CSS for .card h2 {margin: 10px 0 0; font-size: clamp(17px, 2.4vw, 21px); font-weight:800; letter-spacing:.01em}
.card p {margin:6px 0 0; font-size:13px; color:var(--muted); line-height:1.45}
.card .hint{margin-top:8px;font-size:11.5px;color:rgba(232,241,255,.45)}
.card .btn{display:inline-block; margin-top:12px}

In demo mode, the title should be prominent: font-size clamp(16px,2.6vw,20px).

For the over mode: the title "Score 120" → format as `<span class="big">${score}</span> points`. Let's do: `el.title.innerHTML = 'Score <span class="big">'+score+'</span>'`.

Ok, and the badge for over: "Game over" with danger styling.

Let me write the CSS for the badge danger:
```css
.badge.danger{background:rgba(255,93,143,.14);border-color:rgba(255,93,143,.4);color:#ffc0d2}
.badge.danger::before{background:#ff5d8f;box-shadow:0 0 8px #ff5d8f;animation:none}
```

Paused badge: maybe use the default green? Use a neutral/amber. Add class 'warn':
```css
.badge.warn{background:rgba(255,209,102,.14);border-color:rgba(255,209,102,.4);color:#ffe9b0}
.badge.warn::before{background:#ffd166;box-shadow:0 0 8px #ffd166;animation:none}
```

Now, write everything. I'll be thorough but careful.

Also, the status pill dot colors:
```css
.status[data-state="demo"] .dot{background:#38bdf8;box-shadow:0 0 10px #38bdf8}
.status[data-state="playing"] .dot{background:#4ade80;box-shadow:0 0 10px #4ade80}
.status[data-state="paused"] .dot{background:#ffd166;box-shadow:0 0 10px #ffd166}
.status[data-state="over"] .dot{background:#ff5d8f;box-shadow:0 0 10px #ff5d8f}
```
The default .dot is green.

Let me write the final answer.

One more check on the smooth rendering math with the `prev` mapping. Example: snake cells [A(5,12), B(4,12), C(3,12)], prev same. Moving right: newHead (6,12). keep = 2 (not ate) → cells = [(6,12),(5,12),(4,12)]; prev = snake cells [(5,12),(4,12),(3,12)]. So:
- segment0: from (5,12) to (6,12) ✓
- segment1: from (4,12) to (5,12) ✓
- segment2: from (3,12) to (4,12) ✓
All move right smoothly ✓.

Ate case: cells = [(6,12),(5,12),(4,12),(3,12)]; prev = [(5,12),(4,12),(3,12),(3,12)]:
- seg0: (5,12)→(6,12) ✓
- seg1: (4,12)→(5,12) ✓
- seg2: (3,12)→(4,12) ✓
- seg3: (3,12)→(3,12) stays ✓
Good.

But note at the start of a step, alpha=0, the head is at (5,12) and the previous render (end of last step, alpha=1) had the head at (5,12) ✓ continuous.

However, there's an issue with the initial reset: the first tick happens with alpha ramping from 0 → the head moves from (5,12) to (6,12) ✓.

The body path with round joins: when the snake turns, the polyline corner is rounded by lineJoin='round' ✓.

Also the tail cap is round ✓.

Everything seems fine. Another check: when the snake is length 1 (impossible; min 6).

Let me also make sure the drawn path doesn't include a zero-length segment issue — fine.

Potential visual issue: the gradient from tail to head when the snake is folded — a linear gradient across the bbox may look odd but acceptable.

Let me improve: use a gradient along the direction of travel? Keep it simple.

Now write the code. I'll write it out fully.

Also add `user-select:none` on the app to avoid text selection during rapid key presses.

Also disable the context menu on the canvas? Not needed.

Final detail: `ctx = canvas.getContext('2d', {alpha:false})` → with alpha:false, clearRect makes it black; but we fill the background fully anyway ✓.

Let me write the file.

For the requestAnimationFrame loop, call `requestAnimationFrame(frame)` at the end.

Also, call startDemo() before the first frame.

I need to double check the "acc" logic with a changing step duration: `while (acc>=dur)` — if the score increases, dur decreases and the leftover acc is applied ✓.

Let me also ensure that in the 'over' state the overlay shows and the snake stops ✓.

Let me write out the code now in final form.

Regarding the initial overlay state: data-mode="demo" and the class list without 'hidden' ✓.

Also `el.hint` in demo mode should show key hints; in over mode, show "Space / Enter to restart".

Let me write setOverlay to handle all cases with the hint line.

Finally, the total code. Let me write it.

```js
  function setStatus(text, kind){
    el.status.textContent = text;
    el.statusPill.dataset.state = kind;
  }
```

Also update the status text with the speed: e.g., "Playing · 1.0×". I'll update the status each frame? That's costly-ish but fine (only when changed). Let's update it in tick: `setStatus('Playing · '+(BASE_STEP/stepDuration()).toFixed(1)+'×','playing')` — but calling DOM text each tick is OK (8 times/sec). Let's just update on state changes and on score change. I'll do it in tick after eating.

Let me write everything now.

One more consideration about the canvas CSS size vs. the internal resolution: the canvas element has width/height attributes set to SIZE*DPR (e.g., 1200) and CSS width:100%; height:100% of the board (which has aspect-ratio 1) ✓.

But careful: the .board has `aspect-ratio:1/1` and a computed width; the canvas with width:100%;height:100% fills it ✓. If height isn't determined... With aspect-ratio on .board and width set, the height is computed ✓. The canvas height:100% resolves against the board's height ✓ (the board has a definite height from aspect-ratio). Should be fine in Chrome. To be safe, add `.board{...} canvas{position:absolute;inset:0;width:100%;height:100%}` — with the canvas absolutely positioned, the board's aspect-ratio still determines the height ✓. I'll use position:absolute for the canvas and the overlay to be safe.

Hmm, with the canvas absolutely positioned and inset:0, the width/height are 100% of the board's padding box ✓.

Let me make .board position:relative ✓.

Writing now.

For the SVG logo, define a gradient with a unique id.

Final code below. Let me carefully compose.

Also note: `Math.min(window.devicePixelRatio||1,2)` — if the DPR changes (zoom), ignore.

I'll now produce the final answer with the complete HTML.

Let me double check one thing: `e.key` for space is ' ' ✓; for Enter it's 'Enter' ✓.

Also prevent the default for the arrow keys to stop page scrolling ✓ (preventDefault in keydown).

Finally, the demo's first paint: the overlay shows the prompt. But the requirement says "behind a 'Click or press a key to start' message" — the AI plays behind it ✓.

Let me make the demo card positioned at the bottom so the AI's snake (which starts in the middle-left) is visible.

OK writing the final answer.

Additional polish: add a subtle "glow ring" pulse around the food and a tiny drop shadow under the snake head.

Also, maybe add a small trail of "sparkles" occasionally from the head? Skip.

Let me write the code.

function burst(x,y) — the food position in canvas coords.

I also want the score HUD to pulse green on eating ✓ via bump.

Final: write the answer.