[object Object]

← back to Games Agentabrams

Arcade: correctly label all games; add my Flappy Clone alongside Flappy Sky

d6f86bc51e34946dc13892a22b83d43ec65fb38b · 2026-07-24 07:26:16 -0700 · Steve Abrams

- Rename cube game Rubik's -> Abrams Cube (label + in-game title)
- Move parallel session's Flappy Sky content to games/flappy-sky/ (rename only)
- Restore my daytime Flappy Clone to games/flappy-clone/ (card already matched)
- Add flappy-sky manifest entry so all 5 cards match their served game

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit d6f86bc51e34946dc13892a22b83d43ec65fb38b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 24 07:26:16 2026 -0700

    Arcade: correctly label all games; add my Flappy Clone alongside Flappy Sky
    
    - Rename cube game Rubik's -> Abrams Cube (label + in-game title)
    - Move parallel session's Flappy Sky content to games/flappy-sky/ (rename only)
    - Restore my daytime Flappy Clone to games/flappy-clone/ (card already matched)
    - Add flappy-sky manifest entry so all 5 cards match their served game
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 games.json                    |   24 +-
 games/flappy-clone/index.html | 1420 ++++++++++++++++++-----------------------
 games/flappy-sky/index.html   |  827 ++++++++++++++++++++++++
 games/rubiks-cube/index.html  |    4 +-
 4 files changed, 1478 insertions(+), 797 deletions(-)

diff --git a/games.json b/games.json
index e59987a..f6863d0 100644
--- a/games.json
+++ b/games.json
@@ -2,17 +2,17 @@
   "games": [
     {
       "id": "rubiks-cube",
-      "file": "RubiksCube.aa",
-      "title": "Rubik's Cube 3D",
-      "desc": "Pure CSS-3D Rubik's cube \u2014 drag to orbit, scramble, solve, undo, timer.",
+      "file": "AbramsCube.aa",
+      "title": "Abrams Cube",
+      "desc": "Pure CSS-3D twist cube — drag to orbit, scramble, solve, undo, timer.",
       "path": "games/rubiks-cube/",
       "added": "2026-07-23"
     },
     {
       "id": "atmos-skyfire",
       "file": "AtmosSkyfire.aa",
-      "title": "ATMOS \u2014 Skyfire",
-      "desc": "Three.js arcade flyer \u2014 bank through a gradient sky, fire twin missiles (SPACE/click), dodge or shoot incoming birds. Steer with arrows/WASD/mouse.",
+      "title": "ATMOS — Skyfire",
+      "desc": "Three.js arcade flyer — bank through a gradient sky, fire twin missiles (SPACE/click), dodge or shoot incoming birds. Steer with arrows/WASD/mouse.",
       "path": "games/atmos-skyfire/",
       "added": "2026-07-23"
     },
@@ -20,17 +20,25 @@
       "id": "flappy-clone",
       "file": "FlappyClone.aa",
       "title": "Flappy Clone",
-      "desc": "Polished Flappy-Bird clone \u2014 Space/click/tap to flap through scrolling pipes. Parallax sky, medals, persistent best, sound (M), pause (P). Pure Canvas 2D, zero dependencies.",
+      "desc": "Polished Flappy-Bird clone — Space/click/tap to flap through scrolling pipes. Parallax daytime sky, medals, persistent best, sound (M), pause (P). Pure Canvas 2D, zero dependencies.",
       "path": "games/flappy-clone/",
       "added": "2026-07-23"
     },
