Models 44
Train snakeOne-line

GLM 5.3 Flash

by Z.aiReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
4 min 55 s
Cost
$0.015
Output tokens
29,191
Spent thinking
20,492 tokens
Speed
99.2 tok/s
Console errors
0
First token after
367 ms
First visible text after
4 min 3 s
Prompt tokens
364
Code size
23.8 KB
Finish reason
stop
Served by
BaseTen
Model id
z-ai/glm-5.3-flash
Reasoning sent
{"effort":"high"}
Max output
64,000 tokens
Generated
Nov 10, 2026

What it was asked

The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 681 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Little Line — a tiny train snake</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    background:#f1e7d3;
    color:#4a3b2a;
    font-family:'Trebuchet MS','Segoe UI',Verdana,sans-serif;
    display:flex;justify-content:center;
    padding:18px 12px 24px;
    overflow-x:hidden;
  }
  .wrap{width:100%;max-width:984px}
  header{display:flex;align-items:center;gap:14px;margin-bottom:12px}
  h1{font-size:30px;letter-spacing:.5px;line-height:1.1}
  .tag{color:#8a7a63;font-size:14px;margin-top:2px}
  .hud{display:flex;gap:10px;margin-bottom:12px;flex-wrap:wrap}
  .chip{
    background:#fbf5e8;border:2px solid #b49b74;border-radius:999px;
    padding:6px 16px;font-size:13px;color:#6b5a44;font-weight:bold;letter-spacing:.4px;
  }
  .chip b{font-size:16px;margin-left:6px;color:#9c4a2f}
  .stage{
    position:relative;border:6px solid #7a5233;border-radius:14px;overflow:hidden;
    background:#a6c98b;line-height:0;touch-action:none;
    box-shadow:0 6px 0 rgba(122,82,51,.35),0 14px 26px rgba(90,60,30,.18);
  }
  canvas{display:block}
  #overlay{
    position:absolute;inset:0;display:none;align-items:center;justify-content:center;
    background:rgba(52,40,26,.45);line-height:1.45;pointer-events:none;padding:12px;
  }
  #overlay.show{display:flex}
  .card{
    background:#fbf5e8;border:3px solid #7a5233;border-radius:16px;
    padding:20px 30px;text-align:center;max-width:88%;
    box-shadow:0 8px 20px rgba(60,40,20,.35);
  }
  .card h2{font-size:26px;margin-bottom:6px;color:#7a3b24}
  .card p{font-size:14px;color:#6b5a44;margin-bottom:5px}
  .card p.small{font-size:12px;opacity:.75;margin-top:10px}
  .card .nb{color:#9c4a2f;font-weight:bold}
  .start{
    display:inline-block;margin-top:10px;background:#9c4a2f;color:#fbf5e8;
    font-weight:bold;padding:10px 22px;border-radius:999px;font-size:16px;
    letter-spacing:.5px;animation:pulse 1.6s ease-in-out infinite;
  }
  @keyframes pulse{50%{transform:scale(1.05)}}
  .hint{text-align:center;color:#8a7a63;font-size:13px;margin-top:12px}
  @media(max-width:600px){
    h1{font-size:22px}
    .tag{font-size:12px}
    .chip{padding:4px 11px;font-size:11px}
    .chip b{font-size:13px}
    .card{padding:14px 18px}
    .card h2{font-size:20px}
    .start{font-size:14px;padding:8px 16px}
  }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <svg viewBox="0 0 34 32" width="40" height="38" aria-hidden="true">
      <rect x="3" y="12" width="17" height="13" rx="2" fill="#a03a28"/>
      <rect x="1" y="10" width="19" height="4" rx="1.5" fill="#5f4632"/>
      <rect x="6" y="15" width="6" height="5" rx="1" fill="#f3ead6"/>
      <rect x="18" y="9" width="12" height="16" rx="2.5" fill="#b5432f"/>
      <rect x="22" y="3" width="4" height="7" rx="1" fill="#3f3a36"/>
      <rect x="20.5" y="1" width="7" height="3" rx="1" fill="#3f3a36"/>
      <circle cx="28" cy="13" r="2" fill="#ffd97a" stroke="#3f3a36"/>
      <circle cx="8" cy="27" r="3" fill="#3a332d"/><circle cx="16" cy="27" r="3" fill="#3a332d"/><circle cx="25" cy="27" r="3" fill="#3a332d"/>
      <rect x="0" y="29" width="34" height="2" rx="1" fill="#6d675e"/>
    </svg>
    <div>
      <h1>The Little Line</h1>
      <div class="tag">a tiny steam train picking up passengers on the meadow line</div>
    </div>
  </header>

  <div class="hud">
    <div class="chip">PASSENGERS<b id="score">0</b></div>
    <div class="chip">BEST<b id="best">0</b></div>
    <div class="chip">SERVICE<b id="lvl">Local</b></div>
  </div>

  <div class="stage" id="stage">
    <canvas id="game"></canvas>
    <div id="overlay"></div>
  </div>

  <p class="hint">Steer with the arrow keys or WASD &nbsp;·&nbsp; P pauses &nbsp;·&nbsp; swipe on touch screens</p>
</div>

<script>
(function(){
'use strict';
var COLS=26, ROWS=16, CELL=36, W=COLS*CELL, H=ROWS*CELL;
var cv=document.getElementById('game'), ctx=cv.getContext('2d');
var stage=document.getElementById('stage'), overlay=document.getElementById('overlay');
var elScore=document.getElementById('score'), elBest=document.getElementById('best'), elLvl=document.getElementById('lvl');

var WAGONS=['#d98f3e','#3f8f7a','#c95f4a','#7b6bb0','#4f86c6','#b56a9a'];
var SHIRTS=['#c95f4a','#3f8f7a','#7b6bb0','#4f86c6','#d98f3e','#b56a9a','#5e8c4a'];
var HATS=['#9c4a2f','#4a3b6b','#2f6b57','#8a6a1f'];
var DIRS=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];

var snake, dir, nextDirs, passenger, score, best=0, tickMs, mode='demo', wasDemo=false;
var tickAcc=0, lastTime=0, dist=0, cars=[], wagonBorn={}, crashTime=0;
var smoke=[], puffs=[], floats=[], emitAcc=0;
var scale=1, dpr=1, bg=null;

/* ---------- audio (created only after a user gesture) ---------- */
var actx=null;
function initAudio(){
  if(actx) return;
  try{ actx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ actx=null; }
}
function tone(f0,f1,dur,type,vol,delay){
  if(!actx) return;
  try{
    var t0=actx.currentTime+(delay||0);
    var o=actx.createOscillator(), g=actx.createGain();
    o.type=type; o.frequency.setValueAtTime(f0,t0);
    o.frequency.exponentialRampToValueAtTime(Math.max(1,f1),t0+dur);
    g.gain.setValueAtTime(vol,t0);
    g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
    o.connect(g); g.connect(actx.destination);
    o.start(t0); o.stop(t0+dur+0.05);
  }catch(e){}
}
function whistle(){ tone(660,640,0.30,'triangle',0.08); tone(880,860,0.30,'triangle',0.06,0.02); }
function pickupSnd(){ tone(520,780,0.10,'square',0.045); tone(780,1040,0.12,'square',0.04,0.08); }
function crashSnd(){ tone(300,55,0.55,'sawtooth',0.09); tone(180,40,0.6,'square',0.05,0.05); }

/* ---------- helpers ---------- */
function rr(x,y,w,h,r){
  r=Math.min(r,w/2,h/2);
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,x+w,y+h,r);
  ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r);
  ctx.arcTo(x,y,x+w,y,r);
  ctx.closePath();
}
function h2(x,y){
  var n=(x*374761393+y*668265263)|0;
  n=((n^(n>>>13))*1274126177)|0;
  return (n^(n>>>16))>>>0;
}
function angLerp(a,b,t){
  var d=((b-a+Math.PI*3)%(Math.PI*2))-Math.PI;
  return a+d*t;
}
function popScale(ms){
  var t=Math.min(1,ms/260);
  var c1=1.70158, c3=c1+1, u=t-1;
  return 1+c3*u*u*u+c1*u*u;
}

/* ---------- background meadow (pre-rendered) ---------- */
function buildBG(){
  bg=document.createElement('canvas'); bg.width=W; bg.height=H;
  var b=bg.getContext('2d');
  for(var y=0;y<ROWS;y++)for(var x=0;x<COLS;x++){
    b.fillStyle=((x+y)&1)?'#a6c98b':'#9fc283';
    b.fillRect(x*CELL,y*CELL,CELL,CELL);
    var h=h2(x,y);
    if(h%11===0){ /* little flower */
      var fx=x*CELL+8+(h>>4)%20, fy=y*CELL+8+(h>>9)%20;
      b.fillStyle='#f7f3e8';
      for(var k=0;k<4;k++){
        var an=k*Math.PI/2+0.4;
        b.beginPath(); b.arc(fx+Math.cos(an)*2.4,fy+Math.sin(an)*2.4,1.7,0,7); b.fill();
      }
      b.fillStyle='#e0a83b';
      b.beginPath(); b.arc(fx,fy,1.5,0,7); b.fill();
    }else if(h%7===3){ /* grass tuft */
      var tx=x*CELL+6+(h>>5)%22, ty=y*CELL+10+(h>>10)%20;
      b.strokeStyle='#8db371'; b.lineWidth=1.4; b.lineCap='round';
      b.beginPath();
      b.moveTo(tx,ty); b.lineTo(tx-2,ty-5);
      b.moveTo(tx+2,ty); b.lineTo(tx+2,ty-6);
      b.moveTo(tx+4,ty); b.lineTo(tx+6,ty-4);
      b.stroke();
    }
  }
}

/* ---------- game setup ---------- */
function resetGame(){
  snake=[]; wagonBorn={}; smoke=[]; puffs=[]; floats=[];
  var cy=Math.floor(ROWS/2), cx=Math.floor(COLS/2)-3;
  for(var i=0;i<3;i++) snake.push({x:cx-i,y:cy,px:cx-i,py:cy});
  dir={x:1,y:0}; nextDirs=[];
  score=0; tickMs=150; tickAcc=0; dist=0; emitAcc=0;
  cars=[0,0,0];
  spawnPassenger();
  updateHUD();
}
function spawnPassenger(){
  var head=snake[0], tries=0;
  while(tries++<600){
    var x=Math.floor(Math.random()*COLS), y=Math.floor(Math.random()*ROWS);
    if(Math.abs(x-head.x)+Math.abs(y-head.y)<4) continue;
    var hit=false;
    for(var i=0;i<snake.length;i++) if(snake[i].x===x&&snake[i].y===y){hit=true;break;}
    if(!hit){ passenger={x:x,y:y}; return; }
  }
  passenger=null;
}
function updateHUD(){
  elScore.textContent=score;
  elBest.textContent=best;
  elLvl.textContent = score<5?'Local' : score<12?'Branch line' : score<20?'Express' : 'Non-stop';
}

/* ---------- demo auto-pilot ---------- */
function flood(cx,cy,cap){
  var blocked={};
  for(var i=0;i<snake.length-1;i++) blocked[snake[i].x+','+snake[i].y]=1;
  var seen={}, q=[[cx,cy]], count=0;
  seen[cx+','+cy]=1;
  while(q.length){
    var cur=q.pop(), x=cur[0], y=cur[1];
    count++;
    if(count>=cap) return count;
    for(var k=0;k<4;k++){
      var nx=x+DIRS[k].x, ny=y+DIRS[k].y;
      if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
      var key=nx+','+ny;
      if(seen[key]||blocked[key]) continue;
      seen[key]=1; q.push([nx,ny]);
    }
  }
  return count;
}
function aiSteer(){
  if(!passenger) return;
  var h=snake[0], cands=[];
  for(var k=0;k<4;k++){
    var d=DIRS[k];
    if(d.x===-dir.x&&d.y===-dir.y) continue;
    var nx=h.x+d.x, ny=h.y+d.y;
    if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
    var bad=false;
    for(var i=0;i<snake.length-1;i++) if(snake[i].x===nx&&snake[i].y===ny){bad=true;break;}
    if(bad) continue;
    var area=flood(nx,ny,snake.length+3);
    var s=-Math.abs(nx-passenger.x)-Math.abs(ny-passenger.y)+area*0.02;
    if(area<snake.length) s-=500;
    cands.push({d:d,s:s});
  }
  if(cands.length){
    cands.sort(function(a,b){return b.s-a.s;});
    dir=cands[0].d;
  }
}

/* ---------- tick ---------- */
function onEat(hx,hy){
  score++;
  tickMs=Math.max(78,150-score*3.2);
  var idx=snake.length-1;
  wagonBorn[idx]=performance.now();
  floats.push({x:(passenger.x+0.5)*CELL,y:passenger.y*CELL,txt:'+1 aboard',life:1});
  burst((passenger.x+0.5)*CELL,(passenger.y+0.3)*CELL,'#f3ead6',8);
  pickupSnd();
  if(score%5===0){
    whistle();
    floats.push({x:(hx+0.5)*CELL,y:(hy-0.6)*CELL,txt:'TOOT!',life:1});
  }
  spawnPassenger();
  updateHUD();
}
function crash(){
  crashSnd();
  burst((snake[0].x+0.5)*CELL,(snake[0].y+0.5)*CELL,'#8b8b8b',14);
  wasDemo=(mode==='demo');
  if(!wasDemo){ best=Math.max(best,score); updateHUD(); }
  mode='crash'; crashTime=0;
}
function doTick(){
  if(mode==='demo') aiSteer();
  while(nextDirs.length){
    var d=nextDirs.shift();
    if((d.x!==-dir.x||d.y!==-dir.y)&&(d.x!==dir.x||d.y!==dir.y)){ dir=d; break; }
  }
  var head=snake[0];
  var nx=head.x+dir.x, ny=head.y+dir.y;
  if(nx<0||ny<0||nx>=COLS||ny>=ROWS) return crash();
  var eat=passenger&&(nx===passenger.x)&&(ny===passenger.y);
  var lim=snake.length-(eat?0:1);
  for(var i=0;i<lim;i++) if(snake[i].x===nx&&snake[i].y===ny) return crash();
  var old=snake.map(function(s){return {x:s.x,y:s.y};});
  snake.unshift({x:nx,y:ny,px:head.x,py:head.y});
  if(eat) onEat(nx,ny); else snake.pop();
  for(var j=0;j<snake.length;j++){
    var s=snake[j];
    if(j===snake.length-1&&eat){ s.px=s.x; s.py=s.y; }
    else{ var o=old[j]||{x:s.x,y:s.y}; s.px=o.x; s.py=o.y; }
  }
  dist+=CELL;
}

/* ---------- effects ---------- */
function burst(x,y,color,n){
  for(var k=0;k<n;k++){
    var a=Math.random()*Math.PI*2, sp=20+Math.random()*45;
    puffs.push({x:x,y:y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-18,r:2.5+Math.random()*3,life:1,col:color});
  }
}
function updateEffects(dtms){
  var i;
  for(i=smoke.length-1;i>=0;i--){
    var s=smoke[i];
    s.x+=s.vx*dtms/1000; s.y+=s.vy*dtms/1000;
    s.r+=dtms*0.018; s.life-=dtms/1400;
    if(s.life<=0) smoke.splice(i,1);
  }
  for(i=puffs.length-1;i>=0;i--){
    var p=puffs[i];
    p.x+=p.vx*dtms/1000; p.y+=p.vy*dtms/1000;
    p.vy+=dtms*0.02; p.life-=dtms/650;
    if(p.life<=0) puffs.splice(i,1);
  }
  for(i=floats.length-1;i>=0;i--){
    var f=floats[i];
    f.y-=dtms*0.032; f.life-=dtms/950;
    if(f.life<=0) floats.splice(i,1);
  }
}

/* ---------- drawing ---------- */
function drawTrack(pts){
  if(pts.length<2) return;
  ctx.lineCap='round'; ctx.lineJoin='round';
  ctx.strokeStyle='#cdb48d'; ctx.lineWidth=24;
  ctx.beginPath(); ctx.moveTo(pts[0].x,pts[0].y);
  for(var i=1;i<pts.length;i++) ctx.lineTo(pts[i].x,pts[i].y);
  ctx.stroke();
  /* sleepers */
  ctx.strokeStyle='#97744f'; ctx.lineWidth=4;
  var acc=4;
  for(var j=0;j<pts.length-1;j++){
    var a=pts[j], b2=pts[j+1];
    var dx=b2.x-a.x, dy=b2.y-a.y, L=Math.hypot(dx,dy)||1;
    var ux=dx/L, uy=dy/L, px=-uy, py=ux;
    var d=acc;
    while(d<L){
      var mx=a.x+ux*d, my=a.y+uy*d;
      ctx.beginPath(); ctx.moveTo(mx-px*9,my-py*9); ctx.lineTo(mx+px*9,my+py*9); ctx.stroke();
      d+=12;
    }
    acc=12-(d-L);
  }
  /* rails */
  ctx.strokeStyle='#6d675e'; ctx.lineWidth=2.2;
  for(var m=0;m<pts.length-1;m++){
    var c1=pts[m], c2=pts[m+1];
    var ddx=c2.x-c1.x, ddy=c2.y-c1.y, LL=Math.hypot(ddx,ddy)||1;
    var qx=-ddy/LL, qy=ddx/LL;
    for(var s=-1;s<=1;s+=2){
      ctx.beginPath();
      ctx.moveTo(c1.x+qx*4.5*s,c1.y+qy*4.5*s);
      ctx.lineTo(c2.x+qx*4.5*s,c2.y+qy*4.5*s);
      ctx.stroke();
    }
  }
}
function wheel(x,y,r,ang){
  ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fillStyle='#3a332d'; ctx.fill();
  ctx.beginPath(); ctx.arc(x,y,r*0.55,0,7); ctx.fillStyle='#9a9184'; ctx.fill();
  ctx.strokeStyle='#3a332d'; ctx.lineWidth=1.1;
  for(var k=0;k<2;k++){
    var an=ang+k*Math.PI/2;
    ctx.beginPath();
    ctx.moveTo(x+Math.cos(an)*r*0.85,y+Math.sin(an)*r*0.85);
    ctx.lineTo(x-Math.cos(an)*r*0.85,y-Math.sin(an)*r*0.85);
    ctx.stroke();
  }
}
function drawWagon(i,now){
  var col=WAGONS[(i-1)%WAGONS.length];
  ctx.fillStyle='#4a3b2a'; rr(14,-2.5,7,5,2); ctx.fill(); /* coupling */
  rr(-15,-11,30,22,5); ctx.fillStyle=col; ctx.fill();
  ctx.lineWidth=2; ctx.strokeStyle='rgba(0,0,0,0.28)'; ctx.stroke();
  rr(-16,-13.5,32,5,2.5); ctx.fillStyle='rgba(0,0,0,0.22)'; ctx.fill();
  var wins=[-10,2];
  for(var w=0;w<2;w++){
    rr(wins[w],-6,8,9,2);
    ctx.fillStyle='#f3ead6'; ctx.fill();
    ctx.lineWidth=1.4; ctx.strokeStyle='rgba(0,0,0,0.22)'; ctx.stroke();
  }
  if(i<=score){ /* a passenger rides here */
    var born=wagonBorn[i]||0;
    var s=popScale(now-born);
    var wcx=wins[0]+4;
    ctx.save();
    ctx.translate(wcx,0); ctx.scale(s,s); ctx.translate(-wcx,0);
    ctx.fillStyle=SHIRTS[(i-1)%SHIRTS.length];
    rr(wcx-3.2,-3.4,6.4,5.6,2); ctx.fill();
    ctx.fillStyle='#ecc19a';
    ctx.beginPath(); ctx.arc(wcx,-4.6,2.7,0,7); ctx.fill();
    ctx.fillStyle=HATS[(i-1)%HATS.length];
    ctx.beginPath(); ctx.arc(wcx,-5.4,2.7,Math.PI,0); ctx.fill();
    ctx.restore();
  }
  wheel(-8,11,4.2,dist/6);
  wheel(8,11,4.2,dist/6+0.6);
}
function drawLoco(now){
  ctx.fillStyle='#4a3b2a'; rr(-22,-2.5,7,5,2); ctx.fill(); /* rear coupling */
  ctx.beginPath(); ctx.moveTo(13,11); ctx.lineTo(22,14); ctx.lineTo(22,3); ctx.closePath();
  ctx.fillStyle='#6e4a3a'; ctx.fill(); /* cowcatcher */
  rr(-18,-13,14,23,3); ctx.fillStyle='#a03a28'; ctx.fill();
  ctx.lineWidth=2; ctx.strokeStyle='rgba(0,0,0,0.28)'; ctx.stroke();
  rr(-20,-15.5,18,5,2); ctx.fillStyle='#5f4632'; ctx.fill();
  rr(-15,-9,8,8,2); ctx.fillStyle='#f3ead6'; ctx.fill();
  ctx.lineWidth=1.4; ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.stroke();
  /* driver */
  ctx.fillStyle='#ecc19a'; ctx.beginPath(); ctx.arc(-11,-4.5,2.5,0,7); ctx.fill();
  ctx.fillStyle='#2f5d8a'; ctx.beginPath(); ctx.arc(-11,-5.4,2.5,Math.PI,0); ctx.fill();
  ctx.fillRect(-13.6,-5.6,5.2,1.2);
  /* boiler */
  rr(-5,-10,19,19,5); ctx.fillStyle='#b5432f'; ctx.fill();
  ctx.lineWidth=2; ctx.strokeStyle='rgba(0,0,0,0.28)'; ctx.stroke();
  ctx.strokeStyle='rgba(0,0,0,0.18)'; ctx.lineWidth=1.5;
  ctx.beginPath(); ctx.moveTo(1,-9); ctx.lineTo(1,8); ctx.moveTo(8,-9); ctx.lineTo(8,8); ctx.stroke();
  /* chimney */
  rr(6,-18,6,8,1); ctx.fillStyle='#3f3a36'; ctx.fill();
  rr(4,-21,10,4,1.5); ctx.fill();
  /* brass dome */
  ctx.beginPath(); ctx.arc(-1,-10,3.2,Math.PI,0); ctx.fillStyle='#d9a441'; ctx.fill();
  ctx.lineWidth=1.2; ctx.strokeStyle='rgba(0,0,0,0.3)'; ctx.stroke();
  /* headlamp */
  ctx.beginPath(); ctx.arc(13.5,-4,3,0,7); ctx.fillStyle='#ffd97a'; ctx.fill();
  ctx.lineWidth=1.4; ctx.strokeStyle='#3f3a36'; ctx.stroke();
  wheel(-11,11,4.8,dist/5.5);
  wheel(-1,11,4.8,dist/5.5+0.5);
  wheel(9.5,11,3.6,dist/5.5);
}
function drawTrain(now,pts){
  for(var i=snake.length-1;i>=0;i--){
    var p=pts[i], a=cars[i];
    ctx.save();
    ctx.translate(p.x,p.y); ctx.rotate(a);
    ctx.fillStyle='rgba(60,50,30,0.16)';
    ctx.beginPath(); ctx.ellipse(0,13.5,15,3.6,0,0,7); ctx.fill();
    if(i===0) drawLoco(now); else drawWagon(i,now);
    ctx.restore();
  }
}
function drawPassenger(now){
  if(!passenger) return;
  var bx=(passenger.x+0.5)*CELL, by=(passenger.y+0.5)*CELL;
  var bob=Math.sin(now/280+passenger.x*1.7)*1.3;
  var ground=by+3;
  rr(bx-15,by+2,30,12,3); ctx.fillStyle='#cfc7b8'; ctx.fill();
  ctx.lineWidth=1.5; ctx.strokeStyle='#a49b88'; ctx.stroke();
  ctx.fillStyle='#efe8da'; ctx.fillRect(bx-15,by+2,30,3);
  ctx.fillStyle='rgba(0,0,0,0.14)';
  ctx.beginPath(); ctx.ellipse(bx,ground+0.5,7,2.2,0,0,7); ctx.fill();
  var hv=h2(passenger.x,passenger.y);
  var shirt=SHIRTS[hv%SHIRTS.length], hat=HATS[(hv>>4)%HATS.length];
  var ty=ground-14+bob;
  /* suitcase */
  rr(bx-14,ground-6,6.5,6,1.5); ctx.fillStyle='#8a5a33'; ctx.fill();
  ctx.strokeStyle='#5f3d20'; ctx.lineWidth=1.2; ctx.stroke();
  /* torso */
  rr(bx-4,ty,8,11,3); ctx.fillStyle=shirt; ctx.fill();
  ctx.lineWidth=1.2; ctx.strokeStyle='rgba(0,0,0,0.2)'; ctx.stroke();
  /* still arm */
  ctx.strokeStyle=shirt; ctx.lineWidth=2.2; ctx.lineCap='round';
  ctx.beginPath(); ctx.moveTo(bx-3.5,ty+3); ctx.lineTo(bx-6.5,ty+8); ctx.stroke();
  /* waving arm */
  var wa=-1.9+Math.sin(now/160)*0.55;
  var hx2=bx+4+Math.cos(wa)*7, hy2=ty+2+Math.sin(wa)*7;
  ctx.beginPath(); ctx.moveTo(bx+3.5,ty+3); ctx.lineTo(hx2,hy2); ctx.stroke();
  ctx.fillStyle='#ecc19a'; ctx.beginPath(); ctx.arc(hx2,hy2,1.7,0,7); ctx.fill();
  /* head + cap */
  ctx.fillStyle='#ecc19a'; ctx.beginPath(); ctx.arc(bx,ty-4,3.6,0,7); ctx.fill();
  ctx.lineWidth=1; ctx.strokeStyle='rgba(0,0,0,0.18)'; ctx.stroke();
  ctx.fillStyle=hat; ctx.beginPath(); ctx.arc(bx,ty-5,3.7,Math.PI,0); ctx.fill();
  ctx.fillRect(bx-4.4,ty-5.3,8.8,1.4);
}
function drawSmoke(){
  for(var i=0;i<smoke.length;i++){
    var s=smoke[i];
    ctx.fillStyle='rgba(248,246,240,'+(0.72*s.life).toFixed(3)+')';
    ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,7); ctx.fill();
  }
  for(i=0;i<puffs.length;i++){
    var p=puffs[i];
    ctx.globalAlpha=Math.max(0,p.life);
    ctx.fillStyle=p.col;
    ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill();
  }
  ctx.globalAlpha=1;
}
function drawFloats(){
  ctx.textAlign='center'; ctx.textBaseline='middle';
  ctx.font='bold 15px "Trebuchet MS",sans-serif';
  for(var i=0;i<floats.length;i++){
    var f=floats[i];
    ctx.globalAlpha=Math.max(0,Math.min(1,f.life*1.4));
    ctx.lineWidth=3.5; ctx.strokeStyle='rgba(251,245,232,0.9)';
    ctx.strokeText(f.txt,f.x,f.y);
    ctx.fillStyle='#4a3b2a'; ctx.fillText(f.txt,f.x,f.y);
  }
  ctx.globalAlpha=1;
}

/* ---------- overlay ---------- */
function showOverlay(html){ overlay.innerHTML=html; overlay.classList.add('show'); }
function hideOverlay(){ overlay.classList.remove('show'); }
function idleOverlay(){
  showOverlay('<div class="card"><h2>The Little Line</h2>'+
    '<p>A little steam train, a string of colourful coaches, and passengers waiting on tiny platforms.</p>'+
    '<p>Steer the engine, collect every waiting passenger — but never drive off the meadow or into your own carriages.</p>'+
    '<span class="start">Click or press a key to start</span>'+
    '<p class="small">The auto-pilot is warming up the engine meanwhile…</p></div>');
}
function overOverlay(){
  var nb=(score>=best&&score>0);
  showOverlay('<div class="card"><h2>Derailed!</h2>'+
    '<p>You delivered <b class="nb">'+score+'</b> passenger'+(score===1?'':'s')+' on this run.</p>'+
    (nb?'<p class="nb">A new best!</p>':'<p>Best so far: '+best+'</p>')+
    '<span class="start">Click or press a key to run again</span></div>');
}
function pausedOverlay(){
  showOverlay('<div class="card"><h2>Paused</h2><p>Press P (or click) to keep rolling</p></div>');
}

/* ---------- control ---------- */
function startGame(){
  initAudio();
  resetGame();
  mode='play';
  hideOverlay();
  whistle();
}
function togglePause(){
  if(mode==='play'){ mode='paused'; pausedOverlay(); }
  else if(mode==='paused'){ mode='play'; hideOverlay(); }
}
var KEYMAP={ArrowUp:0,ArrowDown:2,ArrowLeft:1,ArrowRight:3,w:0,s:2,a:1,d:3,W:0,S:2,A:1,D:3};
window.addEventListener('keydown',function(e){
  initAudio();
  if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].indexOf(e.key)>=0) e.preventDefault();
  if(mode==='demo'||mode==='over'){ startGame(); return; }
  if(mode==='paused'){ if(e.key==='p'||e.key==='P') togglePause(); return; }
  if(mode==='play'){
    if(e.key==='p'||e.key==='P'){ togglePause(); return; }
    var idx=KEYMAP[e.key];
    if(idx!==undefined) queueDir(DIRS[idx]);
  }
});
function queueDir(d){
  var last=nextDirs.length?nextDirs[nextDirs.length-1]:dir;
  if(d.x===last.x&&d.y===last.y) return;
  if(d.x===-last.x&&d.y===-last.y) return;
  if(nextDirs.length<3) nextDirs.push(d);
}
stage.addEventListener('click',function(){
  initAudio();
  if(mode==='demo'||mode==='over') startGame();
  else if(mode==='paused') togglePause();
});
var tS=null;
stage.addEventListener('touchstart',function(e){
  initAudio();
  if(e.touches.length) tS={x:e.touches[0].clientX,y:e.touches[0].clientY};
},{passive:true});
stage.addEventListener('touchmove',function(e){ e.preventDefault(); },{passive:false});
stage.addEventListener('touchend',function(e){
  if(!tS) return;
  var t=e.changedTouches[0];
  var dx=t.clientX-tS.x, dy=t.clientY-tS.y; tS=null;
  if(Math.hypot(dx,dy)<22) return; /* taps are handled by click */
  if(mode==='demo'||mode==='over'){ startGame(); return; }
  if(mode!=='play') return;
  queueDir(Math.abs(dx)>Math.abs(dy) ? (dx>0?DIRS[0]:DIRS[1]) : (dy>0?DIRS[2]:DIRS[3]));
});

/* ---------- sizing ---------- */
function resize(){
  var avail=stage.clientWidth||W;
  scale=Math.min(avail/W,1.03);
  dpr=window.devicePixelRatio||1;
  cv.width=Math.round(W*scale*dpr);
  cv.height=Math.round(H*scale*dpr);
  cv.style.width=(W*scale)+'px';
  cv.style.height=(H*scale)+'px';
}
window.addEventListener('resize',resize);

/* ---------- main loop ---------- */
function updateCars(pts,dt){
  while(cars.length<snake.length) cars.push(cars.length?cars[cars.length-1]:0);
  while(cars.length>snake.length) cars.pop();
  var k=Math.min(1,dt/90);
  for(var i=0;i<pts.length;i++){
    var tx;
    if(i===0) tx=Math.atan2(pts[0].y-pts[1].y,pts[0].x-pts[1].x);
    else tx=Math.atan2(pts[i-1].y-pts[i].y,pts[i-1].x-pts[i].x);
    cars[i]=angLerp(cars[i],tx,k);
  }
}
function emitSmoke(dt,now){
  emitAcc+=dt;
  while(emitAcc>115){
    emitAcc-=115;
    var a=cars[0], lx=9, ly=-21;
    var wx=pts[0].x+lx*Math.cos(a)-ly*Math.sin(a);
    var wy=pts[0].y+lx*Math.sin(a)+ly*Math.cos(a);
    smoke.push({
      x:wx,y:wy,
      vx:-dir.x*16+(Math.random()-0.5)*10,
      vy:-28+(Math.random()-0.5)*8,
      r:2.5+Math.random()*2, life:1
    });
    if(smoke.length>60) smoke.shift();
  }
}
var pts=[];
function frame(now){
  requestAnimationFrame(frame);
  var dt=Math.min(100,now-lastTime||16); lastTime=now;
  if(mode==='play'||mode==='demo'){
    tickAcc+=dt;
    var guard=0;
    while(tickAcc>=tickMs&&guard++<4&&mode!=='crash'){
      tickAcc-=tickMs;
      doTick();
    }
  }else if(mode==='crash'){
    crashTime+=dt;
    if(crashTime>1150){
      if(wasDemo){ resetGame(); mode='demo'; }
      else{ mode='over'; overOverlay(); }
    }
  }
  var t=Math.max(0,Math.min(1,tickAcc/tickMs));
  pts=snake.map(function(s){
    return {x:(s.px+(s.x-s.px)*t+0.5)*CELL, y:(s.py+(s.y-s.py)*t+0.5)*CELL};
  });
  updateCars(pts,dt);
  if((mode==='play'||mode==='demo')&&!document.hidden) emitSmoke(dt,now);
  updateEffects(dt);

  ctx.setTransform(scale*dpr,0,0,scale*dpr,0,0);
  ctx.clearRect(0,0,W,H);
  ctx.drawImage(bg,0,0);
  if(mode==='crash'){
    var k=Math.max(0,1-crashTime/650);
    ctx.translate((Math.random()-0.5)*7*k,(Math.random()-0.5)*7*k);
  }
  drawTrack(pts);
  drawPassenger(now);
  drawTrain(now,pts);
  drawSmoke();
  drawFloats();
}

/* ---------- boot ---------- */
buildBG();
resize();
resetGame();
idleOverlay();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (20,492 tokens)
Let me build a snake game where the snake is a little train: locomotive head + colorful wagons following the body path, and food items are passengers waiting on platforms. Need self-playing demo mode behind "Click or press a key to start". Single HTML file, canvas-based, keyboard controls (arrows/WASD), also clickable on-screen d-pad maybe for mobile. No external assets.

Design: a charming little railway world — I'll avoid generic gradients/emoji; draw everything on canvas with careful shapes. Let me make it look hand-crafted: a board with grass tracks, the train path rendered as rails connecting segments, locomotive with chimney, wheels, smoke puffs; wagons in varied colors; passengers as small figures waiting on a little platform that spawns.

Core mechanics:
- Grid snake, e.g. 24×18 cells at 32px.
- Move on timer with tick; smooth interpolation would be nice but keep simple — actually smooth movement adds a lot of charm. I'll interpolate positions between ticks for silky motion. That's more work but makes the train feel like it glides. Let's do it: keep positions of segments as cells, and render with lerp between previous and current positions based on time since last tick.
- Passengers spawn on "platforms": instead of plain dots, draw a small platform (gray slab) with a tiny passenger character (colored dot head + body) standing on it. When eaten, passenger boards: maybe wagon count grows, brief "+1" animation, confetti-ish puff? Keep tasteful: a little heart or steam puff.
- Score = passengers carried; wagons = length. Show score top-left, speed increases slowly.
- Death: hitting walls or self → train derails animation (tilt) + game over overlay with restart. Demo mode: AI plays using a safe greedy pathfinding (prefer moving toward passenger, avoid collisions, avoid dead ends with simple lookahead).
- Start overlay: "Click or press a key to start" — demo behind it. On start, reset game with player control.

Controls: arrows/WASD, prevent reversing. Swipe for touch maybe skip; add on-screen buttons for mobile small. Keep minimal but functional: I'll add touch swipe support.

Visual style: warm cream background page, deep green field, tracks drawn as two rails with sleepers along the train's path? Drawing rails along the body path: for each pair of consecutive segments draw a rail line (dark brown with ties). Actually the snake body itself is the train; drawing track under it is charming. I'll draw track sleepers under each segment pair.

Locomotive drawing: rounded rectangle body in deep red/rust, chimney with smoke puffs animated, cab window, cowcatcher front, wheels (circles with darker rim), headlamp. Wagons: varied palette (mustard, teal, coral, plum), each with wheels and a little passenger visible in the open top once picked up? Nice: wagons carry passengers — show tiny head circles in wagon. Each wagon shows a passenger peeking.

Smoke: particles emitted from chimney drifting up-left opposite of direction... just drift up, fade. Store particle list.

Path rendering: to render train smoothly, keep array of segment centers (in pixels) — for interpolation I need per-segment previous and current cell. Standard technique: each segment stores x,y and px,py; on tick shift. Then lerp with t = timeSinceTick/tickInterval.

For drawing train body as connected chain: compute interpolated points for all segments, then draw a thick rounded "track" beneath: stroke path with width ~ cell*0.6 in brown for ballast? Simpler: draw under-track as dark stroke plus ties. Then draw wagons as rounded rects oriented along direction of travel (angle from neighbor). Each wagon rotated toward movement direction. Wheels drawn at corners.

Locomotive at head: draw engine shape rotated by direction angle. Head direction = from second point to head point.

Eating animation: passenger hops onto train — spawn a small tween: passenger figure flies from platform to nearest wagon, then wagon shows occupant. Simpler: on eat, show a floating "+1" text and a puff; wagon occupancy count = min(length-1, ...) — just show a passenger in every wagon. Fine.

Self-collision: ignore tail cell that will move (classic). Walls kill.

Grid: I'll do a slightly non-square? Keep square cells 32, cols 26, rows 16? Board 1280 wide viewport... I'll size canvas responsive: fit container, cell size computed. Board 20×14? Let's compute dynamically: cell = floor(min(w/26, h/16)). Keep fixed logical grid 26×16 and scale canvas via CSS with devicePixelRatio for crispness.

Platforms: spawn passenger at random free cell. Draw platform slab: light gray rounded rect slightly larger than cell? Within cell: draw platform (stone slab) + passenger: head (skin tone circle), body (colored shirt), maybe waving hand animation (small arm oscillating). Cute.

Sound: skip (no assets). Maybe tiny WebAudio chime on pickup — allowed since inline. WebAudio is fine, no network. Add a soft "toot" and pickup blip using oscillators. Must resume audio context after user gesture; demo mode muted until interaction. I'll init on first input.

Game over: overlay with score, "Press any key to play again". Also pause (P / space)? Add space to pause maybe not necessary. Keep P to pause.

Demo AI: each tick, choose direction among 4 that: doesn't reverse, doesn't collide (walls/body excluding tail), maximize progress toward nearest passenger; tie-break safe. Add flood-fill safety check: prefer moves where reachable area from that cell >= snake length (flood fill count limited). Compute flood fill from candidate cell over grid minus body. If all moves unsafe, pick any safe. This gives decent demo play. Limit flood fill to snake length+2 for speed.

Speed: start tick 150ms, decrease to min 80ms as score grows.

Colors: page bg cream #f6f1e3? Field: soft green #9ec97f... Let me pick a palette: board background alternating grass tones #a8cf8e / #9fc686 (subtle checker). Frame: wood brown. Header with title "Little Line — Tiny Train Snake". Typography: use a Google font? External fonts blocked except CDN allowed by task — "No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task." Task doesn't allow fonts. So use system fonts but avoid making it look default-y... I'm told not to use Arial/system-ui as primary identity font, but no external fonts allowed. I can use a font stack with character: "Georgia"? For a playful game, maybe use canvas-drawn text with rounded feel. I can use font stack: 'Trebuchet MS', 'Verdana' fallback — Trebuchet has charm. Or use letter-spaced uppercase Georgia for title. I'll style with Trebuchet MS and Georgia mix.

Page layout: centered game card with title, scoreboard (passengers, best in-memory only — localStorage blocked; keep session best in variable), canvas, footer hint of controls. No horizontal scrollbar; mobile scale.

Canvas crispness: set canvas width=cols*cell*dpr etc.

Let me now write code carefully.

Structure:

```js
const COLS=26, ROWS=16;
let cell; // computed
state: snake array of {x,y,px,py}, dir, nextDir queue, passenger {x,y,phase}, score, best, tickMs, acc time, mode: 'demo'|'play'|'over'|'ready'
```

Ready state: demo running, overlay "Click or press a key to start". When input: switch to play, reset game. On death in play: overlay game over, press key → back to play (reset). In demo death: auto restart demo after brief pause.

Input handling: keydown → if mode ready/over → start game; else set dir. Also click → same.

Tick logic:
- dir = nextDir if valid.
- newHead = head+dir; check wall/self → crash.
- unshift head; if eat passenger: score++, speed up, spawn new passenger, boardingAnim; else pop tail.
- set px,py = previous positions: before moving, record old positions. Implementation: for each segment store prev = current before shift. Easy: on tick, iterate from tail to head: seg.px=seg.x... Actually simplest: keep snake as array; before mutation, oldSnake = snake.map(clone); after mutation set each new snake[i].px/py from matching old index: new snake[i] corresponds to old snake[i-1] (body follows). For i>0, new seg position = old snake[i-1] position, and its prev = old snake[i] position. Head: new pos = newHead, prev = old head. So:

```js
const old = snake.map(s=>({x:s.x,y:s.y}));
snake.unshift({x:hx,y:hy,px:old[0].x,py:old[0].y});
if(!ate) snake.pop(); else ...
for(let i=1;i<snake.length;i++){ snake[i].px = old[i-1].x?? ...}
```

Wait after unshift, snake[1] is old head. Its prev should be old head's position = its current? Hmm. Interpolation: each segment moves from prev cell to current cell during tick. Segment i (i≥1) current cell = old[i-1]; it should have been at old[i] before. So set snake[i].px = old[i].x, py = old[i].y — but if we popped the tail, old array still has it. When eating, length grows: new tail = old tail stays; its prev = same cell (no movement) — set px=x. Let me just do:

```js
for (let i=0;i<snake.length;i++){ const s=snake[i]; const o = old[i-1] !== undefined ? ... }
```

Cleaner: after building new snake array with positions, for each i: prev = (i === snake.length-1 && grew) ? current : (old[i] ? old[i] : current). Actually mapping: new[i] corresponds to old[i-1]? No — new[i] position equals old[i-1] position (for i≥1). The segment that is now at index i was previously at index i (old[i]) before shifting... conceptually segments move forward. So segment at new index i moved from old[i] to old[i-1] (= its new position). For the new head (i=0): from old[0] to newHead. For new tail when grew (i=len-1): it's the old tail which didn't move: prev = its position.

So: after mutation, for i in 0..len-1: if (i===len-1 && grew) s.px=s.x, s.py=s.y; else { const o = old[i] || s; s.px=o.x; s.py=o.y; } where for i=0, old[0] is old head. Good. Edge: when not grew, len new = old len, old[i] exists for all i. When grew, new len = old+1; old[i] exists for i<newLen-1. Good.

Rendering (each animation frame):
1. Compute t = clamp((now - lastTick)/tickMs, 0, 1). Ease maybe linear for constant train speed (linear better).
2. Draw background grass checker + subtle border.
3. Draw tracks under whole body: build path of interpolated points from tail to head; stroke wide dark brown (ballast) width cell*0.7, then lighter rail lines: two strokes offset perpendicular? Simplify: stroke width cell*0.62 color #8a6a4a (sleepers? draw ties as short perpendicular rects along path at intervals). Then draw two thin rails: stroke the same path twice with lineWidth 2 offset by ±railGap perpendicular — offsetting a polyline is complex. Alternative: draw ties (perpendicular small rects) along path, then one dark stroke thin center for rails pair look... Simpler acceptable: ballast stroke (tan), tie marks, then two thin dark lines by stroking path with lineWidth 4 dark then lineWidth 1.6 center? Eh.

Better approach: for each pair of consecutive interpolated points, draw ties: at the midpoint draw perpendicular line width; and draw rails as two strokes computed by offsetting each segment perpendicular — since segments are axis-aligned (grid moves), perpendicular offset is trivial! All path points between ticks move axis-aligned; corners have two points at same cell? Interpolated points: each segment center. Consecutive segments are adjacent cells, so vector is axis-aligned mostly (except during corner transition where one segment moves x and neighbor moves y → diagonal). Hmm, during a turn, adjacent interpolated centers can be diagonal briefly. Perpendicular offset per segment still fine: compute unit perp of each segment vector, offset ±3px, draw line. Small visual glitch at corners acceptable, or use round joins. I'll draw the rail as: for each consecutive pair (p[i], p[i+1]) with i from tail..head-? Actually draw from head to tail; offset lines look like two rails. Plus ties every fixed distance: walk along path accumulating distance, every cell*0.5 place a tie (perpendicular rect). Good enough.

Alternatively skip separate rails: draw dark stroke width cell*0.55 (the "track bed") then two light lines via the per-segment offset method. I'll implement offset rail drawing with round caps.

4. Draw passenger platform at passenger cell: slab, sign, passenger figure with waving arm (sin(time)). 
5. Draw train: iterate body from tail to head-? Draw wagons for segments 1..len-1, locomotive for head. Each segment i: pos = interp point; angle = direction toward next segment toward head (i.e., atan2 of (p[i-1]-p[i]) for i≥1; for head use dir vector interpolated). Draw rotated.

Wagon drawing (in local coords, centered, facing +x direction of travel): body rounded rect width cell*0.8, height cell*0.66, color from palette by index; roof line; two wheel pairs (black circles with gray hub) at bottom corners; if occupied (score >= index) draw a little passenger head in window. Wagon windows: 2 small windows.

Locomotive (local coords, facing +x): 
- boiler: rounded rect front portion, cab at rear, chimney near front top, dome, headlamp at front, cowcatcher triangle at front bottom, wheels: 2 big + 1 small, smokestack. Colors: deep red #b5432f body, dark roof, brass dome #d9a441.
- Smoke particles emitted at chimney world position each ~90ms while moving: puff circles rising, expanding, fading, slight drift opposite travel dir.

Direction of drawing: angle from segment's previous-to-current movement? Better: angle = direction from point i+1 (behind) to point i (ahead)? For segment i, the train moves head-ward; the segment's facing = direction from p[i+1] to p[i]... wait tail to head: point i+1 is closer to tail. Facing (front of that car) = toward head = from p[i+1] to p[i]? For wagon i (i≥1): front points toward head, so vector = p[i-1] - p[i]... Hmm p[i-1] is nearer head. Yes angle = atan2(p[i-1].y - p[i].y, p[i-1].x - p[i].x). For head (i=0): vector = head - p[1] but better use actual current dir interpolated: vector = p0 - p1 works too. Use p0 - p1 for all, with i=len-1 tail using p[len-1]-p[len-2] reversed... for tail, front toward head = p[len-2] - p[len-1]. So general: for i<len-1: v = p[i] - p[i+1]; for tail i=len-1: v = p[i-1] - p[i] reversed sign... wait that's also "toward head": p[len-2]-p[len-1]. And for i<len-1, toward head is p[i-1]-p[i], not p[i]-p[i+1]. Let me recheck: index 0 = head. Head front direction = movement direction = p[0]-p[1] (since head moves away from neck). Yes head front = p0 - p1. For wagon i: its front toward head = direction from its position toward segment ahead (i-1) = p[i-1] - p[i]. For i≥1 that's fine. Tail i=len-1: p[len-2]-p[len-1]. Same formula works for all i≥1: v = p[i-1]-p[i]... but during interpolation p[i-1]-p[i] can shrink/point oddly at corners; acceptable, add angle smoothing: store smoothed angle per segment, lerp angles carefully (wrap). Simpler: compute angle, then for rendering use it directly; short glitches during turns are okay-ish but could look janky. Alternative: keep per-segment smoothed angle: each frame, target angle; smooth via shortest-arc lerp with factor min(1, dt*10). I'll do smoothing for nicer corners.

Actually another classic approach: draw the train as one continuous rounded body along the path (like a thick stroke) with stripes, plus wheels and details. A thick stroke with round caps following the path looks like a nice train worm, but loses locomotive/wagon distinctness. I prefer distinct cars. I'll do rotated cars with angle smoothing.

Car positions: to avoid overlaps at corners, spacing = cell; cars of length ~0.8 cell rotate around center — fine.

Passenger boarding effect: when eaten, spawn floating text "+1" at cell, and a puff. Also increment score. Also small chance passenger types: hat color variation.

Score UI: draw inside canvas top-left as small wooden sign? I'll do HTML header above canvas: "Passengers: N · Best: M · Speed". Cleaner.

DPR handling: canvas.style width = cssW; canvas.width = cssW*dpr; ctx.scale(dpr,dpr).

Resize: recompute cell from container width and ROWS/COLS aspect. Board aspect 26:16 = 1.625. Available width = min(container, 900?) Let's target max width ~ 960px, height fits 800 viewport: header ~90px, canvas height = w/1.625. If w=920 → h=566, plus header ~ 60 + footer 40 → ~700, fine within 800. On mobile 375: w=343 → h=211, fine.

Cell size float ok since we scale ctx: set logical size cols*CELL const (e.g., CELL=36 logical, canvas logical 936×576), then CSS scale to fit. Simplest: fixed logical cell=36, canvas logical size 936×576, CSS width 100% max 960. Use ctx.setTransform(dpr*ratio...). Let me do: displayW = container width (max 960); scale = displayW/936; canvas.width = 936*scale*dpr; ctx.setTransform(scale*dpr,0,0,scale*dpr,0,0). Height = 576*scale. Good, no recompute of game coords.

Fonts in canvas: use `600 20px 'Trebuchet MS', sans-serif`.

Game over: derailing — on crash, set mode 'crash', animate train pieces offset/rotation for ~900ms then show overlay. Simple: shake + rotate whole canvas slightly, tint red? Keep simple: shake canvas transform + "Oh no!" then overlay. I'll do: on crash, crashTimer=0; during crash mode, apply small random offsets decreasing, train cars scatter slightly (add random offsets per car growing). Then overlay overlay "Game over — You delivered N passengers".

Overlay design: HTML absolutely positioned over canvas, cream card, headline, key hint. Demo mode overlay: title "Little Line", subtitle "a tiny train snake", "Click or press a key to start", plus note "watching the auto-pilot". Overlay semi-transparent so demo visible behind.

Pause: 'p' toggles pause in play mode with overlay "Paused".

WebAudio: function beep(freq,dur,type,gain). toot on start (two-tone whistle), blip on pickup (ascending), sad trombone-ish on crash (descending). Guard with try/catch and audioCtx created on first gesture. Demo mode: also allow sounds? Autoplay policies block until gesture; create ctx lazily on first user gesture; in demo before gesture, skip sound.

Passenger spawn: random free cell not on snake, not too close to head (dist > 3). Keep list of 1 passenger at a time. Maybe occasionally 2 passengers? Keep 1.

Also nice: every 5 passengers, a "station bell" and speed increases; show speed as "Express Lv".

Tail direction for tail car angle handled.

Wheels animation: rotate wheel spokes based on distance traveled — keep global distance counter; wheel rotation = dist/r. Draw wheel as circle with 2 spoke lines rotated. Nice touch, cheap.

Smoke: emitted from loco chimney: compute world pos of chimney (local point (−?) rotated). Emit every 120ms of movement: particle {x,y,vx,vy,life,size}. Update & draw as soft circles rgba(90,80,75,alpha). Also bigger puff on pickup.

Passenger waiting animation: bobbing, arm wave: draw simple figure: body capsule, head circle, waving arm line rotating. Also a small signpost "STOP" or station name? Draw platform: rounded gray slab wider than cell? Keep within cell ±4.

Demo AI details:

```js
function aiDir(){
  const head=snake[0];
  const target=passenger;
  const dirs=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
  let best=null,bestScore=-Infinity;
  for(const d of dirs){
    if(d.x===-dir.x&&d.y===-dir.y) continue;
    const nx=head.x+d.x, ny=head.y+d.y;
    if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
    if(hitsBody(nx,ny,true)) continue; // tail cell will move, allow
    // flood fill safety
    const area=flood(nx,ny);
    let s = -(Math.abs(nx-target.x)+Math.abs(ny-target.y));
    if(area < snake.length) s -= 1000; // risky
    s += area*0.01; // prefer open space slightly
    // straightness bonus small
    if(bestScore<s){bestScore=s;best=d;}
  }
  return best || dir; // if none, keep (will crash → demo restarts)
}
```

hitsBody(nx,ny, ignoreTail): check snake cells except last (which moves) — but careful when growing... fine.

flood: BFS from (nx,ny) on grid with body blocked (excluding tail), count up to cap (snake.length+4). Use array visited, queue.

Demo runs same tick loop; on crash in demo: show brief "derailed" and auto-restart demo after 1.2s (mode stays 'ready' with demoActive true).

Mode management: 
- state 'idle': demo playing, overlay start visible.
- on gesture: startGame() → mode 'play', overlay hidden, whistle.
- crash in play → mode 'over', overlay game over; key/click → startGame().
- crash in demo (idle) → demoRespawn timer.

Overlay content changes per mode. Use single overlay div, update innerHTML.

Also on start, reset snake: length 4 centered, dir right, spawn passenger.

Let me also add subtle grass texture: checkerboard two greens + tiny flowers? Small dots occasionally (deterministic via hash of cell) — cute flowers on some cells (3px white/yellow dot clusters). Deterministic pseudo-random from cell coords.

Board border: rounded wooden frame via CSS on canvas container (border 6px solid #7a5233, border-radius). Plus inner shadow.

Header: "🚂"? No emoji. Title text "The Little Line" with a small CSS train? Keep text + a small inline SVG loco icon? Inline SVG allowed (self-contained). I'll add a tiny inline SVG of a train for the header — nice touch. Or draw title purely typographic. I'll do a small inline SVG locomotive glyph.

HUD: three stat chips: Passengers, Best, Speed. Styled cream chips with brown border.

Footer: control hints "Arrows / WASD to steer · P to pause". On touch: swipe.

Touch: touchstart/touchend compute swipe direction; also tap = start when idle/over.

Prevent arrow scroll: e.preventDefault on arrows/space.

Now sizes: CELL=36, COLS=26 → 936; ROWS=16 → 576. Snake speed tick 150→80ms.

Wagon palette: ['#e0a33b' mustard, '#3f8f7a' teal, '#d96a4f' coral, '#7b6bb0' plum, '#4f86c6' blue... assign wagonColors[i % n].

Occupant: wagon i has passenger if score >= i? Wagons count = snake.length-1. Occupants = number of passengers picked = score, capped at wagons. Show occupant in wagons 1..min(score, wagons). Actually each pickup adds one passenger → the newest fills next wagon. Wagons beyond score show empty seats (windows only).

Headcar drawing details (local coords, facing +x, size ~ cell*0.86 long, cell*0.7 tall):

```
- cowcatcher: path from front bottom angled
- boiler: rect from x=-6..20, y=-14..8 (w26 h22) rounded, color red
- cab: rect x=-20..-4, y=-18..10 taller, red darker roof
- chimney: rect at x=12..18, y=-24..-14 with flared top
- dome: small brass semicircle at x=2
- headlamp: circle at front x=20,y=-2 warm yellow with glow ring
- wheels: big at (-12,10),(2,10), small front (16,10)
- buffer/front coupling
```

Coordinates: center at (0,0), car spans roughly x∈[-20,20], y∈[-24,14]. Height ~ cell*0.65=23 → car height ~ 30px total; cell=36 leaves gaps. Let me size: car body height 24, length 30. Wheels radius 5 below.

Also draw couplings: small dark link between cars — the rails stroke underneath connects visually; skip.

Track drawing beneath: stroke path tail→head with:
1. lineWidth cell*0.66, color '#c9b28a' (ballast) — maybe too wide overlapping grass nicely, round joins.
2. ties: walk path, every 10px draw perpendicular line width cell*0.6, length? tie = line perpendicular, lineWidth 3, color '#8a6a4f', length cell*0.5.

Hmm ties across whole path each frame — path length ~ len*cell, len up to ~50 → fine.

3. rails: two offset strokes: for each segment (p[i]→p[i+1]) compute perp unit, draw line offset ±3.2, lineWidth 2, color '#6b4f3a' or steel '#5b5b5f'. Use metallic gray '#7d7d84'.

Ballast color on grass: '#b89b74' tan. Good.

Order: grass → ballast+ties+rails → platform/passenger → smoke behind train? Smoke above train: draw train then smoke (smoke rises above). Draw smoke after train.

Floating texts: {x,y,text,life} rising.

Passenger eaten effect: also small heart? Keep puff + "+1 aboard".

HUD update in DOM each pickup.

Best score: keep variable (session).

Edge: passenger spawn under overlay fine.

Write the CSS: body cream #f4ead8? Let's go warm paper #f3ecdd, text brown #4a3b2a. Card bg #fbf6ea with brown border. Title font Georgia italic? I'll use "Trebuchet MS" bold for title, letterspaced small-caps subtitle.

Noise: no gradients as decoration — solid colors, borders, shadows ok.

Let me write the code now.

Also handle visibility: pause loop when hidden? Not needed; dt clamp to avoid jumps: dt = min(now-last, 100).

Angle smoothing: carAngles[i] smoothed toward target each frame: 

```js
function angleLerp(a,b,t){ let d=((b-a+Math.PI*3)%(Math.PI*2))-Math.PI; return a+d*t; }
```

Store angles array sized to snake length; when snake grows, push tail angle = last.

For head angle target: atan2(p0-p1). For others: atan2(p[i-1]-p[i]) — wait for i≥1 that's direction toward head. Hmm but at spawn all in a row → angle 0 for all. Good.

Edge: right after turning, p[i-1]-p[i] for the car behind the corner is diagonal briefly — smoothing fixes.

Crash animation: mode 'crash': crashT accumulates; each car gets offset = dirPerp * sin? Simple: shake whole board: ctx.translate(rand*3*(1-k)) and cars jitter; steam burst particles; after 1s → show over overlay (mode 'over' but keep drawing last frame behind). During 'over', keep animating smoke/passenger? Freeze game ticks but keep render loop with t frozen at crash moment. I'll freeze interpolation t at value when crashed.

Implement crash: on collision, set mode='crash', crashTime=0, store frozen t=interpolated positions? Simply stop ticking; render continues with t=1? The head moved into wall visually? We reject the move, so snake stays. Show bump: shake + puff at head. Fine.

Sounds:
- whistle: two oscillators (600 & 800 Hz) triangle, 0.25s, gain envelope.
- pickup: square blip 660→880 quick.
- crash: sawtooth 200→60 down 0.5s.
- new passenger spawn: soft tick? skip.

Volume low (0.05-0.12).

Demo AI: also avoid reversing; when no passenger (shouldn't happen), wander.

Let me make sure demo AI doesn't die too fast: flood fill check with cap = snake.length makes it decent. Also add: don't pick a move into a corridor narrower than needed — flood handles it.

One more nicety: passenger has a little suitcase (brown square beside). Cute.

Also draw destination station? Skip.

Platform: when passenger exists, draw slab rounded rect covering cell inset 2, height 60% at bottom? Draw platform as horizontal slab across cell bottom half: rect(x+3, y+cell*0.45, cell-6, cell*0.4) gray #b9b3a6 with darker edge, plus passenger standing on top (upper half). Passenger: head r4 skin #e8b98a, hat? body 8×10 colored shirt, waving arm: line from shoulder rotating ±30°, hand dot. Suitcase: 6×6 brown at side.

Also a small sign pole? Skip, keep clean.

Boarding animation: on pickup, animate passenger figure scaling up into nearest wagon? Simple puff + +1 text is fine, plus wagon occupant appears with pop (store occupantBorn[i] = time, scale = min(1,(now-born)/200)).

Occupant mapping: wagon index i (1-based from loco) gets occupant if i <= score... but score counts total pickups; wagons = len-1 grows with score so wagon i occupied iff i<=score → all wagons occupied always (since wagons == score). Right! Each pickup adds both a wagon and a passenger, so every wagon is always occupied. Good — every wagon shows a passenger. Pop animation per wagon when created: wagonBorn[i]=now.

Initial snake length 4 → 3 wagons? But score 0 → empty wagons at start? Contradiction with "every wagon occupied". At start, len=4: 3 wagons empty. After first pickup: 4 cars, 3 wagons, score 1 → wagon 1 occupied, 2-3 empty? Hmm inconsistent. Simplest: occupants = score, wagons = score too (snake length = 1 + wagons +? ). Let me define: snake starts length 3 (loco + 1 wagon? or loco + 2 empty coaches). Coaches always exist (a train has coaches), passengers board into them; when coaches full, a new coach is added. So: capacity = coaches count; coaches = max(1, ceil(score/ SEATS)) with SEATS=2 windows per wagon? Getting complex. Simplify: score = passengers; each wagon holds 1 passenger visible; wagons = max(1, score) but train starts with 1 empty wagon. So snake length = 2 + score? That grows fast. Snake classic starts length 3. Let: snake length = 2 + max(1, score)? Hmm.

Alternative simpler: wagons = snake.length - 1; occupant shown in wagon i if i <= score. Start length 3 (loco + 2 empty wagons). After k pickups, length = 3+k... wait classic snake grows by 1 per food: length = 3 + score, wagons = 2 + score, occupied wagons = min(score, wagons) = score (since wagons≥score+2? wagons = score+2 > score). So newest wagon at back is always empty, others occupied? Occupied = wagons 1..score, empty wagons score+1, score+2 at the tail. Visually: empty coaches trailing behind — that's actually realistic (train picks up passengers into coaches; rear coaches fill up over time? no, front fills first). Eh — realistic enough: front wagons full, tail wagons empty waiting. Actually cuter: passengers sit in the wagon nearest the loco. Fine.

Hmm, but then two trailing empty wagons always — fine, looks like a proper train.

Let me reduce start to loco + 2 wagons = length 3.

Now write everything.

Also handle the "nextDir" queue to allow quick double turns: keep queue of up to 2.

Draw HUD in DOM; update on change.

Let me code.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Little Line — a tiny train snake</title>
<style>...</style>
</head>
<body>
<div class="wrap">
  <header>
    <svg class="logo">...</svg>
    <div><h1>The Little Line</h1><p class="tag">a tiny train picking up passengers</p></div>
  </header>
  <div class="hud">
    <div class="chip">Passengers <b id="score">0</b></div>
    <div class="chip">Best <b id="best">0</b></div>
    <div class="chip">Service <b id="lvl">Local</b></div>
  </div>
  <div class="stage">
    <canvas id="game"></canvas>
    <div id="overlay">...</div>
  </div>
  <p class="hint">Steer with arrow keys or WASD · P pauses · swipe on touch</p>
</div>
<script>...</script>
</body>
</html>
```

Service level name by speed tier: Local / Branch / Express / nonstop fun.

Overlay markup variants via JS.

Now the JS in full. I'll be careful with details.

```js
const COLS=26, ROWS=16, CELL=36;
const W=COLS*CELL, H=ROWS*CELL;
const cv=document.getElementById('game'), ctx=cv.getContext('2d');
let scale=1, dpr=1;
function resize(){
  const stage=document.querySelector('.stage');
  const avail=stage.clientWidth; // stage width from CSS
  scale=Math.min(avail/W,1);
  dpr=window.devicePixelRatio||1;
  cv.width=Math.round(W*scale*dpr); cv.height=Math.round(H*scale*dpr);
  cv.style.width=(W*scale)+'px'; cv.style.height=(H*scale)+'px';
}
window.addEventListener('resize',resize);
```

Stage CSS: width:100%; max-width 960. Actually set canvas display block margin auto.

Hmm .stage width = wrap width; wrap max-width 980 with padding. avail = stage.clientWidth. scale=avail/W capped at... allow upscale a bit? cap at 1.05. For 1280 viewport, wrap 980 - padding 24*2? Let wrap max-width 984; padding 12; stage ~ 960 → scale 1.025. cap scale ≤ 1.025? Just allow scale up to 1.05.

State:

```js
let snake, dir, nextDirs=[], passenger, score, best=0, tickMs, mode='idle', tickAcc=0, lastTime=0, growPending=false;
let smoke=[], floats=[], puffs=[];
let cars=[]; // smoothed angles
let dist=0; // traveled for wheels
let crashTime=0, demoWait=0;
```

resetGame():

```js
snake=[]; const cy=Math.floor(ROWS/2), cx=Math.floor(COLS/2)-2;
for(let i=0;i<3;i++) snake.push({x:cx-i,y:cy,px:cx-i,py:cy});
dir={x:1,y:0}; nextDirs=[]; score=0; tickMs=150;
cars=new Array(3).fill(0);
spawnPassenger();
updateHUD();
```

spawnPassenger: pick random free cell with manhattan dist from head ≥ 4; loop.

tick():

```js
function tick(){
  if(mode==='demo-ai'?) ... apply AI dir
  // direction from queue
  while(nextDirs.length){ const d=nextDirs.shift(); if(!(d.x===-dir.x&&d.y===-dir.y)&&!(d.x===dir.x&&d.y===dir.y)){dir=d;break;} }
  const head=snake[0];
  const nx=head.x+dir.x, ny=head.y+dir.y;
  // collision
  const willGrow = ... // eating this tick? determine before: if nx,ny == passenger → grow
  const eat = passenger && nx===passenger.x && ny===passenger.y;
  // body collision: ignore tail if not growing
  for(let i=0;i<snake.length-((!eat)?1:0);i++){ if(snake[i].x===nx&&snake[i].y===ny) return crash(); }
  if(nx<0||ny<0||nx>=COLS||ny>=ROWS) return crash();
  const old=snake.map(s=>({x:s.x,y:s.y}));
  snake.unshift({x:nx,y:ny,px:head.x,py:head.y});
  if(eat){ score++; ... spawn, speed, sounds, float, puff; } else snake.pop();
  for(let i=0;i<snake.length;i++){
    const s=snake[i];
    if(i===snake.length-1 && eat){ s.px=s.x; s.py=s.y; }
    else { const o=old[i]||s; s.px=o.x; s.py=o.y; }
  }
  cars.push(0) if grew (angle aligns later)
  dist += CELL;
  smoke emit handled in render by timer based on dist.
}
```

Wait bug: for i≥1, new s position already set? I unshift new head only and didn't move others — others keep old coordinates which ARE their new positions (classic snake: body segments keep positions, head added). Right: in array-shift snake, only new head added; segment i's cell was old[i-1]'s cell — no, segment objects: old snake[0] object is now at index 1 with same coords — correct, its position is unchanged as a cell but as "carriage i" it's the carriage that was ahead. For interpolation we treat carriage i as moving from old[i] to current snake[i].pos. old[i] for carriage i: old array index i = the carriage previously behind... hmm old[i] coords = previous cell of the carriage now at index i? The carriage now at index i is the object old[i-1]?? No — objects shift: after unshift, snake[i] (i≥1) === old[i-1] object. Its coords are old[i-1].coords. For interpolation of carriage index i: from = old[i] (where carriage-slot i was) → to = current coords (old[i-1]). So s.px should be old[i].x. But s is object old[i-1]; I set s.px=old[i].x — but old[i] may equal old[i-1]... no, distinct cells. But note: when not eating, old array length = snake length (after pop, len same as old). old[i] exists for all i. Good. When eating, snake length = old+1; for i = newLen-1 (the last object = old tail object), old[i] undefined → px=x (stationary tail). Correct.

For head i=0: old[0] = old head coords; s.px=old[0].x = head.x. Good.

But careful: old[i] for i≥1 corresponds to coords of carriage slot — yes as reasoned.

Also cars angles array: index per carriage slot. After tick, carriages shifted; angle of slot i should carry from previous slot? Simplest: recompute target each frame and smooth with stored array; after growth push copy of last angle. Angles smoothing per frame:

```js
for(let i=0;i<snake.length;i++){
  const p=pts[i]; let t;
  if(i===0) t=Math.atan2(p[0].y-p[1].y,p[0].x-p[1].x);
  else t=Math.atan2(p[i-1].y-p[i].y,p[i-1].x-p[i].x);
```

Wait for i≥1, toward head = p[i-1]-p[i]. For i=0 head: p0 - p1 = toward front (movement). Consistent. But when snake length changes... cars[i] for new tail = copy cars[last] before push. When snake grows: cars.push(cars[cars.length-1]).

Hmm but there's subtlety: at a corner, p[i-1]-p[i] for the wagon just behind the corner points diagonal-into-corner; smoothing over ~100ms makes it look like the wagon rotates through the corner. Good.

Interpolated points:

```js
const t=Math.min(1,tickAcc/tickMs);
pts = snake.map(s=>({x:(s.px+(s.x-s.px)*t+0.5)*CELL, y:(...)*CELL}));
```

Hmm px→x lerp: (lerp+0.5)*CELL.

When paused or before first tick, px==x fine.

Main loop:

```js
function frame(now){
  requestAnimationFrame(frame);
  const dt=Math.min(100, now-lastTime); lastTime=now;
  if(mode==='play'||mode==='idle'){ // idle = demo playing
    tickAcc+=dt;
    const interval = mode==='idle'? demoTick : tickMs;
    while(tickAcc>=interval){ tickAcc-=interval; doTick(); if(mode==='crash'||...) break; }
  } else if(mode==='crash'){ crashTime+=dt; if(crashTime>1100){ if(demo) restartDemo else showOver(); } }
  render(now,dt);
}
```

Let me define modes: 'idle' (demo), 'play', 'crash-play', 'crash-demo', 'over', 'paused'. Simpler: mode in {'demo','play','crash','over','paused'}, plus flag wasDemo. Crash: after timer, if wasDemo → reset & mode='demo' (overlay start still shown); else mode='over' with overlay.

Pause only in play: 'p' toggles mode 'paused' (render continues, tickAcc frozen, show overlay "Paused — press P").

Demo tick speed: fixed 130ms regardless? Use same tickMs progression but demo resets. Use demo tickMs starting 140, also speeds with score; on demo reset, reset to 140.

AI: call inside doTick when mode==='demo': compute desired dir, set dir directly (respect no-reverse).

Render:

```js
function render(now,dt){
  ctx.setTransform(scale*dpr,0,0,scale*dpr,0,0);
  // grass
  for cells checker fill.
  // flowers deterministic
  // crash shake
  if(mode==='crash'){ const k=1-crashTime/1100; ctx.translate((Math.random()-0.5)*6*k, (Math.random()-0.5)*6*k); }
  drawTrack(pts);
  drawPassenger(now);
  drawTrain(now, dt);
  drawSmoke(dt);
  drawFloats(dt);
}
```

Smoke update uses dt; emission: accumulate smokeAcc += dt while moving (mode play/demo and not crash); when >110ms emit at chimney world pos.

Chimney world pos: loco local point (10,-20) rotated by cars[0] + pts[0].

Compute local→world: wx = p.x + lx*cos(a) - ly*sin(a) etc.

drawTrain: iterate i from snake.length-1 down to 0 so loco drawn last (on top). For each i: pos pts[i], angle cars[i] (smoothed in update step before drawing — do smoothing inside render before drawing loop).

Wheels rotation: wheelAngle = dist / wheelR.

Draw wagon i (i≥1):

```js
ctx.save(); translate(p.x,p.y); rotate(a);
const col=WAGON[(i-1)%WAGON.length];
// shadow ellipse? skip
// body
roundRect(-15,-13,30,26,5) fill col, stroke darken.
// roof stripe: rect(-15,-13,30,5) darker
// windows: two rects (-9,-6,7,9) & (2,-6,7,9) cream #f6efdc, stroke
// occupant: if i<=score: head circle in window 1? draw passenger head+shoulders popping from roof? Show in windows: small head circle #e8b98a r3 in each window, with pop scale for newest.
// wheels: circles at (-9,14),(9,14) r4.5 dark with hub; spokes rotated by wheelAngle.
// coupling: small rect at front? skip
ctx.restore();
```

Wait car length 30 < CELL 36 gap 6 between cars — couplings: draw a short dark line from car front to next car? Since gaps during straight are 6px, draw coupling rect from x=15 to 21 at y=0 (toward head) for each wagon, dark. Draw before body so body overlaps end. Fine, tiny detail: draw rect(14,-2,8,4) dark brown.

Locomotive draw (facing +x):

```js
// coupling behind? 
// cowcatcher: triangle path (20,13)->(27,13)->(20,-2) fill dark red? actually front is +x. cowcatcher at front bottom: moveTo(16,12) lineTo(26,12) lineTo(26,2)? Let me design:
body: boiler roundRect(-6,-12,22,22,4) fill #b5432f  (front part)
cab: roundRect(-20,-15,15,26,3) fill #a03a28; roof: rect(-22,-17,19,5) fill #5c4a3a? hmm roof darker #6e4a3a.
chimney: rect(8,-21,6,9) fill #3f3a36; flare: rect(6,-23,10,4).
dome: circle (0,-12,3.5) brass #d9a441.
headlamp: circle(16,-8? front face) — put lamp at front top of boiler: circle(15,-8,3) fill #ffd97a stroke dark. Actually front of boiler x=16. Lamp circle at (16,-6,3.5).
cowcatcher: path (16,10)->(24,14)->(16,14)? Let me: moveTo(14,12) lineTo(24,14) lineTo(24,6) close → wedge at front bottom. fill #6e4a3a.
wheels: big r5 at (-12,12),(0,12), small r3.5 at (12,12). spokes.
smoke stack world position local (11,-22).
```

Car vertical extent: y from -23 to 16 → height 39 > CELL 36 slightly overlapping rails/neighbors vertically — trains can be slightly bigger than track, fine, but overlapping adjacent cells vertically might look cramped; reduce: total height ~30: y ∈ [-16,14]. Adjust: boiler y -10..12 (h22)... let me finalize numbers:

- wheels at y=11, r 4.5 → bottom 15.5.
- boiler: roundRect(-5,-11,21,20,4) → y -11..9.
- cab: roundRect(-21,-14,16,24,3) → y -14..10; roof rect(-23,-16,20,4).
- chimney: rect(8,-19,6,8) + cap rect(6,-21,10,3).
- dome circle (−1,−11,3).
- lamp circle (15,-4,3).
- cowcatcher: moveTo(15,11) lineTo(25,14) lineTo(25,5) closePath.

Total x span -23..25 = 48 > CELL 36 → cars would overlap each other along the chain. Cars are spaced 36 apart, so length must ≤ ~34 incl couplings. Shorten: x span -16..17: cab -16..-2 (w14), boiler -2..15 (w17), chimney at x 8..13, cowcatcher to 20. Ok:

- cab: roundRect(-17,-13,13,23,3) → x -17..-4
- roof: rect(-19,-15,17,4)
- boiler: roundRect(-4,-10,18,19,4) → -4..14
- chimney: rect(7,-18,5,8), cap rect(5,-20,9,3)
- dome: circle (−1,−10,3)
- lamp: (13.5,-4,3)
- cowcatcher: (13,10)->(21,13)->(21,4)
- wheels: (-11,11,r4.5),( -1,11,4.5),(9,11,3.5)

Wagon: body roundRect(-14,-11,28,22,5); roof rect(-15,-13,30,4)? that makes length 30, ok. wheels (-8,11),(8,11) r4. windows: two roundRect(-9,-6,7,8) and (2,-6,7,8) cream; occupant head r2.8 at window center-bottomish.

Coupling: for each car, draw small dark rect from x=17..21 y -1.5..1.5? gap 6: 18-? body ends 17..? wagon ends at 15 (+roof), loco front 21. Just draw coupling for wagons: rect(15,-2,7,4) fill #4a3b2a — spans gap. Draw before body of the car ahead? Draw couplings first in a separate pass: for each i≥1, line from pts[i] toward pts[i-1] length up to 8: draw dark rounded line width 4 from p[i]+dirNorm*10 to p[i]+dirNorm*18. Simpler: in wagon local coords, rect(-24? front is +x): coupling from x=15 to 19: rect(15,-2,6,4). Loco also rear coupling: rect(-21,-2,6,4). ok.

Occupant pop: store wagonBorn = now when grew; scale=easeOutBack(min(1,(now-born)/250)).

Floats: {x,y,txt,life:1}; rise, alpha fade.

Puffs (pickup burst): small circles radiating.

Grass flowers: for each cell, hash h=(x*73856093 ^ y*19349663)>>>0; if h%17==0 draw flower: 3 tiny petals dots white + center yellow at deterministic offset. Subtle.

Grass checker: colors '#a9cc86' and '#a1c57e'. Hmm want warm storybook green. '#a6c98b'/'#9ec283'. Border frame via CSS.

Track ballast over grass: stroke '#c7ad85', lineWidth 24? CELL=36, ballast width 26. ties color '#8f6b4a' width... ties: perpendicular segments length 20, thickness 4, spacing 12, color '#97744f'. rails: offset ±5, width 2.5, color '#6f6a63' steel. Ballast edges: stroke darker tan line width 28 under? Keep: ballast 26 '#c9b189', then ties, then rails.

Draw track BEFORE passenger? Passenger platforms sit off-track visually on grass; if passenger cell is under the future track... the train will run over platform — acceptable, platform drawn above track, train above platform? Train drawn after → train passes over platform. Fine.

Passenger drawing (world coords, cell x,y):

```js
const bx=(x+0.5)*CELL, by=(y+0.5)*CELL;
platform: roundRect(bx-15, by-2, 30, 14, 3) fill '#cfc7b8' stroke '#a89f8d'; edge line.
passenger stands at (bx, by-8) bobbing: bob=sin(now/300)*1.5;
shadow ellipse.
body: roundRect(-4,-8+? ...) draw: torso roundRect(bx-4, by-16+bob, 8, 11,3) shirt color varies by hash; head circle (bx, by-19+bob, 3.5) skin; hat? maybe cap: arc.
waving arm: from (bx+3,by-13+bob) line to hand at angle oscillating; hand circle.
suitcase: rect(bx+7,by-1,7,6) brown with handle.
```

If passenger is "about to be eaten" nothing special.

Also add exclamation "!" bubble? No.

HUD chips styling: cream bg, brown border 2px, rounded-full, Trebuchet.

Overlay: position absolute inset 0, display flex center, background rgba(58,46,32,0.45), inner card cream, rounded 14, border 3 brown, shadow. Content per mode:

- idle: mini train svg? headline "The Little Line", sub "All aboard! Steer the train, collect waiting passengers.", big "Click or press a key to start", small "Auto-pilot is driving the demo train".
- over: "Derailed!" + "You delivered N passengers" + "Click or press any key to play again" + best note if new best "New record!".
- paused: "Paused" + "Press P to continue".

Keydown handler:

```js
window.addEventListener('keydown', e=>{
  initAudio();
  const k=e.key;
  if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(k)) e.preventDefault();
  if(mode==='demo'||mode==='over'){ startGame(); return; }
  if(mode==='paused'){ if(k==='p'||k==='P'){togglePause()} return; }
  if(mode==='play'){
    if(k==='p'||k==='P') return togglePause();
    const map={ArrowUp:[0,-1],w:[0,-1],W:[0,-1],...};
    if(map[k]) queueDir(map[k]);
  }
});
```

Also ignore key repeat spam fine.

Click on stage: if demo/over → start; if paused → resume? fine.

Touch swipe: on stage touchstart record; touchmove/touchend delta → dir if |d|>24; also treat tap (small delta) as start when idle/over. preventDefault to avoid scroll (passive:false).

queueDir(d): push to nextDirs if last queued dir differs; cap length 3.

startGame(): resetGame(); mode='play'; overlay hide; whistle sound.

Audio:

```js
let actx=null;
function initAudio(){ if(actx) return; try{ actx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} }
function tone(f0,f1,dur,type,vol,delay=0){ if(!actx) return; try{ const t0=actx.currentTime+delay; const o=actx.createOscillator(), g=actx.createGain(); o.type=type; o.frequency.setValueAtTime(f0,t0); o.frequency.exponentialRampToValueAtTime(Math.max(1,f1),t0+dur); g.gain.setValueAtTime(vol,t0); g.gain.exponentialRampToValueAtTime(0.0001,t0+dur); o.connect(g).connect(actx.destination); o.start(t0); o.stop(t0+dur+0.05);}catch(e){} }
whistle(){ tone(660,660,0.28,'triangle',0.08); tone(880,880,0.28,'triangle',0.06); }
pickupSnd(){ tone(520,780,0.12,'square',0.05); tone(780,1040,0.12,'square',0.04,0.08); }
crashSnd(){ tone(300,60,0.5,'sawtooth',0.09); }
```

Demo mode shouldn't call sounds before gesture — actx null until gesture, so safe.

But note: demo runs before gesture; when user presses key, startGame called with whistle — good.

Also clicking canvas focuses iframe? The iframe receives clicks; keydown on window works after click. Good.

Demo AI flood fill:

```js
function flood(cx,cy,cap){
  const blocked=new Set(snake.slice(0,snake.length-1).map(s=>s.x+','+s.y));
  // also exclude? tail will move; excluding tail from blocked
  const seen=new Set([cx+','+cy]); const q=[[cx,cy]]; let count=0;
  while(q.length){ const [x,y]=q.pop(); count++; if(count>=cap) return count;
    for(const d of DIRS){ const nx=x+d.x, ny=y+d.y; if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue; const k=nx+','+ny; if(seen.has(k)||blocked.has(k)) continue; seen.add(k); q.push([nx,ny]); } }
  return count;
}
```

Use stack fine.

AI choose:

```js
function aiSteer(){
  const h=snake[0]; const cand=[];
  for(const d of DIRS){
    if(d.x===-dir.x && d.y===-dir.y) continue;
    const nx=h.x+d.x, ny=h.y+d.y;
    if(nx<0||ny<0||nx>=COLS||ny>=ROWS) continue;
    let blocked=false;
    for(let i=0;i<snake.length-1;i++){ if(snake[i].x===nx&&snake[i].y===ny){blocked=true;break;} }
    if(blocked) continue;
    const area=flood(nx,ny,snake.length+3);
    let s=-Math.abs(nx-passenger.x)-Math.abs(ny-passenger.y)+area*0.02;
    if(area<snake.length) s-=500;
    cand.push({d,s});
  }
  if(cand.length){ cand.sort((a,b)=>b.s-a.s); dir=cand[0].d; }
}
```

Note: excluding tail from blocked because tail moves — but if eating this tick tail stays; minor, ignore.

One catch: in demo, crash possible anyway (rare) → handled by crash-demo restart.

Speed tiers: tickMs = max(78, 150 - score*3.5). Service names: score<5 'Local', <12 'Branch line', <20 'Express', else 'Non-stop'.

Also draw a subtle "next passenger" indicator? skip.

Now DOM overlay functions:

```js
const overlay=document.getElementById('overlay');
function showOverlay(html){ overlay.innerHTML=html; overlay.classList.add('show'); }
function hideOverlay(){ overlay.classList.remove('show'); }
```

Overlay pointer-events: none? Clicks should pass to stage handler — attach click on stage (which contains overlay) so fine; set overlay pointer-events none to be safe? If pointer-events none, click goes to stage anyway. Set overlay pointer-events:none and handle clicks on .stage. But cursor: keep.

New best: best=Math.max(best,score) at crash.

Also crash puff at head: spawn 10 gray puffs.

Let me also make smoke drift: vy=-20px/s, vx = -dirVector*10 + wind, size grows, alpha fades over life 1.4s. Store {x,y,vx,vy,r,life,max}.

Emission: emitAcc += dt; while(emitAcc>110){emitAcc-=110; emit}. Only when mode play/demo and !crash. In pause, no emit.

Also puff on pickup: 8 smoke circles at passenger pos.

Floats draw: ctx.font bold 16px; fill '#4a3b2a' with white stroke? draw text with alpha.

Alright, also ensure no console errors: guard everything.

Rounded rect helper (ctx.roundRect exists in modern Chrome — I'll use ctx.roundRect, Chrome 99+, fine, but add fallback path helper to be safe). I'll write helper rr(x,y,w,h,r) using arcTo.

Now the flower rendering each frame for 416 cells + checker: fine performance-wise; could pre-render background to offscreen canvas once — do that for perf: bgCanvas rendered once at logical size with grass+flowers; drawImage each frame. Offscreen canvas at logical size W×H (936×576) — drawImage scaled via current transform; may blur slightly with scale; acceptable. Actually draw bg directly each frame is also fine (~416 fills). I'll pre-render for cleanliness.

Note: drawImage of offscreen under transform scale*dpr — crisp enough.

Write final code. Also mobile: hint text mentions swipe. Media query: shrink header font, chips wrap.

Check 1280×800: wrap centered, header ~70px, hud 40, canvas 576 (scale maybe 1.0 → 936×576), total ~700 fits.

Let me now also handle "render even when paused/over": loop always runs render.

During 'over', keep smoke fading etc. tickAcc not advancing. Interpolation t: when mode not actively ticking, freeze t at current tickAcc value (don't advance). So compute t from tickAcc which only advances in play/demo. But render uses t=clamp(tickAcc/tickMs) — during crash, tickAcc stays at crash moment (maybe mid-transition ~0.5) — trains frozen mid-cell while shake. That's fine.

Hmm: at crash, tickAcc could be near 1 or mid. Fine.

After crash in demo auto-restart: resetGame sets px=x so t irrelevant.

One more: when starting game from demo mid-motion, resetGame ensures continuity.

Wheel angle uses dist; reset dist on reset.

Write wagon occupant as tiny passenger: head + torso inside window rect (clip? just draw small circle + shoulders within window). Draw after windows: torso roundRect(wx-3, wy, 6,5,2) shirt color per wagon occupant index, head circle (wx, wy-1, 2.6).

Let me now also consider: the loco front lamp glow at night? skip.

Station bell every 5 passengers: float text "Toot!" plus whistle sound. Cute: every 5 pickups, do whistle() and float "TOOT!" at head.

Alright, code writing time. Also title svg icon: small loco 28×28 inline:

```svg
<svg viewBox="0 0 32 32" width="34" height="34">
 <rect x="4" y="14" width="16" height="10" rx="2" fill="#b5432f"/> (cab)
 <rect x="18" y="10" width="10" height="14" rx="2" fill="#a03a28"/> (boiler)
 <rect x="21" y="5" width="4" height="6" fill="#4a423b"/> (chimney)
 <rect x="2" y="12" width="18" height="3" fill="#6e4a3a"/> (roof)
 <circle cx="9" cy="26" r="3" fill="#3f3a36"/><circle cx="16" cy="26" r="3"/><circle cx="24" cy="26" r="3"/>
 <circle cx="26" cy="13" r="1.8" fill="#ffd97a"/>
 rails line
</svg>
```

Fine.

Also I want headlamp beam? skip.

Let me carefully write the render train with smoothing update:

```js
function updateCars(pts,dt){
  while(cars.length<snake.length) cars.push(cars.length?cars[cars.length-1]:0);
  while(cars.length>snake.length) cars.pop();
  const k=Math.min(1,dt/90);
  for(let i=0;i<pts.length;i++){
    let tx;
    if(i===0) tx=Math.atan2(pts[0].y-pts[1].y, pts[0].x-pts[1].x);
    else tx=Math.atan2(pts[i-1].y-pts[i].y, pts[i-1].x-pts[i].x);
    cars[i]=angLerp(cars[i],tx,k);
  }
}
```

Edge: snake.length==1 impossible (starts 3).

Draw order: draw from tail (i=len-1) to head 0.

Passenger shirt colors: PALETTE like wagon colors; use hash.

Now, floats and puffs updates with dt in render — but if paused, dt still flows; guard: only update effects when mode!=='paused'. Fine either way.

Also when tab inactive, dt clamp 100 keeps things sane.

HUD update: function updateHUD sets textContent score/best and service tier.

I think ready to write. Also ensure no horizontal scrollbar: body overflow-x hidden, box-sizing border-box.

Let me write the full file.

Additional flourish for first paint: demo running immediately — yes, resetGame + mode 'demo' on load, overlay shown.

Careful: before first user gesture, no AudioContext — ok.

Write code:

```js
const DIRS=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
```

doTick:

```js
function doTick(){
  if(mode==='demo') aiSteer();
  // apply queued (play)
  while(nextDirs.length){
    const d=nextDirs.shift();
    if((d.x!==-dir.x||d.y!==-dir.y)&&(d.x!==dir.x||d.y!==dir.y)){ dir=d; break; }
  }
  const head=snake[0];
  const nx=head.x+dir.x, ny=head.y+dir.y;
  if(nx<0||ny<0||nx>=COLS||ny>=ROWS) return crash();
  const eat = passenger && nx===passenger.x && ny===passenger.y;
  const lim = snake.length - (eat?0:1);
  for(let i=0;i<lim;i++){ if(snake[i].x===nx&&snake[i].y===ny) return crash(); }
  const old=snake.map(s=>({x:s.x,y:s.y}));
  snake.unshift({x:nx,y:ny,px:head.x,py:head.y});
  if(eat) onEat(); else snake.pop();
  for(let i=0;i<snake.length;i++){
    const s=snake[i];
    if(i===snake.length-1 && eat){ s.px=s.x; s.py=s.y; }
    else { const o=old[i]||{x:s.x,y:s.y}; s.px=o.x; s.py=o.y; }
  }
  if(eat) cars.push(cars[cars.length-1]||0);
  dist+=CELL;
}
```

Wait: for i≥1 new object is old[i-1], and its prev should be old[i]. But careful: object identity — old[i-1] is snake[i]; old[i] is a different object (old[i]) whose coords are the previous slot. But hmm: old[i] coords = old cell of carriage slot i. And snake[i] currently at coords old[i-1]. So s.px=old[i].x moves slot i from old[i]→old[i-1]. Correct.

But subtle: old[i] is object; I stored copies map — I mapped to plain {x,y} copies. Good.

onEat():

```js
function onEat(){
  score++;
  tickMs=Math.max(78,150-score*3.2);
  floats.push({x:(passenger.x+0.5)*CELL, y:(passenger.y)*CELL, txt:'+1', life:1});
  burst((passenger.x+0.5)*CELL,(passenger.y+0.2)*CELL,'#e8b98a');
  wagonBorn[snake.length]=performance.now(); // index of new wagon = old length? after unshift length grew by 1 before pop... in onEat called before loop; snake.length now includes new head without pop → wagons = length-1... new wagon index = snake.length-1.
```

Let me set wagonBorn as object keyed by wagon index (1-based): after tick completes, wagons = snake.length-1; new wagon index = snake.length-1 (at time after onEat, length already incremented). Call onEat after unshift → snake.length = old+1, new wagon idx = snake.length-1 = old length. wagonBorn[idx]=now. But idx grows unbounded; fine object.

Also spawn passenger, sounds: pickupSnd(); if(score%5===0){ whistle(); floats.push({x:headx..., txt:'Toot!'}) } — head pos: use nx,ny.

spawnPassenger after eat: new passenger not on snake.

crash():

```js
function crash(){
  crashSnd();
  burst((snake[0].x+0.5)*CELL,(snake[0].y+0.5)*CELL,'#8b8b8b',14);
  wasDemo = (mode==='demo');
  mode='crash'; crashTime=0;
  if(!wasDemo){ best=Math.max(best,score); }
}
```

After crash timer in frame: if wasDemo → resetGame(); mode='demo'; (overlay stays 'idle' content — it's already showing? In demo the idle overlay is visible; keep). else mode='over'; showOverlay(overHTML).

But during 'crash' the overlay in demo remains visible — good.

startGame:

```js
function startGame(){ initAudio(); resetGame(); mode='play'; hideOverlay(); whistle(); }
```

From 'over' keydown → startGame.

Paused: togglePause: if mode==='play' → mode='paused', showOverlay(pausedHTML); else if 'paused' → mode='play', hideOverlay(), lastTime reset handled by dt clamp.

Keydown while mode 'crash': ignore (or buffer start? ignore).

Click handler on stage: initAudio(); if(mode==='demo'||mode==='over') startGame(); else if(mode==='paused') togglePause();

Touch: touchstart store; touchend: dx,dy; if small → click behavior (handled by click event anyway — mobile taps fire click; so only handle swipes in touchend and preventDefault touchmove on stage to stop scroll). Set touch-action: none on stage CSS.

Overlay idle content includes "Click or press a key to start" prominently.

Now render passenger: only if passenger exists.

Also small detail: platform drawn only when passenger present (platform appears with passenger). Good.

drawTrack(pts): need at least 2 points (len 3 ok).

```js
function drawTrack(pts){
  // ballast
  ctx.lineCap='round'; ctx.lineJoin='round';
  ctx.strokeStyle='#cdb48d'; ctx.lineWidth=24;
  pathThrough(pts); ctx.stroke();
  // ties
  ctx.strokeStyle='#97744f'; ctx.lineWidth=4;
  // walk
  let acc=6;
  for(let i=0;i<pts.length-1;i++){
    const a=pts[i], b=pts[i+1];
    const dx=b.x-a.x, dy=b.y-a.y, L=Math.hypot(dx,dy)||1;
    const ux=dx/L, uy=dy/L, px=-uy, py=ux;
    let d=acc;
    while(d<L){
      const mx=a.x+ux*d, my=a.y+uy*d;
      ctx.beginPath(); ctx.moveTo(mx-px*9,my-py*9); ctx.lineTo(mx+px*9,my+py*9); ctx.stroke();
      d+=12;
    }
    acc=d-L; // carry
```

Hmm carry logic: acc should be distance until next tie from b: remaining = d - L after loop where d first ≥ L... Let me: iterate d starting from acc; after while, tie spacing consumed = last d - L → next tie at distance (12 - (d - L))? Let me just: acc = (d - L) gives overshoot amount; next segment start offset = 12 - overshoot. So set startAcc = 12 - (d - L). Implement:

```js
let d=acc;
while(d<L){ draw tie at d; d+=12; }
acc = 12 - (d - L);
```

with initial acc maybe 6.

Then rails: for each segment draw two offset lines:

```js
ctx.strokeStyle='#6d675e'; ctx.lineWidth=2.2;
for(let i=0;i<pts.length-1;i++){
  const a=pts[i],b=pts[i+1]; const dx=b.x-a.x,dy=b.y-a.y,L=Math.hypot(dx,dy)||1;
  const px=-dy/L, py=dx/L;
  for(const s of [-4.5,4.5]){
    ctx.beginPath(); ctx.moveTo(a.x+px*s, a.y+py*s); ctx.lineTo(b.x+px*s, b.y+py*s); ctx.stroke();
  }
}
```

Ballast width 24 < CELL 36 — rails width span 9 — good proportions.

But ballast under platform area where train hasn't been — no, track only where snake is/was. The track "follows" the train (no persistent rails). That's fine stylistically (the little train lays its own track like a road). 

drawTrain:

```js
function drawTrain(now){
  for(let i=snake.length-1;i>=0;i--){
    const p=pts[i], a=cars[i];
    ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(a);
    // ground shadow
    ctx.fillStyle='rgba(60,50,30,0.18)'; ellipse(0,13,16,5)? maybe skip
    if(i===0) drawLoco(now); else drawWagon(i,now);
    ctx.restore();
  }
}
```

Wheels function:

```js
function wheel(x,y,r,ang){
  ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fillStyle='#3a332d'; ctx.fill();
  ctx.beginPath(); ctx.arc(x,y,r*0.55,0,7); ctx.fillStyle='#8d8578'; ctx.fill();
  ctx.strokeStyle='#3a332d'; ctx.lineWidth=1.2;
  ctx.beginPath(); ctx.moveTo(x+Math.cos(ang)*r*0.9,y+Math.sin(ang)*r*0.9); ctx.lineTo(x-Math.cos(ang)*r*0.9,y-Math.sin(ang)*r*0.9); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(x+Math.cos(ang+1.57)*r*0.9,y+Math.sin(ang+1.57)*r*0.9); ctx.lineTo(x-Math.cos(ang+1.57)*r*0.9,y-Math.sin(ang+1.57)*r*0.9); ctx.stroke();
}
```

wa = dist/6.

rr helper:

```js
function rr(x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }
```

drawWagon(i, now):

```js
const col=WAGONS[(i-1)%WAGONS.length];
// coupling front (toward head, +x)
ctx.fillStyle='#4a3b2a'; rr(14,-2.5,7,5,2); ctx.fill();
// body
rr(-15,-11,30,22,5); ctx.fillStyle=col; ctx.fill(); ctx.lineWidth=2; ctx.strokeStyle=shade(col)? precomputed darker: use 'rgba(0,0,0,0.25)' stroke. 
// roof band
rr(-16,-13,32,5,2.5); ctx.fillStyle='rgba(0,0,0,0.22)'; ctx.fill();
// windows
for wx of [-9.5,2.5]: rr(wx,-6,8,9,2); fill '#f3ead6'; stroke rgba(0,0,0,.2)
// occupants: if i<=score → head in each window? one passenger per wagon: draw in first window; maybe both windows occupied? 1 passenger per wagon: draw in window nearer front.
occ: idx=i; born=wagonBorn[idx]; s=popScale(now-born);
if(i<=score){ const cxw=-5.5? window1 center x=-9.5+4=-5.5; draw head circle(cx, -1, 3*s) skin; torso rr(cx-3.4, -1? ...
```

Occupant: head at (wcx, -2.5*s?) Let me: torso rr(wcx-3, -2, 6,5,2) fill shirtCol; head arc(wcx, -3.5, 2.8*s). Scale by s via translate/scale around window center. Simplest: ctx.save(); translate(wcx,0); scale(s,s); translate(-wcx,0); draw; restore.

Shirt color for occupant: OCC[(i-1)%OCC.length] distinct pleasing palette.

Wagon colors palette WAGONS = ['#d98f3e','#3f8f7a','#c95f4a','#7b6bb0','#4f86c6','#b56a9a'].

drawLoco(now):

```js
// rear coupling
ctx.fillStyle='#4a3b2a'; rr(-22,-2.5,7,5,2); fill
// cowcatcher
ctx.beginPath(); moveTo(13,11); lineTo(22,14); lineTo(22,3); closePath(); fill '#6e4a3a';
// cab
rr(-18,-13,14,23,3); fill '#a03a28'; stroke rgba(0,0,0,.25)
// cab roof
rr(-20,-15,18,5,2); fill '#5f4632';
// cab window
rr(-15,-9,8,8,2); fill '#f3ead6'; stroke; 
// driver! little driver head in cab window: circle(-11,-5,2.6) skin + cap? cute: driver with cap.
// boiler
rr(-5,-10,19,19,5); fill '#b5432f'; stroke
// boiler bands: two vertical lines
// chimney
fillRect via rr(6,-18,6,8,1) fill '#3f3a36'; cap rr(4,-21,10,4,1) fill '#3f3a36'
// dome brass
arc(-1,-10,3.2) fill '#d9a441' stroke rgba(0,0,0,.3)
// lamp
arc(13.5,-4,3) fill '#ffd97a' stroke '#3f3a36'
// wheels
wheel(-11,11,4.5); wheel(-1,11,4.5); wheel(9.5,11,3.5)
```

Driver: in cab window, head circle skin r2.5 at (-11,-4), cap blue arc.

Smoke emission point: local (9,-21) rotated at pts[0] with cars[0].

burst(x,y,color): for k<10: puffs.push({x,y,vx:cos*sp,vy:sin*sp-20,r:3+rand*3,life:1,decay}).

Smoke update:

```js
for(s of smoke){ s.x+=s.vx*dtms; s.y+=s.vy*dtms; s.r+=dtms*0.02; s.life-=dtms/1400; }
filter life>0. draw: fillStyle rgba(235,230,220,alpha) — white steam on light bg? use gray rgba(120,115,110, alpha*0.5)? Steam: soft light gray with darker outline? Use rgba(250,247,240,0.75*life) with subtle stroke? On green grass, white steam reads well. Choose fill 'rgba(248,246,240,'+(0.7*life)+')'.
```

vx: -dir.x*18 + (rand-0.5)*8, vy: -26 + (rand-0.5)*6.

Floats: y -= dt*0.03; life -= dt/900.

OK, passenger detail drawing:

```js
function drawPassenger(now){
  if(!passenger) return;
  const bx=(passenger.x+0.5)*CELL, by=(passenger.y+0.5)*CELL;
  const bob=Math.sin(now/280+passenger.x)*1.2;
  // platform
  rr(bx-15, by+2, 30, 12, 3); fill '#cfc7b8'; stroke '#a49b88' lw1.5
  // platform edge stripe
  ctx.fillStyle='#efe8da'; fillRect(bx-15, by+2, 30, 3)? use rr small.
  // shadow under passenger
  ellipse(bx, by+1, 7,2.4) rgba(0,0,0,.15)
  const py=by-6+bob*0? body base at by+... passenger stands on platform top edge (~by+3). Let ground=by+3.
  torso: rr(bx-4, ground-14+bob, 8, 10, 3) fill shirt
  head: arc(bx, ground-17+bob, 3.6) fill '#ecc19a'; stroke thin
  cap: arc upper half fill hatCol: arc(bx, ground-18+bob, 3.6, PI, 0) fill
  waving arm: shoulder (bx+4, ground-12+bob); angle=−1.2+sin(now/150)*0.5; hand=(shoulder + len7 rotated); line width2 shirt-dark; hand dot skin r1.6
  other arm static: line to (bx-5,ground-6)
  legs: two short lines from torso bottom to ground: skip (torso reaches ground)
  suitcase: rr(bx-13, ground-6, 6,6,1) fill '#8a5a33'; handle small.
}
```

Platform occupies lower part of cell; passenger stands on it. Track ballast may pass under platform — visually ok.

Also draw a tiny sign next to platform: pole + flag "HALT"? skip to reduce clutter. Maybe small lamp post? skip.

Flowers on bg canvas:

```js
function buildBG(){
  bg=document.createElement('canvas'); bg.width=W; bg.height=H; const b=bg.getContext('2d');
  for x,y: fill checker; 
  flowers deterministic: h=hash; if(h%13===0){ petal dots }
  also few darker grass tufts: if h%9===0 small 3-line tuft darker green.
}
```

Draw order in render: drawImage(bg), shake translate, track, passenger, train, smoke, puffs, floats.

Floats text: ctx.font='bold 15px Trebuchet MS'; textAlign center; fillStyle 'rgba(74,59,42,alpha)'; maybe white halo: strokeStyle 'rgba(255,255,255,0.8)' lw3 strokeText then fill.

HUD service tier update in updateHUD.

Best persistence: variable only.

Now CSS:

```css
*{box-sizing:border-box;margin:0}
html,body{height:100%}
body{background:'#efe6d3'?; font-family:'Trebuchet MS', 'Segoe UI', sans-serif; color:#4a3b2a; display:flex; justify-content:center; padding:18px 12px; overflow-x:hidden}
```

Background: warm paper with subtle pattern? Solid '#f1e7d3'. Maybe dotted pattern via repeating radial? Keep solid + slight texture using CSS repeating-linear-gradient? I said avoid decorative gradients; solid is fine.

.wrap{width:100%; max-width:984px}
header{display:flex; align-items:center; gap:12px; margin-bottom:10px}
h1{font-size:30px; letter-spacing:0.5px}
.tag{color:#8a7a63; font-size:14px}
.hud{display:flex; gap:10px; margin-bottom:10px; flex-wrap:wrap}
.chip{background:#fbf5e8; border:2px solid #b49b74; border-radius:999px; padding:6px 14px; font-size:14px; font-weight:bold...}
.chip b{font-size:16px; margin-left:4px; color:#9c4a2f}
.stage{position:relative; border:6px solid #7a5233; border-radius:14px; overflow:hidden; box-shadow:0 6px 0 rgba(122,82,51,0.35), 0 12px 24px rgba(90,60,30,0.18); background:#a6c98b; line-height:0}
canvas{display:block}
#overlay{position:absolute; inset:0; display:none; align-items:center; justify-content:center; background:rgba(52,40,26,0.45); line-height:1.4; pointer-events:none}
#overlay.show{display:flex}
.card{background:#fbf5e8; border:3px solid #7a5233; border-radius:16px; padding:22px 34px; text-align:center; box-shadow:0 8px 20px rgba(60,40,20,0.35); max-width:86%}
.card h2{font-size:26px; margin-bottom:6px}
.card p{font-size:14px; color:#6b5a44; margin-bottom:4px}
.start{display:inline-block; margin-top:10px; background:#9c4a2f; color:#fbf5e8; font-weight:bold; padding:10px 22px; border-radius:999px; font-size:16px; letter-spacing:0.5px; animation: pulse 1.6s ease-in-out infinite}
@keyframes pulse{50%{transform:scale(1.05)}}
.hint{text-align:center; color:#8a7a63; font-size:13px; margin-top:10px}
@media(max-width:600px){ h1 24px; chips smaller }
```

Line-height 0 on stage kills overlay line-height — set overlay line-height normal inside.

Wait pulse animation = animation not gradient, fine.

Now overlay content builder:

```js
function idleHTML(){ return `<div class="card"><h2>The Little Line</h2><p>A little steam train, a string of colourful coaches, and passengers waiting on the platform.</p><p>Steer with the arrow keys or WASD — collect every passenger, don't drive off the meadow or into yourself.</p><span class="start">Click or press a key to start</span><p class="small">The auto-pilot is warming up the engine meanwhile…</p></div>`; }
```

Game over:

```js
`<div class="card"><h2>Derailed!</h2><p>You delivered <b>${score}</b> passenger${s} to their coaches.</p>${newBest?'<p class="newbest">New best!</p>':''}<span class="start">Click or press a key to run again</span></div>`
```

Paused: `<div class="card"><h2>Paused</h2><p>Press P to keep rolling</p></div>`.

Everything set. Now write final code fully and carefully check for errors.

Potential pitfalls:
- pts computed in render needs current interpolation; define pts as module-level, computed at top of render.
- updateCars before drawTrain.
- When snake length 3 minimum fine.
- In demo, ensure passenger exists before aiSteer uses it — spawn in resetGame.
- flood with cap: cap = snake.length+3.
- performance: flood each tick (≤4 floods, each ≤ ~60 cells) trivial.

Passenger spawn free cell: build occupancy set; random tries 500.

Also prevent passenger spawning on the cell directly in front? no need.

Render grass drawn from bg canvas — but stage bg behind canvas same green so letterboxing invisible.

Resize: call on load & resize; canvas CSS width set.

Check the scale/transform: I set ctx.setTransform(scale*dpr,...) each frame at start — yes in render.

One more: tie drawing initial acc: start acc=4.

Also crash overlay shows after 1.1s; during crash keep rendering with shake decay: k = max(0,1-crashTime/600).

Demo restart delay: after crash 1200ms.

For the demo, also auto-drive with slight imperfection? Flood-based is good.

Sound on eat in demo? actx null until gesture — silent. After gesture user starts game anyway.

Edge: keydown 'p' during demo → treated as start (any key starts). Fine — any key starts means P too. ok.

Let me also handle Space to start & prevent scroll — included.

Alright, also add subtle vignette? no.

Double-check interpolation when game just started: px=x so pts static until first tick — good.

Check drawTrack with pts where consecutive points identical? Only if two segments share cell — never.

Corner case during interpolation: adjacent interpolated points distance varies (0..CELL*~1.4). Ballast path with round join handles it.

Now write the final HTML. Also remember: everything visible in 3s — demo running immediately. Good.

Let me write out occupant pop scale easeOutBack:

```js
function popScale(ms){ const t=Math.min(1,ms/260); const c=1.70158; return 1 + (t<1 ? 0 : 0); }
```

Simpler: s = t<1 ? (1 + Math.sin(t*Math.PI)*0.4*t?) Let me do simple overshoot: s = t>=1?1: (1.6*t*t - 0.6*t*t*t)? Use easeOutBack: `1 + 2.2*Math.pow(t-1,3) + 1.2*Math.pow(t-1,2)`? Standard: c1=1.70158,c3=c1+1; s=1+c3*(t-1)^3 + c1*(t-1)^2. Use that.

Font check: 'Trebuchet MS' widely available; fallback sans-serif.

Also add meta viewport — included.

Time to write it all out.

One more consideration for the head angle when snake just spawned: cars all 0, pts along row — fine.

Also when the train reverses?? Can't reverse.

Let me also make demo AI turn toward passenger but with lookahead to avoid trapping itself in the classic spiral: flood cap handles.

Final: write code. Estimate ~450 lines. Let's go.

For hash: function h2(x,y){ let n=x*374761393 + y*668265263; n=(n^(n>>>13))*1274126177; return (n^(n>>>16))>>>0; }

Flowers: if(h2(x,y)%11===0): draw at offset ox=(h>>4)%20+8, oy=(h>>8)%20+8: 4 petals white r2 around center r1.6 gold. Tufts: if %7===2: three short darker green lines.

Draw on bg canvas.

Now writing. Also make sure canvas ctx.textBaseline etc.

For overlay 'small' class: font-size 12px, opacity .7.

Also ensure the overlay card doesn't overflow on 375px: max-width 86%, padding smaller on mobile media query.

Write it.