<!-- SOLID CITY · part 2101 -->
the upper facades become portal arrays ----
  // (King: "the rest will be port holes aka portals")
  (function(){
    var pm = new T.MeshBasicMaterial({ map: null, transparent: true, side: T.DoubleSide });
    // Build a shared porthole texture once
    var pc = document.createElement('canvas'); pc.width = pc.height = 128;
    var px = pc.getContext('2d');
    var pgr = px.createRadialGradient(64,64,4,64,64,64);
    pgr.addColorStop(0,'#fff8e8'); pgr.addColorStop(0.25,'#f2c86a');
    pgr.addColorStop(0.5,'#9a6ae8'); pgr.addColorStop(0.75,'#4a6ae8'); pgr.addColorStop(1,'#1a1a3a');
    px.fillStyle = pgr; px.beginPath(); px.arc(64,64,64,0,7); px.fill();
    px.strokeStyle = '#f2c86a'; px.lineWidth = 3;
    for (var pr = 12; pr < 64; pr += 13) { px.beginPath(); px.arc(64,64,pr,0,7); px.stroke(); }
    var ptex = new T.CanvasTexture(pc); ptex.encoding = T.sRGBEncoding;
    var pmat = new T.MeshBasicMaterial({ map: ptex, transparent: true, side: T.DoubleSide });
    window._hyperPortholes = [];
    // Portholes on the 2 podium towers (above podium, every 25m)
    [pickA, pickB].forEach(function(tb){
      if(!tb) return;
      var baseY = 28; // podium top
      for (var py = baseY+18; py < baseY+tb.h; py += 26) {
        for (var f = 0; f < 4; f++) {
          var ang = f*Math.PI/2;
          var rad = tb.w*0.62;
          var hx = tb.x + Math.cos(ang)*rad, hz = tb.z + Math.sin(ang)*rad;
          var hole = new T.Mesh(new T.CircleGeometry(3.2, 20), pmat);
          hole.position.set(hx, py, hz);
          hole.rotation.y = -ang + Math.PI/2;
          // face outward
          hole.lookAt(hx*2-tb.x, py, hz*2-tb.z);
          root.add(hole);
          window._hyperPortholes.push(hole);
        }
      }
    });
    // Portholes on spire upper cylinders (every 30m from y=120 to 480)
    for (var sy = 130; sy < 480; sy += 32) {
      for (var sf = 0; sf < 6; sf++) {
        var sa = sf*Math.PI/3;
        var sr = 20 - (sy/480)*10; // taper
        var shx = Math.cos(sa)*sr, shz = -58 + Math.sin(sa)*sr;
        var shole = new T.Mesh(new T.CircleGeometry(2.8, 20), pmat);
        shole.position.set(shx, sy, shz);
        shole.lookAt(shx*2, sy, shz*2+58);
        root.add(shole);
        window._hyperPortholes.push(shole);
      }
    }
    // Portholes on all other towers (every 30m, 2 per face)
    built.forEach(function(tb){
      if(tb===pickA||tb===pickB) return;
      for (var py2 = 40; py2 < tb.h; py2 += 34) {
        for (var f2 = 0; f2 < 4; f2++) {
          var ang2 = f2*Math.PI/2 + 0.4;
          var rad2 = tb.w*0.6;
          var hx2 = tb.x + Math.cos(ang2)*rad2, hz2 = tb.z + Math.sin(ang2)*rad2;
          var hole2 = new T.Mesh(new T.CircleGeometry(2.6, 20), pmat);
          hole2.position.set(hx2, py2, hz2);
          hole2.lookAt(hx2*2-tb.x, py2, hz2*2-tb.z);
          root.add(hole2);
          window._hyperPortholes.push(hole2);
        }
      }
    });
  })();

  // ---- central plaza: fountain ----
  var plaza=new T.Mesh(new T.CylinderGeometry(42,44,0.8,40),new T.MeshLambertMaterial({color:0x363c46}));
  plaza.position.y=0.5;root.add(plaza);
  var basin=new T.Mesh(new T.CylinderGeometry(12,14,2.4,24),steel);basin.position.y=1.6;root.add(basin);
  var water=new T.Mesh(new T.CylinderGeometry(11,11,0.6,24),
    new T.MeshBasicMaterial({color:0x6ab8e8,transparent:true,opacity:0.85}));
  water.position.y=2.6;water.name='fountainwater';root.add(water);
  var jet=new T.Mesh(new T.CylinderGeometry(0.8,1.6,16,8),
    new T.MeshBasicMaterial({color:0xbfe4ff,transparent:true,opacity:0.7}));
  jet.position.y=10;jet.name='fountainjet';root.add(jet);
  var obel=new T.Mesh(new T.CylinderGeometry(2,3.4,22,4),gold);
  obel.position.y=11;obel.rotation.y=Math.PI/4;root.add(obel);
  foot.push([0,0,30,30,0,24,0]);
  var dmark=quietMarker(T,'THE HYPER CITY');dmark.position.set(0,0,48);root.add(dmark);

  // ---- waterfront promenade (west edge) ----
  var prom=new T.Mesh(new T.BoxGeometry(36,1.2,380),new T.MeshLambertMaterial({color:0x3a3f47}));
  prom.position.set(-192,0.4,0);root.add(prom);
  // railing (collision so he doesn't stroll into the sea)
  var railM=new T.MeshLambertMaterial({color:0x8a94a0});
  for(var rz=-180;rz<=180;rz+=12){
    var post=new T.Mesh(new T.BoxGeometry(0.5,3,0.5),railM);
    post.position.set(-209,2.2,rz);root.add(post);
  }
  var rail=new T.Mesh(new T.BoxGeometry(0.6,0.5,372),railM);
  rail.position.set(-209,3.8,0);root.add(rail);
  foot.push([-209,0,2,372,0,4,0]);
  // piers + boats
  var boatHullM=new T.MeshLambertMaterial({color:0x5a3a28});
  var boatMastM=new T.MeshLambertMaterial({color:0x8a6a48});
  var sailM=new T.MeshLambertMaterial({color:0xe8e0d0,side:T.DoubleSide});
  [-90,30,120].forEach(function(pz){
    var pier=new T.Mesh(new T.BoxGeometry(70,1.6,10),deckM);
    pier.position.set(-244,0.2,pz);root.add(pier);
    foot.push([-244,pz,70,10,0,1.2,0]);
    for(var b=0;b<2;b++){
      var bx=-244+(b?18:-14), bz=pz+(b?6:-5);
      var hull=new T.M