Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed

# Conflicts:
#	public/doxa-maps/index.html
This commit is contained in:
doxa-maps-88655c70 2026-09-25 04:59:26 +00:00
commit debb67338a
24 changed files with 93239 additions and 120 deletions

File diff suppressed because it is too large Load diff

View file

@ -39,7 +39,12 @@ const page = await (await browser.newContext({ viewport: { width: 1280, height:
let current = '(page load)'
const seen = {}
const note = (kind, v) => { const s = (seen[current] = seen[current] || { errors: [], failed: [] }); s[kind].push(v) }
page.on('pageerror', (e) => note('errors', String(e.message || e).slice(0, 160)))
// Keep the top of the STACK, not just the message: a message like "reading 'getRootNode'" names no
// caller, and the owner cannot fix it without knowing which call site threw (asked 2026-09-25).
page.on('pageerror', (e) => {
const frames = String(e.stack || '').split('\n').slice(1, 5).map((l) => l.trim().replace(/https?:\/\/[^/]+/, '').replace(/\?v=[0-9a-f]+/, '')).join(' <- ')
note('errors', String(e.message || e).slice(0, 160) + (frames ? ' [' + frames + ']' : ''))
})
page.on('response', (r) => {
if (r.status() < 400) return
let host = ''

View file

@ -32,7 +32,19 @@
"tallyEach": "كل",
"tallyIs": "هو شعب واحد —",
"tallyOf": "منها،",
"tallyLink": "لديها رابط"
"tallyLink": "لديها رابط",
"doxa0": "لدى الجميع شيء",
"doxa1": "‪1–4‬ بلا شيء",
"doxa5": "‪5–14‬",
"doxa15": "‪15–39‬",
"doxa40": "‪40+‬",
"cvd1": "‪1–4‬ لديهم موارد",
"cvd5": "‪5–14‬",
"cvd15": "‪15–39‬",
"cvd40": "‪40+‬",
"onMap": "شعوب دوكسا على هذه الخريطة",
"notPlaced": "ضمن القائمة، بلا موقع بعد",
"outOfScope": "لا توجد شعوب دوكسا — غير ملوَّنة"
},
"tabs": {
"groups": "أكبر الشعوب",

View file

@ -32,7 +32,19 @@
"tallyEach": "Jeder",
"tallyIs": "ist EINE Volksgruppe —",
"tallyOf": "davon,",
"tallyLink": "mit einem Link"
"tallyLink": "mit einem Link",
"doxa0": "alle haben etwas",
"doxa1": "1–4 ohne",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 erreicht",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Doxa-Volksgruppen auf dieser Karte",
"notPlaced": "auf der Liste, noch nicht verortet",
"outOfScope": "keine Doxa-Volksgruppen — nicht eingefärbt"
},
"tabs": {
"groups": "Größte Gruppen",

View file

@ -32,7 +32,19 @@
"tallyEach": "Each",
"tallyIs": "is ONE people group —",
"tallyOf": "of them,",
"tallyLink": "with a link"
"tallyLink": "with a link",
"doxa0": "all have something",
"doxa1": "1–4 with nothing",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 reached",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Doxa people groups on this map",
"notPlaced": "on the roster, not yet placed",
"outOfScope": "no Doxa people groups — not coloured"
},
"tabs": {
"groups": "Largest groups",

View file

@ -32,7 +32,19 @@
"tallyEach": "Cada",
"tallyIs": "es UN pueblo —",
"tallyOf": "de ellos,",
"tallyLink": "con un enlace"
"tallyLink": "con un enlace",
"doxa0": "todos tienen algo",
"doxa1": "1–4 sin nada",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 alcanzados",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Pueblos de Doxa en este mapa",
"notPlaced": "en la lista, aún sin ubicar",
"outOfScope": "sin pueblos de Doxa — sin color"
},
"tabs": {
"groups": "Pueblos más grandes",

View file

@ -32,7 +32,19 @@
"tallyEach": "Chaque",
"tallyIs": "est UN peuple —",
"tallyOf": "d'entre eux,",
"tallyLink": "avec un lien"
"tallyLink": "avec un lien",
"doxa0": "tous ont quelque chose",
"doxa1": "1–4 sans rien",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 atteints",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Peuples Doxa sur cette carte",
"notPlaced": "sur la liste, pas encore localisés",
"outOfScope": "aucun peuple Doxa — non coloré"
},
"tabs": {
"groups": "Peuples les plus nombreux",

View file

@ -32,7 +32,19 @@
"tallyEach": "हर",
"tallyIs": "एक जनसमूह है —",
"tallyOf": "में से,",
"tallyLink": "के पास लिंक है"
"tallyLink": "के पास लिंक है",
"doxa0": "सभी के पास कुछ है",
"doxa1": "1–4 के पास कुछ नहीं",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 तक पहुँच",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "इस मानचित्र पर दोक्सा जनसमूह",
"notPlaced": "सूची में, अभी स्थान नहीं",
"outOfScope": "कोई दोक्सा जनसमूह नहीं — रंग नहीं"
},
"tabs": {
"groups": "सबसे बड़े जनसमूह",

View file

@ -32,7 +32,19 @@
"tallyEach": "Ogni",
"tallyIs": "è UN popolo —",
"tallyOf": "di questi,",
"tallyLink": "con un link"
"tallyLink": "con un link",
"doxa0": "tutti hanno qualcosa",
"doxa1": "1–4 senza nulla",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 raggiunti",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Popoli Doxa su questa mappa",
"notPlaced": "in elenco, non ancora collocati",
"outOfScope": "nessun popolo Doxa — non colorato"
},
"tabs": {
"groups": "Popoli più numerosi",

View file

@ -32,7 +32,19 @@
"tallyEach": "Cada",
"tallyIs": "é UM povo —",
"tallyOf": "deles,",
"tallyLink": "com um link"
"tallyLink": "com um link",
"doxa0": "todos têm algo",
"doxa1": "1–4 sem nada",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 alcançados",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Povos Doxa neste mapa",
"notPlaced": "na lista, ainda sem localização",
"outOfScope": "sem povos Doxa — sem cor"
},
"tabs": {
"groups": "Maiores povos",

View file

@ -32,7 +32,19 @@
"tallyEach": "Fiecare",
"tallyIs": "este UN popor —",
"tallyOf": "dintre ele,",
"tallyLink": "cu un link"
"tallyLink": "cu un link",
"doxa0": "toate au ceva",
"doxa1": "1–4 fără nimic",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 atinse",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Popoare Doxa pe această hartă",
"notPlaced": "pe listă, încă nelocalizate",
"outOfScope": "niciun popor Doxa — necolorat"
},
"tabs": {
"groups": "Cele mai mari popoare",

View file

@ -32,7 +32,19 @@
"tallyEach": "Каждая",
"tallyIs": "— это ОДИН народ,",
"tallyOf": "из них",
"tallyLink": "со ссылкой"
"tallyLink": "со ссылкой",
"doxa0": "у всех что-то есть",
"doxa1": "1–4 без ресурсов",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 достигнуты",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "Народы Doxa на этой карте",
"notPlaced": "в списке, пока без координат",
"outOfScope": "нет народов Doxa — не окрашено"
},
"tabs": {
"groups": "Крупнейшие народы",

View file

@ -32,7 +32,19 @@
"tallyEach": "每个",
"tallyIs": "代表一个族群 —",
"tallyOf": "个族群中,",
"tallyLink": "个有链接"
"tallyLink": "个有链接",
"doxa0": "全部都有资源",
"doxa1": "1–4 个一无所有",
"doxa5": "5–14",
"doxa15": "15–39",
"doxa40": "40+",
"cvd1": "1–4 个已触及",
"cvd5": "5–14",
"cvd15": "15–39",
"cvd40": "40+",
"onMap": "本地图上的 Doxa 族群",
"notPlaced": "在名单上,尚未定位",
"outOfScope": "没有 Doxa 族群 — 不着色"
},
"tabs": {
"groups": "最大的族群",

View file

@ -10,9 +10,11 @@
* the key doubles as a filter so the gap can be looked at on its own, and a shared URL
* reopens the group you were looking at.
*
* The country shading is an AVERAGE across a country's groups and the dots are
* individual groups, which is why a red dot can sit on a green country — that case is
* the point of the map, not a rendering fault.
* The country shading is a COUNT of Doxa's groups with nothing found — never a share,
* because 106 of their 175 countries hold one or two groups and a percentage over that
* denominator is noise. The dots are individual groups, which is why a dark dot can sit
* on a light country — that case is the point of the map, not a rendering fault. A
* country Doxa does not work is left uncoloured, not drawn as covered.
*
* > design note 2026-08-01: "mapping out the gospel resources is super important for me
* > … key city posters, cities with internet, gospel resources — all these are
@ -112,52 +114,74 @@ const err = ref(null)
const mode = ref('gap')
// Bands, not a continuous ramp: a reader can hold four categories, not a gradient.
/**
* ═══ THE COLOUR SCALE, AND WHY IT COUNTS INSTEAD OF DIVIDING ════════════════════════════
*
* The old ramp painted a country by the SHARE of its groups with nothing found. Measured
* against Doxa's roster that is not a statistic, it is noise: 106 of their 175 countries
* hold one or two groups, and the United States holds three — Coquille, Pequots, Umpqua.
* One of three without a resource is 33%, so the whole United States rendered amber next
* to India, which holds 855. The map read as "most of the world is in crisis" because of
* arithmetic, not because of the world. Swapping in a prettier ramp would have kept the
* lie and made it handsomer.
*
* So the scale is a COUNT: how many of Doxa's groups in this country have nothing found.
* A count cannot be inflated by a small denominator, it is what a team would actually plan
* around, and it puts India, Sudan and Laos where they belong.
*
* OUT OF SCOPE IS NOT ZERO. A country with no Doxa groups gets NO FILL at all. Painting it
* "green/covered" claimed a finding about a place this dataset says nothing about, which is
* the same class of error as drawing an unmeasured figure as a zero.
*
* THE RAMP. Single family, light to dark, on the house light palette — sage for "every Doxa
* group here has something", then four steps of clay deepening with the count. No yellow:
* a mid-spectrum yellow over a pale basemap is what made the old map look jaundiced, and it
* also carries the least luminance contrast of any hue against #f3f3f1. Sage and clay sit
* far apart in hue AND in lightness, so the scale survives being read without colour.
*/
const NO_DOXA_FILL = 'rgba(0,0,0,0)'
const BANDS = [
/* labelKey, not label: the band names are rendered through t() so they translate with
the rest of the chrome. The id and the colour are data and stay as they are. */
{ id: 'critical', labelKey: 'gospel.legend.band60', color: '#b42318' },
{ id: 'high', labelKey: 'gospel.legend.band30', color: '#e07a2b' },
{ id: 'partial', labelKey: 'gospel.legend.bandSome', color: '#e8c15a' },
{ id: 'covered', labelKey: 'gospel.legend.bandAll', color: '#10b981' },
{ id: 'd0', labelKey: 'gospel.legend.doxa0', color: '#92b195', min: 0, max: 0 },
{ id: 'd1', labelKey: 'gospel.legend.doxa1', color: '#e8d9cd', min: 1, max: 4 },
{ id: 'd5', labelKey: 'gospel.legend.doxa5', color: '#cfa388', min: 5, max: 14 },
{ id: 'd15', labelKey: 'gospel.legend.doxa15', color: '#b06f57', min: 15, max: 39 },
{ id: 'd40', labelKey: 'gospel.legend.doxa40', color: '#7d3b30', min: 40, max: Infinity },
]
// Same four colours read the other way round: green now means "most groups have
// something found", so the eye is not asked to re-learn the palette per mode.
/* Coverage mode asks the opposite question over the same roster: how many Doxa groups here
DO have something. Same count logic, same reason, mirrored ramp. */
const COVER_BANDS = [
{ id: 'c-high', labelKey: 'gospel.legend.cov80', color: '#10b981' },
{ id: 'c-mid', labelKey: 'gospel.legend.cov50', color: '#e8c15a' },
{ id: 'c-low', labelKey: 'gospel.legend.cov1', color: '#e07a2b' },
{ id: 'c-none', labelKey: 'gospel.legend.cov0', color: '#b42318' },
{ id: 'c1', labelKey: 'gospel.legend.cvd1', color: '#e8d9cd', min: 1, max: 4 },
{ id: 'c5', labelKey: 'gospel.legend.cvd5', color: '#a8c4a9', min: 5, max: 14 },
{ id: 'c15', labelKey: 'gospel.legend.cvd15', color: '#6f9673', min: 15, max: 39 },
{ id: 'c40', labelKey: 'gospel.legend.cvd40', color: '#3b463d', min: 40, max: Infinity },
]
const bandFor = (list, n) => list.find((b) => n >= b.min && n <= b.max) || null
/** A country's fill. Null tally = Doxa does not work here = no fill, never a colour. */
const featureColor = (iso3) => {
const t = doxaByCountry.value[iso3]
if (!t) return NO_DOXA_FILL
const n = mode.value === 'gap' ? t.none : t.groups - t.none
const b = bandFor(mode.value === 'gap' ? BANDS : COVER_BANDS, n)
return b ? b.color : NO_DOXA_FILL
}
const bandColor = (b) => (BANDS.find((x) => x.id === b) || {}).color || NO_DOXA_FILL
const activeBands = computed(() => (mode.value === 'gap' ? BANDS : COVER_BANDS))
// PIN TIERS reuse the band hexes above rather than declaring a fourth palette, so a pin
// and the country under it never disagree about what "nothing found" looks like. Read
// straight off BANDS so editing a band moves the pins with it.
// PIN TIERS ARE THEIR OWN SCALE NOW, and deliberately so. They used to index into BANDS,
// which was safe while both scales measured the same thing. They no longer do: a band is a
// COUNT of Doxa groups in a country, a pin is what ONE group HAS. Indexing a count ramp by
// a tier would have painted "nothing found" in the colour of "1-4 groups" — two different
// facts wearing one colour, which is the disagreement that comment was written to prevent.
// Dark green reads as complete, sage as partial, oxblood as empty; all three sit apart from
// the clay country ramp in hue and in lightness, so a pin never dissolves into its country.
const PIN_COLOR = {
2: BANDS[3].color, // Scripture AND film in their own language
1: BANDS[2].color, // one of the two
0: BANDS[0].color, // nothing found in the pools checked — the pin that matters most
2: '#3b463d', // Scripture AND film in their own language
1: '#92b195', // one of the two
0: '#7d3b30', // nothing found in the pools checked — the pin that matters most
}
const bandColor = (b) => (BANDS.find((x) => x.id === b) || {}).color || '#9aa2ad'
// Share of a country's groups with ANY resource found, derived from the same counts
// the gap band uses — so the two modes can never disagree about the same country.
const coverPct = (d) => {
const g = Number(d?.groups) || 0
if (!g) return null
return ((g - (Number(d?.no_resource) || 0)) / g) * 100
}
const coverColor = (d) => {
const p = coverPct(d)
if (p === null) return '#9aa2ad'
if (p >= 80) return '#10b981'
if (p >= 50) return '#e8c15a'
if (p > 0) return '#e07a2b'
return '#b42318'
}
const featureColor = (d) => (mode.value === 'gap' ? bandColor(d.band) : coverColor(d))
const byIso = computed(() => {
const m = {}
for (const r of rows.value) m[r.iso3] = r
@ -174,6 +198,9 @@ const pgLoading = ref(false)
const pgErr = ref(null)
const selectedGroup = ref(null) // set = the people-group detail is showing
const pinRows = shallowRef([]) // [name, lat, lng, iso3, code, population, tier]
const doxaTotal = ref(0) // every group on Doxa's roster, placed or not
const doxaUnplaced = ref(0) // on the roster, no coordinates in this build yet
const doxaByCountry = shallowRef({})
const pinCodes = ref({})
const pinBridges = ref({}) // '<iso639-3>|<ISO3 country>' -> the in-country bridge, if any
const pinCount = ref(0)
@ -790,7 +817,7 @@ function openGroup(g) { selectedGroup.value = g }
function closeSheet() { selected.value = null; selectedGroup.value = null }
async function load() {
const [data, adm0, pinData] = await Promise.all([
const [data, adm0, pinData, doxaData] = await Promise.all([
fetch(`${assetsBase.value}/tables/gospel-by-country.json`).then((r) => r.json()),
// world-adm0 now INCLUDES India, so there is one ADM0 file and no patched
// variant. It used to omit IND (the 100-list splits India into 10 provincial
@ -804,12 +831,40 @@ async function load() {
// than behind a tap. URLs are normalised into `codes` by ISO 639-3, so the 12,379
// pins carry a code rather than 12,379 copies of the same jesusfilm.org link.
fetch(`${assetsBase.value}/tables/people-group-pins.json`).then((r) => r.json()),
/* ═══ THE MAP DRAWS DOXA'S ROSTER, NOT EVERY PEOPLE GROUP ON EARTH ═══════════════
* people-group-pins.json stays exactly as it is — 12,379 groups, 8,437 verified
* links, the whole GRN sweep — because it is the RESEARCH RECORD and the resource
* index every sheet reads. What changed is what gets DRAWN. Doxa works the ~2,100
* groups on pray.doxa.life, and a map of everyone buries the ones they can reach.
* Nothing is lost; the wider set simply stops being dumped on the map. */
fetch(`${assetsBase.value}/tables/doxa-people-groups.json`).then((r) => r.json()),
])
rows.value = data.countries || []
pinCodes.value = pinData.codes || {}
pinBridges.value = pinData.bridges || {}
pinRows.value = pinData.pins || []
/* Same seven-slot shape the rest of this file already speaks, so narrowing the roster
changes WHICH pins exist and nothing about how they are drawn, clicked or filtered. */
const roster = (doxaData.rows || [])
doxaTotal.value = roster.length
pinRows.value = roster
.filter((r) => r.mapped && r.lat != null && r.lng != null)
.map((r) => [r.name, r.lat, r.lng, r.iso3, r.iso639_3 || '', r.population || 0, r.tier ?? 0])
pinCount.value = pinRows.value.length
doxaUnplaced.value = doxaTotal.value - pinCount.value
/* Countries Doxa actually works, and how many of their groups have nothing. A country
outside this set is NOT "zero" — it is out of scope, and painting it at all was the
misleading part: the United States holds three Doxa groups, so one without a resource
read as 33% and turned the whole country amber. */
const tally = {}
for (const r of roster) {
const t = (tally[r.iso3] ||= { groups: 0, none: 0, people: 0 })
t.groups += 1
t.people += r.population || 0
if (r.mapped && r.tier === 0) t.none += 1
}
doxaByCountry.value = tally
return adm0
}
@ -827,8 +882,10 @@ function paint(map, adm0) {
properties: {
iso3: iso,
band: d?.band || 'none',
color: d ? featureColor(d) : 'rgba(0,0,0,0)',
hasData: !!d,
/* Keyed by ISO3, not by the old country row: scope now comes from Doxa's roster,
and a country they do not work gets no fill rather than a "covered" colour. */
color: featureColor(iso),
hasData: !!doxaByCountry.value[iso],
},
geometry: f.geometry,
}
@ -1037,9 +1094,9 @@ useShadowStyles(
// of it rather than run under it. 46px = 32px button + 3px inset + clearance.
'@media(max-width:480px){.gm-legend{width:auto;right:46px}}' +
'.is-dark .gm-legend{background:rgba(19,32,25,.95);color:#e5e7eb}' +
'.gm-title{font-weight:700;font-size:12.5px;margin-bottom:5px}' +
'.gm-title{font-weight:700;font-size:15.5px;margin-bottom:5px}' +
'.gm-keys{display:flex;flex-wrap:wrap;gap:5px 12px}' +
'.gm-key{display:flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}' +
'.gm-key{display:flex;align-items:center;gap:5px;font-size:14px;white-space:nowrap}' +
// This one label is a phrase, not a legend word, so it is allowed to wrap rather than
// be clipped by the nowrap above.
'.gm-key.wide{white-space:normal;text-align:left;line-height:1.25}' +
@ -1049,17 +1106,17 @@ useShadowStyles(
'.gm-pinkey{margin-top:7px;padding-top:6px;border-top:1px solid rgba(0,0,0,.12)}' +
'.is-dark .gm-pinkey{border-top-color:rgba(255,255,255,.16)}' +
'.gm-keys-btn{gap:4px 8px}' +
'.gm-kbtn{border:1px solid transparent;background:transparent;color:inherit;font:inherit;font-size:11px;' +
'.gm-kbtn{border:1px solid transparent;background:transparent;color:inherit;font:inherit;font-size:14px;' +
'padding:2px 6px;border-radius:999px;cursor:pointer;min-height:24px;display:inline-flex;align-items:center;gap:5px}' +
'.gm-kbtn:hover{background:#e9ebe6;border-color:#d1d4cd}' +
'.gm-kbtn.on{background:#3b463d;border-color:#3b463d;color:#fff}' +
'.gm-kbtn b{font-variant-numeric:tabular-nums;opacity:.75;font-weight:700}' +
'.gm-filtered{margin-top:5px;font-size:10.5px;line-height:1.45;color:#3b463d}' +
'.gm-filtered{margin-top:5px;font-size:13.5px;line-height:1.45;color:#3b463d}' +
'.is-dark .gm-filtered{color:#e5e7eb}' +
'.gm-kclear{border:1px solid #d1d4cd;background:#fff;color:#3b463d;font:600 10.5px inherit;' +
'padding:3px 8px;border-radius:6px;cursor:pointer;margin-left:4px;min-height:24px}' +
'.gm-pinkey-h{font-size:11px;font-weight:600;margin-bottom:4px}' +
'.gm-caveat{margin-top:5px;font-size:10.5px;opacity:.72;line-height:1.4}' +
'.gm-pinkey-h{font-size:14px;font-weight:600;margin-bottom:4px}' +
'.gm-caveat{margin-top:5px;font-size:13.5px;opacity:.72;line-height:1.4}' +
'.gm-prose-note{opacity:.9;font-weight:600}' +
// DETAIL: a bottom sheet on a phone, a corner card on desktop.
// ON A PHONE THE SHEET IS THE PAGE, and the shared pin hint sits on top of it. The hint
@ -1078,29 +1135,29 @@ useShadowStyles(
'@media(min-width:700px){.gm-sheet{left:auto;right:14px;bottom:14px;width:330px;border-radius:12px}}' +
'.is-dark .gm-sheet{background:#132019;color:#e5e7eb}' +
'.gm-sh-head{display:flex;align-items:center;gap:10px;margin-bottom:8px}' +
'.gm-sh-name{font-weight:800;font-size:15px}' +
'.gm-sh-name{font-weight:800;font-size:18px}' +
'.gm-x{margin-left:auto;border:1px solid rgba(0,0,0,.16);background:transparent;color:inherit;' +
'font:600 12px inherit;padding:6px 12px;border-radius:8px;cursor:pointer;min-height:34px}' +
'.gm-stat{display:flex;justify-content:space-between;gap:12px;padding:4px 0;' +
'border-bottom:1px solid rgba(0,0,0,.07);font-variant-numeric:tabular-nums}' +
'.is-dark .gm-stat{border-bottom-color:rgba(255,255,255,.09)}' +
'.gm-stat b{font-weight:700}' +
'.gm-note{margin-top:9px;font-size:11px;line-height:1.5;opacity:.75}' +
'.gm-err{position:absolute;top:50%;left:0;right:0;text-align:center;font-size:13px;opacity:.7}' +
'.gm-note{margin-top:9px;font-size:14px;line-height:1.5;opacity:.75}' +
'.gm-err{position:absolute;top:50%;left:0;right:0;text-align:center;font-size:16px;opacity:.7}' +
// MODE TOGGLE — sits in the legend, because the legend is what it changes.
'.gm-fold{width:100%;margin:0 0 6px;border:1px solid #d1d4cd;background:#f3f3f1;color:#3b463d;font:600 11px inherit;padding:6px 10px;border-radius:8px;cursor:pointer;min-height:32px}' +
'.gm-fold:hover{background:#e9ebe6}' +
'.gm-search{position:relative;margin-bottom:6px}' +
'.gm-q{width:100%;box-sizing:border-box;border:1px solid #d1d4cd;border-radius:8px;background:#fff;color:#3b463d;font:12px inherit;padding:7px 26px 7px 9px;min-height:32px}' +
'.gm-q:focus{outline:2px solid #92b195;outline-offset:1px}' +
'.gm-qx{position:absolute;right:4px;top:50%;transform:translateY(-50%);border:0;background:transparent;color:#6d766e;font-size:16px;line-height:1;cursor:pointer;padding:4px 6px;min-height:24px}' +
'.gm-qx{position:absolute;right:4px;top:50%;transform:translateY(-50%);border:0;background:transparent;color:#6d766e;font-size:19px;line-height:1;cursor:pointer;padding:4px 6px;min-height:24px}' +
'.gm-qres{margin:-2px 0 7px;border:1px solid #d1d4cd;border-radius:8px;background:#fff;overflow:hidden}' +
'.gm-qrow{display:flex;align-items:baseline;gap:6px;width:100%;text-align:left;border:0;background:transparent;font:inherit;font-size:11px;color:#3b463d;padding:6px 8px;cursor:pointer;min-height:30px;border-bottom:1px solid #eceee9}' +
'.gm-qrow{display:flex;align-items:baseline;gap:6px;width:100%;text-align:left;border:0;background:transparent;font:inherit;font-size:14px;color:#3b463d;padding:6px 8px;cursor:pointer;min-height:30px;border-bottom:1px solid #eceee9}' +
'.gm-qrow:last-child{border-bottom:0}' +
'.gm-qrow:hover{background:#f3f3f1}' +
'.gm-qn{font-weight:700}' +
'.gm-ql{opacity:.7}' +
'.gm-qnone,.gm-qmore{padding:6px 8px;font-size:10.5px;color:#6d766e}' +
'.gm-qnone,.gm-qmore{padding:6px 8px;font-size:13.5px;color:#6d766e}' +
'.gm-modes{display:flex;gap:6px;margin-bottom:6px}' +
'.gm-mode{font:600 10.5px inherit;padding:4px 10px;border-radius:999px;cursor:pointer;' +
'border:1px solid rgba(0,0,0,.16);background:transparent;color:inherit;min-height:26px}' +
@ -1108,7 +1165,7 @@ useShadowStyles(
'.is-dark .gm-mode{border-color:rgba(255,255,255,.22)}' +
'.is-dark .gm-mode.on{background:#10b981;border-color:#10b981;color:#04241a}' +
// RESOURCE LIST inside the sheet.
'.gm-scope{margin:7px 0 0;font-size:11.5px;line-height:1.45;color:#3b463d}' +
'.gm-scope{margin:7px 0 0;font-size:14.5px;line-height:1.45;color:#3b463d}' +
'.gm-scope b{font-weight:700}' +
'.is-dark .gm-scope{color:#e5e7eb}' +
// Same frame as the sign-language note: a caveat, visually separate from the chips so it
@ -1123,26 +1180,26 @@ useShadowStyles(
'.gm-gtab.on{background:#92b195;border-color:#92b195;color:#fff}' +
'.is-dark .gm-gtab{background:transparent;border-color:rgba(255,255,255,.22);color:#e5e7eb}' +
'.is-dark .gm-gtab.on{background:#92b195;color:#10241a}' +
'.gm-pg-rel{display:block;margin-top:2px;font-size:10px;color:#6d766e;font-style:italic}' +
'.gm-pg-rel{display:block;margin-top:2px;font-size:13px;color:#6d766e;font-style:italic}' +
'.is-dark .gm-pg-rel{color:#9fb3a4}' +
'.gm-rel{margin:8px 0 0;padding:8px 10px;border:1px solid #d1d4cd;border-left:3px solid #92b195;' +
'border-radius:6px;background:#fff;font-size:11.5px;line-height:1.5}' +
'.gm-rel code{font-size:11px;background:#f3f3f1;padding:1px 4px;border-radius:3px}' +
'border-radius:6px;background:#fff;font-size:14.5px;line-height:1.5}' +
'.gm-rel code{font-size:14px;background:#f3f3f1;padding:1px 4px;border-radius:3px}' +
'.is-dark .gm-rel{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);color:#e5e7eb}' +
'.gm-sign{margin:8px 0 0;padding:8px 10px;border:1px solid #d1d4cd;border-left:3px solid #6d766e;' +
'border-radius:8px;background:#f3f3f1;color:#3b463d;font-size:11.5px;line-height:1.5}' +
'border-radius:8px;background:#f3f3f1;color:#3b463d;font-size:14.5px;line-height:1.5}' +
'.is-dark .gm-sign{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);color:#e5e7eb}' +
'.gm-pray{display:block;margin:8px 0 0;padding:9px 11px;border-radius:8px;text-decoration:none;' +
'background:#3b463d;color:#fff;font:600 12.5px inherit;text-align:center}' +
'.gm-pray:hover,.gm-pray:focus-visible{background:#2f382f}' +
'.gm-pray-n{display:block;margin-top:2px;font-weight:500;font-size:11px;opacity:.85}' +
'.gm-tablink{display:block;margin:10px 0 2px;padding:9px 12px;border:1px solid #d1d4cd;border-left:3px solid #92b195;border-radius:8px;background:#f3f3f1;color:#3b463d;font-weight:600;font-size:12.5px;text-decoration:none;min-height:36px;box-sizing:border-box}' +
'.gm-pray-n{display:block;margin-top:2px;font-weight:500;font-size:14px;opacity:.85}' +
'.gm-tablink{display:block;margin:10px 0 2px;padding:9px 12px;border:1px solid #d1d4cd;border-left:3px solid #92b195;border-radius:8px;background:#f3f3f1;color:#3b463d;font-weight:600;font-size:15.5px;text-decoration:none;min-height:36px;box-sizing:border-box}' +
'.gm-tablink:hover{background:#e9ebe6}' +
'.is-dark .gm-tablink{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);color:#e5e7eb}' +
'.gm-stat-alt span{opacity:.72}' +
'.gm-sec{margin-top:12px}' +
'.gm-sec-h{font-weight:800;font-size:12px;letter-spacing:.02em;margin-bottom:6px}' +
'.gm-load{padding:10px 0;font-size:11.5px;opacity:.65}' +
'.gm-sec-h{font-weight:800;font-size:15px;letter-spacing:.02em;margin-bottom:6px}' +
'.gm-load{padding:10px 0;font-size:14.5px;opacity:.65}' +
// .gm-pg is now the SHELL only. The resource rows are a <div> shell (so the chips
// inside can be real anchors) while the no-resource rows are still a <button>; both
// must look identical, so the shell carries the box and .gm-pg-btn/.gm-pg-open carry
@ -1157,8 +1214,8 @@ useShadowStyles(
// look, so the row still reads as one tap-target for the name + language line.
'.gm-pg-open{display:block;width:100%;text-align:left;padding:0;margin:0;border:0;' +
'background:transparent;color:inherit;font:inherit;cursor:pointer}' +
'.gm-pg-n{display:block;font-weight:700;font-size:12.5px}' +
'.gm-pg-l{display:block;font-size:11px;opacity:.75;margin-top:1px}' +
'.gm-pg-n{display:block;font-weight:700;font-size:15.5px}' +
'.gm-pg-l{display:block;font-size:14px;opacity:.75;margin-top:1px}' +
'.gm-code{display:inline-block;font:600 10px ui-monospace,SFMono-Regular,Menlo,monospace;' +
'padding:1px 5px;border-radius:4px;background:rgba(0,0,0,.09);margin-left:5px;vertical-align:1px}' +
'.is-dark .gm-code{background:rgba(255,255,255,.12)}' +
@ -1167,7 +1224,7 @@ useShadowStyles(
// entry can be "Aligned Bible (translationCore-Create-BCS) - Door43 - Portions of both",
// which simply ran off the edge of a 330px sheet and could not be read at all. Long chips
// wrap instead; max-width keeps them inside the panel either way.
'.gm-chip{font-size:10px;padding:2px 7px;border-radius:999px;white-space:nowrap;max-width:100%;' +
'.gm-chip{font-size:13px;padding:2px 7px;border-radius:999px;white-space:nowrap;max-width:100%;' +
'background:rgba(0,0,0,.06);text-decoration:none;color:inherit;border:1px solid transparent}' +
'.gm-chip.wrap{white-space:normal;overflow-wrap:anywhere;line-height:1.35}' +
'.is-dark .gm-chip{background:rgba(255,255,255,.08)}' +
@ -1183,11 +1240,11 @@ useShadowStyles(
// "unknown") must keep looking inert, because it is.
'a.gm-chip{cursor:pointer}' +
'a.gm-chip:hover,a.gm-chip:focus-visible{text-decoration:underline}' +
'.gm-none{font-size:11px;opacity:.7}' +
'.gm-none{font-size:14px;opacity:.7}' +
'.gm-back{border:1px solid rgba(0,0,0,.16);background:transparent;color:inherit;' +
'font:600 11px inherit;padding:5px 11px;border-radius:8px;cursor:pointer;min-height:30px}' +
'.is-dark .gm-back{border-color:rgba(255,255,255,.22)}' +
'.gm-more{font-size:11px;opacity:.66;margin-top:2px}',
'.gm-more{font-size:14px;opacity:.66;margin-top:2px}',
'doxa-gospel-map',
)
</script>
@ -1327,10 +1384,12 @@ useShadowStyles(
two datasets: the fill came from a curated set that had one group for Papua New
Guinea while the map drew 831 pins there, so 135 of 170 countries were coloured
by something the dots on top of them contradicted. -->
<b>A dot and the country under it mean different things.</b> The country is an AVERAGE
across its groups; a dot is that ONE group's own language. So a red dot can sit on a green
country — that is the case worth finding, not a rendering fault. Both are counted from the
same index, so the colour and the dots can no longer disagree.
<b>A dot and the country under it mean different things.</b> A dot is ONE of Doxa's
people groups and its own language. A country is a COUNT of those groups — how many of
Doxa's are there with nothing found — never a percentage: 106 of these 175 countries hold
one or two groups, and a share over a denominator of two says nothing. A country Doxa does
not work is left UNCOLOURED rather than drawn as covered, because this dataset has no
finding about it either way.
<br>“None found” = not in the pools checked (eBible, PNG.Bible, JESUS Film, Door43,
Faith Comes By Hearing, Scripture Earth and Global Recordings Network). GRN is reached
through their 5fish site, and that sweep is now <b>finished for every group in the gap</b>
@ -1342,7 +1401,7 @@ useShadowStyles(
<template v-if="mode === 'coverage'">
A “found” colour is equally bounded: it counts what these pools list, not everything that exists.
</template>
<br>Either way the colour is only a country average — tap a country to see the actual resources, group by group.
<br>Tap a country to see the actual resources, group by group.
</div>
</div>

View file

@ -0,0 +1,183 @@
/**
* fetch-doxa-people-groups.mjs — the 2,107 people groups DOXA actually prays for.
*
* ═══ WHY THIS EXISTS ════════════════════════════════════════════════════════════════════
* This map was built on the full 12,379-pin people-groups dataset, and the Driver's reading
* of it was blunt and correct: it is MISLEADING for the job it is meant to do. Doxa does not
* work 12,379 groups. They work the ~2,100 on pray.doxa.life, and a map that shows every
* group on earth buries the ones they can actually reach — and paints countries by a gap
* that is not theirs.
*
* ═══ THE MEASUREMENT THAT SETTLES THE COLOUR ARGUMENT ═══════════════════════════════════
* Measured 2026-09-25 against the live API:
* 2,107 Doxa people groups across 175 countries
* India 855 · China 154 · Sudan 105 · Brazil 103 · Laos 95
* BUT 106 of those 175 countries hold only ONE OR TWO Doxa groups
* and the United States holds THREE — Coquille, Pequots, Umpqua
* The old choropleth paints a country by the PERCENTAGE of its groups with nothing found.
* Over a denominator of three, one group without a resource is 33% and the whole United
* States turns amber. That is the "mainly yellow map" complaint, and it is not a palette
* problem underneath — it is a statistic that means nothing at n=3 being drawn as if it
* meant something. A prettier ramp over the same arithmetic would be a prettier lie.
*
* ═══ WHAT THIS DOES AND DOES NOT KEEP ═══════════════════════════════════════════════════
* Nothing found for the other 10,000 groups is thrown away — every index, every verified
* link and every sweep result stays exactly where it is in data/tables/. This file adds the
* DOXA ROSTER as its own table so the map can be narrowed to it without losing the research
* behind it. The wider tables remain the record; the map stops being the place they are
* dumped.
*
* ═══ THE JOIN, AND WHAT IT COSTS ════════════════════════════════════════════════════════
* The API carries no coordinates and no ISO 639-3 code, so both come from the pin index by
* (name, country) — the same key scripts/fetch-pray-index.mjs already uses, and the one join
* on this map that is deliberately not by language, because you pray for a people in a
* place. Measured: 1,724 of 2,107 match a pin and EVERY ONE of those carries a language
* code. 383 do not match and are recorded with `mapped: false` rather than dropped — they
* are Doxa's groups whether or not this build can place them, and a roster that silently
* loses 18% of them would be its own kind of lie.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { writeSnapshot } from './_snapshot-guard.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '../../..')
const PINS = resolve(REPO, 'data/tables/people-group-pins.json')
const OUT = resolve(REPO, 'data/tables/doxa-people-groups.json')
const API = 'https://pray.doxa.life/api/people-groups/list'
const SITE = 'https://pray.doxa.life'
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
const res = await fetch(API, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(90000) })
if (!res.ok) {
console.error(`DOXA-PG: ABORT — the roster API answered HTTP ${res.status}. Refusing to write a partial roster.`)
process.exit(1)
}
const { posts } = await res.json()
if (!Array.isArray(posts) || posts.length < 1000) {
console.error(`DOXA-PG: ABORT — the roster came back with ${posts?.length ?? 0} entries. That is not the roster.`)
process.exit(1)
}
const { codes, pins } = JSON.parse(readFileSync(PINS, 'utf8'))
/**
* ═══ THE NAME JOIN, AND THE ONE VARIATION IT IS SAFE TO ABSORB ══════════════════════════
*
* An exact name match left 383 of Doxa's groups unplaced — 18% of their roster invisible on
* the map and absent from every count. Reading the misses rather than guessing at them, one
* class is a spelling convention and nothing more: the roster writes the qualifier FIRST and
* the pin dataset writes it LAST, around a comma.
*
* Acre Arara <-> Arara, Acre
* Ashkuni Nuristani <-> Nuristani Ashkuni
* Banka Samogho <-> Samogho, Banka
*
* Same words, same country, different order. Matching on the SORTED TOKEN SET absorbs
* exactly that and nothing looser. Measured: 94 recovered this way plus 4 that only needed
* diacritics folded, and ZERO ambiguous — every one resolved to a single pin inside its own
* country.
*
* WHAT IT REFUSES, and why that matters more than what it accepts. Fuzzy matching would
* "recover" far more and would be wrong. Doxa lists Akha in Laos; the pins carry Akha Jipea,
* Akha Jaweuy and Akha Piso — a parent against three sub-varieties, and picking one would
* invent a language for a people. Doxa lists Akeu in China; the nearest pin is Ake, a
* different language. So: same country, identical token set, and UNIQUE. Two candidates is a
* refusal, not a coin toss. 285 have no candidate at all and stay unplaced, which is the
* honest state — they need a source this build does not have, not a cleverer string match.
*/
const fold = (n) => String(n || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
const tokenSet = (n) => fold(n).split(' ').filter(Boolean).sort().join(' ')
const key = (n, c) => `${fold(n)}|${String(c || '').toUpperCase()}`
const tkey = (n, c) => `${tokenSet(n)}|${String(c || '').toUpperCase()}`
const pinBy = new Map()
const pinByTokens = new Map()
for (let i = 0; i < pins.length; i++) {
const k = key(pins[i][0], pins[i][3])
if (!pinBy.has(k)) pinBy.set(k, [])
pinBy.get(k).push(i)
const t = tkey(pins[i][0], pins[i][3])
if (!pinByTokens.has(t)) pinByTokens.set(t, [])
pinByTokens.get(t).push(i)
}
const val = (f) => (f && typeof f === 'object' ? f.value : f) || ''
const lab = (f) => (f && typeof f === 'object' ? f.label : f) || ''
let mapped = 0, ambiguous = 0, unmapped = 0, reordered = 0
const rows = posts.map((q) => {
const iso3 = val(q.country_code)
let how = 'exact'
let hit = pinBy.get(key(q.name, iso3))
if (!hit) {
const t = pinByTokens.get(tkey(q.name, iso3))
/* Unique or nothing. A second candidate means the roster name and the pin name are not
the same people, and there is no tie to break that would not be a guess. */
if (t && t.length === 1) { hit = t; how = 'reordered'; reordered++ }
else if (t && t.length > 1) ambiguous++
} else if (hit.length > 1) ambiguous++
const p = hit ? pins[hit[0]] : null
if (p) mapped++; else { unmapped++; how = '' }
const code = p ? p[4] || '' : ''
const c = code ? codes[code] : null
return {
name: q.name,
slug: q.slug,
pray_url: `${SITE}/${q.slug}?source=doxalife`,
iso3,
country: lab(q.country_code),
region: lab(q.wagf_region),
block: lab(q.wagf_block),
rop1: lab(q.rop1),
religion: lab(q.religion),
population: Number(q.population) || 0,
people_praying: Number(q.people_praying) || 0,
people_committed: Number(q.people_committed) || 0,
adopted_by_churches: Number(q.adopted_by_churches) || 0,
/* From the pin index, by (name, country). Empty where this build cannot place them. */
mapped: !!p,
/* How this row found its pin: exact name, or the same words in the other order. Empty
when it did not. Recorded so a reader can audit the join instead of trusting it. */
matched_by: how,
lat: p ? p[1] : null,
lng: p ? p[2] : null,
iso639_3: code,
language: c ? c.l || '' : '',
tier: p ? p[6] : null,
}
})
const byCountry = {}
for (const r of rows) byCountry[r.iso3] = (byCountry[r.iso3] || 0) + 1
const thin = Object.values(byCountry).filter((n) => n <= 2).length
writeSnapshot(
OUT,
`${JSON.stringify({
_note:
'The people groups DOXA prays for, from pray.doxa.life. THIS is what the gospel map ' +
'draws — not the full 12,379-group dataset, which stays in people-group-pins.json as ' +
'the research record. mapped:false means the roster carries them but this build has no ' +
'coordinates for them yet; they are kept, never dropped.',
source: API,
generated: new Date().toISOString().slice(0, 10),
counts: {
groups: rows.length,
countries: Object.keys(byCountry).length,
mapped,
unmapped,
ambiguousNameCountryKeys: ambiguous,
matchedByReorder: reordered,
countriesWithTwoOrFewer: thin,
},
rows,
}, null, 1)}\n`,
(j) => (j.rows || []).length,
'Doxa people group(s)',
)
console.log(`DOXA-PG: ${rows.length} group(s) across ${Object.keys(byCountry).length} countries`)
console.log(` placed on the map: ${mapped} no coordinates yet: ${unmapped} ambiguous, refused: ${ambiguous}`)
console.log(` of those placed, ${reordered} matched on the same words in the other order`)
console.log(` countries holding 2 or fewer Doxa groups: ${thin} — a percentage over that denominator is not a measurement`)
console.log(`wrote ${OUT}`)

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" 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

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" 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

@ -596,7 +596,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD",
"integrity": "sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA",
"elements": [
"gospel-resources-map"
],
@ -616,7 +616,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -630,7 +630,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -644,7 +644,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -658,7 +658,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -672,7 +672,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]