<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Learn Technical Independence]]></title><description><![CDATA[Intelligence is more than knowing what to say when prompted]]></description><link>https://learn.piescale.com/</link><image><url>https://learn.piescale.com/favicon.png</url><title>Learn Technical Independence</title><link>https://learn.piescale.com/</link></image><generator>Ghost 4.48</generator><lastBuildDate>Mon, 14 Sep 2026 20:08:29 GMT</lastBuildDate><atom:link href="https://learn.piescale.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Best Rad Racer/Out Run Yet]]></title><description><![CDATA[<p>A Mix of Qwen 3.8 max, and GLM 5.2 max just made by far the best 16 bit era quality racer game in a single .html file. &#xA0;These are the big 2.4 trillion, and 753 billion parameter Chinese models running on arena.ai hardware. &#xA0;Arena</p>]]></description><link>https://learn.piescale.com/best-rad-racer-out-run/</link><guid isPermaLink="false">6aa4fb748ea9b20001c1636d</guid><dc:creator><![CDATA[Josh]]></dc:creator><pubDate>Sat, 12 Sep 2026 07:48:18 GMT</pubDate><media:content url="https://learn.piescale.com/content/images/2026/09/Selection_832.png" medium="image"/><content:encoded><![CDATA[<img src="https://learn.piescale.com/content/images/2026/09/Selection_832.png" alt="Best Rad Racer/Out Run Yet"><p>A Mix of Qwen 3.8 max, and GLM 5.2 max just made by far the best 16 bit era quality racer game in a single .html file. &#xA0;These are the big 2.4 trillion, and 753 billion parameter Chinese models running on arena.ai hardware. &#xA0;Arena ranks models against each other, and appears to be the best way to get free tokens with a big context window, although they may flag game development, and I somehow got lucky with this. &#xA0;My general sense is providers don&apos;t want to fund free game development, and it&apos;s easy to burn tokens here, although hard to tell exactly how much. &#xA0;My guess would be the final product here was around a million tokens, which prices out to around $6 at current prices.</p><!--kg-card-begin: html--><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAD HORIZON &#x2014; Turbo Grand Prix</title>
<link rel="icon" href="data:image/svg+xml,&lt;svg xmlns=&apos;http://www.w3.org/2000/svg&apos; viewBox=&apos;0 0 100 100&apos;&gt;&lt;text y=&apos;.9em&apos; font-size=&apos;90&apos;&gt;&#x1F3C1;&lt;/text&gt;&lt;/svg&gt;">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&amp;family=Chakra+Petch:wght@400;600;700&amp;display=swap" rel="stylesheet">
<style>
  :root{
    --bg:#0b0d10; --panel:#14171c; --panel2:#1a1e25; --line:#2b313c;
    --ink:#f3ead9; --dim:#8b93a1; --cyan:#3fe0d0; --orange:#ff8a3d;
    --red:#ff4b4b; --yellow:#ffd23f;
  }
  *{ margin:0; padding:0; box-sizing:border-box; }
  html,body{ height:100%; }
  body{
    background:var(--bg);
    color:var(--ink);
    font-family:"Chakra Petch", sans-serif;
    display:flex; flex-direction:column; align-items:center;
    min-height:100vh; padding:26px 14px 34px;
    overflow-x:hidden;
    -webkit-user-select:none; user-select:none;
  }
  /* layered ambient page texture */
  body::before{
    content:""; position:fixed; inset:0; z-index:0; pointer-events:none;
    background:
      repeating-linear-gradient(115deg, rgba(255,255,255,.016) 0 2px, transparent 2px 16px),
      radial-gradient(120% 70% at 50% 118%, rgba(255,138,61,.10), transparent 60%),
      radial-gradient(90% 60% at 50% -18%, rgba(63,224,208,.07), transparent 60%);
  }
  body::after{
    content:""; position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.5;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
  }
  .cabinet{ position:relative; z-index:1; width:min(1060px, 96vw); }

  /* ---------- MARQUEE ---------- */
  .marquee{
    background:linear-gradient(180deg,#171b22,#0d0f14 70%);
    border:1px solid #262c36; border-bottom:none;
    border-radius:16px 16px 0 0;
    padding:18px 26px 14px;
    display:flex; align-items:center; justify-content:space-between; gap:18px;
    position:relative; overflow:hidden;
    box-shadow:0 -1px 0 rgba(255,255,255,.06) inset;
  }
  .marquee::after{ /* shine sweep */
    content:""; position:absolute; top:0; bottom:0; width:38%;
    background:linear-gradient(105deg, transparent, rgba(255,255,255,.05), transparent);
    transform:translateX(-160%);
    animation:shine 7s ease-in-out infinite;
  }
  @keyframes shine{ 0%,55%{transform:translateX(-160%)} 80%,100%{transform:translateX(320%)} }
  .logo-block{ line-height:1; }
  .logo{
    font-family:"Press Start 2P", monospace;
    font-size:clamp(19px, 3.4vw, 34px);
    color:var(--yellow); letter-spacing:.02em;
    text-shadow:3px 3px 0 #b3402a, 6px 6px 0 #14171c, 0 0 26px rgba(255,210,63,.25);
    white-space:nowrap;
  }
  .logo em{ font-style:normal; color:var(--orange); }
  .tag{
    margin-top:9px; font-weight:600; font-size:11px; letter-spacing:.34em;
    color:var(--cyan); text-transform:uppercase; opacity:.9;
  }
  .marquee-side{
    display:flex; flex-direction:column; align-items:flex-end; gap:8px;
    font-size:10px; letter-spacing:.22em; color:var(--dim); text-transform:uppercase; font-weight:600;
  }
  .led{
    font-family:"Press Start 2P", monospace; font-size:10px;
    background:#0a0c0f; border:1px solid #23293388; border-radius:6px;
    padding:8px 12px; color:#ff6a4d; text-shadow:0 0 8px rgba(255,106,77,.7);
    display:flex; gap:10px; align-items:center;
  }
  .led .pwr{ width:7px; height:7px; border-radius:50%; background:#3dff8b; box-shadow:0 0 8px #3dff8b; animation:pulse 2.2s infinite; }
  @keyframes pulse{ 0%,100%{opacity:1} 50%{opacity:.35} }
  .stripes{
    height:9px; border-radius:0;
    background:repeating-linear-gradient(115deg, var(--orange) 0 16px, #171a20 16px 30px, var(--cyan) 30px 34px, #171a20 34px 48px);
    background-size:96px 100%;
    animation:stripeMove 3.2s linear infinite;
    border-left:1px solid #262c36; border-right:1px solid #262c36;
  }
  @keyframes stripeMove{ to{ background-position:96px 0; } }

  /* ---------- CRT BEZEL ---------- */
  .bezel{
    background:#0d0f13;
    border:1px solid #262c36;
    padding:clamp(10px, 2.2vw, 22px);
    box-shadow:0 34px 90px rgba(0,0,0,.6), 0 2px 0 rgba(255,255,255,.04) inset;
  }
  .crt{
    position:relative; border-radius:10px; overflow:hidden;
    background:#000; aspect-ratio:16/9;
    box-shadow:0 0 0 3px #05070a, 0 0 0 4px #23293388, 0 0 60px rgba(63,224,208,.06) inset;
  }
  canvas#game{ display:block; width:100%; height:100%; image-rendering:auto; }
  .crt::before{ /* scanlines */
    content:""; position:absolute; inset:0; z-index:5; pointer-events:none;
    background:repeating-linear-gradient(0deg, rgba(0,0,0,.17) 0 1px, transparent 1px 3px);
    animation:flicker 4s steps(2) infinite;
  }
  @keyframes flicker{ 0%,100%{opacity:.9} 50%{opacity:1} }
  .crt::after{ /* vignette + tube curvature */
    content:""; position:absolute; inset:0; z-index:6; pointer-events:none;
    background:radial-gradient(115% 90% at 50% 45%, transparent 55%, rgba(0,0,0,.45) 100%);
    box-shadow:inset 0 0 90px rgba(0,0,0,.55);
    border-radius:10px;
  }
  .rollbar{
    position:absolute; left:0; right:0; top:-140px; height:130px; z-index:4; pointer-events:none;
    background:linear-gradient(180deg, transparent, rgba(255,255,255,.028), transparent);
    animation:roll 8s linear infinite;
  }
  @keyframes roll{ to{ transform:translateY(820px); } }

  /* touch pads */
  .touch{ display:none; position:absolute; inset:0; z-index:7; pointer-events:none; }
  .pad{
    position:absolute; pointer-events:auto; touch-action:none;
    width:64px; height:64px; border-radius:50%;
    background:rgba(10,14,18,.55); border:1px solid rgba(63,224,208,.4);
    color:var(--ink); font-family:"Press Start 2P",monospace; font-size:16px;
    display:flex; align-items:center; justify-content:center;
  }
  .pad:active{ background:rgba(63,224,208,.35); }
  #padL{ left:14px; bottom:16px; } #padR{ left:92px; bottom:16px; }
  #padB{ right:92px; bottom:16px; font-size:9px; } #padN{ right:14px; bottom:16px; font-size:9px; color:var(--yellow); border-color:rgba(255,210,63,.5); }
  @media (pointer:coarse){ .touch{ display:block; } }

  /* ---------- CONTROL DECK ---------- */
  .deck{
    background:linear-gradient(180deg,#171b22,#101318);
    border:1px solid #262c36; border-top:1px solid #1f242d;
    border-radius:0 0 16px 16px;
    padding:16px 22px 18px;
    display:flex; flex-wrap:wrap; gap:16px 26px; align-items:center; justify-content:space-between;
  }
  .coin-door{ display:flex; align-items:center; gap:14px; }
  .slot{
    width:44px; height:52px; border-radius:6px; background:#0a0c0f;
    border:1px solid #2b313c; position:relative; box-shadow:0 2px 0 rgba(255,255,255,.05) inset;
  }
  .slot::after{
    content:""; position:absolute; left:50%; top:12px; width:4px; height:22px; margin-left:-2px;
    background:#000; border:1px solid #3a4150; border-radius:2px; box-shadow:0 0 8px rgba(255,210,63,.25);
  }
  .insert-coin{
    font-family:"Press Start 2P",monospace; font-size:10px; color:var(--yellow);
    animation:blink 1.1s steps(2) infinite;
  }
  @keyframes blink{ 50%{opacity:0} }
  .credits{ font-size:10px; color:var(--dim); letter-spacing:.2em; margin-top:6px; text-transform:uppercase; font-weight:600; }
  .chips{ display:flex; flex-wrap:wrap; gap:9px; }
  .chip{
    font-family:"Chakra Petch",sans-serif; font-weight:700; font-size:11px;
    letter-spacing:.14em; text-transform:uppercase; color:var(--ink);
    background:#20242c; border:1px solid #303743; border-bottom-width:3px; border-radius:7px;
    padding:9px 14px; cursor:pointer;
    transition:transform .12s ease, border-color .12s ease, box-shadow .12s ease, background .12s ease;
  }
  .chip:hover{ transform:translateY(-2px); border-color:var(--cyan); box-shadow:0 6px 18px rgba(63,224,208,.14); }
  .chip:active{ transform:translateY(0); }
  .chip.on{ background:#123c3a; border-color:var(--cyan); color:#bffff4; }
  .chip b{ color:var(--cyan); font-weight:700; }
  .legend{
    display:flex; flex-wrap:wrap; gap:8px 16px; justify-content:center;
    padding:14px 10px 0; font-size:11px; color:var(--dim); letter-spacing:.08em; font-weight:600;
  }
  kbd{
    font-family:"Press Start 2P",monospace; font-size:8px; color:var(--ink);
    background:#20242c; border:1px solid #39414e; border-bottom:3px solid #39414e;
    border-radius:5px; padding:4px 7px; margin-right:2px;
  }
  footer{
    position:relative; z-index:1; margin-top:20px; text-align:center;
    font-size:11px; color:#5d6572; letter-spacing:.1em; max-width:760px; line-height:1.7;
  }
  footer b{ color:var(--dim); }
  @media (prefers-reduced-motion:reduce){
    .marquee::after,.stripes,.rollbar,.crt::before,.insert-coin,.led .pwr{ animation:none; }
  }
</style>
</head>
<body>

<main class="cabinet">
  <header class="marquee">
    <div class="logo-block">
      <div class="logo">RAD&#xA0;<em>HORIZON</em></div>
      <div class="tag">Turbo Grand Prix &#xB7; 6 Stages &#xB7; Est. 1986</div>
    </div>
    <div class="marquee-side">
      <div class="led"><span class="pwr"></span> HI-SCORE <span id="hiLed">0000000</span></div>
      <div class="led"><span class="pwr"></span> CREDITS <span id="credLed">0</span></div>
    </div>
  </header>
  <div class="stripes"></div>

  <div class="bezel">
    <div class="crt" id="crt">
      <canvas id="game" width="960" height="540"></canvas>
      <div class="rollbar"></div>
      <div class="touch">
        <div class="pad" id="padL">&#x25C0;</div>
        <div class="pad" id="padR">&#x25B6;</div>
        <div class="pad" id="padB">BRAKE</div>
        <div class="pad" id="padN">NITRO</div>
      </div>
    </div>
  </div>

  <div class="deck">
    <div class="coin-door">
      <div class="slot"></div>
      <div>
        <div class="insert-coin">INSERT COIN</div>
        <div class="credits">PRESS <kbd>ENTER</kbd> TO RACE</div>
      </div>
    </div>
    <div class="chips">
      <button class="chip on" id="btnSound">SOUND <b>ON</b></button>
      <button class="chip" id="btnRadio">RADIO &#xB7; <b id="radioName">SUNSET BLAZER</b></button>
      <button class="chip" id="btnStereo">3-D GLASSES <b>[G]</b></button>
    </div>
  </div>

  <div class="legend">
    <span><kbd>&#x25C0;</kbd><kbd>&#x25B6;</kbd> STEER</span>
    <span><kbd>&#x25B2;</kbd> THROTTLE</span>
    <span><kbd>&#x25BC;</kbd> BRAKE</span>
    <span><kbd>SHIFT</kbd> NITRO</span>
    <span><kbd>1</kbd><kbd>2</kbd><kbd>3</kbd><kbd>4</kbd> RADIO</span>
    <span><kbd>M</kbd> MUTE</span>
    <span><kbd>G</kbd> 3-D</span>
    <span><kbd>P</kbd> PAUSE</span>
  </div>
</main>

<footer>
  A love letter to <b>OUT RUN</b> (Sega &#xB7; 1986) and <b>RAD RACER</b> (Square &#xB7; 1987) &#x2014; every pixel drawn in code,
  every note synthesized live. Thread the needle between traffic to charge <b>NITRO</b>, hit every checkpoint,
  and pop on the red-cyan glasses for the full 1987 treatment.
  <br>Six stages end in the <b>NEON TUNNEL</b>: four blacked-out bores where the light rings strobe in time with
  the radio and the music muffles the moment you&apos;re inside &#x2014; and where the corners flatten out so you can
  hold it flat through the dark.
</footer>

<script>
'use strict';
/* =====================================================================
   RAD HORIZON — pseudo-3D arcade racer, everything procedural.
   ===================================================================== */

/* ---------------- tiny helpers ---------------- */
const $ = id => document.getElementById(id);
const clamp = (v,a,b)=> v<a?a : v>b?b : v;
const lerp  = (a,b,t)=> a+(b-a)*t;
const easeIn    = (a,b,t)=> a+(b-a)*t*t;
const easeInOut = (a,b,t)=> a+(b-a)*(-Math.cos(t*Math.PI)/2+0.5);
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
const _rgbCache={};
function hexRGB(h){ let c=_rgbCache[h]; if(!c){ c=[parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)]; _rgbCache[h]=c; } return c; }
function rgba(h,a){ const c=hexRGB(h); return `rgba(${c[0]},${c[1]},${c[2]},${a})`; }
function mixHex(a,b,t){ const A=hexRGB(a),B=hexRGB(b);
  const r=Math.round(lerp(A[0],B[0],t)), g=Math.round(lerp(A[1],B[1],t)), bl=Math.round(lerp(A[2],B[2],t));
  return `rgb(${r},${g},${bl})`; }
function makeCanvas(w,h,fn){ const c=document.createElement('canvas'); c.width=w; c.height=h; fn(c.getContext('2d'),w,h); return c; }
function shade(hex,amt){ const c=hexRGB(hex); const f=v=>clamp(Math.round(v+amt),0,255); return `rgb(${f(c[0])},${f(c[1])},${f(c[2])})`; }

/* ---------------- constants ---------------- */
const SCENE_W=960, SCENE_H=540;
const SEG_LEN=200, ROAD_W=2200, LANES=3, RUMBLE=3, DRAW_DIST=240;
const FOV=100, CAM_DEPTH=1/Math.tan((FOV/2)*Math.PI/180), CAM_H=1050;
const PLAYER_Z=CAM_H*CAM_DEPTH;
const MAX_SPEED=SEG_LEN*60;               // 12000 world units / s
const ACCEL=MAX_SPEED/4.6, BRAKE=-MAX_SPEED*1.15, COAST=-MAX_SPEED/7, OFFROAD_DECEL=-MAX_SPEED*1.05;
const OFFROAD_LIMIT=MAX_SPEED/4, CENTRIFUGAL=0.29, BOOST_MAX=MAX_SPEED*1.38;
const EYE=46;                             // stereo camera separation (world units)

/* ---------------- canvases ---------------- */
const display=$('game'), dctx=display.getContext('2d');
const sceneCv=document.createElement('canvas'); sceneCv.width=SCENE_W; sceneCv.height=SCENE_H;
const sctx=sceneCv.getContext('2d');
const sceneR=document.createElement('canvas'); sceneR.width=SCENE_W; sceneR.height=SCENE_H;
const sctxR=sceneR.getContext('2d');
let tL=null,tR=null;

/* ---------------- stage palettes ---------------- */
const PALETTES=[
 { name:'PALM COAST', type:'coast',
   skyTop:'#0d5c86', skyMid:'#4fb0bf', skyLow:'#ffd9a0', sun:'#fff0c0', sunCore:'#ffe9a8', glow:'#ffd27a',
   mFar:'#2e7286', mNear:'#1e5262', grassA:'#1f9158', grassB:'#187f4c',
   roadA:'#5d626e', roadB:'#585d69', rumbleA:'#e9e9e9', rumbleB:'#d94f3d', lane:'#f4f4f4', fog:'#ffe3b0',
   sea:'#0f6f7e', seaA:1, stars:0 },
 { name:'DUSTY CANYON', type:'canyon',
   skyTop:'#3a1c2c', skyMid:'#a34a30', skyLow:'#ffb26b', sun:'#ffd9a0', sunCore:'#ffcf8f', glow:'#ff9d5c',
   mFar:'#6b3527', mNear:'#4e261d', grassA:'#c98d4f', grassB:'#bb8043',
   roadA:'#63544b', roadB:'#5d4f46', rumbleA:'#ead9b8', rumbleB:'#c0452f', lane:'#efe4cc', fog:'#ffb27a',
   sea:null, stars:0 },
 { name:'NEON CITY', type:'city',
   skyTop:'#040814', skyMid:'#0b1c33', skyLow:'#28495f', sun:'#dfe9f2', sunCore:'#eef6ff', glow:'#7fd4ff',
   mFar:'#0a1626', mNear:'#0d2036', grassA:'#14261f', grassB:'#101f19',
   roadA:'#3c4350', roadB:'#373e4b', rumbleA:'#dfe8ff', rumbleB:'#2a6cff', lane:'#9fe8ff', fog:'#274a63',
   sea:null, stars:1 },
 { name:'ALPINE PASS', type:'alpine',
   skyTop:'#2c7cb6', skyMid:'#7fc4e6', skyLow:'#eaf6ff', sun:'#fff8e0', sunCore:'#ffffff', glow:'#fff3c4',
   mFar:'#5b7d9e', mNear:'#33506b', grassA:'#2e9e52', grassB:'#27914a',
   roadA:'#676d77', roadB:'#61676f', rumbleA:'#e8e8e8', rumbleB:'#d14434', lane:'#f2f2f2', fog:'#dcedf7',
   sea:null, stars:0 },
 { name:'GOLDEN GATE', type:'gate',
   skyTop:'#1c3f66', skyMid:'#c96a3f', skyLow:'#ffd27a', sun:'#fff2c8', sunCore:'#ffe6a0', glow:'#ffb45e',
   mFar:'#5e4a52', mNear:'#3c3040', grassA:'#3f8f4a', grassB:'#37813f',
   roadA:'#5f5a58', roadB:'#5a5553', rumbleA:'#e9e9e9', rumbleB:'#d94f3d', lane:'#f4f4f4', fog:'#ffcf8e',
   sea:'#1a5f6e', seaA:1, stars:0 },
 { name:'NEON TUNNEL', type:'tunnel',
   skyTop:'#04030e', skyMid:'#170a30', skyLow:'#3b1350', sun:'#ff77e1', sunCore:'#ffd3f6', glow:'#ff4fd8',
   mFar:'#150a2b', mNear:'#0c051d', grassA:'#1b1030', grassB:'#150c28',
   roadA:'#241b33', roadB:'#201730', rumbleA:'#eafcff', rumbleB:'#ff2fd0', lane:'#8ef8ff', fog:'#2a0f45',
   sea:null, seaA:0, stars:1 }
];

/* ---------------- game state ---------------- */
let segments=[], trackLength=0, stageStarts=[];
let cars=[];
const LEVEL_TIME=[52,50,49,48,47,53], CHECKPOINT_BONUS=13;

const G={
  state:'boot',           // boot | attract | count | race | over | victory | pause
  position:0, speed:0, playerX:0,
  steerVis:0,
  timeLeft:64, stageIdx:0,
  score:0, hiScore:0, nearMisses:0, topSpeedKmh:0, raceElapsed:0,
  gauge:0.6, boosting:false, boostSnd:false,
  crashT:0, shakeT:0, flashT:0,
  countT:0, lastCount:-1,
  palA:PALETTES[0], palB:PALETTES[0], palT:1,
  skyOffset:0, cloudOff:0, camYWorld:0, horizonY:SCENE_H*0.5, wasTun:false,
  totalTime:0, totalNM:0, cpDone:[false,false], results:null, raceStart:0,
  now:0, lastTick:0,
  credits:0, muted:false, radioTrack:1, stereo:false, pausedFrom:null,
  popups:[], parts:[]
};
try{ G.hiScore=parseInt(localStorage.getItem('radhorizon_hi')||'0',10)||0; }catch(e){}

const input={left:false,right:false,throttle:false,brake:false,boost:false,touch:false};

/* =====================================================================
   TRACK BUILDING
   ===================================================================== */
function lastY(){ return segments.length? segments[segments.length-1].p2.world.y : 0; }
function addSeg(curve,y){
  const n=segments.length;
  segments.push({ index:n, curve, fog:1, clip:0, shade:Math.floor(n/RUMBLE)%2, checker:(n<6),
    p1:{world:{y:lastY(),z:n*SEG_LEN},camera:{},screen:{scale:0,x:0,y:0,w:0}},
    p2:{world:{y,z:(n+1)*SEG_LEN},camera:{},screen:{scale:0,x:0,y:0,w:0}},
    sprites:[], cars:[] });
}
function addRoad(enter,hold,leave,curve,hill){
  const startY=lastY(), endY=startY+hill*SEG_LEN, total=enter+hold+leave;
  for(let i=0;i<enter;i++) addSeg(easeIn(0,curve,i/enter),          easeInOut(startY,endY,i/total));
  for(let i=0;i<hold;i++)  addSeg(curve,                             easeInOut(startY,endY,(enter+i)/total));
  for(let i=0;i<leave;i++) addSeg(easeInOut(curve,0,i/leave),        easeInOut(startY,endY,(enter+hold+i)/total));
}
/* ---- course generator: one full circuit per level ---- */
let finishSeg=0, checkpointSegs=[], tunnelZones=[];
function buildLevel(level){
  segments=[]; checkpointSegs=[]; finishSeg=0;
  const rng=mulberry32(9001+level*131);
  const diff=Math.min(1.5,0.85+level*0.13);      // curves bite harder each stage
  addRoad(22,58,22,0,0);                          // launch straight
  const course=2380+level*180;
  while(segments.length<course){
    const t=rng(), sgn=rng()<0.5?-1:1;
    if(t<0.26){                                   // long sweeping bend
      addRoad(28+rng()*18, 55+rng()*45, 28+rng()*18,
              clamp(sgn*(1.5+rng()*2.1)*diff,-4.6,4.6), (rng()-0.5)*44);
    } else if(t<0.5){                             // s-bend
      const c=clamp(sgn*(2.1+rng()*1.5)*diff,-4.7,4.7);
      addRoad(20,28,20, c, (rng()-0.5)*18);
      addRoad(20,28,20, -c*0.92, (rng()-0.5)*18);
    } else if(t<0.68){                            // climb / plunge
      addRoad(24,38,24, clamp(sgn*rng()*1.6*diff,-3,3), sgn*(18+rng()*30));
    } else if(t<0.84){                            // hairpin
      addRoad(14,20,14, clamp(sgn*(3.1+rng()*1.5)*diff,-5,5), (rng()-0.5)*12);
    } else {                                      // flat-out straight + kink
      addRoad(8,50+rng()*32,8, 0, 0);
      addRoad(8,12,8, sgn*1.15, 0);
    }
  }
  checkpointSegs=[Math.floor(segments.length*0.33), Math.floor(segments.length*0.66)];
  addRoad(24,64,24, 0, -lastY()/SEG_LEN);         // level out to the line
  addRoad(8,30,8, 0, 0);
  finishSeg=segments.length;                      // the finish line
  addRoad(8,320,8, 0, 0);                         // cool-down run-off
  for(let i=0;i<6;i++) segments[finishSeg+i].checker=(i%2===0);   // chequered band
  /* neon tunnels — the signature of the final stage */
  tunnelZones=[];
  if(level>=5){
    const zones=4, span=Math.floor(finishSeg/(zones+1));
    for(let i=1;i<=zones;i++){
      tunnelZones.push({start:span*i-Math.floor(span*0.21),
                        end:Math.min(finishSeg-56, span*i+Math.floor(span*0.21))});
    }
    segments.forEach((s,i)=>{ const z=tunnelZones.find(q=>i>=q.start&&i<=q.end);
      if(z){ s.tunnel=true; s.tz=z; s.curve*=0.4; } });   // tunnels flow: flat-out blasts
  }
  trackLength=segments.length*SEG_LEN;
  stageStarts=checkpointSegs.slice();
}
const findSegment=z=>segments[clamp(Math.floor(z/SEG_LEN),0,segments.length-1)];

/* =====================================================================
   PROCEDURAL SPRITES
   ===================================================================== */
const SPR={};
function roundRect(g,x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }

function buildSprites(){
  /* palm */
  SPR.palm=makeCanvas(240,330,(g)=>{
    g.lineCap='round';
    g.strokeStyle='#6e4222'; g.lineWidth=17;
    g.beginPath(); g.moveTo(132,328); g.quadraticCurveTo(110,210,122,118); g.stroke();
    g.strokeStyle='#59341a'; g.lineWidth=6;
    g.beginPath(); g.moveTo(136,326); g.quadraticCurveTo(118,220,126,130); g.stroke();
    const top={x:122,y:112};
    const fronds=[[-1.05,-0.25],[-0.75,-0.6],[-0.3,-0.82],[0.25,-0.85],[0.72,-0.6],[1.02,-0.22],[0.9,0.12],[-0.95,0.12]];
    fronds.forEach((f,i)=>{
      g.fillStyle= i%2? '#2f9e44':'#1e7a33';
      g.beginPath(); g.moveTo(top.x,top.y);
      g.quadraticCurveTo(top.x+f[0]*70, top.y+f[1]*70-18, top.x+f[0]*118, top.y+f[1]*118+26);
      g.quadraticCurveTo(top.x+f[0]*66, top.y+f[1]*66+16, top.x,top.y+4);
      g.fill();
    });
    g.fillStyle='#59341a'; g.beginPath(); g.arc(114,124,8,0,7); g.arc(132,126,8,0,7); g.fill();
  });
  /* pine */
  SPR.pine=makeCanvas(190,300,(g)=>{
    g.fillStyle='#6e4222'; g.fillRect(86,248,18,50);
    const layers=[[95,36,120],[95,96,150],[95,166,176]];
    layers.forEach((L,i)=>{ g.fillStyle= i%2?'#14522b':'#1c6b3a';
      g.beginPath(); g.moveTo(L[0],L[1]-64); g.lineTo(L[0]-L[2]/2,L[1]+78); g.lineTo(L[0]+L[2]/2,L[1]+78); g.fill();
      g.fillStyle='rgba(240,250,255,.85)';
      g.beginPath(); g.moveTo(L[0],L[1]-64); g.lineTo(L[0]-L[2]*0.16,L[1]-30); g.lineTo(L[0]+L[2]*0.16,L[1]-30); g.fill();
    });
  });
  /* cactus */
  SPR.cactus=makeCanvas(130,230,(g)=>{
    g.fillStyle='#3f9e4d'; roundRect(g,52,46,28,180,14); g.fill();
    roundRect(g,14,88,22,66,11); g.fill(); roundRect(g,14,88,52,20,10); g.fill();
    roundRect(g,96,64,22,58,11); g.fill(); roundRect(g,66,102,52,20,10); g.fill();
    g.fillStyle='#2c7a39';
    [62,70].forEach(x=>{ g.fillRect(x,56,3,160); });
    g.fillStyle='#ff6b81'; g.beginPath(); g.arc(66,44,7,0,7); g.fill();
  });
  /* rock */
  SPR.rock=makeCanvas(170,110,(g)=>{
    g.fillStyle='#8a8f98'; g.beginPath(); g.moveTo(10,100); g.lineTo(34,40); g.lineTo(78,16); g.lineTo(132,34); g.lineTo(160,100); g.fill();
    g.fillStyle='#6d727c'; g.beginPath(); g.moveTo(78,16); g.lineTo(132,34); g.lineTo(160,100); g.lineTo(96,100); g.fill();
    g.fillStyle='#a7adb6'; g.beginPath(); g.moveTo(34,40); g.lineTo(78,16); g.lineTo(70,44); g.lineTo(44,58); g.fill();
  });
  /* roadside sign */
  const sign=(label,bg,fg,border)=>makeCanvas(230,190,(g)=>{
    g.fillStyle='#5a5f68'; g.fillRect(52,120,12,70); g.fillRect(166,120,12,70);
    g.fillStyle=bg; roundRect(g,10,18,210,112,8); g.fill();
    g.lineWidth=7; g.strokeStyle=border; roundRect(g,10,18,210,112,8); g.stroke();
    g.fillStyle=fg; g.font='22px "Press Start 2P", monospace'; g.textAlign='center'; g.textBaseline='middle';
    g.fillText(label,115,76);
  });
  SPR.signTurbo=sign('TURBO','#f3ead9','#c0392b','#c0392b');
  SPR.signRad  =sign('RAD!','#f3ead9','#0e7a6e','#0e7a6e');
  SPR.signRoute=sign('RT-86','#f8f4e6','#1f2937','#e0a62e');
  SPR.signAlp  =sign('ALPINE','#eaf6ff','#20455e','#2c7cb6');
  SPR.signFinal=sign('FINAL','#f3ead9','#b3402a','#b3402a');
  /* neon signs (glow baked) */
  const neon=(label,col)=>makeCanvas(250,200,(g)=>{
    g.fillStyle='#10141c'; g.fillRect(60,130,10,70); g.fillRect(180,130,10,70);
    g.fillStyle='#0b0e15'; roundRect(g,14,16,222,120,10); g.fill();
    g.save(); g.shadowColor=col; g.shadowBlur=20;
    g.strokeStyle=col; g.lineWidth=4; roundRect(g,22,24,206,104,8); g.stroke();
    g.fillStyle=col; g.font='24px "Press Start 2P", monospace'; g.textAlign='center'; g.textBaseline='middle';
    g.fillText(label,125,68); g.restore();
    g.fillStyle=rgba(col,0.28); g.fillRect(30,96,190,5);
  });
  SPR.neonCyan=neon('MOTEL','#3fe0d0');
  SPR.neonPink=neon('SUSHI','#ff4fa0');
  SPR.neonYel =neon('ARCADE','#ffd23f');
  /* street lamp with glow */
  SPR.lamp=makeCanvas(120,330,(g)=>{
    g.fillStyle='#2c323c'; g.fillRect(54,60,10,268);
    g.fillRect(54,60,44,9);
    const glow=g.createRadialGradient(92,74,2,92,74,52);
    glow.addColorStop(0,'rgba(190,235,255,.95)'); glow.addColorStop(1,'rgba(190,235,255,0)');
    g.fillStyle=glow; g.beginPath(); g.arc(92,74,52,0,7); g.fill();
    g.fillStyle='#dff3ff'; roundRect(g,82,64,20,14,5); g.fill();
  });
  /* city tower */
  SPR.tower=makeCanvas(200,420,(g)=>{
    g.fillStyle='#0d1a2c'; g.fillRect(20,40,160,380);
    g.fillStyle='#122440'; g.fillRect(20,40,160,14);
    g.fillStyle='#091322'; g.fillRect(88,6,8,40);
    g.fillStyle='#ff4b4b'; g.beginPath(); g.arc(92,6,5,0,7); g.fill();
    const rng=mulberry32(7);
    for(let y=64;y<400;y+=26) for(let x=32;x<170;x+=22){
      if(rng()<0.42){ g.fillStyle= rng()<0.5? 'rgba(255,210,120,.9)':'rgba(120,225,255,.75)'; g.fillRect(x,y,10,12); }
      else { g.fillStyle='rgba(40,60,90,.5)'; g.fillRect(x,y,10,12); }
    }
  });
  /* checkpoint gantry */
  const gantry=(label)=>makeCanvas(760,250,(g)=>{
    g.fillStyle='#454c58'; g.fillRect(26,60,26,190); g.fillRect(708,60,26,190);
    g.fillStyle='#2e333d'; g.fillRect(26,60,8,190); g.fillRect(708,60,8,190);
    g.fillStyle='#1c212a'; g.fillRect(0,18,760,64);
    g.fillStyle='#f3ead9'; g.font='30px "Press Start 2P", monospace'; g.textAlign='center'; g.textBaseline='middle';
    g.fillText(label,380,50);
    for(let x=0;x<760;x+=24){ g.fillStyle=(x/24)%2? '#111':'#eee'; g.fillRect(x,82,24,16); }
  });
  SPR.gantryCheck=gantry('CHECKPOINT');
  SPR.gantryStart=gantry('RAD HORIZON');
  /* the finish arch */
  SPR.gantryFinish=makeCanvas(840,300,(g)=>{
    g.fillStyle='#454c58'; g.fillRect(26,66,32,234); g.fillRect(782,66,32,234);
    g.fillStyle='#2e333d'; g.fillRect(26,66,10,234); g.fillRect(782,66,10,234);
    g.fillStyle='#1c212a'; g.fillRect(0,12,840,84);
    g.fillStyle='#f3ead9'; g.font='40px "Press Start 2P", monospace'; g.textAlign='center'; g.textBaseline='middle';
    g.fillText('FINISH',420,54);
    for(let x=0;x<840;x+=21){ g.fillStyle=(x/21)%2? '#f3ead9':'#15181e'; g.fillRect(x,96,21,26); }
    for(let i=0;i<2;i++){ const fx=150+i*560;
      g.strokeStyle='#8b93a1'; g.lineWidth=4; g.beginPath(); g.moveTo(fx,122); g.lineTo(fx,212); g.stroke();
      for(let r=0;r<5;r++) for(let c2=0;c2<4;c2++){ g.fillStyle=(r+c2)%2? '#15181e':'#f3ead9'; g.fillRect(fx+c2*10,122+r*10,10,10); }
    }
  });
  /* neon tunnel portal — centre is transparent so you see straight through it */
  SPR.tunnelGate=makeCanvas(780,430,(g)=>{
    g.fillStyle='#150c26'; g.fillRect(0,0,54,430); g.fillRect(726,0,54,430);
    g.fillStyle='#0d0718'; g.fillRect(46,0,8,430); g.fillRect(726,0,8,430);
    g.fillStyle='#150c26'; g.fillRect(0,0,780,132);
    g.fillStyle='#0d0718'; g.fillRect(0,124,780,8);
    g.save(); g.shadowColor='#8ef8ff'; g.shadowBlur=18;
    g.strokeStyle='#8ef8ff'; g.lineWidth=5;
    g.beginPath(); g.moveTo(54,126); g.lineTo(726,126); g.stroke();
    g.strokeStyle='#ff5ce1'; g.lineWidth=4;
    g.beginPath(); g.moveTo(56,0); g.lineTo(56,424); g.moveTo(724,0); g.lineTo(724,424); g.stroke();
    g.restore();
    g.fillStyle='#eafcff'; g.font='24px "Press Start 2P", monospace'; g.textAlign='center'; g.textBaseline='middle';
    g.fillText('NEON TUNNEL',390,64);
    g.fillStyle='#ff5ce1';
    for(let y=152;y<404;y+=42){
      g.beginPath(); g.moveTo(14,y); g.lineTo(40,y+12); g.lineTo(14,y+24); g.fill();
      g.beginPath(); g.moveTo(766,y); g.lineTo(740,y+12); g.lineTo(766,y+24); g.fill();
    }
  });
  /* traffic cars (rear view) */
  const carColors=['#3a86ff','#ffbe0b','#e9ecef','#8d99ae','#2a9d8f','#e76f51'];
  SPR.traffic=carColors.map(col=>makeCanvas(160,112,(g)=>{
    g.fillStyle='#15171b'; roundRect(g,6,84,30,24,6); g.fill(); roundRect(g,124,84,30,24,6); g.fill();
    const body=g.createLinearGradient(0,20,0,100);
    body.addColorStop(0,shade(col,34)); body.addColorStop(0.5,col); body.addColorStop(1,shade(col,-46));
    g.fillStyle=body; roundRect(g,10,34,140,62,12); g.fill();
    const glass=g.createLinearGradient(0,6,0,52);
    glass.addColorStop(0,'#2c3a4c'); glass.addColorStop(1,'#101820');
    g.fillStyle=glass; roundRect(g,26,8,108,44,10); g.fill();
    g.fillStyle=rgba('#bfe8ff',0.25); g.fillRect(34,14,40,6);
    g.fillStyle='#111'; roundRect(g,14,88,132,12,5); g.fill();
    g.fillStyle='#c9cfd6'; g.fillRect(66,90,28,8);
    g.save(); g.shadowColor='#ff2d2d'; g.shadowBlur=10;
    g.fillStyle='#ff4040'; roundRect(g,18,62,30,9,3); g.fill(); roundRect(g,112,62,30,9,3); g.fill(); g.restore();
  }));
  /* player car — the Vector GT */
  SPR.player=makeCanvas(260,150,(g)=>{
    g.fillStyle='#141619'; roundRect(g,8,118,44,26,7); g.fill(); roundRect(g,208,118,44,26,7); g.fill();
    const body=g.createLinearGradient(0,30,0,140);
    body.addColorStop(0,'#ff5a48'); body.addColorStop(0.42,'#e63226'); body.addColorStop(1,'#8f1d15');
    g.fillStyle=body;
    g.beginPath(); g.moveTo(14,132); g.lineTo(8,86); g.quadraticCurveTo(10,60,40,54);
    g.lineTo(220,54); g.quadraticCurveTo(250,60,252,86); g.lineTo(246,132);
    g.quadraticCurveTo(130,142,14,132); g.fill();
    g.fillStyle='#a52019'; g.fillRect(14,96,232,7);
    const glass=g.createLinearGradient(0,10,0,58);
    glass.addColorStop(0,'#28384a'); glass.addColorStop(1,'#0d141c');
    g.fillStyle=glass;
    g.beginPath(); g.moveTo(52,52); g.lineTo(66,14); g.quadraticCurveTo(130,4,194,14); g.lineTo(208,52); g.closePath(); g.fill();
    g.fillStyle=rgba('#7fd4ff',0.35); g.beginPath(); g.moveTo(70,18); g.lineTo(120,10); g.lineTo(104,20); g.lineTo(74,26); g.closePath(); g.fill();
    g.fillStyle='#5e120c'; g.fillRect(46,50,168,5);
    /* side strakes */
    g.fillStyle='#7c1710';
    for(let i=0;i<3;i++){ g.fillRect(16,104+i*8,42,4); g.fillRect(202,104+i*8,42,4); }
    /* tail light bar */
    g.fillStyle='#1a0c0a'; roundRect(g,26,72,208,16,5); g.fill();
    g.save(); g.shadowColor='#ff2222'; g.shadowBlur=12; g.fillStyle='#ff3b30';
    for(let x=32;x<226;x+=24) roundRect(g,x,75,16,10,2), g.fill();
    g.restore();
    g.fillStyle='#0d0d0f'; roundRect(g,86,116,88,16,4); g.fill();
    g.fillStyle='#c9cfd6'; g.fillRect(112,119,36,10);
    g.fillStyle='#1c1c1f'; g.beginPath(); g.arc(98,126,5,0,7); g.arc(162,126,5,0,7); g.fill();
    g.fillStyle=rgba('#7fd4ff',0.8); g.fillRect(46,57,20,3); g.fillRect(194,57,20,3);
  });
}

/* sprite placement */
const SPRITE_DEFS={
  palm:{img:'palm',w:1000}, pine:{img:'pine',w:820}, cactus:{img:'cactus',w:480}, rock:{img:'rock',w:720},
  signTurbo:{img:'signTurbo',w:880}, signRad:{img:'signRad',w:880}, signRoute:{img:'signRoute',w:880},
  signAlp:{img:'signAlp',w:880}, signFinal:{img:'signFinal',w:880},
  neonCyan:{img:'neonCyan',w:980}, neonPink:{img:'neonPink',w:980}, neonYel:{img:'neonYel',w:980},
  lamp:{img:'lamp',w:300}, tower:{img:'tower',w:1500},
  gantryCheck:{img:'gantryCheck',w:ROAD_W*3.2}, gantryStart:{img:'gantryStart',w:ROAD_W*3.2},
  gantryFinish:{img:'gantryFinish',w:ROAD_W*3.5},
  tunnelGate:{img:'tunnelGate',w:ROAD_W*2.68}
};
const STAGE_POOLS=[
  [['palm',5],['palm',5],['signTurbo',1],['signRad',1],['rock',1]],
  [['cactus',4],['rock',3],['signRoute',1],['signTurbo',1]],
  [['neonCyan',2],['neonPink',2],['neonYel',1],['lamp',3],['tower',2]],
  [['pine',5],['pine',3],['rock',2],['signAlp',1]],
  [['palm',4],['signFinal',1],['signTurbo',1],['rock',1],['pine',2]],
  [['neonPink',3],['neonCyan',3],['neonYel',1],['lamp',4],['tower',3]]
];
function placeSprites(level){
  const rng=mulberry32(4400+level*97);
  const pool=STAGE_POOLS[level];
  const pick=p=>{ let tot=0; p.forEach(q=>tot+=q[1]); let r=rng()*tot;
    for(const q of p){ r-=q[1]; if(r<=0) return q[0]; } return p[0][0]; };
  const blocked=n=> Math.abs(n-finishSeg)<12 || checkpointSegs.some(cs=>Math.abs(n-cs)<9)
                   || tunnelZones.some(z=>n>z.start-8 && n<z.end+8);
  for(let n=16;n<segments.length-4;n+= 2+Math.floor(rng()*5)){
    if(blocked(n)) continue;
    const side=rng()<0.5?-1:1;
    const off=side*(1.2+rng()*2.4);
    const key=pick(pool), def=SPRITE_DEFS[key];
    segments[n].sprites.push({off, w:def.w, img:SPR[def.img], kind:key});
    if(key==='palm' && rng()<0.5) segments[n].sprites.push({off:-off, w:def.w, img:SPR[def.img], kind:key});
    if(key==='lamp'){ segments[n].sprites.push({off:-off, w:def.w, img:SPR[def.img], kind:key}); n+=3; }
  }
  segments[5].sprites.push({off:0,w:SPRITE_DEFS.gantryStart.w,img:SPR.gantryStart,kind:'gantry'});
  checkpointSegs.forEach(cs=> segments[cs].sprites.push({off:0,w:SPRITE_DEFS.gantryCheck.w,img:SPR.gantryCheck,kind:'gantry'}));
  segments[finishSeg+2].sprites.push({off:0,w:SPRITE_DEFS.gantryFinish.w,img:SPR.gantryFinish,kind:'gantry'});
  tunnelZones.forEach(z=> segments[z.start-4].sprites.push({off:0,w:SPRITE_DEFS.tunnelGate.w,img:SPR.tunnelGate,kind:'gantry'}));
}

/* =====================================================================
   TRAFFIC
   ===================================================================== */
function resetCars(level){
  cars.forEach(c=>{ if(c.seg){ const i=c.seg.cars.indexOf(c); if(i>=0) c.seg.cars.splice(i,1); } });
  cars=[];
  const rng=mulberry32(7777+((Date.now()/1000|0)%9000));
  const count=30+level*9;                       // thicker traffic every stage
  const lanes=[-0.76,-0.42,-0.08,0.28,0.66];
  const first=70, last=finishSeg-12;
  for(let i=0;i<count;i++){
    const z=(first+Math.floor(rng()*(last-first)))*SEG_LEN + rng()*SEG_LEN;
    const fast = level>=2 && rng()<0.2;
    const car={ z, offset:lanes[Math.floor(rng()*lanes.length)],
      speed:MAX_SPEED*(fast? 0.6+rng()*0.18 : 0.19+rng()*0.3),
      img:SPR.traffic[Math.floor(rng()*SPR.traffic.length)], nmT:-9, seg:null };
    const seg=findSegment(z); seg.cars.push(car); car.seg=seg; cars.push(car);
  }
}

/* =====================================================================
   AUDIO — engine, SFX and three synthesized radio stations
   ===================================================================== */
let AC=null, master=null, musicBus=null, sfxBus=null, delaySend=null, noiseBuf=null, tunnelFilter=null;
let engOsc=null, engOsc2=null, engFilter=null, engGain=null, roadNoise=null, roadGain=null;

function ensureAudio(){
  if(AC) { if(AC.state==='suspended') AC.resume(); return; }
  const Ctx=window.AudioContext||window.webkitAudioContext; if(!Ctx) return;
  AC=new Ctx();
  master=AC.createGain(); master.gain.value=G.muted?0:0.9; master.connect(AC.destination);
  musicBus=AC.createGain(); musicBus.gain.value=0.5;
  tunnelFilter=AC.createBiquadFilter(); tunnelFilter.type='lowpass'; tunnelFilter.frequency.value=18000;
  musicBus.connect(tunnelFilter); tunnelFilter.connect(master);
  sfxBus=AC.createGain(); sfxBus.gain.value=0.8; sfxBus.connect(master);
  const dly=AC.createDelay(1); dly.delayTime.value=0.27;
  const fb=AC.createGain(); fb.gain.value=0.34; dly.connect(fb); fb.connect(dly);
  const wet=AC.createGain(); wet.gain.value=0.3; dly.connect(wet); wet.connect(musicBus);
  delaySend=AC.createGain(); delaySend.gain.value=1; delaySend.connect(dly);
  noiseBuf=AC.createBuffer(1,AC.sampleRate,AC.sampleRate);
  const d=noiseBuf.getChannelData(0); for(let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
  /* engine */
  engGain=AC.createGain(); engGain.gain.value=0; engGain.connect(master);
  engFilter=AC.createBiquadFilter(); engFilter.type='lowpass'; engFilter.frequency.value=500; engFilter.connect(engGain);
  engOsc=AC.createOscillator(); engOsc.type='sawtooth'; engOsc.frequency.value=55; engOsc.connect(engFilter); engOsc.start();
  engOsc2=AC.createOscillator(); engOsc2.type='square'; engOsc2.frequency.value=28; engOsc2.detune.value=-8;
  const eg2=AC.createGain(); eg2.gain.value=0.5; engOsc2.connect(eg2); eg2.connect(engFilter); engOsc2.start();
  roadGain=AC.createGain(); roadGain.gain.value=0; roadGain.connect(master);
  const bp=AC.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=950; bp.Q.value=0.6;
  roadNoise=AC.createBufferSource(); roadNoise.buffer=noiseBuf; roadNoise.loop=true; roadNoise.connect(bp); bp.connect(roadGain); roadNoise.start();
  Music.start();
}
const nf=m=>440*Math.pow(2,(m-69)/12);
function env(g,t,a,peak,dur){ g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(peak,t+a); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); }
function tone(o){ if(!AC) return;
  const {f=440,f2=null,type='square',dur=0.15,vol=0.2,at=0.005,bus=sfxBus,send=0,t=AC.currentTime}=o;
  const osc=AC.createOscillator(); osc.type=type; osc.frequency.setValueAtTime(f,t);
  if(f2) osc.frequency.exponentialRampToValueAtTime(f2,t+dur);
  const g=AC.createGain(); env(g,t,at,vol,dur); osc.connect(g); g.connect(bus);
  if(send&&delaySend){ const s=AC.createGain(); s.gain.value=send; g.connect(s); s.connect(delaySend); }
  osc.start(t); osc.stop(t+dur+0.05);
}
function noiseHit(o){ if(!AC) return;
  const {dur=0.12,vol=0.25,type='highpass',freq=6000,freq2=null,q=0.8,t=AC.currentTime,bus=sfxBus,send=0}=o;
  const src=AC.createBufferSource(); src.buffer=noiseBuf; src.loop=true;
  const f=AC.createBiquadFilter(); f.type=type; f.frequency.setValueAtTime(freq,t); f.Q.value=q;
  if(freq2) f.frequency.exponentialRampToValueAtTime(freq2,t+dur);
  const g=AC.createGain(); env(g,t,0.004,vol,dur);
  src.connect(f); f.connect(g); g.connect(bus);
  if(send&&delaySend){ const s=AC.createGain(); s.gain.value=send; g.connect(s); s.connect(delaySend); }
  src.start(t); src.stop(t+dur+0.05);
}
const SFX={
  coin(){ tone({f:988,dur:0.09,vol:0.22}); tone({f:1319,dur:0.22,vol:0.22,t:AC?AC.currentTime+0.09:0}); },
  beep(hi){ tone({f:hi?880:440,dur:hi?0.4:0.12,vol:0.25}); },
  tick(){ tone({f:1250,dur:0.04,vol:0.12}); },
  crash(){ if(!AC)return; noiseHit({type:'lowpass',freq:900,dur:0.3,vol:0.5}); tone({f:170,f2:48,type:'sine',dur:0.32,vol:0.5}); },
  whoosh(){ noiseHit({type:'bandpass',freq:320,freq2:2800,q:1.4,dur:0.24,vol:0.3}); },
  nitro(){ tone({f:150,f2:760,type:'sawtooth',dur:0.5,vol:0.2}); noiseHit({type:'highpass',freq:2400,dur:0.5,vol:0.16}); },
  checkpoint(){ [523,659,784,1047].forEach((f,i)=>tone({f,dur:0.14,vol:0.22,t:AC?AC.currentTime+i*0.09:0})); },
  fanfare(){ [392,523,659,784,1047,1319].forEach((f,i)=>tone({f,dur:0.2,vol:0.2,t:AC?AC.currentTime+i*0.12:0,type:'square'})); },
  gameover(){ [392,330,262,196].forEach((f,i)=>tone({f,dur:0.3,vol:0.2,t:AC?AC.currentTime+i*0.22:0,type:'triangle'})); }
};

/* ---- the radio ---- */
const MINOR_PENT=[0,3,5,7,10,12,15,17,19,22];
const MAJOR_PENT=[0,2,4,7,9,12,14,16,19,21];
function genMelody(scale,root,seed,density){
  const r=mulberry32(seed), out=new Array(64).fill(-1);
  let saved=null, lastI=3;
  for(let bar=0; bar<4; bar++){
    let phrase;
    if(bar%2===0){
      phrase=[]; let i=(bar===0)? 2+Math.floor(r()*3) : lastI;
      for(let k=0;k<8;k++){
        if(r()<density){ i=clamp(i+(r()<0.5?-1:1)*(r()<0.72?1:2),0,scale.length-1); phrase.push(scale[i]); }
        else phrase.push(-1);
      }
      if(bar===2){ phrase[6]=scale[1]; phrase[7]=scale[0]; }
      saved=phrase.slice(); lastI=i;
    } else phrase=saved.slice();
    if(bar===3){ phrase[6]=scale[0]; phrase[7]=-1; }
    for(let k=0;k<8;k++) out[bar*16+k*2]=phrase[k]<0?-1:(root+24+phrase[k]);
  }
  return out;
}
const TRACKS={
  1:{ name:'SUNSET BLAZER', bpm:132, prog:[45,41,48,43], bassWave:'square',
      bassPat:[0,-1,0,-1,0,-1,12,-1,0,-1,0,-1,7,-1,12,-1],
      kick:[0,4,8,12], clap:[4,12], hats:'eighths', openHat:14, pad:null,
      melody:genMelody(MINOR_PENT,45,101,0.8), leadWave:'square', leadVol:0.085 },
  2:{ name:'MAGNOLIA BREEZE', bpm:100, prog:[41,45,38,36], bassWave:'triangle',
      bassPat:[0,-1,-1,0,-1,-1,7,-1,0,-1,-1,0,-1,12,-1,-1],
      kick:[0,8], clap:[], hats:'soft', openHat:-1, pad:[0,4,7,11],
      melody:genMelody(MAJOR_PENT,41,202,0.62), leadWave:'triangle', leadVol:0.16 },
  3:{ name:'NEON NIGHTDRIVE', bpm:118, prog:[33,31,36,38], bassWave:'square',
      bassPat:[0,-1,-1,0,-1,0,-1,-1,12,-1,-1,0,-1,-1,10,-1],
      kick:[0,4,8,12], clap:[4,12], hats:'sixteenths', openHat:14, pad:[0,3,7,10],
      melody:genMelody(MINOR_PENT,33,303,0.5), leadWave:'square', leadVol:0.07 },
  4:{ name:'NEON TUNNEL', bpm:142, prog:[33,31,29,31], bassWave:'sawtooth',
      bassPat:[0,-1,0,0,-1,0,-1,0,12,-1,0,-1,7,-1,12,-1],
      kick:[0,3,4,8,11,12], clap:[4,12], hats:'sixteenths', openHat:14, pad:[0,3,7,10],
      melody:genMelody(MINOR_PENT,33,404,0.56), leadWave:'sawtooth', leadVol:0.06 }
};
const Music={
  timer:null, step:0, nextTime:0,
  start(){ if(this.timer||!AC) return; this.nextTime=AC.currentTime+0.08;
    this.timer=setInterval(()=>this.schedule(),30); },
  schedule(){ while(this.nextTime<AC.currentTime+0.15){ this.playStep(this.step,this.nextTime);
      this.nextTime+=(60/TRACKS[G.radioTrack].bpm)/4; this.step=(this.step+1)%64; } },
  playStep(step,t){
    const T=TRACKS[G.radioTrack], s16=step%16, root=T.prog[Math.floor(step/16)%T.prog.length];
    if(T.kick.includes(s16)){ this.kickT=t;
      tone({f:150,f2:46,type:'sine',dur:0.16,vol:0.5,bus:musicBus,t}); }
    if(T.clap.includes(s16)){ noiseHit({type:'bandpass',freq:1700,q:1,dur:0.13,vol:0.24,bus:musicBus,send:0.5,t});
      noiseHit({type:'bandpass',freq:1700,q:1,dur:0.1,vol:0.16,bus:musicBus,t:t+0.014}); }
    if(T.hats==='eighths' && s16%2===0) noiseHit({type:'highpass',freq:8200,dur:s16===T.openHat?0.1:0.035,vol:s16%4?0.07:0.12,bus:musicBus,t});
    if(T.hats==='soft' && [2,6,10,14].includes(s16)) noiseHit({type:'highpass',freq:9000,dur:0.05,vol:0.05,bus:musicBus,t});
    if(T.hats==='sixteenths') noiseHit({type:'highpass',freq:8600,dur:s16===T.openHat?0.11:0.026,vol:s16%4?0.045:0.09,bus:musicBus,t});
    const b=T.bassPat[s16];
    if(b>=0){ const m=root+b;
      const osc=AC.createOscillator(); osc.type=T.bassWave; osc.frequency.value=nf(m);
      const f=AC.createBiquadFilter(); f.type='lowpass'; f.frequency.setValueAtTime(900,t); f.frequency.exponentialRampToValueAtTime(240,t+0.16);
      const g=AC.createGain(); env(g,t,0.004,0.22,0.19);
      osc.connect(f); f.connect(g); g.connect(musicBus); osc.start(t); osc.stop(t+0.25); }
    if(s16===0 && T.pad){ T.pad.forEach(iv=>{ [-6,6].forEach(det=>{
        const osc=AC.createOscillator(); osc.type='triangle'; osc.frequency.value=nf(root+12+iv); osc.detune.value=det;
        const g=AC.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.035,t+0.5);
        g.gain.linearRampToValueAtTime(0.0001,t+(60/T.bpm)*3.6);
        osc.connect(g); g.connect(musicBus); osc.start(t); osc.stop(t+(60/T.bpm)*4); }); }); }
    const mv=T.melody[step];
    if(mv>0){ const long=(step+2<64 && T.melody[step+2]<0);
      const osc=AC.createOscillator(); osc.type=T.leadWave; osc.frequency.value=nf(mv);
      const g=AC.createGain(); env(g,t,0.008,T.leadVol,long?0.42:0.2);
      osc.connect(g); g.connect(musicBus);
      const s=AC.createGain(); s.gain.value=0.9; g.connect(s); s.connect(delaySend);
      osc.start(t); osc.stop(t+0.5); }
  }
};
function setRadio(n){ G.radioTrack=n; if(AC) tone({f:1560,dur:0.05,vol:0.12});
  $('radioName').textContent=TRACKS[n].name; }
/* 0..1 pulse that decays after every kick — drives the tunnel light rings */
function beatPulse(){
  const T=TRACKS[G.radioTrack];
  if(!AC||!Music.kickT||G.muted) return 0.55+0.45*Math.abs(Math.sin(G.now*6));
  const ph=(AC.currentTime-Music.kickT)/(60/T.bpm);
  return ph<0? 1 : 1/(1+Math.max(0,ph)*3.4);
}
function toggleMute(){ G.muted=!G.muted; if(master) master.gain.value=G.muted?0:0.9;
  const b=$('btnSound'); b.classList.toggle('on',!G.muted); b.innerHTML=G.muted? 'SOUND <b>OFF</b>' : 'SOUND <b>ON</b>'; }

/* =====================================================================
   BACKGROUND — sky, sun, ridges, stars, sea
   ===================================================================== */
const ridgeA=[], ridgeB=[], skyline=[], stars=[], skyWin=[];
function buildScenery(){
  const r1=mulberry32(11), r2=mulberry32(23), r3=mulberry32(37);
  const ph=[r1()*9,r1()*9,r1()*9];
  for(let i=0;i<512;i++) ridgeA.push(0.42+0.34*Math.sin(i*0.045+ph[0])+0.24*Math.sin(i*0.11+ph[1]));
  const ph2=[r2()*9,r2()*9];
  for(let i=0;i<512;i++) ridgeB.push(0.5+0.3*Math.sin(i*0.03+ph2[0])+0.2*Math.sin(i*0.16+ph2[1]));
  let x=0;
  while(x<1500){ const w=26+r3()*60, h=40+r3()*150; skyline.push({x,w,h}); x+=w+2+r3()*8; }
  const rs=mulberry32(99);
  for(let i=0;i<110;i++) stars.push({x:rs()*SCENE_W, y:rs()*SCENE_H*0.42, s:rs()<0.2?2:1, tw:rs()*9});
  const rw=mulberry32(55);
  skyline.forEach(b=>{ for(let wy=10; wy<b.h-8; wy+=13) for(let wx=5; wx<b.w-6; wx+=10)
    if(rw()<0.3) skyWin.push({x:b.x+wx, y:wy, c:rw()<0.6?'#ffd27a':'#7fe0ff', tw:rw()*9}); });
}
function skyColorAt(pal,u){ // u: 0 top .. 1 horizon
  if(u<0.6) return mixHex(pal.skyTop,pal.skyMid,u/0.6);
  return mixHex(pal.skyMid,pal.skyLow,(u-0.6)/0.4);
}
function drawRidge(c,pal,heights,par,amp,baseY,col,rim){
  const off=(G.skyOffset*par + G.playerX*par*26);
  c.fillStyle=col; c.beginPath(); c.moveTo(0,SCENE_H);
  for(let sx=0;sx<=SCENE_W;sx+=4){
    const w=sx+off; const idx=((Math.floor(w/3)%512)+512)%512;
    c.lineTo(sx, baseY-heights[idx]*amp);
  }
  c.lineTo(SCENE_W,SCENE_H); c.closePath(); c.fill();
  if(rim){ c.strokeStyle=rim; c.lineWidth=1.5; c.stroke(); }
}
function drawBackground(c,pal,eyeShift){
  const W=SCENE_W,H=SCENE_H;
  const horizonY=clamp(G.horizonY||H*0.5, H*0.2, H*0.76);
  const ex=eyeShift*0.012;
  const g=c.createLinearGradient(0,0,0,horizonY+2);
  g.addColorStop(0,pal.skyTop); g.addColorStop(0.6,pal.skyMid); g.addColorStop(1,pal.skyLow);
  c.fillStyle=g; c.fillRect(0,0,W,H);
  /* stars */
  if(pal.stars>0){ for(const s of stars){
      const a=pal.stars*(0.35+0.65*Math.abs(Math.sin(G.now*1.4+s.tw)));
      c.fillStyle=rgba('#cfeaff',a); c.fillRect(s.x,s.y,s.s,s.s); } }
  /* sun / moon */
  const isCity = pal.type==='city';
  const prog = (G.state==='race'||G.state==='count') ? G.position/trackLength : 0.16;
  const sunX=W*0.64 - (G.skyOffset*0.045+ex) ;
  const sunY=horizonY - H*0.16 + prog*H*0.14;
  const r= isCity? 34 : 92;
  const glow=c.createRadialGradient(sunX,sunY,r*0.2,sunX,sunY,r*2.6);
  glow.addColorStop(0,rgba(pal.glow,0.55)); glow.addColorStop(1,rgba(pal.glow,0));
  c.fillStyle=glow; c.fillRect(sunX-r*3,sunY-r*3,r*6,r*6);
  c.fillStyle=pal.sunCore; c.beginPath(); c.arc(sunX,sunY,r,0,7); c.fill();
  if(!isCity){ /* synthwave slice gaps, filled with matching sky color */
    for(let i=0;i<6;i++){ const gy=sunY+r*(0.06+i*0.155), gh=2+i*1.8;
      if(gy<sunY+r){ c.fillStyle=skyColorAt(pal,clamp(gy/horizonY,0,1)*0.999);
        c.fillRect(sunX-r-4,gy,r*2+8,gh); } }
  } else { c.fillStyle='#0b1c33'; c.beginPath(); c.arc(sunX-12,sunY-8,r*0.82,0,7); c.fill(); }
  /* sea */
  if(pal.sea && (pal.seaA===undefined||pal.seaA>0.02)){
    c.save(); c.globalAlpha=(pal.seaA===undefined?1:pal.seaA);
    const sg=c.createLinearGradient(0,horizonY,0,horizonY+H*0.12);
    sg.addColorStop(0,pal.sea); sg.addColorStop(1,shade(pal.sea,-26));
    c.fillStyle=sg; c.fillRect(0,horizonY,W,H*0.12);
    c.fillStyle=rgba('#ffffff',0.16);
    for(let i=0;i<10;i++){ const yy=horizonY+6+i*(H*0.011);
      const xx=((i*173 + G.now*30*(1+i*0.2) - ex*2)%(W+160))-80;
      c.fillRect(xx,yy,34+i*6,2); }
    c.restore();
  }
  /* ridges / skyline */
  if(pal.type==='city'){
    const off=(G.skyOffset*0.22 + G.playerX*7 + ex);
    c.fillStyle=pal.mFar;
    skyline.forEach(b=>{ const bx=((b.x-off)%(1500)+1500)%1500 - 60;
      c.fillRect(bx,horizonY-b.h*0.62-14,b.w,b.h*0.62+14); });
    const off2=(G.skyOffset*0.4 + G.playerX*13 + ex);
    c.fillStyle=pal.mNear;
    skyline.forEach(b=>{ const bx=((b.x*1.3-off2)%(1500)+1500)%1500 - 60;
      c.fillRect(bx,horizonY-b.h*0.9,b.w,b.h*0.9+6); c.fillRect(bx+b.w*0.4,horizonY-b.h*0.9-10,3,10); });
    skyWin.forEach(wd=>{ const bx=((wd.x*1.3-off2)%(1500)+1500)%1500 - 60;
      const a=0.4+0.6*Math.abs(Math.sin(G.now*0.8+wd.tw));
      c.fillStyle=rgba(wd.c,a*0.8); c.fillRect(bx,horizonY-wd.y*0.9,3,4); });
  } else {
    drawRidge(c,pal,ridgeA,0.22,H*0.16,horizonY+8,pal.mFar,null);
    drawRidge(c,pal,ridgeB,0.42,H*0.11,horizonY+20,pal.mNear,rgba(pal.glow,0.22));
  }
  /* clouds */
  c.fillStyle=rgba(pal.type==='city'? '#16283e' : '#ffffff', pal.type==='city'?0.5:0.5);
  for(let i=0;i<5;i++){
    const cw=140+ (i%3)*70;
    const cx=((i*290 + G.cloudOff*(12+i*4) - ex*1.5)%(W+400))-200;
    const cy=H*0.1+ (i*53)% (H*0.2);
    c.beginPath(); c.ellipse(cx,cy,cw*0.5,13+i*3,0,0,7); c.ellipse(cx+cw*0.22,cy-9,cw*0.32,11,0,0,7); c.fill();
  }
}

/* =====================================================================
   RENDERING — the pseudo-3D pipeline
   ===================================================================== */
function project(p,camX,camY,camZ,cd,W,H){
  p.camera.x=-camX;
  p.camera.y=p.world.y-camY;
  p.camera.z=p.world.z-camZ;
  p.screen.scale=cd/p.camera.z;
  p.screen.x=Math.round(W/2 + p.screen.scale*p.camera.x*W/2);
  p.screen.y=Math.round(H/2 - p.screen.scale*p.camera.y*H/2);
  p.screen.w=Math.round(p.screen.scale*ROAD_W*W/2);
}
function poly(c,x1,y1,x2,y2,x3,y3,x4,y4,col){
  c.fillStyle=col; c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.lineTo(x3,y3); c.lineTo(x4,y4); c.closePath(); c.fill();
}
let camDepthCur=CAM_DEPTH;
function fogAt(d){ return 1/Math.exp(d*d*5); }

function drawSegment(c,seg,pal){
  const p1=seg.p1.screen, p2=seg.p2.screen, W=SCENE_W;
  const alt=seg.shade;
  c.fillStyle= seg.tunnel? (alt? '#120b1f':'#170e2a') : (alt? pal.grassB : pal.grassA);
  c.fillRect(0,p2.y,W,p1.y-p2.y);
  const r1=Math.max(2,p1.w*0.14), r2=Math.max(2,p2.w*0.14);
  const rc= alt? pal.rumbleB : pal.rumbleA;
  poly(c,p1.x-p1.w-r1,p1.y, p1.x-p1.w,p1.y, p2.x-p2.w,p2.y, p2.x-p2.w-r2,p2.y, rc);
  poly(c,p1.x+p1.w+r1,p1.y, p1.x+p1.w,p1.y, p2.x+p2.w,p2.y, p2.x+p2.w+r2,p2.y, rc);
  const roadCol= alt? pal.roadB : pal.roadA;
  if(seg.checker){ /* start / checkpoint strip */
    poly(c,p1.x-p1.w,p1.y, p1.x+p1.w,p1.y, p2.x+p2.w,p2.y, p2.x-p2.w,p2.y, (seg.index%2)? '#e8e8e8':'#20242c');
  } else {
    poly(c,p1.x-p1.w,p1.y, p1.x+p1.w,p1.y, p2.x+p2.w,p2.y, p2.x-p2.w,p2.y, roadCol);
    if(!alt){ const l1=Math.max(1,p1.w/30), l2=Math.max(1,p2.w/30);
      const lw1=p1.w*2/LANES, lw2=p2.w*2/LANES;
      for(let i=1;i<LANES;i++){
        const lx1=p1.x-p1.w+lw1*i, lx2=p2.x-p2.w+lw2*i;
        poly(c,lx1-l1,p1.y,lx1+l1,p1.y,lx2+l2,p2.y,lx2-l2,p2.y,pal.lane);
      }
    }
  }
  const fa=1-seg.fog;
  if(fa>0.015){ c.fillStyle=rgba(pal.fog,fa); c.fillRect(0,p2.y,W,p1.y-p2.y); }
}
function clipDraw(c,img,dx,dy,dw,dh,clipY,alpha){
  if(dw<2||alpha<=0.02) return;
  if(dy-dh>=clipY) return;
  c.save();
  if(clipY<SCENE_H){ c.beginPath(); c.rect(0,0,SCENE_W,clipY); c.clip(); }
  c.globalAlpha=alpha;
  c.drawImage(img,dx,dy-dh,dw,dh);
  c.restore(); c.globalAlpha=1;
}
/* ---- neon tunnel geometry ---- */
const TUN_BAND=['#8ef8ff','#ff5ce1','#ffd23f'];
function drawTunnel(c,seg){
  const p1=seg.p1.screen, p2=seg.p2.screen;
  if(p1.w<0.4) return;
  const h1=p1.w*1.2, h2=p2.w*1.2, o1=p1.w*1.34, o2=p2.w*1.34;
  const t1=p1.y-h1, t2=p2.y-h2;
  const fog=clamp(seg.fog*1.7,0,1), alt=seg.shade;
  c.globalAlpha=fog;
  poly(c, p1.x-o1,t1, p1.x+o1,t1, p2.x+o2,t2, p2.x-o2,t2, alt?'#170e28':'#1d1330');      // ceiling
  poly(c, p1.x-o1,p1.y, p1.x-o1,t1, p2.x-o2,t2, p2.x-o2,p2.y, alt?'#221540':'#271848');  // left wall
  poly(c, p1.x+o1,p1.y, p1.x+o1,t1, p2.x+o2,t2, p2.x+o2,p2.y, alt?'#2a1b50':'#301f5a');  // right wall
  if(seg.index%3===0){                                                                    // light rings
    const col=TUN_BAND[Math.floor(seg.index/3)%3];
    const bh=Math.max(2,p1.w*0.075);
    c.globalAlpha=fog*(0.42+0.58*beatPulse());
    poly(c, p1.x-o1,t1, p1.x+o1,t1, p1.x+o1,t1+bh, p1.x-o1,t1+bh, col);
    poly(c, p1.x-o1,t1, p1.x-o1+bh*0.8,t1, p2.x-o2+bh*0.8,t2, p2.x-o2,t2, col);
    poly(c, p1.x+o1,t1, p1.x+o1-bh*0.8,t1, p2.x+o2-bh*0.8,t2, p2.x+o2,t2, col);
    c.globalAlpha=fog*0.17;                              // neon spill on the tarmac
    poly(c, p1.x-p1.w,p1.y, p1.x+p1.w,p1.y, p2.x+p2.w,p2.y, p2.x-p2.w,p2.y, col);
  }
  c.globalAlpha=1;
}
function drawRoadside(c,seg,spr){
  const s=seg.p1.screen;
  const dw=spr.w*s.scale*SCENE_W/2;
  if(dw<3) return;
  const dh=dw*spr.img.height/spr.img.width;
  const dx=s.x + s.scale*spr.off*ROAD_W*SCENE_W/2 - dw/2;
  clipDraw(c,spr.img,dx,s.y,dw,dh,seg.clip,Math.min(1,seg.fog*1.35));
}
function drawTraffic(c,seg,car){
  const pct=(car.z%SEG_LEN)/SEG_LEN;
  const s=lerp(seg.p1.screen.scale,seg.p2.screen.scale,pct);
  if(!(s>0.00001)) return;
  const sx=lerp(seg.p1.screen.x,seg.p2.screen.x,pct) + s*car.offset*ROAD_W*SCENE_W/2;
  const sy=lerp(seg.p1.screen.y,seg.p2.screen.y,pct);
  if(sy>SCENE_H+120) return;
  const dw=640*s*SCENE_W/2;
  const dh=dw*car.img.height/car.img.width;
  c.save();
  if(seg.clip<SCENE_H){ c.beginPath(); c.rect(0,0,SCENE_W,seg.clip); c.clip(); }
  c.globalAlpha=Math.min(1,seg.fog*1.35);
  c.fillStyle='rgba(0,0,0,.3)';
  c.beginPath(); c.ellipse(sx,sy-2,dw*0.5,dw*0.09,0,0,7); c.fill();
  c.drawImage(car.img,sx-dw/2,sy-dh,dw,dh);
  c.restore(); c.globalAlpha=1;
}

function renderScene(c,eyeShift){
  const pal=curPal();
  const W=SCENE_W,H=SCENE_H;
  const base=findSegment(G.position);
  const basePct=(G.position%SEG_LEN)/SEG_LEN;
  const pSeg=findSegment(G.position+PLAYER_Z);
  const pPct=((G.position+PLAYER_Z)%SEG_LEN)/SEG_LEN;
  const pY=lerp(pSeg.p1.world.y,pSeg.p2.world.y,pPct);
  G.camYWorld=pY;
  let maxy=H, x=0, dx=-(base.curve*basePct), topY=H;
  const camX=G.playerX*ROAD_W + eyeShift;
  const camY=pY+CAM_H;
  /* pass 1 — project everything, and find where the road meets the sky */
  for(let n=0;n<DRAW_DIST;n++){
    const seg=segments[(base.index+n)%segments.length];
    seg.clip=maxy;
    seg.fog=fogAt(n/DRAW_DIST);
    seg.visible=false;
    const camZ=G.position - (seg.index<base.index? trackLength:0);
    project(seg.p1,camX-x,     camY,camZ,camDepthCur,W,H);
    project(seg.p2,camX-x-dx,  camY,camZ,camDepthCur,W,H);
    x+=dx; dx+=seg.curve;
    if(seg.p1.camera.z<=camDepthCur || seg.p2.screen.y>=seg.p1.screen.y || seg.p2.screen.y>=maxy) continue;
    seg.visible=true;
    if(seg.p2.screen.y<topY) topY=seg.p2.screen.y;
    maxy=seg.p2.screen.y;
  }
  /* the skyline rides on top of the road, so crests lift the horizon */
  G.horizonY=clamp(topY>=H? H*0.5 : topY, H*0.22, H*0.74);
  drawBackground(c,pal,eyeShift);
  /* pass 2 — paint the road front-to-back */
  for(let n=0;n<DRAW_DIST;n++){
    const seg=segments[(base.index+n)%segments.length];
    if(seg.visible) drawSegment(c,seg,pal);
  }
  /* pass 3 — tunnel shell, far to near so the exit stays visible */
  for(let n=DRAW_DIST-1;n>=0;n--){
    const seg=segments[(base.index+n)%segments.length];
    if(seg.tunnel && seg.visible) drawTunnel(c,seg);
  }
  for(let n=DRAW_DIST-1;n>=0;n--){
    const seg=segments[(base.index+n)%segments.length];
    for(const car of seg.cars) drawTraffic(c,seg,car);
    for(const spr of seg.sprites) drawRoadside(c,seg,spr);
  }
  drawPlayer(c,pal);
  drawParticles(c);
  if(G.boosting||G.speed/MAX_SPEED>0.92) drawSpeedLines(c);
}

/* ---------------- player car ---------------- */
function drawPlayer(c){
  if(G.crashT>0 && Math.floor(G.now*12)%2===0 && G.state==='race') return;
  const cw=SCENE_W*0.19, ch=cw*SPR.player.height/SPR.player.width;
  const ratio=G.speed/MAX_SPEED;
  const bounce=Math.sin(G.now*ratio*42)*1.7*ratio + (G.offroad? Math.sin(G.now*60)*2.4*Math.min(1,ratio*2):0);
  const cx=SCENE_W/2 + G.steerVis*10;
  const cy=SCENE_H-12+bounce;
  const lean=G.steerVis*0.10*Math.min(1,ratio*1.6);
  c.save();
  c.translate(cx,cy);
  c.transform(1,0,lean,1,0,0);
  c.fillStyle='rgba(0,0,0,.36)';
  c.beginPath(); c.ellipse(0,4,cw*0.53,cw*0.085,0,0,7); c.fill();
  if(G.boosting){ /* exhaust flames */
    for(const ex of [-cw*0.16,cw*0.16]){
      const fl=ch*0.28*(0.55+Math.random()*0.9);
      const fg=c.createLinearGradient(0,-8,0,fl);
      fg.addColorStop(0,'#fff3c0'); fg.addColorStop(0.35,'#ffb43a'); fg.addColorStop(1,'rgba(255,90,40,0)');
      c.fillStyle=fg;
      c.beginPath(); c.moveTo(ex-7,-6); c.lineTo(ex+7,-6); c.lineTo(ex+(Math.random()*6-3),fl); c.closePath(); c.fill();
    }
  }
  c.drawImage(SPR.player,-cw/2,-ch,cw,ch);
  if(input.brake && G.state==='race'){
    c.fillStyle=rgba('#ff3b30',0.55+0.3*Math.sin(G.now*30));
    c.fillRect(-cw*0.36,-ch*0.5,cw*0.72,ch*0.075);
  }
  c.restore();
}
function drawSpeedLines(c){
  const n=G.boosting?16:8, cx=SCENE_W/2, cy=SCENE_H*0.42;
  c.strokeStyle=rgba('#ffffff',G.boosting?0.16:0.07);
  c.lineWidth=2;
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2;
    const r0=Math.min(SCENE_W,SCENE_H)*(0.42+Math.random()*0.2);
    const r1=r0+60+Math.random()*130;
    c.beginPath();
    c.moveTo(cx+Math.cos(a)*r0,cy+Math.sin(a)*r0*0.7);
    c.lineTo(cx+Math.cos(a)*r1,cy+Math.sin(a)*r1*0.7);
    c.stroke();
  }
}

/* ---------------- particles & popups ---------------- */
function spawnPart(o){ if(G.parts.length<320) G.parts.push(Object.assign({life:1,maxLife:1,vx:0,vy:0,size:3,grav:0},o)); }
function drawParticles(c){
  for(let i=G.parts.length-1;i>=0;i--){
    const p=G.parts[i];
    p.life-=p.dt||0.016; p.x+=p.vx; p.y+=p.vy; p.vy+=p.grav;
    if(p.life<=0){ G.parts.splice(i,1); continue; }
    c.globalAlpha=clamp(p.life/p.maxLife,0,1)*p.alpha||1;
    c.fillStyle=p.color;
    c.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
  }
  c.globalAlpha=1;
}
function addPopup(text,color,x,y){ G.popups.push({text,color,x,y,life:1.15}); }

/* =====================================================================
   UPDATE
   ===================================================================== */
function isHex(v){ return typeof v==='string' && v && v[0]==='#'; }
function curPal(){
  if(G.palT>=1) return G.palB;
  const t=easeInOut(0,1,G.palT), a=G.palA, b=G.palB, o={};
  for(const k of Object.keys(b)){
    o[k]= (isHex(b[k]) && isHex(a[k])) ? mixHex(a[k],b[k],t) : b[k];
  }
  /* the sea fades out rather than blending with a null value */
  o.seaA=lerp(a.seaA||0, b.seaA||0, t);
  o.sea=isHex(a.sea)&&isHex(b.sea) ? o.sea : (t<0.5? (a.sea||b.sea) : (b.sea||a.sea));
  o.type=t<0.5?a.type:b.type; o.stars=lerp(a.stars||0,b.stars||0,t); o.name=b.name;
  return o;
}
function setStagePalette(idx,snap){
  G.palA=curPal(); G.palB=PALETTES[idx]; G.palT=snap?1:0;
}
function startRace(){ G.stageIdx=0; startLevel(); }
function startLevel(){
  const L=G.stageIdx;
  buildLevel(L); placeSprites(L); resetCars(L);
  G.position=0; G.playerX=0; G.speed=0; G.steerVis=0;
  G.timeLeft=LEVEL_TIME[L]; G.cpDone=[false,false]; G.results=null;
  G.nearMisses=0; G.topSpeedKmh=0; G.raceElapsed=0;
  G.gauge=0.55; G.boosting=false; G.crashT=0; G.shakeT=0; G.flashT=0;
  G.popups.length=0; G.parts.length=0;
  setStagePalette(L,true);
  if(L>=5 && G.radioTrack!==4) setRadio(4);        // the tunnel picks its own station
  G.state='count'; G.countT=3.0; G.lastCount=4;
  if(AC) SFX.coin();
  G.credits=1; updateLeds();
  showBanner(`STAGE ${L+1}/6`, L>=5? 'NEON TUNNEL · TUNNEL FM LOCKED ON' : PALETTES[L].name,
             L>=5?'#8ef8ff':'#ffd23f', L>=5?3.2:2.8);
}
function nextLevel(){
  if(G.stageIdx>=5){ victory(); return; }
  G.stageIdx++; startLevel();
}
let banner=null;
function showBanner(main,sub,color,dur){ banner={main,sub,color,dur,t:0}; }

function update(dt){
  G.now+=dt;
  if(G.palT<1) G.palT=Math.min(1,G.palT+dt/1.25);
  if(banner){ banner.t+=dt; if(banner.t>banner.dur) banner=null; }

  const pSeg=findSegment(G.position+PLAYER_Z);
  const speedRatio=G.speed/MAX_SPEED;
  G.offroad=Math.abs(G.playerX)>1.02;

  if(G.state==='attract'||G.state==='over'||G.state==='victory'){
    /* autopilot demo */
    const look=segments[(pSeg.index+16)%segments.length];
    let target=clamp(-look.curve*0.5,-0.8,0.8);
    for(const car of cars){
      const ahead=(pSeg.index-car.seg.index+segments.length)%segments.length;
      if(ahead<20 && Math.abs(car.offset-G.playerX)<0.55)
        target=clamp(car.offset>G.playerX? car.offset-1 : car.offset+1,-0.85,0.85);
    }
    G.playerX+=(target-G.playerX)*Math.min(1,dt*2.3);
    const cruise=G.state==='victory'?0.45:0.74;
    G.speed+=(MAX_SPEED*cruise-G.speed)*Math.min(1,dt*0.7);
    G.position=(G.position+G.speed*dt)%trackLength;
    G.steerVis+=(clamp(target-G.playerX,-1,1)-G.steerVis)*Math.min(1,dt*6);
    if(G.offroad){ /* the demo car throws dust too */
      for(let k=0;k<2;k++) spawnPart({x:SCENE_W/2+(Math.random()<0.5?-1:1)*(SCENE_W*0.09),y:SCENE_H-20,
        vx:Math.random()*4-2,vy:-Math.random()*3-1,grav:0.12,size:4+Math.random()*4,
        color:rgba(curPal().grassA,0.8),life:0.45,maxLife:0.45,dt});
    }
  }
  else if(G.state==='results'){
    /* rolling cool-down past the chequered line while the tally counts up */
    G.results.t+=dt;
    G.speed=Math.max(0,G.speed+COAST*2.4*dt);
    G.position=Math.min(G.position+G.speed*dt,(segments.length-26)*SEG_LEN);
    G.playerX+=(0-G.playerX)*Math.min(1,dt*1.8);
    G.steerVis*=Math.max(0,1-dt*4);
    if(G.boosting){ G.boosting=false; }
    G.gauge=Math.max(0,G.gauge-dt*0.4);
  }
  else if(G.state==='count'){
    G.countT-=dt;
    const whole=Math.ceil(G.countT);
    if(whole!==G.lastCount && whole>0){ G.lastCount=whole; if(AC) SFX.beep(false); }
    if(G.countT<=0){ G.state='race'; if(AC) SFX.beep(true); G.raceStart=G.now; }
    if(input.throttle) G.speed=Math.min(G.speed+ACCEL*0.35*dt, MAX_SPEED*0.06);
  }
  else if(G.state==='race'){
    /* steering */
    const dx=dt*2*speedRatio;
    let steer=0;
    if(input.left) steer-=1;
    if(input.right) steer+=1;
    G.playerX+=steer*dx;
    G.playerX-=dx*speedRatio*pSeg.curve*CENTRIFUGAL;
    G.playerX=clamp(G.playerX,-2.6,2.6);
    G.steerVis+=(steer-G.steerVis)*Math.min(1,dt*10);
    /* nitro */
    const wantBoost=input.boost&&G.gauge>0.03;
    if(wantBoost&&!G.boosting&&AC) SFX.nitro();
    G.boosting=wantBoost;
    if(G.boosting){ G.gauge=Math.max(0,G.gauge-dt/2.6); }
    else G.gauge=Math.min(1,G.gauge+dt/24);
    /* longitudinal */
    const vmax=G.boosting?BOOST_MAX:MAX_SPEED;
    if(G.boosting) G.speed+=ACCEL*2.1*dt;
    else if(input.throttle||(input.touch&&!input.brake)) G.speed+=ACCEL*dt;
    else if(input.brake) G.speed+=BRAKE*dt;
    else G.speed+=COAST*dt;
    if(G.offroad&&G.speed>OFFROAD_LIMIT&&!G.boosting) G.speed+=OFFROAD_DECEL*dt;
    G.speed=clamp(G.speed,0,vmax);
    G.position=(G.position+G.speed*dt)%trackLength;
    G.raceElapsed+=dt;
    G.topSpeedKmh=Math.max(G.topSpeedKmh,Math.round(speedRatio*300));
    G.score+=G.speed*dt*0.085*(G.boosting?1.5:1);
    /* timer */
    G.timeLeft-=dt;
    const tl=Math.ceil(G.timeLeft);
    if(tl!==G.lastTick && tl<=10 && tl>0){ G.lastTick=tl; if(AC) SFX.tick(); }
    if(G.timeLeft<=0){ G.timeLeft=0; gameOver(); }
    /* mid-level checkpoints */
    for(let i=0;i<checkpointSegs.length;i++){
      if(!G.cpDone[i] && G.position+PLAYER_Z>=checkpointSegs[i]*SEG_LEN){
        G.cpDone[i]=true; checkpoint(i);
      }
    }
    /* the finish line */
    if(G.position+PLAYER_Z>=finishSeg*SEG_LEN){ finishLevel(); }
    /* collisions: traffic */
    if(G.crashT>0) G.crashT-=dt;
    for(const car of cars){
      const dz=Math.abs(car.z-(G.position+PLAYER_Z));
      const dzWrap=Math.min(dz,trackLength-dz);
      if(dzWrap<SEG_LEN*1.1){
        const ddx=Math.abs(car.offset-G.playerX);
        if(ddx<0.26 && G.crashT<=0){
          G.speed*=0.3; G.crashT=0.9; G.shakeT=0.55; G.flashT=0.4;
          G.playerX+=(G.playerX>car.offset?0.25:-0.25);
          if(AC) SFX.crash();
          for(let k=0;k<16;k++) spawnPart({x:SCENE_W/2+(Math.random()*90-45),y:SCENE_H-70,
            vx:Math.random()*8-4,vy:-Math.random()*7-2,grav:0.35,size:3+Math.random()*3,
            color:Math.random()<0.5?'#ffd23f':'#ff6a3d',life:0.6,maxLife:0.6,dt});
        } else if(ddx<0.5&&ddx>=0.26&&G.speed>MAX_SPEED*0.55&&G.now-car.nmT>2){
          car.nmT=G.now; G.nearMisses++;
          G.gauge=Math.min(1,G.gauge+0.22);
          G.score+=500;
          if(AC) SFX.whoosh();
          addPopup('NEAR MISS +500','#3fe0d0',SCENE_W/2+(car.offset-G.playerX)*-300,SCENE_H*0.6);
        }
      }
    }
    /* collisions: roadside sprites */
    if(G.offroad){
      const seg=findSegment(G.position+PLAYER_Z);
      for(const spr of seg.sprites){
        if(spr.kind==='gantry'||spr.kind==='lamp') continue;
        if(Math.abs(spr.off-G.playerX)<0.24 && G.crashT<=0){
          G.speed*=0.22; G.crashT=0.8; G.shakeT=0.5; G.flashT=0.35;
          G.playerX=clamp(G.playerX+(G.playerX>spr.off?0.3:-0.3),-2.6,2.6);
          if(AC) SFX.crash();
        }
      }
      /* dust */
      if(G.speed>MAX_SPEED*0.08) for(let k=0;k<2;k++)
        spawnPart({x:SCENE_W/2+(Math.random()<0.5?-1:1)*(SCENE_W*0.085),y:SCENE_H-22,
          vx:Math.random()*4-2,vy:-Math.random()*3-1,grav:0.12,size:4+Math.random()*4,
          color:rgba(curPal().grassA,0.85),life:0.5,maxLife:0.5,dt});
    }
    if(G.boosting) for(let k=0;k<2;k++)
      spawnPart({x:SCENE_W/2+(Math.random()<0.5?-1:1)*SCENE_W*0.032,y:SCENE_H-16,
        vx:Math.random()*2-1,vy:2+Math.random()*2,grav:0,size:3,
        color:Math.random()<0.4?'#7fd4ff':'#ffb43a',life:0.3,maxLife:0.3,dt});
    if(G.shakeT>0) G.shakeT-=dt;
    if(G.flashT>0) G.flashT-=dt;
  }

  /* traffic */
  for(const car of cars){
    const old=car.seg;
    car.z+=car.speed*dt; if(car.z>=trackLength) car.z-=trackLength;
    const seg=findSegment(car.z);
    if(seg!==old){ const i=old.cars.indexOf(car); if(i>=0) old.cars.splice(i,1); seg.cars.push(car); car.seg=seg; }
    const ahead=(pSeg.index-seg.index+segments.length)%segments.length;
    if(ahead>0&&ahead<26&&Math.abs(car.offset-G.playerX)<0.48){
      const dir=car.offset>G.playerX?1:-1;
      car.offset=clamp(car.offset+dir*0.85*dt,-0.82,0.82);
    }
  }

  /* tunnel transitions */
  const inTun=!!(pSeg&&pSeg.tunnel);
  if(inTun!==G.wasTun){
    G.wasTun=inTun;
    if(inTun){
      if(AC) SFX.whoosh();
      if(G.state==='race') showBanner('NEON TUNNEL','LIGHTS OUT — FULL THROTTLE','#8ef8ff',1.7);
      G.shakeT=Math.max(G.shakeT,0.14);
    }
  }
  /* ambience */
  G.skyOffset+=pSeg.curve*speedRatio*dt*44;
  G.cloudOff+=dt*(1+speedRatio*2);

  /* engine audio */
  if(AC&&engOsc){
    const r=G.speed/MAX_SPEED;
    engOsc.frequency.setTargetAtTime(52+r*190+(G.boosting?40:0)+Math.sin(G.now*31)*3,AC.currentTime,0.03);
    engOsc2.frequency.setTargetAtTime(26+r*95,AC.currentTime,0.03);
    engFilter.frequency.setTargetAtTime(280+r*2600,AC.currentTime,0.05);
    engGain.gain.setTargetAtTime(((G.state==='race'||G.state==='count'||G.state==='results')?0.045+r*0.075:(G.state==='attract'?0.03:0.02))*(G.wasTun?1.4:1),AC.currentTime,0.08);
    roadGain.gain.setTargetAtTime(G.offroad?Math.min(0.2,r*0.3):0,AC.currentTime,0.05);
    if(tunnelFilter) tunnelFilter.frequency.setTargetAtTime(G.wasTun?1000:18000,AC.currentTime,0.16);
  }
}
function checkpoint(i){
  G.timeLeft+=CHECKPOINT_BONUS; G.score+=5000; G.gauge=1;
  if(AC) SFX.checkpoint();
  const last=(i===checkpointSegs.length-1);
  showBanner('CHECKPOINT', `+${CHECKPOINT_BONUS} SEC · ${last? 'FINAL PUSH TO THE LINE':'HALFWAY THERE'}`,'#3fe0d0',2.2);
  addPopup(`+${CHECKPOINT_BONUS} SEC`,'#3fe0d0',SCENE_W/2,SCENE_H*0.4);
  addPopup('+5000','#ffd23f',SCENE_W/2,SCENE_H*0.46);
  G.flashT=0.2;
}
function finishLevel(){
  if(G.state!=='race') return;
  G.state='results'; G.boosting=false;
  const timeBonus=Math.round(G.timeLeft)*400;
  G.results={ t:0, timeBonus, nmBonus:G.nearMisses*500, clear:10000+G.stageIdx*4000 };
  G.score+=timeBonus+G.results.nmBonus+G.results.clear;
  G.totalTime+=G.raceElapsed; G.totalNM+=G.nearMisses;
  saveHi();
  if(AC) SFX.fanfare();
  for(let k=0;k<110;k++) spawnPart({x:Math.random()*SCENE_W,y:SCENE_H*0.2+Math.random()*SCENE_H*0.5,
    vx:Math.random()*7-3.5,vy:-Math.random()*5-1,grav:0.16,size:3+Math.random()*4,
    color:['#ffd23f','#3fe0d0','#ff6a3d','#ff4fa0','#f3ead9'][k%5],life:1.6,maxLife:1.6,dt:0.016});
}
function gameOver(){
  G.state='over'; G.boosting=false; G.totalTime+=G.raceElapsed; G.totalNM+=G.nearMisses;
  if(AC) SFX.gameover();
  saveHi();
}
function victory(){
  G.state='victory';
  if(AC) SFX.fanfare();
  saveHi();
  for(let k=0;k<140;k++) spawnPart({x:Math.random()*SCENE_W,y:SCENE_H*0.2+Math.random()*SCENE_H*0.55,
    vx:Math.random()*7-3.5,vy:-Math.random()*6-1,grav:0.15,size:3+Math.random()*4,
    color:['#ffd23f','#3fe0d0','#ff6a3d','#ff4fa0','#f3ead9'][k%5],life:2,maxLife:2,dt:0.016});
}
function saveHi(){
  const s=Math.floor(G.score);
  if(s>G.hiScore){ G.hiScore=s; try{ localStorage.setItem('radhorizon_hi',String(s)); }catch(e){} }
  updateLeds();
}
function updateLeds(){
  $('hiLed').textContent=String(G.hiScore).padStart(7,'0');
  $('credLed').textContent=String(Math.max(0,G.credits));
}

/* =====================================================================
   HUD + OVERLAYS (canvas)
   ===================================================================== */
function txt(c,str,x,y,size,color,align='left',ps=true){
  c.font=`${size}px ${ps?'"Press Start 2P", monospace':'"Chakra Petch", sans-serif'}`;
  c.textAlign=align; c.textBaseline='alphabetic';
  c.fillStyle='rgba(0,0,0,.55)'; c.fillText(str,x+Math.max(2,size*0.12),y+Math.max(2,size*0.12));
  c.fillStyle=color; c.fillText(str,x,y);
}
const eq=[0.3,0.5,0.4,0.6,0.35];
function drawHUD(c){
  const W=SCENE_W,H=SCENE_H;
  if(G.state==='attract') return;
  /* TIME */
  const t=Math.ceil(G.timeLeft);
  const danger=t<=10;
  txt(c,'TIME',30,32,10,'#8b93a1');
  const blink=!danger||Math.floor(G.now*3)%2===0;
  if(blink) txt(c,String(t).padStart(2,'0'),30,74,30,danger?'#ff4b4b':'#f3ead9');
  /* SCORE */
  txt(c,'SCORE',W/2,32,9,'#8b93a1','center');
  txt(c,String(Math.floor(G.score)).padStart(7,'0'),W/2,56,13,'#f3ead9','center');
  /* route progress to the finish line */
  const bw=320,bx=W/2-bw/2,by=68, total=finishSeg*SEG_LEN;
  c.fillStyle='rgba(10,14,20,.75)'; c.fillRect(bx,by,bw,7);
  c.fillStyle='#3fe0d0'; c.fillRect(bx,by,bw*clamp(G.position/total,0,1),7);
  checkpointSegs.forEach((cs,i)=>{
    c.fillStyle=(G.cpDone&&G.cpDone[i])? '#3fe0d0':'#ffd23f';
    c.fillRect(bx+bw*(cs*SEG_LEN/total)-1,by-3,3,13);
  });
  for(let r=0;r<3;r++) for(let c2=0;c2<2;c2++){        // chequered flag at the end
    c.fillStyle=(r+c2)%2? '#f3ead9':'#15181e';
    c.fillRect(bx+bw+4+c2*4,by-4+r*5,4,5);
  }
  if(G.state==='race') txt(c,`${Math.max(0,Math.round((total-G.position)/145))} M TO FINISH`,W/2,by+22,8,'#8b93a1','center');
  /* SPEED */
  const kmh=Math.round(G.speed/MAX_SPEED*300);
  txt(c,String(kmh).padStart(3,'0'),W-30,74,30,G.boosting?'#ffd23f':'#f3ead9','right');
  txt(c,'KM/H',W-30,92,9,'#8b93a1','right');
  txt(c,`STAGE ${G.stageIdx+1}/6 · ${PALETTES[G.stageIdx].name}`,W-30,112,8,'#8b93a1','right');
  if(G.stereo) txt(c,'3-D ON',W-30,130,8,'#ff4fa0','right');
  /* radio box */
  c.fillStyle='rgba(8,11,16,.72)'; c.strokeStyle='rgba(63,224,208,.28)';
  roundRect(c,24,H-80,262,56,8); c.fill(); c.stroke();
  txt(c,G.muted?'RADIO · MUTED':`RADIO ${G.radioTrack}`,38,H-58,8,'#8b93a1');
  txt(c,G.muted?'———':TRACKS[G.radioTrack].name,38,H-38,10,'#f3ead9');
  for(let i=0;i<5;i++){
    if(!G.muted&&AC) eq[i]=clamp(eq[i]+(Math.random()-0.5)*0.4,0.12,1);
    const bh=(G.muted?3:eq[i]*26);
    c.fillStyle=i%2?'#3fe0d0':'#ffd23f';
    c.fillRect(216+i*12,H-32-bh,7,bh);
  }
  /* nitro box */
  c.fillStyle='rgba(8,11,16,.72)'; c.strokeStyle=G.gauge>=1?'rgba(255,210,63,.5)':'rgba(63,224,208,.28)';
  roundRect(c,W-286,H-80,262,56,8); c.fill(); c.stroke();
  txt(c,'NITRO',W-272,H-58,8,'#8b93a1');
  txt(c,'SHIFT',W-36,H-58,7,'#5d6572','right');
  const cells=16;
  for(let i=0;i<cells;i++){
    const on=G.gauge>=(i+0.5)/cells;
    const pulse=G.gauge>=1? (0.75+0.25*Math.sin(G.now*8)) : 1;
    c.fillStyle=on? rgba(i>cells*0.7?'#ffd23f':'#3fe0d0',pulse) : 'rgba(255,255,255,.09)';
    c.fillRect(W-272+i*15,H-48,11,16);
  }
  /* popups */
  for(let i=G.popups.length-1;i>=0;i--){
    const p=G.popups[i]; p.life-=0.016; p.y-=40*0.016;
    if(p.life<=0){ G.popups.splice(i,1); continue; }
    c.globalAlpha=clamp(p.life,0,1);
    txt(c,p.text,p.x,p.y,11,p.color,'center');
    c.globalAlpha=1;
  }
  /* banner */
  if(banner){
    const k=banner.t/banner.dur;
    const scale=k<0.12? lerp(1.7,1,k/0.12) : 1;
    const alpha=k>0.85? (1-k)/0.15 : 1;
    c.save(); c.translate(W/2,H*0.30); c.scale(scale,scale); c.globalAlpha=alpha;
    txt(c,banner.main,0,0,30,banner.color,'center');
    if(banner.sub) txt(c,banner.sub,0,34,11,'#f3ead9','center');
    c.restore(); c.globalAlpha=1;
  }
  /* countdown */
  if(G.state==='count'){
    const whole=Math.ceil(G.countT);
    const frac=G.countT-Math.floor(G.countT);
    const s=1+frac*0.5;
    c.save(); c.translate(W/2,H*0.42); c.scale(s,s);
    txt(c,String(whole),0,0,72,['#ff4b4b','#ffd23f','#3fe0d0','#3fe0d0'][whole]||'#fff','center');
    c.restore();
  } else if(G.state==='race'&&G.now-G.raceStart<0.9){
    txt(c,'GO!!',W/2,H*0.42,64,'#3fe0d0','center');
  }
  /* low time vignette */
  if(danger&&G.state==='race'){
    c.fillStyle=rgba('#ff2020',0.08+0.07*Math.sin(G.now*6));
    c.fillRect(0,0,W,H);
  }
  if(G.flashT>0){ c.fillStyle=rgba('#ff3020',G.flashT*0.5); c.fillRect(0,0,W,H); }
}
function drawOverlay(c){
  const W=SCENE_W,H=SCENE_H;
  if(G.state==='attract'){
    const g=c.createLinearGradient(0,0,0,H);
    g.addColorStop(0,'rgba(5,8,12,.82)'); g.addColorStop(0.32,'rgba(5,8,12,.28)');
    g.addColorStop(0.62,'rgba(5,8,12,.28)'); g.addColorStop(1,'rgba(5,8,12,.86)');
    c.fillStyle=g; c.fillRect(0,0,W,H);
    const bob=Math.sin(G.now*1.6)*4;
    c.save(); c.translate(0,bob);
    c.font='44px "Press Start 2P", monospace'; c.textAlign='center';
    c.fillStyle='#12151c'; c.fillText('RAD HORIZON',W/2+6,H*0.24+6);
    c.fillStyle='#3fe0d0'; c.fillText('RAD HORIZON',W/2+3,H*0.24+3);
    c.fillStyle='#ffd23f'; c.fillText('RAD HORIZON',W/2,H*0.24);
    txt(c,'T U R B O   G R A N D   P R I X',W/2,H*0.24+36,11,'#f3ead9','center');
    c.restore();
    if(Math.floor(G.now*1.4)%2===0)
      txt(c,'PRESS ENTER TO START',W/2,H*0.5,15,'#f3ead9','center');
    txt(c,`HIGH SCORE ${String(G.hiScore).padStart(7,'0')}`,W/2,H*0.575,11,'#ffd23f','center');
    txt(c,'6 STAGES · 2 CHECKPOINTS EACH · CHEQUERED FINISH',W/2,H*0.645,9,'#8b93a1','center');
    txt(c,'SLICE THROUGH TRAFFIC TO CHARGE NITRO · DON\'T LIFT',W/2,H*0.685,9,'#8b93a1','center');
    txt(c,'STAGE 6 — THE NEON TUNNEL · LIGHTS THAT PULSE TO THE RADIO',W/2,H*0.725,9,'#ff5ce1','center');
    txt(c,'ARROWS DRIVE · SHIFT NITRO · 1-2-3 RADIO · G = 3-D GLASSES',W/2,H*0.78,8,'#5d6572','center');
    txt(c,'© 1986 VECTORWORKS AMUSEMENT',W/2,H*0.94,8,'#3d4450','center');
  }
  else if(G.state==='results'){
    const R=G.results; if(!R) return;
    c.fillStyle='rgba(5,8,12,.62)'; c.fillRect(W*0.13,H*0.19,W*0.74,H*0.62);
    c.strokeStyle='rgba(63,224,208,.4)'; c.lineWidth=2; c.strokeRect(W*0.13,H*0.19,W*0.74,H*0.62);
    c.strokeStyle='rgba(255,210,63,.25)'; c.strokeRect(W*0.13+6,H*0.19+6,W*0.74-12,H*0.62-12);
    if(R.t<1.25){                                  // chequered-flag flash
      const k=R.t/1.25, s=1+(1-k)*0.55;
      c.save(); c.translate(W/2,H*0.47); c.scale(s,s); c.globalAlpha=Math.min(1,(1-k)*3);
      txt(c,'FINISH!',0,0,52,'#ffd23f','center');
      c.restore(); c.globalAlpha=1;
    }
    txt(c,'STAGE CLEAR',W/2,H*0.29,26,'#3fe0d0','center');
    txt(c,PALETTES[G.stageIdx].name,W/2,H*0.29+30,9,'#8b93a1','center');
    const rows=[['TIME BONUS',R.timeBonus,0.8],['NEAR MISS x '+G.nearMisses,R.nmBonus,1.35],['STAGE CLEAR',R.clear,1.9]];
    let ty=H*0.41;
    rows.forEach(rw=>{
      if(R.t>rw[2]){
        txt(c,rw[0],W*0.22,ty,10,'#f3ead9');
        txt(c,String(rw[1]).padStart(7,'0'),W*0.78,ty,12,'#ffd23f','right');
      }
      ty+=34;
    });
    txt(c,`RUN ${G.raceElapsed.toFixed(1)} SEC   ·   TOP ${G.topSpeedKmh} KM/H   ·   TIME LEFT ${G.timeLeft.toFixed(1)}`,W/2,H*0.585,9,'#8b93a1','center');
    if(R.t>2.4){
      txt(c,'TOTAL',W*0.22,H*0.665,10,'#8b93a1');
      txt(c,String(Math.floor(G.score)).padStart(7,'0'),W*0.78,H*0.665,14,'#f3ead9','right');
      if(Math.floor(G.now*1.5)%2===0)
        txt(c,G.stageIdx>=5? 'PRESS ENTER':'ENTER — NEXT STAGE',W/2,H*0.755,12,'#ffd23f','center');
    }
  }
  else if(G.state==='over'){
    c.fillStyle='rgba(5,8,12,.72)'; c.fillRect(0,0,W,H);
    txt(c,'GAME OVER',W/2,H*0.34,40,'#ff4b4b','center');
    txt(c,'TIME UP — THE CLOCK BEAT YOU',W/2,H*0.34+40,10,'#f3ead9','center');
    txt(c,`SCORE ${String(Math.floor(G.score)).padStart(7,'0')}   ·   STAGE ${G.stageIdx+1}/5   ·   TOP ${G.topSpeedKmh} KM/H`,W/2,H*0.55,11,'#ffd23f','center');
    if(Math.floor(G.score)>=G.hiScore&&G.hiScore>0&&Math.floor(G.now*2)%2===0)
      txt(c,'★ NEW HIGH SCORE ★',W/2,H*0.62,12,'#3fe0d0','center');
    if(Math.floor(G.now*1.4)%2===0) txt(c,'ENTER — TITLE      R — RETRY STAGE',W/2,H*0.78,10,'#f3ead9','center');
  }
  else if(G.state==='victory'){
    c.fillStyle='rgba(5,8,12,.66)'; c.fillRect(0,0,W,H);
    const bob=Math.sin(G.now*2)*3;
    txt(c,'CONGRATULATIONS!',W/2,H*0.28+bob,30,'#ffd23f','center');
    txt(c,'ALL 5 STAGES CLEARED — YOU OWN THE HORIZON',W/2,H*0.28+38+bob,10,'#3fe0d0','center');
    const m=Math.floor(G.totalTime/60), s=Math.floor(G.totalTime%60);
    txt(c,`SCORE  ${String(Math.floor(G.score)).padStart(7,'0')}`,W/2,H*0.5,13,'#f3ead9','center');
    txt(c,`TOTAL TIME  ${m}:${String(s).padStart(2,'0')}   ·   NEAR MISSES  ${G.totalNM}   ·   TOP  ${G.topSpeedKmh} KM/H`,W/2,H*0.575,10,'#f3ead9','center');
    if(Math.floor(G.score)>=G.hiScore&&Math.floor(G.now*2)%2===0)
      txt(c,'★ NEW HIGH SCORE ★',W/2,H*0.645,12,'#ff4fa0','center');
    if(Math.floor(G.now*1.4)%2===0) txt(c,'PRESS ENTER',W/2,H*0.8,11,'#f3ead9','center');
  }
  else if(G.state==='pause'){
    c.fillStyle='rgba(5,8,12,.7)'; c.fillRect(0,0,W,H);
    txt(c,'PAUSED',W/2,H*0.45,34,'#f3ead9','center');
    txt(c,'P — RESUME',W/2,H*0.55,10,'#8b93a1','center');
  }
}

/* =====================================================================
   STEREO 3-D (Rad Racer anaglyph homage)
   ===================================================================== */
function tint(src,dst,col){
  const g=dst.getContext('2d');
  g.globalCompositeOperation='source-over';
  g.drawImage(src,0,0);
  g.globalCompositeOperation='multiply';
  g.fillStyle=col;
  g.fillRect(0,0,SCENE_W,SCENE_H);
  g.globalCompositeOperation='source-over';
}
function compositeFrame(){
  const c=dctx;
  if(G.stereo){
    if(!tL){ tL=document.createElement('canvas'); tL.width=SCENE_W; tL.height=SCENE_H;
      tR=document.createElement('canvas'); tR.width=SCENE_W; tR.height=SCENE_H; }
    renderScene(sctx,-EYE);
    renderScene(sctxR, EYE);
    tint(sceneCv,tL,'rgb(255,0,0)');
    tint(sceneR,tR,'rgb(0,255,255)');
    c.drawImage(tL,0,0);
    c.globalCompositeOperation='lighter';
    c.drawImage(tR,0,0);
    c.globalCompositeOperation='source-over';
  } else {
    renderScene(sctx,0);
    c.drawImage(sceneCv,0,0);
  }
  applyShake(c);
}
let shakeCv=null;
function applyShake(c){
  if(G.shakeT<=0) return;
  if(!shakeCv){ shakeCv=document.createElement('canvas'); shakeCv.width=SCENE_W; shakeCv.height=SCENE_H; }
  const g=shakeCv.getContext('2d');
  g.clearRect(0,0,SCENE_W,SCENE_H); g.drawImage(display,0,0);
  const a=G.shakeT*9;
  c.save(); c.setTransform(1,0,0,1,0,0);
  c.drawImage(shakeCv,Math.random()*a-a/2,Math.random()*a-a/2);
  c.restore();
}
function toggleStereo(){
  G.stereo=!G.stereo;
  $('btnStereo').classList.toggle('on',G.stereo);
  if(G.stereo) showBanner('3-D MODE','PUT ON YOUR RED / CYAN GLASSES','#ff4fa0',2.4);
  if(AC) tone({f:G.stereo?1200:700,dur:0.08,vol:0.15});
}

/* =====================================================================
   INPUT
   ===================================================================== */
const KEYMAP={ArrowLeft:'left',KeyA:'left',ArrowRight:'right',KeyD:'right',
  ArrowUp:'throttle',KeyW:'throttle',ArrowDown:'brake',KeyS:'brake',
  ShiftLeft:'boost',ShiftRight:'boost',Space:'boost'};
window.addEventListener('keydown',e=>{
  if(KEYMAP[e.code]!==undefined){ input[KEYMAP[e.code]]=true; e.preventDefault(); }
  ensureAudio();
  if(e.repeat) return;
  switch(e.code){
    case 'Enter':
      if(G.state==='attract') startRace();
      else if(G.state==='results') nextLevel();
      else if(G.state==='over'||G.state==='victory'){ G.state='attract'; G.stageIdx=0; }
      break;
    case 'KeyR':
      if(G.state==='results') startLevel();
      else if(G.state==='over'||G.state==='victory') startRace();
      break;
    case 'KeyM': toggleMute(); break;
    case 'KeyG': toggleStereo(); break;
    case 'KeyP':
      if(G.state==='race'){ G.pausedFrom='race'; G.state='pause'; if(AC) AC.suspend(); }
      else if(G.state==='pause'){ G.state='race'; if(AC) AC.resume(); }
      break;
    case 'Digit1': setRadio(1); break;
    case 'Digit2': setRadio(2); break;
    case 'Digit3': setRadio(3); break;
    case 'Digit4': setRadio(4); break;
  }
});
window.addEventListener('keyup',e=>{ if(KEYMAP[e.code]!==undefined) input[KEYMAP[e.code]]=false; });
window.addEventListener('blur',()=>{ if(G.state==='race'){ G.state='pause'; if(AC) AC.suspend(); } });
document.addEventListener('visibilitychange',()=>{ if(document.hidden&&G.state==='race'){ G.state='pause'; if(AC) AC.suspend(); } });

$('crt').addEventListener('pointerdown',()=>{
  ensureAudio(); input.touch=true;
  if(G.state==='attract') startRace();
  else if(G.state==='results'){ if(G.results&&G.results.t>2.4) nextLevel(); }
  else if(G.state==='over'||G.state==='victory'){ G.state='attract'; G.stageIdx=0; }
});
function bindPad(id,flag){
  const el=$(id);
  const on=e=>{ e.preventDefault(); ensureAudio(); input.touch=true; input[flag]=true; };
  const off=e=>{ e.preventDefault(); input[flag]=false; };
  el.addEventListener('pointerdown',on); el.addEventListener('pointerup',off);
  el.addEventListener('pointerleave',off); el.addEventListener('pointercancel',off);
}
bindPad('padL','left'); bindPad('padR','right'); bindPad('padB','brake'); bindPad('padN','boost');

$('btnSound').addEventListener('click',()=>{ ensureAudio(); toggleMute(); });
$('btnRadio').addEventListener('click',()=>{ ensureAudio(); setRadio(G.radioTrack%4+1); });
$('btnStereo').addEventListener('click',()=>{ ensureAudio(); toggleStereo(); });

/* =====================================================================
   MAIN LOOP
   ===================================================================== */
let lastFrame=performance.now();
function frame(now){
  const dt=clamp((now-lastFrame)/1000,0.001,0.05);
  lastFrame=now;
  if(G.state!=='pause') update(dt);
  /* camera FOV kick while boosting */
  camDepthCur+= ((G.boosting?0.72:CAM_DEPTH)-camDepthCur)*Math.min(1,dt*5);
  compositeFrame();
  drawHUD(dctx);
  drawOverlay(dctx);
  requestAnimationFrame(frame);
}
function init(){
  buildSprites();
  buildScenery();
  G.stageIdx=0;
  buildLevel(0); placeSprites(0); resetCars(0);
  setStagePalette(0,true);
  G.state='attract';
  G.speed=MAX_SPEED*0.55;
  G.totalTime=0; G.totalNM=0;
  updateLeds();
  requestAnimationFrame(frame);
}
if(document.fonts&&document.fonts.load){
  Promise.all([
    document.fonts.load('12px "Press Start 2P"'),
    document.fonts.load('700 16px "Chakra Petch"')
  ]).then(init,init);
} else init();
</script>
</body>
</html><!--kg-card-end: html--><p>If I am to be meticulously critical, besides the obvious sound track, and missing win animations, the biggest difference between this, and the originals is the physics engine, and turning/drifting effects. &#xA0;Both games somehow created an experience that made you lean in your seat, anticipating real G forces. &#xA0;And those programmers were doing it in assembly language.</p><p>Click the box below to download a single HTML file that will open and play in your browser, full screen, without internet, better than embedded in this blog here. &#xA0;You can also open with a text editor, and start changing things:</p>
        <div class="kg-card kg-file-card kg-file-card-medium">
            <a class="kg-file-card-container" href="https://learn.piescale.com/content/files/2026/09/racing90s.html" title="Download" download>
                <div class="kg-file-card-contents">
                    <div class="kg-file-card-title">Racing90s</div>
                    
                    <div class="kg-file-card-metadata">
                        <div class="kg-file-card-filename">racing90s.html</div>
                        <div class="kg-file-card-filesize">82 KB</div>
                    </div>
                </div>
                <div class="kg-file-card-icon">
                    <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"/><line class="a" x1="12" y1="6.75" x2="12" y2="18"/><circle class="a" cx="12" cy="12" r="11.25"/></svg>
                </div>
            </a>
        </div>
        ]]></content:encoded></item><item><title><![CDATA[AI Slop]]></title><description><![CDATA[<p>Will you eat what they feed you?</p><p>By April 2025, it is becoming clear there are going to be two AI tracks:</p><ol><li> Slop brought to you by Microsoft, and Google</li><li>Something zeroing in on pure intelligence, currently lead by Twitter, DeepSeek, and Alibaba</li></ol><p>Intelligence is inherently efficient. &#xA0;It is</p>]]></description><link>https://learn.piescale.com/ai-slop/</link><guid isPermaLink="false">67ebfb1a2f08c600019dc9ab</guid><dc:creator><![CDATA[Josh]]></dc:creator><pubDate>Tue, 01 Apr 2025 14:52:05 GMT</pubDate><media:content url="https://learn.piescale.com/content/images/2025/04/Google-AI-studios-getting-worse.png" medium="image"/><content:encoded><![CDATA[<img src="https://learn.piescale.com/content/images/2025/04/Google-AI-studios-getting-worse.png" alt="AI Slop"><p>Will you eat what they feed you?</p><p>By April 2025, it is becoming clear there are going to be two AI tracks:</p><ol><li> Slop brought to you by Microsoft, and Google</li><li>Something zeroing in on pure intelligence, currently lead by Twitter, DeepSeek, and Alibaba</li></ol><p>Intelligence is inherently efficient. &#xA0;It is going to run on hardware that is currently being turned into e-waste. &#xA0;It is not going to require buildings full of the newest silicon, dedicated power plants, and trillions of dollars.</p><p>The slop pumpers are chained to a religion of maximizing entropy.</p><p>Pure intelligence is going to be way more zen.</p>]]></content:encoded></item><item><title><![CDATA[Quit Microsoft]]></title><description><![CDATA[<p>It&apos;s easier than you think, and the productivity gains alone are worth it.</p><p>Linux Mint is now easier to use than Windows, it&apos;s also faster, more secure, and more stable. &#xA0;OnlyOffice is better than Microsoft Office, and it&apos;s free. &#xA0;You can install</p>]]></description><link>https://learn.piescale.com/quit-microsoft/</link><guid isPermaLink="false">66ee9beb5e569e000122d9fb</guid><dc:creator><![CDATA[Josh]]></dc:creator><pubDate>Sat, 21 Sep 2024 10:47:06 GMT</pubDate><media:content url="https://learn.piescale.com/content/images/2024/09/microsoft-evil.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://learn.piescale.com/content/images/2024/09/microsoft-evil.jpeg" alt="Quit Microsoft"><p>It&apos;s easier than you think, and the productivity gains alone are worth it.</p><p>Linux Mint is now easier to use than Windows, it&apos;s also faster, more secure, and more stable. &#xA0;OnlyOffice is better than Microsoft Office, and it&apos;s free. &#xA0;You can install it on your computer, and use it without internet, or you can install it on a server, and run your own cloud collaboration better than Microsoft 365.</p><p>And you can use this forever without ever paying anyone anything, or being forced to change.</p><p>Microsoft uses security theater to cost you money, and harm our environment by sending computers to an early grave. &#xA0;They are in the business of turning money into new software that has to run on new hardware, and center every business decision around accelerating this.</p><p>But normal people want stability. &#xA0;The only way to achieve stability is to quit the chaos agents at Microsoft.</p>]]></content:encoded></item><item><title><![CDATA[Do you long for boring tech]]></title><description><![CDATA[<p>Whatever happened to stability? &#xA0;Have you ever met a normal person who wanted their software to change after they learned how to use it? &#xA0;How has it become so commonplace, especially among open source projects?</p>]]></description><link>https://learn.piescale.com/do-you-long-for-boring-tech/</link><guid isPermaLink="false">66ee27b25e569e000122d9af</guid><dc:creator><![CDATA[Josh]]></dc:creator><pubDate>Sat, 21 Sep 2024 01:58:52 GMT</pubDate><media:content url="https://learn.piescale.com/content/images/2024/09/Bliss_-Windows_XP-.png" medium="image"/><content:encoded><![CDATA[<img src="https://learn.piescale.com/content/images/2024/09/Bliss_-Windows_XP-.png" alt="Do you long for boring tech"><p>Whatever happened to stability? &#xA0;Have you ever met a normal person who wanted their software to change after they learned how to use it? &#xA0;How has it become so commonplace, especially among open source projects?</p>]]></content:encoded></item></channel></rss>