<!-- SOLID CITY · part 3261 -->
,.15)*f*s*.55, 1.0); }' });
  var beam = new T.Mesh(new T.CylinderGeometry(2.2, 6, 60, 20, 1, true), beamMat); beam.position.y = 30; pad.add(beam);
  var sign = new T.Mesh(new T.PlaneGeometry(14, 3.5), curve(new T.MeshBasicMaterial({ map: A.labelTex('SPACE MISSION', 'Launch pad \u00b7 shoot down the alien fleet'), side: T.DoubleSide, transparent: true })));
  sign.position.set(0, 9, -14.5); pad.add(sign);
  [-6, 6].forEach(function (x) { var post = new T.Mesh(new T.BoxGeometry(0.4, 9, 0.4), curve(new T.MeshLambertMaterial({ color: 0x3a2a1a }))); post.position.set(x, 3.6, -14.5); pad.add(post); });
  world.add(pad);
  A.portals.push({ x: PADP.x, z: PADP.z, d: { label: 'Space Mission', title: 'SPACE MISSION', go: 'LAUNCH SPACE MISSION', url: URL_SPACE + '?from=hangar',
    text: 'Jump in, blast off through the clouds into space, shoot down the alien fleet, then fly home and park on this pad. Phones: stick + FIRE + BOOST. Desktop: WASD, mouse, Space, Shift.' } });

  /* ---- 4. WORMHOLE ---- */
  var wy = A.groundAt(WH.x, WH.z);
  var wh = new T.Group(); wh.position.set(WH.x, wy, WH.z); wh.name = 'wormhole';
  var swirl = new T.ShaderMaterial({ uniforms: { uT: { value: 0 } }, transparent: true, depthWrite: false, side: T.DoubleSide, vertexShader: VS,
    fragmentShader: 'uniform float uT; varying vec2 vUv; void main(){ vec2 p = vUv*2.0-1.0; float r = length(p); if (r > 1.0) discard;' +
      ' float a = atan(p.y,p.x); float s = sin(a*3.0 - log(r+.03)*7.0 + uT*3.2); float s2 = sin(a*5.0 - log(r+.03)*11.0 - uT*2.1);' +
      ' float arms = smoothstep(.0,1.0,s*.5+.5); vec3 c = mix(vec3(.30,.05,.75), vec3(.15,.95,1.0), arms*.8 + .2*s2);' +
      ' c = mix(c, vec3(1.0,.95,1.0), smoothstep(.32,.0,r)); c *= .65 + .6*smoothstep(1.0,.6,r);' +
      ' float al = smoothstep(1.0,.82,r) * (.62 + .38*arms); al = max(al, smoothstep(.35,.0,r));' +
      ' gl_FragColor = vec4(c, al); }' });
  var disk = new T.Mesh(new T.CircleGeometry(WH_R - 0.3, 48), swirl); disk.position.y = WH_H; wh.add(disk);
  var halo = new T.Mesh(new T.PlaneGeometry(WH_R * 3.6, WH_R * 3.6), new T.ShaderMaterial({ uniforms: { uT: { value: 0 } }, transparent: true, depthWrite: false, side: T.DoubleSide, blending: T.AdditiveBlending, vertexShader: VS,
    fragmentShader: 'uniform float uT; varying vec2 vUv; void main(){ float r = length(vUv*2.0-1.0); float g = smoothstep(1.0,.3,r) * (.8+.2*sin(uT*2.0)); gl_FragColor = vec4(vec3(.55,.25,1.0)*g*.55, 1.0); }' }));
  halo.position.y = WH_H; halo.position.z = -0.05; halo.renderOrder = 1; wh.add(halo);
  var ringMat = curve(new T.MeshBasicMaterial({ color: 0xc58cff }));
  var ring = new T.Mesh(new T.TorusGeometry(WH_R, 0.85, 10, 64), ringMat); ring.position.y = WH_H; wh.add(ring);
  var orb1 = new T.Mesh(new T.TorusGeometry(WH_R + 1.6, 0.18, 6, 64), curve(new T.MeshBasicMaterial({ color: 0x5ff4ff }))); orb1.position.y = WH_H; wh.add(orb1);
  var orb2 = new T.Mesh(new T.TorusGeometry(WH_R + 2.6, 0.12, 6, 64), curve(new T.MeshBasicMaterial({ color: 0xff7ad9 }))); orb2.position.y = WH_H; wh.add(orb2);
  var stone = curve(new T.MeshLambertMaterial({ color: 0x2a2236 }));
  [-1, 1].forEach(function (s) {
    var pyl = new T.Mesh(new T.BoxGeometry(2.6, WH_H + 2, 2.6), stone); pyl.position.set(s * (WH_R + 1.4), (WH_H + 2) / 2, 0); wh.add(pyl);
    var cap = new T.Mesh(new T.OctahedronGeometry(1.4), curve(new T.MeshBasicMaterial({ color: 0x9a6bff }))); cap.position.set(s * (WH_R + 1.4), WH_H + 3.6, 0); wh.add(cap);
    A.solids.push({ x0: WH.x + s * (WH_R + 1.4) - 1.3, x1: WH.x + s * (WH_R + 1.4) + 1.3, z0: WH.z - 1.3, z1: WH.z + 1.3, y0: wy - 1, y1: wy + WH_H + 4, wormhole: true });
  });
  // ring itself (top arc) collides for flyers: a thin slab over the top of the ring
  A.solids.push({ x0: WH.x - WH_R - 1, x1: WH.x + WH_R + 1, z0: WH.z - 1, z1: WH.z + 1, y0: wy + WH_H + WH_R - 1, y1: wy + WH_H + WH_R + 1, wormhole: true });
  var plinth = new T.Mesh(new T.CylinderGeometry(WH_R + 5, WH_R + 6, 0.4, 40), curve(new T.MeshLambertMaterial({ color: 0x1b1626 }))); plinth.position.y = 0.05; wh.add(plinth);
  var wsign = new T.Mesh(new T.PlaneGeometry(13, 3.25), curve(new T.MeshBasicMaterial({ map: A.labelTex('WORMHOLE', 'to the Space Mission'), side: T.DoubleSide, transparent: true })));
  wsign.position.set(0, WH_H + WH_R + 5, 0); wh.add(wsign);
  var spr = new T.Sprite(new T.SpriteMaterial({ map: new T.CanvasTexture(glowTex(170, 110, 255)), blending: T.AdditiveBlending, depthWrite: false, transparent: true }));
  spr.scale.set(70, 70, 1); spr.position.y = WH_H; wh.add(spr);   // far-away beacon glow
  world.add(wh);

  var mats = [padMat, beamMat, swirl, halo.material];
  /* animation + trigger loop (cheap: a few uniforms and distance checks) */
  var S = A.S, cool = false, prompt = null, last = performance.now(), t = 0;
  function loop(now) {
    requestAnimationFrame(loop);
    var dt = Math.min(0.1, (now - last) / 1000); last = now; t += dt;
    var cx = A.camera.position.x + (A.origin ?