Add visual effects for swiping in the game in a new updated single HTML file: Colorful snake game

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover"
  />
  <meta name="description" content="Play Cyber-Neon Snake, a modern twist on the classic snake game with vibrant visuals and effects.">
  <title>Cyber‑Neon Snake</title>
  <style>
    :root {
      --neon-green: #0f9;
      --neon-green-light: #0cffaa;
      --neon-green-dark: #0c7;
      --neon-cyan: #0ff;
      --neon-cyan-medium: #0cc; /* For hover/focus states */
      --neon-cyan-dark: #099;   /* For active states */
      
      --text-color-on-neon: #000;
      --text-color-primary: var(--neon-green);
      --text-color-flash: #fff;

      --bg-dark-primary: #111;
      --bg-gradient-1: #0f2027;
      --bg-gradient-2: #203a43;
      --bg-gradient-3: #2c5364;
      --bg-grid-lines: #222;
      --bg-modal-overlay: rgba(0,0,0,0.85);

      --border-radius-lg: 20px;
      --border-radius-md: 12px;

      --font-primary: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }

    /* Reset & Base */
    *, *::before, *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    html, body {
      height: 100%;
      overflow: hidden;
      user-select: none; /* Applied globally */
      background: linear-gradient(45deg, var(--bg-gradient-1), var(--bg-gradient-2), var(--bg-gradient-3));
      background-size: 600% 600%;
      animation: bgShift 20s ease infinite;
      font-family: var(--font-primary);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      color: var(--text-color-primary);
    }
    @keyframes bgShift {
      0% { background-position: 0% 50%; }
      50% { background-position: 100% 50%; }
      100% { background-position: 0% 50%; }
    }

    /* Game Frame */
    #game {
      position: relative;
      width: 90vmin;
      height: 90vmin;
      background: var(--bg-dark-primary);
      border: 4px solid var(--neon-green);
      border-radius: var(--border-radius-lg);
      box-shadow: inset 0 0 20px rgba(0, 255, 153, 0.66); /* #0f9a */
      touch-action: none;
      outline: none; /* For the main game container itself if focused */
    }
    canvas {
      width: 100%;
      height: 100%;
      display: block;
      background: var(--bg-dark-primary); /* Match game background */
      border-radius: calc(var(--border-radius-lg) - 4px); /* Account for border */
    }

    /* Neon Scoreboard */
    @keyframes neonFlicker {
      0%, 18%, 22%, 25%, 53%, 57%, 100% {
        text-shadow:
          0 0 5px var(--neon-green),
          0 0 10px var(--neon-green),
          0 0 20px var(--neon-green),
          0 0 40px var(--neon-cyan),
          0 0 80px var(--neon-cyan);
      }
      20%, 24%, 55% {
        text-shadow: none;
      }
    }
    #scoreboard {
      margin-top: 12px;
      text-align: center;
      font-size: 1.5rem;
      font-weight: 700;
      animation: neonFlicker 3s infinite;
    }
    #scoreboard.flash {
      color: var(--text-color-flash);
      text-shadow:
        0 0 25px var(--neon-cyan),
        0 0 50px var(--neon-cyan),
        0 0 75px var(--neon-cyan);
      transition: text-shadow 0.2s, color 0.2s;
    }

    /* Controls Buttons */
    .btn {
      background: var(--neon-green);
      border: none;
      border-radius: var(--border-radius-md);
      padding: 14px 0;
      font-size: 1.4rem;
      font-weight: 700;
      box-shadow: 0 0 20px var(--neon-green);
      cursor: pointer;
      transition: background 0.2s, box-shadow 0.2s, transform 0.1s, outline-color 0.2s;
      color: var(--text-color-on-neon);
      outline: 2px solid transparent; /* Base for focus transition */
      outline-offset: 2px;
    }
    .btn:hover,
    .btn:focus-visible {
      background: var(--neon-green-light);
      box-shadow: 0 0 30px var(--neon-green-light);
      outline-color: var(--neon-green-light);
      transform: scale(1.05);
    }
    .btn:active {
      background: var(--neon-green-dark);
      box-shadow: inset 0 0 30px var(--neon-green-dark);
      transform: scale(0.95);
    }
    #controls { /* Grid for control buttons */
      margin-top: 12px;
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 12px;
      width: 90vmin;
      max-width: 400px;
    }
    @media (max-width: 400px) {
      .btn {
        font-size: 1.2rem;
        padding: 10px 0;
      }
    }

    /* Restart button (page) & Modal Button (shared styles) */
    .action-button { /* Common base for restart/modal buttons */
      background: var(--neon-cyan);
      color: var(--text-color-on-neon);
      border: none;
      border-radius: var(--border-radius-md);
      padding: 12px 0;
      font-size: 1.3rem;
      font-weight: 700;
      box-shadow: 0 0 25px var(--neon-cyan);
      cursor: pointer;
      transition: background 0.3s, box-shadow 0.3s, transform 0.2s, outline-color 0.2s;
      outline: 2px solid transparent; /* Base for focus transition */
      outline-offset: 2px;
    }
    .action-button:hover,
    .action-button:focus-visible {
      background: var(--neon-cyan-medium);
      box-shadow: 0 0 40px var(--neon-cyan-medium);
      outline-color: var(--neon-cyan-medium);
      transform: scale(1.05);
    }
    .action-button:active {
      background: var(--neon-cyan-dark);
      box-shadow: inset 0 0 30px var(--neon-cyan-dark);
      transform: scale(0.95);
    }

    #restartBtn {
      composes: action-button; /* Conceptual, apply class in HTML */
      margin-top: 16px;
      width: 90vmin;
      max-width: 400px;
      display: none; /* Controlled by JS */
    }

    /* Game Over Modal */
    #gameOverModal {
      position: fixed;
      top: 0; left: 0; right: 0; bottom: 0;
      background: var(--bg-modal-overlay);
      display: flex;
      align-items: center;
      justify-content: center;
      color: var(--text-color-primary);
      font-size: 2rem;
      font-weight: 700;
      text-align: center;
      padding: 1rem;
      z-index: 1000;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.3s ease;
      /* border-radius applied to inner content if needed, not overlay */
    }
    #gameOverModal.show {
      opacity: 1;
      pointer-events: auto;
    }
    #gameOverModal div { /* Inner content wrapper */
        background: var(--bg-dark-primary);
        padding: 2rem;
        border-radius: var(--border-radius-md);
        border: 2px solid var(--neon-cyan);
        box-shadow: 0 0 30px var(--neon-cyan);
    }
    #gameOverModal h2 {
        margin-bottom: 0.5rem;
    }
    #gameOverModal p {
        font-size: 1.2rem; /* Smaller than title */
        font-weight: normal;
        margin-bottom: 1.5rem;
    }
    #gameOverModal button { /* Specifically the button inside the modal */
      composes: action-button; /* Conceptual, apply class in HTML */
      margin-top: 1rem; /* Overrides if any from action-button */
      padding: 12px 24px; /* Specific padding for modal button */
      /* width: auto; /* Allow content to size it */
    }
  </style>
