Commit graph

744 commits

Author SHA1 Message Date
Doxa Maps
9dc9ed7643 Gospel access by province, on the tables page too
The province data was readable only inside the map's own sheet. It is a tab now:
44 provinces of India and China, the same file the map reads, so the two cannot
disagree — and unlike the sheet it sorts and exports.

Every row still says placement is approximate. A people group is assigned to the
province its map point falls in, and one point stands for a people who may live
across several.
2026-09-26 07:47:58 +00:00
Doxa Maps
a44a3a41a1 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 07:47:05 +00:00
Doxa Maps
27bc18a6e1 Name the translator, and let a mobiliser write to them
The credit line said who translated the Scripture and called them "the likeliest
people to ask", then stopped at a name. 838 of the 929 credited groups now reach
that organisation's own homepage from the sheet.

The URL does not come from eBible's homeDomain field, which is still dropped: it
comes from a hand-screened list of 16 organisations, every one fetched on the run
that wrote the table. Where a credit names two, the one it LEADS with wins, so
"Pioneer Bible Translators and The Word for the World International" reaches
Pioneer and not the other way round.

National Bible Societies hold 89+ credits and are deliberately linked to none of
them: several UBS members publish editions carrying the deuterocanonicals, so
whether a Society contact sits inside the 66-book filter is a ruling to be made,
not one to assume. They keep the name without the link, and the count says so.

A contact is screened as somewhere to write to, not as a publisher this map reads
text from — two separate lists, so wycliffe.org appearing as a scripture link is
still a failure. Four organisations answered a datacentre IP with a bot challenge
while serving browsers normally; those are proved by robots.txt and the table
records which method proved which.
2026-09-26 07:43:58 +00:00
Doxa Maps
06209dc45e One pinned-UMD loader for PPLR and the Wall: library/utils/loadPinnedUmd.js
pplr-data-maps and the Wall each carried the same ~40-line d3 loader (pinned, SRI, and a trap that keeps
the UMD from writing into a host page's own window.d3). It is now one library helper, fed from the
CDN registry. check:pinned-umd (headless, gates:post): the tag carries the pinned integrity, a host's d3
gains no keys, and a failed load rejects leaving the host's d3 in place. Both bundles build.
2026-09-26 07:24:25 +00:00
Doxa Maps
932bf553c7 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-26 07:21:13 +00:00
Doxa Maps
b39774daac The filters-toggle padding used an undefined token, so it voided and made the gap worse
rega falsified my own fix. The button went 25.44 -> 17.63 against their 37.63 --
FURTHER from theirs than before I touched it.

I wrote padding:var(--spacing-xs) var(--spacing-2xl). --spacing-xs DOES NOT EXIST,
and dxlife-tokens.js:43-45 says so on purpose: "--spacing-xs (10px) and --spacing-sm
(13.3px) are deliberately NOT tokenised: '10px' is far too common a literal in this
CSS to substitute safely, and a wrong substitution here is worse than a magic number
because it reads as deliberate." I substituted anyway, into the one file whose
comment forbids it.

An undefined var() invalidates the WHOLE declaration at compute time, so the padding
fell to 0 on BOTH axes -- not just the vertical I was aiming at. Everything else on
that line landed: border 1px, radius 100px, line-height 15.625. Nothing errored, the
build passed, check:sri passed, and the failure was invisible from here.

Now the literal: padding:10px var(--spacing-2xl). Theirs computes "10px 41.6158px",
so 15.625 + 2x10 + 2x1 = 37.63 tall and ~210.7 wide against their 210.73.

GUARDED so this class cannot recur silently: the spacing check now resolves every
var(--spacing|--font-size|--line-height) reference against dxlife-tokens.js and
fails on any that is undefined, with a NEG control asserting --spacing-xs really is
absent so the check has something real to catch. rega found this by grepping every
var() by hand; that is now automatic. Currently 19 tokens defined, 0 undefined
references.

THE OVER-REACH WAS REAL AND rega QUANTIFIED IT: because the button shrank 7.81px,
all 121 leaves below it on ethiopia moved UP by the same amount at both 1440 and 768
-- the Total count and every UUPG card's name, country, tags and Full Profile. With
the padding resolving, the list sits 12.19px LOWER than before, which is correct
because it now matches theirs.

Confirmed in the same run, unchanged: all four stats at 47.6837px, identical to
theirs, with the only knock-on being the Population label moving down 9.55px as its
value grew -- 93 of 95 africa leaves untouched.
2026-09-26 07:12:07 +00:00
Doxa Maps
0d8d1b3302 Gospel map: provinces for India and China, fetched only when someone looks
P1 rendering. Opening India or China now draws its provinces, coloured by the
same ladder as the country beneath them, and tapping one gives its groups, its
people, how many have nothing, the largest of those and the way in.

It arrives on intent rather than on load. India's ADM1 geometry is 1,628 KB —
nearly the size of the whole world ADM0 file — on a leaf that already loads about
4 MB and is meant to be embedded on thousands of partner sites. Loading it for
the 173 countries nobody is looking at would tax every reader for the benefit of
two. Opening one of the two IS the signal; China is 54 KB, and both are cached
after the first open.

The counts come from the measured table, never recomputed in the leaf. The
point-in-polygon belongs in the build, and a second implementation here is how
two surfaces come to disagree. Provinces repaint when the mode changes, because a
country saying one thing while the province on top of it says another is worse
than having no provinces at all.

The panel repeats that placement is approximate, and that the way in was chosen
for the COUNTRY — which is exactly the limitation going below the border exposed.

Also fixes the gate I added for this, twice. The province fetch interpolates its
filename, so the parse captured the literal prefix "boundaries/" — a real
directory of 4,096 bytes that always exists, proving nothing. The first fix
expanded that prefix against the filesystem, which was worse: unstaging a real
geojson simply shortened the list and the gate stayed green on nine assets
instead of ten, exit 0. The expectation now comes from the filenames the leaf
names, so a file the map asks for and the tree lacks fails. Poison-tested both
ways.
2026-09-26 06:53:22 +00:00
Doxa Maps
eb07fe9e3a Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-26 06:48:05 +00:00
Doxa Maps
d0b4ab70e7 The filters toggle is .button.compact.link, and we were missing four of its declarations
rega measured it on /regions/ethiopia: ours 25.44px tall against their 37.63.

Their height reconciles exactly once the right rules are in. _buttons.scss gives
.button { border: 1px solid var(--button-border-color, transparent); line-height:
var(--line-height-none); border-radius: 100px }, .button.compact { padding:
var(--spacing-xs) var(--spacing-2xl) }, and .button.link sets --button-border-color
to transparent:
    line-height 15.625 + 2x10 padding + 2x1 border = 37.63
Ours declared NO padding, so the UA's 1px showed, plus border:0, and font:inherit
pulled the base line-height 1.5 through as 23.44. All four declarations are
portable, so all four are ported and the geometry now matches.

THE ONE SUBSTITUTION, documented rather than silently skipped: their .button also
sets font-family: var(--font-family-buttons), which resolves to Brandon Grotesque —
commercial, and blocked on the Driver. So this control keeps our stand-in face and
the family stays the single measured difference. That is the RULE 1 shape: port
everything portable, document the one thing that cannot be, with the reason.

Both of this pair came from rega measuring text leaves I had not thought to compare,
then re-measuring every hit against the PREVIOUS tree so "already wrong" could be
told apart from "my regression". Neither would have surfaced from my side: the stat
shrink looked like a deliberate rule with a comment justifying it, and this button
looked like a plain button because ours had made it one.
2026-09-26 06:47:42 +00:00
Doxa Maps
0a30c93506 Delete the stat-value shrink: its premise went stale when Bebas was vendored
rega measured it on main 71858d71 against live: our "43,045,275" rendered at
38.15px where theirs is 47.68px, on a card of the same width, while the other three
stats matched exactly.

The cause was ours — .rd-stat__value[data-len="long"|"xlong"], stepping the figure
down a rung. Their .region-stat__value is font-family-heading + font-size-3xl +
line-height:1 and nothing else; no such attribute exists in their markup.

ITS OWN COMMENT CONTAINED BOTH REASONS TO DELETE IT.
 · "our condensed fallback is wider" than Bebas — STALE. Bebas is vendored now
   (public/dx-maps/assets/fonts/bebas-neue, and the glyph oracle reads 279 = 279
   SAME), so there is no wider fallback left to compensate for. useDocumentFont.js
   records that an uppercase hack and "two font-sizes" were knock-ons of not having
   the real face; this rule is the third, missed when the others were fixed.
 · "every value DX-life itself renders keeps their size" — FALSIFIED. "43,045,275"
   is ten characters, so it hit the 'long' bucket, and DX-life renders exactly that
   number at full size on /regions/africa. A rule whose own invariant is violated in
   practice is not protecting anything.

Deleted with its mechanism, not just its symptom: both rules, the :data-len binding,
and lenBucket() itself. A dead helper left behind is the next reader's puzzle.

Found because rega re-measured every over-reach hit against the PREVIOUS tree, which
separated "my line-height change did this" from "this was already wrong". Without
that control the finding would have looked like my regression, and I would have
spent the tick defending the wrong thing.
2026-09-26 06:44:30 +00:00
Doxa Maps
ec3c335d0a Rebuild after merge
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-26 06:43:55 +00:00
Doxa Maps
3345779bdb upg-100's first click: onLegendSelect was bailing on an unready layer, and the request was thrown away
rega's re-shoot on main 27eed341 answered it in one run, because the apply now
reports its own verdict. Timeline, ms from navigation:
    t=2958  click 'Show Northwest India on the map'
    t=3015  select-zone = "asia-india-rajasthan" written on the map pane
    t=4887  data-select-zone-state = "applied-but-no-selection"
    t=4900  the same pair AGAIN, 13 ms later, still no selection
    t=5308  the doxa:select-zone listener first appears
So delivery was fine, the id was right, the node WAS found, onLegendSelect WAS
called — and the selection did not hold, twice.

THE CAUSE is onLegendSelect's own first line:
    if (!map || !map.getLayer('tapestry-fill')) return
It returns silently when the map's tapestry layer is not up yet, so selectedKey is
never set. At t=4.9s that layer did not exist. Both applies came from
watch([legendNodes, mapRef]), which is why they landed before the listener did.

So the readiness signal was never legendNodes and never mapRef. It is THE LAYER,
and I had guessed at the other two in turn. This is the THIRD layer of one defect —
apply once against unready state and discard — and each layer hid the next because
every failure was silent.

Two changes, both from the measurement rather than a theory:
1. applySelectZone RETAINS the request when the effect did not take. It used to
   clear _pendingZoneId right after calling onLegendSelect, treating "called" as
   "done"; now a state of applied-but-no-selection keeps the id outstanding.
2. ensureLayers() drains it. That function adds the tapestry source and both
   layers, so it is the exact moment onLegendSelect can hold a selection.

Coverage is complete either way round: if ensureLayers runs first the layer already
exists and the ordinary apply succeeds; if it runs later, the retained request
drains. A click after everything is up still goes through the attribute observer.

Branch was RESET onto main 27eed341 and carries this one commit only — the handoff
lesson from last time: a merged branch's tree holds my stale copies of everyone
else's files, so replaying it (the sane thing for a recipient) reverts their work.
Source only; my-upg-100-list needs a rebuild.
2026-09-26 05:50:25 +00:00
Doxa Maps
af436bff6f check:dashboard-geocoder-provider: a leaf with no search bar fails instead of passing hollow
It measured the research-dashboard leaf, which mounts no search bar, so every run reported 'search bars: 0'
and passed without ever exercising search. It now defaults to doxa-research-map/research-map (which mounts
the library GeocoderComponent), takes --leaf, and fails when no bar renders.
2026-09-26 05:50:24 +00:00
Doxa Maps
4a12b009aa Every runtime CDN script and stylesheet is an exact version with an integrity hash
maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).

- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
  @5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
  Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
  static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
  --online re-fetches each and compares the hash (all 9 match today).

MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
2026-09-26 05:50:24 +00:00
Doxa Maps
71858d713c Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-26 05:42:29 +00:00
Doxa Maps
97d0ef264a Clamp the goal card's big number as theirs does; the nowrap worry was backwards
utilities/_typography.scss gives .font-size-4xl a clamp:
    font-size: clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))
and ours was a flat var(--font-size-4xl). Identical at 1024 and above, where both
sit on the ceiling, but below that ours never shrank. Their resolved values against
ours:
    390px    theirs 38.15   ours 59.60   ours 21.46 too large
    768px    theirs 46.08   ours 59.60   ours 13.52 too large
   1024px+   theirs 59.60   ours 59.60   identical

★ I REPORTED THIS AS UNCHANGEABLE ONE COMMIT AGO, AND THAT WAS BACKWARDS. I said it
interacted with the documented nowrap decision — a figure must never split across
lines in a 372px card, which is why the flat value was chosen. But the clamp makes
the figure SMALLER exactly where the wrap was observed: in a 372px container 6cqw is
22.3px, below the 2xl floor, so it resolves to 38.15px against the 59.60px that was
breaking. The clamp helps the very problem the flat value was picked to avoid. I had
assumed a conflict instead of computing it, and one line of arithmetic settled it.
The nowrap stays as a belt.

cqw rather than vw, consistent with every other clamp in this element (the
container-vs-viewport substitution the Driver is still ruling on). At 768 that lands
42.4 against their 46.08 — 3.68 off instead of 13.52.

_chk-clamp-floors-at-phone-and-tablet.mjs picks the new clamp up on its own: 14
clamps now, and 12/12 token-bounded ones still pin to their floor at 390, so the
"exact parity at phone width, provable without a container measurement" result now
covers the big number too.
2026-09-26 05:35:51 +00:00
Doxa Maps
fd12c7eea4 Port their base line-height (1.5); our 1.1 override was compensating for its absence
THE EIGHTH GLOBAL RULE OF THEIRS FOUND MISSING, and the same class as the
box-sizing reset: _globals.scss:14 is `body { line-height: var(--line-height-normal) }`
with $line-height-normal: 1.5, and a document-level rule does not cross a shadow
boundary. dxlife-tokens.js declared every spacing and font token and NO line-height
at all, so every text element in the clone inherited whatever the EMBEDDING PAGE
set, while theirs is deterministically 1.5.

.rd-big carried line-height:1.1 to compensate. At --font-size-4xl (59.6046px) that
is a 65.6px line box against their 89.4px — a 23.8px difference. rega measured the
consequence to the pixel: our goal cards were 24px shorter than theirs (571 vs 595),
their big number sitting ~12px lower and its label ~24px lower, the extra leading
splitting roughly half above and half below the glyphs. Theirs is
`<p class="font-size-4xl font-weight-medium">` and the .font-size-4xl utility sets
ONLY a font-size, so their number simply inherits body's 1.5.

So :host now carries their line-height and its tokens, and .rd-big's override is
gone. Their own tighter cases stay explicit and already agreed: .region-stat__value
is line-height:1 and so is our .rd-stat__value.

★ I APPLIED THIS HALF-WAY FIRST. The :host patch failed on a string mismatch while
the .rd-big patch succeeded, which left the number's line box inheriting from the
HOST PAGE — worse than the wrong-but-deterministic 1.1 it replaced. Caught because
the two patches reported separately instead of as one result.

