<!-- SOLID CITY · part 3405 -->
, 'rgba(255,190,90,.6)'); r.addColorStop(1, 'rgba(255,120,60,0)'); g.fillStyle = r; g.fillRect(0, 0, w, h); }, false);
const gridTex = (glow) => { const t = canvasTex(128, 128, (g, w) => { g.fillStyle = glow ? "#000" : "#2a1c24"; g.fillRect(0, 0, w, w); g.fillStyle = glow ? "#5a0a16" : "#3a2430"; g.fillRect(0, 0, w, 3); g.fillRect(0, 0, 3, w); if (glow) { g.fillStyle = "#6a4a14"; g.fillRect(62, 0, 2, w); } }); t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(130, 130); t.anisotropy = 4; return t; };
const TEX_GRID = gridTex(false), TEX_GRIDE = gridTex(true);
const TONE = '#include <tonemapping_fragment>\n#include <colorspace_fragment>';

/* ======================= WORLD ======================= */
const U = { time: { value: 0 }, sun: { value: SUN }, haze: { value: new THREE.Color() }, hazeDist: { value: 8000 }, alt01: { value: 0 }, dip: { value: 0 } };

/* --- sky dome (follows the camera): the city's dusk at the ground, black space + a glowing limb above --- */
const sky = new THREE.Mesh(new THREE.SphereGeometry(1000, 32, 20), new THREE.ShaderMaterial({
  uniforms: { uAlt: U.alt01, uSun: U.sun, uDip: U.dip },
  vertexShader: 'varying vec3 vD; void main(){ vD = normalize((modelMatrix*vec4(position,1.)).xyz - cameraPosition); gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.); }',
  fragmentShader: `uniform float uAlt; uniform vec3 uSun; uniform float uDip; varying vec3 vD;
  vec3 lin(vec3 c){ return pow(c, vec3(2.2)); }
  void main(){
    vec3 d = normalize(vD); float e = d.y + uDip;
    vec2 hz = normalize(d.xz + 1e-5), hs = normalize(uSun.xz);
    float sa = max(dot(hz, hs), 0.);
    vec3 hor = mix(lin(vec3(.56,.17,.2)), lin(vec3(1.,.47,.2)), pow(sa, 3.));
    vec3 c = mix(hor, lin(vec3(.3,.08,.18)), smoothstep(0., .2, e));
    c = mix(c, lin(vec3(.06,.025,.1)), smoothstep(.15, .75, e));
    if (e < 0.) c = mix(hor * .7, lin(vec3(.09,.035,.06)), smoothstep(0., -.3, e));
    float lim = max(e, 0.);
    vec3 limb = mix(lin(vec3(.18,.38,1.)), lin(vec3(1.,.5,.26)), pow(sa, 2.)) * (exp(-lim * 30.) * .75 + exp(-lim * 8.) * .05);
    vec3 sp = (e < 0.) ? lin(vec3(.02,.03,.06)) : limb;
    c = mix(c, sp, uAlt);
    float s = max(dot(d, uSun), 0.);
    c += vec3(1., .86, .62) * pow(s, 9000.) * 40. + lin(vec3(1., .55, .25)) * (pow(s, 40.) * .5 + pow(s, 6.) * .25) * mix(1., .25, uAlt);
    gl_FragColor = vec4(c, 1.);
    ${TONE}
  }`,
  side: THREE.BackSide, depthWrite: false, depthTest: false
}));
sky.renderOrder = -10; sky.frustumCulled = false; scene.add(sky);

/* --- stars + a milky band (fade in above ~3 km) --- */
const stars = (() => {
  const n = MOBILE ? 1100 : 1800, pos = new Float32Array(n * 3), sz = new Float32Array(n), pr = mulberry(3);
  const band = new V3(0.3, 0.8, 0.5).normalize(), tmp = new V3();
  for (let i = 0; i < n; i++) {
    tmp.set(pr() * 2 - 1, pr() * 2 - 1, pr() * 2 - 1).normalize();
    if (i < n * 0.4) tmp.addScaledVector(band, -tmp.dot(band) * (0.85 + pr() * 0.15)).normalize();
    pos.set([tmp.x * 900, tmp.y * 900, tmp.z * 900], i * 3); sz[i] = 0.6 + Math.pow(pr(), 6) * 3.2;
  }
  const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.BufferAttribute(pos, 3)); g.setAttribute('aSize', new THREE.BufferAttribute(sz, 1));
  const m = new THREE.ShaderMaterial({
    uniforms: { uA: { value: 0 }, uT: U.time, uPx: { value: 1 } },
    vertexShader: 'attribute float aSize; uniform float uT; uniform float uPx; varying float vTw; void main(){ vTw = .65 + .35*sin(uT*2.3 + position.x*.7 + position.z); gl_PointSize = aSize * uPx * 1.6; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.); }',
    fragmentShader: 'uniform float uA; varying float vTw; void main(){ vec2 p = gl_PointCoord-.5; float a = smoothstep(.5,.0,length(p)); gl_FragColor = vec4(vec3(1.,.96,.9)*a*uA*vTw, 1.); }',
    blending: THREE.AdditiveBlending, depthWrite: false, depthTest: false
  });
  const p = new THREE.Points(g, m); p.renderOrder = -9; p.frustumCulled = false; scene.add(p); return p;
})();

/* --- planet: a spherical cap with fine rings near the pad (no faceting at the coast), shaded ocean + haze --- */
function capGeometry(radius, rings, segs, thetaMax) {
  const pos = [], idx = [], k = 1.1, den = Math.pow(k, rings) - 1;
  pos.push(0, radius, 0);
  for (let i = 1; i <= rings; i++) { const th = thetaMax * (Math.pow(k, i) - 1) / den; for (let j = 0; j < segs; j++) { const ph = j / segs * Math.PI * 2; pos.push(radius * Math.sin(th) * Math.cos(ph), radius * Math.cos(th), radius * Math.sin(th) * Math.sin(ph)); } }
  for (let j = 0; j < segs; j++) idx.push(0, 1 + (j + 1) % segs, 1 + j);
  for (let i = 1; i < rings; i++) { const a = 1 + (i - 1) * segs, b = 1 + i * segs; for (let j = 0; j < segs; j++) { const j2 = (j + 1) % segs; idx.push(a + j, a + j2, b + j, a + j2, b + j2, b + j); } }
  const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3)); g.setIndex(idx); return g;
}
const ocean = new THR