</head>
<body>
  <main id="game" role="region" aria-label="Snake game area" tabindex="0">
    <canvas id="canvas" aria-label="Game canvas"></canvas> <!-- aria-live removed, scoreboard handles announcements -->
  </main>

  <nav id="controls" aria-label="Directional controls">
    <button class="btn" data-dir="left" aria-label="Move left" type="button">←</button>
    <button class="btn" data-dir="up" aria-label="Move up" type="button">↑</button>
    <button class="btn" data-dir="down" aria-label="Move down" type="button">↓</button>
    <button class="btn" data-dir="right" aria-label="Move right" type="button">→</button>
  </nav>

  <div id="scoreboard" aria-live="polite" aria-atomic="true" role="status">
    Score: <span id="currentScore">0</span> | High: <span id="highScore">0</span>
  </div>

  <button id="restartBtn" class="action-button" type="button" aria-label="Restart game">Restart Game</button>

  <!-- Game Over Modal -->
  <div id="gameOverModal" role="dialog" aria-modal="true" aria-labelledby="gameOverTitle" aria-describedby="gameOverDesc" tabindex="-1">
    <div>
      <h2 id="gameOverTitle">Game Over!</h2>
      <p id="gameOverDesc">Your final score is <span id="finalScore">0</span>.</p>
      <button id="modalRestartBtn" class="action-button" type="button">Play Again</button>
    </div>
  </div>

  <!-- Sound effect file must be in same folder -->
  <audio id="eatSfx" src="eat.mp3" preload="auto" aria-hidden="true"></audio>

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

    // --- Game Configuration ---
    const GAME_CONFIG = Object.freeze({
      TILE_COUNT: 24,
      FPS: 10, // Target frames per second for game logic
      MAX_APPLES: 3,
      SNAKE_INITIAL_LENGTH: 3,
      SNAKE_INITIAL_POS: { x: 12, y: 12 }, // Head position
      INITIAL_VELOCITY: { x: 1, y: 0 }, // Moving right
      POINTS_PER_APPLE: 10,
      TIME_BONUS_MS: 3000, // Milliseconds for time bonus window
      TIME_BONUS_POINTS: 5,
      COMBO_BONUS_MULTIPLIER: 2,
      PARTICLE_COUNT_ON_EAT: 12,
      MIN_SWIPE_DISTANCE: 30, // Pixels
      LOCAL_STORAGE_HIGH_SCORE_KEY: 'cyberNeonSnakeHighScore_v2', // v2 to avoid conflicts if structure changed
      // Visuals for JS drawing
      SNAKE_SEGMENT_PADDING_FACTOR: 0.05, // e.g., 5% of gridSize
      SNAKE_SEGMENT_RADIUS_FACTOR: 0.2,   // e.g., 20% of gridSize
      APPLE_BODY_RADIUS_X_FACTOR: 0.4,
      APPLE_BODY_RADIUS_Y_FACTOR: 0.55,
      APPLE_STEM_OFFSET_Y_FACTOR: -0.3,
      APPLE_STEM_RADIUS_X_FACTOR: 0.15,
      APPLE_STEM_RADIUS_Y_FACTOR: 0.3,
    });

    // --- DOM & Audio References ---
    const gameEl     = document.getElementById('game');
    const canvas     = document.getElementById('canvas');
    const ctx        = canvas.getContext('2d');
    const scoreEl    = document.getElementById('currentScore');
    const highEl     = document.getElementById('highScore');
    const scoreboardEl = document.getElementById('scoreboard');
    const eatSfx     = document.getElementById('eatSfx');
    const controlBtns= document.querySelectorAll('#controls .btn'); // More specific selector
    const restartBtn = document.getElementById('restartBtn');
    const gameOverModalEl = document.getElementById('gameOverModal');
    const finalScoreEl = document.getElementById('finalScore');
    const modalRestartBtn = document.getElementById('modalRestartBtn');

    // --- Game State Variables ---
    let gridSize; // Size of each grid cell in pixels
    let snake, velocity, nextVel, apples, score, gameOver;
    let comboCounter, lastAppleTime, hue; // hue for dynamic coloring
    let effects; // For particles and score pop-ups
    
    let highScore = 0;
    try {
      highScore = parseInt(localStorage.getItem(GAME_CONFIG.LOCAL_STORAGE_HIGH_SCORE_KEY), 10) || 0;
    } catch (e) {
      console.warn(`Could not access localStorage to retrieve high score: ${e.message}`);
    }
    highEl.textContent = highScore;

    // Timing for fixed FPS
    let lastTime = 0;
    let accumulatedTime = 0; // Renamed delta for clarity
    const gameLogicInterval = 1000 / GAME_CONFIG.FPS;

    // --- Game Initialization & Reset ---
    function initGame() {
      snake = [];
      for (let i = 0; i < GAME_CONFIG.SNAKE_INITIAL_LENGTH; i++) {
        // Snake grows backwards from the head
        snake.push({ 
          x: GAME_CONFIG.SNAKE_INITIAL_POS.x - i * GAME_CONFIG.INITIAL_VELOCITY.x, // Assuming initial X velocity
          y: GAME_CONFIG.SNAKE_INITIAL_POS.y - i * GAME_CONFIG.INITIAL_VELOCITY.y  // Assuming initial Y velocity
        });
      }
      // Ensure the head is at SNAKE_INITIAL_POS if velocity is not (1,0) or (0,1) initially.
      // The current logic works for {x:1, y:0} or {x:0, y:1}. For {-1,0} or {0,-1} it would be +i.
      // Correcting for generic initial velocity for segment placement:
      snake = [];
      const initialHeadX = GAME_CONFIG.SNAKE_INITIAL_POS.x;
      const initialHeadY = GAME_CONFIG.SNAKE_INITIAL_POS.y;
      for (let i = 0; i < GAME_CONFIG.SNAKE_INITIAL_LENGTH; i++) {
          snake.push({
              x: initialHeadX - i * (GAME_CONFIG.INITIAL_VELOCITY.x !== 0 ? Math.sign(GAME_CONFIG.INITIAL_VELOCITY.x) : 0),
              y: initialHeadY - i * (GAME_CONFIG.INITIAL_VELOCITY.y !== 0 ? Math.sign(GAME_CONFIG.INITIAL_VELOCITY.y) : 0)
          });
      }


      velocity = { ...GAME_CONFIG.INITIAL_VELOCITY };
      nextVel = { ...GAME_CONFIG.INITIAL_VELOCITY };
      apples = [];
      score = 0;
      comboCounter = 0;
      lastAppleTime = 0;
      hue = 0; // Reset hue for color cycle
      effects = { score: [], particles: [] };
      gameOver = false;
      
      scoreEl.textContent = score;
      restartBtn.style.display = 'none';
      gameOverModalEl.classList.remove('show');
      
      spawnApples();
      gameEl.focus(); // For keyboard controls
    }

    // --- Canvas Sizing ---
    function resizeCanvas(){
      const gameAreaSize = gameEl.clientWidth; // Use clientWidth of the #game container
      canvas.width  = gameAreaSize;
      canvas.height = gameAreaSize;
      gridSize      = gameAreaSize / GAME_CONFIG.TILE_COUNT;
      // Re-draw immediately after resize if game is active
      if (!gameOver && snake) draw(); 
    }
    window.addEventListener('resize', resizeCanvas);

    // --- Drawing Helpers ---
    function dynamicColorGradient(isSnakeSegment){
      hue = (hue + 1) % 360; // Slower hue shift
      if(isSnakeSegment){
        const c1 = `hsl(${hue}, 100%, 50%)`;
        const c2 = `hsl(${(hue + 40) % 360}, 100%, 60%)`;
        const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        gradient.addColorStop(0, c1);
        gradient.addColorStop(1, c2);
        return gradient;
      } else { // For apples
        const gradient = ctx.createRadialGradient(0, 0, gridSize * 0.1, 0, 0, gridSize * 0.6);
        gradient.addColorStop(0, `hsl(${hue}, 100%, 70%)`);
        gradient.addColorStop(1, `hsl(${(hue + 60) % 360}, 80%, 40%)`);
        return gradient;
      }
    }

    function drawRoundedRect(x, y, width, height, radius){
      ctx.beginPath();
      ctx.moveTo(x + radius, y);
      ctx.lineTo(x + width - radius, y);
      ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
      ctx.lineTo(x + width, y + height - radius);
      ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
      ctx.lineTo(x + radius, y + height);
      ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
      ctx.lineTo(x, y + radius);
      ctx.quadraticCurveTo(x, y, x + radius, y);
      ctx.closePath();
      ctx.fill();
    }

    function isCellEmpty(x, y){
      return !snake.some(segment => segment.x === x && segment.y === y) &&
             !apples.some(apple => apple.x === x && apple.y === y);
    }

    // --- Apple Management ---
    function spawnApples(){
      while(apples.length < GAME_CONFIG.MAX_APPLES){
        const newApple = {
          x: Math.floor(Math.random() * GAME_CONFIG.TILE_COUNT),
          y: Math.floor(Math.random() * GAME_CONFIG.TILE_COUNT)
        };
        if(isCellEmpty(newApple.x, newApple.y)) {
          apples.push(newApple);
        }
      }
    }

    // --- Visual Effects ---
    function addScoreEffect(x, y, amount){
      effects.score.push({
        x: x * gridSize + gridSize / 2,
        y: y * gridSize + gridSize / 2,
        alpha: 1, text: `+${amount}`, size: 24, life: 60 // life in frames
      });
    }

    function addEatParticles(x, y){
      for(let i = 0; i < GAME_CONFIG.PARTICLE_COUNT_ON_EAT; i++){
        effects.particles.push({
          x: x * gridSize + gridSize / 2,
          y: y * gridSize + gridSize / 2,
          dx: (Math.random() - 0.5) * 8, // Velocity x
          dy: (Math.random() - 0.5) * 8, // Velocity y
          alpha: 1, size: Math.random() * 4 + 2, life: 30 // life in frames
        });
      }
    }

    function updateEffects(){
      // Score pop-ups
      for(let i = effects.score.length - 1; i >= 0; i--){
        const effect = effects.score[i];
        effect.y -= 1; // Speed of rising
        effect.alpha -= (1 / effect.life); // Fade out based on lifetime
        effect.size += 0.2;
        if(effect.alpha <= 0) effects.score.splice(i, 1);
      }
      // Particles
      for(let i = effects.particles.length - 1; i >= 0; i--){
        const particle = effects.particles[i];
        particle.x += particle.dx;
        particle.y += particle.dy;
        particle.alpha -= (1 / particle.life); // Fade out
        particle.color = `hsla(${(hue + i * 20) % 360}, 100%, 70%, ${particle.alpha})`;
        if(particle.alpha <= 0) effects.particles.splice(i, 1);
      }
    }

    function flashScoreboard(){
      scoreboardEl.classList.add('flash');
      setTimeout(() => scoreboardEl.classList.remove('flash'), 200); // Duration of flash
      try {
        eatSfx.currentTime = 0; // Rewind to start
        eatSfx.play();
      } catch(e) {
        console.warn("Audio playback failed (eatSfx):", e.message);
      }
    }

    // --- Game Logic Update ---
    function updateGameLogic(){
      if(gameOver) return;

      // Apply pending direction change if valid
      // Prevents reversing directly into itself
      if (!((nextVel.x === -velocity.x && velocity.x !==0) || (nextVel.y === -velocity.y && velocity.y !==0))) {
        velocity = { ...nextVel };
      }
      
      const head = {
        x: (snake[0].x + velocity.x + GAME_CONFIG.TILE_COUNT) % GAME_CONFIG.TILE_COUNT,
        y: (snake[0].y + velocity.y + GAME_CONFIG.TILE_COUNT) % GAME_CONFIG.TILE_COUNT
      };

      // Self-collision check
      // Skip checking the very last segment if it's about to be removed
      const checkCollisionAgainst = snake.slice(0, snake.length -1);
      if(checkCollisionAgainst.some(segment => segment.x === head.x && segment.y === head.y)){
        gameOver = true;
        displayGameOver();
        return;
      }

      snake.unshift(head); // Add new head

      let ateApple = false;
      for(let i = apples.length - 1; i >= 0; i--){ // Iterate backwards for safe removal
        if(apples[i].x === head.x && apples[i].y === head.y){
          const currentTime = Date.now();
          const timeBonus = (currentTime - lastAppleTime < GAME_CONFIG.TIME_BONUS_MS) ? GAME_CONFIG.TIME_BONUS_POINTS : 0;
          const comboBonus = comboCounter * GAME_CONFIG.COMBO_BONUS_MULTIPLIER;
          const pointsGained = GAME_CONFIG.POINTS_PER_APPLE + timeBonus + comboBonus;
          
          score += pointsGained;
          comboCounter++;
          lastAppleTime = currentTime;
          scoreEl.textContent = score;

          if(score > highScore){
            highScore = score;
            try {
              localStorage.setItem(GAME_CONFIG.LOCAL_STORAGE_HIGH_SCORE_KEY, highScore.toString());
            } catch (e) {
              console.warn(`Could not save high score to localStorage: ${e.message}`);
            }
            highEl.textContent = highScore;
          }
          
          addScoreEffect(head.x, head.y, pointsGained);
          addEatParticles(head.x, head.y);
          flashScoreboard();
          
          apples.splice(i, 1);
          spawnApples();
          ateApple = true;
          break; 
        }
      }

      if(!ateApple){
        comboCounter = 0; // Reset combo if no apple eaten
        snake.pop(); // Remove tail
      }
    }

    // --- Drawing Functions ---
    function drawGame(){
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // Draw grid (optional, for debugging or style)
      ctx.strokeStyle = 'var(--bg-grid-lines)';
      ctx.lineWidth = 1;
      for(let i = 0; i <= GAME_CONFIG.TILE_COUNT; i++){
        const pos = i * gridSize;
        ctx.beginPath(); ctx.moveTo(pos, 0); ctx.lineTo(pos, canvas.height); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(0, pos); ctx.lineTo(canvas.width, pos); ctx.stroke();
      }

      // Draw apples
      apples.forEach(apple => {
        const appleCenterX = apple.x * gridSize + gridSize / 2;
        const appleCenterY = apple.y * gridSize + gridSize / 2;
        ctx.save();
        ctx.translate(appleCenterX, appleCenterY);
        
        ctx.shadowColor = '#f44'; // Red glow for apples
        ctx.shadowBlur = 18;
        ctx.fillStyle = dynamicColorGradient(false); // Use apple part of gradient
        
        // Apple body
        ctx.beginPath();
        ctx.ellipse(0, 0, 
                    gridSize * GAME_CONFIG.APPLE_BODY_RADIUS_X_FACTOR, 
                    gridSize * GAME_CONFIG.APPLE_BODY_RADIUS_Y_FACTOR, 
                    Math.PI / 6, 0, 2 * Math.PI);
        ctx.fill();
        
        // Apple stem
        ctx.fillStyle = '#282'; // Dark green for stem
        ctx.beginPath();
        ctx.ellipse(0, gridSize * GAME_CONFIG.APPLE_STEM_OFFSET_Y_FACTOR, 
                    gridSize * GAME_CONFIG.APPLE_STEM_RADIUS_X_FACTOR, 
                    gridSize * GAME_CONFIG.APPLE_STEM_RADIUS_Y_FACTOR, 
                    Math.PI / 4, 0, 2 * Math.PI);
        ctx.fill();
        ctx.restore();
      });

      // Draw snake
      const segmentPadding = gridSize * GAME_CONFIG.SNAKE_SEGMENT_PADDING_FACTOR;
      const segmentRadius = gridSize * GAME_CONFIG.SNAKE_SEGMENT_RADIUS_FACTOR;
      snake.forEach((segment, index) => {
        const segX = segment.x * gridSize;
        const segY = segment.y * gridSize;
        ctx.save();
        ctx.fillStyle = dynamicColorGradient(true); // Use snake part of gradient
        ctx.shadowColor = 'var(--neon-cyan)';
        ctx.shadowBlur = (index === 0 ? 25 : 18); // Head glows more
        
        drawRoundedRect(
          segX + segmentPadding, 
          segY + segmentPadding, 
          gridSize - (2 * segmentPadding), 
          gridSize - (2 * segmentPadding), 
          segmentRadius
        );
        ctx.restore();
      });

      // Draw effects (particles, score pop-ups)
      effects.particles.forEach(p => {
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, 2 * Math.PI);
        ctx.fill();
      });
      effects.score.forEach(e => {
        ctx.fillStyle = `rgba(0, 255, 153, ${e.alpha})`; // Neon green for score text
        ctx.font = `${e.size}px ${getComputedStyle(document.body).fontFamily.split(',')[0] || 'Arial'}`; // Use page font
        ctx.textAlign = 'center';
        ctx.shadowColor = 'var(--neon-green)';
        ctx.shadowBlur = 10;
        ctx.fillText(e.text, e.x, e.y);
        ctx.shadowColor = 'transparent'; // Reset shadow for other drawings
      });
    }

    // --- Input Handling ---
    let touchStartCoords = null;
    
    canvas.addEventListener('touchstart', e => {
      if(e.touches.length === 1) {
        touchStartCoords = { x: e.touches[0].clientX, y: e.touches[0].clientY };
      }
    }, { passive: true });

    canvas.addEventListener('touchmove', e => {
      if(!touchStartCoords || gameOver) return;
      const dx = e.touches[0].clientX - touchStartCoords.x;
      const dy = e.touches[0].clientY - touchStartCoords.y;
      
      if(Math.abs(dx) > GAME_CONFIG.MIN_SWIPE_DISTANCE || Math.abs(dy) > GAME_CONFIG.MIN_SWIPE_DISTANCE){
        let direction;
        if (Math.abs(dx) > Math.abs(dy)) {
          direction = dx > 0 ? 'right' : 'left';
        } else {
          direction = dy > 0 ? 'down' : 'up';
        }
        changeDirection(direction);
        touchStartCoords = null; // Reset after a swipe is registered
      }
    }, { passive: true });

    canvas.addEventListener('touchend', () => { touchStartCoords = null; });

    controlBtns.forEach(button => button.addEventListener('click', () => {
        if (gameOver) return;
        changeDirection(button.dataset.dir);
    }));

    window.addEventListener('keydown', e => {
      if(gameOver && e.key !== 'Enter' && e.key !== ' ') return; // Allow Enter/Space to restart from modal

      if (gameOverModalEl.classList.contains('show') && (e.key === 'Enter' || e.key === ' ')) {
          e.preventDefault();
          initGame(); // Restart game if modal is shown and Enter/Space is pressed
          return;
      }
      if (gameOver) return;


      const keyDirectionMap = {
        ArrowUp: 'up', w: 'up', W: 'up',
        ArrowDown: 'down', s: 'down', S: 'down',
        ArrowLeft: 'left', a: 'left', A: 'left',
        ArrowRight: 'right', d: 'right', D: 'right'
      };
      const direction = keyDirectionMap[e.key];
      if(direction) {
        e.preventDefault(); // Prevent page scrolling
        changeDirection(direction);
      }
    });

    function changeDirection(direction){
      const directionVectors = {
        up:    {x:0, y:-1},
        down:  {x:0, y:1},
        left:  {x:-1,y:0},
        right: {x:1, y:0}
      };
      const newVel = directionVectors[direction];
      if(newVel) {
        // Update nextVel, actual change happens in updateGameLogic to prevent 180 turn in same frame
        nextVel = newVel;
      }
    }

    // --- Game Over & Restart ---
    function displayGameOver(){
      finalScoreEl.textContent = score;
      gameOverModalEl.classList.add('show');
      modalRestartBtn.focus(); // Focus the restart button in the modal
      // Optionally show the page restart button as well, or rely on modal
      // restartBtn.style.display = 'block'; 
    }

    restartBtn.addEventListener('click', initGame);
    modalRestartBtn.addEventListener('click', initGame);

    // --- Main Game Loop ---
    function gameLoop(currentTime = 0){
      if (!lastTime) lastTime = currentTime; // Initialize lastTime on first frame
      accumulatedTime += currentTime - lastTime;
      lastTime = currentTime;

      // Fixed timestep for game logic
      while(accumulatedTime >= gameLogicInterval){
        updateGameLogic();
        updateEffects(); // Effects can update at logic rate or display rate
        accumulatedTime -= gameLogicInterval;
      }
      
      drawGame(); // Drawing happens every frame
      requestAnimationFrame(gameLoop);
    }

    // --- Start Game ---
    resizeCanvas(); // Initial sizing
    initGame();     // Initial game state
    requestAnimationFrame(gameLoop); // Start the loop

  })();
  </script>
</body>
</html>