Update from source 9c6dda2a
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

This commit is contained in:
Doxa Maps 2026-09-27 07:39:01 +00:00
parent dbead061ed
commit 91d8e48f99
33 changed files with 631 additions and 164 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -166,14 +166,26 @@ const decl = JSON.parse(readFileSync(declPath, 'utf8'))
// Records: the same source everything else uses. NOT fetched here — a gate that needs the network is a
// gate that SKIPs in CI, which is how check:packets-all went unevaluated on every build for weeks.
const recordsPath = flag('records', join(REPO, 'library/scripts/posters/out/_records.json'))
// ★ THE COMMITTED FIXTURE IS THE DEFAULT, the live cache only an override.
// ⚠ IT USED TO DEFAULT TO library/scripts/posters/out/_records.json, WHICH IS GITIGNORED
// (.gitignore:94) — so on a clean checkout this gate exited 75 and reported NOT MEASURED forever,
// which is indistinguishable from an unwired gate. studio measured exactly that before wiring it into
// gates:post (L13U) and correctly refused to take it. A gate that cannot run in CI protects nothing;
// this tree has been here before with check:packets-all, which SKIPped on every build for weeks.
// data/people-groups/zoning-records.json carries slug + every field a selector reads — 2,107 records,
// ~300 KB — so the gate measures on a clean tree with no network and no generation step.
const DEFAULT_RECORDS = join(REPO, 'data/people-groups/zoning-records.json')
const LIVE_CACHE = join(REPO, 'library/scripts/posters/out/_records.json')
const recordsPath = flag('records', existsSync(LIVE_CACHE) ? LIVE_CACHE : DEFAULT_RECORDS)
if (!existsSync(recordsPath)) {
console.log(`${TAG}: NOT MEASURED — no records at ${recordsPath}.`)
console.log(' Generate them once (OFFLINE=1 or a packet run caches them), or pass --records <file>.')
console.log(' Exit 75 rather than 0: "found no records" is not "found nothing wrong".')
console.log(' The committed fixture is data/people-groups/zoning-records.json; pass --records <file>')
console.log(' to use another. Exit 75 rather than 0: "found no records" is not "found nothing wrong".')
process.exit(75)
}
const records = JSON.parse(readFileSync(recordsPath, 'utf8'))
const rawRecords = JSON.parse(readFileSync(recordsPath, 'utf8'))
// The fixture wraps its rows so it can carry its own provenance; the live cache is a bare array.
const records = Array.isArray(rawRecords) ? rawRecords : (rawRecords.records || [])
const clusters = JSON.parse(readFileSync(join(DATA, 'clusters.json'), 'utf8'))
const { buildLegendNodes, leafZones } = await import(pathToFileURL(join(DATA, 'legend-tree.js')).href)

View file