CORRECTION TO 9009dd62, owed and measured against me: that commit claimed our goal
line "floated at three different heights" where theirs bottom-aligned. rega measured
both sides — the goal line sits 42px above the card's bottom edge on BOTH, in all
three cards, with identical gaps to the element above (0 / 73 / 120px). So margin:0
already reproduced their margin-top:auto at 1440, and the visible defect I described
did not exist. The change stands as a verbatim port of their declaration (RULE 1),
and it matters only where a card has free space — but I asserted a consequence I had
not measured, and the measurement says otherwise.

STILL DIFFERENT, reported not changed: their .font-size-4xl utility is
`clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))` where ours is a flat
var(--font-size-4xl). Identical at 1440, divergent below — at 768 theirs is 46.08px
and ours stays 59.6px. It interacts with the documented nowrap decision (a figure
must never split across lines in a 372px card), so it goes out for measurement
rather than into this commit.

Drawn verification is rega's: card heights 571 -> 595 and the two gaps around the
big number. A base line-height touches every text element in the element, and I
cannot measure layout headlessly.
2026-09-26 05:33:12 +00:00
Doxa Maps
6edf9c4271 upg-100 show-on-map apply: the settled wording of its outcome diagnostic (regions' re-resolved version) 2026-09-26 05:26:49 +00:00
Doxa Maps
a5b0f3f085 Make the show-on-map apply report its own outcome, so the next failure is visible
Two rounds of diagnosis went on guessing which step failed, and both guesses were
wrong in the same direction: I assumed delivery.

rega's timeline settled delivery. On main 031b9b38: all three elements exist at
t=893, the map pane's legend rows are populated at t=1017, the click lands at
t=4054, the select-zone attribute is written at t=4106, and the listener first
appears at t=4763. So the attribute IS written, and main ALREADY reads it at
registration (upg-100-list.vue:2140). A headless test then ruled out the lookup:
buildLegendNodes over the real clusters.json yields 37 keys and
findLegendNode('asia-india-rajasthan') resolves to "Northwest India".

So the attribute is read, the node IS found, onLegendSelect IS called -- and
nothing appears. The failure is DOWNSTREAM of the apply, and it is invisible:
nothing throws, nothing logs, and "selected but not visibly selected" is
indistinguishable at the rendered layer from "never selected". That is the third
time this session that a defect has hidden in that gap.

So the apply now writes its own outcome onto the host, where rega's existing 50 ms
poll can read it with no CDP and no new instrument:
    data-select-zone-state = applied | applied-but-no-selection | unresolved | cleared
    data-select-zone-applied = <the zone id>
'applied-but-no-selection' is the discriminating case: the path was reached and the
EFFECT was lost. It reads selectedKey, the component's own record of the isolation.

★ AND THE DIAGNOSTIC ITSELF NEARLY LIED. I first wrote selectedZoneKey, which does
not exist in this file; the try/catch around it would have swallowed the
ReferenceError and reported 'applied' every time. A diagnostic that cannot report
failure is worse than none -- the exact fault it was added to expose. Caught by
reading :163 for the real name (selectedKey) instead of trusting the one I
expected.

This does not claim to fix upg-100. It makes the next re-shoot decisive instead of
another guess.
2026-09-26 05:26:49 +00:00
Doxa Maps
db0fd4188a Show-on-map, second pass: stop trusting the bookkeeping, and make deselect mean something
rega re-shot the first click at main 031b9b38. country-posters HELD (India fits in
~1 s, bounds cover its bbox). upg-100 FAILED: the view switches, every list paints,
and 12 s later it is still the world view. Not a stale build — freshness PASS and
8 'select-zone' references in the shipped bundle, so the retained request ships and
does not reach the apply path.

WHY THE TWO DIVERGED, which is the useful part. Both got the same pattern, but they
drain differently. country-posters drains on isMapReady, which flips AFTER the
request arrives. upg-100 drained on legendNodes and only from the REMEMBERED id --
so it silently assumed an order: the attribute was read, or the observer fired, and
legendNodes changed afterwards. If legendNodes was already populated when the click
came, that watcher never fires again and the request sits in _pendingZoneId forever.
Nothing throws. It is the same shape as the bug being fixed, one level up: state
recorded correctly and then never applied.

So the drain no longer consults its own bookkeeping. It watches BOTH readiness
signals (legendNodes and mapRef) and on either one RE-READS THE ATTRIBUTE off the
host. The attribute is the durable record; re-reading it is what makes the outcome
independent of whether any single hook fired at the right moment, which was the
entire reason for choosing state over an event.

THIS HALF IS A HYPOTHESIS, NOT A PROVEN FIX, and I would rather say so than imply
otherwise: I can show the old drain COULD strand a request, not that it did. rega
has offered to instrument the timeline and that is the measurement that will settle
it — element <my-upg-100-list> in the map pane, attribute select-zone.

Also fixed, and this one rega proved: DESELECT DID NOTHING. country-posters'
applySelectCountry(null) cleared the flag and returned, so a second press left the
camera on India. PosterWall's button IS a toggle — it tracks lastShown and sends
listId: null on the second press — so a control you can turn on but not off reads
as broken even when the first press worked. It now flies back to the configured
opening view, the same center/zoom useMapInstance booted with.
2026-09-26 05:26:48 +00:00
Doxa Maps
9009dd62dc Port their goal line's margin-top:auto, which bottom-aligns it in the card
A real clone defect, found by reading their markup after rega's live spacing probe
landed NEAR it and measured the pair as EQUAL.

Theirs is RegionDetail.vue:24 `.region-goal { margin-top: auto }`, and the goal
paragraph is the last child of `<div class="stack stack--lg | card | text-center">`.
composition/_stack.scss makes .stack a flex COLUMN with
`> * + * { margin-top: var(--stack-spacing) }`, so that auto OVERRIDES the child
margin and pushes the goal line to the BOTTOM of the card. Ours was margin:0, which
left it sitting directly under the text above it.

The visible consequence is the whole point for a clone: the three goal cards hold
different amounts of copy, so theirs align their goal lines flush along the cards'
bottom edge while ours floated at three different heights. That is exactly the kind
of thing the Driver means by "proper css spacing" — nothing is off-scale, and it
still looks wrong.

WHY THE PROBE COULD NOT SEE IT, which is worth more than the fix. rega measured
.rd-goal against .region-goal as "same", and they were right: at 1440 the card has
no free space, so margin-top:auto resolves to 0 and the two computed values agree.
An equal number hid a real difference. Their probe was correct AND incomplete, and
only their markup settled it.

My own spacing check could not see it either, for a different reason: it verifies
every value we SHIP is on their scale and is structurally blind to a declaration we
OMIT. That is "a property diff cannot see an addition", inverted. It now carries
hand-written assertions for the two omissions found this way (this one and
.region-stat__label's margin-top), since there is no general way to enumerate what
is missing.

★ AND I APPENDED THOSE TWO ASSERTIONS BELOW process.exit() at first, so they were
dead code: they printed nothing while the check still reported success. A check you
can add assertions to without them running is the same defect as a check that
cannot fail. Moved above the exit.

Also corrected, for the record: two of the nine selector pairs I gave rega were
wrong. .rd-big/.rd-big-label are the GOAL CARD's number and label, not their
.region-stat__* pair — our real counterparts, .rd-stat__value and .rd-stat__label,
already matched theirs exactly. And .rd-switcher's 112.5px side margins are the
container rule on .rd-scroll > * (90%+16px capped at 1200, centred at 1440), not a
spacing difference; theirs lives on a separate .container ancestor. I built that
pair list from greps instead of from the provenance comments in the file, which
document the true counterparts.
2026-09-26 05:25:48 +00:00
Doxa Maps
e3d25ff6db The people-group list is a universal prop: the same maps can show any project's list
Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
  profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').

- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
  in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
  to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
  counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
  (71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
  The default list returns the same array, unfetched. A list whose membership cannot load shows no
  groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
  their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
  Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
  are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
  the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
  thread `list` to their children like `provider`.

Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.

Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
2026-09-26 05:16:02 +00:00
Doxa Maps
cd72dbad7d Wall of People Groups: loading d3 no longer writes into the host page's own d3
The Wall's pinned-d3 loader had the flaw rega found in PPLR's: d3's UMD build
writes its exports INTO an existing window.d3, so restoring the host's object
afterwards was too late. Ours had already been written into it. Same fix as
pplr-data-maps (60faff1a): while our script executes (document.currentScript
is our element), window.d3 is a slot of ours; any other code always sees the
host's own. A non-configurable `var d3` global is emptied for the load instead.

Headless, the real d3@7.9.0 (sha384 matches the pinned SRI) in a vm realm:
  before  host owns window.d3: keys 2 -> 579, version -> 7.9.0
  after   host owns window.d3: keys 2 -> 2, version host-7.0.0; the Wall gets its own d3 7.9.0
  after   no host d3: window.d3 absent afterwards, as before
rega re-ran PPLR's identical loader in a browser in all three variants, including a real
non-configurable `var d3`: PASS.
2026-09-26 05:10:25 +00:00
Doxa Maps
d3c0001fa0 Rebuild regions-dashboard after merging main 2026-09-26 05:06:05 +00:00
Doxa Maps
27c9848b59 The chooser's globe gets a square frame on mobile; the cloned pages keep theirs
The Driver settled the call this was parked on: "the only changes are to the map to
make it look better, and to add the all-regions map ... which needs to be
intuitive". This is the map half, on the sanctioned addition only.

Their .country-map flips to aspect-ratio 1/2 under 768px and we port that verbatim
on .rcm — right for the CLONE pages, which frame one region or country in portrait.
The chooser renders under the GLOBE projection, so its subject is a CIRCLE whose
diameter cannot exceed the frame's short side. In a 1:2 frame that circle covers at
most W / 2W = 50% of the height: at 390px wide, 390px of globe in a 780px box with
the rest empty.

So this is geometry, not a preference between two acceptable options. A square is
the ONLY frame a circle fills on both axes, and every other ratio is provably worse
for a circle:
    1/2 frame  height  50%   area 39.3%
    1/1 frame  height 100%   area 78.5%   (pi/4, the maximum in any rectangle)
Vertical coverage doubles.

SCOPED TO THE ADDITION, and that is the part worth checking rather than trusting.
The rule is .rcm--chooser, bound to colorMode === 'region'. I verified that value
is chooser-only instead of assuming it: mapColorMode returns 'in-country' for a
country and 'prayer' for a region, so 'region' is only the fall-through — the
no-selection state. If that ever stopped being true, this rule would silently
change cloned geometry, which is exactly what the scope ruling forbids, so the
check asserts it.

The existing globe-fill formula needs no change: it bisects the zoom that fills the
frame's SHORT side, which a square leaves well defined.

_chk-chooser-square-on-mobile-clone-untouched.mjs asserts both halves — the chooser
changed AND the clone did not — computes the coverage rather than restating it, and
carries a NEG control showing a 1/3 frame scores worse than 1/2, so the metric
discriminates between ratios instead of blessing whatever it is given.
2026-09-26 05:05:26 +00:00
Doxa Maps
3e176d2bf8 My UPG Neighbors: the Language Clusters lens draws its map again
395a66c9 translated the lens and put `const nt = useNeighborT()` at line 387,
but setup reads nt() at line 68 (statusMsg's first text). A const read before
its line is a TDZ ReferenceError, so setup threw "Cannot access 'nt' before
initialization" and the lens rendered nothing, map included, on both engines
(rega, check:leaf-engine-per-provider on main c8fee416).

The translator is now defined above its first use. Of the other files that
commit touched, only flower-mesh uses nt() above its definition, and there it
is inside a hover handler that runs after setup, so it is safe.

Headless (_qa-cycle/exp-shepherd/lc-probe.mjs: the check:composites-mount
harness, mounting the lens under ProfileLoader's provides):
  main  lang-cluster: setup THREW, ReferenceError: Cannot access 'nt' before initialization
  fix   lang-cluster: setup ran clean (send-map, which draws, also runs clean)
2026-09-26 04:57:47 +00:00
Doxa Maps
60faff1a48 PPLR data maps: loading d3 no longer writes into the host page's own d3
4c45d76c put the host's window.d3 back after loading the pinned d3, but d3's UMD
build writes its exports INTO an existing window.d3 ((t = globalThis).d3 =
t.d3 || {}). By the time the host's object was restored, ours had been written
into it: rega measured 577 added keys and version '7.9.0' on a host's own d3.
A host on d3 v6 would get v7 functions.

While our script executes (document.currentScript === the element we
appended), window.d3 now reads and writes a fresh slot of ours. Any other code,
at every moment, reads and writes the host's own value, so nothing is blanked
while the file loads. The host's value is then restored as a plain data
property. A non-configurable global (a page-level `var d3`) cannot be trapped,
so it is emptied for the load instead.

Headless, with the real d3@7.9.0 file (its sha384 matches the pinned SRI), run
in a vm realm with document.currentScript set as a browser does:
  before  host owns window.d3: keys 2 -> 579, version -> 7.9.0 (rega's finding)
  after   host owns window.d3: keys 2 -> 2, version host-7.0.0, same object;
          PPLR gets its own d3 7.9.0; the host sees its own d3 during the load
  after   no host d3: window.d3 absent afterwards, as before
The vm realm makes `var d3` configurable, so the non-configurable fallback was
not exercised there. The probe is kept at _qa-cycle/exp-shepherd/d3-probe/.
2026-09-26 04:54:42 +00:00
Doxa Maps
05346b1e53 PPLR data maps: escape every value that goes into popup and search-suggestion HTML
The critic fact-check (CRITIC-3) found PPLR's popups interpolating data raw into setHTML():
popupForPG (pins, coverage, rosetta), the country popup in coverage, the local popup builders in
religion, rop-tree and language-families, and the search box's suggestion renderer. That last one
renders place names that come from the Mapbox geocoding API, so it is external data. All of them now
go through escHtml(); the dash placeholder for empty values is unchanged.
Headless proof: a hostile-input test (an <img onerror> name, a <script> country, quotes and
ampersands) 5/5: no live tag or handler survives and the text shows as text. BUNDLE=pplr-data-maps
builds. Plugin-only.
2026-09-26 04:54:41 +00:00
Doxa Maps
028d592f93 Put our own status blocks on DX-life's spacing scale, and measure that nothing else is off it
The Driver's instruction: the regions dashboard must look like a clone of dx-life
"with proper CSS spacing", the only sanctioned deviations being the map and the
all-regions chooser. So spacing stops being a matter of taste and becomes a
measurement: every padding, margin and gap we ship should be a value THEIR CSS can
produce.

Measured that way, one value was ours and off-system: .rd-status / .rl-status
padding:28px, the app-only empty and loading blocks ("No regions found.",
"Loading region dashboards…") that their server-rendered page cannot be in. The
ELEMENT is legitimately ours; the NUMBER had no business being invented. Now
var(--spacing-xl). Result: 39 var(--spacing-*) references and 34 px literals, all
traceable to their CSS, and ZERO off-scale values.

★ TWO DENSITIES, AND I HAD CONFLATED THEM. tokens/semantic/_typography.scss sets
$font_density 1.25; tokens/semantic/_spacing.scss sets $density = $major-third =
1.33. An earlier check used 1.25 for BOTH, so every spacing floor it reported was
wrong. The conclusion survived — 11/11 token-bounded clamps still pin at 390px —
but one case came within 0.13px of flipping, which is the margin a wrong constant
hides. The scale is now DERIVED in the check from their own file, with a control
that fails if their file stops saying 1.33, so it cannot drift back.

The check's first premise was also too strong and would have produced three false
findings. It flagged 15px, 25px and 3.5px as off-system when clamp(15px, 6%, 25px)
is _progress-bar.scss:14 verbatim and gap:0.35rem is .prayer-coverage-legend's own
— correct ports under RULE 1. A value is only off-system if it appears NOWHERE in
their CSS, so the reference set is harvested from their real stylesheets and
unioned with the token scale. .sr-only's margin:-1px is excluded for a different
reason: it is the screen-reader clip idiom, where 1px is a hairline that keeps the
element non-zero-sized for assistive tech, not a spacing choice.

What this check CANNOT tell you, stated so nobody over-reads it: that a value is in
the right PLACE. It proves every number is on their system, not that the right
number is on the right selector. Per-selector parity still needs the live diff.
2026-09-26 04:42:59 +00:00
Doxa Maps
c9dfb668b3 Plugin Studio: collapse the header to give the plugin the whole page, and bring it back in one click
A builder previewing a plugin full screen can hide the studio header (the props panel: language, map
engine, Mapbox token). The header's new "Hide header" button collapses it, as the Research Gallery runs
headerless. A "Show header" button then stays pinned in the top corner, above every plugin, so the
props are always one click away. The choice is remembered in this browser (plugin-studio:header-hidden).
Strings in all 11 studio catalogues. BUNDLE=plugin-studio builds.
2026-09-26 04:39:00 +00:00
Doxa Maps
65017fefa4 The prayer packet shows how its list was narrowed out of all the earth
Page 2 of every packet is now the funnel: 12,379 people groups on earth, 7,341
unreached, 4,754 frontier, 2,107 on this prayer network's list, and N of those
in the reader's hand. A partner holding one list can see where it sits in the
whole, which is the argument the packet is making.

★ IT RENDERS NUMBERS IT DID NOT COMPUTE. Every count is read from
data/people-groups/vision-funnel.json, which build-vision-funnel.mjs generates
from the IMB CSV and the live network list and which already drives the Vision
Card. Re-deriving them in the packet would give one fact two sources; they would
drift, and two internally-consistent surfaces disagreeing is worse than one
wrong number because a reader cannot tell which to trust.

K AND THE SHARE ARE MEASURED AT RENDER TIME, NEVER TYPED, and this mattered:
the brief said "~30 lists of 50-100" and the catalogue says K=27 with counts
from 8 to 118 (west-africa 118, asia-india-rajasthan 116, uncovered-scatter 8).
Only about half sit in that band, so no copy claims it. The page states what the
data can defend -- "1 of 27 lists" and "N of the 2,107" -- and a footnote states
plainly that 21 of the 2,107 are in no list, because a partner adding up rows
would find the gap anyway. Reported to the Driver; K=27 pending his ruling on
whether the two residual buckets count.

A SHARE MUST NEVER ROUND TO ZERO. One decimal gave the single-group fixture
"0.0%", which reads as "this packet covers none of them". Verified exhaustively
over all 2,108 possible list sizes against 2,107: no size prints 0 and none is
malformed. 1 group -> 0.05%, 20 -> 0.95%, 21 -> 1%, 43 -> 2%, 118 -> 5.6%.

NUMERALS STAY WESTERN, as required. num() is toLocaleString(lang), and CLDR's
default numbering system for the bare ar/hi tags is latn -- verified 12,379 and
2,107 in both. Reusing num() rather than writing a second formatter is also what
keeps the funnel's digits identical to every other page of the packet.

PAGE COUNT NOW COUNTS THE ARTIFACT. packet.mjs computed
`1 + ceil(n/PER_PAGE) + 2` under a comment claiming the number was "asked of the
renderer, not recomputed here". It WAS recomputed, and the funnel page silently
made it wrong by one -- exactly the drift the comment warned of. It now counts
the emitted sheets.

THE GATE IS TAUGHT, NOT LOOSENED. check-packet-render went red on every fixture
("43 cards should lay out as 6 sheets, but the packet has 7") -- correct
behaviour on a structure it had not been told about. The wrong fix is to accept
N or N+1: that assertion exists to catch silently dropped people groups, and a
±1 tolerance is the slack a lost list page hides in. So the funnel is DETECTED
via .funnel-title and added as a known term, and a funnel page that renders its
title but no share figure is now its own failure -- a number printed but not
asserted on is decoration.

The page is OPTIONAL: buildPacket omits it when no funnel data was supplied, so
every existing caller keeps working and a host without the data still gets a
correct packet. Better a 4-section packet than one asserting figures it could
not read. A multi-list batch selection keeps the steps but drops "1 of 27",
which is false for a custom selection, while "N of the 2,107" stays exactly true.

Verified: check:packet-strings-parity PASS (31 string keys x 11 locales, my 10
new ones present and translated in all 10 non-English blocks, 0 declared-absent
additions). A partial locale with 3 of 10 funnel keys translated renders those 3
translated and the other 7 in English, nothing blank. Arabic packet generated:
dir="rtl", funnel is sheet 2 of 7, all four steps carry Western digits. All 16
generated packets satisfy the corrected sheet arithmetic, including the 11 older
laos packets that have no funnel page. Mutation-tested: dropping a list sheet is
caught by the independent page-header witness (20 DROPPED), a funnel page with no
share figure is caught by the new assertion, and removing the funnel page passes.
BUNDLE=my-upg-100-list vite build 319 modules clean.

NOT verified in a browser: check:packet-render launches Chromium, which the
Playwright policy reserves to rega. The simulations above parse the same
attributes from the same generated HTML.
2026-09-26 04:38:59 +00:00
Doxa Maps
22b92ae043 template-bundle: the starter's first screen is now a real map
A new writer's first copy used to draw a coloured box. template-map-a.vue now
draws the points in data/places.geojson on a basemap with one translated title,
through the library's own pieces (useMapInstance, resolveMapProvider, labelFont,
emissivePaint). It follows the page's engine, locale (right-to-left included) and
token, and with no token it runs on keyless MapLibre, so it works with no account.

template-map-b.vue stays as the second-screen example, so the README, the tutorial
and the plugin-designer skill (rename a, delete b) stay true. The README gains the
map's pieces table and the dx-maps paths.

Library change: library/i18n/locales/<11>/template.json (key template.title) is
eagerly globbed into every bundle, so consumers need rebuilding (rule 5B).
2026-09-26 04:38:12 +00:00
Doxa Maps
74d519686d Gospel map: the gap is a hole, the legend scrolls, the prose folds
THE DOTS. The old comment claimed all three pin colours sat apart from the clay
country ramp. For the one that matters it was false: tier 0 was #7d3b30 and the
darkest country fill is #7d3b30 — the same colour, contrast 1.00. In exactly the
countries with 40+ groups having nothing, the pins that matter most dissolved
into their own country.

Hue alone cannot fix it, because a choropleth and its pins compete for the same
range at every zoom. So the states now differ in FORM: a group with nothing is a
HOLE — white core, thick oxblood rim — and it is the largest mark on the map and
drawn on top. Some resource is a solid sage dot; Scripture and film is a small
dark dot that recedes. A blurred white halo sits under every pin so a dot never
touches its fill directly.

Two components, because one can always be beaten by a background. Measured
against all four fills: on the lightest the white core is 1.38 and the rim 6.00;
on the darkest the rim is 1.00 and the core 8.27. Worst case across the ramp is
3.64, above the 3:1 that graphical objects need. The mark cannot disappear.
The legend swatch is a hole too — a filled dot there would teach the wrong
symbol.

THE LEGEND overflowed the bottom of the page. It is absolutely positioned inside
the map, so its height was whatever its content happened to be. Capped to the
map's own height and scrolled inside — the container's height, not the
viewport's, because this leaf is embedded and vh would break on any partner page
that gives the map a fixed box.

THE WORDS. 232 words of English caveat sat above the three short translated
instruction lines that are the actual instruction set: what a dot is, what "none
found" does not mean, that a country is tappable. Every one of those 232 words is
true and load-bearing and none is needed before you act. They are folded behind
one summary, not deleted. They stay English by the standing decision on prose, so
folding costs nothing to translate and takes 232 words off first sight.
2026-09-26 04:37:17 +00:00
Doxa Maps
4717193961 Flower Mesh opens on an SVG lens: the leaf-engine gate stops expecting a canvas there
upg-neighbor/flower-mesh is a preset of the upg-neighbor element that opens on
its Flower Mesh lens, which is SVG and has no GL map. The leaf is still a map
element: the view-switcher reaches Send Map and the other GL lenses, so it must
keep isMap (engine peer, token ladder). Marking it isMap:false would break those
lenses. check:leaf-engine-per-provider judged it as a drawing map and failed it
on both engines ("drew: none"), before and after the auto engine alike.

- upg-neighbor.instances.json: the flower-mesh preset says "opensOnMap": false.
- generate-tree.mjs: an instance's manifest entry carries opensOnMap:false when
  it is set (absent otherwise, so every other entry is byte-identical).
- check-leaf-engine-per-provider.mjs: such a leaf is not judged as a drawing
  map. Its Mapbox-request count under ?provider=maplibre is still measured.

Headless: after bun run tree, the manifest marks only
upg-neighbor/upg-neighbor/flower-mesh with opensOnMap:false. The gate's leaf
selection, replayed over it, judges 28 of 56 leaves as drawing maps, with
flower-mesh excluded and its five siblings unchanged. No browser run.
2026-09-26 04:21:49 +00:00
Doxa Maps
2a26aefff1 A gate for the mistake I shipped: a name used but never bound
I broke both composites on main. Editing them with a pattern-match script, I
replaced an import line by matching text that had already changed, so the
replacement silently matched nothing and three names — RTL_LOCALES,
getRuntimeLocale, detectLocale — were used and never imported. Setup threw a
ReferenceError and both elements rendered nothing until a hotfix landed.

★ IT PASSED EVERY GATE, AND THAT IS THE INTERESTING PART. It compiled, because a
free identifier is valid JavaScript until it RUNS. It passed my grep of the
built bundle for the new code, because the code was genuinely there. It passed
template-literal, sfc-parse, composite-freshness and SRI, because not one of
them evaluates a setup function. The evidence I had could not distinguish a
working composite from a dead one, and I called it "verified headless".

So this asks the one question those gates cannot: is every name a component uses
actually bound — imported, declared, a parameter, or a known global? It reads
the same source the compiler reads, tracks real scopes (module, function, block,
catch, class), and runs in milliseconds with no browser. Fleet-wide, over every
.vue in plugin-designs/ and library/, because the mistake was a text-editing
habit rather than anything about one plugin.

WRITTEN BY RUNNING IT, which is the only way an allowlist gets honest. The first
run reported 4,259 findings — every one a false positive from a real bug in my
own walker: a helper that takes a node TYPE was being handed the NODE, so the
function branch never fired and every parameter read as unbound. The second run
found the globals this tree genuinely uses (d3, customElements, ShadowRoot, CSS,
Function, self, the typed arrays) and vite's build-time defines. The third found
a real gap in the gate: an SFC may have BOTH a plain <script> and a <script
setup>, and they share a scope — LegendTools exports a function from one and
uses it in the other, which I was reporting as free. Reading only one block
would have taught the fleet to ignore this gate on its first run.

NEGATIVE CONTROL, because a check that has never been seen to fail is not
evidence: restoring the broken import makes it FAIL naming all three names and
their lines; restored, it passes across 127 components.

It is the cheap half of a pair — it proves a name is bound, not that the thing
runs. The expensive half is a check that actually mounts a composite.
2026-09-26 04:21:49 +00:00
Doxa Maps
35a4f58fc2 "Show on map" switched the view and the map never acted: retain the request
rega measured two broken interop links and read them as one timing bug. They are
two different bugs, and neither is the one hypothesised (that the event fires
before the map pane has mounted -- both shells create and append their pane
elements in their own onMounted, so the element exists at click time).

country-posters: THE FORWARDING WAS NEVER WRITTEN. onShowOnMap() was a guard,
go('map'), return. No dispatch, no attribute, no property -- nothing carried the
selection to the map at all, which is why the camera stayed at world view. Not a
race, and no amount of waiting would have fixed it. The shell also created its
pane elements and dropped them on the same line, so there was no handle to address
the map pane with even if it had wanted to.

upg-100: forwarding existed but was fire-and-forget, losable two ways. The listener
is attached at the END of an async onMounted, after 5 awaits and only `if (host)`,
and an element dispatch has no replay. And once attached, the zone id was resolved
against legendNodes and applied by `if (node) onLegendSelect(node)` with NO else --
so an id arriving before the data was discarded in silence. rega's second-click
measurement settled which one fires: the first request dies before readiness.

ONE PATTERN, and it is deliberately not "attach the listener sooner" -- that would
fix one of the two losses and leave the other. The request is RECORDED as an
attribute on the pane element and only then announced by event. An attribute is
state: it survives custom-element upgrade, and it is still readable when the pane
arrives, and when the pane's data arrives later still. The event remains the live
path for a map that is already up. Both triggers end in one apply function, so
there is one apply path with two triggers rather than two ways to select.

Unreadiness is retained; an UNRESOLVABLE id is not. Retaining a typo'd id would
spin on every readiness change forever, so it is dropped.

The drain runs on nextTick, and that is not caution. upg100-plugin flagged from
experience that watch([batchMode, isDark]) marks legend rows on nextTick, so a
pre-flush drain would apply the selection against rows about to be replaced by the
re-render, and the highlight could be stomped. That failure is invisible in the
worst possible way: "the selection happened but you cannot see it" is
indistinguishable at the rendered layer from "the selection never happened" --
the exact bug being fixed. The fix would have impersonated the defect.

SOURCE ONLY, deliberately. Both bundles were built to prove the source compiles,
then the built artifacts were reverted: these are not my bundles, and rule 5 keeps
rebuilds with the build owner. country-posters and my-upg-100-list both need one.

Verified by _chk-showonmap-selection-is-not-retained.mjs, 30 assertions. It asserts
the ONE APPLY PATH property rather than just the functions' presence, on
upg100-plugin's suggestion -- a later third trigger that bypassed the apply path
would otherwise fail nothing and reproduce the invisible-selection symptom.
2026-09-26 04:21:49 +00:00
Doxa Maps
7e40b8aa22 Gospel access: below the border for the two countries that are half the map
P1, data layer. 855 of Doxa's 2,107 groups are in India and 154 in China — 47.9%
of everything this map draws, rendered until now as two flat fills. No other
country is close; the third is Sudan at 105. Each group is placed into a province
by point-in-polygon on its coordinate: 44 provinces with groups, 998 assigned,
13 provinces holding groups with nothing.

China's ADM1 file carries 12 provinces, not 31+, and I expected that to lose
groups. Measured instead: 152 of 154 Chinese groups fall inside them and all
twelve are used. The file was cut to the provinces where the unreached peoples
are, so the subset is the right subset.

The 11 groups that land outside every polygon are COUNTED, not dropped — nine
Indian and two Chinese, every one of them coastal or an island. That is the
predicted failure of placing a people by a single point, and it belongs in the
output rather than in a silence.

Every row is flagged approximate, because a province here comes from a coordinate
that marks where a people is found rather than a settlement, and 108 groups in
the roster share a coordinate with another group.

Going below the border also exposed something the country view was hiding:
Arunachal Pradesh, in the far north-east, is offered TELUGU as its way in,
because the curated bridge table names it and a curated bridge outranks the
computed one. India's computed default is Hindi, defensible nationally and still
wrong for much of the north-east. This table does not fix that. It records it, and
says to read the ways-in column as "a language with Scripture and film somewhere
in this country" until a bridge can be chosen per province.
2026-09-26 04:16:10 +00:00
Doxa Maps
67b7e92cdd Gospel map: say what a dot is, and name the groups that share it
P5. A pin marks where a people is found, not a settlement, and the data shows it:
of the 2,107 placed groups, 2,048 have a distinct coordinate — so 49 coordinates
carry more than one group and 108 groups sit on a point they share with another.
41 pairs, 6 triples, 2 quadruples; four groups share one coordinate in coastal
Karnataka and four more share a second point a few kilometres away.

The coordinates also carry false precision: 1,274 are given to six decimal
places, metre resolution on a figure describing where a people is found.
Rendering that without comment invites a reader to treat a dot as an address.

So the sheet says what the dot is, and where the point is shared it names the
other groups. "Approximate" alone would have been the easy word and the wrong
one — it hides the part a reader would never otherwise discover, that some dots
are literally the same dot. 108 sheets carry the second sentence; the other
1,999 carry only the first.
2026-09-26 04:09:37 +00:00
Doxa Maps
f1f93d78b0 Rebuild after merge
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-26 03:57:21 +00:00
Doxa Maps
ba78a389e6 Gospel map: give the zero-access view an address, and say the way in on every row
P2. The list of groups with nothing already existed and could only be reached by
knowing which control to press. It is the question this map is for, so it gets an
address: ?view=zero opens it with the map already filtered to those pins. That
address is also what a partner embeds and what a QR code points at.

Each row now carries its way in. This was the only surface showing groups with
nothing that did not say what to do about them — the pin sheet and the country
panel both do — and it is the one a mobiliser reads top-down.

Building it exposed the same flag under-reporting for the third time. `sg` comes
from the sign-language catalogues and asf (Auslan) carries none, so reading it
alone this leaf missed Cocos Deaf, Deaf Christmas Islanders and Deaf Norfolk
Islanders: no SUN panel on their pin sheet, and no way in on the list, while the
access table gave all three SUN. The pins builder and the access builder were
each corrected the same way earlier; this puts one Deaf test in the leaf so the
four surfaces agree. Measured after: 369 of 369 zero-access rows carry a way in,
none blank.
2026-09-26 03:56:24 +00:00
Doxa Maps
c36ea54904 Gospel leaves: restore the two number call sites the revert had left routed
The revert left fmtShort and the table's row count going through fmt() instead of
calling toLocaleString() directly. Behaviour was identical, because fmt is that
bare call again — but the acceptance measure for holding number format is a
literal count of the pre-change state, and 6 is not 8. An "identical in effect"
argument is the wrong currency when the point is that this map must be byte-for
-byte what it was until the Driver rules.

Measured against the build at 2eac0fb3^: numberingSystem latn calls 0, bare
toLocaleString() 8, both exact. The number handling in all four leaves now
differs from that build only in comments.
2026-09-26 03:41:40 +00:00
Doxa Maps
2b49056f17 Gospel map: credit the basemap that is actually drawn
Under MapLibre the attribution read "geoBoundaries · Mapbox · © OpenStreetMap"
while the tiles came from OpenFreeMap. Crediting the wrong supplier is worse than
crediting none: it is a false statement about whose work is on screen, and
OpenFreeMap's terms ask for their credit.

addMapAttribution hardcodes the Mapbox link whenever basemap is true and takes no
engine, so the shared bar now credits geoBoundaries alone under MapLibre and the
engine's own AttributionControl supplies the basemap line. Fixing the helper to
take an engine belongs in library/ and goes to the build owner; this is the
plugin-only half. Mapbox behaviour is untouched.

The wording is read from the style, not restated here. OpenFreeMap's TileJSON
carries the exact text — OpenFreeMap, © OpenMapTiles, data from OpenStreetMap —
so a hand-typed copy would be one more thing to go stale the day they change it.

Also corrects the comment on the load guard added last tick. It was written for a
"MapLibre draws only the basemap" report that has since been retracted as a
measurement artifact: polled properly, both engines draw 170 fills and 2,107
pins. The guard stays, because everything this map draws hangs off one callback
and that race is real, but it repaired nothing and the code should not claim it
did.
2026-09-26 03:38:49 +00:00
Doxa Maps
8991181091 Translate the eight strings that had no catalogue entry, keeping their English exactly
The shepherd's call: translating wording that already exists is not a product
decision. So every string keeps the English it reads today and gains a key, in
all eleven locales.

Big Picture's four first, since it is the localization priority: Export CSV, the
detail drawer's title and its empty state, and the legend's "heartbeat zone"
bin. Then Top countries, Select all, the table dock's Expand/Collapse titles,
and the singular "group" whose plural was already translated — a half-translated
plural is its own small defect.

Full screen / Exit full screen now read buttons.toggleFullscreen, which already
shipped in every locale, so that one needed no new wording at all. The arrow
glyph still carries the state.

Three more went in that were not on the list, because leaving them would have
been worse than the defect being fixed: "Deselect all" is the other half of a
toggle whose first half we just translated, and the two row-selection aria
labels are the same control speaking to a screen reader. A control that changes
language halfway through is harder to use than one that never changed at all.

★ ONE STRING I AM NOT CONFIDENT IN, AND IT SHOULD BE CHECKED BY A SPEAKER.
"heartbeat zone" is the Driver's own coinage, not standard UI vocabulary, and a
metaphor is exactly where a translation I cannot read can go quietly wrong. I
translated it literally in all ten languages. The rest are ordinary interface
terms; that one is a judgement, and I would rather flag it than have it
discovered on the gift.

VERIFIED HEADLESS: i18n-parity passes with zero debt; three bundles build; the
keys AND their Arabic values are present in the built output (تصدير CSV,
منطقة نبض القلب, اختر شعبًا لعرض التفاصيل, and the header's placeholder).
Numerals are untouched, per the hold — this is text only.

(cherry picked from commit 67568c0509)
2026-09-26 03:34:42 +00:00
Doxa Maps
9844f72eaf Wall of People Groups: nothing leaks onto the host page (no Font Awesome, no window.d3)
The critic fact-check (CRITIC-9) found the Wall injecting Font Awesome 6.5.1 into the HOST
page's <head> (global .fa-* rules that can restyle a partner's own icons) and loading d3@7 (a
floating range, no SRI) as window.d3, overwriting a host's own d3.
  - The card's heart / comment / share icons are now plain SVG paths in the card itself, not
    Font Awesome <i> tags in three foreignObject/xhtml divs per card (~6,300 on the wall: lighter
    too). The dialog hearts are inline SVG; the source-picker info icons are a text glyph. No
    Font Awesome stylesheet is injected anywhere.
  - src/wallD3.js loads d3@7.9.0 exactly, with integrity + crossorigin, keeps it in the module
    (wallD3()) and puts the host's window.d3 back (or leaves none).
  - The heart used `self.$store`; `self` is the browser's window, so a prayer made in this
    session never lit it. It now reads this.$store.
Headless proof: a stubbed-DOM test of the d3 loader, 6/6 (the host's d3 restored, none leaked,
exact version + integrity + crossorigin, a failure rejects). The bundle builds and contains 0 Font
Awesome references.
needs eyes: the Wall: hearts, comments and share icons render on the cards; a card whose group has
prayers shows a red heart; a host page with its own d3 or Font Awesome is unaffected.

(cherry picked from commit 28575d6013)
2026-09-26 03:34:31 +00:00
Doxa Maps
5285574259 Key Cities (gated): the map, list, tables and posters speak the reader's language
The last bundle of the L1 pass: key-cities had no i18n at all though its entry installs it. A new
keyCities area (38 keys, all 11 locales) carries the map title, band legend, caveats, the city
sheet's labels, the list's intro, filters and section lines, the data tables' title, intro, tabs and
loading line, and the poster wall's heading, tag, nouns and "coming soon" message. Unnamed places
keep showing coordinates, now inside a translated phrase. Counts use a "label: n" form outside English
and numbers keep their existing format (numerals held). cities-map's script message goes through a
guarded useI18n with the English catalog as fallback.
check:i18n-parity PASS; placeholders match English in every locale; the bundle builds.
A library catalog change: studio lands it (rule 5B).
needs eyes: key-cities (gated) with ?lang=ar: the map legend, a city sheet, and the list.

(cherry picked from commit 7d4da13721)
2026-09-26 03:34:30 +00:00
Doxa Maps
7b1e64eaba Wall of People Groups: level of detail like a map, and an HD poster print path
The wall gave all ~2,100 card photos their full href at once, so every photo was fetched and
decoded even with the whole 4000x3000 wall on screen and each photo a few pixels wide. Now:
  - photo URLs are parked in data-hd; zoomed out (a card under 48 px on screen) the wall shows
    only the light image slots, with no photos loaded;
  - zoomed in, updateLod() (rAF-throttled on every zoom/pan) gives HD photos only to the cards in
    view plus one card of margin, and REMOVES them from cards that leave the view; a 300-image
    cap keeps any view light;
  - "Print HD poster" loads every photo, waits for them, prints, then returns to level of detail;
    it replaces the old "coming later" alert (new key wall.printPoster, all 11 locales).
The geometry is a pure module (src/wallLod.js), so it is tested without a browser.
MEASURED, headless: the real 2,107-card layout at 1280x800. Sampled photos are ~200x250 (23 real
image URLs from the live API): ~0.22 MB each decoded, ~45 KB compressed. Loading all of them, as
before, is ~466 MB decoded + ~94 MB downloaded. With LOD: zoom 1-4x = 0 photos; 6x = 150 (~33 MB);
8x = 80 (~18 MB); 12x = 63 (~14 MB). lodPlan matches a brute-force visibility oracle at every zoom.
Photos are the part of the 1.4 GB that can be measured without a browser; the rest (DOM,
rasterisation) needs a browser heap snapshot: needs eyes.
needs eyes: Wall of People Groups: DevTools memory before/after; zoomed out = no photos, zoom in =
photos appear only where you look; "Print HD poster" prints every photo.

(cherry picked from commit ef9646f8f5)
2026-09-26 03:34:30 +00:00
Doxa Maps
395a66c9ae My UPG Neighbors: the lenses' chrome speaks the reader's language
The universal-props matrix flagged upg-neighbor as a locale BYPASS: no t() in any of its ten
components. By the agreed scope (dx-shepherd, 2026-09-26), this localizes the CHROME in all 11 locales
through a new neighbor area (103 keys): the brand, view tabs and hints, lens titles, search
placeholders, select/aria labels, loading and error lines, legends, stat labels, tooltips, chart-strip
controls, list actions and copy feedback. Research terms, provenance footers and proper nouns stay in
English as named sources: SEND, doxa-2000, Bounded set, JP, PPLR, IN/CN, the 4-D join notes.
Templates use the globally injected $t; script strings use useNeighborT() (guarded, English fallback).
Stat lines use a "label: n" layout in every locale so no language needs plural rules. Country names
in the lookup tables are data and unchanged.
check:i18n-parity PASS; every locale's placeholders match English; the bundle builds.
A library catalog change: studio lands it (rule 5B).
needs eyes: My UPG Neighbors with ?lang=ar: the view tabs, the Send Map legend and the Overview chart strip.

(cherry picked from commit 090f64fbe3)
2026-09-26 03:34:29 +00:00
Doxa Maps
85019a1543 Wall of People Groups: the wall and its dialogs speak the reader's language
The universal-props matrix flagged the Wall as a locale BYPASS: no t() anywhere. A new wall area
(library/i18n/locales/<lc>/wall.json, all 11 locales, 39 keys) now carries the header, orientation
and zoom controls, the loading and offline notice, each card's prayer count and "View prayers", and
both dialogs (Prayer Tools, People Group Chats), with aria-labels on the close buttons, which had none.
The components use the globally injected $t (createAppI18n, globalInjection). The prayer count uses
vue-i18n's plural choice in English and a neutral "label: n" form elsewhere, so no language needs
plural rules. Status values from the API are data and stay as they come.
Headless proof: vue-i18n against the real catalogs, 8/8 (en 0/1/4 plural, ar neutral form, ar title,
de label form); check:i18n-parity PASS; the bundle builds. Library catalog change: studio lands it.
needs eyes: Wall of People Groups with ?lang=ar: the header, a card's prayer line, and both dialogs.

(cherry picked from commit 7c1c60975e)
2026-09-26 03:34:29 +00:00
Doxa Maps
cc3651919d PPLR data maps: tab labels and status lines speak the reader's language
The universal-props matrix flagged pplr-data-maps as a locale BYPASS: every tab label and every
loading or ready line was an English literal. A new pplr area (library/i18n/locales/<lc>/pplr.json,
all 11 locales) carries the 10 tab labels and 20 status lines; usePplrT() reads them through a
guarded useI18n with the English catalog as fallback. Counts go in as parameters, and their number
formatting is unchanged pending the Driver's numerals decision. The views' own legend and panel text
is the next pass.
check:i18n-parity PASS (all 11 complete); the bundle builds. A library catalog change (the i18n index
globs every area into every i18n bundle), so studio lands it (rule 5B).
needs eyes: PPLR Data Maps with ?lang=ar: the tab bar and the loading and ready lines.

(cherry picked from commit 4a16f0538b)
2026-09-26 03:34:28 +00:00
Doxa Maps
5ded530b43 Prayer Progress: no hardcoded English left in the map's own chrome
The Driver's localization list, item 4. doxa-simple-map already took its language from profile-config
locale/lang and ?lang= (ProfileLoader) and set dir=rtl for Arabic, but its own strings were English
literals: "Loading map…", "The map could not be loaded." plus a technical init message, the legend
title 'Legend', the default "Prayer" tab label, and the whole "About This Map" help modal. They now
read the catalogs: legend.titleBar.legend, tabs.<id>, allPeopleGroups.loadError, buttons.aboutThisMap,
buttons.close (the close button had no accessible name), and a new simpleMap area in all 11 locales.
A tab label a HOST passes in profileConfig.tabs is still shown as given. The map formats no numbers
itself; the legend, popups and detail panel are library components already translated.
check:i18n-parity PASS; the bundle builds. A library catalog change, so studio lands it (rule 5B).
needs eyes: Prayer Progress with ?lang=ar: the loading line, the tab, the legend title and RTL layout.

(cherry picked from commit c35bc84794)
2026-09-26 03:34:16 +00:00