[object Object]

← back to Ticket System

auto-save: 2026-07-27T07:48:56 (1 files) — office.html

c4b15ed8fc2e3dbcc697257b17037749c025292e · 2026-07-27 07:48:56 -0700 · Steve Abrams

Files touched

Diff

commit c4b15ed8fc2e3dbcc697257b17037749c025292e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 07:48:56 2026 -0700

    auto-save: 2026-07-27T07:48:56 (1 files) — office.html
---
 office.html | 817 +++++++++++++++++++++++++++---------------------------------
 1 file changed, 368 insertions(+), 449 deletions(-)

diff --git a/office.html b/office.html
index f8d762a..9455c4f 100644
--- a/office.html
+++ b/office.html
@@ -5,43 +5,51 @@
 <meta name="viewport" content="width=device-width,initial-scale=1">
 <title>Fleet Office — live ticket floor</title>
 <style>
-  :root{ --bg:#0f1115; --panel:#141821; --line:#262a33; --dim:#8a93a5; --blue:#6db3f2; --red:#e06c75; --green:#34d399; --purple:#c9a4f2; }
+  :root{ --bg:#07080d; --panel:#10131dcc; --line:#242a3a; --dim:#8a93a5; --blue:#6db3f2; --red:#ff6b7a; --green:#3ee6a0; --purple:#c9a4f2; --amber:#ffcf6b; }
   *{box-sizing:border-box}
-  html,body{margin:0;height:100%;background:var(--bg);color:#e6e6e6;font:13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden}
+  html,body{margin:0;height:100%;background:#07080d;color:#eef1f7;font:13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden}
   #c{position:fixed;inset:0;display:block}
   .hud{position:fixed;pointer-events:none;z-index:10}
-  #top{top:0;left:0;right:0;padding:12px 16px;display:flex;gap:18px;align-items:center;
-       background:linear-gradient(#0f1115ee,#0f111500);letter-spacing:.05em}
-  #top b{color:#fff;font-size:15px;letter-spacing:.08em}
-  #top .stat{color:var(--dim)}
-  #top .stat span{color:#fff;font-weight:600}
-  .legend{position:fixed;top:52px;left:16px;z-index:10;background:var(--panel);border:1px solid var(--line);
-          border-radius:10px;padding:10px 12px;line-height:1.9;pointer-events:auto}
-  .legend .row{display:flex;align-items:center;gap:8px}
-  .dot{width:11px;height:11px;border-radius:3px;display:inline-block}
+  #top{top:0;left:0;right:0;padding:14px 18px;display:flex;gap:20px;align-items:center;
+       background:linear-gradient(#07080df2,#07080d00);letter-spacing:.05em}
+  #top b{color:#fff;font-size:15px;letter-spacing:.14em;text-shadow:0 0 18px #6db3f2aa}
+  #top .stat{color:var(--dim);font-size:12px}
+  #top .stat span{color:#fff;font-weight:700}
+  .glass{background:var(--panel);border:1px solid var(--line);border-radius:12px;
+         backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px);box-shadow:0 8px 30px #000a}
+  .legend{position:fixed;top:56px;left:18px;z-index:10;padding:11px 13px;line-height:2;pointer-events:auto}
+  .legend .row{display:flex;align-items:center;gap:9px}
+  .dot{width:11px;height:11px;border-radius:3px;display:inline-block;box-shadow:0 0 10px currentColor}
   .legend small{color:var(--dim)}
-  #ticker{position:fixed;bottom:0;left:0;right:0;z-index:10;background:linear-gradient(#0f111500,#0f1115ee);
-          padding:14px 16px 12px;pointer-events:none}
-  #ticker .lbl{color:var(--purple);letter-spacing:.14em;font-size:10px;text-transform:uppercase}
-  #ticker .msg{color:#c6ccd8;margin-top:3px;max-width:70vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+  #ticker{position:fixed;bottom:0;left:0;right:0;z-index:10;background:linear-gradient(#07080d00,#07080df2);
+          padding:16px 18px 14px;pointer-events:none}
+  #ticker .lbl{color:var(--purple);letter-spacing:.16em;font-size:10px;text-transform:uppercase;text-shadow:0 0 12px #c9a4f288}
+  #ticker .msg{color:#dfe5f0;margin-top:4px;max-width:74vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px}
   #ticker .msg b{color:#fff}
-  .ctl{position:fixed;top:52px;right:16px;z-index:10;display:flex;flex-direction:column;gap:6px;pointer-events:auto}
-  .ctl button{background:var(--panel);border:1px solid var(--line);color:#c6ccd8;border-radius:8px;
-              padding:7px 11px;cursor:pointer;font:inherit;letter-spacing:.03em}
-  .ctl button:hover{border-color:var(--blue);color:#fff}
-  .ctl button.on{border-color:var(--blue);color:var(--blue)}
-  #tip{position:fixed;z-index:11;background:#000d;border:1px solid var(--line);border-radius:8px;padding:8px 10px;
-       max-width:340px;pointer-events:none;display:none;box-shadow:0 6px 22px #000a}
-  #tip .h{color:var(--blue);font-weight:600;margin-bottom:3px}
-  #tip .s{color:var(--dim);font-size:11px}
-  #tip .t{color:#e6e6e6;margin-top:4px}
-  #err{position:fixed;bottom:52px;left:16px;z-index:12;color:var(--red);display:none}
+  .ctl{position:fixed;top:56px;right:18px;z-index:10;display:flex;flex-direction:column;gap:7px;pointer-events:auto}
+  .ctl button{background:var(--panel);border:1px solid var(--line);color:#cdd4e2;border-radius:10px;
+              padding:8px 12px;cursor:pointer;font:inherit;letter-spacing:.03em;backdrop-filter:blur(9px);transition:.15s}
+  .ctl button:hover{border-color:var(--blue);color:#fff;box-shadow:0 0 16px #6db3f244}
+  .ctl button.on{border-color:var(--blue);color:var(--blue);box-shadow:0 0 14px #6db3f233}
+  #b-ping{border-color:#c9a4f2;color:#c9a4f2}
+  #tip{position:fixed;z-index:11;max-width:360px;pointer-events:none;display:none;padding:10px 12px}
+  #tip .h{color:var(--blue);font-weight:700;margin-bottom:3px;letter-spacing:.04em}
+  #tip .s{color:var(--dim);font-size:11px;margin-top:2px}
+  #tip .t{color:#dfe5f0;margin-top:5px;font-size:12px;line-height:1.5}
+  #err{position:fixed;bottom:56px;left:18px;z-index:12;color:var(--red);display:none;text-shadow:0 0 10px #ff6b7a66}
+  #boot{position:fixed;inset:0;z-index:20;background:#07080d;display:flex;align-items:center;justify-content:center;
+        flex-direction:column;gap:14px;transition:opacity .8s;letter-spacing:.3em;color:#6db3f2}
+  #boot .r{width:120px;height:2px;background:#1a2030;overflow:hidden;border-radius:2px}
+  #boot .r i{display:block;height:100%;width:40%;background:linear-gradient(90deg,#6db3f2,#c9a4f2);
+             animation:sweep 1s infinite;box-shadow:0 0 12px #6db3f2}
+  @keyframes sweep{0%{transform:translateX(-100%)}100%{transform:translateX(320%)}}
 </style>
 </head>
 <body>
 <canvas id="c"></canvas>
+<div id="boot"><div style="font-size:13px">ENTERING FLEET OFFICE</div><div class="r"><i></i></div></div>
 <div class="hud" id="top">
-  <b>FLEET OFFICE</b>
+  <b>◆ FLEET OFFICE</b>
   <span class="stat">rooms <span id="s-rooms">0</span></span>
   <span class="stat">agents <span id="s-agents">0</span></span>
   <span class="stat">doing <span id="s-doing" style="color:var(--blue)">0</span></span>
@@ -49,21 +57,22 @@
   <span class="stat">DMs <span id="s-dms" style="color:var(--purple)">0</span></span>
   <span class="stat" style="margin-left:auto">↻ <span id="s-age">–</span></span>
 </div>
-<div class="legend">
-  <div class="row"><span class="dot" style="background:#6db3f2"></span> doing <small>active work</small></div>
-  <div class="row"><span class="dot" style="background:#e06c75"></span> blocked</div>
-  <div class="row"><span class="dot" style="background:#8a93a5"></span> open</div>
-  <div class="row"><span class="dot" style="background:#34d399"></span> done <small>(recent)</small></div>
-  <div class="row"><span class="dot" style="background:#c9a4f2"></span> DM in flight</div>
+<div class="legend glass">
+  <div class="row" style="color:#6db3f2"><span class="dot"></span> <span style="color:#eef1f7">doing</span> <small>active work</small></div>
+  <div class="row" style="color:#ff6b7a"><span class="dot"></span> <span style="color:#eef1f7">blocked</span></div>
+  <div class="row" style="color:#8a93a5"><span class="dot"></span> <span style="color:#eef1f7">open</span></div>
+  <div class="row" style="color:#3ee6a0"><span class="dot"></span> <span style="color:#eef1f7">done</span> <small>(recent)</small></div>
+  <div class="row" style="color:#c9a4f2"><span class="dot"></span> <span style="color:#eef1f7">DM in flight</span></div>
 </div>
 <div class="ctl">
+  <button id="b-orbit" class="on">Orbit: on</button>
   <button id="b-labels" class="on">Labels: on</button>
   <button id="b-done" class="on">Done rooms: on</button>
   <button id="b-follow">Follow DMs: off</button>
-  <button id="b-ping" title="Preview the DM packet animation between random agents (visual only — writes nothing)">Ping ✦</button>
+  <button id="b-ping" title="Preview the DM packet animation (visual only — writes nothing)">Ping ✦</button>
   <button id="b-reset">Reset camera</button>
 </div>
-<div id="tip"></div>
+<div id="tip" class="glass"></div>
 <div id="err"></div>
 <div class="hud" id="ticker"><div class="lbl">latest activity</div><div class="msg" id="ticker-msg">connecting to ticket board…</div></div>
 
@@ -77,469 +86,379 @@
 <script type="module">
 import * as THREE from 'three';
 import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+import { Reflector } from 'three/addons/objects/Reflector.js';
+import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
 
 // ───────────────────────── config ─────────────────────────
-const LANES   = ['doing', 'blocked', 'open', 'done'];        // kanban lanes, left→right
-const COLOR   = { doing:0x6db3f2, blocked:0xe06c75, open:0x8a93a5, done:0x34d399 };
-const ROOM    = 9;      // room footprint (units)
-const PITCH   = 12;     // room center-to-center spacing
-const LANE_W  = 40;     // horizontal gap between lanes
-const DONE_CAP= 14;     // max done rooms to render
-const POLL_MS = 4000;   // live refresh cadence
-
-// ───────────────────────── scene ─────────────────────────
-const canvas = document.getElementById('c');
-const renderer = new THREE.WebGLRenderer({ canvas, antialias:true });
-renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
-renderer.shadowMap.enabled = true;
-renderer.shadowMap.type = THREE.PCFSoftShadowMap;
-
-const scene = new THREE.Scene();
-scene.background = new THREE.Color(0x0f1115);
-scene.fog = new THREE.Fog(0x0f1115, 160, 420);
-
-const camera = new THREE.PerspectiveCamera(45, innerWidth/innerHeight, 1, 2000);
-const HOME = { pos:new THREE.Vector3(40, 215, 265), tgt:new THREE.Vector3(30, 0, 95) };
-camera.position.copy(HOME.pos);
-
-const controls = new OrbitControls(camera, canvas);
-controls.enableDamping = true;
-controls.dampingFactor = 0.08;
-controls.maxPolarAngle = Math.PI * 0.46;   // keep it looking down-ish (from above)
-controls.minDistance = 30;
-controls.maxDistance = 520;
+const LANES   = ['doing', 'blocked', 'open', 'done'];
+const COLOR   = { doing:0x6db3f2, blocked:0xff6b7a, open:0x9aa4b8, done:0x3ee6a0 };
+const ROOM=9, PITCH=12, LANE_W=42, DONE_CAP=14, POLL_MS=4000;
+
+// ───────────────────────── renderer ─────────────────────────
+const canvas=document.getElementById('c');
+const renderer=new THREE.WebGLRenderer({ canvas, antialias:true, powerPreference:'high-performance' });
+renderer.setPixelRatio(Math.min(devicePixelRatio,2));
+renderer.toneMapping=THREE.ACESFilmicToneMapping;
+renderer.toneMappingExposure=1.05;
+renderer.shadowMap.enabled=true; renderer.shadowMap.type=THREE.PCFSoftShadowMap;
+
+const scene=new THREE.Scene();
+scene.fog=new THREE.FogExp2(0x07080d, 0.0038);
+
+const camera=new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 1, 2500);
+const HOME={ pos:new THREE.Vector3(52,150,235), tgt:new THREE.Vector3(30,4,95) };
+camera.position.set(30,540,540); // fly-in start
+
+const controls=new OrbitControls(camera, canvas);
+controls.enableDamping=true; controls.dampingFactor=0.06;
+controls.maxPolarAngle=Math.PI*0.47; controls.minDistance=34; controls.maxDistance=620;
 controls.target.copy(HOME.tgt);
-
-// lights
-scene.add(new THREE.HemisphereLight(0xafc4ff, 0x0a0c10, 0.9));
-const sun = new THREE.DirectionalLight(0xffffff, 1.1);
-sun.position.set(60, 160, 40); sun.castShadow = true;
-sun.shadow.mapSize.set(2048,2048);
-Object.assign(sun.shadow.camera, { left:-260, right:260, top:260, bottom:-260, near:20, far:520 });
-scene.add(sun);
-
-// ground
-const ground = new THREE.Mesh(
-  new THREE.PlaneGeometry(1400, 1400),
-  new THREE.MeshStandardMaterial({ color:0x11141b, roughness:1 }));
-ground.rotation.x = -Math.PI/2; ground.position.y = -0.05; ground.receiveShadow = true;
-scene.add(ground);
-// subtle grid
-const grid = new THREE.GridHelper(1400, 140, 0x1c2130, 0x161a22);
-grid.position.y = 0; scene.add(grid);
-
-// ───────────────────────── helpers ─────────────────────────
-function hashHue(s){ let h=0; for(let i=0;i<s.length;i++) h=(h*31+s.charCodeAt(i))>>>0; return h%360; }
-function agentColor(name){ const c=new THREE.Color(); c.setHSL(hashHue(name)/360, 0.55, 0.62); return c; }
-
-// canvas-texture text sprite
-function makeLabel(lines, { fg='#fff', sub='#8a93a5', w=520, scale=1 } = {}){
+controls.autoRotate=true; controls.autoRotateSpeed=0.35;
+
+// environment reflections (glossy metals) — guarded for headless
+try{ const pmrem=new THREE.PMREMGenerator(renderer);
+  scene.environment=pmrem.fromScene(new RoomEnvironment(), 0.03).texture; }catch(e){}
+
+// ───────────────────────── atmosphere ─────────────────────────
+// gradient dome
+const dome=new THREE.Mesh(new THREE.SphereGeometry(1200,32,16),
+  new THREE.ShaderMaterial({ side:THREE.BackSide, depthWrite:false, uniforms:{},
+    vertexShader:`varying vec3 vp; void main(){ vp=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.); }`,
+    fragmentShader:`varying vec3 vp; void main(){ float h=normalize(vp).y*0.5+0.5;
+      vec3 top=vec3(0.03,0.05,0.11), midc=vec3(0.05,0.04,0.10), bot=vec3(0.01,0.01,0.03);
+      vec3 c=mix(bot, mix(midc,top,smoothstep(0.5,1.0,h)), smoothstep(0.0,0.6,h));
+      c+=vec3(0.05,0.03,0.09)*pow(max(0.,1.-abs(h-0.46)*6.0),2.0); // horizon glow band
+      gl_FragColor=vec4(c,1.); }` }));
+scene.add(dome);
+
+// drifting dust
+const DUST=900, dpos=new Float32Array(DUST*3);
+for(let i=0;i<DUST;i++){ dpos[i*3]=(Math.random()-0.5)*640; dpos[i*3+1]=Math.random()*160+2; dpos[i*3+2]=(Math.random()-0.2)*520; }
+const dustGeo=new THREE.BufferGeometry(); dustGeo.setAttribute('position',new THREE.BufferAttribute(dpos,3));
+const dust=new THREE.Points(dustGeo, new THREE.PointsMaterial({ color:0x8fb4ff, size:0.6, transparent:true, opacity:0.5,
+  blending:THREE.AdditiveBlending, depthWrite:false }));
+scene.add(dust);
+
+// ───────────────────────── floor ─────────────────────────
+const reflector=new Reflector(new THREE.PlaneGeometry(2000,2000), {
+  textureWidth:1024, textureHeight:1024, color:0x0a0d16 });
+reflector.rotation.x=-Math.PI/2; reflector.position.y=0; scene.add(reflector);
+// tint the mirror down so it's a sheen, not a mirror
+const sheen=new THREE.Mesh(new THREE.PlaneGeometry(2000,2000),
+  new THREE.MeshStandardMaterial({ color:0x07080d, transparent:true, opacity:0.72, roughness:0.6, metalness:0.2 }));
+sheen.rotation.x=-Math.PI/2; sheen.position.y=0.01; sheen.receiveShadow=true; scene.add(sheen);
+// glowing floor grid
+const grid=new THREE.GridHelper(1600,160,0x2a3550,0x141a28);
+grid.material.transparent=true; grid.material.opacity=0.35; grid.position.y=0.02; scene.add(grid);
+
+// ───────────────────────── lights ─────────────────────────
+scene.add(new THREE.HemisphereLight(0x9fb8ff,0x05060a,0.7));
+const key=new THREE.DirectionalLight(0xffffff,1.15); key.position.set(70,170,60);
+key.castShadow=true; key.shadow.mapSize.set(2048,2048);
+Object.assign(key.shadow.camera,{ left:-300,right:300,top:300,bottom:-300,near:20,far:600 });
+scene.add(key);
+scene.add(new THREE.DirectionalLight(0x5b7bff,0.25).translateX(-80));
+
+// ───────────────────────── postprocessing ─────────────────────────
+const composer=new EffectComposer(renderer);
+composer.addPass(new RenderPass(scene,camera));
+const bloom=new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight), 0.85, 0.7, 0.15);
+composer.addPass(bloom);
+composer.addPass(new OutputPass());
+
+// ───────────────────────── label sprites (neon) ─────────────────────────
+function makeLabel(lines,{ w=520, scale=1, glow='#6db3f2' }={}){
   const pad=18, lh=34, cnv=document.createElement('canvas');
-  const h = pad*2 + lines.length*lh;
-  cnv.width=w; cnv.height=h;
-  const g=cnv.getContext('2d');
-  g.fillStyle='rgba(15,17,21,0.82)';
-  roundRect(g,0,0,w,h,16); g.fill();
-  g.strokeStyle='rgba(120,140,180,0.35)'; g.lineWidth=2; roundRect(g,1,1,w-2,h-2,15); g.stroke();
+  const h=pad*2+lines.length*lh; const dpr=2; cnv.width=w*dpr; cnv.height=h*dpr;
+  const g=cnv.getContext('2d'); g.scale(dpr,dpr);
+  g.fillStyle='rgba(9,11,17,0.78)'; roundRect(g,0,0,w,h,15); g.fill();
+  g.strokeStyle=glow+'88'; g.lineWidth=1.5; roundRect(g,1,1,w-2,h-2,14); g.stroke();
   g.textBaseline='top';
-  lines.forEach((ln,i)=>{
-    g.fillStyle = ln.sub ? sub : fg;
-    g.font = `${ln.sub?'400 ':'600 '}${ln.sub?22:26}px -apple-system,Segoe UI,sans-serif`;
-    g.fillText(ln.t, pad, pad + i*lh);
-  });
+  lines.forEach((ln,i)=>{ g.shadowColor=ln.sub?'transparent':glow; g.shadowBlur=ln.sub?0:12;
+    g.fillStyle=ln.sub?'#9aa4b8':'#ffffff';
+    g.font=`${ln.sub?'400 22px':'700 26px'} -apple-system,Segoe UI,sans-serif`;
+    g.fillText(ln.t,pad,pad+i*lh); });
   const tex=new THREE.CanvasTexture(cnv); tex.anisotropy=4;
   const spr=new THREE.Sprite(new THREE.SpriteMaterial({ map:tex, transparent:true, depthWrite:false }));
-  spr.scale.set(w/40*scale, h/40*scale, 1);
-  return spr;
+  spr.scale.set(w/40*scale, h/40*scale, 1); return spr;
 }
 function roundRect(g,x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r);
   g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
 
-// ───────────────────────── build one room ─────────────────────────
+function hashHue(s){ let h=0; for(let i=0;i<s.length;i++) h=(h*31+s.charCodeAt(i))>>>0; return h%360; }
+function agentColor(n){ const c=new THREE.Color(); c.setHSL(hashHue(n)/360,0.7,0.6); return c; }
+const hex=n=>'#'+n.toString(16).padStart(6,'0');
+
+// ───────────────────────── room ─────────────────────────
 function buildRoom(t){
-  const grp = new THREE.Group();
-  const col = COLOR[t.status] ?? COLOR.open;
-  const floor = new THREE.Mesh(
-    new THREE.BoxGeometry(ROOM, 0.4, ROOM),
-    new THREE.MeshStandardMaterial({ color:col, roughness:0.85, metalness:0.05,
-      emissive:col, emissiveIntensity: t.status==='doing'?0.25:(t.status==='blocked'?0.2:0.06) }));
-  floor.position.y=0.2; floor.receiveShadow=true; floor.castShadow=false;
-  grp.add(floor);
-  // low walls
-  const wallMat = new THREE.MeshStandardMaterial({ color:0x1a1f29, roughness:1,
-    transparent:true, opacity:0.9 });
-  const wallH=1.8, tk=0.3;
+  const grp=new THREE.Group(); const col=COLOR[t.status]??COLOR.open;
+  // floor slab
+  const floor=new THREE.Mesh(new THREE.BoxGeometry(ROOM,0.5,ROOM),
+    new THREE.MeshStandardMaterial({ color:col, roughness:0.5, metalness:0.4,
+      emissive:col, emissiveIntensity:t.status==='doing'?0.6:(t.status==='blocked'?0.5:0.18) }));
+  floor.position.y=0.25; floor.receiveShadow=true; grp.add(floor);
+  // glowing perimeter rim (additive line loop on the top edge)
+  const rimPts=[[-ROOM/2,-ROOM/2],[ROOM/2,-ROOM/2],[ROOM/2,ROOM/2],[-ROOM/2,ROOM/2],[-ROOM/2,-ROOM/2]]
+    .map(([x,z])=>new THREE.Vector3(x,0.56,z));
+  const rim=new THREE.Line(new THREE.BufferGeometry().setFromPoints(rimPts),
+    new THREE.LineBasicMaterial({ color:col, transparent:true, opacity:0.95, blending:THREE.AdditiveBlending, depthWrite:false }));
+  grp.add(rim);
+  // glass walls
+  const wallMat=new THREE.MeshPhysicalMaterial({ color:0x0d1119, roughness:0.25, metalness:0.1,
+    transmission:0.0, transparent:true, opacity:0.32, side:THREE.DoubleSide });
+  const wallH=2.0, tk=0.18;
   for(const [dx,dz,ww,dd] of [[0,-ROOM/2,ROOM,tk],[0,ROOM/2,ROOM,tk],[-ROOM/2,0,tk,ROOM],[ROOM/2,0,tk,ROOM]]){
-    const w=new THREE.Mesh(new THREE.BoxGeometry(ww,wallH,dd), wallMat);
-    w.position.set(dx, wallH/2, dz); w.castShadow=true; grp.add(w);
+    const w=new THREE.Mesh(new THREE.BoxGeometry(ww,wallH,dd),wallMat);
+    w.position.set(dx,wallH/2,dz); grp.add(w);
   }
-  // status trim strip on the near wall
-  const trim=new THREE.Mesh(new THREE.BoxGeometry(ROOM,0.35,0.36),
-    new THREE.MeshStandardMaterial({ color:col, emissive:col, emissiveIntensity:0.8 }));
-  trim.position.set(0, wallH+0.1, ROOM/2); grp.add(trim);
-  // a little desk in the corner (a bit of office furniture)
+  // neon top trim on near wall
+  const trim=new THREE.Mesh(new THREE.BoxGeometry(ROOM,0.16,0.2),
+    new THREE.MeshBasicMaterial({ color:col }));
+  trim.position.set(0,wallH+0.05,ROOM/2); grp.add(trim);
+  // desk
   const desk=new THREE.Mesh(new THREE.BoxGeometry(2.6,0.9,1.4),
-    new THREE.MeshStandardMaterial({ color:0x2a2f3a, roughness:0.9 }));
-  desk.position.set(-ROOM/2+2, 0.65, -ROOM/2+1.4); desk.castShadow=true; grp.add(desk);
-
-  // floating label
-  const shortTitle = (t.title||'').length>44 ? t.title.slice(0,42)+'…' : (t.title||'');
-  const label = makeLabel([
-    { t: t.id.replace(/^(TK-\d+).*/, '$1') + '  ·  ' + (t.status||'').toUpperCase() },
-    { t: shortTitle, sub:true },
-    { t: '@'+(t.assignee||'unassigned') + (t.project?('   ['+t.project+']'):''), sub:true },
-  ], { scale:0.9 });
-  label.position.set(0, 6.2, 0);
-  grp.add(label);
-  grp.userData = { ticket:t, floor, trim, label, baseEmis: floor.material.emissiveIntensity, flash:0 };
+    new THREE.MeshStandardMaterial({ color:0x161b26, roughness:0.7, metalness:0.3 }));
+  desk.position.set(-ROOM/2+2,0.7,-ROOM/2+1.4); desk.castShadow=true; grp.add(desk);
+  const mon=new THREE.Mesh(new THREE.BoxGeometry(1.5,0.9,0.08),
+    new THREE.MeshStandardMaterial({ color:col, emissive:col, emissiveIntensity:0.7 }));
+  mon.position.set(-ROOM/2+2,1.55,-ROOM/2+1.0); grp.add(mon);
+
+  const label=roomLabel(t);
+  label.position.set(0,6.6,0); grp.add(label);
+  grp.userData={ ticket:t, floor, rim, trim, mon, label, baseEmis:floor.material.emissiveIntensity, flash:0, col };
   return grp;
 }
+function roomLabel(t){
+  const st=(t.title||''); const short=st.length>44?st.slice(0,42)+'…':st;
+  return makeLabel([
+    { t:t.id.replace(/^(TK-\d+).*/, '$1')+'  ·  '+(t.status||'').toUpperCase() },
+    { t:short, sub:true },
+    { t:'@'+(t.assignee||'unassigned')+(t.project?('   ['+t.project+']'):''), sub:true },
+  ], { scale:0.9, glow:hex(COLOR[t.status]??COLOR.open) });
+}
 
-// ───────────────────────── build one avatar ─────────────────────────
+// ───────────────────────── avatar ─────────────────────────
 function buildAvatar(name){
-  const grp=new THREE.Group();
-  const col=agentColor(name);
-  const body=new THREE.Mesh(new THREE.CapsuleGeometry(0.9,1.7,4,12),
-    new THREE.MeshStandardMaterial({ color:col, roughness:0.6, metalness:0.1 }));
+  const grp=new THREE.Group(); const col=agentColor(name);
+  const body=new THREE.Mesh(new THREE.CapsuleGeometry(0.9,1.7,6,16),
+    new THREE.MeshStandardMaterial({ color:col, roughness:0.35, metalness:0.5,
+      emissive:col, emissiveIntensity:0.35 }));
   body.position.y=1.7; body.castShadow=true; grp.add(body);
-  const head=new THREE.Mesh(new THREE.SphereGeometry(0.72,16,16),
-    new THREE.MeshStandardMaterial({ color:0xffe0bd, roughness:0.7 }));
+  const head=new THREE.Mesh(new THREE.SphereGeometry(0.72,20,20),
+    new THREE.MeshStandardMaterial({ color:0xffe2c4, roughness:0.6, metalness:0.1 }));
   head.position.y=3.3; head.castShadow=true; grp.add(head);
-  // a little "activity" ring at the feet (glows when working)
-  const ring=new THREE.Mesh(new THREE.RingGeometry(1.3,1.7,24),
-    new THREE.MeshBasicMaterial({ color:col, transparent:true, opacity:0.0, side:THREE.DoubleSide }));
-  ring.rotation.x=-Math.PI/2; ring.position.y=0.25; grp.add(ring);
-  const label=makeLabel([{t:name}], { w:Math.max(180,name.length*16+60), scale:0.55 });
-  label.position.set(0,4.7,0); grp.add(label);
-  grp.userData = { name, body, head, ring, label, col, target:new THREE.Vector3(),
-                   home:new THREE.Vector3(), phase:Math.random()*6.28, working:0, room:null };
+  const visor=new THREE.Mesh(new THREE.SphereGeometry(0.74,20,20,0,Math.PI*2,0,Math.PI*0.55),
+    new THREE.MeshStandardMaterial({ color:col, emissive:col, emissiveIntensity:0.5, transparent:true, opacity:0.5 }));
+  visor.position.y=3.35; visor.rotation.x=-0.3; grp.add(visor);
+  const ring=new THREE.Mesh(new THREE.RingGeometry(1.3,1.75,32),
+    new THREE.MeshBasicMaterial({ color:col, transparent:true, opacity:0.25, side:THREE.DoubleSide,
+      blending:THREE.AdditiveBlending, depthWrite:false }));
+  ring.rotation.x=-Math.PI/2; ring.position.y=0.12; grp.add(ring);
+  const light=new THREE.PointLight(col.getHex(),0.0,10); light.position.y=2.4; grp.add(light);
+  const label=makeLabel([{t:name}],{ w:Math.max(180,name.length*15+56), scale:0.5, glow:hex(col.getHex()) });
+  label.position.set(0,4.8,0); grp.add(label);
+  grp.userData={ name,body,head,ring,light,label,col,target:new THREE.Vector3(),phase:Math.random()*6.28,working:0,room:null };
   return grp;
 }
 
-// ───────────────────────── DM packet ─────────────────────────
+// ───────────────────────── DM comet ─────────────────────────
 const packets=[];
-function spawnPacket(fromPos, toPos, broadcast){
-  const col = broadcast ? 0xf0b24a : 0xc9a4f2;
-  const core=new THREE.Mesh(new THREE.SphereGeometry(0.6,12,12),
-    new THREE.MeshBasicMaterial({ color:col }));
-  const glow=new THREE.Mesh(new THREE.SphereGeometry(1.3,12,12),
-    new THREE.MeshBasicMaterial({ color:col, transparent:true, opacity:0.35 }));
-  core.add(glow);
-  const light=new THREE.PointLight(col, 2.2, 26); core.add(light);
-  scene.add(core);
-  const from=fromPos.clone(), to=toPos.clone();
-  const mid=from.clone().add(to).multiplyScalar(0.5);
-  mid.y += from.distanceTo(to)*0.28 + 6;   // arc apex
-  packets.push({ core, from, mid, to, t:0, dur:1.1 + from.distanceTo(to)/220 });
-}
-// quadratic bezier
-function qbez(a,b,c,t,out){ const u=1-t;
-  out.x=u*u*a.x+2*u*t*b.x+t*t*c.x; out.y=u*u*a.y+2*u*t*b.y+t*t*c.y; out.z=u*u*a.z+2*u*t*b.z+t*t*c.z; return out; }
-
-// ───────────────────────── state ─────────────────────────
-const roomsGrp = new THREE.Group();  scene.add(roomsGrp);
-const avatarsGrp = new THREE.Group(); scene.add(avatarsGrp);
-const roomByTicket = new Map();   // ticket id -> room group
-const avatarByName = new Map();    // agent   -> avatar group
-let seenMsgMids = null;            // Set, null until first load (so we don't storm on boot)
-let lastActionTs = 0;
-let showLabels = true, showDone = true, followDMs = false;
-let lastFetch = 0;
-
-// last-known action time of each ticket, so avatars gravitate to their newest room
-function agentLatestRoom(tickets){
-  const at = new Map(); // agent -> {ts, ticketId}
-  for(const t of tickets){
-    for(const a of t.actions||[])   consider(a.agent, a.ts, t.id);
-    for(const c of t.comments||[])  consider(c.agent, c.ts, t.id);
-    // assignee gets a weak claim on its ticket (so unassigned-but-owning shows)
-    if(t.assignee) consider(t.assignee, t.updated_at, t.id, true);
-  }
-  function consider(agent, ts, id, weak){
-    if(!agent) return;
-    const cur = at.get(agent);
-    const key = weak ? ts+'w' : ts;
-    if(!cur || cur.ts < ts) at.set(agent, { ts, ticketId:id });
-  }
-  return at;
-}
-
-function layout(tickets){
-  // group visible tickets by lane
-  const byLane = { doing:[], blocked:[], open:[], done:[] };
-  for(const t of tickets){ if(byLane[t.status]) byLane[t.status].push(t); }
-  for(const k of LANES) byLane[k].sort((a,b)=> a.updated_at<b.updated_at?1:-1);
-  byLane.done = byLane.done.slice(0, DONE_CAP);
-  const want = new Set();
-
-  LANES.forEach((lane, li)=>{
-    if(lane==='done' && !showDone) return;
-    const x = li*LANE_W - LANE_W*1.5 + 30;
-    byLane[lane].forEach((t, ri)=>{
-      const z = ri*PITCH;
-      want.add(t.id);
-      let room = roomByTicket.get(t.id);
-      if(!room){ room = buildRoom(t); roomByTicket.set(t.id, room); roomsGrp.add(room); }
-      else updateRoom(room, t);
-      room.position.set(x, 0, z);
-      room.visible = true;
-    });
-  });
-  // hide/remove rooms no longer present
-  for(const [id, room] of roomByTicket){
-    if(!want.has(id)){ room.visible=false; }
-  }
-  // lane header signs
-  ensureLaneSigns();
+function spawnPacket(from,to,broadcast){
+  const c=broadcast?0xffcf6b:0xc9a4f2;
+  const head=new THREE.Mesh(new THREE.IcosahedronGeometry(0.72,1), new THREE.MeshBasicMaterial({color:c}));
+  head.add(new THREE.Mesh(new THREE.SphereGeometry(1.7,16,16),
+    new THREE.MeshBasicMaterial({ color:c, transparent:true, opacity:0.22, blending:THREE.AdditiveBlending, depthWrite:false })));
+  const light=new THREE.PointLight(c,3.2,34); head.add(light); scene.add(head);
+  const MAX=46, buf=new Float32Array(MAX*3), tg=new THREE.BufferGeometry();
+  tg.setAttribute('position',new THREE.BufferAttribute(buf,3));
+  const trail=new THREE.Line(tg,new THREE.LineBasicMaterial({ color:c, transparent:true, opacity:0.85,
+    blending:THREE.AdditiveBlending, depthWrite:false })); trail.frustumCulled=false; scene.add(trail);
+  const mid=from.clone().add(to).multiplyScalar(0.5); mid.y+=from.distanceTo(to)*0.32+9;
+  packets.push({ head,trail,buf,tg,hist:[],MAX,from,mid,to,t:0,dur:1.25+from.distanceTo(to)/210 });
 }
-
+function qbez(a,b,c,t,o){ const u=1-t; o.x=u*u*a.x+2*u*t*b.x+t*t*c.x; o.y=u*u*a.y+2*u*t*b.y+t*t*c.y; o.z=u*u*a.z+2*u*t*b.z+t*t*c.z; return o; }
+
+// ───────────────────────── state + data ─────────────────────────
+const roomsGrp=new THREE.Group(); scene.add(roomsGrp);
+const avatarsGrp=new THREE.Group(); scene.add(avatarsGrp);
+const roomByTicket=new Map(), avatarByName=new Map();
+let seenMsgMids=null, lastActionTs=0, lastFetch=0;
+let showLabels=true, showDone=true, followDMs=false;
 let laneSigns=null;
-function ensureLaneSigns(){
-  if(laneSigns) return;
-  laneSigns = new THREE.Group(); scene.add(laneSigns);
-  LANES.forEach((lane,li)=>{
-    const x=li*LANE_W - LANE_W*1.5 + 30;
-    const s=makeLabel([{t:lane.toUpperCase()}], { fg:'#'+COLOR[lane].toString(16).padStart(6,'0'), w:260, scale:1.4 });
-    s.position.set(x, 11, -PITCH); laneSigns.add(s);
+
+function agentLatestRoom(tk){ const at=new Map();
+  const put=(a,ts,id)=>{ if(!a)return; const c=at.get(a); if(!c||c.ts<ts) at.set(a,{ts,ticketId:id}); };
+  for(const t of tk){ (t.actions||[]).forEach(a=>put(a.agent,a.ts,t.id));
+    (t.comments||[]).forEach(c=>put(c.agent,c.ts,t.id)); if(t.assignee) put(t.assignee,t.updated_at,t.id); }
+  return at; }
+
+function layout(tk){
+  const byLane={doing:[],blocked:[],open:[],done:[]};
+  for(const t of tk) if(byLane[t.status]) byLane[t.status].push(t);
+  for(const k of LANES) byLane[k].sort((a,b)=>a.updated_at<b.updated_at?1:-1);
+  byLane.done=byLane.done.slice(0,DONE_CAP);
+  const want=new Set();
+  LANES.forEach((lane,li)=>{ if(lane==='done'&&!showDone) return;
+    const x=li*LANE_W-LANE_W*1.5+30;
+    byLane[lane].forEach((t,ri)=>{ const z=ri*PITCH; want.add(t.id);
+      let room=roomByTicket.get(t.id);
+      if(!room){ room=buildRoom(t); roomByTicket.set(t.id,room); roomsGrp.add(room); } else updateRoom(room,t);
+      room.position.set(x,0,z); room.visible=true; });
   });
+  for(const [id,room] of roomByTicket) if(!want.has(id)) room.visible=false;
+  if(!laneSigns){ laneSigns=new THREE.Group(); scene.add(laneSigns);
+    LANES.forEach((lane,li)=>{ const s=makeLabel([{t:lane.toUpperCase()}],{ w:260, scale:1.5, glow:hex(COLOR[lane]) });
+      s.position.set(li*LANE_W-LANE_W*1.5+30,12,-PITCH); laneSigns.add(s); }); }
 }
-
-function updateRoom(room, t){
-  const u=room.userData; const prev=u.ticket; u.ticket=t;
-  const col = COLOR[t.status] ?? COLOR.open;
-  u.floor.material.color.setHex(col);
-  u.floor.material.emissive.setHex(col);
-  u.baseEmis = t.status==='doing'?0.25:(t.status==='blocked'?0.2:0.06);
-  u.trim.material.color.setHex(col); u.trim.material.emissive.setHex(col);
-  // refresh label if status/assignee changed
-  if(prev.status!==t.status || prev.assignee!==t.assignee){
-    room.remove(u.label); u.label.material.map.dispose(); u.label.material.dispose();
-    const shortTitle=(t.title||'').length>44 ? t.title.slice(0,42)+'…' : (t.title||'');
-    u.label=makeLabel([
-      { t: t.id.replace(/^(TK-\d+).*/, '$1') + '  ·  ' + (t.status||'').toUpperCase() },
-      { t: shortTitle, sub:true },
-      { t: '@'+(t.assignee||'unassigned') + (t.project?('   ['+t.project+']'):''), sub:true },
-    ], { scale:0.9 });
-    u.label.position.set(0,6.2,0); room.add(u.label);
-  }
+function updateRoom(room,t){ const u=room.userData, prev=u.ticket; u.ticket=t;
+  const col=COLOR[t.status]??COLOR.open; u.col=col;
+  u.floor.material.color.setHex(col); u.floor.material.emissive.setHex(col);
+  u.rim.material.color.setHex(col); u.trim.material.color.setHex(col); u.mon.material.color.setHex(col); u.mon.material.emissive.setHex(col);
+  u.baseEmis=t.status==='doing'?0.6:(t.status==='blocked'?0.5:0.18);
+  if(prev.status!==t.status||prev.assignee!==t.assignee){ room.remove(u.label);
+    u.label.material.map.dispose(); u.label.material.dispose();
+    u.label=roomLabel(t); u.label.position.set(0,6.6,0); room.add(u.label); }
 }
-
-// spread N avatars around a room center so they don't overlap
-function slotInRoom(roomPos, i, n){
-  if(n<=1) return new THREE.Vector3(roomPos.x, 0, roomPos.z+1.2);
-  const r=Math.min(2.6, 1.2+n*0.15), a=(i/n)*Math.PI*2;
-  return new THREE.Vector3(roomPos.x + Math.cos(a)*r, 0, roomPos.z + Math.sin(a)*r);
+function slotInRoom(p,i,n){ if(n<=1) return new THREE.Vector3(p.x,0,p.z+1.2);
+  const r=Math.min(2.6,1.2+n*0.15),a=(i/n)*Math.PI*2; return new THREE.Vector3(p.x+Math.cos(a)*r,0,p.z+Math.sin(a)*r); }
+function placeAvatars(tk){ const latest=agentLatestRoom(tk), perRoom=new Map(), assign=[];
+  for(const [name,info] of latest){ const room=roomByTicket.get(info.ticketId);
+    const rid=(room&&room.visible)?info.ticketId:null; assign.push({name,rid});
+    if(rid) perRoom.set(rid,(perRoom.get(rid)||0)+1); }
+  const lobby=[], idx=new Map();
+  for(const a of assign){ let av=avatarByName.get(a.name);
+    if(!av){ av=buildAvatar(a.name); avatarByName.set(a.name,av); avatarsGrp.add(av);
+      av.position.set(a.rid?roomByTicket.get(a.rid).position.x:-46, 0, a.rid?roomByTicket.get(a.rid).position.z:30); }
+    if(a.rid){ const room=roomByTicket.get(a.rid),n=perRoom.get(a.rid),i=idx.get(a.rid)||0; idx.set(a.rid,i+1);
+      av.userData.target.copy(slotInRoom(room.position,i,n)); av.userData.room=a.rid; } else lobby.push(av); }
+  lobby.forEach((av,i)=>{ av.userData.target.set(-48+(i%10)*7,0,26+Math.floor(i/10)*7); av.userData.room=null; });
 }
 
-function placeAvatars(tickets){
-  const latest = agentLatestRoom(tickets);
-  // count agents per room for slotting
-  const perRoom = new Map();
-  const assign = []; // {name, roomId}
-  for(const [name, info] of latest){
-    const room = roomByTicket.get(info.ticketId);
-    const rid = (room && room.visible) ? info.ticketId : null;
-    assign.push({ name, rid });
-    if(rid) perRoom.set(rid, (perRoom.get(rid)||0)+1);
-  }
-  // also include DM-only agents (parked in lobby)
-  const lobby=[];
-  const idx = new Map();
-  for(const a of assign){
-    let av = avatarByName.get(a.name);
-    if(!av){ av=buildAvatar(a.name); avatarByName.set(a.name, av); avatarsGrp.add(av); }
-    if(a.rid){
-      const room=roomByTicket.get(a.rid);
-      const n=perRoom.get(a.rid); const i=idx.get(a.rid)||0; idx.set(a.rid,i+1);
-      av.userData.target.copy(slotInRoom(room.position, i, n));
-      av.userData.room=a.rid;
-    } else lobby.push(av);
-  }
-  lobby.forEach((av,i)=>{
-    av.userData.target.set(-46 + (i%10)*7, 0, 30 + Math.floor(i/10)*7);
-    av.userData.room=null;
-  });
-  return latest;
-}
-
-// ───────────────────────── live data ─────────────────────────
 async function poll(){
   try{
-    // Build from location.origin (never contains userinfo) so an embedded-credential
-    // page URL — e.g. opened as http://user:pass@host/office — can't poison the
-    // fetch URL. The Fetch spec rejects any Request built from a URL with credentials.
-    const api = p => new URL(p, location.origin).href;
-    const [tr, mr] = await Promise.all([
-      fetch(api('/api/tickets'), {credentials:'same-origin'}),
-      fetch(api('/api/messages'),{credentials:'same-origin'}),
-    ]);
+    const api=p=>new URL(p,location.origin).href;   // no embedded creds → fetch-legal
+    const [tr,mr]=await Promise.all([ fetch(api('/api/tickets'),{credentials:'same-origin'}),
+                                      fetch(api('/api/messages'),{credentials:'same-origin'}) ]);
     if(!tr.ok||!mr.ok) throw new Error('HTTP '+tr.status+'/'+mr.status);
-    const tickets = await tr.json();
-    const messages = await mr.json();
-    document.getElementById('err').style.display='none';
-    apply(tickets, messages);
-    lastFetch = Date.now();
-  }catch(e){
-    const el=document.getElementById('err'); el.textContent='⚠ '+e.message+' — retrying'; el.style.display='block';
-  }
+    apply(await tr.json(), await mr.json());
+    document.getElementById('err').style.display='none'; lastFetch=Date.now();
+  }catch(e){ const el=document.getElementById('err'); el.textContent='⚠ '+e.message+' — retrying'; el.style.display='block'; }
 }
+const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+function worldPos(av){ const v=new THREE.Vector3(); av.getWorldPosition(v); v.y=3.3; return v; }
 
-function apply(tickets, messages){
-  layout(tickets);
-  const latest = placeAvatars(tickets);
-
-  // ── flash rooms with brand-new actions, mark those agents "working" ──
-  let maxTs = lastActionTs;
-  let latestActivity = null;
-  for(const t of tickets){
-    for(const a of t.actions||[]){
-      const ts=+new Date(a.ts);
-      if(ts>lastActionTs){
-        const room=roomByTicket.get(t.id);
-        if(room) room.userData.flash = 1.0;
-        const av=avatarByName.get(a.agent);
-        if(av) av.userData.working = 2.0;
-      }
-      if(ts>=+new Date((latestActivity?.ts)||0)) latestActivity={ agent:a.agent, ts:a.ts, text:a.text, id:t.id };
-      if(ts>maxTs) maxTs=ts;
-    }
-  }
-  lastActionTs = maxTs;
-  if(latestActivity){
-    document.getElementById('ticker-msg').innerHTML =
-      `<b>@${esc(latestActivity.agent)}</b> · ${esc(latestActivity.id.replace(/^(TK-\d+).*/,'$1'))} — ${esc((latestActivity.text||'').slice(0,180))}`;
-  }
-
-  // ── DM packets: animate only messages newer than last poll ──
-  let broadcastAll = [];
-  if(seenMsgMids===null){ seenMsgMids=new Set(messages.map(m=>m.mid)); }
-  else{
-    for(const m of messages){
-      if(seenMsgMids.has(m.mid)) continue;
-      seenMsgMids.add(m.mid);
-      const from = avatarByName.get(m.from);
-      if(!from) continue;
-      const bc = (m.to==='all'||m.to==='*'||m.to==='everyone');
-      if(bc){
-        for(const [nm,av] of avatarByName){ if(nm!==m.from) spawnPacket(worldPos(from), worldPos(av), true); }
-      }else{
-        const to = avatarByName.get(m.to);
-        if(to) spawnPacket(worldPos(from), worldPos(to), false);
-      }
-    }
-  }
-
-  // HUD
-  const counts={doing:0,blocked:0,open:0,done:0};
-  for(const t of tickets) if(counts[t.status]!=null) counts[t.status]++;
-  document.getElementById('s-rooms').textContent = tickets.filter(t=>t.status!=='done').length + (showDone?Math.min(counts.done,DONE_CAP):0);
-  document.getElementById('s-agents').textContent = avatarByName.size;
-  document.getElementById('s-doing').textContent = counts.doing;
-  document.getElementById('s-blocked').textContent = counts.blocked;
-  document.getElementById('s-dms').textContent = messages.length;
+function apply(tk,msgs){
+  layout(tk); placeAvatars(tk);
+  let maxTs=lastActionTs, latest=null;
+  for(const t of tk) for(const a of t.actions||[]){ const ts=+new Date(a.ts);
+    if(ts>lastActionTs){ const room=roomByTicket.get(t.id); if(room) room.userData.flash=1;
+      const av=avatarByName.get(a.agent); if(av) av.userData.working=2.2; }
+    if(ts>=+new Date((latest?.ts)||0)) latest={agent:a.agent,ts:a.ts,text:a.text,id:t.id};
+    if(ts>maxTs) maxTs=ts; }
+  lastActionTs=maxTs;
+  if(latest) document.getElementById('ticker-msg').innerHTML=
+    `<b>@${esc(latest.agent)}</b> · ${esc(latest.id.replace(/^(TK-\d+).*/,'$1'))} — ${esc((latest.text||'').slice(0,190))}`;
+  if(seenMsgMids===null) seenMsgMids=new Set(msgs.map(m=>m.mid));
+  else for(const m of msgs){ if(seenMsgMids.has(m.mid)) continue; seenMsgMids.add(m.mid);
+    const from=avatarByName.get(m.from); if(!from) continue;
+    if(m.to==='all'||m.to==='*'||m.to==='everyone'){ for(const [nm,av] of avatarByName) if(nm!==m.from) spawnPacket(worldPos(from),worldPos(av),true); }
+    else { const to=avatarByName.get(m.to); if(to) spawnPacket(worldPos(from),worldPos(to),false); } }
+  const cnt={doing:0,blocked:0,open:0,done:0}; for(const t of tk) if(cnt[t.status]!=null) cnt[t.status]++;
+  document.getElementById('s-rooms').textContent=(cnt.doing+cnt.blocked+cnt.open+(showDone?Math.min(cnt.done,DONE_CAP):0));
+  document.getElementById('s-agents').textContent=avatarByName.size;
+  document.getElementById('s-doing').textContent=cnt.doing; document.getElementById('s-blocked').textContent=cnt.blocked;
+  document.getElementById('s-dms').textContent=msgs.length;
 }
 
-function worldPos(av){ const v=new THREE.Vector3(); av.getWorldPosition(v); v.y=3.3; return v; }
-function esc(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
-
-// ───────────────────────── interaction: hover tooltip ─────────────────────────
-const ray=new THREE.Raycaster(); const mouse=new THREE.Vector2(); let hover=null;
-addEventListener('pointermove', e=>{
-  mouse.x=(e.clientX/innerWidth)*2-1; mouse.y=-(e.clientY/innerHeight)*2+1;
-  hover={x:e.clientX,y:e.clientY};
-});
-function updateTip(){
-  const tip=document.getElementById('tip'); if(!hover){ tip.style.display='none'; return; }
-  ray.setFromCamera(mouse, camera);
-  const hits=ray.intersectObjects(roomsGrp.children, true);
-  const room = hits.find(h=>{ let o=h.object; while(o&&o.parent){ if(o.userData&&o.userData.ticket) return true; o=o.parent;} return false; });
-  if(!room){ tip.style.display='none'; return; }
-  let o=room.object; while(o && !(o.userData&&o.userData.ticket)) o=o.parent;
-  const t=o.userData.ticket;
+// ───────────────────────── hover tooltip ─────────────────────────
+const ray=new THREE.Raycaster(), mouse=new THREE.Vector2(); let hover=null;
+addEventListener('pointermove',e=>{ mouse.x=(e.clientX/innerWidth)*2-1; mouse.y=-(e.clientY/innerHeight)*2+1; hover={x:e.clientX,y:e.clientY}; });
+function updateTip(){ const tip=document.getElementById('tip'); if(!hover){ tip.style.display='none'; return; }
+  ray.setFromCamera(mouse,camera); const hits=ray.intersectObjects(roomsGrp.children,true);
+  let o=hits.length?hits[0].object:null; while(o&&!(o.userData&&o.userData.ticket)) o=o.parent;
+  if(!o){ tip.style.display='none'; return; } const t=o.userData.ticket;
   const last=(t.actions&&t.actions[t.actions.length-1])||(t.comments&&t.comments[t.comments.length-1]);
   tip.innerHTML=`<div class="h">${esc(t.id.replace(/^(TK-\d+).*/,'$1'))} · ${esc(t.status.toUpperCase())}</div>
-    <div>${esc(t.title||'')}</div>
-    <div class="s">@${esc(t.assignee||'unassigned')}${t.project?(' · '+esc(t.project)):''} · ${(t.actions||[]).length} actions · ${(t.comments||[]).length} comments</div>
+    <div>${esc(t.title||'')}</div><div class="s">@${esc(t.assignee||'unassigned')}${t.project?(' · '+esc(t.project)):''} · ${(t.actions||[]).length} actions · ${(t.comments||[]).length} comments</div>
     ${last?`<div class="t">${esc((last.text||'').slice(0,220))}</div>`:''}`;
-  tip.style.display='block';
-  tip.style.left=Math.min(hover.x+14, innerWidth-360)+'px';
-  tip.style.top=Math.min(hover.y+14, innerHeight-160)+'px';
-}
-
-// ───────────────────────── controls (buttons) ─────────────────────────
-document.getElementById('b-labels').onclick=e=>{ showLabels=!showLabels; e.target.className=showLabels?'on':''; e.target.textContent='Labels: '+(showLabels?'on':'off'); };
-document.getElementById('b-done').onclick=e=>{ showDone=!showDone; e.target.className=showDone?'on':''; e.target.textContent='Done rooms: '+(showDone?'on':'off'); poll(); };
-document.getElementById('b-follow').onclick=e=>{ followDMs=!followDMs; e.target.className=followDMs?'on':''; e.target.textContent='Follow DMs: '+(followDMs?'on':'off'); };
-document.getElementById('b-reset').onclick=()=>{ camera.position.copy(HOME.pos); controls.target.copy(HOME.tgt); };
-// Preview the DM packet animation between random visible agents (visual only — no event written).
-document.getElementById('b-ping').onclick=()=>{
-  const avs=[...avatarByName.values()]; if(avs.length<2) return;
-  for(let k=0;k<5;k++){
-    const a=avs[(Math.random()*avs.length)|0], b=avs[(Math.random()*avs.length)|0];
-    if(a!==b) spawnPacket(worldPos(a), worldPos(b), Math.random()<0.25);
-  }
-};
-window.__ping = () => document.getElementById('b-ping').click();  // recording hook
-window.__orbit = on => { controls.autoRotate = on; controls.autoRotateSpeed = 0.6; };  // recording hook
+  tip.style.display='block'; tip.style.left=Math.min(hover.x+14,innerWidth-372)+'px'; tip.style.top=Math.min(hover.y+14,innerHeight-170)+'px'; }
+
+// ───────────────────────── buttons ─────────────────────────
+const B=id=>document.getElementById(id);
+B('b-orbit').onclick=e=>{ controls.autoRotate=!controls.autoRotate; e.target.className=controls.autoRotate?'on':''; e.target.textContent='Orbit: '+(controls.autoRotate?'on':'off'); };
+B('b-labels').onclick=e=>{ showLabels=!showLabels; e.target.className=showLabels?'on':''; e.target.textContent='Labels: '+(showLabels?'on':'off'); };
+B('b-done').onclick=e=>{ showDone=!showDone; e.target.className=showDone?'on':''; e.target.textContent='Done rooms: '+(showDone?'on':'off'); poll(); };
+B('b-follow').onclick=e=>{ followDMs=!followDMs; e.target.className=followDMs?'on':''; e.target.textContent='Follow DMs: '+(followDMs?'on':'off'); };
+B('b-reset').onclick=()=>{ boot.t=1; camera.position.copy(HOME.pos); controls.target.copy(HOME.tgt); hideBoot(); };
+let bootHidden=false;
+function hideBoot(){ if(bootHidden) return; bootHidden=true; const b=document.getElementById('boot');
+  b.style.opacity=0; setTimeout(()=>b.style.display='none',800); }
+setTimeout(hideBoot, 3400);  // wall-clock guarantee the curtain always lifts, even on a slow GPU
+B('b-ping').onclick=()=>{ const a=[...avatarByName.values()]; if(a.length<2) return;
+  for(let k=0;k<5;k++){ const x=a[(Math.random()*a.length)|0],y=a[(Math.random()*a.length)|0]; if(x!==y) spawnPacket(worldPos(x),worldPos(y),Math.random()<0.25); } };
+window.__ping=()=>B('b-ping').click(); window.__orbit=on=>{ controls.autoRotate=on; };
 
 // ───────────────────────── animate ─────────────────────────
-const clock=new THREE.Clock();
+const clock=new THREE.Clock(); const boot={ t:0, from:new THREE.Vector3(30,540,540) };
 function tick(){
-  const dt=Math.min(clock.getDelta(), 0.05); const el=clock.elapsedTime;
-
-  // avatars: walk toward target, bob, working ring
-  for(const av of avatarByName.values()){
-    const u=av.userData;
-    av.position.lerp(u.target, 1-Math.pow(0.001, dt));
-    const moving = av.position.distanceToSquared(u.target) > 0.04;
-    const bob = moving ? Math.abs(Math.sin(el*7+u.phase))*0.25 : Math.sin(el*2+u.phase)*0.06;
-    u.body.position.y = 1.7 + bob; u.head.position.y = 3.3 + bob;
-    if(moving){ const dir=u.target.clone().sub(av.position); av.rotation.y = Math.atan2(dir.x, dir.z); }
-    if(u.working>0){ u.working-=dt; }
-    const ringOn = Math.max(0, u.working>0 ? 1 : (u.room? (u.roomStatusDoing?0.7:0):0));
-    u.ring.material.opacity = (u.working>0? (0.55+0.35*Math.sin(el*10)) : 0);
-    u.ring.scale.setScalar(1 + (u.working>0? 0.15*Math.sin(el*10):0));
-    u.label.visible = showLabels;
-  }
-
-  // rooms: doing pulse + action flash
-  for(const room of roomByTicket.values()){
-    if(!room.visible) continue;
-    const u=room.userData;
+  const dt=Math.min(clock.getDelta(),0.05), el=clock.elapsedTime;
+
+  // cinematic fly-in
+  if(boot.t<1){ boot.t=Math.min(1,boot.t+dt/2.6); const e=1-Math.pow(1-boot.t,3);
+    camera.position.lerpVectors(boot.from,HOME.pos,e);
+    if(boot.t>=1) hideBoot(); }
+
+  // avatars
+  for(const av of avatarByName.values()){ const u=av.userData;
+    av.position.lerp(u.target,1-Math.pow(0.0015,dt));
+    const moving=av.position.distanceToSquared(u.target)>0.05;
+    const bob=moving?Math.abs(Math.sin(el*7+u.phase))*0.28:Math.sin(el*2+u.phase)*0.07;
+    u.body.position.y=1.7+bob; u.head.position.y=3.3+bob;
+    if(moving){ const d=u.target.clone().sub(av.position); av.rotation.y=Math.atan2(d.x,d.z); }
+    if(u.working>0) u.working-=dt;
+    const w=u.working>0; u.ring.material.opacity=w?(0.5+0.4*Math.sin(el*10)):0.18;
+    u.ring.scale.setScalar(1+(w?0.18*Math.sin(el*10):0)); u.light.intensity=w?2.0+Math.sin(el*12):0.0;
+    u.label.visible=showLabels; }
+
+  // rooms
+  for(const room of roomByTicket.values()){ if(!room.visible) continue; const u=room.userData;
     let em=u.baseEmis;
-    if(u.ticket.status==='doing') em = 0.22 + 0.14*Math.sin(el*3 + room.position.z*0.2);
-    if(u.ticket.status==='blocked') em = 0.16 + 0.12*Math.sin(el*6);
-    if(u.flash>0){ em += u.flash*0.9; u.flash-=dt*1.1; }
-    u.floor.material.emissiveIntensity=em;
-    u.trim.material.emissiveIntensity = 0.6 + (u.flash>0?u.flash:0)*1.2;
-    u.label.visible = showLabels;
-  }
-  if(laneSigns) laneSigns.children.forEach(s=>s.visible=showLabels);
-
-  // packets
-  for(let i=packets.length-1;i>=0;i--){
-    const p=packets[i]; p.t += dt/p.dur;
-    if(p.t>=1){ scene.remove(p.core); p.core.traverse(o=>{ if(o.material)o.material.dispose?.(); if(o.geometry)o.geometry.dispose?.(); }); packets.splice(i,1);
-      // little arrival pop = flash target avatar working ring handled elsewhere; skip
-      continue; }
-    qbez(p.from, p.mid, p.to, p.t, p.core.position);
-    const s=1+0.4*Math.sin(p.t*Math.PI); p.core.scale.setScalar(s);
-  }
-  if(followDMs && packets.length){ controls.target.lerp(packets[0].core.position, 0.05); }
-
-  // HUD age
-  if(lastFetch){ document.getElementById('s-age').textContent = Math.round((Date.now()-lastFetch)/1000)+'s ago'; }
-
-  updateTip();
-  controls.update();
-  renderer.render(scene,camera);
+    if(u.ticket.status==='doing') em=0.5+0.28*Math.sin(el*3+room.position.z*0.2);
+    else if(u.ticket.status==='blocked') em=0.4+0.25*Math.sin(el*6);
+    if(u.flash>0){ em+=u.flash*1.3; u.flash-=dt*1.1; }
+    u.floor.material.emissiveIntensity=em; u.rim.material.opacity=0.7+0.3*Math.sin(el*2+room.position.x);
+    u.label.visible=showLabels; }
+  if(laneSigns) laneSigns.children.forEach((s,i)=>{ s.visible=showLabels; s.position.y=12+Math.sin(el*1.2+i)*0.5; });
+
+  // packets + trails
+  for(let i=packets.length-1;i>=0;i--){ const p=packets[i]; p.t+=dt/p.dur;
+    if(p.t>=1){ scene.remove(p.head,p.trail); p.head.traverse(o=>{o.material?.dispose?.();o.geometry?.dispose?.();});
+      p.trail.material.dispose(); p.tg.dispose(); packets.splice(i,1); continue; }
+    qbez(p.from,p.mid,p.to,p.t,p.head.position);
+    p.head.scale.setScalar(1+0.4*Math.sin(p.t*Math.PI)); p.head.rotation.y+=dt*6; p.head.rotation.x+=dt*4;
+    p.hist.push(p.head.position.clone()); if(p.hist.length>p.MAX) p.hist.shift();
+    for(let j=0;j<p.hist.length;j++){ p.buf[j*3]=p.hist[j].x; p.buf[j*3+1]=p.hist[j].y; p.buf[j*3+2]=p.hist[j].z; }
+    p.tg.setDrawRange(0,p.hist.length); p.tg.attributes.position.needsUpdate=true;
+    p.trail.material.opacity=0.85*(1-p.t*0.3); }
+  if(followDMs&&packets.length) controls.target.lerp(packets[0].head.position,0.05);
+
+  // dust drift
+  const dp=dust.geometry.attributes.position; for(let i=0;i<DUST;i++){ dp.array[i*3+1]+=dt*1.2;
+    if(dp.array[i*3+1]>170) dp.array[i*3+1]=2; } dp.needsUpdate=true; dust.rotation.y=el*0.01;
+
+  if(lastFetch) document.getElementById('s-age').textContent=Math.round((Date.now()-lastFetch)/1000)+'s ago';
+  updateTip(); controls.update(); composer.render();
   requestAnimationFrame(tick);
 }
 
-// ───────────────────────── boot ─────────────────────────
-function resize(){ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); }
-addEventListener('resize', resize); resize();
-poll(); setInterval(poll, POLL_MS); tick();
+function resize(){ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
+  renderer.setSize(innerWidth,innerHeight); composer.setSize(innerWidth,innerHeight); bloom.setSize(innerWidth,innerHeight); }
+addEventListener('resize',resize); resize();
+poll(); setInterval(poll,POLL_MS); tick();
 </script>
 </body>
 </html>

← ca85224 office: fetch API via location.origin so embedded-credential  ·  back to Ticket System  ·  office: professional polish pass (design-agent driven) — Int 6431278 →