<!-- SOLUV · part 442 -->
tx.fillRect(hx, yT + fs * 0.06, fs * 0.12, fs * 0.1); ctx.fillRect(hx, yB - fs * 0.16, fs * 0.12, fs * 0.1); }
      const cx = (x0 + x1) / 2, cy = (yT + yB) / 2; ctx.beginPath(); ctx.moveTo(cx - fs * 0.12, cy - fs * 0.17); ctx.lineTo(cx + fs * 0.18, cy); ctx.lineTo(cx - fs * 0.12, cy + fs * 0.17); ctx.closePath(); ctx.fill();
      ctx.globalCompositeOperation = 'source-over'; adv = fs * 1.16;
    } else if (ch === CAM || ch === CAM_STILL) { // placeholder video thumbnail: small rounded frame with a head-and-shoulders silhouette
      const x0 = buffer + fs * 0.06, x1 = buffer + fs * 1.0, yT = this.baseY - fs * 0.8, yB = this.baseY + fs * 0.1, r = fs * 0.14;
      ctx.lineWidth = fs * 0.075; ctx.strokeStyle = '#000'; ctx.beginPath(); ctx.roundRect(x0, yT, x1 - x0, yB - yT, r); ctx.stroke();
      const cx = (x0 + x1) / 2; ctx.save(); ctx.beginPath(); ctx.roundRect(x0, yT, x1 - x0, yB - yT, r); ctx.clip();
      ctx.beginPath(); ctx.arc(cx, yT + fs * 0.33, fs * 0.15, 0, 2 * Math.PI); ctx.fill();
      ctx.beginPath(); ctx.ellipse(cx, yB + fs * 0.05, fs * 0.3, fs * 0.3, 0, Math.PI, 2 * Math.PI); ctx.fill(); ctx.restore();
      if (ch === CAM_STILL) { // replay badge: punch a disc out of the corner, then a small play triangle in it
        const bx = x1 - fs * 0.16, by = yB - fs * 0.17; ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.arc(bx, by, fs * 0.2, 0, 2 * Math.PI); ctx.fill();
        ctx.globalCompositeOperation = 'source-over'; ctx.beginPath(); ctx.moveTo(bx - fs * 0.07, by - fs * 0.11); ctx.lineTo(bx + fs * 0.11, by); ctx.lineTo(bx - fs * 0.07, by + fs * 0.11); ctx.closePath(); ctx.fill();
      }
      adv = fs * 1.12;
    } else { adv = ctx.measureText(ch).width; ctx.fillText(ch, buffer, this.baseY); }
    const img = ctx.getImageData(0, 0, w, h).data;
    const INF = 1e20, go = this.go, gi = this.gi;
    for (let i = 0; i < w * h; i++) {
      const a = img[i * 4 + 3] / 255;
      if (a === 1) { go[i] = 0; gi[i] = INF; } else if (a === 0) { go[i] = INF; gi[i] = 0; }
      else { const d = 0.5 - a; go[i] = d > 0 ? d * d : 0; gi[i] = d < 0 ? d * d : 0; }
    }
    this.edt(go, w, h); this.edt(gi, w, h);
    const col = slot % this.cols, row = (slot / this.cols) | 0, ox = col * w, oy = row * h;
    for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
      const i = y * w + x, d = Math.sqrt(go[i]) - Math.sqrt(gi[i]);
      this.data[(oy + y) * size + ox + x] = Math.max(0, Math.min(255, Math.round(255 - 255 * (d / this.radius + this.cutoff))));
    }
    this.dirty = true;
    return { adv: adv / fs, x0: -buffer / fs, x1: (w - buffer) / fs, yT: this.baseY / fs, yB: -(h - this.baseY) / fs,
      u0: ox / size, u1: (ox + w) / size, vT: oy / size, vB: (oy + h) / size, empty: !ch.trim() };
  }
  edt(g, w, h) {
    for (let x = 0; x < w; x++) this.edt1(g, x, w, h);
    for (let y = 0; y < h; y++) this.edt1(g, y * w, 1, w);
  }
  edt1(g, off, stride, len) {
    const { f, v, z } = this; const INF = 1e20;
    v[0] = 0; z[0] = -INF; z[1] = INF; f[0] = g[off];
    for (let q = 1, k = 0, s = 0; q < len; q++) {
      f[q] = g[off + q * stride]; const q2 = q * q;
      do { const r = v[k]; s = (f[q] - f[r] + q2 - r * r) / (q - r) / 2; } while (s <= z[k] && --k > -1);
      k++; v[k] = q; z[k] = s; z[k + 1] = INF;
    }
    for (let q = 0, k = 0; q < len; q++) { while (z[k + 1] < q) k++; const r = v[k], qr = q - r; g[off + q * stride] = f[r] + qr * qr; }
  }
  flush() { if (this.dirty) { this.tex.needsUpdate = true; this.dirty = false; } }
}

/* ---------------- shaders ---------------- */
const TEXT_VS = `
attribute vec3 aAnchor; attribute vec4 aRect; attribute vec4 aUV; attribute vec4 aColor; attribute vec2 aSG; attribute vec3 aR; attribute vec3 aU;
varying vec2 vUV; varying vec4 vColor; varying float vGlow;
void main(){
  vec2 c = mix(aRect.xy, aRect.zw, position.xy);
  vec3 w = aAnchor + (aR * c.x + aU * c.y) * aSG.x;
  vUV = mix(aUV.xy, aUV.zw, position.xy); vColor = aColor; vGlow = aSG.y;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(w, 1.0); // layout units -> world (x0.13) via the world group
}`;
const TEXT_FS = `
uniform sampler2D uAtlas; varying vec2 vUV; varying vec4 vColor; varying float vGlow;
void main(){
  float d = texture2D(uAtlas, vUV).r;
  float w = max(fwidth(d) * 0.7, 0.004);
  float a = smoothstep(0.75 - w, 0.75 + w, d);
  vec3 hc = vec3(0.01, 0.015, 0.05); float hs = 0.7;
  if (vGlow > 1.5) { hc = vec3(0.1, 0.85, 1.0); hs = 0.65; } else if (vGlow > 0.8) { hc = vec3(1.0, 0.68, 0.15); hs = 0.6; } else if (vGlow > 0.3) { hc = vec3(0.55, 0.36, 0.06); hs = 0.5; }
  float ha = hs * smoothstep(0.38, 0.72, d);
  float outA = a + ha * (1.0 - a);
  vec3 rgb = vColor.rgb * a + hc * ha * (1.0 - a);
  if (outA * vColor.a < 0.004) discard;
  gl_FragColor = vec4(rgb * vColor.a, outA * vColor.a);
}`;
const PTS_VS = `
attribute float size; attribute vec3 color; uniform float uFocal; uniform float uDpr; uniform float uMax; varying vec3 vC; 