+    {
+      "id": "flappy-sky",
+      "file": "FlappySky.aa",
+      "title": "Flappy Sky",
+      "desc": "Flap through the neon gates against a twinkling night sky — how far can you fly? Medals, persistent best, sound (M), pause (P/Esc). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "path": "games/flappy-sky/",
+      "added": "2026-07-24"
+    },
     {
       "id": "flappy-flight",
       "file": "FlappyFlight.aa",
       "title": "Flappy Flight",
-      "desc": "Polished Flappy-Bird-style flyer \u2014 Space/click/tap to flap through scrolling pipes. Twinkling parallax night sky, moon, medals, persistent best. True pause (P/Esc, auto-pauses off-focus), mute (M or speaker icon). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Polished Flappy-Bird-style flyer — Space/click/tap to flap through scrolling pipes. Twinkling parallax night sky, moon, medals, persistent best. True pause (P/Esc, auto-pauses off-focus), mute (M or speaker icon). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/flappy-flight/",
       "added": "2026-07-23"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/games/flappy-clone/index.html b/games/flappy-clone/index.html
index b92f2c2..a19602e 100644
--- a/games/flappy-clone/index.html
+++ b/games/flappy-clone/index.html
@@ -1,826 +1,672 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-<meta charset="UTF-8" />
-<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
-<meta name="theme-color" content="#0e1018" />
-<meta name="description" content="Flappy Sky — a polished, self-contained Flappy-Bird-style game. Space, click, or tap to flap through neon gates." />
-<title>Flappy Sky</title>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+<meta name="theme-color" content="#4ec0ca">
+<title>Flappy Clone</title>
 <style>
-  :root { --ink: #f8f8ff; }
-
-  html, body {
-    margin: 0;
-    padding: 0;
-    background: #0e1018;
-    overflow: hidden;
-    height: 100%;
-    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-    -webkit-tap-highlight-color: transparent;
-    touch-action: none;
-  }
-
-  canvas {
-    display: block;
-    background: radial-gradient(circle at 20% 20%, #586df2 0, #243046 40%, #0e1018 100%);
-    /* keeps focus outline from ever painting a box around the game */
-    outline: none;
-  }
-
-  /* --- overlay + ui --------------------------------------------------- */
-  #overlay {
-    position: absolute;
-    inset: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    pointer-events: none;
-    user-select: none;
-    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
-    box-sizing: border-box;
-  }
-
-  #ui {
-    color: var(--ink);
-    text-align: center;
-    text-shadow: 0 2px 14px rgba(0,0,0,0.8);
-    transition: opacity .25s ease, transform .25s ease;
-    max-width: 92vw;
-  }
-  #ui.pop { animation: pop .4s cubic-bezier(.2,1.3,.4,1) both; }
-  @keyframes pop { 0% { transform: scale(.85); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
-
-  #title {
-    font-size: clamp(34px, 8vw, 54px);
-    font-weight: 900;
-    letter-spacing: 0.08em;
-    text-transform: uppercase;
-    margin: 0 0 6px;
-    background: linear-gradient(180deg, #fff 0%, #b9d3ff 60%, #72fffb 100%);
-    -webkit-background-clip: text;
-    background-clip: text;
-    -webkit-text-fill-color: transparent;
-    filter: drop-shadow(0 3px 10px rgba(0,0,0,.6));
-  }
-
-  #subtitle { font-size: clamp(13px, 3.4vw, 16px); opacity: 0.85; margin-bottom: 22px; }
-
-  /* medal + result block on game over */
-  #result { display: none; margin-bottom: 18px; }
-  #result.show { display: block; }
-  #medal {
-    width: 92px; height: 92px; margin: 4px auto 10px;
-    border-radius: 50%;
-    display: grid; place-items: center;
-    font-size: 34px;
-    box-shadow: 0 0 0 4px rgba(255,255,255,.15), 0 8px 24px rgba(0,0,0,.55), inset 0 2px 10px rgba(255,255,255,.4);
-    position: relative;
-  }
-  #medal::after {
-    content: ""; position: absolute; inset: 8px; border-radius: 50%;
-    border: 2px dashed rgba(255,255,255,.35);
-  }
-  .scoreline { display:flex; gap: 26px; justify-content:center; margin: 6px 0 2px; }
-  .scoreline .k { font-size: 11px; letter-spacing:.18em; text-transform:uppercase; opacity:.65; }
-  .scoreline .v { font-size: 30px; font-weight: 800; line-height: 1.05; }
-  #newbest { font-size: 12px; font-weight: 800; letter-spacing:.12em; color:#ffe07a;
-             text-transform: uppercase; margin-top: 6px; opacity: 0; }
-  #newbest.show { opacity: 1; animation: flick .8s ease-in-out infinite alternate; }
-  @keyframes flick { from { filter: brightness(1);} to { filter: brightness(1.6);} }
-
-  #prompt {
-    font-size: clamp(14px, 3.6vw, 17px);
-    opacity: 0.95;
-    display: inline-flex; align-items: center; gap: 8px;
-    padding: 10px 18px; border-radius: 999px;
-    background: rgba(255,255,255,.08);
-    border: 1px solid rgba(255,255,255,.16);
-    backdrop-filter: blur(3px);
-    animation: breathe 1.8s ease-in-out infinite;
-  }
-  @keyframes breathe { 0%,100% { transform: translateY(0);} 50% { transform: translateY(-4px);} }
-
-  /* --- HUD ------------------------------------------------------------ */
-  #scoreTop {
-    position: absolute;
-    top: calc(14px + env(safe-area-inset-top));
-    left: 50%;
-    transform: translateX(-50%);
-    color: #fff;
-    font-size: clamp(34px, 9vw, 52px);
-    font-weight: 900;
-    text-shadow: 0 3px 0 rgba(0,0,0,.35), 0 0 18px rgba(120,180,255,.5);
-    pointer-events: none;
-    transition: transform .08s ease;
-  }
-  #scoreTop.bump { transform: translateX(-50%) scale(1.28); }
-  #scoreTop.hidden { display: none; }
-
-  #best {
-    position: absolute;
-    top: calc(16px + env(safe-area-inset-top));
-    right: calc(14px + env(safe-area-inset-right));
-    color: rgba(255,255,255,.8);
-    font-size: 13px; font-weight: 700; letter-spacing:.05em;
-    text-shadow: 0 0 6px rgba(0,0,0,.8);
-    pointer-events: none;
-  }
-  #best.hidden { display: none; }
-
-  /* control buttons (mute / pause) */
-  #controls {
-    position: absolute;
-    top: calc(12px + env(safe-area-inset-top));
-    left: calc(12px + env(safe-area-inset-left));
-    display: flex; gap: 8px;
-  }
-  .btn {
-    width: 38px; height: 38px; border-radius: 10px;
-    border: 1px solid rgba(255,255,255,.18);
-    background: rgba(255,255,255,.07);
-    color: #fff; font-size: 17px; cursor: pointer;
-    display: grid; place-items: center;
-    backdrop-filter: blur(4px);
-    transition: background .15s ease, transform .1s ease;
-    -webkit-user-select: none; user-select: none;
-  }
-  .btn:hover { background: rgba(255,255,255,.16); }
-  .btn:active { transform: scale(.9); }
-
-  #smallInfo {
-    position: absolute;
-    bottom: calc(9px + env(safe-area-inset-bottom));
-    right: calc(12px + env(safe-area-inset-right));
-    font-size: 11px;
-    color: rgba(255,255,255,0.4);
-    text-shadow: 0 0 4px rgba(0,0,0,0.8);
-    pointer-events: none;
-  }
-
-  /* pause veil */
-  #pauseVeil {
-    position: absolute; inset: 0;
-    display: none; align-items: center; justify-content: center;
-    background: rgba(6,8,16,.55); backdrop-filter: blur(2px);
-    color: #fff; font-size: clamp(22px, 6vw, 34px); font-weight: 800;
-    letter-spacing: .12em; text-transform: uppercase; pointer-events: none;
-  }
-  #pauseVeil.show { display: flex; }
-
-  .hidden { display: none; }
+*{margin:0;padding:0;box-sizing:border-box}
+html,body{width:100%;height:100%;overflow:hidden;background:#1a1a2e;font-family:system-ui,-apple-system,sans-serif}
+canvas{display:block;margin:0 auto;background:#70c5ce;cursor:pointer;touch-action:none}
 </style>
 </head>
 <body>
-<canvas id="game" tabindex="-1" aria-label="Flappy Sky game canvas"></canvas>
-
-<div id="scoreTop" class="hidden"><span id="scoreVal">0</span></div>
-<div id="best" class="hidden">Best <span id="bestVal">0</span></div>
-
-<div id="controls">
-  <button id="muteBtn" class="btn" title="Mute (M)" aria-label="Toggle sound">🔊</button>
-  <button id="pauseBtn" class="btn" title="Pause (P)" aria-label="Pause game">⏸</button>
-</div>
-
-<div id="overlay">
-  <div id="ui" class="pop">
-    <div id="title">FLAPPY SKY</div>
-    <div id="subtitle">Flap through the neon gates. How far can you fly?</div>
-
-    <div id="result">
-      <div id="medal">🥇</div>
-      <div class="scoreline">
-        <div><div class="k">Score</div><div class="v" id="rScore">0</div></div>
-        <div><div class="k">Best</div><div class="v" id="rBest">0</div></div>
-      </div>
-      <div id="newbest">★ New Best! ★</div>
-    </div>
-
-    <div id="prompt">Press <b>Space</b>, Click, or Tap to Start</div>
-  </div>
-</div>
-
-<div id="pauseVeil">Paused</div>
-
-<div id="smallInfo">Space / Click / Tap · P pause · M mute</div>
-
+<canvas id="c"></canvas>
 <script>
-(function () {
-  "use strict";
-
-  const canvas = document.getElementById("game");
-  const ctx = canvas.getContext("2d", { alpha: false });
-
-  // ---- DOM refs -------------------------------------------------------
-  const overlay   = document.getElementById("overlay");
-  const ui        = document.getElementById("ui");
-  const uiPrompt  = document.getElementById("prompt");
-  const result    = document.getElementById("result");
-  const medalEl    = document.getElementById("medal");
-  const rScore    = document.getElementById("rScore");
-  const rBest     = document.getElementById("rBest");
-  const newbest   = document.getElementById("newbest");
-  const scoreTop  = document.getElementById("scoreTop");
-  const scoreVal  = document.getElementById("scoreVal");
-  const bestBox   = document.getElementById("best");
-  const bestVal   = document.getElementById("bestVal");
-  const pauseVeil = document.getElementById("pauseVeil");
-  const muteBtn   = document.getElementById("muteBtn");
-  const pauseBtn  = document.getElementById("pauseBtn");
-
-  const reduceMotion = window.matchMedia &&
-    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
-
-  // ---- viewport -------------------------------------------------------
-  let W, H, scale, groundH;
-  function resize() {
-    const dpr = Math.min(window.devicePixelRatio || 1, 2.5);
-    W = window.innerWidth;
-    H = window.innerHeight;
-    canvas.width  = Math.round(W * dpr);
-    canvas.height = Math.round(H * dpr);
-    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
-    scale = Math.min(Math.max(Math.min(W, H) / 600, 0.6), 1.6);
-    groundH = Math.round(Math.min(90 * scale, H * 0.14));
-    initBackground();
-    // keep the idle bird sensibly placed after a rotate/resize
-    if (bird && state !== "playing") { bird.x = W * 0.3; bird.baseY = H * 0.42; }
-  }
-
-  // ---- tuning ---------------------------------------------------------
-  const GRAVITY = 1500;      // px/s^2
-  const FLAP    = -450;      // impulse
-  const PIPE_W  = 84;        // pipe width (px, scaled at draw)
-  const PIPE_INTERVAL_START = 1500;  // ms between pipes
-  const PIPE_INTERVAL_MIN   = 950;
-  const GAP_START = 200;     // px gap (pre-scale)
-  const GAP_MIN   = 138;
-  const SPEED_START = 200;   // px/s
-  const SPEED_MAX   = 400;
-  const RAMP = 25;           // score at which difficulty maxes out
-  const FIXED_DT = 1 / 120;  // physics step
-
-  // ---- state ----------------------------------------------------------
-  let state = "ready"; // ready | playing | paused | gameover
-  let bird, pipes, particles, stars, hills, ground;
-  let score = 0, best = 0;
-  let pipeInterval, gapSize, speed;
-  let timeSincePipe = 0;
-  let shake = 0, flashAlpha = 0, hitStop = 0;
-  let lastTime = 0, acc = 0;
-  let muted = false;
-  let bgScroll = 0;
-
-  try { best = parseInt(localStorage.getItem("flappy_sky_best"), 10) || 0; } catch (e) {}
-  try { muted = localStorage.getItem("flappy_sky_muted") === "1"; } catch (e) {}
-  bestVal.textContent = best;
-
-  // ---- audio (WebAudio, no assets) -----------------------------------
-  let actx = null;
-  function audio() {
-    if (!actx) {
-      try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
-      catch (e) { actx = null; }
-    }
-    if (actx && actx.state === "suspended") actx.resume();
-    return actx;
-  }
-  function tone(freq, dur, type, vol, slideTo) {
-    if (muted) return;
-    const a = audio(); if (!a) return;
-    const t0 = a.currentTime;
-    const osc = a.createOscillator();
-    const g = a.createGain();
-    osc.type = type || "square";
-    osc.frequency.setValueAtTime(freq, t0);
-    if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(1, slideTo), t0 + dur);
-    g.gain.setValueAtTime(0.0001, t0);
-    g.gain.exponentialRampToValueAtTime(vol || 0.12, t0 + 0.008);
-    g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
-    osc.connect(g); g.connect(a.destination);
-    osc.start(t0); osc.stop(t0 + dur + 0.02);
-  }
-  const sfx = {
-    flap:  () => tone(560, 0.12, "square",   0.10, 780),
-    score: () => { tone(720, 0.09, "triangle", 0.12); setTimeout(() => tone(960, 0.10, "triangle", 0.12), 70); },
-    hit:   () => { tone(200, 0.18, "sawtooth", 0.16, 80); },
-    die:   () => { tone(340, 0.5, "sine", 0.12, 60); },
-    swoosh:() => tone(300, 0.14, "sine", 0.05, 120)
+(function(){
+const canvas=document.getElementById('c');
+const ctx=canvas.getContext('2d');
+
+// ---- Virtual world: all game logic runs in a fixed 480x720 space. ----
+// The canvas is scaled to the display only at draw time, so resizing never
+// corrupts object positions and physics stays resolution-independent.
+const VW=480, VH=720;
+let cssW=VW, cssH=VH, dpr=1;
+
+function resize(){
+  const maxW=480, maxH=720;
+  const rw=window.innerWidth, rh=window.innerHeight;
+  // Fit the 480:720 (2:3) frame inside the viewport, allow slight upscale.
+  const s=Math.min(rw/maxW, rh/maxH, 1.5);
+  cssW=Math.floor(maxW*s);
+  cssH=Math.floor(maxH*s);
+  dpr=Math.min(window.devicePixelRatio||1, 2);
+  canvas.width=Math.floor(cssW*dpr);
+  canvas.height=Math.floor(cssH*dpr);
+  canvas.style.width=cssW+'px';
+  canvas.style.height=cssH+'px';
+}
+resize();
+window.addEventListener('resize',resize);
+
+// ---- Tunable constants (in virtual units / per 60Hz tick) ----
+const GRAVITY=0.35;
+const FLAP=-6.8;
+const PIPE_W=60;
+const PIPE_GAP=150;
+const PIPE_SPEED=2.2;
+const BIRD_R=16;
+const GROUND_H=80;
+const STEP=1000/60;            // fixed physics timestep (ms)
+
+let bird,pipes,score,best,state,frame,particles,clouds,hills,speed,gapSize,shake;
+let muted=false;
+
+try{ best=parseInt(localStorage.getItem('flappy_best'))||0; }catch(e){ best=0; }
+try{ muted=localStorage.getItem('flappy_muted')==='1'; }catch(e){ muted=false; }
+
+// UI buttons in virtual coords (top-left, away from centered score).
+const BTN={size:30, y:14, muteX:14, pauseX:52};
+
+function init(){
+  bird={x:VW*0.28,y:VH*0.4,vy:0,rot:0,wing:0};
+  pipes=[];
+  score=0;
+  state='ready';
+  frame=0;
+  particles=[];
+  speed=PIPE_SPEED;
+  gapSize=PIPE_GAP;
+  shake=0;
+  spawnClouds();
+  spawnHills();
+}
+
+function spawnClouds(){
+  clouds=[];
+  for(let i=0;i<6;i++){
+    clouds.push({
+      x:Math.random()*VW*1.5,
+      y:VH*0.05+Math.random()*VH*0.35,
+      w:40+Math.random()*60,
+      speed:0.15+Math.random()*0.25,
+      op:0.4+Math.random()*0.4
+    });
+  }
+}
+
+function spawnHills(){
+  hills=[];
+  for(let i=0;i<8;i++){
+    hills.push({
+      x:i*VW*0.4+Math.random()*50,
+      y:VH-GROUND_H,
+      w:120+Math.random()*100,
+      h:40+Math.random()*80,
+      layer:i%2,
+      speed:0.3+(i%2)*0.2
+    });
+  }
+}
+
+function addPipe(){
+  const minY=100;
+  const maxY=VH-GROUND_H-gapSize-80;
+  const top=minY+Math.random()*(maxY-minY);
+  pipes.push({x:VW+10,top:top,gap:gapSize,passed:false});
+}
+
+// ---- Web Audio: tiny synthesized blips, no external assets ----
+let actx=null;
+function beep(freq,dur,type,vol,slideTo){
+  if(muted)return;
+  try{
+    if(!actx)actx=new (window.AudioContext||window.webkitAudioContext)();
+    if(actx.state==='suspended')actx.resume();
+    const o=actx.createOscillator(), g=actx.createGain();
+    o.type=type||'square';
+    const t=actx.currentTime;
+    o.frequency.setValueAtTime(freq,t);
+    if(slideTo)o.frequency.exponentialRampToValueAtTime(slideTo,t+dur);
+    g.gain.setValueAtTime((vol||0.15),t);
+    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
+    o.connect(g);g.connect(actx.destination);
+    o.start(t);o.stop(t+dur);
+  }catch(e){}
+}
+const sndFlap =()=>beep(520,0.09,'square',0.12,300);
+const sndScore=()=>{beep(660,0.08,'square',0.12);setTimeout(()=>beep(880,0.09,'square',0.12),70);};
+const sndHit  =()=>beep(180,0.25,'sawtooth',0.2,60);
+const sndDie  =()=>setTimeout(()=>beep(120,0.4,'triangle',0.18,50),120);
+
+function flap(){
+  if(state==='ready'){state='play';bird.vy=FLAP;sndFlap();}
+  else if(state==='play'){bird.vy=FLAP;burst(bird.x,bird.y,6,'#fff');sndFlap();}
+  else if(state==='over'){init();}
+  // if paused, flap does nothing (resume is via button/key)
+}
+
+function togglePause(){
+  if(state==='play')state='paused';
+  else if(state==='paused')state='play';
+}
+
+function toggleMute(){
+  muted=!muted;
+  try{localStorage.setItem('flappy_muted',muted?'1':'0');}catch(e){}
+}
+
+function burst(x,y,n,col){
+  for(let i=0;i<n;i++){
+    particles.push({
+      x,y,
+      vx:(Math.random()-0.5)*4,
+      vy:(Math.random()-0.5)*4-1,
+      life:20+Math.random()*15,
+      max:35,
+      col:col||'#ffd700',
+      r:2+Math.random()*3
+    });
+  }
+}
+
+// ---- Input: map pointer to virtual coords, test UI buttons first ----
+function toVirtual(clientX,clientY){
+  const r=canvas.getBoundingClientRect();
+  return {
+    x:(clientX-r.left)*(VW/r.width),
+    y:(clientY-r.top)*(VH/r.height)
   };
-
-  // ---- background -----------------------------------------------------
-  function initBackground() {
-    stars = [];
-    const starCount = Math.round((W * H) / 16000);
-    for (let i = 0; i < starCount; i++) {
-      stars.push({
-        x: Math.random() * W,
-        y: Math.random() * (H - groundH) * 0.9,
-        r: Math.random() * 1.5 + 0.4,
-        speed: 6 + Math.random() * 12,
-        phase: Math.random() * Math.PI * 2,
-        tw: 1.2 + Math.random() * 1.6
-      });
-    }
-    hills = [
-      { base: 0.70, amp: 42 * scale, freq: 0.0042, speed: 16, off: 0,   color: "#141a33" },
-      { base: 0.78, amp: 58 * scale, freq: 0.0030, speed: 30, off: 240, color: "#0c1024" }
-    ];
-    ground = { off: 0 };
-  }
-
-  // ---- game objects ---------------------------------------------------
-  function resetGame() {
-    bird = { x: W * 0.3, baseY: H * 0.42, y: H * 0.42, vy: 0, r: 16 * scale, rot: 0, flapT: 0 };
-    pipes = [];
-    particles = [];
-    timeSincePipe = 0;
-    score = 0;
-    scoreVal.textContent = "0";
-    pipeInterval = PIPE_INTERVAL_START;
-    gapSize = GAP_START * scale;
-    speed = SPEED_START;
-    shake = 0; flashAlpha = 0; hitStop = 0;
-  }
-
-  function difficulty() {
-    const t = Math.min(score / RAMP, 1);
-    pipeInterval = PIPE_INTERVAL_START - (PIPE_INTERVAL_START - PIPE_INTERVAL_MIN) * t;
-    gapSize = (GAP_START - (GAP_START - GAP_MIN) * t) * scale;
-    speed = SPEED_START + (SPEED_MAX - SPEED_START) * t;
-  }
-
-  function spawnPipe() {
-    const playH = H - groundH;
-    const margin = Math.max(50 * scale, playH * 0.09);
-    const minTop = margin;
-    const maxTop = playH - margin - gapSize;
-    const top = maxTop <= minTop ? (playH - gapSize) / 2
-                                 : minTop + Math.random() * (maxTop - minTop);
-    // hue drifts a touch over the run so later pipes read hotter
-    const hue = 165 - Math.min(score, 40) * 1.2;
-    pipes.push({ x: W + PIPE_W, top: top, gap: gapSize, scored: false, hue: hue });
-  }
-
-  function addParticles(x, y, count, hue, power) {
-    if (reduceMotion) count = Math.ceil(count / 3);
-    for (let i = 0; i < count; i++) {
-      const ang = Math.random() * Math.PI * 2;
-      const mag = (Math.random() * 150 + 40) * scale * (power || 1);
-      particles.push({
-        x, y,
-        vx: Math.cos(ang) * mag,
-        vy: Math.sin(ang) * mag - 40 * scale,
-        life: 0.5 + Math.random() * 0.35, age: 0,
-        size: (Math.random() * 3 + 2) * scale,
-        hue: hue + (Math.random() * 30 - 15)
-      });
-    }
-  }
-
-  // ---- input (single unified path, dedupes touch+mouse) --------------
-  let lastFlap = 0;
-  function flap() {
-    audio(); // unlock on first gesture
-    const now = performance.now();
-    if (now - lastFlap < 55) return; // debounce double-fire (touch->mouse)
-    lastFlap = now;
-
-    if (state === "ready") {
-      startGame();
-      doFlap();
-    } else if (state === "playing") {
-      doFlap();
-    } else if (state === "paused") {
-      resume();
-    } else if (state === "gameover") {
-      // small guard so the death tap doesn't instantly restart
-      if (now - gameoverAt > 350) toReady();
-    }
-  }
-  function doFlap() {
-    bird.vy = FLAP;
-    bird.flapT = 1;
-    addParticles(bird.x - bird.r * 0.8, bird.y + bird.r * 0.4, 6, 205, 0.7);
-    sfx.flap();
-  }
-
-  function startGame() {
-    state = "playing";
-    ui.classList.remove("pop");
-    overlay.classList.add("hidden");
-    scoreTop.classList.remove("hidden");
-    bestBox.classList.remove("hidden");
-    result.classList.remove("show");
-    sfx.swoosh();
-  }
-
-  function toReady() {
-    state = "ready";
-    resetGame();
-    result.classList.remove("show");
-    newbest.classList.remove("show");
-    uiPrompt.textContent = "Press Space, Click, or Tap to Start";
-    overlay.classList.remove("hidden");
-    ui.classList.remove("pop"); void ui.offsetWidth; ui.classList.add("pop");
-    scoreTop.classList.add("hidden");
-    bestBox.classList.add("hidden");
-  }
-
-  let gameoverAt = 0;
-  function gameOver() {
-    if (state !== "playing") return;
-    state = "gameover";
-    gameoverAt = performance.now();
-    hitStop = 0.08;
-    shake = reduceMotion ? 0 : 16;
-    flashAlpha = reduceMotion ? 0.25 : 0.6;
-    addParticles(bird.x, bird.y, reduceMotion ? 10 : 30, 40, 1.5);
-    sfx.hit(); sfx.die();
-
-    const isBest = score > best;
-    if (isBest) { best = score; try { localStorage.setItem("flappy_sky_best", best); } catch (e) {} }
-    bestVal.textContent = best;
-
-    rScore.textContent = score;
-    rBest.textContent = best;
-    setMedal(score);
-    result.classList.add("show");
-    newbest.classList.toggle("show", isBest);
-    uiPrompt.textContent = "Click / Tap / Space to Play Again";
-
-    // reveal overlay a beat after the death punch
-    setTimeout(() => {
-      if (state === "gameover") {
-        overlay.classList.remove("hidden");
-        ui.classList.remove("pop"); void ui.offsetWidth; ui.classList.add("pop");
-      }
-    }, 260);
-  }
-
-  function setMedal(s) {
-    let icon = "🥉", bg = "linear-gradient(160deg,#c9865b,#8a5a34)";
-    if (s >= 40)      { icon = "💎"; bg = "linear-gradient(160deg,#8ef7ff,#2aa7c9)"; }
-    else if (s >= 25) { icon = "🥇"; bg = "linear-gradient(160deg,#ffe27a,#d3a520)"; }
-    else if (s >= 12) { icon = "🥈"; bg = "linear-gradient(160deg,#e8eef7,#9aa7bd)"; }
-    else if (s >= 4)  { icon = "🥉"; bg = "linear-gradient(160deg,#c9865b,#8a5a34)"; }
-    else              { icon = "🍃"; bg = "linear-gradient(160deg,#7f8aa3,#4a5570)"; }
-    medalEl.textContent = icon;
-    medalEl.style.background = bg;
-  }
-
-  // ---- pause ----------------------------------------------------------
-  function togglePause() {
-    if (state === "playing") pause();
-    else if (state === "paused") resume();
-  }
-  function pause() {
-    if (state !== "playing") return;
-    state = "paused";
-    pauseVeil.classList.add("show");
-    pauseBtn.textContent = "▶";
-  }
-  function resume() {
-    if (state !== "paused") return;
-    state = "playing";
-    pauseVeil.classList.remove("show");
-    pauseBtn.textContent = "⏸";
-    lastTime = performance.now(); acc = 0; // avoid a physics jump
-  }
-
-  function toggleMute() {
-    muted = !muted;
-    muteBtn.textContent = muted ? "🔇" : "🔊";
-    try { localStorage.setItem("flappy_sky_muted", muted ? "1" : "0"); } catch (e) {}
-    if (!muted) sfx.swoosh();
-  }
-  muteBtn.textContent = muted ? "🔇" : "🔊";
-
-  // ---- listeners ------------------------------------------------------
-  window.addEventListener("resize", resize);
-  window.addEventListener("orientationchange", resize);
-
-  document.addEventListener("keydown", (e) => {
-    if (e.code === "Space" || e.code === "ArrowUp" || e.code === "Enter") {
-      e.preventDefault(); flap();
-    } else if (e.code === "KeyP") {
-      e.preventDefault(); togglePause();
-    } else if (e.code === "KeyM") {
-      e.preventDefault(); toggleMute();
-    }
+}
+function hitBtn(v,bx){
+  return v.x>=bx && v.x<=bx+BTN.size && v.y>=BTN.y && v.y<=BTN.y+BTN.size;
+}
+function handlePointer(clientX,clientY){
+  const v=toVirtual(clientX,clientY);
+  if(hitBtn(v,BTN.muteX)){toggleMute();return;}
+  if(hitBtn(v,BTN.pauseX)){ if(state==='play'||state==='paused')togglePause(); return; }
+  flap();
+}
+
+canvas.addEventListener('mousedown',e=>{e.preventDefault();handlePointer(e.clientX,e.clientY);});
+canvas.addEventListener('touchstart',e=>{
+  e.preventDefault();
+  const t=e.changedTouches[0];
+  handlePointer(t.clientX,t.clientY);
+},{passive:false});
+window.addEventListener('keydown',e=>{
+  if(e.code==='Space'||e.key===' '){e.preventDefault();flap();}
+  else if(e.key==='p'||e.key==='P'||e.code==='Escape'){e.preventDefault();togglePause();}
+  else if(e.key==='m'||e.key==='M'){e.preventDefault();toggleMute();}
+});
+// Pause when the tab/window loses focus so you don't die while away.
+window.addEventListener('blur',()=>{ if(state==='play')state='paused'; });
+
+function update(){
+  frame++;
+  if(shake>0)shake*=0.9;
+
+  // parallax (also drifts slowly on non-play screens)
+  const bgMul=(state==='play')?1:0.3;
+  clouds.forEach(c=>{
+    c.x-=c.speed*bgMul;
+    if(c.x+c.w<-10){c.x=VW+Math.random()*100;c.y=VH*0.05+Math.random()*VH*0.35;}
+  });
+  hills.forEach(h=>{
+    h.x-=h.speed*(state==='play'?1:0.2);
+    if(h.x+h.w<-10){h.x=VW+Math.random()*80;h.h=40+Math.random()*80;h.w=120+Math.random()*100;}
   });
 
-  // pointer handles mouse + touch + pen uniformly; debounce covers the rest
-  canvas.addEventListener("pointerdown", (e) => { e.preventDefault(); flap(); });
-  overlay.addEventListener("pointerdown", (e) => { e.preventDefault(); flap(); });
-  // fallback for browsers without pointer events
-  if (!window.PointerEvent) {
-    canvas.addEventListener("mousedown", flap);
-    canvas.addEventListener("touchstart", (e) => { e.preventDefault(); flap(); }, { passive: false });
-  }
-
-  muteBtn.addEventListener("pointerdown", (e) => { e.stopPropagation(); e.preventDefault(); toggleMute(); });
-  pauseBtn.addEventListener("pointerdown", (e) => { e.stopPropagation(); e.preventDefault(); togglePause(); });
+  particles=particles.filter(p=>{
+    p.x+=p.vx;p.y+=p.vy;p.vy+=0.08;p.life--;
+    return p.life>0;
+  });
 
-  // auto-pause when the tab/window loses focus mid-game
-  document.addEventListener("visibilitychange", () => { if (document.hidden) pause(); });
-  window.addEventListener("blur", () => pause());
+  if(state!=='play')return;
 
-  // ---- physics --------------------------------------------------------
-  function step(dt) {
-    bgScroll += dt;
+  bird.vy+=GRAVITY;
+  bird.y+=bird.vy;
+  bird.rot=Math.min(Math.PI/3,Math.max(-Math.PI/4,bird.vy*0.06));
+  bird.wing=Math.sin(frame*0.4)*0.4;
 
-    // parallax always animates (even on menus) for a living background
-    for (const s of stars) {
-      s.x -= s.speed * dt;
-      if (s.x < -4) { s.x = W + 4; s.y = Math.random() * (H - groundH) * 0.9; }
-      s.phase += dt * s.tw;
-    }
-    for (const h of hills) h.off += h.speed * dt;
-
-    if (state === "playing") {
-      ground.off = (ground.off + speed * dt) % 48;
-      difficulty();
-
-      bird.flapT = Math.max(0, bird.flapT - dt * 6);
-      bird.vy += GRAVITY * dt;
-      bird.y  += bird.vy * dt;
-      bird.rot = Math.max(-0.55, Math.min(1.4, Math.atan2(bird.vy, 420)));
-
-      // soft ceiling — clamp instead of instant death (classic feel)
-      if (bird.y - bird.r < 0) { bird.y = bird.r; if (bird.vy < 0) bird.vy = 0; }
-
-      timeSincePipe += dt * 1000;
-      if (timeSincePipe >= pipeInterval) { timeSincePipe -= pipeInterval; spawnPipe(); }
-
-      const bx = bird.x, by = bird.y, br = bird.r * 0.82;
-      const pw = PIPE_W * scale;
-
-      for (let i = pipes.length - 1; i >= 0; i--) {
-        const p = pipes[i];
-        p.x -= speed * dt;
-        if (p.x + pw < -20) { pipes.splice(i, 1); continue; }
-
-        if (!p.scored && p.x + pw < bx) {
-          p.scored = true; score++;
-          scoreVal.textContent = score;
-          scoreTop.classList.remove("bump"); void scoreTop.offsetWidth; scoreTop.classList.add("bump");
-          sfx.score();
-        }
-        // circle-vs-rect collision (accurate)
-        if (hitPipe(bx, by, br, p, pw)) { gameOver(); return; }
-      }
+  // soft ceiling: bonk instead of instant death (fairer than the original)
+  if(bird.y-BIRD_R<0){bird.y=BIRD_R;if(bird.vy<0)bird.vy=0;}
 
-      // ground death
-      if (by + br > H - groundH) { bird.y = H - groundH - br; gameOver(); return; }
-    } else if (state === "ready" || state === "gameover") {
-      // clean bob around a fixed base — no accumulating drift
-      const t = bgScroll;
-      bird.y = bird.baseY + Math.sin(t * 2.4) * 8 * scale;
-      bird.rot = Math.sin(t * 1.6) * 0.18;
-    }
+  // difficulty ramps with score
+  const diff=Math.min(score*0.08,2.5);
+  speed=PIPE_SPEED+diff;
+  gapSize=Math.max(PIPE_GAP-score*2.5,100);
 
-    // particles
-    for (let i = particles.length - 1; i >= 0; i--) {
-      const p = particles[i];
-      p.age += dt;
-      if (p.age >= p.life) { particles.splice(i, 1); continue; }
-      p.x += p.vx * dt; p.y += p.vy * dt;
-      p.vy += 520 * dt; p.vx *= 0.986;
-    }
-
-    if (shake > 0)      shake = Math.max(0, shake - dt * 60);
-    if (flashAlpha > 0) flashAlpha = Math.max(0, flashAlpha - dt * 2.2);
-  }
-
-  function hitPipe(cx, cy, r, p, pw) {
-    // top rect: (p.x,0,pw,p.top) ; bottom rect: (p.x,p.top+gap,pw,rest)
-    const bottomY = p.top + p.gap;
-    return rectHit(cx, cy, r, p.x, 0, pw, p.top) ||
-           rectHit(cx, cy, r, p.x, bottomY, pw, (H - groundH) - bottomY);
-  }
-  function rectHit(cx, cy, r, rx, ry, rw, rh) {
-    const nx = Math.max(rx, Math.min(cx, rx + rw));
-    const ny = Math.max(ry, Math.min(cy, ry + rh));
-    const dx = cx - nx, dy = cy - ny;
-    return dx * dx + dy * dy < r * r;
+  if(pipes.length===0||pipes[pipes.length-1].x<VW-180-diff*15){
+    addPipe();
   }
 
-  // ---- rendering ------------------------------------------------------
-  function drawBackground() {
-    const grd = ctx.createLinearGradient(0, 0, 0, H);
-    grd.addColorStop(0, "rgba(40,50,90,0.25)");
-    grd.addColorStop(1, "rgba(0,0,0,0.55)");
-    ctx.fillStyle = grd;
-    ctx.fillRect(0, 0, W, H);
-
-    for (const s of stars) {
-      const a = 0.25 + 0.55 * (0.5 + 0.5 * Math.sin(s.phase));
-      ctx.fillStyle = "rgba(230,240,255," + a.toFixed(3) + ")";
-      ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, 6.2832); ctx.fill();
+  for(let i=pipes.length-1;i>=0;i--){
+    const p=pipes[i];
+    p.x-=speed;
+    if(!p.passed&&p.x+PIPE_W<bird.x){
+      p.passed=true;
+      score++;
+      burst(bird.x+20,bird.y,8,'#ffd700');
+      sndScore();
+      if(score>best){best=score;try{localStorage.setItem('flappy_best',best);}catch(e){}}
     }
-
-    // haze band
-    const cy = H * 0.3;
-    const cg = ctx.createLinearGradient(0, cy - 80, 0, cy + 80);
-    cg.addColorStop(0, "rgba(160,190,255,0)");
-    cg.addColorStop(0.4, "rgba(150,180,255,0.07)");
-    cg.addColorStop(1, "rgba(80,110,170,0)");
-    ctx.fillStyle = cg;
-    ctx.fillRect(0, cy - 80, W, 160);
-
-    for (const h of hills) {
-      const baseY = (H - groundH) * h.base;
-      ctx.fillStyle = h.color;
-      ctx.beginPath();
-      ctx.moveTo(0, H);
-      for (let x = 0; x <= W + 24; x += 20) {
-        const y = baseY + Math.sin((x + h.off) * h.freq) * h.amp;
-        ctx.lineTo(x, y);
+    if(p.x+PIPE_W<-10)pipes.splice(i,1);
+
+    // collision (circle-vs-rect, forgiving radius)
+    const bx=bird.x,by=bird.y,br=BIRD_R*0.85;
+    const px=p.x,pw=PIPE_W;
+    if(bx+br>px&&bx-br<px+pw){
+      if(by-br<p.top||by+br>p.top+p.gap){
+        die();
       }
-      ctx.lineTo(W, H); ctx.closePath(); ctx.fill();
     }
   }
 
-  function drawPipes() {
-    const pw = PIPE_W * scale;
-    const lip = 14 * scale;
-    const playH = H - groundH;
-    for (const p of pipes) {
-      const bottomY = p.top + p.gap;
-      const c1 = "hsl(" + p.hue + ",90%,55%)";
-      const c2 = "hsl(" + (p.hue + 18) + ",95%,72%)";
-      const c3 = "hsl(" + (p.hue - 8) + ",85%,42%)";
-
-      ctx.save();
-      ctx.shadowColor = "hsla(" + p.hue + ",100%,60%,0.6)";
-      ctx.shadowBlur = reduceMotion ? 0 : 16;
-
-      drawPipeBody(p.x, 0, pw, p.top, c1, c2, c3, lip, true);
-      drawPipeBody(p.x, bottomY, pw, playH - bottomY, c1, c2, c3, lip, false);
-      ctx.restore();
-    }
-  }
-  function drawPipeBody(x, y, w, h, c1, c2, c3, lip, isTop) {
-    if (h <= 0) return;
-    const g = ctx.createLinearGradient(x, 0, x + w, 0);
-    g.addColorStop(0, c3); g.addColorStop(0.18, c1);
-    g.addColorStop(0.5, c2); g.addColorStop(0.82, c1); g.addColorStop(1, c3);
-    ctx.fillStyle = g;
-    // main shaft
-    ctx.fillRect(x, y, w, h);
-    // lip (mouth end)
-    const ly = isTop ? y + h - lip : y;
-    const lw = w + 8 * scale;
-    ctx.fillRect(x - 4 * scale, ly, lw, lip);
-    // glossy highlight
-    ctx.fillStyle = "rgba(255,255,255,0.22)";
-    ctx.fillRect(x + w * 0.16, y, w * 0.1, h);
-  }
+  const groundY=VH-GROUND_H;
+  if(bird.y+BIRD_R>groundY)die();
+}
+
+function die(){
+  if(state!=='play')return;
+  state='over';
+  shake=12;
+  burst(bird.x,bird.y,20,'#ff6b6b');
+  burst(bird.x,bird.y,12,'#fff');
+  sndHit();sndDie();
+}
+
+function roundRect(x,y,w,h,r){
+  ctx.beginPath();
+  ctx.moveTo(x+r,y);
+  ctx.arcTo(x+w,y,x+w,y+h,r);
+  ctx.arcTo(x+w,y+h,x,y+h,r);
+  ctx.arcTo(x,y+h,x,y,r);
+  ctx.arcTo(x,y,x+w,y,r);
+  ctx.closePath();
+}
+
+function drawBg(){
+  const g=ctx.createLinearGradient(0,0,0,VH);
+  g.addColorStop(0,'#4ec0ca');
+  g.addColorStop(0.5,'#70c5ce');
+  g.addColorStop(1,'#b8e0e8');
+  ctx.fillStyle=g;
+  ctx.fillRect(0,0,VW,VH);
+
+  // sun
+  ctx.beginPath();
+  ctx.arc(VW*0.82,VH*0.12,30,0,Math.PI*2);
+  ctx.fillStyle='rgba(255,240,180,0.7)';
+  ctx.fill();
+  ctx.beginPath();
+  ctx.arc(VW*0.82,VH*0.12,18,0,Math.PI*2);
+  ctx.fillStyle='rgba(255,250,220,0.9)';
+  ctx.fill();
+
+  // clouds
+  clouds.forEach(c=>{
+    ctx.globalAlpha=c.op;
+    ctx.fillStyle='#fff';
+    drawCloud(c.x,c.y,c.w/50);
+  });
+  ctx.globalAlpha=1;
 
-  function drawGround() {
-    const gy = H - groundH;
-    const g = ctx.createLinearGradient(0, gy, 0, H);
-    g.addColorStop(0, "#3a3f2e");
-    g.addColorStop(0.12, "#4a5136");
-    g.addColorStop(1, "#20241a");
-    ctx.fillStyle = g;
-    ctx.fillRect(0, gy, W, groundH);
-
-    // top rim
-    ctx.fillStyle = "rgba(120,200,150,0.35)";
-    ctx.fillRect(0, gy, W, 3 * scale);
-
-    // scrolling hatch
-    ctx.strokeStyle = "rgba(0,0,0,0.18)";
-    ctx.lineWidth = 2;
+  // hills (far then near)
+  hills.filter(h=>h.layer===0).forEach(h=>{
+    ctx.fillStyle='#5aad5a';
     ctx.beginPath();
-    for (let x = -48 + (-ground.off); x < W + 48; x += 24) {
-      ctx.moveTo(x, gy + 6 * scale);
-      ctx.lineTo(x + 14, H);
-    }
-    ctx.stroke();
-  }
-
-  function drawBird() {
-    const b = bird;
-    ctx.save();
-    ctx.translate(b.x, b.y);
-    ctx.rotate(b.rot);
-    const R = b.r;
-
-    // body
-    const bg = ctx.createRadialGradient(-R * 0.2, -R * 0.2, R * 0.2, 0, 0, R * 1.2);
-    bg.addColorStop(0, "#fffbe6"); bg.addColorStop(0.5, "#ffe07a"); bg.addColorStop(1, "#ff9f3f");
-    ctx.fillStyle = bg;
-    ctx.beginPath(); ctx.ellipse(0, 0, R * 1.15, R * 0.92, 0, 0, 6.2832); ctx.fill();
-
-    // belly
-    ctx.fillStyle = "rgba(255,255,255,0.85)";
-    ctx.beginPath(); ctx.ellipse(-R * 0.1, R * 0.12, R * 0.68, R * 0.56, 0, 0, 6.2832); ctx.fill();
-
-    // wing — flap amplitude tied to flapT (springs on each flap)
-    const wingBase = state === "playing" ? (0.35 - bird.flapT * 1.3)
-                                         : Math.sin(bgScroll * 8) * 0.4;
-    ctx.save();
-    ctx.translate(-R * 0.1, 0);
-    ctx.rotate(-0.7 + wingBase);
-    const wg = ctx.createLinearGradient(-R * 0.2, -R * 0.4, R * 0.5, R * 0.6);
-    wg.addColorStop(0, "#ffe7a3"); wg.addColorStop(1, "#ffb25a");
-    ctx.fillStyle = wg;
-    ctx.beginPath(); ctx.ellipse(0, 0, R * 0.92, R * 0.5, 0, 0, 6.2832); ctx.fill();
-    ctx.strokeStyle = "rgba(180,110,40,0.35)"; ctx.lineWidth = 1;
-    ctx.stroke();
-    ctx.restore();
-
-    // eye
-    ctx.fillStyle = "#fff";
-    ctx.beginPath(); ctx.arc(R * 0.42, -R * 0.22, R * 0.26, 0, 6.2832); ctx.fill();
-    ctx.fillStyle = "#1c1f2b";
-    ctx.beginPath(); ctx.arc(R * 0.5, -R * 0.22, R * 0.14, 0, 6.2832); ctx.fill();
-    ctx.fillStyle = "rgba(255,255,255,0.8)";
-    ctx.beginPath(); ctx.arc(R * 0.55, -R * 0.26, R * 0.05, 0, 6.2832); ctx.fill();
-
-    // beak
-    ctx.fillStyle = "#ff9f2f";
+    ctx.ellipse(h.x+h.w/2,h.y,h.w/2,h.h,0,Math.PI,0);
+    ctx.fill();
+  });
+  hills.filter(h=>h.layer===1).forEach(h=>{
+    ctx.fillStyle='#4a9a4a';
     ctx.beginPath();
-    ctx.moveTo(R * 0.92, -R * 0.05);
-    ctx.lineTo(R * 1.42, R * 0.14);
-    ctx.lineTo(R * 0.92, R * 0.3);
-    ctx.closePath(); ctx.fill();
-    ctx.fillStyle = "#e07a1e";
+    ctx.ellipse(h.x+h.w/2,h.y,h.w/2,h.h*1.1,0,Math.PI,0);
+    ctx.fill();
+  });
+}
+
+function drawCloud(x,y,s){
+  ctx.beginPath();
+  ctx.arc(x,y,12*s,0,Math.PI*2);
+  ctx.arc(x+16*s,y-6*s,14*s,0,Math.PI*2);
+  ctx.arc(x+34*s,y,13*s,0,Math.PI*2);
+  ctx.arc(x+16*s,y+4*s,11*s,0,Math.PI*2);
+  ctx.fill();
+}
+
+function drawGround(){
+  const gy=VH-GROUND_H;
+  ctx.fillStyle='#ded895';
+  ctx.fillRect(0,gy,VW,GROUND_H);
+  ctx.fillStyle='#5ee05e';
+  ctx.fillRect(0,gy,VW,14);
+  ctx.fillStyle='#4ec04e';
+  ctx.fillRect(0,gy+10,VW,6);
+  // scrolling grass tufts
+  const off=(frame*(state==='play'?speed:0.3)*0.5)%24;
+  ctx.fillStyle='#3da83d';
+  for(let x=-off;x<VW;x+=24){
     ctx.beginPath();
-    ctx.moveTo(R * 0.92, R * 0.14);
-    ctx.lineTo(R * 1.42, R * 0.14);
-    ctx.lineTo(R * 0.92, R * 0.3);
-    ctx.closePath(); ctx.fill();
-
-    // tail
-    ctx.fillStyle = "#ffb562";
+    ctx.moveTo(x,gy+14);
+    ctx.lineTo(x+8,gy);
+    ctx.lineTo(x+16,gy+14);
+    ctx.fill();
+  }
+  ctx.strokeStyle='#c4a84a';
+  ctx.lineWidth=2;
+  ctx.beginPath();
+  ctx.moveTo(0,gy);
+  ctx.lineTo(VW,gy);
+  ctx.stroke();
+}
+
+function drawPipes(){
+  const pw=PIPE_W;
+  pipes.forEach(p=>{
+    const cap=pw*0.15;
+    const grad1=ctx.createLinearGradient(p.x,0,p.x+pw,0);
+    grad1.addColorStop(0,'#3d9e2f');
+    grad1.addColorStop(0.3,'#5ed14a');
+    grad1.addColorStop(0.7,'#4caf3a');
+    grad1.addColorStop(1,'#2d7a22');
+    // top pipe body
+    ctx.fillStyle=grad1;
+    ctx.fillRect(p.x+2,0,pw-4,p.top);
+    roundRect(p.x-cap,p.top-28,pw+cap*2,28,4);
+    ctx.fill();
+    ctx.fillStyle='rgba(255,255,255,0.15)';
+    ctx.fillRect(p.x+4,0,6,p.top-28);
+    // bottom pipe
+    const by=p.top+p.gap;
+    ctx.fillStyle=grad1;
+    ctx.fillRect(p.x+2,by,pw-4,VH-by-GROUND_H);
+    roundRect(p.x-cap,by,pw+cap*2,28,4);
+    ctx.fill();
+    ctx.fillStyle='rgba(255,255,255,0.15)';
+    ctx.fillRect(p.x+4,by+28,6,VH-by-GROUND_H-28);
+    // outlines
+    ctx.strokeStyle='rgba(0,0,0,0.2)';
+    ctx.lineWidth=2;
+    ctx.strokeRect(p.x+2,0,pw-4,p.top-28);
+    ctx.strokeRect(p.x+2,by+28,pw-4,VH-by-GROUND_H-28);
+  });
+}
+
+function drawBird(){
+  const r=BIRD_R;
+  ctx.save();
+  ctx.translate(bird.x,bird.y);
+  ctx.rotate(bird.rot);
+
+  ctx.beginPath();
+  ctx.ellipse(0,0,r*1.1,r*0.95,0,0,Math.PI*2);
+  const bg=ctx.createRadialGradient(-r*0.3,-r*0.3,0,0,0,r);
+  bg.addColorStop(0,'#ffe566');
+  bg.addColorStop(1,'#f0c020');
+  ctx.fillStyle=bg;
+  ctx.fill();
+  ctx.strokeStyle='#d4a010';
+  ctx.lineWidth=1.5;
+  ctx.stroke();
+
+  ctx.save();
+  ctx.rotate(bird.wing);
+  ctx.beginPath();
+  ctx.ellipse(-2,2,r*0.65,r*0.4,0.3,0,Math.PI*2);
+  ctx.fillStyle='#f5d040';
+  ctx.fill();
+  ctx.strokeStyle='#d4a010';
+  ctx.lineWidth=1;
+  ctx.stroke();
+  ctx.restore();
+
+  ctx.beginPath();
+  ctx.arc(r*0.4,-r*0.25,r*0.38,0,Math.PI*2);
+  ctx.fillStyle='#fff';
+  ctx.fill();
+  ctx.strokeStyle='#333';
+  ctx.lineWidth=1;
+  ctx.stroke();
+  ctx.beginPath();
+  ctx.arc(r*0.5,-r*0.25,r*0.18,0,Math.PI*2);
+  ctx.fillStyle='#222';
+  ctx.fill();
+  ctx.beginPath();
+  ctx.arc(r*0.55,-r*0.35,r*0.08,0,Math.PI*2);
+  ctx.fillStyle='#fff';
+  ctx.fill();
+
+  ctx.beginPath();
+  ctx.moveTo(r*0.7,0);
+  ctx.lineTo(r*1.45,r*0.12);
+  ctx.lineTo(r*0.7,r*0.3);
+  ctx.closePath();
+  ctx.fillStyle='#ff8c00';
+  ctx.fill();
+  ctx.strokeStyle='#e07000';
+  ctx.lineWidth=1;
+  ctx.stroke();
+  ctx.beginPath();
+  ctx.moveTo(r*0.7,r*0.12);
+  ctx.lineTo(r*1.3,r*0.12);
+  ctx.strokeStyle='#e07000';
+  ctx.stroke();
+
+  ctx.beginPath();
+  ctx.ellipse(2,r*0.35,r*0.55,r*0.4,0,0,Math.PI*2);
+  ctx.fillStyle='rgba(255,255,255,0.35)';
+  ctx.fill();
+
+  ctx.restore();
+}
+
+function drawParticles(){
+  particles.forEach(p=>{
+    ctx.globalAlpha=p.life/p.max;
+    ctx.fillStyle=p.col;
     ctx.beginPath();
-    ctx.moveTo(-R * 1.05, -R * 0.12);
-    ctx.lineTo(-R * 1.55, -R * 0.32);
-    ctx.lineTo(-R * 1.2, R * 0.18);
-    ctx.closePath(); ctx.fill();
-
-    ctx.restore();
-  }
-
-  function drawParticles() {
-    for (const p of particles) {
-      const t = p.age / p.life;
-      ctx.globalAlpha = (1 - t) * 0.9;
-      ctx.fillStyle = "hsl(" + p.hue + ",100%,72%)";
-      ctx.beginPath();
-      ctx.arc(p.x, p.y, p.size * (1 - t * 0.5), 0, 6.2832);
-      ctx.fill();
-    }
-    ctx.globalAlpha = 1;
-  }
-
-  function render() {
-    let sx = 0, sy = 0;
-    if (shake > 0) {
-      sx = (Math.random() * 2 - 1) * shake;
-      sy = (Math.random() * 2 - 1) * shake;
+    ctx.arc(p.x,p.y,p.r*0.6,0,Math.PI*2);
+    ctx.fill();
+  });
+  ctx.globalAlpha=1;
+}
+
+function drawButtons(){
+  // Mute button
+  drawIconBtn(BTN.muteX,BTN.y,()=>{
+    ctx.strokeStyle='#fff';ctx.fillStyle='#fff';ctx.lineWidth=2;
+    const cx=BTN.muteX+9,cy=BTN.y+BTN.size/2;
+    ctx.beginPath();
+    ctx.moveTo(cx-4,cy-4);ctx.lineTo(cx,cy-4);ctx.lineTo(cx+5,cy-9);
+    ctx.lineTo(cx+5,cy+9);ctx.lineTo(cx,cy+4);ctx.lineTo(cx-4,cy+4);
+    ctx.closePath();ctx.fill();
+    if(muted){
+      ctx.beginPath();ctx.moveTo(cx+9,cy-6);ctx.lineTo(cx+16,cy+6);
+      ctx.moveTo(cx+16,cy-6);ctx.lineTo(cx+9,cy+6);ctx.stroke();
+    }else{
+      ctx.beginPath();ctx.arc(cx+8,cy,5,-0.6,0.6);ctx.stroke();
+      ctx.beginPath();ctx.arc(cx+8,cy,9,-0.6,0.6);ctx.stroke();
     }
-    ctx.save();
-    ctx.translate(sx, sy);
-
-    drawBackground();
-    drawPipes();
-    drawGround();
-    drawParticles();
-    drawBird();
-
-    ctx.restore();
-
-    if (flashAlpha > 0) {
-      ctx.fillStyle = "rgba(255,80,80," + flashAlpha.toFixed(3) + ")";
-      ctx.fillRect(0, 0, W, H);
+  });
+  // Pause / play button (only while in a round)
+  if(state==='play'||state==='paused'){
+    drawIconBtn(BTN.pauseX,BTN.y,()=>{
+      ctx.fillStyle='#fff';
+      const cx=BTN.pauseX+BTN.size/2,cy=BTN.y+BTN.size/2;
+      if(state==='play'){
+        ctx.fillRect(cx-6,cy-7,4,14);
+        ctx.fillRect(cx+2,cy-7,4,14);
+      }else{
+        ctx.beginPath();
+        ctx.moveTo(cx-5,cy-7);ctx.lineTo(cx+7,cy);ctx.lineTo(cx-5,cy+7);
+        ctx.closePath();ctx.fill();
+      }
+    });
+  }
+}
+function drawIconBtn(x,y,drawIcon){
+  ctx.fillStyle='rgba(0,0,0,0.25)';
+  roundRect(x,y,BTN.size,BTN.size,7);ctx.fill();
+  drawIcon();
+}
+
+function medalFor(s){
+  if(s>=40)return{name:'PLATINUM',c1:'#e5e4e2',c2:'#b0c4de'};
+  if(s>=30)return{name:'GOLD',c1:'#ffd700',c2:'#daa520'};
+  if(s>=20)return{name:'SILVER',c1:'#e0e0e0',c2:'#a8a8a8'};
+  if(s>=10)return{name:'BRONZE',c1:'#cd7f32',c2:'#8c5a2b'};
+  return null;
+}
+
+function drawUI(){
+  ctx.textAlign='center';
+  ctx.textBaseline='middle';
+
+  if(state==='ready'){
+    ctx.font='bold 42px system-ui,sans-serif';
+    ctx.fillStyle='rgba(0,0,0,0.25)';
+    ctx.fillText('FLAPPY',VW/2+2,VH*0.22+2);
+    ctx.fillStyle='#fff';
+    ctx.strokeStyle='#e87a20';
+    ctx.lineWidth=4;
+    ctx.strokeText('FLAPPY',VW/2,VH*0.22);
+    ctx.fillText('FLAPPY',VW/2,VH*0.22);
+
+    ctx.font='bold 28px system-ui,sans-serif';
+    ctx.fillStyle='rgba(0,0,0,0.25)';
+    ctx.fillText('CLONE',VW/2+2,VH*0.29+2);
+    ctx.fillStyle='#ffd700';
+    ctx.strokeStyle='#c49000';
+    ctx.lineWidth=3;
+    ctx.strokeText('CLONE',VW/2,VH*0.29);
+    ctx.fillText('CLONE',VW/2,VH*0.29);
+
+    const pulse=0.7+Math.sin(frame*0.08)*0.3;
+    ctx.globalAlpha=pulse;
+    ctx.font='16px system-ui,sans-serif';
+    ctx.fillStyle='#fff';
+    ctx.strokeStyle='rgba(0,0,0,0.3)';
+    ctx.lineWidth=3;
+    ctx.strokeText('Click / Tap / Space to flap',VW/2,VH*0.55);
+    ctx.fillText('Click / Tap / Space to flap',VW/2,VH*0.55);
+    ctx.globalAlpha=1;
+
+    if(best>0){
+      ctx.font='14px system-ui,sans-serif';
+      ctx.fillStyle='rgba(255,255,255,0.75)';
+      ctx.fillText('Best: '+best,VW/2,VH*0.62);
     }
   }
 
-  // ---- main loop (fixed-step physics, smooth render) ------------------
-  function loop(ts) {
-    if (!lastTime) lastTime = ts;
-    let frame = (ts - lastTime) / 1000;
-    lastTime = ts;
-    if (frame > 0.1) frame = 0.1; // clamp big gaps (tab restore)
-
-    if (hitStop > 0) { hitStop -= frame; }
-    else {
-      acc += frame;
-      while (acc >= FIXED_DT) { step(FIXED_DT); acc -= FIXED_DT; }
+  if(state==='play'||state==='over'||state==='paused'){
+    ctx.font='bold 48px system-ui,sans-serif';
+    ctx.fillStyle='rgba(0,0,0,0.2)';
+    ctx.fillText(String(score),VW/2+2,VH*0.12+2);
+    ctx.fillStyle='#fff';
+    ctx.strokeStyle='rgba(0,0,0,0.4)';
+    ctx.lineWidth=4;
+    ctx.strokeText(String(score),VW/2,VH*0.12);
+    ctx.fillText(String(score),VW/2,VH*0.12);
+  }
+
+  if(state==='paused'){
+    ctx.fillStyle='rgba(0,0,0,0.35)';
+    ctx.fillRect(0,0,VW,VH);
+    ctx.font='bold 34px system-ui,sans-serif';
+    ctx.fillStyle='#fff';
+    ctx.fillText('PAUSED',VW/2,VH*0.44);
+    ctx.font='15px system-ui,sans-serif';
+    ctx.fillStyle='rgba(255,255,255,0.85)';
+    ctx.fillText('Press P or tap ▶ to resume',VW/2,VH*0.52);
+  }
+
+  if(state==='over'){
+    const pw=220,ph=200;
+    const px=(VW-pw)/2,py=VH*0.26;
+    ctx.fillStyle='rgba(0,0,0,0.45)';
+    roundRect(px+3,py+3,pw,ph,12);ctx.fill();
+    ctx.fillStyle='rgba(255,255,255,0.94)';
+    roundRect(px,py,pw,ph,12);ctx.fill();
+    ctx.strokeStyle='#e87a20';
+    ctx.lineWidth=3;
+    roundRect(px,py,pw,ph,12);ctx.stroke();
+
+    ctx.font='bold 28px system-ui,sans-serif';
+    ctx.fillStyle='#e74c3c';
+    ctx.fillText('GAME OVER',VW/2,py+32);
+
+    // medal
+    const med=medalFor(score);
+    const medX=px+42, medY=py+92;
+    if(med){
+      const mg=ctx.createRadialGradient(medX-6,medY-6,2,medX,medY,26);
+      mg.addColorStop(0,med.c1);mg.addColorStop(1,med.c2);
+      ctx.beginPath();ctx.arc(medX,medY,26,0,Math.PI*2);
+      ctx.fillStyle=mg;ctx.fill();
+      ctx.strokeStyle='rgba(0,0,0,0.25)';ctx.lineWidth=2;ctx.stroke();
+      ctx.font='9px system-ui,sans-serif';
+      ctx.fillStyle='rgba(0,0,0,0.55)';
+      ctx.fillText(med.name,medX,medY);
+    }else{
+      ctx.beginPath();ctx.arc(medX,medY,26,0,Math.PI*2);
+      ctx.strokeStyle='#ccc';ctx.setLineDash([4,4]);ctx.lineWidth=2;ctx.stroke();
+      ctx.setLineDash([]);
+      ctx.font='9px system-ui,sans-serif';ctx.fillStyle='#bbb';
+      ctx.fillText('—',medX,medY);
     }
 
-    render();
-    requestAnimationFrame(loop);
-  }
+    ctx.textAlign='left';
+    ctx.font='13px system-ui,sans-serif';
+    ctx.fillStyle='#888';
+    ctx.fillText('SCORE',px+92,py+78);
+    ctx.fillText('BEST',px+92,py+112);
+    ctx.font='bold 26px system-ui,sans-serif';
+    ctx.fillStyle='#333';
+    ctx.fillText(String(score),px+92,py+98);
+    ctx.fillStyle=(score>=best&&score>0)?'#e87a20':'#333';
+    ctx.fillText(String(best),px+92,py+132);
+    ctx.textAlign='center';
+
+    if(score>=best&&score>0){
+      ctx.font='bold 12px system-ui,sans-serif';
+      ctx.fillStyle='#e87a20';
+      ctx.fillText('NEW BEST!',VW/2,py+160);
+    }
 
-  // ---- boot -----------------------------------------------------------
-  resize();
-  resetGame();
+    const bp=0.85+Math.sin(frame*0.1)*0.15;
+    ctx.globalAlpha=bp;
+    ctx.font='14px system-ui,sans-serif';
+    ctx.fillStyle='#e87a20';
+    ctx.fillText('Click / Space to restart',VW/2,py+183);
+    ctx.globalAlpha=1;
+  }
+}
+
+function render(){
+  const s=cssW/VW;                 // uniform virtual->CSS scale (aspect preserved)
+  ctx.setTransform(dpr*s,0,0,dpr*s,0,0);
+  ctx.clearRect(0,0,VW,VH);
+  ctx.save();
+  if(shake>0){
+    ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
+  }
+  drawBg();
+  drawPipes();
+  drawGround();
+  drawBird();
+  drawParticles();
+  ctx.restore();
+  drawButtons();
+  drawUI();
+}
+
+// ---- Fixed-timestep main loop: physics is framerate-independent ----
+let last=performance.now(), acc=0;
+function loop(now){
+  let dt=now-last; last=now;
+  if(dt>250)dt=250;              // clamp after tab-switch / stalls
+  acc+=dt;
+  let steps=0;
+  while(acc>=STEP && steps<5){ update(); acc-=STEP; steps++; }
+  render();
   requestAnimationFrame(loop);
+}
+
+init();
+requestAnimationFrame(loop);
 })();
 </script>
 </body>
diff --git a/games/flappy-sky/index.html b/games/flappy-sky/index.html
new file mode 100644
index 0000000..b92f2c2
--- /dev/null
+++ b/games/flappy-sky/index.html
@@ -0,0 +1,827 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
+<meta name="theme-color" content="#0e1018" />
+<meta name="description" content="Flappy Sky — a polished, self-contained Flappy-Bird-style game. Space, click, or tap to flap through neon gates." />
+<title>Flappy Sky</title>
+<style>
+  :root { --ink: #f8f8ff; }
+
+  html, body {
+    margin: 0;
+    padding: 0;
+    background: #0e1018;
+    overflow: hidden;
+    height: 100%;
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+    -webkit-tap-highlight-color: transparent;
+    touch-action: none;
+  }
+
+  canvas {
+    display: block;
+    background: radial-gradient(circle at 20% 20%, #586df2 0, #243046 40%, #0e1018 100%);
+    /* keeps focus outline from ever painting a box around the game */
+    outline: none;
+  }
+
+  /* --- overlay + ui --------------------------------------------------- */
+  #overlay {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    pointer-events: none;
+    user-select: none;
+    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
+    box-sizing: border-box;
+  }
+
+  #ui {
+    color: var(--ink);
+    text-align: center;
+    text-shadow: 0 2px 14px rgba(0,0,0,0.8);
+    transition: opacity .25s ease, transform .25s ease;
+    max-width: 92vw;
+  }
+  #ui.pop { animation: pop .4s cubic-bezier(.2,1.3,.4,1) both; }
+  @keyframes pop { 0% { transform: scale(.85); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
+
+  #title {
+    font-size: clamp(34px, 8vw, 54px);
+    font-weight: 900;
+    letter-spacing: 0.08em;
+    text-transform: uppercase;
+    margin: 0 0 6px;
+    background: linear-gradient(180deg, #fff 0%, #b9d3ff 60%, #72fffb 100%);
+    -webkit-background-clip: text;
+    background-clip: text;
+    -webkit-text-fill-color: transparent;
+    filter: drop-shadow(0 3px 10px rgba(0,0,0,.6));
+  }
+
+  #subtitle { font-size: clamp(13px, 3.4vw, 16px); opacity: 0.85; margin-bottom: 22px; }
+
+  /* medal + result block on game over */
+  #result { display: none; margin-bottom: 18px; }
+  #result.show { display: block; }
+  #medal {
+    width: 92px; height: 92px; margin: 4px auto 10px;
+    border-radius: 50%;
+    display: grid; place-items: center;
+    font-size: 34px;
+    box-shadow: 0 0 0 4px rgba(255,255,255,.15), 0 8px 24px rgba(0,0,0,.55), inset 0 2px 10px rgba(255,255,255,.4);
+    position: relative;
+  }
+  #medal::after {
+    content: ""; position: absolute; inset: 8px; border-radius: 50%;
+    border: 2px dashed rgba(255,255,255,.35);
+  }
+  .scoreline { display:flex; gap: 26px; justify-content:center; margin: 6px 0 2px; }
+  .scoreline .k { font-size: 11px; letter-spacing:.18em; text-transform:uppercase; opacity:.65; }
+  .scoreline .v { font-size: 30px; font-weight: 800; line-height: 1.05; }
+  #newbest { font-size: 12px; font-weight: 800; letter-spacing:.12em; color:#ffe07a;
+             text-transform: uppercase; margin-top: 6px; opacity: 0; }
+  #newbest.show { opacity: 1; animation: flick .8s ease-in-out infinite alternate; }
+  @keyframes flick { from { filter: brightness(1);} to { filter: brightness(1.6);} }
+
+  #prompt {
+    font-size: clamp(14px, 3.6vw, 17px);
+    opacity: 0.95;
+    display: inline-flex; align-items: center; gap: 8px;
+    padding: 10px 18px; border-radius: 999px;
+    background: rgba(255,255,255,.08);
+    border: 1px solid rgba(255,255,255,.16);
+    backdrop-filter: blur(3px);
+    animation: breathe 1.8s ease-in-out infinite;
+  }
+  @keyframes breathe { 0%,100% { transform: translateY(0);} 50% { transform: translateY(-4px);} }
+
+  /* --- HUD ------------------------------------------------------------ */
+  #scoreTop {
+    position: absolute;
+    top: calc(14px + env(safe-area-inset-top));
+    left: 50%;
+    transform: translateX(-50%);
+    color: #fff;
+    font-size: clamp(34px, 9vw, 52px);
+    font-weight: 900;
+    text-shadow: 0 3px 0 rgba(0,0,0,.35), 0 0 18px rgba(120,180,255,.5);
+    pointer-events: none;
+    transition: transform .08s ease;
+  }
+  #scoreTop.bump { transform: translateX(-50%) scale(1.28); }
+  #scoreTop.hidden { display: none; }
+
+  #best {
+    position: absolute;
+    top: calc(16px + env(safe-area-inset-top));
+    right: calc(14px + env(safe-area-inset-right));
+    color: rgba(255,255,255,.8);
+    font-size: 13px; font-weight: 700; letter-spacing:.05em;
+    text-shadow: 0 0 6px rgba(0,0,0,.8);
+    pointer-events: none;
+  }
+  #best.hidden { display: none; }
+
+  /* control buttons (mute / pause) */
+  #controls {
+    position: absolute;
+    top: calc(12px + env(safe-area-inset-top));
+    left: calc(12px + env(safe-area-inset-left));
+    display: flex; gap: 8px;
+  }
+  .btn {
+    width: 38px; height: 38px; border-radius: 10px;
+    border: 1px solid rgba(255,255,255,.18);
+    background: rgba(255,255,255,.07);
+    color: #fff; font-size: 17px; cursor: pointer;
+    display: grid; place-items: center;
+    backdrop-filter: blur(4px);
+    transition: background .15s ease, transform .1s ease;
+    -webkit-user-select: none; user-select: none;
+  }
+  .btn:hover { background: rgba(255,255,255,.16); }
+  .btn:active { transform: scale(.9); }
+
+  #smallInfo {
+    position: absolute;
+    bottom: calc(9px + env(safe-area-inset-bottom));
+    right: calc(12px + env(safe-area-inset-right));
+    font-size: 11px;
+    color: rgba(255,255,255,0.4);
+    text-shadow: 0 0 4px rgba(0,0,0,0.8);
+    pointer-events: none;
+  }
+
+  /* pause veil */
+  #pauseVeil {
+    position: absolute; inset: 0;
+    display: none; align-items: center; justify-content: center;
+    background: rgba(6,8,16,.55); backdrop-filter: blur(2px);
+    color: #fff; font-size: clamp(22px, 6vw, 34px); font-weight: 800;
+    letter-spacing: .12em; text-transform: uppercase; pointer-events: none;
+  }
+  #pauseVeil.show { display: flex; }
+
+  .hidden { display: none; }
+</style>
+</head>
+<body>
+<canvas id="game" tabindex="-1" aria-label="Flappy Sky game canvas"></canvas>
+
+<div id="scoreTop" class="hidden"><span id="scoreVal">0</span></div>
+<div id="best" class="hidden">Best <span id="bestVal">0</span></div>
+
+<div id="controls">
+  <button id="muteBtn" class="btn" title="Mute (M)" aria-label="Toggle sound">🔊</button>
+  <button id="pauseBtn" class="btn" title="Pause (P)" aria-label="Pause game">⏸</button>
+</div>
+
+<div id="overlay">
+  <div id="ui" class="pop">
+    <div id="title">FLAPPY SKY</div>
+    <div id="subtitle">Flap through the neon gates. How far can you fly?</div>
+
+    <div id="result">
+      <div id="medal">🥇</div>
+      <div class="scoreline">
+        <div><div class="k">Score</div><div class="v" id="rScore">0</div></div>
+        <div><div class="k">Best</div><div class="v" id="rBest">0</div></div>
+      </div>
+      <div id="newbest">★ New Best! ★</div>
+    </div>
+
+    <div id="prompt">Press <b>Space</b>, Click, or Tap to Start</div>
+  </div>
+</div>
+
+<div id="pauseVeil">Paused</div>
+
+<div id="smallInfo">Space / Click / Tap · P pause · M mute</div>
+
+<script>
+(function () {
+  "use strict";
+
+  const canvas = document.getElementById("game");
+  const ctx = canvas.getContext("2d", { alpha: false });
+
+  // ---- DOM refs -------------------------------------------------------
+  const overlay   = document.getElementById("overlay");
+  const ui        = document.getElementById("ui");
+  const uiPrompt  = document.getElementById("prompt");
+  const result    = document.getElementById("result");
+  const medalEl    = document.getElementById("medal");
+  const rScore    = document.getElementById("rScore");
+  const rBest     = document.getElementById("rBest");
+  const newbest   = document.getElementById("newbest");
+  const scoreTop  = document.getElementById("scoreTop");
+  const scoreVal  = document.getElementById("scoreVal");
+  const bestBox   = document.getElementById("best");
+  const bestVal   = document.getElementById("bestVal");
+  const pauseVeil = document.getElementById("pauseVeil");
+  const muteBtn   = document.getElementById("muteBtn");
+  const pauseBtn  = document.getElementById("pauseBtn");
+
+  const reduceMotion = window.matchMedia &&
+    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+  // ---- viewport -------------------------------------------------------
+  let W, H, scale, groundH;
+  function resize() {
+    const dpr = Math.min(window.devicePixelRatio || 1, 2.5);
+    W = window.innerWidth;
+    H = window.innerHeight;
+    canvas.width  = Math.round(W * dpr);
+    canvas.height = Math.round(H * dpr);
+    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+    scale = Math.min(Math.max(Math.min(W, H) / 600, 0.6), 1.6);
+    groundH = Math.round(Math.min(90 * scale, H * 0.14));
+    initBackground();
+    // keep the idle bird sensibly placed after a rotate/resize
+    if (bird && state !== "playing") { bird.x = W * 0.3; bird.baseY = H * 0.42; }
+  }
+
+  // ---- tuning ---------------------------------------------------------
+  const GRAVITY = 1500;      // px/s^2
+  const FLAP    = -450;      // impulse
+  const PIPE_W  = 84;        // pipe width (px, scaled at draw)
+  const PIPE_INTERVAL_START = 1500;  // ms between pipes
+  const PIPE_INTERVAL_MIN   = 950;
+  const GAP_START = 200;     // px gap (pre-scale)
+  const GAP_MIN   = 138;
+  const SPEED_START = 200;   // px/s
+  const SPEED_MAX   = 400;
+  const RAMP = 25;           // score at which difficulty maxes out
+  const FIXED_DT = 1 / 120;  // physics step
+
+  // ---- state ----------------------------------------------------------
+  let state = "ready"; // ready | playing | paused | gameover
+  let bird, pipes, particles, stars, hills, ground;
+  let score = 0, best = 0;
+  let pipeInterval, gapSize, speed;
+  let timeSincePipe = 0;
+  let shake = 0, flashAlpha = 0, hitStop = 0;
+  let lastTime = 0, acc = 0;
+  let muted = false;
+  let bgScroll = 0;
+
+  try { best = parseInt(localStorage.getItem("flappy_sky_best"), 10) || 0; } catch (e) {}
+  try { muted = localStorage.getItem("flappy_sky_muted") === "1"; } catch (e) {}
+  bestVal.textContent = best;
+
+  // ---- audio (WebAudio, no assets) -----------------------------------
+  let actx = null;
+  function audio() {
+    if (!actx) {
+      try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
+      catch (e) { actx = null; }
+    }
+    if (actx && actx.state === "suspended") actx.resume();
+    return actx;
+  }
+  function tone(freq, dur, type, vol, slideTo) {
+    if (muted) return;
+    const a = audio(); if (!a) return;
+    const t0 = a.currentTime;
+    const osc = a.createOscillator();
+    const g = a.createGain();
+    osc.type = type || "square";
+    osc.frequency.setValueAtTime(freq, t0);
+    if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(1, slideTo), t0 + dur);
+    g.gain.setValueAtTime(0.0001, t0);
+    g.gain.exponentialRampToValueAtTime(vol || 0.12, t0 + 0.008);
+    g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
+    osc.connect(g); g.connect(a.destination);
+    osc.start(t0); osc.stop(t0 + dur + 0.02);
+  }
+  const sfx = {
+    flap:  () => tone(560, 0.12, "square",   0.10, 780),
+    score: () => { tone(720, 0.09, "triangle", 0.12); setTimeout(() => tone(960, 0.10, "triangle", 0.12), 70); },
+    hit:   () => { tone(200, 0.18, "sawtooth", 0.16, 80); },
+    die:   () => { tone(340, 0.5, "sine", 0.12, 60); },
+    swoosh:() => tone(300, 0.14, "sine", 0.05, 120)
+  };
+
+  // ---- background -----------------------------------------------------
+  function initBackground() {
+    stars = [];
+    const starCount = Math.round((W * H) / 16000);
+    for (let i = 0; i < starCount; i++) {
+      stars.push({
+        x: Math.random() * W,
+        y: Math.random() * (H - groundH) * 0.9,
+        r: Math.random() * 1.5 + 0.4,
+        speed: 6 + Math.random() * 12,
+        phase: Math.random() * Math.PI * 2,
+        tw: 1.2 + Math.random() * 1.6
+      });
+    }
+    hills = [
+      { base: 0.70, amp: 42 * scale, freq: 0.0042, speed: 16, off: 0,   color: "#141a33" },
+      { base: 0.78, amp: 58 * scale, freq: 0.0030, speed: 30, off: 240, color: "#0c1024" }
+    ];
+    ground = { off: 0 };
+  }
+
+  // ---- game objects ---------------------------------------------------
+  function resetGame() {
+    bird = { x: W * 0.3, baseY: H * 0.42, y: H * 0.42, vy: 0, r: 16 * scale, rot: 0, flapT: 0 };
+    pipes = [];
+    particles = [];
+    timeSincePipe = 0;
+    score = 0;
+    scoreVal.textContent = "0";
+    pipeInterval = PIPE_INTERVAL_START;
+    gapSize = GAP_START * scale;
+    speed = SPEED_START;
+    shake = 0; flashAlpha = 0; hitStop = 0;
+  }
+
+  function difficulty() {
+    const t = Math.min(score / RAMP, 1);
+    pipeInterval = PIPE_INTERVAL_START - (PIPE_INTERVAL_START - PIPE_INTERVAL_MIN) * t;
+    gapSize = (GAP_START - (GAP_START - GAP_MIN) * t) * scale;
+    speed = SPEED_START + (SPEED_MAX - SPEED_START) * t;
+  }
+
+  function spawnPipe() {
+    const playH = H - groundH;
+    const margin = Math.max(50 * scale, playH * 0.09);
+    const minTop = margin;
+    const maxTop = playH - margin - gapSize;
+    const top = maxTop <= minTop ? (playH - gapSize) / 2
+                                 : minTop + Math.random() * (maxTop - minTop);
+    // hue drifts a touch over the run so later pipes read hotter
+    const hue = 165 - Math.min(score, 40) * 1.2;
+    pipes.push({ x: W + PIPE_W, top: top, gap: gapSize, scored: false, hue: hue });
+  }
+
+  function addParticles(x, y, count, hue, power) {
+    if (reduceMotion) count = Math.ceil(count / 3);
+    for (let i = 0; i < count; i++) {
+      const ang = Math.random() * Math.PI * 2;
+      const mag = (Math.random() * 150 + 40) * scale * (power || 1);
+      particles.push({
+        x, y,
+        vx: Math.cos(ang) * mag,
+        vy: Math.sin(ang) * mag - 40 * scale,
+        life: 0.5 + Math.random() * 0.35, age: 0,
+        size: (Math.random() * 3 + 2) * scale,
+        hue: hue + (Math.random() * 30 - 15)
+      });
+    }
+  }
+
+  // ---- input (single unified path, dedupes touch+mouse) --------------
+  let lastFlap = 0;
+  function flap() {
+    audio(); // unlock on first gesture
+    const now = performance.now();
+    if (now - lastFlap < 55) return; // debounce double-fire (touch->mouse)
+    lastFlap = now;
+
+    if (state === "ready") {
+      startGame();
+      doFlap();
+    } else if (state === "playing") {
+      doFlap();
+    } else if (state === "paused") {
+      resume();
+    } else if (state === "gameover") {
+      // small guard so the death tap doesn't instantly restart
+      if (now - gameoverAt > 350) toReady();
+    }
+  }
+  function doFlap() {
+    bird.vy = FLAP;
+    bird.flapT = 1;
+    addParticles(bird.x - bird.r * 0.8, bird.y + bird.r * 0.4, 6, 205, 0.7);
+    sfx.flap();
+  }
+
+  function startGame() {
+    state = "playing";
+    ui.classList.remove("pop");
+    overlay.classList.add("hidden");
+    scoreTop.classList.remove("hidden");
+    bestBox.classList.remove("hidden");
+    result.classList.remove("show");
+    sfx.swoosh();
+  }
+
+  function toReady() {
+    state = "ready";
+    resetGame();
+    result.classList.remove("show");
+    newbest.classList.remove("show");
+    uiPrompt.textContent = "Press Space, Click, or Tap to Start";
+    overlay.classList.remove("hidden");
+    ui.classList.remove("pop"); void ui.offsetWidth; ui.classList.add("pop");
+    scoreTop.classList.add("hidden");
+    bestBox.classList.add("hidden");
+  }
+
+  let gameoverAt = 0;
+  function gameOver() {
+    if (state !== "playing") return;
+    state = "gameover";
+    gameoverAt = performance.now();
+    hitStop = 0.08;
+    shake = reduceMotion ? 0 : 16;
+    flashAlpha = reduceMotion ? 0.25 : 0.6;
+    addParticles(bird.x, bird.y, reduceMotion ? 10 : 30, 40, 1.5);
+    sfx.hit(); sfx.die();
+
+    const isBest = score > best;
+    if (isBest) { best = score; try { localStorage.setItem("flappy_sky_best", best); } catch (e) {} }
+    bestVal.textContent = best;
+
+    rScore.textContent = score;
+    rBest.textContent = best;
+    setMedal(score);
+    result.classList.add("show");
+    newbest.classList.toggle("show", isBest);
+    uiPrompt.textContent = "Click / Tap / Space to Play Again";
+
+    // reveal overlay a beat after the death punch
+    setTimeout(() => {
+      if (state === "gameover") {
+        overlay.classList.remove("hidden");
+        ui.classList.remove("pop"); void ui.offsetWidth; ui.classList.add("pop");
+      }
+    }, 260);
+  }
+
+  function setMedal(s) {
+    let icon = "🥉", bg = "linear-gradient(160deg,#c9865b,#8a5a34)";
+    if (s >= 40)      { icon = "💎"; bg = "linear-gradient(160deg,#8ef7ff,#2aa7c9)"; }
+    else if (s >= 25) { icon = "🥇"; bg = "linear-gradient(160deg,#ffe27a,#d3a520)"; }
+    else if (s >= 12) { icon = "🥈"; bg = "linear-gradient(160deg,#e8eef7,#9aa7bd)"; }
+    else if (s >= 4)  { icon = "🥉"; bg = "linear-gradient(160deg,#c9865b,#8a5a34)"; }
+    else              { icon = "🍃"; bg = "linear-gradient(160deg,#7f8aa3,#4a5570)"; }
+    medalEl.textContent = icon;
+    medalEl.style.background = bg;
+  }
+
+  // ---- pause ----------------------------------------------------------
+  function togglePause() {
+    if (state === "playing") pause();
+    else if (state === "paused") resume();
+  }
+  function pause() {
+    if (state !== "playing") return;
+    state = "paused";
+    pauseVeil.classList.add("show");
+    pauseBtn.textContent = "▶";
+  }
+  function resume() {
+    if (state !== "paused") return;
+    state = "playing";
+    pauseVeil.classList.remove("show");
+    pauseBtn.textContent = "⏸";
+    lastTime = performance.now(); acc = 0; // avoid a physics jump
+  }
+
+  function toggleMute() {
+    muted = !muted;
+    muteBtn.textContent = muted ? "🔇" : "🔊";
+    try { localStorage.setItem("flappy_sky_muted", muted ? "1" : "0"); } catch (e) {}
+    if (!muted) sfx.swoosh();
+  }
+  muteBtn.textContent = muted ? "🔇" : "🔊";
+
+  // ---- listeners ------------------------------------------------------
+  window.addEventListener("resize", resize);
+  window.addEventListener("orientationchange", resize);
+
+  document.addEventListener("keydown", (e) => {
+    if (e.code === "Space" || e.code === "ArrowUp" || e.code === "Enter") {
+      e.preventDefault(); flap();
+    } else if (e.code === "KeyP") {
+      e.preventDefault(); togglePause();
+    } else if (e.code === "KeyM") {
+      e.preventDefault(); toggleMute();
+    }
+  });
+
+  // pointer handles mouse + touch + pen uniformly; debounce covers the rest
+  canvas.addEventListener("pointerdown", (e) => { e.preventDefault(); flap(); });
+  overlay.addEventListener("pointerdown", (e) => { e.preventDefault(); flap(); });
+  // fallback for browsers without pointer events
+  if (!window.PointerEvent) {
+    canvas.addEventListener("mousedown", flap);
+    canvas.addEventListener("touchstart", (e) => { e.preventDefault(); flap(); }, { passive: false });
+  }
+
+  muteBtn.addEventListener("pointerdown", (e) => { e.stopPropagation(); e.preventDefault(); toggleMute(); });
+  pauseBtn.addEventListener("pointerdown", (e) => { e.stopPropagation(); e.preventDefault(); togglePause(); });
+
+  // auto-pause when the tab/window loses focus mid-game
+  document.addEventListener("visibilitychange", () => { if (document.hidden) pause(); });
+  window.addEventListener("blur", () => pause());
+
+  // ---- physics --------------------------------------------------------
+  function step(dt) {
+    bgScroll += dt;
+
+    // parallax always animates (even on menus) for a living background
+    for (const s of stars) {
+      s.x -= s.speed * dt;
+      if (s.x < -4) { s.x = W + 4; s.y = Math.random() * (H - groundH) * 0.9; }
+      s.phase += dt * s.tw;
+    }
+    for (const h of hills) h.off += h.speed * dt;
+
+    if (state === "playing") {
+      ground.off = (ground.off + speed * dt) % 48;
+      difficulty();
+
+      bird.flapT = Math.max(0, bird.flapT - dt * 6);
+      bird.vy += GRAVITY * dt;
+      bird.y  += bird.vy * dt;
+      bird.rot = Math.max(-0.55, Math.min(1.4, Math.atan2(bird.vy, 420)));
+
+      // soft ceiling — clamp instead of instant death (classic feel)
+      if (bird.y - bird.r < 0) { bird.y = bird.r; if (bird.vy < 0) bird.vy = 0; }
+
+      timeSincePipe += dt * 1000;
+      if (timeSincePipe >= pipeInterval) { timeSincePipe -= pipeInterval; spawnPipe(); }
+
+      const bx = bird.x, by = bird.y, br = bird.r * 0.82;
+      const pw = PIPE_W * scale;
+
+      for (let i = pipes.length - 1; i >= 0; i--) {
+        const p = pipes[i];
+        p.x -= speed * dt;
+        if (p.x + pw < -20) { pipes.splice(i, 1); continue; }
+
+        if (!p.scored && p.x + pw < bx) {
+          p.scored = true; score++;
+          scoreVal.textContent = score;
+          scoreTop.classList.remove("bump"); void scoreTop.offsetWidth; scoreTop.classList.add("bump");
+          sfx.score();
+        }
+        // circle-vs-rect collision (accurate)
+        if (hitPipe(bx, by, br, p, pw)) { gameOver(); return; }
+      }
+
+      // ground death
+      if (by + br > H - groundH) { bird.y = H - groundH - br; gameOver(); return; }
+    } else if (state === "ready" || state === "gameover") {
+      // clean bob around a fixed base — no accumulating drift
+      const t = bgScroll;
+      bird.y = bird.baseY + Math.sin(t * 2.4) * 8 * scale;
+      bird.rot = Math.sin(t * 1.6) * 0.18;
+    }
+
+    // particles
+    for (let i = particles.length - 1; i >= 0; i--) {
+      const p = particles[i];
+      p.age += dt;
+      if (p.age >= p.life) { particles.splice(i, 1); continue; }
+      p.x += p.vx * dt; p.y += p.vy * dt;
+      p.vy += 520 * dt; p.vx *= 0.986;
+    }
+
+    if (shake > 0)      shake = Math.max(0, shake - dt * 60);
+    if (flashAlpha > 0) flashAlpha = Math.max(0, flashAlpha - dt * 2.2);
+  }
+
+  function hitPipe(cx, cy, r, p, pw) {
+    // top rect: (p.x,0,pw,p.top) ; bottom rect: (p.x,p.top+gap,pw,rest)
+    const bottomY = p.top + p.gap;
+    return rectHit(cx, cy, r, p.x, 0, pw, p.top) ||
+           rectHit(cx, cy, r, p.x, bottomY, pw, (H - groundH) - bottomY);
+  }
+  function rectHit(cx, cy, r, rx, ry, rw, rh) {
+    const nx = Math.max(rx, Math.min(cx, rx + rw));
+    const ny = Math.max(ry, Math.min(cy, ry + rh));
+    const dx = cx - nx, dy = cy - ny;
+    return dx * dx + dy * dy < r * r;
+  }
+
+  // ---- rendering ------------------------------------------------------
+  function drawBackground() {
+    const grd = ctx.createLinearGradient(0, 0, 0, H);
+    grd.addColorStop(0, "rgba(40,50,90,0.25)");
+    grd.addColorStop(1, "rgba(0,0,0,0.55)");
+    ctx.fillStyle = grd;
+    ctx.fillRect(0, 0, W, H);
+
+    for (const s of stars) {
+      const a = 0.25 + 0.55 * (0.5 + 0.5 * Math.sin(s.phase));
+      ctx.fillStyle = "rgba(230,240,255," + a.toFixed(3) + ")";
+      ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, 6.2832); ctx.fill();
+    }
+
+    // haze band
+    const cy = H * 0.3;
+    const cg = ctx.createLinearGradient(0, cy - 80, 0, cy + 80);
+    cg.addColorStop(0, "rgba(160,190,255,0)");
+    cg.addColorStop(0.4, "rgba(150,180,255,0.07)");
+    cg.addColorStop(1, "rgba(80,110,170,0)");
+    ctx.fillStyle = cg;
+    ctx.fillRect(0, cy - 80, W, 160);
+
+    for (const h of hills) {
+      const baseY = (H - groundH) * h.base;
+      ctx.fillStyle = h.color;
+      ctx.beginPath();
+      ctx.moveTo(0, H);
+      for (let x = 0; x <= W + 24; x += 20) {
+        const y = baseY + Math.sin((x + h.off) * h.freq) * h.amp;
+        ctx.lineTo(x, y);
+      }
+      ctx.lineTo(W, H); ctx.closePath(); ctx.fill();
+    }
+  }
+
+  function drawPipes() {
+    const pw = PIPE_W * scale;
+    const lip = 14 * scale;
+    const playH = H - groundH;
+    for (const p of pipes) {
+      const bottomY = p.top + p.gap;
+      const c1 = "hsl(" + p.hue + ",90%,55%)";
+      const c2 = "hsl(" + (p.hue + 18) + ",95%,72%)";
+      const c3 = "hsl(" + (p.hue - 8) + ",85%,42%)";
+
+      ctx.save();
+      ctx.shadowColor = "hsla(" + p.hue + ",100%,60%,0.6)";
+      ctx.shadowBlur = reduceMotion ? 0 : 16;
+
+      drawPipeBody(p.x, 0, pw, p.top, c1, c2, c3, lip, true);
+      drawPipeBody(p.x, bottomY, pw, playH - bottomY, c1, c2, c3, lip, false);
+      ctx.restore();
+    }
+  }
+  function drawPipeBody(x, y, w, h, c1, c2, c3, lip, isTop) {
+    if (h <= 0) return;
+    const g = ctx.createLinearGradient(x, 0, x + w, 0);
+    g.addColorStop(0, c3); g.addColorStop(0.18, c1);
+    g.addColorStop(0.5, c2); g.addColorStop(0.82, c1); g.addColorStop(1, c3);
+    ctx.fillStyle = g;
+    // main shaft
+    ctx.fillRect(x, y, w, h);
+    // lip (mouth end)
+    const ly = isTop ? y + h - lip : y;
+    const lw = w + 8 * scale;
+    ctx.fillRect(x - 4 * scale, ly, lw, lip);
+    // glossy highlight
+    ctx.fillStyle = "rgba(255,255,255,0.22)";
+    ctx.fillRect(x + w * 0.16, y, w * 0.1, h);
+  }
+
+  function drawGround() {
+    const gy = H - groundH;
+    const g = ctx.createLinearGradient(0, gy, 0, H);
+    g.addColorStop(0, "#3a3f2e");
+    g.addColorStop(0.12, "#4a5136");
+    g.addColorStop(1, "#20241a");
+    ctx.fillStyle = g;
+    ctx.fillRect(0, gy, W, groundH);
+
+    // top rim
+    ctx.fillStyle = "rgba(120,200,150,0.35)";
+    ctx.fillRect(0, gy, W, 3 * scale);
+
+    // scrolling hatch
+    ctx.strokeStyle = "rgba(0,0,0,0.18)";
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    for (let x = -48 + (-ground.off); x < W + 48; x += 24) {
+      ctx.moveTo(x, gy + 6 * scale);
+      ctx.lineTo(x + 14, H);
+    }
+    ctx.stroke();
+  }
+
+  function drawBird() {
+    const b = bird;
+    ctx.save();
+    ctx.translate(b.x, b.y);
+    ctx.rotate(b.rot);
+    const R = b.r;
+
+    // body
+    const bg = ctx.createRadialGradient(-R * 0.2, -R * 0.2, R * 0.2, 0, 0, R * 1.2);
+    bg.addColorStop(0, "#fffbe6"); bg.addColorStop(0.5, "#ffe07a"); bg.addColorStop(1, "#ff9f3f");
+    ctx.fillStyle = bg;
+    ctx.beginPath(); ctx.ellipse(0, 0, R * 1.15, R * 0.92, 0, 0, 6.2832); ctx.fill();
+
+    // belly
+    ctx.fillStyle = "rgba(255,255,255,0.85)";
+    ctx.beginPath(); ctx.ellipse(-R * 0.1, R * 0.12, R * 0.68, R * 0.56, 0, 0, 6.2832); ctx.fill();
+
+    // wing — flap amplitude tied to flapT (springs on each flap)
+    const wingBase = state === "playing" ? (0.35 - bird.flapT * 1.3)
+                                         : Math.sin(bgScroll * 8) * 0.4;
+    ctx.save();
+    ctx.translate(-R * 0.1, 0);
+    ctx.rotate(-0.7 + wingBase);
+    const wg = ctx.createLinearGradient(-R * 0.2, -R * 0.4, R * 0.5, R * 0.6);
+    wg.addColorStop(0, "#ffe7a3"); wg.addColorStop(1, "#ffb25a");
+    ctx.fillStyle = wg;
+    ctx.beginPath(); ctx.ellipse(0, 0, R * 0.92, R * 0.5, 0, 0, 6.2832); ctx.fill();
+    ctx.strokeStyle = "rgba(180,110,40,0.35)"; ctx.lineWidth = 1;
+    ctx.stroke();
+    ctx.restore();
+
+    // eye
+    ctx.fillStyle = "#fff";
+    ctx.beginPath(); ctx.arc(R * 0.42, -R * 0.22, R * 0.26, 0, 6.2832); ctx.fill();
+    ctx.fillStyle = "#1c1f2b";
+    ctx.beginPath(); ctx.arc(R * 0.5, -R * 0.22, R * 0.14, 0, 6.2832); ctx.fill();
+    ctx.fillStyle = "rgba(255,255,255,0.8)";
+    ctx.beginPath(); ctx.arc(R * 0.55, -R * 0.26, R * 0.05, 0, 6.2832); ctx.fill();
+
+    // beak
+    ctx.fillStyle = "#ff9f2f";
+    ctx.beginPath();
+    ctx.moveTo(R * 0.92, -R * 0.05);
+    ctx.lineTo(R * 1.42, R * 0.14);
+    ctx.lineTo(R * 0.92, R * 0.3);
+    ctx.closePath(); ctx.fill();
+    ctx.fillStyle = "#e07a1e";
+    ctx.beginPath();
+    ctx.moveTo(R * 0.92, R * 0.14);
+    ctx.lineTo(R * 1.42, R * 0.14);
+    ctx.lineTo(R * 0.92, R * 0.3);
+    ctx.closePath(); ctx.fill();
+
+    // tail
+    ctx.fillStyle = "#ffb562";
+    ctx.beginPath();
+    ctx.moveTo(-R * 1.05, -R * 0.12);
+    ctx.lineTo(-R * 1.55, -R * 0.32);
+    ctx.lineTo(-R * 1.2, R * 0.18);
+    ctx.closePath(); ctx.fill();
+
+    ctx.restore();
+  }
+
+  function drawParticles() {
+    for (const p of particles) {
+      const t = p.age / p.life;
+      ctx.globalAlpha = (1 - t) * 0.9;
+      ctx.fillStyle = "hsl(" + p.hue + ",100%,72%)";
+      ctx.beginPath();
+      ctx.arc(p.x, p.y, p.size * (1 - t * 0.5), 0, 6.2832);
+      ctx.fill();
+    }
+    ctx.globalAlpha = 1;
+  }
+
+  function render() {
+    let sx = 0, sy = 0;
+    if (shake > 0) {
+      sx = (Math.random() * 2 - 1) * shake;
+      sy = (Math.random() * 2 - 1) * shake;
+    }
+    ctx.save();
+    ctx.translate(sx, sy);
+
+    drawBackground();
+    drawPipes();
+    drawGround();
+    drawParticles();
+    drawBird();
+
+    ctx.restore();
+
+    if (flashAlpha > 0) {
+      ctx.fillStyle = "rgba(255,80,80," + flashAlpha.toFixed(3) + ")";
+      ctx.fillRect(0, 0, W, H);
+    }
+  }
+
+  // ---- main loop (fixed-step physics, smooth render) ------------------
+  function loop(ts) {
+    if (!lastTime) lastTime = ts;
+    let frame = (ts - lastTime) / 1000;
+    lastTime = ts;
+    if (frame > 0.1) frame = 0.1; // clamp big gaps (tab restore)
+
+    if (hitStop > 0) { hitStop -= frame; }
+    else {
+      acc += frame;
+      while (acc >= FIXED_DT) { step(FIXED_DT); acc -= FIXED_DT; }
+    }
+
+    render();
+    requestAnimationFrame(loop);
+  }
+
+  // ---- boot -----------------------------------------------------------
+  resize();
+  resetGame();
+  requestAnimationFrame(loop);
+})();
+</script>
+</body>
+</html>
diff --git a/games/rubiks-cube/index.html b/games/rubiks-cube/index.html
index 911cb58..692b46c 100644
--- a/games/rubiks-cube/index.html
+++ b/games/rubiks-cube/index.html
@@ -3,7 +3,7 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Rubik's Cube — CSS 3D</title>
+<title>Abrams Cube — CSS 3D</title>
 <style>
   :root { --s: 60px; }
   * { box-sizing: border-box; user-select: none; -webkit-user-select: none; }
@@ -72,7 +72,7 @@
 </style>
 </head>
 <body>
-<h1>Rubik's Cube</h1>
+<h1>Abrams Cube</h1>
 <p class="hint">Drag to orbit &middot; scroll to zoom &middot; keys U/D/L/R/F/B/M/E/S turn faces (Shift = counter-clockwise) &middot; &#8984;Z / Ctrl+Z undo</p>
 <div id="hud"><span>Moves <b id="movesHud">0</b></span><span>Time <b id="timeHud">0:00</b></span></div>
 <div id="viewport">

← de543fd Add Flappy Flight to the arcade  ·  back to Games Agentabrams  ·  auto-save: 2026-07-24T07:56:29 (1 files) — index.html 3e43c0f →