<!-- SOLID CITY · part 76 -->
del">\uD83D\uDDD1 Delete</button>' : '') + '</div><p id="u-msg" class="u-err"></p>');
    bind('#u-reader', function () { close(); CityBook.open(0); });
    bind('#u-ed', function () { editEntry(e, cid); });
    bind('#u-del', function () { if (!confirm('Delete \u201c' + e.title + '\u201d? This cannot be undone.')) return; post('/api/library/' + e.id + '/delete', {}).then(function (j) { if (j.ok) openLibrary(cid || e.campus); else $('u-msg').textContent = j.error || 'Could not delete.'; }); });
  }).catch(function () { body.innerHTML = '<p class="u-err">Could not load that entry.</p>'; });
})(); }
function editEntry(e, cid) { nav(function () {
  var camp = (e && e.campus) || cid || 'humanities';
  show(e ? 'Edit entry' : 'New library entry', '<label class="u-meta">Title</label><input id="u-t" maxlength="120" value="' + esc(e ? e.title : '') + '">' +
    '<label class="u-meta">Campus</label><select id="u-c">' + CUR.campuses.map(function (c) { return '<option value="' + esc(c.id) + '"' + (c.id === camp ? ' selected' : '') + '>' + esc(c.name) + '</option>'; }).join('') + '</select>' +
    '<label class="u-meta">Body (markdown: # heading, **bold**, *italic*, - lists, `code`, [link](https://...))</label><textarea id="u-b" style="min-height:180px">' + esc(e ? e.body : '') + '</textarea><p class="u-meta" id="u-n"></p>' +
    '<label class="u-meta">Links (one per line, http/https, up to 10)</label><textarea id="u-k">' + esc(e ? (e.links || []).join('\n') : '') + '</textarea>' +
    '<div class="u-row"><button type="button" class="gold" id="u-save">Save</button><button type="button" id="u-prevw">Preview</button></div><p id="u-msg" class="u-err"></p><div id="u-pv" class="u-md"></div>');
  var cnt = function () { var n = new Blob([$('u-b').value]).size; $('u-n').textContent = (n / 1024).toFixed(1) + ' KB of 20 KB'; $('u-n').className = n > 20480 ? 'u-err' : 'u-meta'; };
  $('u-b').addEventListener('input', cnt); cnt();
  bind('#u-prevw', function () { $('u-pv').innerHTML = window.SafeMD ? SafeMD.render($('u-b').value) : ''; });
  bind('#u-save', function () {
    var data = { title: $('u-t').value, campus: $('u-c').value, body: $('u-b').value, links: $('u-k').value.split(/\s+/).filter(Boolean) }, btn = $('u-save'); btn.disabled = true;
    post(e ? '/api/library/' + e.id : '/api/library', data).then(function (j) { if (j.ok) { U.hist.pop(); openEntry(j.id, data.campus); } else $('u-msg').textContent = j.error || (j._s === 401 ? 'Log in first.' : 'Could not save.'); })
      .catch(function () { $('u-msg').textContent = 'Network error.'; }).then(function () { btn.disabled = false; });
  });
})(); }

/* a small always-there entry point */
function uniButton() {
  if ($('uni-btn')) return; styles(); var b = document.createElement('button'); b.type = 'button'; b.id = 'uni-btn'; b.textContent = '\uD83C\uDF93 UNIVERSITY';
  b.addEventListener('pointerdown', function (e) { e.stopPropagation(); });
  b.addEventListener('click', function (e) { e.stopPropagation(); if (U.open) close(); else openDirectory(); });
  ($('sc-hud') || document.body).appendChild(b);
}
uniButton();

window.CityCampus = { update: update, radar: radar, actHere: actHere, open: openCampus, directory: openDirectory, library: openLibrary, close: close,
  course: function (cid, kid) { need(function () { openCourse(cid, kid); }); }, go: goCampus,
  state: function () { return { initMs: U.initMs, state: U.state, halls: Object.keys(U.halls), roads: ROADS.length, open: !!U.open }; }, roads: function () { return ROADS; }, HUB: HUB };
})();


===== FILE: public/city/capture-dom.js =====
/* Solid City capture: WORLD + MENUS (2026-10-10, the King: "photograph the menu too").
   A small self-contained DOM-to-canvas renderer (no third-party code): every visible HUD layer / open panel is cloned
   with its computed styles inlined, images/canvases/fonts inlined as data: URLs, ::before/::after turned into real
   spans, scroll positions kept, then drawn through an SVG <foreignObject> at the TARGET scale, so text stays sharp.
   Glass (backdrop-filter: blur) is reproduced by blurring the world pixels under the glass before the layer is drawn.
   One layer that cannot render is skipped (reported), never the whole shot. window.CityDomShot.compose(ctx, W, H, opts). */
(function () {
'use strict';
var SVGNS = 'http://www.w3.org/2000/svg', XHTML = 'http://www.w3.org/1999/xhtml';
var PROPS = ['display', 'box-sizing', 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
  'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
  'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width', 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
  'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-r