This commit is contained in:
commit
0f9ca8af20
20 changed files with 412 additions and 88 deletions
|
|
@ -51,6 +51,24 @@ import { chromium } from 'playwright'
|
|||
// Defaults to the DEV server, because this gate drives a live interaction and the dev surface is
|
||||
// the one that always reflects current source. Point BASE_URL/PROBE_PATH at the built tree
|
||||
// (`bun run serve` on 4173) to run it against a built leaf or the cap-5 shell.
|
||||
//
|
||||
// ⚠ THE DEV DEFAULT IS FLAKY UNDER LOAD, AND A FLAKY GATE IS WORSE THAN A FAILING ONE — it
|
||||
// teaches you to re-run until green. MEASURED 2026-09-24 on a box with several agents working:
|
||||
// dev 5173 -> 1 PASS, 3 FAIL in four consecutive runs ("the map never populated after 90s")
|
||||
// built 4173 -> PASS, PASS
|
||||
// The product is fine; the VARIANCE is the dev server's. A dev page load pulls 88 unbundled
|
||||
// module requests against the built leaf's 3, and Vite compiles them on demand — so the wait
|
||||
// this gate already retries THREE TIMES at 90s can still lose. Raising the timeout is the wrong
|
||||
// fix for the same reason: it is not one slow moment, it is contention.
|
||||
//
|
||||
// SO: when you need a trustworthy answer rather than a quick one, run it against the BUILT tree:
|
||||
// BASE_URL=http://127.0.0.1:4173 \
|
||||
// PROBE_PATH=/doxa-maps/research-dashboard/research-dashboard/index.html \
|
||||
// node internal/check-facet-changes-map.mjs
|
||||
// The built tree can be stale — that is the trade — but it is DETERMINISTIC, which is what a gate
|
||||
// needs. The dev default is kept because it is right for hand-running against uncommitted source.
|
||||
// This is also why the gate is NOT wired into a wave: a wave step that is green 1 run in 4 would
|
||||
// train everyone to ignore the wave.
|
||||
const BASE = process.env.BASE_URL || 'http://127.0.0.1:5173'
|
||||
const PATH_ = process.env.PROBE_PATH || '/'
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,21 @@ const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps'
|
|||
|
||||
// The Driver's sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive,
|
||||
// whitespace-collapsed) so a row may carry its subtitle.
|
||||
// The Driver's four, asserted as a RELATIVE ORDER rather than as positions 1-4.
|
||||
//
|
||||
// ⚠ THIS USED TO DEMAND A FOUR-ROW RAIL, and it had been FAILING ever since the rail gained its
|
||||
// three-section structure (4c74bd7, 2026-09-23, "Restore three-section tab structure:
|
||||
// Dashboards / Prayer Mobilization / Gospel Research"). MEASURED 2026-09-24: the rail renders
|
||||
// SEVEN rows —
|
||||
// Big Picture · Simple Research · Regions · UPG-100 List · Prayer Progress ·
|
||||
// Cities with Internet · Gospel Resources
|
||||
// — and the Driver's four are ALL PRESENT, at 0/3/4/6, in their specified relative order. The
|
||||
// product honours the instruction; this gate was asserting a shape the instruction never named.
|
||||
// It quoted the Driver correctly and then over-read it: "(Big Picture, UPG-100, Prayer Progress,
|
||||
// Gospel Resources)" names an ORDER, not a row count, and nothing there forbids a row between.
|
||||
//
|
||||
// So: assert what was actually specified. Tab 1 must still be Big Picture — that IS positional,
|
||||
// it is the landing surface — and the other three must appear after it, in order.
|
||||
const EXPECTED = ['Big Picture', 'UPG-100', 'Prayer Progress', 'Gospel Resources']
|
||||
|
||||
const WALK = `(sel)=>{const o=[],s=new Set();const w=(r)=>{if(!r||s.has(r))return;s.add(r);try{o.push(...r.querySelectorAll(sel))}catch(e){};for(const e of r.querySelectorAll('*'))if(e.shadowRoot)w(e.shadowRoot)};w(document);return o}`
|
||||
|
|
@ -59,8 +74,14 @@ try {
|
|||
// last remaining directory URL in internal/.
|
||||
await page.goto(`${BASE}/doxa-maps-spa/doxa-maps-spa/index.html`, { waitUntil: 'load', timeout: 45000 })
|
||||
await page.waitForTimeout(8000)
|
||||
// ⚠ DEDUPE: '.vtl-tab' matches NESTED elements, so each rendered row comes back ~5 times —
|
||||
// measured 35 matches for 7 real rows. The old code took the first four of that list, which
|
||||
// were all fragments of rows 1 and 2, and then reported them as rows 1-4. Keep only the
|
||||
// OUTERMOST match per row (one that has no .vtl-tab ancestor), which is the row itself.
|
||||
rows = await page.evaluate(
|
||||
`(${WALK})('.vtl-tab').map(t => (t.textContent || '').trim().replace(/\\s+/g, ' '))`
|
||||
`(${WALK})('.vtl-tab')
|
||||
.filter(t => !t.parentElement || !t.parentElement.closest('.vtl-tab'))
|
||||
.map(t => (t.textContent || '').trim().replace(/\\s+/g, ' '))`
|
||||
)
|
||||
} catch (e) {
|
||||
err = e.message.slice(0, 160)
|
||||
|
|
@ -82,19 +103,34 @@ if (!rows.length) {
|
|||
}
|
||||
|
||||
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
||||
const actual = rows.slice(0, EXPECTED.length)
|
||||
const mismatches = EXPECTED.map((want, i) => {
|
||||
const got = actual[i] || '(missing)'
|
||||
return norm(got).startsWith(norm(want)) ? null : { i, want, got: got.slice(0, 52) }
|
||||
}).filter(Boolean)
|
||||
// Where each expected name lands in the RENDERED rail. -1 = absent.
|
||||
const at = EXPECTED.map((want) => rows.findIndex((r) => norm(r).startsWith(norm(want))))
|
||||
const fails = []
|
||||
|
||||
if (mismatches.length) {
|
||||
console.error(`SPA-TAB-ORDER: FAIL — ${mismatches.length} of ${EXPECTED.length} rail rows are out of order.\n`)
|
||||
EXPECTED.forEach((want, i) => {
|
||||
const got = (actual[i] || '(missing)').slice(0, 52)
|
||||
const ok = norm(got).startsWith(norm(want))
|
||||
console.error(` ${ok ? 'ok ' : 'FAIL'} ${i + 1}. want "${want}" got "${got}"`)
|
||||
})
|
||||
// 1. PRESENCE. A name the Driver specified must be in the rail at all.
|
||||
EXPECTED.forEach((want, i) => {
|
||||
if (at[i] === -1) fails.push(`"${want}" is not in the rail at all`)
|
||||
})
|
||||
// 2. TAB 1 IS POSITIONAL, and only tab 1. It is the landing surface — the first thing anyone
|
||||
// opening the SPA sees — which is a claim about position, not merely about order.
|
||||
if (at[0] > 0) {
|
||||
fails.push(`tab 1 is "${(rows[0] || '').slice(0, 40)}", not "${EXPECTED[0]}" — the SPA opens `
|
||||
+ 'somewhere other than the landing surface')
|
||||
}
|
||||
// 3. THE REST ARE RELATIVE. The instruction named an order, not a row count, so a row sitting
|
||||
// between two of them is not a violation — a row OUT OF ORDER is.
|
||||
for (let i = 1; i < EXPECTED.length; i++) {
|
||||
if (at[i] !== -1 && at[i - 1] !== -1 && at[i] < at[i - 1]) {
|
||||
fails.push(`"${EXPECTED[i]}" (row ${at[i] + 1}) comes before "${EXPECTED[i - 1]}" `
|
||||
+ `(row ${at[i - 1] + 1}) — the Driver's order is reversed here`)
|
||||
}
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`SPA-TAB-ORDER: FAIL — ${fails.length} problem(s) with the Driver's rail order.\n`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
console.error('\n Rendered rail:')
|
||||
rows.forEach((r, i) => console.error(` ${i + 1}. ${r.slice(0, 52)}`))
|
||||
console.error('\n Tab 1 is the LANDING SURFACE — the first thing anyone opening the product SPA')
|
||||
console.error(' sees, and the opening step of the onboarding ramp. A reordered rail produces no')
|
||||
console.error(' error and a SPA that still works; it just opens somewhere else.')
|
||||
|
|
@ -103,7 +139,13 @@ if (mismatches.length) {
|
|||
}
|
||||
|
||||
console.log(`SPA-TAB-ORDER: PASS — the rail opens in the specified order (${rows.length} rows total).`)
|
||||
EXPECTED.forEach((w, i) => console.log(` ${i + 1}. ${(actual[i] || '').slice(0, 56)}`))
|
||||
// Print the WHOLE rendered rail and mark the Driver's four, so a reader can see the rows that
|
||||
// sit between them rather than only the names being asserted. A gate that prints just its own
|
||||
// four would hide the very structure that made the old positional assertion wrong.
|
||||
rows.forEach((r, i) => {
|
||||
const k = at.indexOf(i)
|
||||
console.log(` ${String(i + 1).padStart(2)}. ${k >= 0 ? '★' : ' '} ${r.slice(0, 54)}`)
|
||||
})
|
||||
console.log(' Asserted on the RENDERED rail rather than on NAV_MODEL: the array is edited for')
|
||||
console.log(' reasons unrelated to ordering (badges, sections, narrow-screen flags), and a source')
|
||||
console.log(' parse can pass while a rendering bug shows something else.')
|
||||
|
|
|
|||
|
|
@ -81,6 +81,16 @@ export const BEBAS_NEUE_FACES = [
|
|||
// Google's own unicode-ranges (css2 v24) so the subset split behaves as it does on their site.
|
||||
const POPPINS_LATIN = 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
|
||||
const POPPINS_LATIN_EXT = 'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF'
|
||||
// FIGTREE — the free stand-in for Brandon Grotesque, which is COMMERCIAL and must not ship in a
|
||||
// public giveaway (Driver decision 2026-09-24). Chosen by measurement: at 500/19.5312px it renders
|
||||
// their button text at 126px against Brandon's 128px on live doxa.life, the closest of 19 libre
|
||||
// grotesques; Inter was +6 and Archivo +7. Weight 500 only — that is the single weight the two
|
||||
// rules that named Brandon actually use.
|
||||
export const FIGTREE_FACES = [
|
||||
{ weight: '500', src: 'fonts/figtree/figtree-500-latin.woff2', unicodeRange: POPPINS_LATIN },
|
||||
{ weight: '500', src: 'fonts/figtree/figtree-500-latin-ext.woff2', unicodeRange: POPPINS_LATIN_EXT },
|
||||
]
|
||||
|
||||
export const POPPINS_FACES = [
|
||||
{ weight: '400', src: 'fonts/poppins/poppins-400-latin.woff2', unicodeRange: POPPINS_LATIN },
|
||||
{ weight: '400', src: 'fonts/poppins/poppins-400-latin-ext.woff2', unicodeRange: POPPINS_LATIN_EXT },
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
"check:facet-one-field": "node internal/check-facet-one-field.mjs # ONE AXIS = ONE CONTROL-REACHABLE FACET. Asserts on the PARTITION each facet induces over the rows, NOT on which r.<field> it reads: three fields (religion, primary_language, rop1) are legitimately read by two facets each as tier-1/tier-2 pairs, so a gate on duplicate field READS would fail correct code. Scoped to facets a user can actually select (rail + dropdowns + charts), because a search-only projection sharing a partition is fine until it becomes a control. run check:facet-one-field:self-test for the NEGATIVE test.",
|
||||
"check:facet-one-field:self-test": "node internal/check-facet-one-field.mjs --self-test",
|
||||
"check:facet-changes-map": "node internal/check-facet-changes-map.mjs # a facet selection must CHANGE THE DOTS — and the dots must SAY what they mean. Asserts (1) the rd-bubbles feature set changes by count AND identity, (2) every surviving dot satisfies the selected level, (3) while a prayer filter is active circle-color is a DATA-DRIVEN EXPRESSION reading peoplePraying, not a flat literal. Assertion 3 is the one that matters: the prayer filter ALREADY narrowed 2107->689 correctly while the Driver reported it broken, because every dot painted the same orange. Browser gate (needs `bun run dev` on 5173, or BASE_URL/PROBE_PATH at a served tree), so NOT in build:unlocked. run the `check:facet-changes-map:self-test` script for the NEGATIVE test.",
|
||||
"check:facet-changes-map:built": "sh -c 'BASE_URL=${BASE_URL:-http://127.0.0.1:4173} PROBE_PATH=${PROBE_PATH:-/doxa-maps/research-dashboard/research-dashboard/index.html} exec node internal/check-facet-changes-map.mjs' # the SAME gate against the BUILT tree instead of the dev server. Measured 2026-09-24: the dev default is 1 PASS / 3 FAIL in four runs on a busy box (88 unbundled module requests vs the built leaf's 3), while the built tree passes deterministically. Use this when you need a trustworthy answer; use the dev default when checking uncommitted source.",
|
||||
"check:facet-changes-map:self-test": "node internal/check-facet-changes-map.mjs --self-test # reverts the fix IN THE LIVE PAGE and requires the gate to FAIL. SELF_TEST_MODE=colour (default, isolates the colour assertion — the reported bug) | props | both | frozen (blocks the source update, isolating the set assertion). All four modes verified caught.",
|
||||
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs # the HEARTBEAT ZONE threshold (the choropleth's top bin) must come from config AND the shipped config must still render today's ramp. Two sides: (B) a configured threshold must MOVE the edge — catches re-hard-coding; (A2) dashboard.config.js's own map.* must still resolve to 5/20/60/150/Infinity — catches the shipped default silently changing. A2 was MISSING at first and heartbeatThreshold:999 shipped past this gate printing 'default ramp unchanged'. A grep for '150' cannot do either job: it false-positives on the legitimate default and false-negatives on a re-hard-coding at 200. run check:heartbeat:self-test for the NEGATIVE tests (3 cases).",
|
||||
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test # THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
|
||||
|
|
@ -144,7 +145,7 @@
|
|||
"check:synced-css": "node internal/check-synced-css-copies.mjs",
|
||||
"check:composite-reg": "node internal/check-composite-registration.mjs",
|
||||
"check:filter-width": "node internal/check-filter-width-stable.mjs",
|
||||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
|
||||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs # the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||||
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
|
||||
"check:mjs-parse": "node internal/check-mjs-parses.mjs",
|
||||
"check:snapshot-guard": "node internal/check-snapshot-guard.mjs # every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
|||
// house pattern (doxa-simple-map.vue:18, :246, :1346) — an EMBEDDED element must not reach for
|
||||
// document.documentElement.dir, which belongs to the host page.
|
||||
import { RTL_LOCALES } from '@map/i18n/index.js'
|
||||
import { useDocumentFont, BEBAS_NEUE_FACES, POPPINS_FACES } from '@map/composables/useDocumentFont.js'
|
||||
import { useDocumentFont, BEBAS_NEUE_FACES, POPPINS_FACES, FIGTREE_FACES } from '@map/composables/useDocumentFont.js'
|
||||
import FilterDropdown from '@map/components/search/FilterDropdown.vue'
|
||||
import UupgsList from './src/UupgsList.vue'
|
||||
// Their strings, via their own English keys (see src/useRegionsI18n.js). The goal sentences,
|
||||
|
|
@ -97,6 +97,10 @@ useDocumentFont('Bebas Neue', BEBAS_NEUE_FACES, assetsBase.value)
|
|||
// computed-style diff, which read 68/68 while our body text was a system fallback 5.5%
|
||||
// wider per glyph. Registered the same way and for the same reason as Bebas.
|
||||
useDocumentFont('Poppins', POPPINS_FACES, assetsBase.value)
|
||||
// Figtree stands in for Brandon Grotesque, which is commercial and must not ship in a public
|
||||
// giveaway (Driver, 2026-09-24). @font-face is DOCUMENT-scoped, so it registers here beside
|
||||
// the other two rather than inside the shadow root that uses it.
|
||||
useDocumentFont('Figtree', FIGTREE_FACES, assetsBase.value)
|
||||
|
||||
// ── state ───────────────────────────────────────────────────────────────────
|
||||
const { t, tf , locale } = useRegionsI18n()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import { useRegionsI18n } from './src/useRegionsI18n.js'
|
|||
// document.documentElement.dir, which belongs to the host page.
|
||||
import { RTL_LOCALES } from '@map/i18n/index.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDocumentFont, BEBAS_NEUE_FACES, POPPINS_FACES } from '@map/composables/useDocumentFont.js'
|
||||
import { useDocumentFont, BEBAS_NEUE_FACES, POPPINS_FACES, FIGTREE_FACES } from '@map/composables/useDocumentFont.js'
|
||||
import { summarizeRegions, PEOPLE_GROUP_LIST_FIELDS } from '@map/utils/countries-meta.js'
|
||||
// DX-life's spacing/type scales under THEIR token names. Prepended to this
|
||||
// component's shadow CSS so every var() below resolves inside the shadow root.
|
||||
|
|
@ -119,6 +119,10 @@ useDocumentFont('Bebas Neue', BEBAS_NEUE_FACES, assetsBase.value)
|
|||
// computed-style diff, which read 68/68 while our body text was a system fallback 5.5%
|
||||
// wider per glyph. Registered the same way and for the same reason as Bebas.
|
||||
useDocumentFont('Poppins', POPPINS_FACES, assetsBase.value)
|
||||
// Figtree stands in for Brandon Grotesque, which is commercial and must not ship in a public
|
||||
// giveaway (Driver, 2026-09-24). @font-face is DOCUMENT-scoped, so it registers here beside
|
||||
// the other two rather than inside the shadow root that uses it.
|
||||
useDocumentFont('Figtree', FIGTREE_FACES, assetsBase.value)
|
||||
|
||||
// ── state ───────────────────────────────────────────────────────────────────
|
||||
const regions = ref([])
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
|||
// The region palette comes from the shared strategy, never from a table in here:
|
||||
// check:one-palette exists because a private palette in a consumer is what once made two
|
||||
// surfaces disagree about what a colour meant.
|
||||
import { applyColor as regionColorExpression } from '@map/colors/wagf-region.js'
|
||||
import { applyColor as regionColorExpression, PALETTE as REGION_PALETTE, getColor as regionColorFor } from '@map/colors/wagf-region.js'
|
||||
|
||||
|
||||
const emit = defineEmits(['pick-country'])
|
||||
|
|
@ -129,6 +129,34 @@ const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
|||
showEdgeBorders: true,
|
||||
})
|
||||
|
||||
// THE REGION COLOURS, CARRIED ONTO THE LANDMASSES (Driver card, 2026-09-24).
|
||||
// The pins already paint by region via the shared expression (regionColorExpression, below at the
|
||||
// pin layer). The polygons cannot use that expression directly: it matches on ['get','region'],
|
||||
// which is a property of OUR geojson, while the boundary tileset's features carry
|
||||
// `iso_3166_1_alpha_3` and know nothing about WAGF regions. So this builds the bridge — country
|
||||
// code -> that country's region -> THE SAME PALETTE. No second palette, which is exactly what
|
||||
// check:one-palette exists to prevent.
|
||||
// The library's fallback, taken from the library itself: getColor({}) resolves an unknown region
|
||||
// to DEFAULT_COLOR. That constant is not exported, and adding an export to a file every bundle
|
||||
// imports is not worth one colour (RULE 2).
|
||||
const REGION_DEFAULT = regionColorFor({})
|
||||
const regionFillExpression = computed(() => {
|
||||
const byCode = new Map()
|
||||
for (const p of props.points) {
|
||||
if (!p || !p.code || !p.region) continue
|
||||
if (!byCode.has(p.code)) byCode.set(p.code, p.region)
|
||||
}
|
||||
if (!byCode.size) return REGION_DEFAULT
|
||||
const match = ['match', ['get', 'iso_3166_1_alpha_3']]
|
||||
for (const [code, region] of byCode) match.push(code, REGION_PALETTE[region] || REGION_DEFAULT)
|
||||
match.push(REGION_DEFAULT)
|
||||
return match
|
||||
})
|
||||
// Their fill is 0.12 — near-invisible, which is right for a single highlighted country on a
|
||||
// CLONED page. The chooser is ours and its whole job is to read AS seven regions at a glance, so
|
||||
// it carries more. The cloned pages keep 0.12 exactly.
|
||||
const FILL_OPACITY_REGION = 0.42
|
||||
|
||||
const SRC_PINS = 'rcm-people-groups'
|
||||
const SRC_COUNTRIES = 'rcm-country-boundaries'
|
||||
|
||||
|
|
@ -225,7 +253,10 @@ function addLayers() {
|
|||
m.addLayer({
|
||||
id: 'rcm-focus-fill', type: 'fill', source: SRC_COUNTRIES,
|
||||
...srcLayer, filter: countryFilter(),
|
||||
paint: { 'fill-color': C.brand, 'fill-opacity': 0.12 },
|
||||
paint: {
|
||||
'fill-color': props.colorMode === 'region' ? regionFillExpression.value : C.brand,
|
||||
'fill-opacity': props.colorMode === 'region' ? FILL_OPACITY_REGION : 0.12,
|
||||
},
|
||||
})
|
||||
m.addLayer({
|
||||
id: 'rcm-focus-outline', type: 'line', source: SRC_COUNTRIES,
|
||||
|
|
@ -321,6 +352,13 @@ function applySubject() {
|
|||
for (const id of ['rcm-focus-fill', 'rcm-focus-outline']) {
|
||||
if (m.getLayer(id)) m.setFilter(id, countryFilter())
|
||||
}
|
||||
// The fill follows the mode too, not just the filter: switching between the chooser and a
|
||||
// cloned page must repaint the landmasses, or the region colours would leak onto their pages.
|
||||
if (m.getLayer('rcm-focus-fill')) {
|
||||
const byRegion = props.colorMode === 'region'
|
||||
m.setPaintProperty('rcm-focus-fill', 'fill-color', byRegion ? regionFillExpression.value : C.brand)
|
||||
m.setPaintProperty('rcm-focus-fill', 'fill-opacity', byRegion ? FILL_OPACITY_REGION : 0.12)
|
||||
}
|
||||
// PROJECTION follows the mode, and only the chooser changes it.
|
||||
// Mapbox v3 defaults to the GLOBE below about zoom 6, so a world-view chooser rendered as a
|
||||
// small sphere floating in a 1200x780 frame — most of the surface empty, the regions tiny.
|
||||
|
|
|
|||
|
|
@ -407,7 +407,14 @@ useShadowStyles(
|
|||
' box-sizing:border-box; padding:10px var(--spacing-2xl); color:#f3f3f1; background:#92b195;' +
|
||||
' border-radius:100px; border:1px solid transparent; line-height:1; text-decoration:none;' +
|
||||
' text-transform:uppercase; font-size:var(--font-size-md);' +
|
||||
' font-family:"Brandon Grotesque",Poppins,-apple-system,"Segoe UI",sans-serif;' +
|
||||
// THEIR FAMILY IS "Brandon Grotesque", WHICH IS COMMERCIAL — the one substitution RULE 1
|
||||
// allows, and the Driver ruled it must not ship in a public giveaway (2026-09-24).
|
||||
// Figtree chosen by MEASUREMENT, not by name: on live doxa.life their button renders
|
||||
// 'FULL PROFILE' at 128.00px in Brandon at 500/19.5312px. Of 19 libre grotesques measured
|
||||
// the same way, Figtree is 126 (-2.00) — closest — against Inter 134, Archivo 135,
|
||||
// Poppins 119, Montserrat 148. (The 134.33 in the old scorecard is the GENERIC SANS
|
||||
// fallback, not Brandon; their page really does load Brandon_Grotesque_medium.woff2.)
|
||||
' font-family:Figtree,Poppins,-apple-system,"Segoe UI",sans-serif;' +
|
||||
' transition:background-color .3s ease, color .3s ease; cursor:pointer; }' +
|
||||
'.uupg__more-button:hover{ background:#2a332a; color:#f3f3f1; }' +
|
||||
// their 576px reflow: the photo and the header stop sharing a row
|
||||
|
|
@ -420,7 +427,7 @@ useShadowStyles(
|
|||
'.ul-more{ margin-inline:auto; margin-top:var(--spacing-2xl); display:block; width:fit-content;' +
|
||||
' padding:10px var(--spacing-2xl); color:#f3f3f1; background:#92b195; border-radius:100px;' +
|
||||
' border:1px solid transparent; line-height:1; text-transform:uppercase; font-size:var(--font-size-md);' +
|
||||
' font-family:"Brandon Grotesque",Poppins,-apple-system,"Segoe UI",sans-serif; cursor:pointer; }' +
|
||||
' font-family:Figtree,Poppins,-apple-system,"Segoe UI",sans-serif; cursor:pointer; }' +
|
||||
'.ul-more:hover{ background:#2a332a; }',
|
||||
'uupgs-list'
|
||||
)
|
||||
|
|
|
|||
100
public/assets/fonts/figtree/LICENSE
vendored
Normal file
100
public/assets/fonts/figtree/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
Figtree — SIL Open Font License, Version 1.1
|
||||
Designer: Erik D. Kennedy. Distributed via Google Fonts.
|
||||
Vendored as the free stand-in for Brandon Grotesque (commercial), by Driver decision
|
||||
2026-09-24. Chosen by MEASUREMENT, not by name: at 500/19.5312px Figtree renders
|
||||
'FULL PROFILE' at 126px against Brandon's 128px on live doxa.life — the closest of 19
|
||||
libre grotesques tested. See _probe-glyph-width.mjs.
|
||||
|
||||
Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/assets/fonts/figtree/figtree-500-latin-ext.woff2
vendored
Normal file
BIN
public/assets/fonts/figtree/figtree-500-latin-ext.woff2
vendored
Normal file
Binary file not shown.
BIN
public/assets/fonts/figtree/figtree-500-latin.woff2
vendored
Normal file
BIN
public/assets/fonts/figtree/figtree-500-latin.woff2
vendored
Normal file
Binary file not shown.
100
public/doxa-maps/assets/fonts/figtree/LICENSE
generated
Normal file
100
public/doxa-maps/assets/fonts/figtree/LICENSE
generated
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
Figtree — SIL Open Font License, Version 1.1
|
||||
Designer: Erik D. Kennedy. Distributed via Google Fonts.
|
||||
Vendored as the free stand-in for Brandon Grotesque (commercial), by Driver decision
|
||||
2026-09-24. Chosen by MEASUREMENT, not by name: at 500/19.5312px Figtree renders
|
||||
'FULL PROFILE' at 126px against Brandon's 128px on live doxa.life — the closest of 19
|
||||
libre grotesques tested. See _probe-glyph-width.mjs.
|
||||
|
||||
Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/doxa-maps/assets/fonts/figtree/figtree-500-latin-ext.woff2
generated
Normal file
BIN
public/doxa-maps/assets/fonts/figtree/figtree-500-latin-ext.woff2
generated
Normal file
Binary file not shown.
BIN
public/doxa-maps/assets/fonts/figtree/figtree-500-latin.woff2
generated
Normal file
BIN
public/doxa-maps/assets/fonts/figtree/figtree-500-latin.woff2
generated
Normal file
Binary file not shown.
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -874,7 +874,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3",
|
||||
"integrity": "sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -894,7 +894,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -908,7 +908,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -922,7 +922,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
118
public/doxa-maps/regions-dashboard/regions-dashboard.js
generated
118
public/doxa-maps/regions-dashboard/regions-dashboard.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-V6UcPxx9/C30jhjP3qxO1v/Ib4nMhu+uPJL0dd0geMrA236NuP9HZchD58RT3DL3" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-jj4JBxH2lbWxtqsuDMdk7yN2IiLQfLFmwLVj8nPk33JRqqSaaz3XOjz3dDxAG+3c" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue