<!-- SOLID CITY · part 1981 -->
R_BOARD');if(!hit.accepted){s.vx=s.vz=0;return hit;}
  if(s.mode==='AIR'&&s.vy<=0){const h=groundedPose(world,s,.08,'HOVER_BOARD');
    if(h&&s.pos.y<=h.y+.2){s.pos.y=h.y;s.vy=0;s.mode='RIDE';}}
  return {accepted:true,mode:s.mode,speed:len2(s.vx,s.vz)};
}
export function createSpaceRider(pos,yaw=0){return {type:'SPACE_RIDER',pos:copy(pos),yaw,v:0,lean:0,leanRate:0,vy:0,mode:'RIDE',cadence:0};}
export function stepSpaceRider(s,input,world,dt=CRAFT_STEP){
  requireWorld(world);const x=shape(input.x||0),y=shape(input.y||0);
  if(s.mode==='RIDE'){
    const h=groundedPose(world,s,.52,'SPACE_RIDER');if(!h)return {accepted:false,reason:'NO_RIDER_SUPPORT'};
    const cadence=60*Math.abs(s.v)/(2.2*2.1);s.cadence=cadence;
    if(y>0)s.v+=y*3.2*clamp((115-cadence)/115,0,1)*dt;
    if(y<0)s.v=Math.max(0,s.v+5.5*y*dt);
    if(y===0)s.v=Math.max(0,s.v-(.15+.015*s.v*s.v)*dt);
    const lock=lerp(35,10,clamp(s.v/10,0,1))*PI/180;
    let omega=s.v/1.05*Math.tan(x*lock);
    omega=clamp(omega,-.9*9.81/Math.max(s.v,1),.9*9.81/Math.max(s.v,1));
    s.yaw+=omega*dt;
    const target=s.v<1.5?0:clamp(Math.atan2(s.v*omega,9.81),-42*PI/180,42*PI/180);
    s.leanRate+=(36*(target-s.lean)-11*s.leanRate)*dt;s.lean+=s.leanRate*dt;
    s.pos.y=h.y;
    if(input.jump===true){s.mode='AIR';s.vy=4.2;}
  }else{s.vy-=20*dt;}
  const f=forward(s.yaw),to={x:s.pos.x+f.x*s.v*dt,y:s.pos.y+s.vy*dt,z:s.pos.z+f.z*s.v*dt};
  const hit=move(world,s,to,.7,'SPACE_RIDER');if(!hit.accepted){s.v=0;return hit;}
  if(s.mode==='AIR'&&s.vy<=0){const h=groundedPose(world,s,.52,'SPACE_RIDER');
    if(h&&s.pos.y<=h.y+.2){s.pos.y=h.y;s.vy=0;s.mode='RIDE';}}
  return {accepted:true,mode:s.mode,speed:s.v,lean:s.lean,cadence:s.cadence};
}
export function createSpaceShip(pos,yaw=0){return {type:'SPACE_SHIP',pos:copy(pos),vel:{x:0,y:0,z:30},yaw,pitch:0,bank:0,pitchRate:0,rollRate:0,throttle:0,mode:'FLIGHT'};}
export function stepSpaceShip(s,input,world,dt=CRAFT_STEP){
  requireWorld(world);const x=shape(input.x||0),y=shape(input.y||0);
  const p={m:1000,g:9.81,rho:1.225,S:16,CD0:.027,k:.054,CL0:.30,CLslope:.075,stall:14,Pmax:160000,Tstatic:4800};
  const vx=s.vel.x,vy=s.vel.y,vz=s.vel.z,V=Math.max(.01,Math.hypot(vx,vy,vz));
  const horizontal=Math.hypot(vx,vz),gamma=Math.atan2(vy,horizontal);
  const alpha=(s.pitch-gamma)*180/PI+2;
  const authority=clamp((V/40)**2,.35,1);
  const pitchCmd=y*38*PI/180*authority*clamp((16-alpha)/5,0,1);
  s.pitchRate+=(pitchCmd-s.pitchRate)*(1-decay(1/.30,dt));
  s.rollRate+=(x*115*PI/180*authority-s.rollRate)*(1-decay(1/.25,dt));
  s.pitch=clamp(s.pitch+s.pitchRate*dt,-80*PI/180,80*PI/180);
  s.bank=clamp(s.bank+s.rollRate*dt,-60*PI/180,60*PI/180);
  if(Math.abs(x)<.01)s.bank=approach(s.bank,0,20*PI/180*dt);
  s.yaw+=(9.81*Math.tan(s.bank)/Math.max(V,12))*dt;
  s.throttle+=(clamp(input.throttle??.65,0,1)-s.throttle)*(1-decay(1/.8,dt));
  let CL=p.CL0+p.CLslope*alpha;
  if(alpha>p.stall)CL=Math.max(.85,1.35-.045*(alpha-p.stall));
  const CD=p.CD0+p.k*CL*CL;
  const qS=.5*p.rho*V*V*p.S,L=qS*CL,D=qS*CD;
  const T=Math.min(p.Tstatic,s.throttle*p.Pmax/Math.max(V,15));
  const f=forward(s.yaw),cp=Math.cos(s.pitch),sp=Math.sin(s.pitch);
  const nose={x:f.x*cp,y:sp,z:f.z*cp};
  // Lift direction is perpendicular to velocity, aligned toward banked body-up.
  const lateral={x:Math.cos(s.yaw),y:0,z:-Math.sin(s.yaw)};
  const up={x:-f.x*sp*Math.cos(s.bank)+lateral.x*Math.sin(s.bank),y:cp*Math.cos(s.bank),z:-f.z*sp*Math.cos(s.bank)+lateral.z*Math.sin(s.bank)};
  const dot=(up.x*vx+up.y*vy+up.z*vz)/V;
  let lx=up.x-dot*vx/V,ly=up.y-dot*vy/V,lz=up.z-dot*vz/V;
  const ll=Math.hypot(lx,ly,lz);
  if(ll<1e-6)return {accepted:false,reason:'LIFT_BASIS_DEGENERATE'};
  lx/=ll;ly/=ll;lz/=ll;
  const accel={x:(T*nose.x-D*vx/V+L*lx)/p.m,y:(T*nose.y-D*vy/V+L*ly)/p.m-p.g,z:(T*nose.z-D*vz/V+L*lz)/p.m};
  const nv={x:vx+accel.x*dt,y:vy+accel.y*dt,z:vz+accel.z*dt};
  const to={x:s.pos.x+nv.x*dt,y:s.pos.y+nv.y*dt,z:s.pos.z+nv.z*dt};
  const hit=move(world,s,to,3,'SPACE_SHIP');if(!hit.accepted)return hit;
  s.vel=nv;
  return {accepted:true,speed:Math.hypot(nv.x,nv.y,nv.z),alphaDeg:alpha,stall:alpha>p.stall};
}
/* Fixed-step driver: caller invokes once per rendered frame with measured frameDt.
 * Dropped accumulated time is reported; it is not secretly simulated.
 */
export function createCraftClock(stepFn,state,world){
  let accumulator=0;
  return function tick(frameDt,input){
    accumulator+=clamp(frameDt,0,.1);let count=0,result={accepted:true};
    while(accumulator>=CRAFT_STEP&&count<6){result=stepFn(state,input,world,CRAFT_STEP);accumulator-=CRAFT_STEP;count++;
      if(!result.accepted)break;}
    const dropped=accumulator>=CRAFT_STEP;if(dropped)accumulator=0;
    return {...result,steps:count,droppedTime:dropped,alpha:accumulator/CRAFT_STEP};
  };
}


===== FILE: public/city/fleet-place.js =====
/* ------------------------------------------------------------------
 * An epistle of placement, from Solid.
 *
 * Dear board,
 *
