← Race the Sun results

Race the Sun

muse spark 1.2 contributor · full

Initial view of Race the Sun

Browser check: Artifact runtime error. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 40,332 bytes · SHA-256 794dc1792924
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RACE THE SUN — Endless Solar Run</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=JetBrains+Mono:wght@700&display=swap');
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{width:100%;height:100%;overflow:hidden;background:#0a0a14;font-family:'Space Grotesk',system-ui,sans-serif}
  canvas{display:block;width:100%;height:100%}
  #hud{position:absolute;inset:0;pointer-events:none;display:flex;flex-direction:column;justify-content:space-between;padding:14px 18px}
  .topbar{display:flex;gap:12px;align-items:stretch;flex-wrap:wrap}
  .pill{background:rgba(10,12,28,0.72);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.12);border-radius:14px;padding:10px 14px;display:flex;flex-direction:column;justify-content:center;min-width:110px;flex:1}
  .pill label{font-size:9px;letter-spacing:0.18em;color:rgba(255,255,255,0.55);font-weight:700}
  .pill strong{font-family:'JetBrains Mono',monospace;font-size:18px;color:#fff;line-height:1}
  .pill small{font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.6)}
  .energy-wrap{flex:1.6;min-width:200px}
  .bar{height:10px;background:rgba(255,255,255,0.12);border-radius:999px;overflow:hidden;margin-top:6px;position:relative}
  .bar i{position:absolute;inset:0;background:linear-gradient(90deg,#ff3b30,#ffcc00);width:100%;transform-origin:left;transition:width 0.12s linear}
  .bar b{position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.5));opacity:0}
  .boosting .bar b{opacity:1;animation:shine 0.3s linear infinite}
  @keyframes shine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
  .bottombar{display:flex;justify-content:space-between;align-items:end;gap:12px}
  .speedo{background:rgba(10,12,28,0.72);border:1px solid rgba(255,255,255,0.12);border-radius:14px;padding:10px 14px;min-width:140px}
  .speedo label{font-size:9px;letter-spacing:0.18em;color:rgba(255,255,255,0.55);font-weight:700}
  .speedo strong{font-family:'JetBrains Mono',monospace;color:#fff;font-size:20px}
  .controls-hint{color:rgba(255,255,255,0.55);font-size:11px;line-height:1.4;text-align:right}
  .controls-hint kbd{background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.2);border-bottom-width:2px;border-radius:4px;padding:1px 5px;font-family:'JetBrains Mono',monospace;font-size:10px;color:#fff}
  #overlay{position:absolute;inset:0;display:grid;place-items:center;background:radial-gradient(800px 600px at 50% 30%, rgba(255,120,40,0.18), transparent 70%), linear-gradient(180deg, rgba(10,10,20,0.2), rgba(5,5,15,0.85));backdrop-filter:blur(2px)}
  .card{width:min(560px,92vw);background:rgba(14,16,32,0.88);border:1px solid rgba(255,255,255,0.14);border-radius:22px;padding:28px;box-shadow:0 20px 60px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1);text-align:center;pointer-events:auto}
  .badge{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(90deg,#ff3b30,#ff8a00);color:#fff;font-weight:700;font-size:11px;letter-spacing:0.14em;padding:6px 12px;border-radius:999px}
  h1{font-size:42px;line-height:0.9;letter-spacing:-0.04em;color:#fff;margin:14px 0 8px;font-weight:800}
  h1 span{background:linear-gradient(90deg,#ffcc00,#ff3b30);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
  .subtitle{color:rgba(255,255,255,0.65);font-size:13px;line-height:1.5}
  .cta{margin-top:18px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
  .btn{appearance:none;border:0;border-radius:999px;padding:13px 22px;font-weight:800;letter-spacing:0.06em;font-size:13px;cursor:pointer;transition:transform 0.08s, box-shadow 0.08s}
  .btn-primary{background:#fff;color:#111;box-shadow:0 8px 24px rgba(255,255,255,0.18)}
  .btn-primary:hover{transform:translateY(-1px)}
  .btn-ghost{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.14)}
  .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:18px}
  .stat{background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:12px}
  .stat label{font-size:9px;letter-spacing:0.14em;color:rgba(255,255,255,0.5);font-weight:700}
  .stat strong{font-family:'JetBrains Mono',monospace;color:#fff;font-size:16px;display:block;margin-top:4px}
  .hidden{display:none !important}
  #flash{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none;transition:opacity 0.12s}
  .shake{animation:shake 0.18s}
  @keyframes shake{0%,100%{transform:translate(0,0)}25%{transform:translate(-6px,2px)}50%{transform:translate(6px,-2px)}75%{transform:translate(-3px,-4px)}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="flash"></div>
<div id="hud">
  <div class="topbar">
    <div class="pill"><label>DISTANCE</label><strong id="hudDist">0 m</strong><small id="hudBest">BEST 0 m</small></div>
    <div class="pill"><label>SCORE</label><strong id="hudScore">0</strong><small id="hudMult">×1.0 MULTIPLIER</small></div>
    <div class="pill energy-wrap" id="energyPill"><label>ENERGY — STAY IN LIGHT <span id="sunPct" style="float:right;color:#ffcc00">☀ 100%</span></label><div class="bar"><i id="energyBar"></i><b></b></div><small id="energyHint" style="display:flex;justify-content:space-between;margin-top:4px"><span>SHADOW DRAINS ×3</span><span style="color:#ffcc00">SPACE = BOOST</span></small></div>
  </div>
  <div class="bottombar">
    <div class="speedo"><label>SPEED</label><div><strong id="hudSpeed">0</strong> <small style="color:rgba(255,255,255,0.6);font-family:'JetBrains Mono',monospace">KM/H</small></div><div class="bar" style="height:4px;margin-top:6px"><i id="speedBar" style="background:linear-gradient(90deg,#00e5ff,#7c4dff)"></i></div></div>
    <div class="controls-hint">
      <div><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> or <kbd>ARROWS</kbd> — strafe & pitch</div>
      <div><kbd>SPACE</kbd> boost &nbsp; <kbd>R</kbd> restart &nbsp; <kbd>P</kbd> pause</div>
    </div>
  </div>
</div>

<div id="overlay">
  <div class="card" id="titleCard">
    <div class="badge">◉ SOLAR ENDLESS RUNNER — 60 FPS</div>
    <h1>RACE THE <span>SUN</span></h1>
    <p class="subtitle">The sun is sinking. Your craft lives on light. Weave through the geometric wasteland, skim energy cells, chain multipliers — and outrun the dusk. Speed builds every second.</p>
    <div class="cta">
      <button class="btn btn-primary" id="playBtn">▶ IGNITE ENGINES — SPACE</button>
      <button class="btn btn-ghost" id="howBtn">HOW TO PLAY</button>
    </div>
    <div class="stats">
      <div class="stat"><label>HIGH SCORE</label><strong id="statHigh">0</strong></div>
      <div class="stat"><label>BEST DISTANCE</label><strong id="statDist">0 m</strong></div>
      <div class="stat"><label>LAST RUN</label><strong id="statLast">—</strong></div>
    </div>
    <p class="subtitle" style="margin-top:14px;font-size:11px;opacity:0.7">Tip: Near-miss obstacles for bonus score + screen shake. Tall shadows drain energy — stay sunlit. Boost sparingly.</p>
  </div>

  <div class="card hidden" id="gameOverCard">
    <div class="badge" style="background:linear-gradient(90deg,#1a1a2e,#ff3b30)">☠ SIGNAL LOST</div>
    <h1 id="goTitle" style="font-size:34px">ECLIPSED</h1>
    <p class="subtitle" id="goReason">You ran out of light.</p>
    <div class="stats">
      <div class="stat"><label>SCORE</label><strong id="goScore">0</strong></div>
      <div class="stat"><label>DISTANCE</label><strong id="goDist">0 m</strong></div>
      <div class="stat"><label>MULTIPLIER</label><strong id="goMult">×1.0</strong></div>
    </div>
    <div class="cta">
      <button class="btn btn-primary" id="retryBtn">↻ RETRY — R</button>
      <button class="btn btn-ghost" id="menuBtn">MENU</button>
    </div>
  </div>

  <div class="card hidden" id="howCard">
    <h1 style="font-size:28px">FLIGHT MANUAL</h1>
    <div style="text-align:left;color:rgba(255,255,255,0.8);font-size:13px;line-height:1.6;margin-top:12px;display:grid;gap:10px">
      <div><b style="color:#ffcc00">MOVE</b> — A/D or ←/→ strafe, W/S or ↑/↓ pitch. Craft banks and leans — snappy 100ms easing with inertia.</div>
      <div><b style="color:#00e5ff">BOOST</b> — Hold SPACE to surge. Costs energy, triggers speed-lines & chromatic burst.</div>
      <div><b style="color:#ff3b30">SUN</b> — Sinks over ~90s. Energy drains slowly in light, 3× in shadow. Pick up ⚡ to recharge.</div>
      <div><b style="color:#7cff7c">PICKUPS</b> — ⚡ Energy (cyan), ✦ Score (gold +100×mult), ◈ Multiplier orb (magenta, +0.2×, max 5×).</div>
      <div><b>NEAR MISS</b> — Skim obstacles for +50 and juicy shake. Crash = instant game over.</div>
    </div>
    <div class="cta"><button class="btn btn-primary" id="backBtn">BACK</button></div>
  </div>
</div>

<script>
// === TUNABLES ===
const C = {
  // movement
  baseSpeed: 38,          // world units per second at start
  maxSpeed: 165,          // cap
  accel: 0.62,            // speed increase per second
  boostMult: 1.55,        // speed multiplier while boosting
  boostDrain: 22,         // energy per second while boosting
  strafeSpeed: 14,        // max lateral speed
  pitchSpeed: 9,
  easing: 0.18,           // lerp factor per frame (~100ms)
  inertia: 0.92,          // velocity damping
  boundsX: 26,            // horizontal limits
  boundsY: 14,            // vertical limits (pitch)
  // energy / sun
  energyMax: 100,
  drainLight: 2.2,        // per second in light
  drainShadow: 7.5,       // per second in shadow
  sunDuration: 92,        // seconds until sunset (game over if not extended by pickups? we just drain)
  // generation
  spawnInterval: 0.42,    // seconds between waves (decreases with speed)
  pickupChance: 0.62,
  // scoring
  scorePerSec: 10,
  tokenScore: 100,
  nearMissScore: 50,
  // visuals
  fov: 420,
  camZ: -28,
  camY: 9,
  horizonY: 0.38,         // 0-1 screen height
  // colors
  skyTop: '#0b0b1e',
  skyMid: '#3a1a6a',
  skyHorizon: '#ff6a2a',
  ground: '#0f0f1a',
  gridColor: 'rgba(255,120,40,0.18)',
  // difficulty
  obstacleDensityRamp: 0.018, // increase per second
};

// === SETUP ===
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W=0,H=0,DPR=1;
function resize(){
  DPR = Math.min(2, devicePixelRatio||1);
  W = innerWidth; H = innerHeight;
  canvas.width = W*DPR; canvas.height = H*DPR;
  canvas.style.width = W+'px'; canvas.style.height = H+'px';
  ctx.setTransform(DPR,0,0,DPR,0,0);
}
addEventListener('resize', resize); resize();

// === STATE ===
let seed = (Math.random()*1e9)|0;
function rnd(){ seed = (seed*1664525+1013904223)>>>0; return seed/4294967296; }
function rand(a,b){ return a + rnd()*(b-a); }

const state = {
  mode: 'title', // title | playing | paused | gameover
  t: 0, distance: 0, score: 0, mult: 1, speed: C.baseSpeed,
  energy: C.energyMax, sunT: 1, // 1 = high noon, 0 = set
  x: 0, y: 0, vx: 0, vy: 0, targetX:0, targetY:0, roll:0, pitch:0,
  shake: 0, flash: 0, boosting: false,
  nearMissCooldown: 0,
  spawnTimer: 0,
  objects: [], particles: [], speedLines: [],
  keys: {},
  highScore: parseInt(localStorage.getItem('rts_high')||'0'),
  bestDist: parseInt(localStorage.getItem('rts_dist')||'0'),
  lastScore: 0, lastDist:0,
};

const hudDist = document.getElementById('hudDist');
const hudBest = document.getElementById('hudBest');
const hudScore = document.getElementById('hudScore');
const hudMult = document.getElementById('hudMult');
const hudSpeed = document.getElementById('hudSpeed');
const energyBar = document.getElementById('energyBar');
const speedBar = document.getElementById('speedBar');
const sunPct = document.getElementById('sunPct');
const energyPill = document.getElementById('energyPill');
const overlay = document.getElementById('overlay');
const titleCard = document.getElementById('titleCard');
const gameOverCard = document.getElementById('gameOverCard');
const howCard = document.getElementById('howCard');
const flashEl = document.getElementById('flash');

// === AUDIO (WebAudio) ===
let actx=null;
function ensureAudio(){ if(!actx) actx = new (window.AudioContext||window.webkitAudioContext)(); if(actx.state==='suspended') actx.resume(); }
function tone(freq, dur, type='sine', gain=0.18, slideTo){
  if(!actx) return;
  const o=actx.createOscillator(), g=actx.createGain();
  o.type=type; o.frequency.value=freq; o.connect(g); g.connect(actx.destination);
  g.gain.value=gain; g.gain.exponentialRampToValueAtTime(0.001, actx.currentTime+dur);
  if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, actx.currentTime+dur);
  o.start(); o.stop(actx.currentTime+dur);
}
function sfxPickup(kind){
  ensureAudio();
  if(kind==='energy') { tone(520,0.12,'sine',0.22,880); setTimeout(()=>tone(880,0.12,'sine',0.14,1040),80); }
  else if(kind==='score') { tone(700,0.09,'square',0.12,900); }
  else if(kind==='mult') { tone(300,0.15,'sawtooth',0.14,600); setTimeout(()=>tone(600,0.18,'sine',0.16,900),120); }
  else if(kind==='near') { tone(180,0.08,'square',0.18,90); }
  else if(kind==='crash') { tone(120,0.4,'sawtooth',0.35,30); tone(60,0.5,'square',0.2); }
  else if(kind==='boost') { tone(80,0.2,'sawtooth',0.08,160); }
}
let windNode=null, windGain=null;
function startWind(){
  ensureAudio();
  if(windNode) return;
  const bufSize = actx.sampleRate*1;
  const buf = actx.createBuffer(1, bufSize, actx.sampleRate);
  const ch = buf.getChannelData(0);
  for(let i=0;i<bufSize;i++) ch[i]=(Math.random()*2-1)*0.5;
  windNode = actx.createBufferSource(); windNode.buffer=buf; windNode.loop=true;
  const filter = actx.createBiquadFilter(); filter.type='bandpass'; filter.frequency.value=900; filter.Q.value=0.7;
  windGain = actx.createGain(); windGain.gain.value=0;
  windNode.connect(filter); filter.connect(windGain); windGain.connect(actx.destination);
  windNode.start();
}
function updateWind(){
  if(!windGain) return;
  const s = state.speed / C.maxSpeed;
  windGain.gain.value = state.mode==='playing' ? s*0.07 : 0;
  if(windGain && actx){
    // slight pitch with speed
  }
}

// === INPUT ===
addEventListener('keydown', e=>{
  const k = e.key.toLowerCase();
  state.keys[k]=true;
  if(k===' '){ e.preventDefault(); if(state.mode==='title') startGame(); else if(state.mode==='gameover') startGame(); else if(state.mode==='playing') state.boosting=true; }
  if(k==='r'){ if(state.mode==='gameover' || state.mode==='playing') startGame(); }
  if(k==='p'){ if(state.mode==='playing') setPaused(true); else if(state.mode==='paused') setPaused(false); }
  if(k==='arrowleft' || k==='arrowright' || k==='arrowup' || k==='arrowdown') e.preventDefault();
  if(k==='escape' && state.mode==='paused') setPaused(false);
});
addEventListener('keyup', e=>{
  const k=e.key.toLowerCase();
  state.keys[k]=false;
  if(k===' ') state.boosting=false;
});
['playBtn','retryBtn'].forEach(id=> document.getElementById(id).addEventListener('click', startGame));
document.getElementById('howBtn').addEventListener('click', ()=> showCard('how'));
document.getElementById('backBtn').addEventListener('click', ()=> showCard('title'));
document.getElementById('menuBtn').addEventListener('click', ()=> showCard('title'));
canvas.addEventListener('click', ()=>{
  if(state.mode==='title' || state.mode==='gameover') startGame();
});
function showCard(which){
  titleCard.classList.toggle('hidden', which!=='title');
  gameOverCard.classList.toggle('hidden', which!=='gameover');
  howCard.classList.toggle('hidden', which!=='how');
  overlay.classList.toggle('hidden', which==='none');
  if(which==='none') overlay.style.display='none'; else overlay.style.display='grid';
  if(which==='title') updateTitleStats();
}
function setPaused(p){
  state.mode = p ? 'paused' : 'playing';
  overlay.classList.toggle('hidden', !p);
  if(p){
    overlay.style.display='grid';
    titleCard.classList.add('hidden'); gameOverCard.classList.add('hidden'); howCard.classList.add('hidden');
    // show pause card inline
    titleCard.classList.remove('hidden');
    titleCard.querySelector('h1').innerHTML='PAUSED';
    titleCard.querySelector('.subtitle').textContent='Press P or ESC to resume';
    titleCard.querySelector('#playBtn').textContent='RESUME — P';
    titleCard.querySelector('#playBtn').onclick=()=>setPaused(false);
  } else {
    overlay.style.display='none';
    titleCard.querySelector('h1').innerHTML='RACE THE <span>SUN</span>';
    titleCard.querySelector('.subtitle').textContent='The sun is sinking. Your craft lives on light. Weave through the geometric wasteland, skim energy cells, chain multipliers — and outrun the dusk. Speed builds every second.';
    document.getElementById('playBtn').textContent='▶ IGNITE ENGINES — SPACE';
    document.getElementById('playBtn').onclick=startGame;
  }
}
function updateTitleStats(){
  document.getElementById('statHigh').textContent = state.highScore.toLocaleString();
  document.getElementById('statDist').textContent = state.bestDist.toLocaleString()+' m';
  document.getElementById('statLast').textContent = state.lastScore ? state.lastScore.toLocaleString() : '—';
}

// === GENERATION ===
function spawnWave(){
  const speedFactor = Math.min(1, state.t/90);
  const count = 1 + (rnd()<0.5+speedFactor*0.3 ? 1 : 0) + (rnd()<speedFactor*0.6 ? 1 : 0);
  const z = 220 + rand(0,40); // ahead
  // choose pattern
  const pattern = Math.floor(rand(0,4));
  const spots = [];
  if(pattern===0){ // random scattered
    for(let i=0;i<count;i++) spots.push({x: rand(-C.boundsX+4, C.boundsX-4), y: rand(-2,6)});
  } else if(pattern===1){ // gate with gap
    const gap = Math.floor(rand(0,3))-1; // -1,0,1
    for(let lane=-1; lane<=1; lane++) if(lane!==gap) spots.push({x: lane*13, y: 0});
    if(rnd()<0.5) spots.push({x: gap*13, y: 8}); // overhead
  } else if(pattern===2){ // wall
    const x = rand(-10,10);
    spots.push({x, y:0, w: rand(14,22), h: rand(8,14)});
  } else { // staggered pillars
    for(let i=0;i<count;i++) spots.push({x: (i-1)*12 + rand(-4,4), y: rand(-1,2)});
  }

  spots.forEach(s=>{
    const isObstacle = rnd()<0.72;
    if(isObstacle){
      const type = rnd()<0.33 ? 'pyramid' : rnd()<0.5 ? 'box' : 'arch';
      state.objects.push({
        kind:'obstacle', type, x:s.x, y:s.y||0, z,
        w: s.w|| rand(6,10), h: s.h|| rand(7,13), d: rand(6,10),
        hue: 8 + rand(-6,18), // reddish
        shadowLen: rand(18,34),
      });
    } else {
      // pickup
      const r = rnd();
      let pkind='score';
      if(r<0.45) pkind='energy';
      else if(r<0.75) pkind='score';
      else pkind='mult';
      state.objects.push({kind:'pickup', pkind, x:s.x, y: rand(2,9), z, r:3.2, bob: rand(0,Math.PI*2)});
    }
  });

  // extra pickups in gaps
  if(rnd()<C.pickupChance){
    state.objects.push({kind:'pickup', pkind: rnd()<0.5?'score':'energy', x: rand(-C.boundsX+3, C.boundsX-3), y: rand(3,10), z: z+ rand(12,28), r:3.2, bob: rand(0,Math.PI*2)});
  }
}

// === PARTICLES ===
function emit(x,y,z, n, color, speed=1){
  for(let i=0;i<n;i++){
    state.particles.push({
      x: x + rand(-1,1), y: y+rand(-1,1), z: z+rand(-2,2),
      vx: rand(-6,6)*speed, vy: rand(-4,8)*speed, vz: rand(-8,6)*speed,
      life: rand(0.3,0.7), t:0, color, size: rand(1.5,4.5)
    });
  }
}
function emitSpeedLines(n=1){
  for(let i=0;i<n;i++) state.speedLines.push({x: rand(-W/2,W/2), y: rand(-H*0.2, H*0.6), len: rand(40,120), alpha: rand(0.3,0.7), speed: rand(900,1600)});
}

// === COLLISION ===
function aabbCollide(ax,ay,aw,ah, bx,by,bw,bh){
  return Math.abs(ax-bx) < (aw+bw)/2 && Math.abs(ay-by) < (ah+bh)/2;
}

// === GAME FLOW ===
function resetRun(){
  seed = (Math.random()*1e9)|0;
  state.t=0; state.distance=0; state.score=0; state.mult=1; state.energy=C.energyMax; state.sunT=1;
  state.x=0; state.y=4; state.vx=0; state.vy=0; state.targetX=0; state.targetY=4; state.roll=0; state.pitch=0;
  state.speed=C.baseSpeed; state.objects=[]; state.particles=[]; state.speedLines=[]; state.spawnTimer=0; state.shake=0; state.flash=0; state.boosting=false;
  for(let i=0;i<6;i++) spawnWaveAt(rand(40,200));
}
function spawnWaveAt(z){
  // helper for initial spread
  const x = rand(-C.boundsX+4, C.boundsX-4);
  if(rnd()<0.7) state.objects.push({kind:'obstacle', type:'box', x, y:0, z, w:rand(7,10), h:rand(8,12), d:8, hue:12, shadowLen:24});
  else state.objects.push({kind:'pickup', pkind:'score', x, y:rand(3,8), z, r:3.2, bob:0});
}
function startGame(){
  ensureAudio(); startWind();
  resetRun();
  state.mode='playing';
  showCard('none');
  sfxPickup('boost');
}
function gameOver(reason){
  state.mode='gameover';
  state.lastScore = Math.floor(state.score);
  state.lastDist = Math.floor(state.distance);
  if(state.lastScore > state.highScore){ state.highScore=state.lastScore; localStorage.setItem('rts_high', state.highScore); }
  if(state.lastDist > state.bestDist){ state.bestDist=state.lastDist; localStorage.setItem('rts_dist', state.bestDist); }
  document.getElementById('goScore').textContent = state.lastScore.toLocaleString();
  document.getElementById('goDist').textContent = state.lastDist.toLocaleString()+' m';
  document.getElementById('goMult').textContent = '×'+state.mult.toFixed(1);
  document.getElementById('goReason').textContent = reason;
  document.getElementById('goTitle').textContent = reason.includes('Sun') ? 'SUNSET' : reason.includes('Energy') ? 'POWER LOST' : 'CRITICAL IMPACT';
  showCard('gameover');
  sfxPickup('crash');
  state.shake=14;
  flashEl.style.opacity='0.55'; setTimeout(()=>flashEl.style.opacity='0',180);
}

// === UPDATE ===
let lastTime = performance.now();
function update(dt){
  if(state.mode!=='playing') return;
  state.t += dt;
  // sun sinks
  state.sunT = Math.max(0, 1 - state.t / C.sunDuration);
  // speed ramp
  const targetSpeed = Math.min(C.maxSpeed, C.baseSpeed + state.t * C.accel);
  state.speed += (targetSpeed - state.speed) * 0.02;
  const curSpeed = state.boosting && state.energy>0 ? state.speed * C.boostMult : state.speed;
  state.distance += curSpeed * dt * 0.6; // scale to meters
  state.score += C.scorePerSec * dt * state.mult;

  // input
  const left = state.keys['a']||state.keys['arrowleft'];
  const right = state.keys['d']||state.keys['arrowright'];
  const up = state.keys['w']||state.keys['arrowup'];
  const down = state.keys['s']||state.keys['arrowdown'];
  state.targetX += ( (right?1:0) - (left?1:0) ) * C.strafeSpeed * dt * 3.2;
  state.targetY += ( (up?1:0) - (down?1:0) ) * C.pitchSpeed * dt * 3.2;
  state.targetX = Math.max(-C.boundsX, Math.min(C.boundsX, state.targetX));
  state.targetY = Math.max(-2, Math.min(12, state.targetY));
  // auto-center slightly
  state.targetX *= 0.999;

  // easing with inertia
  const dx = state.targetX - state.x;
  const dy = state.targetY - state.y;
  state.vx = state.vx * C.inertia + dx * C.easing * 60 * dt;
  state.vy = state.vy * C.inertia + dy * C.easing * 60 * dt;
  state.x += state.vx;
  state.y += state.vy;
  state.x = Math.max(-C.boundsX, Math.min(C.boundsX, state.x));
  state.y = Math.max(-1, Math.min(12, state.y));
  state.roll = -state.vx * 0.045;
  state.pitch = -state.vy * 0.04;

  // boost energy
  if(state.boosting && state.energy>0){
    state.energy -= C.boostDrain * dt;
    if(state.energy<=0){ state.energy=0; state.boosting=false; }
    if(Math.random()<0.5) emit(state.x, state.y-1, -6, 2, '#ffcc00', 0.8);
  }

  // energy drain (shadow check: if near tall obstacle ahead, consider shadow)
  let inShadow = false;
  for(const o of state.objects){
    if(o.kind!=='obstacle') continue;
    if(Math.abs(o.z - 12) < 18 && Math.abs(o.x - state.x) < o.w*0.6 && o.h>9){
      // if sun low, shadow longer to one side
      const shadowDir = 1; // sun to right, shadow to left
      const shadowX = o.x - (1-state.sunT)*18;
      if(Math.abs(shadowX - state.x) < 10) inShadow = true;
    }
  }
  // also random shadow when off-center away from sun
  if(state.x < -8 && state.sunT < 0.6) inShadow = true;
  const drain = inShadow ? C.drainShadow : C.drainLight;
  state.energy -= drain * dt;
  if(state.energy<=0){ state.energy=0; gameOver('Energy depleted — you faded into shadow.'); return; }
  if(state.sunT<=0){ gameOver('The sun has set. Darkness claims the dunes.'); return; }

  // spawn
  const interval = Math.max(0.18, C.spawnInterval - state.t*0.003 - (curSpeed-C.baseSpeed)*0.002);
  state.spawnTimer += dt;
  if(state.spawnTimer > interval){ state.spawnTimer=0; spawnWave(); }

  // move world
  for(let i=state.objects.length-1;i>=0;i--){
    const o = state.objects[i];
    o.z -= curSpeed * dt;
    if(o.kind==='pickup') o.bob += dt*4;
    // cull behind
    if(o.z < C.camZ+2){
      state.objects.splice(i,1);
      continue;
    }
    // collision / pickup
    const dz = Math.abs(o.z); // player at z=0
    if(dz < 6){
      if(o.kind==='pickup'){
        const dist = Math.hypot(o.x - state.x, o.y - state.y);
        if(dist < 4.2){
          // collect
          if(o.pkind==='energy'){ state.energy = Math.min(C.energyMax, state.energy+28); state.flash=0.22; }
          else if(o.pkind==='score'){ state.score += C.tokenScore * state.mult; }
          else if(o.pkind==='mult'){ state.mult = Math.min(5, state.mult+0.2); }
          sfxPickup(o.pkind==='energy'?'energy': o.pkind==='mult'?'mult':'score');
          emit(o.x,o.y,o.z, 14, o.pkind==='energy'?'#00e5ff': o.pkind==='mult'?'#ff00ff':'#ffcc00', 1.2);
          state.objects.splice(i,1);
          state.shake = Math.max(state.shake, 3);
          continue;
        }
      } else if(o.kind==='obstacle'){
        const pw=3.2, ph=2.2;
        const ow=o.w, oh=o.h;
        const collide = aabbCollide(state.x, state.y, pw, ph, o.x, o.y+oh/2, ow, oh);
        if(collide){
          emit(state.x, state.y, 0, 28, '#ff3b30', 1.6);
          gameOver('Hull breach — you clipped a monolith at '+Math.floor(curSpeed)+' km/h.');
          return;
        } else {
          // near miss
          const nearDist = Math.hypot(o.x - state.x, (o.y+oh/2)-state.y);
          const threshold = (ow+pw)/2 + 1.2;
          if(nearDist < threshold+1.8 && nearDist > threshold-0.2 && state.nearMissCooldown<=0 && dz<4){
            state.score += C.nearMissScore * state.mult;
            state.nearMissCooldown=0.35;
            state.shake = Math.max(state.shake, 5);
            sfxPickup('near');
            emit(o.x, o.y+oh/2, o.z, 6, '#ffffff', 0.9);
            // flash
            flashEl.style.background='rgba(255,255,255,0.35)'; flashEl.style.opacity='0.35';
            setTimeout(()=>{ flashEl.style.opacity='0'; flashEl.style.background='#fff'; },90);
          }
        }
      }
    }
  }
  if(state.nearMissCooldown>0) state.nearMissCooldown-=dt;

  // particles
  for(let i=state.particles.length-1;i>=0;i--){
    const p=state.particles[i];
    p.t+=dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.z+=p.vz*dt - curSpeed*dt*0.1;
    p.vy -= 6*dt;
    if(p.t>p.life) state.particles.splice(i,1);
  }
  // speed lines
  if(curSpeed > 85){
    if(Math.random()< (curSpeed-80)/140) emitSpeedLines(1);
  }
  for(let i=state.speedLines.length-1;i>=0;i--){
    const s=state.speedLines[i];
    s.y += s.speed*dt*0.02;
    s.alpha -= dt*0.9;
    if(s.alpha<=0) state.speedLines.splice(i,1);
  }

  // shake decay
  state.shake *= Math.pow(0.82, dt*60);
  state.flash *= Math.pow(0.85, dt*60);
}

// === RENDER ===
function project(x,y,z){
  const dz = z - C.camZ;
  const scale = C.fov / (dz);
  const sx = W/2 + (x - 0) * scale;
  const sy = H * C.horizonY + (C.camY - y) * scale * 0.92;
  return {x:sx, y:sy, s:scale, dz};
}

function draw(){
  // shake offset
  const shakeX = (Math.random()-0.5)*state.shake;
  const shakeY = (Math.random()-0.5)*state.shake;
  ctx.save();
  ctx.translate(shakeX, shakeY);

  // sky gradient
  const sunY = H*C.horizonY - state.sunT * H*0.62;
  const sunX = W*0.72;
  const grad = ctx.createLinearGradient(0,0,0,H*C.horizonY+80);
  grad.addColorStop(0, C.skyTop);
  grad.addColorStop(0.45, C.skyMid);
  grad.addColorStop(0.78, '#ff7a2e');
  grad.addColorStop(1, '#ffcc6a');
  ctx.fillStyle=grad;
  ctx.fillRect(0,0,W,H);

  // sun
  const sunR = 44 + (1-state.sunT)*18;
  // glow
  const glow = ctx.createRadialGradient(sunX,sunY,sunR*0.6, sunX,sunY,sunR*5);
  glow.addColorStop(0,'rgba(255,220,120,0.9)');
  glow.addColorStop(0.25,'rgba(255,120,40,0.35)');
  glow.addColorStop(1,'rgba(255,80,20,0)');
  ctx.fillStyle=glow;
  ctx.beginPath(); ctx.arc(sunX,sunY,sunR*5,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='#fff8d0';
  ctx.shadowColor='rgba(255,200,80,0.9)'; ctx.shadowBlur=24;
  ctx.beginPath(); ctx.arc(sunX,sunY,sunR,0,Math.PI*2); ctx.fill();
  ctx.shadowBlur=0;
  // sun bands
  ctx.strokeStyle='rgba(255,255,255,0.18)'; ctx.lineWidth=1;
  for(let i=1;i<=3;i++){ ctx.beginPath(); ctx.arc(sunX,sunY,sunR+i*7, -0.2, Math.PI+0.2); ctx.stroke(); }

  // horizon haze
  ctx.fillStyle='rgba(255,180,120,0.18)';
  ctx.fillRect(0, H*C.horizonY-18, W, 36);

  // ground
  const groundY = H*C.horizonY;
  ctx.fillStyle=C.ground;
  ctx.fillRect(0, groundY, W, H-groundY+20);
  // grid
  ctx.strokeStyle=C.gridColor; ctx.lineWidth=1;
  const gridOffset = (state.distance*2.2) % 40;
  // horizontal lines perspective
  for(let i=0;i<22;i++){
    const z = i*18 - gridOffset;
    const p = project(0,0,z);
    if(p.y < groundY) continue;
    const w = (C.fov/(z - C.camZ))*C.boundsX*2.2;
    ctx.beginPath();
    ctx.moveTo(W/2 - w, p.y);
    ctx.lineTo(W/2 + w, p.y);
    ctx.globalAlpha = Math.max(0, 1 - i*0.07);
    ctx.stroke();
  }
  ctx.globalAlpha=1;
  // vertical lines
  for(let lane=-2; lane<=2; lane++){
    const x = lane*13;
    const p1 = project(x,0, -10);
    const p2 = project(x,0, 320);
    ctx.beginPath(); ctx.moveTo(p1.x,p1.y); ctx.lineTo(p2.x,p2.y); ctx.stroke();
  }
  // distant dunes silhouette
  ctx.fillStyle='rgba(20,14,30,0.9)';
  ctx.beginPath();
  ctx.moveTo(0, groundY);
  for(let x=0;x<=W;x+=18){
    const h = Math.sin(x*0.008 + state.distance*0.01)*18 + Math.cos(x*0.015)*12;
    ctx.lineTo(x, groundY - h - 6);
  }
  ctx.lineTo(W, groundY); ctx.closePath(); ctx.fill();
  // secondary dunes
  ctx.fillStyle='rgba(40,22,50,0.55)';
  ctx.beginPath(); ctx.moveTo(0, groundY);
  for(let x=0;x<=W;x+=22){
    const h = Math.sin(x*0.012 - state.distance*0.008)*10 + 4;
    ctx.lineTo(x, groundY - h);
  }
  ctx.lineTo(W, groundY); ctx.closePath(); ctx.fill();

  // sort objects back to front
  const sorted = [...state.objects].sort((a,b)=> b.z - a.z);
  for(const o of sorted){
    if(o.z < 0) continue;
    if(o.kind==='obstacle'){
      drawObstacle(o);
    } else {
      drawPickup(o);
    }
  }

  // particles
  for(const p of state.particles){
    const pr = project(p.x,p.y,p.z);
    if(pr.dz<=0) continue;
    const a = 1 - p.t/p.life;
    ctx.globalAlpha = a;
    ctx.fillStyle=p.color;
    ctx.beginPath(); ctx.arc(pr.x, pr.y, p.size*pr.s*0.08, 0, Math.PI*2); ctx.fill();
  }
  ctx.globalAlpha=1;

  // player craft
  drawCraft();

  // speed lines overlay
  if(state.speed>90 || state.boosting){
    ctx.save();
    ctx.globalAlpha = Math.min(0.55, (state.speed-70)/120 + (state.boosting?0.2:0));
    ctx.strokeStyle='#ffffff';
    ctx.lineWidth=1.5;
    for(const s of state.speedLines){
      ctx.globalAlpha = s.alpha*0.7;
      ctx.beginPath(); ctx.moveTo(W/2 + s.x*0.6, s.y); ctx.lineTo(W/2 + s.x*0.6, s.y + s.len*0.12); ctx.stroke();
    }
    ctx.restore();
    // chromatic aberration when boosting
    if(state.boosting){
      ctx.save(); ctx.globalAlpha=0.08; ctx.fillStyle='#ff0040';
      ctx.fillRect(0,0,W,H); ctx.globalCompositeOperation='screen';
      ctx.fillStyle='#00ffff'; ctx.fillRect(2,0,W,H); ctx.restore();
    }
    // vignette
    const vig = ctx.createRadialGradient(W/2,H/2, Math.min(W,H)*0.3, W/2,H/2, Math.max(W,H)*0.8);
    vig.addColorStop(0,'transparent'); vig.addColorStop(1,'rgba(0,0,0,0.45)');
    ctx.fillStyle=vig; ctx.fillRect(0,0,W,H);
  }

  // flash
  if(state.flash>0){
    ctx.fillStyle=`rgba(255,255,255,${state.flash*0.22})`;
    ctx.fillRect(0,0,W,H);
  }

  ctx.restore();

  // HUD update
  hudDist.textContent = Math.floor(state.distance).toLocaleString()+' m';
  hudBest.textContent = 'BEST '+state.bestDist.toLocaleString()+' m';
  hudScore.textContent = Math.floor(state.score).toLocaleString();
  hudMult.textContent = '×'+state.mult.toFixed(1)+' MULTIPLIER';
  hudMult.style.color = state.mult>1.4 ? '#ffcc00' : 'rgba(255,255,255,0.6)';
  hudSpeed.textContent = Math.floor((state.boosting? state.speed*C.boostMult : state.speed)*3.1);
  energyBar.style.width = (state.energy/C.energyMax*100)+'%';
  energyBar.style.background = state.energy<25 ? 'linear-gradient(90deg,#ff0040,#ff6a00)' : state.energy<50 ? 'linear-gradient(90deg,#ff8a00,#ffcc00)' : 'linear-gradient(90deg,#00e5ff,#7cff7c)';
  sunPct.textContent = '☀ '+(state.sunT*100|0)+'%';
  sunPct.style.color = state.sunT<0.25 ? '#ff3b30' : '#ffcc00';
  speedBar.style.width = ((state.speed/C.maxSpeed)*100)+'%';
  energyPill.classList.toggle('boosting', state.boosting);
  // low energy pulse
  if(state.energy<20 && state.mode==='playing'){
    energyPill.style.boxShadow = (Math.sin(state.t*8)>0) ? '0 0 18px rgba(255,60,60,0.9)' : 'none';
  } else energyPill.style.boxShadow='none';
}

function drawObstacle(o){
  const base = project(o.x, 0, o.z);
  const top = project(o.x, o.h, o.z);
  const scale = base.s;
  const w = o.w * scale * 0.55;
  const h = (top.y - base.y);
  // shadow on ground
  const shadowW = (o.w + o.shadowLen*(1-state.sunT)) * scale *0.55;
  ctx.fillStyle='rgba(0,0,0,0.35)';
  ctx.beginPath();
  ctx.ellipse(base.x, base.y+2, shadowW, 8*scale*0.08, 0,0,Math.PI*2); ctx.fill();
  // body
  const light = state.sunT>0.5 ? 1 : 0.6 + state.sunT*0.8;
  const hue = o.hue;
  if(o.type==='box'){
    // front face
    ctx.fillStyle=`hsl(${hue} 78% ${Math.floor(52*light)}%)`;
    ctx.fillRect(base.x - w/2, base.y - Math.abs(h), w, Math.abs(h));
    // top face
    ctx.fillStyle=`hsl(${hue} 85% ${Math.floor(68*light)}%)`;
    ctx.beginPath();
    ctx.moveTo(base.x - w/2, base.y - Math.abs(h));
    ctx.lineTo(base.x - w/2 + 10*scale*0.08, top.y);
    ctx.lineTo(base.x + w/2 + 10*scale*0.08, top.y);
    ctx.lineTo(base.x + w/2, base.y - Math.abs(h));
    ctx.closePath(); ctx.fill();
    // side
    ctx.fillStyle=`hsl(${hue} 70% ${Math.floor(32*light)}%)`;
    ctx.beginPath();
    ctx.moveTo(base.x + w/2, base.y - Math.abs(h));
    ctx.lineTo(base.x + w/2 + 10*scale*0.08, top.y);
    ctx.lineTo(base.x + w/2 + 10*scale*0.08, base.y);
    ctx.lineTo(base.x + w/2, base.y);
    ctx.closePath(); ctx.fill();
    // edge highlight
    ctx.strokeStyle='rgba(255,255,255,0.18)'; ctx.lineWidth=1; ctx.strokeRect(base.x - w/2, base.y - Math.abs(h), w, Math.abs(h));
  } else if(o.type==='pyramid'){
    ctx.fillStyle=`hsl(${hue} 80% ${Math.floor(55*light)}%)`;
    ctx.beginPath();
    ctx.moveTo(base.x, top.y);
    ctx.lineTo(base.x - w/2, base.y);
    ctx.lineTo(base.x + w/2, base.y);
    ctx.closePath(); ctx.fill();
    // shading
    ctx.fillStyle=`hsl(${hue} 70% ${Math.floor(35*light)}%)`;
    ctx.beginPath(); ctx.moveTo(base.x, top.y); ctx.lineTo(base.x + w/2, base.y); ctx.lineTo(base.x, base.y); ctx.closePath(); ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,0.15)'; ctx.beginPath(); ctx.moveTo(base.x, top.y); ctx.lineTo(base.x, base.y); ctx.stroke();
  } else { // arch
    const archW = w;
    const archH = Math.abs(h);
    ctx.fillStyle=`hsl(${hue} 75% ${Math.floor(50*light)}%)`;
    ctx.fillRect(base.x - archW/2, base.y - archH, 4*scale*0.18, archH);
    ctx.fillRect(base.x + archW/2 - 4*scale*0.18, base.y - archH, 4*scale*0.18, archH);
    ctx.fillRect(base.x - archW/2, base.y - archH, archW, 4*scale*0.18);
    // inner shadow
    ctx.fillStyle='rgba(0,0,0,0.25)';
    ctx.fillRect(base.x - archW/2 + 4*scale*0.18, base.y - archH + 4*scale*0.18, archW - 8*scale*0.18, archH - 4*scale*0.18);
  }
}

function drawPickup(o){
  const p = project(o.x, o.y + Math.sin(o.bob)*0.6, o.z);
  const s = p.s;
  const r = o.r * s * 0.22;
  // glow
  ctx.shadowColor = o.pkind==='energy' ? '#00e5ff' : o.pkind==='mult' ? '#ff00ff' : '#ffcc00';
  ctx.shadowBlur = 14;
  if(o.pkind==='energy'){
    // diamond
    ctx.fillStyle='#7af2ff';
    ctx.beginPath();
    ctx.moveTo(p.x, p.y - r*1.2);
    ctx.lineTo(p.x + r, p.y);
    ctx.lineTo(p.x, p.y + r*1.2);
    ctx.lineTo(p.x - r, p.y);
    ctx.closePath(); ctx.fill();
    ctx.fillStyle='#fff'; ctx.font=`${Math.max(10, r*0.9)}px JetBrains Mono`; ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('⚡', p.x, p.y+0.5);
  } else if(o.pkind==='score'){
    ctx.fillStyle='#ffcc00';
    ctx.beginPath(); ctx.arc(p.x,p.y,r,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#1a1a1a'; ctx.font=`bold ${Math.max(9, r*0.85)}px Space Grotesk`; ctx.textAlign='center'; ctx.fillText('✦', p.x, p.y+3);
    // orbit ring
    ctx.shadowBlur=0;
    ctx.strokeStyle='rgba(255,204,0,0.45)'; ctx.lineWidth=1;
    ctx.beginPath(); ctx.ellipse(p.x,p.y,r*1.6,r*0.9, Math.sin(o.bob)*0.3,0,Math.PI*2); ctx.stroke();
    ctx.shadowBlur=14;
  } else {
    // mult orb
    const grad = ctx.createRadialGradient(p.x-r*0.3,p.y-r*0.3, r*0.2, p.x,p.y,r*1.2);
    grad.addColorStop(0,'#ffb3ff'); grad.addColorStop(1,'#9a00ff');
    ctx.fillStyle=grad;
    ctx.beginPath(); ctx.arc(p.x,p.y,r,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#fff'; ctx.font=`bold ${Math.max(8,r*0.7)}px JetBrains Mono`; ctx.textAlign='center'; ctx.fillText('×', p.x, p.y+2);
  }
  ctx.shadowBlur=0;
  // tether line to ground
  const gp = project(o.x, 0, o.z);
  ctx.strokeStyle='rgba(255,255,255,0.12)'; ctx.setLineDash([3,4]); ctx.beginPath(); ctx.moveTo(p.x,p.y+r); ctx.lineTo(gp.x,gp.y); ctx.stroke(); ctx.setLineDash([]);
}

function drawCraft(){
  // craft screen position fixed near bottom center, but offset by x/y
  const cx = W/2 + state.x * 14; // screen offset
  const cy = H*0.78 - state.y * 10;
  const roll = state.roll;
  const pitch = state.pitch;
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(roll);
  // shadow on ground
  const gp = project(state.x,0,0);
  ctx.fillStyle='rgba(0,0,0,0.28)';
  ctx.beginPath(); ctx.ellipse((gp.x - cx)*0.35, (H*C.horizonY+28 - cy), 26, 8, 0,0,Math.PI*2); ctx.fill();
  // thrust
  const thrust = 1 + (state.boosting? 1.6 : state.speed/C.maxSpeed);
  ctx.save();
  ctx.translate(0, 14 + pitch*6);
  const flameGrad = ctx.createLinearGradient(0,0,0, 18*thrust);
  flameGrad.addColorStop(0,'#fff7a0'); flameGrad.addColorStop(0.3,'#ff8a00'); flameGrad.addColorStop(1,'transparent');
  ctx.fillStyle=flameGrad;
  ctx.beginPath();
  ctx.moveTo(-7,0); ctx.lineTo(0, 16*thrust); ctx.lineTo(7,0); ctx.closePath(); ctx.fill();
  // side thrusters
  ctx.fillStyle='rgba(0,229,255,0.9)';
  ctx.beginPath(); ctx.ellipse(-10, -2, 3, 6*thrust*0.6, 0,0,Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.ellipse(10, -2, 3, 6*thrust*0.6, 0,0,Math.PI*2); ctx.fill();
  ctx.restore();

  // body
  ctx.fillStyle='#eef2ff';
  ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.lineWidth=1.2;
  ctx.beginPath();
  // sleek dart shape
  ctx.moveTo(0, -22 + pitch*4);
  ctx.lineTo(-13, 10);
  ctx.lineTo(-6, 14);
  ctx.lineTo(6, 14);
  ctx.lineTo(13, 10);
  ctx.closePath();
  ctx.fill(); ctx.stroke();
  // cockpit
  ctx.fillStyle='#0a0a1a';
  ctx.beginPath(); ctx.ellipse(0, -6 + pitch*2, 5, 7, 0,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='#00e5ff'; ctx.globalAlpha=0.9;
  ctx.beginPath(); ctx.ellipse(0, -7 + pitch*2, 3.2, 4.5, 0,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
  // wings detail
  ctx.fillStyle='#7c4dff';
  ctx.fillRect(-13, 2, 6, 2);
  ctx.fillRect(7, 2, 6, 2);
  // highlight
  ctx.strokeStyle='rgba(255,255,255,0.7)'; ctx.lineWidth=1;
  ctx.beginPath(); ctx.moveTo(0,-20); ctx.lineTo(-6,6); ctx.stroke();
  ctx.restore();
}

// === LOOP ===
function loop(now){
  const dt = Math.min(0.033, (now - lastTime)/1000);
  lastTime = now;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
updateTitleStats();
showCard('title');

// subtle auto-demo drift in title to show movement
setInterval(()=>{
  if(state.mode==='title'){
    state.t+=0.016;
    state.sunT = 0.65 + Math.sin(state.t*0.3)*0.08;
  }
},16);
</script>
</body>
</html>