doxa-maps/public/index.html
swift-river542 403bfe72d4
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Rebuild bundles: watermark above the collapsed strip
2026-09-16 22:04:25 -05:00

244 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>doxa-maps</title>
<link rel="icon" href="data:,">
<style>
:root{--bg:#fff;--bg2:#f6f7f9;--bg3:#eceef2;--bd:#e2e5ea;--fg:#15181d;--mut:#667085;--err:#d13c3c}
@media(prefers-color-scheme:dark){:root{--bg:#111318;--bg2:#181b21;--bg3:#232730;--bd:#2b3038;--fg:#eef0f3;--mut:#9aa3b2;--err:#ff7b7b}}
*{box-sizing:border-box}html,body{margin:0;height:100%}
body{background:var(--bg);color:var(--fg);font:14px/1.4 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;display:flex;flex-direction:column;height:100vh}
header{flex:0 0 auto;display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid var(--bd)}
header b{font-size:15px}.n{color:var(--mut);font-size:12px}
#menu{display:none;margin-left:auto}
button,.btn{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2);color:var(--fg);cursor:pointer;text-decoration:none;white-space:nowrap}
button:hover,.btn:hover{background:var(--bg3)}
.body{flex:1 1 auto;display:flex;min-height:0}
nav{flex:0 0 270px;overflow:auto;border-right:1px solid var(--bd);padding:6px 0 20px}
.gh{padding:12px 14px 4px;font-weight:600;font-size:13px;display:flex;justify-content:space-between;gap:8px}.gh span{color:var(--mut);font-weight:400}
.row{display:block;width:100%;text-align:left;border:0;border-radius:0;background:none;padding:6px 14px;font-size:13px;line-height:1.3;white-space:normal}
.row.in{padding-left:28px}.row code{display:block;font-size:11px;color:var(--mut)}
.row:hover{background:var(--bg2)}.row[aria-current=true]{background:var(--bg3);font-weight:600}
main{flex:1 1 auto;display:flex;flex-direction:column;min-width:0;padding:10px 12px 12px;gap:8px}
.bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.bar .t{flex:1 1 auto;min-width:0}.bar b{font-size:15px}.s{display:block;font-size:12px;color:var(--mut)}
code{font-family:ui-monospace,Menlo,monospace}
#embed{display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2)}
#embed label{font-size:12px;color:var(--mut);display:flex;justify-content:space-between;align-items:center}
#embed textarea{width:100%;height:58px;font:11px/1.35 ui-monospace,Menlo,monospace;border:1px solid var(--bd);border-radius:6px;background:var(--bg);color:var(--fg);padding:6px;resize:vertical}
#slot{flex:1 1 auto;position:relative;min-height:240px;border:1px solid var(--bd);border-radius:10px;overflow:hidden;background:var(--bg2)}
#slot>*{position:absolute;inset:0;width:100%;height:100%;display:block;border:0}
.hint{display:grid;place-items:center;padding:20px;text-align:center;color:var(--mut);font-size:13px}
.hint.err{color:var(--err)}
.hint input{font:12px ui-monospace,Menlo,monospace;padding:6px 8px;border-radius:6px;border:1px solid var(--bd);background:var(--bg);color:var(--fg);width:min(320px,70vw)}
[hidden]{display:none!important}
@media(max-width:700px){.body{flex-direction:column}nav{flex:0 0 auto;max-height:45vh;border-right:0;border-bottom:1px solid var(--bd)}#menu{display:inline-block}}
</style>
</head>
<body>
<header><b>doxa-maps</b><span class="n" id="count"></span><button id="menu" type="button" aria-controls="nav" aria-expanded="true">Maps</button></header>
<div class="body">
<nav id="nav" aria-label="Maps"></nav>
<main>
<div class="bar"><div class="t"><b id="ttl">Choose a map</b><span class="s" id="sub"></span></div><a class="btn" id="open" target="_blank" rel="noopener" hidden>Open page</a><button id="emb" type="button" hidden aria-expanded="false">Copy embed</button></div>
<div id="embed" hidden><label>iframe embed <button type="button" data-copy="ifr">Copy</button></label><textarea id="ifr" readonly></textarea><div id="scrw"><label>script embed <button type="button" data-copy="scr">Copy</button></label><textarea id="scr" readonly></textarea></div></div>
<div id="slot"><div class="hint">Choose a map from the list.</div></div>
</main>
</div>
<script>
(function () {
var TREE = "doxa-maps", TOKEN_SRC = "https://doxa.life/api/maps/token", BAKED_TOKEN = "", API_BASE = "https://pray.doxa.life", ASSETS_BASE = "", QA = false;
var DEFAULT_TOKEN_URL = '/api/maps/token';
var BAKED = [{"b":"all-people-groups","s":"./all-people-groups/all-people-groups.js","i":"sha384-ZnhhkQKe4NOO96T23+c31QiLAD6xvVU4+MmH5cr2Iq1KvXMLmjYh0ogopwn2qngq","e":["all-people-groups"],"p":[{"p":"all-people-groups","e":"all-people-groups","u":"./all-people-groups/all-people-groups/index.html","d":"All 12,379 people groups (peoplegroups.org) in four colors — reached,"}]},{"b":"cities-internet","s":"./cities-internet/cities-internet.js","i":"sha384-sE9lnPH2RJoDCaFlG4EYzIL7MQ3Ak/70BSCXg9RBHC0LCv/URhI6+k+jrovFnJx3","e":["cities-internet-map"],"p":[{"p":"connectivity-list","e":"cities-internet-map","u":"./cities-internet/connectivity-list/index.html","d":""},{"p":"connectivity-map","e":"cities-internet-map","u":"./cities-internet/connectivity-map/index.html","d":""},{"p":"data-tables","e":"cities-internet-map","u":"./cities-internet/data-tables/index.html","d":""}]},{"b":"country-posters","s":"./country-posters/country-posters.js","i":"sha384-0N4XF859ZEqlYwK0DM4WEEgBYuUJPrur43HmPSD8F8TnrepT2wqIl++2RDIjf+zv","e":["country-posters-map"],"p":[{"p":"countries-map","e":"country-posters-map","u":"./country-posters/countries-map/index.html","d":""},{"p":"country-poster-wall","e":"country-posters-map","u":"./country-posters/country-poster-wall/index.html","d":""}]},{"b":"demo-map","s":"./demo-map/demo-map.js","i":"sha384-PoK3jcKdPhcRy+tKKgSOJPlp+qf/xjPQyPUvkvyBnxvOvvqHVoVpNrsfEcUmMtIF","e":["demo-map"],"p":[{"p":"demo-map","e":"demo-map","u":"./demo-map/demo-map/index.html","d":"Simple two-tab demo map. Tab 1 colors pins by religion (shared","in":[{"id":"maplibre","t":"Demo map — MapLibre (token-free)","a":"","pr":{"profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"},"e":"demo-map","u":"./demo-map/demo-map/maplibre/index.html"}]}]},{"b":"doxa-research-map","s":"./doxa-research-map/doxa-research-map.js","i":"sha384-RLQjHW1ujtBn2N82gRcdub3jm7eG0Yol52IcWgzv/sjGzEnXEaHupIVT3Ei4QtOL","e":["doxa-research-map"],"p":[{"p":"research-map","e":"doxa-research-map","u":"./doxa-research-map/research-map/index.html","d":""}]},{"b":"doxa-simple-map","s":"./doxa-simple-map/doxa-simple-map.js","i":"sha384-3zg7SenU3WZA9wbCdAR9749ksktoSOyRacCAkhbi1uHhJ9SamP+n7ocrHSWRcxdV","e":["doxa-map","doxa-simple-map"],"p":[{"p":"doxa-simple-map","e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/index.html","d":"","in":[{"id":"prayer","t":"Prayer Map","a":"https://pray.doxa.life","pr":{"profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]},"e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/prayer/index.html"},{"id":"engagement","t":"Engagement Map","a":"https://pray.doxa.life","pr":{"profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]},"e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/engagement/index.html"},{"id":"adoption","t":"Adoption Map","a":"https://pray.doxa.life","pr":{"profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]},"e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/adoption/index.html"}]}]},{"b":"gospel-resources","s":"./gospel-resources/gospel-resources.js","i":"sha384-jxRwI8firVmQQ2GBO4RwmWu9A8DWBIP7cujipx48BIF6GFtOlIeQDxjeCQWC2i9d","e":["gospel-resources-map"],"p":[{"p":"charts","e":"gospel-resources-map","u":"./gospel-resources/charts/index.html","d":""},{"p":"data-tables","e":"gospel-resources-map","u":"./gospel-resources/data-tables/index.html","d":""},{"p":"resources-list","e":"gospel-resources-map","u":"./gospel-resources/resources-list/index.html","d":""},{"p":"resources-map","e":"gospel-resources-map","u":"./gospel-resources/resources-map/index.html","d":""}]},{"b":"my-upg-100-list","s":"./my-upg-100-list/my-upg-100-list.js","i":"sha384-wfPvwhYksJPp8IRjmd3lEzrK0BerRgIOeQYr6JxJ/6XcqLXzAzcVj4OKW6tIFADc","e":["my-upg-100-list-map"],"p":[{"p":"poster-wall","e":"my-upg-100-list-map","u":"./my-upg-100-list/poster-wall/index.html","d":""},{"p":"upg-100-list","e":"my-upg-100-list-map","u":"./my-upg-100-list/upg-100-list/index.html","d":""},{"p":"upg-100-mfe","e":"my-upg-100-list-map","u":"./my-upg-100-list/upg-100-mfe/index.html","d":""}]},{"b":"research-dashboard","s":"./research-dashboard/research-dashboard.js","i":"sha384-YbwXMIQIkPSFnB4EVR35AtlHApWzCG+2lb16xUEKxHLU6zo1jfpppUDZb7i+irxP","e":["research-dashboard-map"],"p":[{"p":"research-dashboard","e":"research-dashboard-map","u":"./research-dashboard/research-dashboard/index.html","d":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store."}]},{"b":"template-bundle","s":"./template-bundle/template-bundle.js","i":"sha384-85Dt1LFnF/2QJuLwCA3wGUdyHVzzvSvn2yR5/yHocZAy0daRVTt4Ne/GQwxjcgfq","e":["template-bundle"],"p":[{"p":"template-map-a","e":"template-bundle","u":"./template-bundle/template-map-a/index.html","d":"Minimal example profile A — a colored placeholder, no Mapbox or data."},{"p":"template-map-b","e":"template-bundle","u":"./template-bundle/template-map-b/index.html","d":"Minimal example profile B — a second profile in the same bundle."}]}];
var snip = function snip(b, p, x) {
var u = (x || p).u, tag = (x && x.e) || p.e || b.e[0] || b.b
var r = { iframe: '<iframe src="' + u + '?utm_source=YOUR_SOURCE" width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>' }
if (!x) r.script = '<script src="' + b.s + '"' + (b.i ? ' integrity="' + b.i + '" crossorigin="anonymous"' : '') + '><\/script>\n<' + tag + " profile-config='" + JSON.stringify({ profile: p.p, tk: 'YOUR_MAPBOX_TOKEN', utmSource: 'YOUR_SOURCE' }) + "'></" + tag + '>'
return r
};
var compact = function compact(m, lean) {
return (m.bundles || []).map(function (src) {
var b = { b: src.bundle, s: src.scriptUrl || '', i: src.integrity || '', e: src.elements || [], p: [] }
b.p = (src.profiles || []).map(function (sp) {
var p = { p: sp.profile, e: sp.element || '', u: sp.iframeUrl || '', d: sp.description || '' }
var em = sp.embed || {}, d = snip(b, p, null)
if (!lean || em.iframe !== d.iframe || em.script !== d.script) p.em = em
var ins = (sp.instances || []).map(function (sx) {
var x = { id: sx.id, t: sx.title || sx.id, a: sx.apiBaseUrl || '', pr: sx.props || {}, e: sx.element || '', u: sx.iframeUrl || '' }
var xe = sx.embed || {}, xd = snip(b, p, x)
if (!lean || xe.iframe !== xd.iframe) x.em = xe
return x
})
if (ins.length) p.in = ins
return p
})
return b
})
};
// Peer engines, the same tags every leaf page carries; loaded once, before the bundle.
var PEERS = {
mapbox: { css: ['https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css', 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css'],
js: ['https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js', 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'] },
maplibre: { css: ['https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.css'], js: ['https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.js'] }
};
var $ = function (id) { return document.getElementById(id) };
var nav = $('nav'), slot = $('slot'), ttl = $('ttl'), sub = $('sub'), open = $('open'), embBtn = $('emb'), embed = $('embed'), ifr = $('ifr'), scr = $('scr'), scrw = $('scrw'), menu = $('menu'), count = $('count');
var ASSETS = ASSETS_BASE || new URL('assets', location.href).href.replace(/\/+$/, '');
var REG = BAKED, active = null, seq = 0, loaded = {}, fetchedToken = '';
var narrow = function () { return window.matchMedia && window.matchMedia('(max-width:700px)').matches };
// Manifest refs are relative to the tree root; this page sits one level above it.
function rel(u) { return /^([a-z]+:|\/)/i.test(u) ? u : TREE + '/' + u.replace(/^\.\//, '') }
function qp(name) { try { return new URLSearchParams(location.search).get(name) || '' } catch (e) { return '' } }
function hint(msg, err) { var d = document.createElement('div'); d.className = 'hint' + (err ? ' err' : ''); d.textContent = msg; slot.innerHTML = ''; slot.appendChild(d); return d }
function assign(t) { for (var i = 1; i < arguments.length; i++) { var s = arguments[i]; for (var k in s) if (Object.prototype.hasOwnProperty.call(s, k)) t[k] = s[k] } return t }
function once(url, kind, attrs) {
if (loaded[url]) return loaded[url];
loaded[url] = new Promise(function (ok, fail) {
var el = document.createElement(kind === 'css' ? 'link' : 'script');
if (kind === 'css') { el.rel = 'stylesheet'; el.href = url } else { el.src = url; assign(el, attrs || {}) }
el.onload = function () { ok() }; el.onerror = function () { delete loaded[url]; fail(new Error('failed to load ' + url)) };
document.head.appendChild(el);
});
return loaded[url];
}
function peer(provider) {
var p = PEERS[provider] || PEERS.mapbox, chain = Promise.resolve();
p.css.forEach(function (u) { once(u, 'css') });
p.js.forEach(function (u) { chain = chain.then(function () { return once(u, 'js') }) });
return chain;
}
function bundle(b) { return once(rel(b.s), 'js', b.i ? { integrity: b.i, crossOrigin: 'anonymous' } : {}) }
function row(key, kind, label, tag, extra) {
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'row' + (extra || ''); btn.setAttribute('data-k', key); btn.setAttribute('data-kind', kind);
var l = document.createElement('span'); l.textContent = label; btn.appendChild(l);
var c = document.createElement('code'); c.textContent = '<' + tag + '>'; btn.appendChild(c);
btn.onclick = function () { location.hash = '#/' + key.split('/').map(encodeURIComponent).join('/') };
return btn;
}
function draw() {
nav.innerHTML = ''; var np = 0, ni = 0;
REG.forEach(function (b) {
var g = document.createElement('div'); g.className = 'grp';
var h = document.createElement('div'); h.className = 'gh'; var hn = document.createElement('span'); hn.textContent = b.b; h.appendChild(hn);
var hc = document.createElement('span'); hc.textContent = b.p.length; h.appendChild(hc); g.appendChild(h);
b.p.forEach(function (p) {
np++; g.appendChild(row(b.b + '/' + p.p, 'profile', p.p, p.e || b.e[0] || b.b));
(p.in || []).forEach(function (x) { ni++; g.appendChild(row(b.b + '/' + p.p + '/' + x.id, 'instance', x.t, x.e || p.e || b.e[0] || b.b, ' in')) });
});
nav.appendChild(g);
});
count.textContent = REG.length + ' bundles · ' + np + ' profiles · ' + ni + ' instances';
}
function find(parts) {
for (var i = 0; i < REG.length; i++) if (REG[i].b === parts[0]) {
var b = REG[i];
for (var j = 0; j < b.p.length; j++) if (b.p[j].p === parts[1]) {
var p = b.p[j];
if (parts.length < 3) return { b: b, p: p, x: null };
for (var k = 0; k < (p.in || []).length; k++) if (p.in[k].id === parts[2]) return { b: b, p: p, x: p.in[k] };
}
}
return null;
}
function select(key) {
var parts = key.split('/').map(function (s) { try { return decodeURIComponent(s) } catch (e) { return s } });
var hit = find(parts);
var rows = nav.querySelectorAll('.row');
for (var i = 0; i < rows.length; i++) rows[i].setAttribute('aria-current', rows[i].getAttribute('data-k') === parts.join('/') ? 'true' : 'false');
if (!hit) { ttl.textContent = 'No such map'; sub.textContent = key; open.hidden = embBtn.hidden = embed.hidden = true; hint('No such map: ' + key, true); return }
var b = hit.b, p = hit.p, x = hit.x, tag = (x && x.e) || p.e || b.e[0] || b.b;
ttl.textContent = x ? x.t : p.p; sub.textContent = b.b + ' · <' + tag + '>' + (p.d ? ' · ' + p.d : '');
open.href = rel((x || p).u); open.hidden = false; embBtn.hidden = false; embed.hidden = true; embBtn.setAttribute('aria-expanded', 'false');
var em = x ? (x.em || snip(b, p, x)) : (p.em || snip(b, p, null));
ifr.value = em.iframe || ''; scr.value = em.script || ''; scrw.hidden = !em.script;
if (narrow()) { nav.hidden = true; menu.setAttribute('aria-expanded', 'false') }
mount(b, p, x, tag);
}
// Same profile-config the leaf page bakes (profile page: profile + dataSource + instanceId;
// instance page: its props), plus the host-supplied share URL of that leaf page.
function mount(b, p, x, tag) {
var cfg = x ? assign({ assetsBase: ASSETS }, x.pr) : { profile: p.p, dataSource: 'pray-tools', instanceId: b.b + '-' + p.p, assetsBase: ASSETS };
if (QA) cfg.qa = true;
cfg.shareUrl = new URL(rel((x || p).u), location.href).href;
window.MAP_APP_API_URL = (x && x.a) || API_BASE;
var provider = cfg.provider === 'maplibre' ? 'maplibre' : 'mapbox', my = ++seq;
hint(provider === 'maplibre' ? 'loading map…' : 'loading token…');
peer(provider).then(function () { return bundle(b) }).then(function () {
if (my !== seq) return;
resolveToken(cfg, provider === 'maplibre', function (token) {
if (my !== seq) return;
try {
var c = token ? assign({}, cfg, { tk: token }) : cfg;
var el = document.createElement(tag); el.setAttribute('profile-config', JSON.stringify(c));
slot.innerHTML = ''; slot.appendChild(el);
} catch (e) { hint(e && e.message ? e.message : String(e), true) }
});
}).catch(function (e) { if (my === seq) hint('Failed to load ' + b.b + ': ' + (e && e.message ? e.message : e), true) });
}
// Mapbox token resolution — the leaf pages' ladder, in precedence order:
// 1. prop (config.tk) 2. ?tk= on this page's URL (stored once for every page of the tree)
// 3. baked token — then, none found, a token ENDPOINT is fetched, first answer wins:
// 4. window.MAP_TOKEN_URL 5. configured TOKEN_SRC 6. same-origin /api/maps/token — else ask once.
function resolveToken(cfg, tokenFree, done) {
if (tokenFree) return done('');
var stored = ''; try { stored = localStorage.getItem('MAPS_STAGING_TK') || '' } catch (e) {}
var urlTk = qp('tk'); if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk) } catch (e) {} }
var directToken = cfg.tk || urlTk || stored || BAKED_TOKEN || fetchedToken;
if (directToken) return done(directToken);
function askToken(prefix) {
var d = hint('', true);
d.innerHTML = (prefix ? '<div></div>' : '') + '<div>map needs a Mapbox token — <input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)"> <button id="tkGo" type="button">use it</button></div>';
if (prefix) d.firstChild.textContent = prefix;
$('tkGo').onclick = function () { var v = ($('tkIn').value || '').trim(); if (!v) return; try { localStorage.setItem('MAPS_STAGING_TK', v) } catch (e) {} done(v) };
}
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u) }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function extractToken(data) { if (typeof data === 'string') return data.trim(); if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || ''; return '' }
function fetchToken(i, errors) {
if (i >= candidates.length) return askToken('Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')');
var url = candidates[i];
return fetch(url).then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
return (r.headers.get('content-type') || '').indexOf('json') !== -1 ? r.json() : r.text();
}).then(function (data) {
var tk = extractToken(data); if (!tk) throw new Error(url + ' → no token in response');
fetchedToken = tk; done(tk);
}).catch(function (e) { errors.push(e && e.message ? e.message : String(e)); return fetchToken(i + 1, errors) });
}
fetchToken(0, []);
}
function copy(text, btn) {
var ok = function () { var t = btn.textContent; btn.textContent = 'Copied'; setTimeout(function () { btn.textContent = t }, 1200) };
var fallback = function () { var ta = btn.parentNode.nextSibling; ta.focus(); ta.select(); try { document.execCommand('copy'); ok() } catch (e) {} };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(ok, fallback); else fallback();
}
embed.addEventListener('click', function (e) { var b = e.target.closest ? e.target.closest('[data-copy]') : null; if (b) copy($(b.getAttribute('data-copy')).value, b) });
embBtn.onclick = function () { embed.hidden = !embed.hidden; embBtn.setAttribute('aria-expanded', String(!embed.hidden)) };
menu.onclick = function () { nav.hidden = !nav.hidden; menu.setAttribute('aria-expanded', String(!nav.hidden)) };
try { window.matchMedia('(max-width:700px)').addEventListener('change', function (e) { if (!e.matches) { nav.hidden = false; menu.setAttribute('aria-expanded', 'true') } }) } catch (e) {}
function route() { var h = (location.hash || '').replace(/^#\/?/, ''); if (h) select(h) }
function init(m) {
if (m) { try { REG = compact(m, false) } catch (e) { REG = BAKED } }
draw(); window.addEventListener('hashchange', route); route();
}
// Live manifest first (the page stays right when bundles change); the baked copy is the fallback.
if (typeof fetch === 'function' && location.protocol !== 'file:') {
fetch(TREE + '/manifest.json', { cache: 'no-cache' }).then(function (r) { return r.ok ? r.json() : null }).then(init, function () { init(null) });
} else init(null);
})();
</script>
</body>
</html>