<!-- SOLID CITY · part 83 -->
el.className || el.tagName) + ': ' + e.message); }
    return { el: el, L: L, job: job, pend: pend };
  });
  return jobs.reduce(function (p, j) {
    return p.then(function () {
      if (!j.L) return;
      return Promise.all(j.pend).then(function () {
        j.job.glass.forEach(function (g) { try { blurRegion(ctx, g, s, ox, oy); } catch (e) {} });
        var html = new XMLSerializer().serializeToString(j.L.clone);
        return fontsCss(html).then(function (fonts) { return drawSvg(ctx, j.L, s, ox, oy, fonts); }).then(function () { drawn++; })
          .catch(function (e) { issues.push((j.el.id || String(j.el.className).split(' ')[0] || j.el.tagName.toLowerCase()) + ' could not be drawn'); });
      });
    });
  }, Promise.resolve()).then(function () { return { layers: list.length, drawn: drawn, issues: issues, ms: Math.round(performance.now() - t0), scale: s, box: [ox, oy, vw * s, vh * s] }; });
}
window.CityDomShot = { compose: compose, layers: layers };
})();


===== FILE: public/city/capture.js =====
/* Solid City SCREEN CAPTURE + HUD DOCK + AUTO-FADE HUD (2026-10-10, order of the King).
   - #sc-dock: compact cluster [camera][university]; the existing #uni-btn is moved in, behaviour unchanged
     (campus.js still owns its click + hide-in-watch logic).
   - Capture renders the live scene ONCE into an offscreen render target at the chosen REAL pixel size
     (MSAA on WebGL2, same sRGB output encoding as the screen so no shader recompiles), reads it back,
     adds the optional EPISTLES.XYZ watermark and encodes PNG/JPEG/WebP. The HUD is DOM, so it is never in the shot.
     Phones: navigator.share({files}) when allowed, otherwise SHARE/OPEN buttons in the panel. Desktop: download.
   - Auto-fade: after 5 s with no pointer/touch/key/wheel/gamepad activity the HUD fades out (0.6 s) and stops
     taking taps; any activity brings it back instantly and the waking tap never presses a hidden button.
     Never fades while a panel/dialog is open or a field has focus. Setting: localStorage 'sc-hud-autofade'. */
(function () {
'use strict';
var A = window.SC && SC.api; if (!A || !A.renderer) return;
var T = A.T, R = A.renderer, $ = function (id) { return document.getElementById(id); };
var LS = 'sc-capture', FLS = 'sc-hud-autofade', IDLE_MS = 5000;
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
var cfg = (function () { var d = { type: 'png', size: 'screen', wm: true, what: 'world' }; try { var j = JSON.parse(lsGet(LS) || '{}'); if (j && typeof j === 'object') { if (/^(png|jpeg|webp)$/.test(j.type)) d.type = j.type; if (j.size) d.size = j.size; if (j.wm === false) d.wm = false; if (j.what === 'menus') d.what = 'menus'; } } catch (e) {} return d; })();
function save() { lsSet(LS, JSON.stringify(cfg)); }
var TYPES = { png: ['image/png', 'PNG', 'png'], jpeg: ['image/jpeg', 'JPEG', 'jpg'], webp: ['image/webp', 'WebP', 'webp'] };

/* ---------- sizes (real pixels) ---------- */
function gl() { return R.getContext(); }
function maxPx() { var g = gl(), m = R.capabilities.maxTextureSize || 4096; try { m = Math.min(m, g.getParameter(g.MAX_RENDERBUFFER_SIZE)); var v = g.getParameter(g.MAX_VIEWPORT_DIMS); m = Math.min(m, v[0], v[1]); } catch (e) {} return Math.min(m, 8192); }
function vp() { var c = R.domElement; return [c.clientWidth || innerWidth, c.clientHeight || innerHeight]; }
function screenScale() { var d = window.devicePixelRatio || 1; return A.IS_TOUCH || A.MOBILE ? Math.max(2, d) : Math.max(1, d); }   // phones: at least 2x
var SIZES = [
  ['screen', 'Screen', function () { var v = vp(), s = screenScale(); return [v[0] * s, v[1] * s]; }],
  ['2x', '2\u00d7 Screen', function () { var v = vp(), s = Math.max(2, window.devicePixelRatio || 1) * (A.IS_TOUCH || A.MOBILE ? 1.5 : 1); return [v[0] * s, v[1] * s]; }],
  ['1080p', '1080p', function () { return [1920, 1080]; }],
  ['4k', '4K UHD', function () { return [3840, 2160]; }],
  ['square', 'Square 1080', function () { return [1080, 1080]; }],
  ['portrait', 'Phone portrait', function () { return [1080, 1920]; }]
];
function dims(key) {
  var s = SIZES.filter(function (x) { return x[0] === key; })[0] || SIZES[0], wh = s[2](), w = Math.round(wh[0]), h = Math.round(wh[1]), m = maxPx(), note = '';
  var lim = Math.min(1, m / w, m / h, Math.sqrt(16.7e6 / (w * h)));            // GPU limits and the iOS 16.7 Mpx canvas cap
  if (lim < 1) { w = Math.floor(w * lim); h = Math.floor(h * lim); note = 'scaled to fit this device'; }
  return { key: s[0], label: s[1], w: w, h: h, note: note };
}

/* ---------- capture core ---------- */
function renderPixels(w, h) {
  var cam = A.camera, oldAspect = cam.aspect, isGL2 = R.capabilities.isWebGL2, rt, ms = false;
  var opts = { minFilter: T.LinearFilter, magFilter: T.LinearFilter, format: T.RGBAFormat, depthBuffer: true, stencilBuffer: false };
  if (isGL2 && T.WebGLMultisampleRend