@ -127,9 +127,25 @@ function paneElement(pane) {
return el
}
// `inert` on every pane except the active one: the same contract as upg-100-mfe (4642e78d). The panes
// are stacked in one scroll-snap column, so the hidden pane is still full-size geometry; inert tells
// PosterWall (67e9ede5) the wall is out of view so it forgets its shown card, and a second
// "Show on map" after Map -> back SHOWS the country again instead of toggling it off (rega row 79).
// inert, not display:none: hiding would tear down the live map.
function markActivePane(activeId) {
if (!column || !activeId) return
for (const pane of column.panes) {
const el = column.mountEl(pane.id)
if (!el) continue
if (pane.id === activeId) el.removeAttribute('inert')
else el.setAttribute('inert', '')
}
}
function go(id) {
view.value = id
column?.scrollTo(id)
markActivePane(id) // now, not after the smooth scroll settles
}
// ── tab 1 -> tab 2 (shell seam) ─────────────────────────────────────────────
@ -183,9 +199,11 @@ onMounted(() => {
column.mountEl(pane.id)?.appendChild(el)
}
// Tabs are a readout as well as a control — follow the user's scroll.
markActivePane(column.current() || view.value) // the fresh-load state, before any switch
offScroll = column.onScroll(() => {
const cur = column.current()
if (cur && cur !== view.value) view.value = cur
if (cur) markActivePane(cur) // always: a switcher-driven scroll already set view.value
})
window.addEventListener(SHOW_ON_MAP, onShowOnMap)
// profile-config `view` opens a chosen pane ("map" or "wall"), so an embed can start on the map.

View file

@ -34,6 +34,7 @@ import GiGrid from './src/components/GiGrid.vue'
import GiDrawer from './src/components/GiDrawer.vue'
import GiTree from './src/components/GiTree.vue'
import GiResources from './src/components/GiResources.vue'
import GiCombos from './src/components/GiCombos.vue'
const T = useGiT()
const board = useBoard()
@ -124,8 +125,8 @@ useShadowStyles(
':host{display:block}' +
'.gb{--gb-bg:#f3f3f1;position:relative;box-sizing:border-box;width:100%;height:100%;min-height:640px;display:grid;background:var(--gb-bg);color:#3b463d;' +
'grid-template-columns:var(--facet-w,264px) minmax(0,1fr) var(--rail-w,300px);' +
'grid-template-rows:auto auto minmax(0,1fr) var(--table-h,38%);' +
'grid-template-areas:"top top top" "note note note" "facets map rail" "facets grid grid";' +
'grid-template-rows:auto auto auto minmax(0,1fr) var(--table-h,38%);' +
'grid-template-areas:"top top top" "combos combos combos" "note note note" "facets map rail" "facets grid grid";' +
'font:13px/1.35 Roboto,-apple-system,"Segoe UI",sans-serif;overflow:hidden}' +
'.gb-top{grid-area:top;display:flex;align-items:center;flex-wrap:wrap;gap:10px 16px;padding:10px 14px;background:#fff;border-bottom:1px solid #d1d4cd}' +
'.gb-title{margin:0;font-size:17px;font-weight:700;letter-spacing:.005em}' +
@ -165,8 +166,8 @@ useShadowStyles(
'.gb-rz-table::after{width:34px;height:3px}' +
'.gb-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}' +
// narrow: one panel at a time, a segmented control, no drag handles
'.gb.narrow{grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto auto minmax(0,1fr);' +
'grid-template-areas:"top" "note" "mtabs" "panel"}' +
'.gb.narrow{grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto auto auto minmax(0,1fr);' +
'grid-template-areas:"top" "combos" "note" "mtabs" "panel"}' +
'.gb.narrow .gb-facets,.gb.narrow .gb-map,.gb.narrow .gb-rail,.gb.narrow .gb-grid{grid-area:panel;border:0}' +
'.gb-mtabs{grid-area:mtabs;display:flex;gap:4px;padding:6px 10px;background:#fff;border-bottom:1px solid #d1d4cd}' +
'.gb-mtab{flex:1;border:1px solid #d1d4cd;background:#fff;color:#3b463d;border-radius:6px;min-height:32px;font:inherit;font-size:12.5px;cursor:pointer}' +
@ -196,6 +197,7 @@ useShadowStyles(
<span v-if="listChip" class="gb-chip" :title="T('gospelInternet.listChip', 'People-group list, chosen by this page')">{{ listChip }}</span>
<button v-if="scoped" type="button" class="gb-reset" @click="resetAll">{{ T('controls.reset_all', 'Reset all') }}</button>
</header>
<GiCombos />
<p class="gb-note" data-notice="standing">
{{ T('gospelInternet.notice.standing', 'Dots are approximate (where a people is found in its country, not a town). Internet figures are the country\'s, or the Indian state\'s, never the group\'s. "Found" means found in the sources checked.') }}
<span v-if="sourcesLine"> {{ T('gospelInternet.notice.sources', 'Sources:') }} {{ sourcesLine }}.</span>

View file

@ -98,8 +98,12 @@ function collapse(n) {
roots.forEach(collapse)
// ── radial layout per family ────────────────────────────────────────────────────────────────────────
const ARC = 1.0 // minimum arc between neighbouring leaves (layout units)
const STEP_MIN = 3.2 // minimum ring spacing
// Two layouts from one algorithm: the SCREEN one (ARC 1: the map engine hides labels that do not fit,
// and reveals them as the reader zooms) and the POSTER one (wider leaf spacing, because a printed sheet
// cannot zoom: every name must fit at one scale). The poster's is stored as `pc` / `pR` beside `c` / `R`.
let ARC = 1.0 // minimum arc between neighbouring leaves (layout units)
let STEP_MIN = 3.2 // minimum ring spacing
const POSTER = { ARC: 1.0, STEP_MIN: 3.2 }
function leaves(n) { return n._leaves ??= (n.kids.length ? n.kids.reduce((s, k) => s + leaves(k), 0) : 1) }
function depth(n) { return n._depth ??= (n.kids.length ? 1 + Math.max(...n.kids.map(depth)) : 0) }
function layoutFamily(root) {
@ -120,23 +124,46 @@ function layoutFamily(root) {
root._R = far + 1.6 + Math.min(3, Math.sqrt(nLeaves) * 0.25)
return root
}
roots.forEach(layoutFamily)
// ── pack the families: largest first, nearest free spot on a spiral ────────────────────────────────
const placed = []
for (const root of [...roots].sort((a, b) => b._R - a._R || a.name.localeCompare(b.name))) {
const R = root._R
let best = null
if (!placed.length) best = [0, 0]
for (let t = 0; !best; t += 0.05) {
const rad = 0.35 * t * Math.max(1, R * 0.2)
const x = rad * Math.cos(t), y = rad * Math.sin(t)
if (placed.every((p) => Math.hypot(p.x - x, p.y - y) >= p.R + R + 0.8)) best = [x, y]
function pack() {
const placed = []
for (const root of [...roots].sort((a, b) => b._R - a._R || a.name.localeCompare(b.name))) {
const R = root._R
let best = null
if (!placed.length) best = [0, 0]
for (let t = 0; !best; t += 0.05) {
const rad = 0.35 * t * Math.max(1, R * 0.2)
const x = rad * Math.cos(t), y = rad * Math.sin(t)
if (placed.every((p) => Math.hypot(p.x - x, p.y - y) >= p.R + R + 0.8)) best = [x, y]
}
placed.push({ x: best[0], y: best[1], R })
root._cx = best[0]; root._cy = best[1]
}
placed.push({ x: best[0], y: best[1], R })
root._cx = best[0]; root._cy = best[1]
return placed
}
// the poster layout first, remembered per node in units of its own extent (so it is scale-free).
// DIALECTS GET NO SLOT ON THE POSTER: resources are catalogued per LANGUAGE, and 2,653 dialect slots
// squeezed the 858 languages so that ~40% of language names could not be printed without overlap.
const clearMemo = (n) => { delete n._leaves; delete n._depth; n.kids.forEach(clearMemo) }
const hideDialects = (n) => { n._allKids = n.kids; n.kids = n.kids.filter((k) => k.level !== 'dialect'); n.kids.forEach(hideDialects) }
const showDialects = (n) => { if (n._allKids) { n.kids = n._allKids; delete n._allKids } n.kids.forEach(showDialects) }
roots.forEach(hideDialects); roots.forEach(clearMemo)
;({ ARC, STEP_MIN } = POSTER)
roots.forEach(layoutFamily)
{
const pl = pack()
let pext = 1
for (const p of pl) pext = Math.max(pext, Math.abs(p.x) + p.R, Math.abs(p.y) + p.R)
const keep = (n, root) => { n._pu = [(root._cx + n._r * Math.cos(n._a)) / pext, (root._cy + n._r * Math.sin(n._a)) / pext]; n.kids.forEach((k) => keep(k, root)) }
for (const root of roots) { keep(root, root); root._pR = root._R / pext }
}
// then the screen layout, exactly as before (dialects back, memos cleared)
roots.forEach(showDialects); roots.forEach(clearMemo)
;({ ARC, STEP_MIN } = { ARC: 1.0, STEP_MIN: 3.2 })
roots.forEach(layoutFamily)
const placed = pack()
// ── to lng/lat through inverse Web Mercator, uniformly scaled, so the drawing is not distorted ─────
let ext = 1
for (const p of placed) ext = Math.max(ext, Math.abs(p.x) + p.R, Math.abs(p.y) + p.R)
@ -162,7 +189,8 @@ function emit(n, root, parentId, isRoot) {
if (n.via) node.via = n.via
if (lv === 'l' || (lv === 'd' && n.iso && groupsBy.has(n.iso))) node.st = statusOf(n.iso)
if (n.iso && groupsBy.has(n.iso)) node.g = groupsBy.get(n.iso)
if (lv === 'f' || isRoot) node.R = Math.round(root._R * K * 1e6) / 1e6
if (lv === 'f' || isRoot) { node.R = Math.round(root._R * K * 1e6) / 1e6; node.pR = Math.round(root._pR * 1e5) / 1e5 }
if (n._pu) node.pc = [Math.round(n._pu[0] * 1e5) / 1e5, Math.round(n._pu[1] * 1e5) / 1e5] // dialects: none
nodes.push(node)
// Edges are NOT written: every node carries its parent id and both coordinates, so the board draws
// each parent->child line itself (4,300 lines in one GeoJSON source). It halves the file.

View file

@ -48,7 +48,7 @@ const get = (lc, key) => {
// SETTLED, not deferred: the correct word in that language IS the English spelling. Each is a decision,
// listed by locale and key so a new identical string is still caught.
const SAME_ON_PURPOSE = new Set(['fr:res.source', 'fr:res.type', 'ro:tree.dialect', 'fr:facet.religion',
'de:facet.region', 'de:facet.religion'])
'de:facet.region', 'de:facet.religion', 'de:tree.poster', 'it:tree.poster', 'ro:tree.poster'])
let fails = 0
if (keys.size < 100) { console.log(`NOT MEASURED: read only ${keys.size} keys (expected 100+); the parser is broken, not the bundle`); process.exitCode = 75 }

View file

@ -42,7 +42,7 @@ try {
const M = await load('src/model.js')
const { useBoard } = await load('src/useBoard.js')
const panels = {}
for (const n of ['GiFacetRail', 'GiMap', 'GiCharts', 'GiGrid', 'GiDrawer', 'GiTree', 'GiResources']) panels[n] = (await load(`src/components/${n}.vue`)).default
for (const n of ['GiCombos', 'GiFacetRail', 'GiMap', 'GiCharts', 'GiGrid', 'GiDrawer', 'GiTree', 'GiResources']) panels[n] = (await load(`src/components/${n}.vue`)).default
const gi = T('gospel-internet.json')
const rows = POISON ? [] : M.normalizeRows(gi)
@ -86,13 +86,18 @@ try {
const en = await render('en')
ok(en.errors.length === 0, `en: every panel rendered with no error (${en.errors.length}${en.errors[0] ? ': ' + en.errors[0].slice(0, 160) : ''})`)
for (const [name, re] of [['GiFacetRail', /data-facet="access"/], ['GiGrid', /data-grid-total/], ['GiCharts', /data-region="charts"/],
['GiTree', /data-tree-legend/], ['GiResources', /data-resources-total/], ['GiMap', /data-region="map"/]]) ok(re.test(en.html), `en: ${name} rendered its region`)
['GiTree', /data-tree-legend[\s\S]*data-tree-poster/], ['GiResources', /data-resources-total/], ['GiMap', /data-region="map"/]]) ok(re.test(en.html), `en: ${name} rendered its region`)
ok(!!open && en.html.includes(open.people_group), `en: the drawer shows the opened group (${open ? open.people_group : 'no row'})`)
ok(/data-notice="approximate-pin"/.test(en.html), 'en: the drawer carries the approximate-pin notice')
const fam = treeDoc.nodes.filter((n) => !n.p).sort((a, b) => b.n.length - a.n.length)
ok(/data-tree-card/.test(en.html), 'en: the tree card shows the node the board is narrowed to')
ok(/href="https?:\/\/[^"]+"/.test(en.html.slice(en.html.indexOf('data-region="resources"'))), 'en: the resources tab lists real links')
ok(/\b\d[\d,]* of [\d,]+ people groups\b/.test(en.html), 'en: the grid prints its scope count')
// the selection strip: the kit's chips name the TREE NODE (not 120 language codes), and a combination
// carries its live count (nothing in own language x 60%+ online = 220 on the whole roster)
const fam = treeDoc.nodes.filter((n) => !n.p).sort((x, y) => 0)[0]
const famNode = (() => { const ix = M.indexTree(treeDoc); return ix.nodes.filter((n) => !n.p).sort((x, y) => ix.isos.get(y.id).size - ix.isos.get(x.id).size)[0] })()
ok(/data-chip="language"/.test(en.html) && en.html.includes(famNode.n), `en: the kit chip names the tree node (${famNode.n})`)
ok(new RegExp('data-combo="digital-now"[^>]*>\\s*<span>[^<]+</span><span class="gic2-n">' + (POISON ? 'x' : '220')).test(en.html), 'en: a combination shows its live count (digital-now = 220)')
void fam
const ar = await render('ar')
ok(ar.errors.length === 0, `ar: every panel rendered with no error (${ar.errors.length})`)

View file

@ -100,6 +100,27 @@ console.log(`info closest two nodes: ${minD.toFixed(4)} px at z0 = ${(minD * 128
// Dialect labels first show at z5.6 (dots at every zoom); two dots closer than 4 px by z7 print as one.
ok(minD * 2 ** 7 >= 4, 'no two nodes print on one another at zoom 7 (>= 4 px apart)')
// 6. the poster: no two labels overlap, at both sheet sizes; every family is named at 4 x 4 ft; the
// language coverage does not fall below what was measured when the poster shipped (671 / 779 of 858)
{
const M = await import(resolve(HERE, '../src/model.js'))
const { buildTreePoster } = await import(resolve(HERE, '../src/treePoster.js'))
const ixP = M.indexTree(tree)
const scP = M.treeScope(ixP, M.normalizeRows(gi))
const ovl = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1]
for (const [size, floor] of [[36, 660], [48, 770]]) {
const { svg, stats } = buildTreePoster(ixP, scP, { sizeIn: size })
const L = stats.labelBoxes
let ov = POISON ? 1 : 0
for (let i = 0; i < L.length; i++) for (let j = i + 1; j < L.length; j++) if (L[i].some((a) => L[j].some((b) => ovl(a, b)))) ov++
const texts = (svg.match(/<text /g) || []).length
ok(ov === 0, `poster ${size} in: no two labels overlap (${stats.labels} labels, ${ov} overlapping pairs)`)
ok(858 - stats.dropped.l >= floor, `poster ${size} in: ${858 - stats.dropped.l} of 858 languages named (floor ${floor})`)
ok(texts >= stats.labels, `poster ${size} in: every placed label is in the SVG (${texts} text elements)`)
if (size === 48) ok(stats.dropped.f === 0, `poster 48 in: every family named (${stats.dropped.f} left off)`)
}
}
if (PNG) {
// a tiny RGBA rasteriser: links, then dots, then family rings. No dependency.
const S = 1400, scale = S / W

View file

@ -0,0 +1,136 @@
<script setup>
/**
* GiCombos.vue — the strip under the title: WHAT IS SELECTED (the kit's DashboardFilterChips, each
* chip removable) and a row of COMBINATIONS worth running, each with its live count.
*
* A combination is just a set of facet values on the one bus, so it drives every panel at once: the
* map, the language tree, the charts, both tables and the CSV. The counts are computed with the bus's
* own predicate over the rows of the current list, so a combination never promises groups it will not
* show. They are the mobiliser's questions, answered by the data we hold:
* - where a digital approach could work now (nothing in their language, a country mostly online);
* - where audio and film can travel online (they hear but cannot read, and the country is online);
* - where it must go another way (nothing in their language, and the country mostly offline);
* - which channel would carry it (nothing in their language, in a WhatsApp or Telegram country);
* - where a Deaf bridge exists; the two countries that hold half the roster.
*/
import { computed, provide } from 'vue'
import { createSelectionBus } from '@map/composables/useSelectionBus.js'
import DashboardFilterChips from '@map/components/dashboards/DashboardFilterChips.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { filterRows, label, FACET_RAIL } from '../model.js'
const T = useGiT()
const board = useBoardState()
const { bus } = board
const fmt = (n) => (Number(n) || 0).toLocaleString()
// ── the kit's chips, fed through the kit's two inject keys (SPEC-DASHBOARD-KIT 5.1) ─────────────────
const names = computed(() => {
const country = {}, language = {}
for (const r of board.all.value) { country[r.iso3] = r.country; language[r.iso639_3] = r.language }
return { country, language }
})
const CODED = { access_level: 'level', access_detail: 'detail', band: 'band', way_in: 'wayIn' }
function labelOne(key, v) {
if (CODED[key]) return label(T, CODED[key], v)
if (key === 'country') return names.value.country[v] || v
if (key === 'language') return names.value.language[v] || v
return v
}
function labelFor(key, value) {
// a tree node narrows by its language codes: name the node, not a list of 120 codes
if (key === 'language' && board.treeNode.value && board.tree.value) {
const n = board.tree.value.byId.get(board.treeNode.value)
if (n) return n.n
}
return [].concat(value).map((v) => labelOne(key, v)).join(', ')
}
provide('selectionBus', bus)
provide('dashboardConfig', {
facets: {
access_level: { label: T('gospelInternet.facet.access', 'Access in their own language') },
access_detail: { label: T('gospelInternet.col.access', 'In own language') },
band: { label: T('gospelInternet.facet.band', 'Country internet use') },
region: { label: T('gospelInternet.facet.region', 'Region') },
country: { label: T('gospelInternet.facet.country', 'Country') },
province: { label: T('gospelInternet.facet.province', 'India and China: state or province') },
language: { label: T('gospelInternet.facet.language', 'Language') },
religion: { label: T('gospelInternet.facet.religion', 'Religion') },
way_in: { label: T('gospelInternet.facet.wayIn', 'Way in when nothing is in their language') },
messaging: { label: T('gospelInternet.facet.messaging', 'Top messaging app (country)') },
},
facetRail: FACET_RAIL,
legend: { labelFor },
})
// ── the combinations ────────────────────────────────────────────────────────────────────────────────
const COMBOS = computed(() => [
{ id: 'digital-now', label: T('gospelInternet.combos.digitalNow', 'Nothing in their language, country 60%+ online'), filters: { access_detail: 'none', band: 'yes' } },
{ id: 'audio-online', label: T('gospelInternet.combos.audioOnline', 'Hear but cannot read, country online'), filters: { access_detail: 'hear', band: ['yes', 'partial'] } },
{ id: 'offline-gap', label: T('gospelInternet.combos.offlineGap', 'Nothing in their language, country mostly offline'), filters: { access_detail: 'none', band: ['no', 'nodata'] } },
{ id: 'whatsapp', label: T('gospelInternet.combos.whatsapp', 'Nothing in their language, WhatsApp country'), filters: { access_detail: 'none', messaging: ['WhatsApp Messenger', 'WhatsApp Business'] } },
{ id: 'telegram', label: T('gospelInternet.combos.telegram', 'Nothing in their language, Telegram country'), filters: { access_detail: 'none', messaging: 'Telegram Messenger' } },
{ id: 'deaf', label: T('gospelInternet.combos.deaf', 'A Deaf bridge exists'), filters: { way_in: 'deaf' } },
{ id: 'india-china-gap', label: T('gospelInternet.combos.indiaChinaGap', 'India and China: nothing in their language'), filters: { country: ['IND', 'CHN'], access_detail: 'none' } },
])
// live counts, over the rows of the current list, with a scratch bus (the board's own is not touched)
const counts = computed(() => {
const out = {}
const rows = board.all.value
for (const c of COMBOS.value) {
const b = createSelectionBus()
for (const [k, v] of Object.entries(c.filters)) b.setFilter(k, v)
out[c.id] = filterRows(rows, b).length
}
return out
})
const same = (a, b) => JSON.stringify([].concat(a ?? []).slice().sort()) === JSON.stringify([].concat(b ?? []).slice().sort())
const activeCombo = computed(() => {
const keys = Object.keys(bus.filterSet)
return COMBOS.value.find((c) => keys.length === Object.keys(c.filters).length
&& Object.entries(c.filters).every(([k, v]) => same(bus.filterSet[k], v)))?.id || ''
})
function run(c) {
if (activeCombo.value === c.id) { bus.clearFilters(); return }
bus.clearFilters()
for (const [k, v] of Object.entries(c.filters)) bus.setFilter(k, v)
}
const scoped = computed(() => Object.keys(bus.filterSet).length > 0)
useShadowStyles(
'.gic2{grid-area:combos;display:flex;align-items:center;gap:10px;padding:6px 14px;background:#fff;border-bottom:1px solid #d1d4cd;' +
'min-width:0;font:12.5px/1.3 Roboto,-apple-system,"Segoe UI",sans-serif;color:#3b463d}' +
'.gic2-h{flex:none;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:#6d766e}' +
'.gic2-sel{display:flex;align-items:center;gap:6px;min-width:0;flex:0 1 auto;max-width:45%}' +
'.gic2-none{color:#9aa19b;font-style:italic;white-space:nowrap}' +
'.gic2-try{display:flex;align-items:center;gap:6px;min-width:0;flex:1 1 auto;overflow-x:auto;scrollbar-width:thin}' +
'.gic2-c{flex:none;display:inline-flex;align-items:center;gap:6px;border:1px solid #d1d4cd;background:#f3f3f1;color:#3b463d;border-radius:999px;' +
'padding:3px 10px;min-height:26px;font:inherit;cursor:pointer;white-space:nowrap}' +
'.gic2-c:hover{border-color:#92b195}' +
'.gic2-c[aria-pressed="true"]{background:#3b463d;border-color:#3b463d;color:#fff}' +
'.gic2-c:focus-visible{outline:2px solid #92b195;outline-offset:1px}' +
'.gic2-n{font-variant-numeric:tabular-nums;font-weight:700}' +
'.gic2-c[disabled]{opacity:.45;cursor:default}',
'gi-combos',
)
</script>
<template>
<div class="gic2" data-region="combos">
<div class="gic2-sel">
<span class="gic2-h">{{ T('gospelInternet.combos.selection', 'Your selection') }}</span>
<DashboardFilterChips v-if="scoped" />
<span v-else class="gic2-none">{{ T('gospelInternet.combos.none', 'Everything — pick a filter or a combination') }}</span>
</div>
<div class="gic2-try" role="group" :aria-label="T('gospelInternet.combos.try', 'Try a combination')">
<span class="gic2-h">{{ T('gospelInternet.combos.try', 'Try a combination') }}</span>
<button v-for="c in COMBOS" :key="c.id" type="button" class="gic2-c" :data-combo="c.id"
:aria-pressed="activeCombo === c.id ? 'true' : 'false'" :disabled="!counts[c.id]"
@click="run(c)">
<span>{{ c.label }}</span><span class="gic2-n">{{ fmt(counts[c.id]) }}</span>
</button>
</div>
</div>
</template>

View file

@ -313,6 +313,8 @@ onMounted(async () => {
// resize the canvas too, or it stretches the old bitmap.
let ro = null
onMounted(() => {
// A phone opens on the map, not on its legend: at 390px the card covered most of the map (rega r75).
if (mapEl.value && mapEl.value.clientWidth && mapEl.value.clientWidth < 520) legendOpen.value = false
if (mapEl.value && typeof ResizeObserver !== 'undefined') {
ro = new ResizeObserver(() => { try { mapRef.value && mapRef.value.resize() } catch (_) {} })
ro.observe(mapEl.value)
@ -335,7 +337,8 @@ useShadowStyles(
'background:rgba(255,255,255,.96);color:#3b463d;border:1px solid #d1d4cd;border-radius:10px;padding:8px 10px;' +
'font:12px/1.4 Roboto,-apple-system,"Segoe UI",sans-serif;box-shadow:0 2px 10px rgba(0,0,0,.10)}' +
'.gim-lh{display:flex;align-items:center;gap:8px;font-weight:600;font-size:12px;margin-bottom:4px}' +
'.gim-lh button{margin-inline-start:auto;border:0;background:none;color:#6d766e;cursor:pointer;font:inherit;min-width:24px;min-height:24px}' +
// a real 28px tap target (WCAG 2.5.8 wants 24): the glyph alone measured 16px (rega r75)
'.gim-lh button{margin-inline-start:auto;border:0;background:none;color:#6d766e;cursor:pointer;font:inherit;display:inline-grid;place-items:center;width:28px;height:28px;padding:0;flex:none}' +
'.gim-keys{display:flex;flex-wrap:wrap;gap:3px 10px;margin-bottom:6px}' +
'.gim-key{display:inline-flex;align-items:center;gap:5px;font-size:11px}' +
'.gim-dot{width:11px;height:11px;border-radius:50%;box-sizing:border-box;border:1.5px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.2);flex:none}' +

View file

@ -29,6 +29,9 @@ import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { createBoardMap, reducedMotion } from '../boardEngine.js'
import { treeScope, TREE_COLOR } from '../model.js'
import { buildTreePoster, posterPrintHtml } from '../treePoster.js'
import { openPrintFrame } from '@map/composables/printInPlace.js'
import { listLabel } from '@map/lists/peopleGroupList.js'
const T = useGiT()
const board = useBoardState()
@ -239,6 +242,7 @@ async function build() {
} catch (_) {}
}
onMounted(() => {
if (el.value && el.value.clientWidth && el.value.clientWidth < 520) legendOpen.value = false // a phone opens on the tree
if (el.value && typeof ResizeObserver !== 'undefined') {
// the engines only follow WINDOW resizes; a dragged rail or an un-hidden panel must resize the canvas
ro = new ResizeObserver(() => { try { mapRef.value && mapRef.value.resize() } catch (_) {} })
@ -267,6 +271,39 @@ const totals = computed(() => {
for (const n of ix.value.nodes) if (!n.p) { const s = scope.value.get(n.id); t.g += s.g; t.y += s.y; t.r += s.r }
return t
})
// ── the poster: the current selection, as one vector sheet with no overlapping labels ───────────────
const posterSize = ref(36)
const posterNote = ref('')
function makePoster() {
const t = ix.value
if (!t || !scope.value) return null
const size = posterSize.value
const list = listLabel(board.listId.value, (k) => T(k, '')) || ''
const { svg, stats } = buildTreePoster(t, scope.value, {
sizeIn: size,
title: T('gospelInternet.tree.posterTitle', 'Gospel access, language family by language family'),
subtitle: [list, T('gospelInternet.tree.legendTitle', 'In their own language')].filter(Boolean).join(' · '),
legend: ['g', 'y', 'r'].map((s) => ({ col: TREE_COLOR[s], text: T('gospelInternet.tree.st.' + s, ST_EN[s]) })),
footer: [T('gospelInternet.tree.sources', 'Tree: Glottolog (CC BY 4.0). Resources: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, JESUS Film, Global Recordings and others, keyed by ISO 639-3.'), citation.value].filter(Boolean),
omittedText: (n) => T('gospelInternet.tree.posterOmitted', '{n} names left off where they would overlap another; every circle is drawn.', { n: fmt(n) }),
})
posterNote.value = stats.omitted ? T('gospelInternet.tree.posterOmitted', '{n} names left off where they would overlap another; every circle is drawn.', { n: fmt(stats.omitted) }) : ''
return { svg, size }
}
function printPoster() {
const p = makePoster()
if (p) openPrintFrame(posterPrintHtml(p.svg, p.size, T('gospelInternet.tree.posterTitle', 'Gospel access, language family by language family')), { autoprint: true })
}
function downloadPoster() {
const p = makePoster()
if (!p) return
try {
const url = URL.createObjectURL(new Blob([p.svg], { type: 'image/svg+xml;charset=utf-8' }))
const a = document.createElement('a'); a.href = url; a.download = `gospel-language-tree-${p.size}in.svg`; a.style.display = 'none'
document.body.appendChild(a); a.click(); setTimeout(() => { try { URL.revokeObjectURL(url); a.remove() } catch (_) {} }, 200)
} catch (_) { /* a sandbox without downloads: the print path still works */ }
}
const ST_EN = { g: 'New Testament or full Bible found', y: 'Something found, but no New Testament', r: 'Nothing found' }
const citation = computed(() => (ix.value && ix.value.meta && ix.value.meta.citation) || '')
@ -286,11 +323,14 @@ useShadowStyles(
'.gtr-card button{margin-top:6px;border:1px solid #d1d4cd;background:#fff;color:#3b463d;border-radius:6px;min-height:26px;padding:2px 10px;font:inherit;cursor:pointer}' +
'.gtr-legend{inset-block-end:10px;inset-inline-start:10px;padding:8px 10px;max-width:min(360px,calc(100% - 64px))}' +
'.gtr-lh{display:flex;align-items:center;font-weight:700;margin-bottom:4px}' +
'.gtr-lh button{margin-inline-start:auto;border:0;background:none;color:#6d766e;cursor:pointer;min-width:24px;min-height:24px;font:inherit}' +
'.gtr-lh button{margin-inline-start:auto;border:0;background:none;color:#6d766e;cursor:pointer;font:inherit;display:inline-grid;place-items:center;width:28px;height:28px;padding:0;flex:none}' +
'.gtr-row{display:flex;align-items:flex-start;gap:6px;margin:2px 0}' +
'.gtr-row .gtr-dot{margin-top:3px}' +
'.gtr-levels{margin:6px 0 2px;font-size:11.5px}' +
'.gtr-note{font-size:10.5px;color:#6d766e;margin-top:4px}' +
'.gtr-poster{display:flex;align-items:center;gap:6px;margin-top:6px;padding-top:6px;border-top:1px solid #e6e8e3;font-weight:600}' +
'.gtr-poster select,.gtr-poster button{font:inherit;font-weight:400;font-size:12px;border:1px solid #d1d4cd;background:#fff;color:#3b463d;border-radius:6px;min-height:26px;padding:2px 8px;cursor:pointer}' +
'.gtr-poster button:focus-visible,.gtr-poster select:focus-visible{outline:2px solid #92b195;outline-offset:1px}' +
'.gtr-min{position:absolute;inset-block-end:10px;inset-inline-start:10px;z-index:4;border:1px solid #d1d4cd;background:#fff;color:#3b463d;' +
'border-radius:8px;padding:5px 10px;min-height:28px;font:600 12px Roboto,sans-serif;cursor:pointer}',
'gi-tree',
@ -331,6 +371,16 @@ useShadowStyles(
<div class="gtr-row"><span class="gtr-dot" :style="{ background: TREE_COLOR.y }"></span><span>{{ T('gospelInternet.tree.st.y', 'Something found, but no New Testament') }}<template v-if="totals"> · {{ fmt(totals.y) }}</template></span></div>
<div class="gtr-row"><span class="gtr-dot" :style="{ background: TREE_COLOR.r }"></span><span>{{ T('gospelInternet.tree.st.r', 'Nothing found') }}<template v-if="totals"> · {{ fmt(totals.r) }}</template></span></div>
<div class="gtr-levels">{{ T('gospelInternet.tree.levels', 'Family › branch › language › dialect. A family or branch shows the worst colour inside it. A dialect shows its language\'s colour, lighter: resources are catalogued per language, not per dialect. Click a circle to narrow the board to it.') }}</div>
<div class="gtr-poster" data-tree-poster>
<span>{{ T('gospelInternet.tree.poster', 'Poster') }}</span>
<select v-model.number="posterSize" :aria-label="T('gospelInternet.tree.posterSizeAria', 'Poster size')">
<option :value="36">{{ T('gospelInternet.tree.poster3ft', '3 × 3 ft') }}</option>
<option :value="48">{{ T('gospelInternet.tree.poster4ft', '4 × 4 ft') }}</option>
</select>
<button type="button" @click="printPoster">{{ T('gospelInternet.tree.posterPrint', 'Print') }}</button>
<button type="button" @click="downloadPoster">{{ T('gospelInternet.tree.posterSvg', 'Download SVG') }}</button>
</div>
<div v-if="posterNote" class="gtr-note">{{ posterNote }}</div>
<div class="gtr-note">{{ T('gospelInternet.tree.sources', 'Tree: Glottolog (CC BY 4.0). Resources: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, JESUS Film, Global Recordings and others, keyed by ISO 639-3.') }} {{ citation }}</div>
</div>
<button v-else type="button" class="gtr-min" @click="legendOpen = true">{{ T('gospelInternet.legend.show', 'Legend') }}</button>

View file

@ -0,0 +1,175 @@
/**
* treePoster.js — the language tree as a PRINTABLE POSTER: one vector SVG, 36 x 36 inches by default,
* every family, branch, language and dialect in its colour, and every label placed so that NO TWO
* LABELS OVERLAP (the agent, 2026-09-27: a 3 ft x 3 ft poster "that had no overlaps").
*
* VECTOR, NOT PIXELS: an SVG prints sharp at any size, so a 3-foot sheet needs no giant canvas (a WebGL
* or 2D canvas that large exceeds what many browsers allow). The map engine's own label placement is
* per-screen; a poster needs its placement solved ONCE for the sheet, which is what this does.
*
* LABELS: placed greedily in priority order (families, then languages with the most groups, then
* branches, then dialects), each trying below / above / right / left of its dot, and kept only where it
* overlaps no placed label and no dot. What cannot be placed is COUNTED and said on the sheet, never
* drawn over another. Text width is estimated per character (0.56 em, a conservative Latin average); a
* margin pads every box so the estimate errs toward space.
*
* Pure: no DOM, no Vue. Returns { svg, stats }. The board prints it through library printInPlace.
*/
const UNIT = 72 // SVG user units per inch (1 pt)
const COL = { g: '#1f8f5f', y: '#e3a82b', r: '#b42318' }
const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
/** Web Mercator x/y (0..1) of a node's lng/lat: the space the tree was laid out in, undistorted. */
function merc([lng, lat]) {
const s = Math.sin(lat * Math.PI / 180)
return [(lng + 180) / 360, 0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)]
}
/**
* @param {object} ix indexTree(doc) (+ .nodes with c, lv, n, R, st, inh)
* @param {Map} scope treeScope(ix, rows): per node { col, m, g, y, r, groups }
* @param {object} o { sizeIn=36, title, subtitle, legend:[{col,text}], footer, omittedText(n) }
*/
export function buildTreePoster(ixIn, scope, o = {}) {
let ix = ixIn
const sizeIn = o.sizeIn || 36
const W = sizeIn * UNIT
const margin = 1.2 * UNIT, head = 2.4 * UNIT, foot = 2.2 * UNIT
const area = { x: margin, y: margin + head, w: W - 2 * margin, h: W - 2 * margin - head - foot }
// The POSTER layout (pc, pR: wider leaf spacing, from build-language-tree.mjs) when the tree carries it,
// else the screen one. A sheet cannot zoom, so it needs the layout made for one fixed scale.
// The poster draws LANGUAGES, not dialects, by default: its layout gives dialects no slot (see the builder).
const drawn = ix.nodes.filter((n) => Array.isArray(n.pc))
const usePoster = drawn.length > 0
if (usePoster) ix = { ...ix, nodes: drawn }
const xy = (n) => (usePoster ? n.pc : merc(n.c))
const rad = (n) => (usePoster ? (n.pR || 0) : (n.R || 0))
// fit the tapestry (dishes included) into the drawing area
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity
for (const n of ix.nodes) {
const [x, y] = xy(n), R = n.p ? 0 : rad(n)
x0 = Math.min(x0, x - R); x1 = Math.max(x1, x + R); y0 = Math.min(y0, y - R); y1 = Math.max(y1, y + R)
}
const k = Math.min(area.w / (x1 - x0), area.h / (y1 - y0))
const ox = area.x + (area.w - (x1 - x0) * k) / 2, oy = area.y + (area.h - (y1 - y0) * k) / 2
const P = (n) => { const [x, y] = xy(n); return [ox + (x - x0) * k, oy + (y - y0) * k] }
const DOT = { f: 5, l: 2.4, b: 1.4, d: 1.1 }
const FONT = { f: 15, l: 5.2, b: 4.2, d: 3.6 } // points; dialects ~1.3 mm, still legible on paper
const pos = new Map(ix.nodes.map((n) => [n.id, P(n)]))
// ── label placement with a spatial grid of occupied boxes ─────────────────────────────────────────
const CELL = 24
const grid = new Map()
const boxes = []
const keyOf = (i, j) => i + ',' + j
const cellsOf = (b) => { const out = []; for (let i = Math.floor(b[0] / CELL); i <= Math.floor(b[2] / CELL); i++) for (let j = Math.floor(b[1] / CELL); j <= Math.floor(b[3] / CELL); j++) out.push(keyOf(i, j)); return out }
const hits = (b) => { for (const c of cellsOf(b)) for (const q of grid.get(c) || []) if (b[0] < q[2] && b[2] > q[0] && b[1] < q[3] && b[3] > q[1]) return true; return false }
const occupy = (b) => { boxes.push(b); for (const c of cellsOf(b)) { if (!grid.has(c)) grid.set(c, []); grid.get(c).push(b) } }
// dots are obstacles too: a label never covers another circle
for (const n of ix.nodes) { const [x, y] = pos.get(n.id), r = (DOT[n.lv] || 1) + 0.4; occupy([x - r, y - r, x + r, y + r]) }
const order = ix.nodes.filter((n) => n.n).map((n) => {
const s = scope.get(n.id) || {}
const rank = !n.p ? 0 : n.lv === 'l' ? 1 : n.lv === 'b' ? 2 : 3
return { n, rank, groups: s.groups || 0 }
}).sort((a, b) => a.rank - b.rank || b.groups - a.groups || a.n.n.localeCompare(b.n.n))
const labels = []
const dropped = { f: 0, b: 0, l: 0, d: 0 }
const kids = ix.kids || new Map()
// a label along a direction, as a chain of small boxes (so the axis-aligned grid can test it)
const chain = (sx, sy, cos, sin, w, fs) => {
const out = [], step = fs * 0.9
for (let t = 0; t <= w; t += step) { const cx = sx + cos * (t + step / 2), cy = sy + sin * (t + step / 2), r = fs * 0.55; out.push([cx - r, cy - r, cx + r, cy + r]) }
return out
}
for (const { n } of order) {
const lv = !n.p ? 'f' : n.lv
const fs = FONT[lv], w = String(n.n).length * fs * 0.56 + 1, h = fs * 1.15
const [x, y] = pos.get(n.id), d = (DOT[lv] || 1) + 1
let placed = null
// 1. a FAMILY's name sits on top of its dish, outside its crowded tree
if (lv === 'f') {
const top = y - rad(n) * k - 6
const b = [x - w / 2 - 0.5, top - fs, x + w / 2 + 0.5, top + fs * 0.25]
if (!hits(b)) placed = { boxes: [b], anchor: 'middle', tx: x, ty: top, rot: 0 }
}
// 2. a LEAF's name runs outward along its branch, like a spoke (the radial-tree convention)
if (!placed && n.p && !(kids.get(n.id) || []).length) {
const [px, py] = pos.get(n.p)
const L = Math.hypot(x - px, y - py) || 1, cos = (x - px) / L, sin = (y - py) / L
const sx = x + cos * d, sy = y + sin * d
const bs = chain(sx, sy, cos, sin, w, fs)
if (!bs.some(hits)) {
let deg = Math.atan2(sin, cos) * 180 / Math.PI
const flip = cos < 0
if (flip) deg += 180
placed = { boxes: bs, anchor: flip ? 'end' : 'start', tx: sx, ty: sy, rot: deg, dy: fs * 0.35 }
}
}
// 3. otherwise below / above / right / left of the dot
if (!placed) {
const tries = [
[x - w / 2, y + d, 'middle', x, y + d + fs * 0.9],
[x - w / 2, y - d - h, 'middle', x, y - d - fs * 0.25],
[x + d, y - h / 2, 'start', x + d, y + fs * 0.35],
[x - d - w, y - h / 2, 'end', x - d, y + fs * 0.35],
]
for (const [bx, by, anchor, tx, ty] of tries) {
const b = [bx - 0.5, by - 0.5, bx + w + 0.5, by + h + 0.5]
if (!hits(b)) { placed = { boxes: [b], anchor, tx, ty, rot: 0 }; break }
}
}
if (!placed) { dropped[lv]++; continue }
placed.boxes.forEach(occupy)
labels.push({ lv, text: n.n, x: placed.tx, y: placed.ty, anchor: placed.anchor, fs, rot: placed.rot, dy: placed.dy || 0,
m: (scope.get(n.id) || {}).m, boxes: placed.boxes })
}
// ── the drawing ───────────────────────────────────────────────────────────────────────────────────
const parts = []
parts.push(`<rect x="0" y="0" width="${W}" height="${W}" fill="#f3f3f1"/>`)
for (const n of ix.nodes) if (!n.p) {
const [x, y] = pos.get(n.id)
parts.push(`<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(rad(n) * k).toFixed(1)}" fill="#ffffff" fill-opacity="0.7" stroke="#d1d4cd" stroke-width="0.8"/>`)
}
const links = []
for (const n of ix.nodes) if (n.p) {
const [a, b] = pos.get(n.p), [c, d] = pos.get(n.id)
links.push(`M${a.toFixed(1)} ${b.toFixed(1)}L${c.toFixed(1)} ${d.toFixed(1)}`)
}
parts.push(`<path d="${links.join('')}" stroke="#8d968e" stroke-width="0.35" stroke-opacity="0.7" fill="none"/>`)
for (const n of ix.nodes) {
const [x, y] = pos.get(n.id), s = scope.get(n.id) || {}
const col = COL[s.col] || '#9aa2ad', lv = !n.p ? 'f' : n.lv
const op = s.m === 0 ? 0.2 : n.inh ? 0.5 : 1
parts.push(`<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${DOT[lv] || 1}" fill="${col}" fill-opacity="${op}" stroke="#fff" stroke-width="${lv === 'f' ? 1 : 0.3}"/>`)
}
for (const l of labels) {
const weight = l.lv === 'f' ? 700 : 400, fill = l.lv === 'b' || l.lv === 'd' ? '#6d766e' : '#3b463d'
const rot = l.rot ? ` transform="rotate(${l.rot.toFixed(1)} ${l.x.toFixed(1)} ${l.y.toFixed(1)})" dy="${l.dy.toFixed(1)}"` : ''
parts.push(`<text x="${l.x.toFixed(1)}" y="${l.y.toFixed(1)}"${rot} font-size="${l.fs}" font-weight="${weight}" text-anchor="${l.anchor}" fill="${fill}"${l.m === 0 ? ' fill-opacity="0.45"' : ''}>${esc(l.text)}</text>`)
}
// title, legend, footer
const omitted = dropped.f + dropped.b + dropped.l + dropped.d
parts.push(`<text x="${margin}" y="${margin + 40}" font-size="44" font-weight="700" fill="#3b463d">${esc(o.title || '')}</text>`)
parts.push(`<text x="${margin}" y="${margin + 80}" font-size="20" fill="#6d766e">${esc(o.subtitle || '')}</text>`)
let lx = margin
for (const it of o.legend || []) {
parts.push(`<circle cx="${lx + 9}" cy="${margin + 122}" r="9" fill="${it.col}"/><text x="${lx + 26}" y="${margin + 129}" font-size="20" fill="#3b463d">${esc(it.text)}</text>`)
lx += 60 + String(it.text).length * 11
}
const fy = W - margin - foot + 50
for (const [i, line] of [...(o.footer || []), omitted && o.omittedText ? o.omittedText(omitted) : ''].filter(Boolean).entries()) {
parts.push(`<text x="${margin}" y="${fy + i * 26}" font-size="16" fill="#6d766e">${esc(line)}</text>`)
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${sizeIn}in" height="${sizeIn}in" viewBox="0 0 ${W} ${W}" font-family="'Noto Sans','Roboto','Segoe UI',Arial,sans-serif">${parts.join('')}</svg>`
return { svg, stats: { nodes: ix.nodes.length, labels: labels.length, dropped, omitted, labelBoxes: labels.map((l) => l.boxes), sizeIn } }
}
/** The print page: one sheet of exactly the poster's size, no margins, nothing but the poster. */
export function posterPrintHtml(svg, sizeIn = 36, title = '') {
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc(title)}</title><style>@page{size:${sizeIn}in ${sizeIn}in;margin:0}html,body{margin:0;padding:0}svg{display:block;width:${sizeIn}in;height:${sizeIn}in}</style></head><body>${svg}</body></html>`
}

View file

@ -223,12 +223,14 @@ const intro = computed(() => {
const b = t('messages.upg100WallIntroEmph')
const c = t('messages.upg100WallIntroB')
const missing = (v, key) => !v || v === key
if (missing(a, 'messages.upg100WallIntroA') || missing(b, 'messages.upg100WallIntroEmph')) {
// ⚠ THE LEGACY KEY STILL CONTAINS THE MARKUP, so it is passed through unescaped exactly as before.
// Escaping it here would print a literal "<strong>" to the reader — a visible regression traded for
// a console warning, which is the wrong way round.
return t('messages.upg100WallIntro')
}
// ★ THE LEGACY upg100WallIntro FALLBACK IS GONE (2026-09-27). It existed for exactly one window: the
// three plain-text keys were landing in studio's catalogue while this consumer was already written,
// so the branch rendered the old single markup-bearing key until they arrived. They are on main in
// all eleven locales, so the branch is now DEAD CODE — and dead code that names a key keeps that key
// alive: studio cannot delete upg100WallIntro while this line reads it (their L13U measurement).
// A fallback whose condition can no longer be true is not safety, it is a reference.
// If A or Emph ever went missing the per-key fallback still yields English from the en block, and
// check:i18n-keys-exist fails the build before a reader could see it.
const tail = missing(c, 'messages.upg100WallIntroB') ? '' : escHtml(c)
return `${escHtml(a)}<strong>${escHtml(b)}</strong>${tail}`
})

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -165,7 +165,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak",
"integrity": "sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ",
"elements": [
"country-posters-map"
],
@ -187,7 +187,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -202,7 +202,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -217,7 +217,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -604,7 +604,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6",
"integrity": "sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk",
"elements": [
"gospel-internet-board"
],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{
@ -814,7 +814,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5",
"integrity": "sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW",
"elements": [
"my-upg-100-list-map"
],
@ -836,7 +836,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -851,7 +851,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -866,7 +866,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -165,7 +165,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak",
"integrity": "sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ",
"elements": [
"country-posters-map"
],
@ -187,7 +187,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -202,7 +202,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -217,7 +217,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-nLy4+pD4T/7yHZJ/6L3+l5ZkjovvoZ+UdwLMZLbS50uo+KfBEfkDSZTko8/EuYak\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-a7b8lN+KcKMcBv8m4itlWhBOJIORYvSz6t43qidfB5ghCedu59a4ZQP4Klh8glxZ\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -604,7 +604,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6",
"integrity": "sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk",
"elements": [
"gospel-internet-board"
],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-5Cv+d2R8SZza4WyDkOZ1m1MC7WPbc1iVPu4mBmZ5iHx0ScaPZrKoJPWxWaJ2M9x6\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-e3A59fHvm9wks4/2lsOVUKuhIaLpyjBtX9h4HPFly79Vi9ux2BG1SFTCM9QVJxvk\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{
@ -814,7 +814,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5",
"integrity": "sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW",
"elements": [
"my-upg-100-list-map"
],
@ -836,7 +836,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -851,7 +851,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -866,7 +866,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-yZNsb36OobOoD86nr2OKMrtXS4SqxMY6dxMsBwveUzDTrdaBCTWU4ebsQ2miIeI5" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-ArVvltGZK+YCdifi5DCHjXkcrWUm1Nz9LpC5doGzcZ54pQI+eKG46BG7tOUFGWyW" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: