Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run

This commit is contained in:
doxa-maps-88655c70 2026-09-24 17:00:25 +00:00
commit 0f9ca8af20
20 changed files with 412 additions and 88 deletions

View file

@ -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 || '/'

View file

@ -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.')

View file

@ -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 },

View file

@ -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.",

View file

@ -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()

View file

@ -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([])

View file

@ -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.

View file

@ -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
View 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.

Binary file not shown.

Binary file not shown.

View 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.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -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>"
}
}
]

View file

@ -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:

File diff suppressed because one or more lines are too long

View file

@ -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:

View file

@ -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: