Commit graph

397 commits

Author SHA1 Message Date
doxa-maps-88655c70
59a3295a01 Merge commit '6090e8e7daea9d1275b6ce3d6346f5940802b9c9' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 00:38:09 +00:00
doxa-maps-88655c70
8da0e9f256 Arabic: align the country and region buttons to the reading start, not the left
Diffing Arabic against live for the first time. dx-life serves /ar/regions under
strategy 'prefix_except_default', measured live as lang="ar" dir="rtl", so this
axis was comparable all along and never had been compared. Our own RTL checks
only ever asserted our own invariants -- that a dir attribute exists, that an
arrow points the right way -- which cannot tell us whether we flip the things
THEY flip.

The defect: .region-card__link and .country-link both set text-align:left.
Theirs is a NuxtLink, an inline <a> that shrink-wraps to its text; ours has to
be a <button> because the control changes the embed's subject in place instead
of navigating a route, and a button's UA default is text-align:center -- so the
port overrode it with the PHYSICAL `left`. In English `left` and `start` are the
same value, which is why this survived a 37-property region-card diff and an
11-property bullet diff. In Arabic they are opposites.

Measured on live /ar/regions against ours at dir=rtl, same 246.9px row and the
same 102.7px of text on both sides: their label sits flush to the row's RIGHT
edge, 0px from the RTL reading start, and ours sat flush LEFT -- every country
label in the list stranded 144.2px on the wrong side of its column. After the
change both sides read 0px from the right and 144.2px from the left, identical.

text-align:start is correct in both directions, and English is unregressed:
the list-vs-live diff still passes 21 of 21, and every measured RTL property now
matches live with 0 differences.

Three defects in the new probe, all of which blamed the product first, are fixed
in it rather than worked around:
- it read dir/lang off the custom element <regions-dashboard>, which carries
  neither. They live on .rl-root, which is the only place they can usefully sit
  because a [lang]/[dir] selector cannot cross a shadow boundary. It had duly
  reported dir=ltr lang=null against their dir=rtl lang=ar.
- it waited only for Arabic text before reading. The chrome translates instantly
  from the client-side catalogue while the same locale change refetches the data,
  and that in-flight state briefly unmounts every .region-card -- so it read a
  page with no cards and called four elements "MISSING on ours" when the product
  was fine (measured: 7 cards before the switch, 7 at +1.5s, +5s and +12s after).
- it read li::before on both sides. The two sides attach the same bullet to
  different elements: theirs styles li::before, ours .country-link::before. The
  bullet's own properties are byte-identical wherever it hangs -- content "",
  width 29.2969px, position absolute, margin-inline-start -39.0625px, left
  256.703px -- and the first country sits at 834.7px on both. That is a
  structural difference to report, and the probe now reports it as a note instead
  of failing four style assertions over it.
2026-09-25 00:37:35 +00:00
doxa-maps-88655c70
2046f2febd Export which kind of gap each row is, and make both surfaces agree
The map explains that "1,845 groups have nothing" is four situations
wearing one number. An exported copy of the gap table could not say
that at all, which is the copy a translation organisation actually
works from. Each row now carries its kind:

  1,001  every reachable pool checked — the real translation gap
    660  GRN not yet swept — the crawl is still running
    183  sign language — pools are key-gated, an access problem
      1  no language code — no pool can index it

Adding it exposed that the two surfaces bucketed differently. The
table tests sign language first; the map tested private-use first, so
47 Deaf groups were filed under "unindexable code" and the Deaf share
read 136 instead of 183. Both were internally honest and they told
different stories, which is worse than either being wrong on its own.

The map now uses the table's order, because being Deaf is the more
actionable fact about those rows than the shape of their code, and it
still says 47 of them are doubly stuck. Both surfaces report 1,001 /
660 / 183 / 1, verified against each other rather than separately.

The meta warns that filtering to sign_language alone is now the whole
Deaf figure and not a subset, since the private-use rows are the same
people.
2026-09-25 00:22:11 +00:00
doxa-maps-88655c70
1c7384ee39 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 00:02:18 +00:00
doxa-maps-88655c70
5e4c76e7b8 Say that 183 groups in the gap are Deaf, not 136
The breakdown I shipped last tick implied two separate populations:
136 sign languages whose pools are key-gated, and 48 groups with a
private-use code that no pool can index. Measured: ALL 47 of the
private-use groups are sign languages too. Read as written, a reader
would put the Deaf share of this gap at 136 groups and 50.6 million
people, when it is 183 groups and 55.2 million — the largest blocked
population on this map, and one whose gap is an agreement away rather
than a translation away.

Both lines stay, because the reasons they are stuck genuinely differ,
but the sheet now says they overlap and gives the combined figure.

Found while researching whether anything open exists for them, which
is recorded next to the other rejected pools so nobody re-probes it:
deafbible.com serves the same 3,894-byte app shell for every URL,
door.international returns 114 bytes, and Deaf Missions is a real
ministry with real Scripture but ASL only — and not one of these 183
groups speaks ASL. Watchtower publish a great deal of sign-language
material and are excluded on doctrine, without exception.
2026-09-24 23:56:07 +00:00
doxa-maps-88655c70
45a7e1a1f5 Say what the gap number is actually made of
"1,913 people groups have nothing" is four different situations
wearing one number, and they call for four different responses. The
sheet now breaks it down, computed from the pins and each code's own
"checked" line rather than typed:

  887 groups, 149.7M people — every pool this build can reach was
      checked and found nothing. This is the real translation gap, and
      the only quarter a translator rather than an administrator can
      close.
  842 groups, 1.1M — Global Recordings Network has not been swept for
      them yet. The crawl runs a batch at a time at the delay they ask
      for, so these resolve themselves.
  136 groups, 50.6M — sign languages, whose pools are key-gated. Fifty
      million people whose gap is an ACCESS problem, not a translation
      one, and reading them as untranslated would send help to the
      wrong place entirely.
   48 groups, 6.9M — a private-use code or none at all, which no pool
      can index. Searching harder cannot move these.

The four sum to 1,913 and the rendered figures were checked against
the same count made offline. Also fixes a missing space that ran two
sentences together — "people.These are the 40 largest" — which the
screenshot caught and the markup did not.
2026-09-24 23:51:46 +00:00
doxa-maps-88655c70
dac7b1ab57 Merge commit 'b6254567e1b32df2c0c5ceb0ee5d0576c47cb159' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 23:38:21 +00:00
doxa-maps-88655c70
78a7baaa96 Fill the chooser's frame: solve the globe's zoom instead of reusing a mercator formula
The chooser's zoom came from log2(width / 512), and the comment above it said why:
"Mercator's world is 512*2^z wide". But the chooser renders under the globe
projection, so that is the same mistake as the region pages framing too wide --
a mercator answer applied to a sphere -- only here it made the frame too SMALL.
Found by looking at the screenshot, not by any assertion: a sphere adrift in
white space.

Measured at 1200x780: the mercator answer of 1.229 put the sphere at 390.61px,
50.1% of the frame's short side, so half the frame was dead space. And the
number was inert -- zoom 0.5, 1.0 and 1.229 all render an IDENTICAL 390.61px
sphere, because Mapbox clamps the globe's apparent size at low zoom, so the
formula's output sat inside the clamp and could have been 0.5 with no visible
difference.

There is no closed form to swap in. Diameter against a clean doubling is 3.6 to
10.5% off above the clamp, because the globe-to-mercator transition is nonlinear
there. What the measurement does establish is monotonicity, which is the only
property a solver needs, so the zoom is now SOLVED against the engine: bisect
the zoom whose limb projection fills 92% of the frame's short side. project() is
pure transform math, every setZoom lands in one synchronous block before a frame
renders, the original zoom is restored in a finally, and the old mercator
formula remains the fallback because a small globe beats a broken chooser.

The first version of the solver measured pole-to-pole and called it the
diameter, which holds only when the centre is on the equator. The chooser
centres on the mean of every people group, latitude 28, so that was a CHORD:
717.6px where the sphere was really 792-835px across, understating it by 1.104x.
The solver over-zoomed and clipped the globe top and bottom -- again visible in
the screenshot while every number still looked right. It now samples the limb,
points at 90 degrees angular distance, and takes the MAX across twelve bearings,
because Mapbox's globe is a perspective camera rather than a true orthographic
one and the silhouette is not a perfect screen-space circle: 396px along the
meridian against 417.3px east and west at one zoom.

Result: the widest axis is 750.6px inside a 780px frame, the whole limb sits in
frame, and dead space falls from 49.9% to under 4%. Pins are correspondingly
bigger click targets. On a phone the ported 1/2 aspect ratio means the globe
fills the WIDTH, which is the right trade -- filling the height would crop it
horizontally and lose pins off both edges -- and that leaves half the height
empty by geometry, which is carded for a Driver decision rather than resolved by
deviating from a clone-faithful rule.

The chooser's behaviour is unchanged and re-proven after the camera moved:
_probe-chooser 15/15 and the new _probe-chooser-pin-opens-country 8/8.
2026-09-24 23:37:47 +00:00
doxa-maps-88655c70
2f77da9d5b Stop the search box clipping the word "code" on a phone
At 390px the placeholder read "Find a people group, language or ISO
cod" — losing the one word that tells a reader an ISO code is accepted
at all, on the control most likely to be used first. Measured: 40
characters of text in 279px of room.

The short form keeps all three things the box accepts and fits with
room to spare, 185px of 279. The full phrase still shows at desktop
width, and the aria-label stays long either way, because a screen
reader has no width to run out of.

Found by checking phone layout after a flight of additions to this
sheet — five gap tabs, a filter, new chips and the bridge panel all
arrived without anyone looking at 390px. The rest held up: no
horizontal overflow, the chips wrap, the sheet scrolls, and the bridge
panel renders at 358px.
2026-09-24 23:25:28 +00:00
doxa-maps-88655c70
d55285eafe Name the language spoken around a group that has nothing
For 230 people groups the map said "nothing found" and stopped, when
that was not the whole truth: a language already spoken widely in
their country does have Scripture or the film. Kimr in Sudan has
nothing in Assangori; Sudanese Arabic, spoken by 65 groups around
them, has both. That is the difference between no way in and a way in
that is not their heart language.

WORDED SO IT CANNOT BE SKIMMED AS "THEY HAVE SCRIPTURE". The heading
says "Not their language", the body names whose language it is and how
widely it is spoken, the strength is quoted from the source, and the
last line says a second language is a way in and not a translation.
The chips above are untouched and still read "none found" five times,
and the tier is unchanged: 2,001 groups still have nothing of their
own, exactly as before.

Two exclusions, both measured rather than assumed. Fourteen of these
rows offer a DEAF people group a SPOKEN bridge — Gawri "has a film" is
no use to Deaf Pakistanis, the same error as offering them recorded
audio — so sign languages are out. Sixty-five carry the curated
strength "weak — may not be a real lingua franca"; a bridge the source
itself doubts is not an answer, so those are out too.

Placed below the whole chip row after a first pass split it, leaving
the Zume chip stranded under the panel.
2026-09-24 23:20:30 +00:00
doxa-maps-88655c70
31b934bdf5 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 23:01:56 +00:00
doxa-maps-88655c70
f7e39d5d5c Clone their map legend exactly, reset and all
Looking at the side-by-side Africa crops after the framing fix showed the
legend rows sitting a few px off and the panel reading differently. Diffing
our block against CountryMap.vue's found six differences that no computed-CSS
probe was covering: font-size 12.5px against their 12px (a rounded-token
artifact of mine, not their scale), colour #2a332a against their #1f1f1f, and
four properties simply missing -- the panel's box-shadow, pointer-events:none,
and the swatch's display, border and ring. The swatch border is the one with
layout consequences: a 10px dot inside a 1px border is 12px tall, so dropping
it shortened every row and moved the whole stack, which is the drift the crop
showed. An old comment here called this legend "one of the sanctioned
additions"; that was wrong and is why it drifted -- their CountryMap.vue
carries this legend with these same four entries, so it is a clone and every
property is theirs to dictate. The one deviation stays and stays documented:
inset-inline-start instead of their physical left, so the legend sits in the
reading-start corner in Arabic.

That left the panel 2px wider than theirs on the right edge alone. The text was
ruled out first: the same string measures 129px on both sides, in the same
Poppins at the same weight. The cause is their GLOBAL reset, _reset.scss lines
3-8, which sets box-sizing:border-box on every element and every ::before and
::after. No scoped style could ever have shown it. Our leaf document already
carries the identical rule, but this component renders in a shadow root and a
document-level universal selector does not cross that boundary, so our swatch
was content-box at 10px+2px while theirs was border-box at 10px. Two swatch
borders, exactly 2px.

Porting the reset into .rcm closes it: the panel now measures 165px against
their 165px, the panel bounding box lands on the same pixel on all four edges,
and the legend corner diffs to a mean 1.14/255 per channel, which is
antialiasing. The map frame is unchanged at 1200x780.

This is the seventh global rule of theirs found missing, and it explains the
seven per-element box-sizing patches scattered through this bundle -- each one a
local workaround for the reset that never arrived. Porting it bundle-wide and
deleting those seven is carded rather than swept in here, because border-box
changes geometry in components whose match against live has to be re-measured.
2026-09-24 22:58:16 +00:00
doxa-maps-88655c70
064ee63a6f Stop the tables saying GRN is missing and miscounting their own rows
Two claims in shipped metadata had gone false, and both were the kind
a reader trusts precisely because it looks specific.

people-group-resources listed Global Recordings Network under "not
included: requires an API key". Wrong on both halves: GRN is included,
583 codes of it, and it needed no key — 5fish resolves by ISO 639-3
and asks only for a crawl delay. A caveat naming a pool as missing
misleads exactly as much as a stale number when the pool is there. The
refresh now moves it out of that list and records how it was reached,
so the list cannot drift again.

The links table said "the 506 zero-reach rows" and "51 of the 168
codes". The true figures are 1,038 and 492: every source added rows
for languages no pin on this map speaks, and a sentence that counts
something has to be recounted when the something changes. Both are
computed now rather than typed.

Found by sweeping every shipped table's metadata for baked numbers
instead of waiting to trip over one. The surfaces came back clean —
their prose is already computed — so the stale claims were all in the
table meta, which is the part nobody looks at and a CSV carries away.
2026-09-24 22:46:16 +00:00
doxa-maps-88655c70
4d9b24a82d Merge commit '840830ebc47250b642d6d58ba1e584a30e74d47d' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:45:25 +00:00
doxa-maps-88655c70
840830ebc4 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:41:49 +00:00
doxa-maps-88655c70
770d29fd25 Frame the region map the way their constructor does, not the way fitBounds does
The Africa crops showed theirs tighter than ours, and 107 identical CSS
properties could never have said why. Everything I could think to compare
matched: mapbox-gl 3.24.0 on both sides (their bun.lock, our CDN pin), the
style, the 1200x780 container, the padding of 40, the maxZoom of 6, the box
itself (their viewFor() pin extent is byte-identical to our rollup), and
NEITHER page sets a projection.

The asymmetry was when the fit runs. They frame the box in the constructor
(CountryMap.vue:179); Mapbox v3 resolves a constructor `bounds` while
getProjection() still reports 'mercator', giving zoom 3.210. We had recreated
that as a post-style-load fitBounds, resolved with the globe already active,
giving 2.777 -- a sphere shows more planet at the same zoom, so it pulls back.
0.433 zoom is 1.35x linear scale, which is exactly the tightness in the crops.
Copy don't recreate: I recreated their fit instead of copying their construction.

useMapInstance gains a bounds/fitBoundsOptions pass-through, inert unless a
caller passes it, for the deep-link case -- the only case Mapbox can honour,
since the dashboard mounts its one map before the fetch resolves. The normal
case goes through fitTheirWay(), which asks the engine the same question their
constructor asks: setProjection('mercator'), cameraForBounds, restore the globe
in a `finally` so a throw cannot leave the map flat, then easeTo. Asking the
engine beats reimplementing a Web Mercator fit that would drift from Mapbox's
own padding and worldSize semantics at the next upgrade.

Proof, three independent oracles: the camera moved 2.777 -> 3.210 with the
projection still 'globe' afterwards; the like-for-like tile oracle now reads
ours z3 / 8 tiles / [[-45,-66.51],[90,40.98]] against theirs z3 / 8 tiles /
[[-45,-66.51],[90,40.98]], the same tiles at 1.00x; and the regenerated crops,
looked at, put the same labels and pins on the same pixels.

Two oracles of mine were wrong and are fixed here too. The tile-extent probe
compared a z2-quantized extent against a z3 one and reported a "1.33x framing
gap" twice -- 180/135 is four tiles at z2 against three at z3 of the same box,
and our camera's own visible box was 135.44 deg against their 135 deg of tiles.
It now refuses to compare extents across levels and reports the level instead,
which is the real signal. The framing probe asserted our zoom equalled the
GLOBE's cameraForBounds answer, so it would have failed the correct clone; it
now compares against the mercator-resolved camera and checks that asking the
mercator question left the map on the globe.
2026-09-24 22:31:20 +00:00
doxa-maps-88655c70
4b4cb30eb8 Rank the translation priority by the place a team is sent
"Can hear it, no text" is a list of 2,556 groups. A team is not sent to
a group, it is sent to a country, and nothing aggregated that set —
the existing country table counts the groups with NOTHING, which is a
different population and does not contain one of these rows, because
none of them is tier 0.

154 countries, with two columns that disagree on purpose. Nigeria and
the DRC lead on LANGUAGES, 221 and 103, which is the translation
workload. China and Pakistan lead on PEOPLE, 218 and 195 million,
across far fewer languages. Which number matters depends on whether
you are funding translators or broadcasters, so both are shown and
neither is ranked for the reader.

The aggregate reconciles with its source exactly — 2,556 groups and
1,329,044,699 people both ways — and the basis travels as a column, so
a CSV that outlives this page still says "no text found in the pools
checked" rather than "no text exists".

Built without a browser: the box was low on memory this tick and a
gate that needs chromium was the wrong thing to add to it.
2026-09-24 22:29:38 +00:00
doxa-maps-88655c70
d5fff78ccd upg-100: ask the registry for the stylesheet URL, and let the gate hear mapbox
Some checks are pending
build-gate / build (push) Waiting to run
A map inside a SHADOW ROOT has to inject its own copy of mapbox-gl.css, because
a document stylesheet does not cross a shadow boundary. This profile did that
by writing the URL out a second time, and the second copy drifted: it pinned
v3.3.0 while every production loader -- generate-tree's baked <script>,
useMapInstance, generate-gallery -- had moved to v3.24.0. So the map ran new JS
against old CSS.

MEASURED on the leaf: mapboxgl.version 3.24.0, the only mapbox stylesheet
reaching the shadow root v3.3.0, and mapbox-gl emitting its own "missing CSS
declarations" warning. The file's own header already says the engine loader is
imported rather than re-declared "because a second copy of one fact drifts".
The stylesheet URL was that second copy. It now comes from the same registry
via engineStylesheetUrl().

★ THE GATE COULD NOT SEE ANY OF IT, which is the more useful half of this
commit. verify-map-paint asserts a live GL context, zero console ERRORS, and
that mapbox emitted attribution -- all true of a map whose stylesheet never
applied, since the basemap paints fine while the chrome around it does not.
Mapbox itself says so, but as a WARNING, and the listener filtered to
type()==='error'. It now listens for _detectMissingCSS's verdict and fails on
it.

MUTATION-TESTED BOTH WAYS on the same leaf: restore v3.3.0 -> FAIL, exit 1;
restore the registry lookup -> PASS, exit 0.

Two things I had wrong on the way, recorded so the next reader does not repeat
them. I first asserted position:absolute on .mapboxgl-canvas-container -- but
v3.24.0 declares only cursor and touch-action there, so static is its CORRECT
value and that assertion would have failed every healthy map. I also read
"0 .mapboxgl-marker elements" as missing pins; mapbox draws pins as GL geometry
in the canvas, so zero is expected. A screenshot showed the polygons, the
region colours and hundreds of pins all rendering. Both were properties I
assumed rather than verified, and the canary's background-color is the one
mapbox actually defines -- it is the test _detectMissingCSS uses.

The canary alone would still have been a false green here: that rule is
identical in v3.3.0 and v3.24.0, so it is salmon under both. It catches an
ABSENT stylesheet; the warning catches a MISMATCHED one. The gate asserts both.
2026-09-24 22:27:52 +00:00
doxa-maps-88655c70
cc3ab8b822 Check the shipped answer, not the code that wrote it
The same drift has now appeared six times, each spelled differently:
negated fields on a line naming "none", a positive hasAny over a
mapped row, a predicate inside a build script, and — found this tick —
a pair of columns no pass had ever touched. A source-level pattern has
to guess how the next one will be written.

Clause N does not guess. It compares the shipped ANSWER against the
pin tier, so it catches the seventh however it is phrased. Measured
when written: people-group-resources.any_resource_found disagreed with
the tier on 194 rows, and its no_resource_found pair contradicted
ITSELF on 7 more, because neither column was in the refresh at all.
Both now move together, 195 and 202 corrected, and the table agrees
with the tier on all 2,103 judgeable rows.

A code with no pin is SKIPPED, not guessed. The tier lives on pins, so
an unpinned code cannot be judged this way — judging it anyway
reported ajp as a defect on the first run, when the row was right and
the check was wrong. That is the failure this repo keeps meeting, so
it is written into the clause rather than left as a comment.

Also corrected there: the table's own note still told readers GRN
answers 403 to every request and had never been searched.
2026-09-24 22:24:24 +00:00
doxa-maps-88655c70
337a1a863b Tell the curated tables that GRN and Scripture Earth exist
The legacy reconciler reported "nothing to change" and was confidently
wrong: its hasAny predicate never learned the two newest pools, so 122
curated rows — 8.9 million people — still carried heart_has_any=false
for groups the pin index can now reach. A reconciler that cannot see a
source is not reconciling, it is confirming.

Corrected exactly the 122 predicted, and the derived columns moved
with them: status 122, opportunity 93, lbc_carries_resource 69,
aligned 59, gap 29. Gap moves least because it means "nothing in the
heart language AND no in-country bridge either" — most of these have a
bridge, so they were already opportunities rather than gaps, and that
documented meaning is intact.

This was the fifth occurrence of one bug, and the gate written for it
had never looked at scripts — only .vue. Extending it to the build
scripts immediately found a sixth, in the Deaf gap table, which is
latent today because no sign language carries either field: GRN is
deliberately skipped for them and Scripture Earth has none. Fixed
while it is still cheap.

The rule now differs by file because the truth does: a surface must
READ the tier, while a build script COMPUTES it and cannot, so a
script must instead name every pool. Both halves verified against the
real pre-fix lines rather than synthetic ones.
2026-09-24 22:15:53 +00:00
doxa-maps-88655c70
90f0c37ddf Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:12:10 +00:00
doxa-maps-88655c70
de3b34beb5 Stop the country sheet calling 729 reached groups unreached
The country sheet split a country's groups into "with resources" and
"no resource found" using its own hand-listed field set, which never
learned about Scripture Earth or GRN. Measured: 729 people groups —
77.9 million people — were filed under "no resource found" while this
same map's pin sheet offered them a working link. Chad alone had 107
groups with something and showed fewer.

It now reads the tier the pin index computed, carried onto the mapped
row. Chad reports 150 groups, 107 with a resource, 43 without, which
is what the tier says.

This is the FOURTH time this bundle has re-derived that decision
somewhere, and the gate written for it missed this one: the gate
looked for negated c.* fields on a line naming "none", and this was a
positive predicate named hasAny over the mapped row.

Widening it took three attempts and the first two were worse than the
bug. "Three OR-ed resource fields" flags canHear(), which ORs media
precisely because it must exclude text. "Spans text and media" flags
`!!c.s || !!(c.au && /text/…)`, because c.au is text or audio
depending on a qualifier and a `|| ''` default inflates the operator
count. Both were confident false findings — the failure this repo has
now met in a forced token, a swallowed flag and a class extractor, and
a gate that cries wolf gets switched off.

So it asks one question it can actually answer: a predicate NAMED for
"has anything" is deciding the tier by definition and must read it.
Verified against the real pre-fix line, not a synthetic one.
2026-09-24 22:10:03 +00:00
doxa-maps-88655c70
77989089e0 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:05:31 +00:00
doxa-maps-88655c70
5a959d6b94 Two posters per row on the regions wall, with a control to change it
The Driver, verbatim: "i need two posters per row on the regiosn posters and
witha setitn got icnreat eh number of poswers per row".

The wall's own grid is repeat(auto-fill, minmax(210px, 1fr)), which lays five
across a region card. It now takes a `columns` count and an optional
`columnsControl`, both opt-in and both defaulting to the behaviour that shipped
— so country-posters and my-upg-100-list, which did not ask for this, keep the
auto-fill they have always had. Measured after the change: they still lay out
five across and neither grew a control.

The control is a <select>, not a stepper or a pair of chevrons. It is what
every reader already knows for "choose one of a few", it is keyboard- and
screen-reader-operable with no work, and it shows its current value without a
second element. Imitating the solved pattern rather than inventing one. Its
label is catalogued in eleven locales like everything else on this surface.

The probe measures GEOMETRY, not CSS. Asserting grid-template-columns would
pass on a rule that says repeat(2, …) while the cards actually wrapped three
across because one had a min-width the track could not honour — and a computed
style is exactly the kind of proxy that has fooled this suite five times today.
It counts how many cards share a rendered top coordinate, which is what a
reader sees: [2,2,2,2,2].

And it proves the control by USING it — set it to 3 and the rows become
[3,3,3,3,3]. A default that happens to be right would pass a one-shot check
with an inert knob.

One thing worth recording for the next person: the first attempt put the
explanatory comment inside the PosterWall tag's attribute list, and the Vue
compiler rejected it. An HTML comment belongs above the tag. This file had
already learned that once.
2026-09-24 22:04:28 +00:00
doxa-maps-88655c70
88a566f49d legend: the height cap now derives from the offset instead of repeating it
Some checks are pending
build-gate / build (push) Waiting to run
THE LEGEND OVERFLOWED THE BOTTOM OF THE MAP because two numbers that had to
agree were written out twice.

.stl-panel is capped at calc(100% - var(--stl-top) - 12px), but this profile
set `top` DIRECTLY with !important instead of setting --stl-top. So the panel
moved down to 58px while the cap kept subtracting 8px, and the two silently
disagreed by exactly the difference.

  MEASURED at 390px in a 420px-tall host: top 58px, max-height calc(100% - 20px),
  panel bottom 458 in a 420px map -- 38px PAST the map. Same 38px at 560px,
  because the error is the mismatch, not the map's size. After: -12px, the
  intended bottom margin, at 390/420, 390/560 and 1440/500 alike.

Two more copies of the same mistake went with it. The mobile rule paired
top:64px with max-height:calc(100vh - 72px) -- a constant unrelated to the
offset, AND the wrong box: 100vh measures the VIEWPORT while the panel is
positioned inside the MAP. Those agree only when the map fills the screen,
which is true of every standalone leaf and false the moment a host puts a
header above it. .fit-content had the same fixed 72px. Both now derive from
--stl-top, so they cannot drift apart again.

★ THIS IS INVISIBLE ON EVERY PUBLISHED LEAF. A standalone leaf gives the map
the whole viewport, and a panel that overflows a full-height map is still on
screen. It only appears when the host makes the map SHORTER than the viewport
-- which is how the element is actually embedded. Verify in a constrained
host, never on the bare leaf.

The a11y half of this work is NOT here. I fixed the same two nameless legend
controls locally while the remote was unreachable, then found on rebase that
they were already fixed on main -- better, with the row's own label
interpolated ("Expand Africa" rather than a generic string), and in one
key namespace instead of the second one I had introduced. Took main's version
whole and dropped my duplicate locale keys; only the CSS above is mine.
2026-09-24 22:00:52 +00:00
doxa-maps-88655c70
661133dd5c Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 21:57:12 +00:00
doxa-maps-88655c70
6958d0a187 Expose the fitted bounds, so a probe can ask the engine what it would choose
Two lines that assign the box we pass to fitBounds onto window, beside the
map instance that has been exposed there for the same reason since this
element was built. It lets a probe call cameraForBounds on the SAME box and
compare the answer with where the map actually sits — which is what separates
"the fit is wrong" from "something overrode the fit".

It earned itself immediately. Looking at our Africa map beside theirs, the
framing differs: theirs cuts the Sahel off the top and shows city labels, ours
shows the whole continent with margin. The engine's own answer for our box at
our frame is zoom 2.777 and the map sits at 2.777, so fitBounds is correct; the
dynamic world-fill minZoom — the best suspect, whose own comment records it
pinning this dashboard at ~1.45 once — measures 1.229, well below the actual
zoom, so it is not overriding anything either. Both cleared by measurement
rather than by argument, which leaves their bounding box, and their map is a
bundled ESM import with no global to read it from.

Recorded as an open, measured difference. Everything about how the map is drawn
matches — projection, style, highlight, pin colours, legend, controls,
attribution, and Botswana and Lesotho unhighlighted on both sides because
neither is among the 41 countries the Africa rollup returns. What it is pointed
at does not.
2026-09-24 21:56:43 +00:00
doxa-maps-88655c70
13a1225182 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 21:51:30 +00:00
doxa-maps-88655c70
f63d75ed80 Plugin studio: an Explorer tree for the rail, imitating VS Code
Some checks are pending
build-gate / build (push) Waiting to run
The studio's rail was hand-rolled vertical tabs stretched into a tree.
It is now an Explorer tree modelled on the VS Code Explorer, keyboarded
per the WAI-ARIA tree pattern:

- CAP 5 -> CAP 1 sections with sticky headers; one-line rows with the
  name and a muted element tag; twisty on the left; indent guides.
- Composites fold; their parts hang beneath, one level at a time, each
  part opening the plugin's one canonical entry.
- A filter box on top shows matches together with the path to them.
- One view action, Collapse all.
- Auto-reveal: opening a plugin by click, URL hash or remembered tab
  scrolls its row into view and opens its section.
- Arrow keys walk the tree, Right/Left open, close and move between
  parent and child, Enter opens, a letter jumps to the next name.
- The slim rail becomes an activity bar with one icon per section.
- Fold state is remembered per page.

The tree lives in plugin-designs/plugin-studio/src/StudioTree.vue and
fills VerticalTabsLayout's new #rail slot, so the layout still owns the
collapse toggle, mobile drawer and slim strip. The alias, depth and
branch row fields added to the layout earlier today are removed again:
nothing else used them.

The studio host now has a default height (85vh, min 480px), as
doxa-maps-spa does. Pasting the published embed snippet into a plain
page mounted a complete studio 0px tall. A page's own height or
--plugin-studio-height still wins.

The studio gates that click tabs by text now also match tree rows.
Rebuilt the plugin-studio bundle and re-baked its SRI.
2026-09-24 21:40:48 +00:00
doxa-maps-88655c70
16de564fb4 Record the pools that were checked and rejected
Three audio-Scripture sources were investigated this tick and none is
shippable. Writing down WHY, beside the unchecked-pool list, so the
next agent does not spend a tick rediscovering it — each was reachable
and doctrinally sound, and the reason is always coverage or the join
key, never suspicion.

AudioTreasure publishes 9 languages and every one is major; zero
overlap with the gap. Davar is a ministry site rather than a
catalogue, with no per-language page and so no URL a worker could be
sent to. Talking Bibles is the interesting one: 177 real audio Bibles,
open robots.txt, but the player keys languages by an internal numeric
id and publishes no ISO code, so a join could only be by NAME — which
this map refuses, because names collide across unrelated languages. It
turned out not to matter: of the 81 whose names match a language we
carry, ZERO are in the gap, so even a perfect ISO join would have
added no coverage.

GRN is the contrast worth keeping next to them — equally name-shy at
first glance, but 5fish resolves by ISO 639-3 directly, which is why
it is in and these are not.
2026-09-24 21:40:28 +00:00
doxa-maps-88655c70
dfe49b4685 Correct two headers that still called the shared rollup a copy
Some checks are pending
build-gate / build (push) Waiting to run
Both tab components' file headers said the region grouping was a COPY of
DX-life's countries-meta.ts "not an import", one of them attributing it to the
other tab. That was true when it was written — their file is a .ts module with
a Nuxt-only COUNTRIES import that cannot resolve outside that app — and it
stopped being true when the file was ported to library/utils/countries-meta.js.
Both tabs import summarizeRegions and PEOPLE_GROUP_LIST_FIELDS from there now,
and grep finds exactly one definition in the repo.

Worth the edit because a comment that still describes the old shape is worse
than no comment: the next reader would have duplicated the rollup a third time
on its authority, and one of the headers told them to copy it from the other
tab. "Kept field-for-field identical so both tabs roll up the same way" is also
no longer a discipline anyone has to maintain — it is the same function.
2026-09-24 21:30:27 +00:00
doxa-maps-88655c70
558786ddf7 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 21:26:47 +00:00
doxa-maps-88655c70
c4c60a4fd1 The font base must be absolute: @font-face is document-scoped
Some checks are pending
build-gate / build (push) Waiting to run
On any third-party page the whole clone rendered in a fallback face, and this
is why. useDocumentFont injects a <style> into the HOST document, so a RELATIVE
src resolves against the HOST PAGE rather than against this bundle. The leaf's
generated config says "../../assets" and the font loader was handed that value
raw. On our own leaf it is invisible — the leaf really is two directories deep,
so it resolves to /doxa-maps/assets and works. On a partner page it resolves
against the partner's URL, the file is not there, and every face fails.

resolveAssetsBase exists for exactly this. Its own header documents this bug
class in these words: "'./assets' or '../../assets' is the ambiguous input whose
meaning depends on the host, which is the whole problem." The poster previews
next door have used it all along. The font loader was the last consumer still
taking the raw value, and fonts are the one consumer where it cannot be merely
safer, because the style lands in a document this element does not own.

TWO WRONG HYPOTHESES DIED FIRST, and both are worth keeping.

A missing Access-Control-Allow-Origin. Plausible: @font-face is
document-scoped, the woff2 is cross-origin, and a font fetch is a CORS fetch. I
built a proxy that forwards the same bytes and adds only that header so the
claim would stand or fall on a pair — and with the header it still failed.
Then I checked what the asset host actually sends, which I should have done
first: serve-strict already answers
    content-type: font/woff2
    access-control-allow-origin: *
So the header was never missing, and the proxy was proving nothing about a
condition that was already satisfied.

A check made too early. document.fonts.check answers "can I render with this
right now", and fonts load lazily, so the probe had to await
document.fonts.ready — the suite's older diff has done that since the day it
was written. Fixed, and still failing.

What settled it was asking the document what it actually held instead of
proposing a third cause: every FontFace on the partner page read status
"error" while the responses were 200. A 200 that is then discarded is not a
network problem and not a timing problem — it is the wrong URL, fetched
successfully from somewhere that has no font at it.

The embed probe now records each font response's ORIGIN and its
access-control-allow-origin header, because "200 bebas-neue-latin.woff2" does
not say which host served it, and in a proxy phase that is the entire question.
A log that cannot distinguish the proxy from the bare host cannot test a claim
about the proxy.
2026-09-24 21:20:34 +00:00
doxa-maps-88655c70
9512c54239 Make the translation-priority list a table you can export
The map could select the groups that can already hear the gospel but
have no text Scripture. A filter cannot be sorted, exported, or handed
to a committee, and this is precisely the list that gets handed to a
committee. None of these groups appears in "Where Nothing Was Found"
either, because none of them is tier 0 — it is the opposite question.

2,355 groups across 1,224 languages, largest first, with CSV and JSON
export. Each row names what the group DOES have and links to it, so it
is actionable without the map: Western Punjabi, 97 million, "audio
Scripture, the JESUS Film". The pools checked and the pools NOT checked
are columns on every row, because an exported spreadsheet strips the
caveat first and the caveat is the part that keeps this honest.

Fixing the map to agree exposed a real defect in the filter shipped
last tick: it counted 49 SIGN languages among its 2,289. "Can hear the
gospel" is the wrong sentence about a Deaf people group, and recorded
audio is neither what they lack nor what they need — this map already
suppresses the audio chip and the GRN attach for them for that reason.
Both surfaces now exclude them and both report 2,355.
2026-09-24 21:14:21 +00:00
doxa-maps-88655c70
bb589d584e Vendor Oswald, because Bebas Neue cannot render Russian at all
Some checks are pending
build-gate / build (push) Waiting to run
Their heading face has no Cyrillic glyphs. That is why base/_globals.scss:35-44
switches every h1/h2/h3 to --font-family-cyrillic under [lang="ru-RU"], with
letter-spacing -0.02em — without it a Russian heading leaves the family
entirely and lands on whatever condensed system face the browser picks. We ship
a `ru` locale and had neither the rule nor the font, so that is exactly what
every Russian heading in this clone was doing.

Four subsets vendored, Google's own unicode-range values carried verbatim so
the browser fetches only what a heading needs: cyrillic 6700B and cyrillic-ext
7688B are the point, latin 12248B and latin-ext 11624B come too because a
Russian heading routinely contains Latin characters — a country name, a figure,
an ampersand — and a Cyrillic-only face drops those back to the fallback
mid-word, which looks worse than not switching. Each file verified to begin
with the wOF2 magic after download, because a fetch that returns an HTML error
page also "succeeds" and a font that is really a 404 fails at render time with
no network error left to find.

Two things the rule needed beyond the font. [lang] cannot cross a shadow
boundary, so the attribute has to be INSIDE the root for a selector in the
root's own stylesheet to see it — our roots now carry :lang for the same reason
they already carried :dir, and it is correct HTML besides. And the selector
accepts both `ru` and `ru-RU`: their site tags the document ru-RU while our
i18n locale is `ru`, and a host page embedding this element may set either, so
a selector matching only one would work on their site and fail on ours.

Only the headings, as theirs. Their .region-stat__value is a <p>, so their
ru-RU rule does not touch it and it keeps the heading family even in Russian —
switching everything would be wrong in a way no Russian reader would report and
no English reader could see.

_probe-cyrillic-headings.mjs checks the mechanism and the result separately, so
a rule that is present but inert cannot pass: the attribute is on the root, the
Oswald subset returned 200, the face is usable once document.fonts.ready
resolves, the h1 and h2 switched, the stat value did not, and English is
untouched in both family and letter-spacing. It awaits fonts.ready before
asking about a font — not doing so is what made a sibling probe report a
cross-origin font failure that did not exist.
2026-09-24 21:05:12 +00:00
doxa-maps-88655c70
510b6edbf8 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 20:55:42 +00:00
doxa-maps-88655c70
5869fed6a5 Hold the total row's height, and translate the three strings my own scan's bound hid
Two fixes and a correction, all three found by turning other agents' lessons on
my own already-green work.

THE TOTAL ROW DISAPPEARED WHILE LOADING. Theirs never removes it: it swaps the
count for a visibility:hidden placeholder so the row holds its line and the
results below do not jump down when the number arrives. Ours v-if-ed the whole
row away, so the layout shifted on every load. It surfaced only after the
class-coverage sweep's extractor was widened to read the whole file instead of
just the template — the class is in markup my template scan reached, but the
sweep had never seen the NAME, so nobody had asked what it was for. Widening it
took that sweep from 22 to 58 classes seen on their UupgsList and from 103 to
134 checked overall, inside a run I had already reported as complete. Of the
ten new names, eight are their useSelectCard variant this clone does not render
and each is recorded with that reason and its line number; the ninth,
line-height-tight, was already correct because we ported the resolved value
rather than the class.

AND A CORRECTION TO MY OWN LOCALISATION CLAIM. I reported zero hardcoded
English in the regions components. That was wrong, and my own instrument is
why: the scan bounded matches at sixty characters, and three failure messages
are longer than that —

    Region data could not be loaded. This is a connection problem, not a filter.
    People groups could not be loaded. This is a connection problem, not a filter.
    The map could not start. This is a map-token or connection problem, not a
    filter — the figures above are unaffected.

Re-scanned with no upper bound and across line breaks, translated into all
eleven locales, wired through t(). A scan with a bound can only find what fits
the bound, and I quoted its zero as though it covered everything.

Verified against live after all of it: the region card diff is now 37 of 37
identical (it grew by the icon's own margin, which nothing had read), the
bullet 11 of 11 exact, the region page 107 of 107, the country page 32 of 32.
2026-09-24 20:55:19 +00:00
doxa-maps-88655c70
795b357ab8 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-24 20:52:29 +00:00
doxa-maps-88655c70
938f58c5f7 Rank the translation-priority set, and sweep GRN batch four
The map could select the 2,289 groups that can hear the gospel but
have no text Scripture; it could not say which of them to weigh first,
and 2,289 dots cannot be read top-down. The gap sheet gains a fifth
tab that ranks them by population and, on each row, names what the
group DOES have — "has audio Scripture, the JESUS Film — no text
found" — so a reader can see whether they are being sent to a
recording or a film before they go.

The tab describes a different population from the other four, so it
does not inherit their sentences: the nothing-found opener, the audio
caveat and the macrolanguage note are all scoped away from it. Its own
paragraph states the bound twice, because the top rows are Western
Punjabi at 97 million and Wu Chinese at 92.5 million, and a reader
skimming those numbers must not come away thinking no text exists in
any variety they speak.

GRN batch four: 170 more codes, 90 with gospel audio. 343 codes now
carry a Words of Life link and tier 0 falls 2,488 to 2,373 — from
3,457 when this work began. 689 of the gap's codes are decided, and
provenance stays exact at 588 claiming GRN, 588 decided, none
decided-but-unnamed.
2026-09-24 20:50:27 +00:00
doxa-maps-88655c70
71ff1d3b02 Plugin studio: call the cap-5 shell "Research Gallery"
Some checks are pending
build-gate / build (push) Waiting to run
The studio listed the app shell by its profile id, doxa-maps-spa. It now
shows the Driver's name for it, Research Gallery, in the rail, in every
composition tree and in the title bar. Display only: the row key, the URL
hash, the remembered tab and the <doxa-maps-spa> tag under the name are
unchanged.

Rows now carry the real profile id separately from the label, because the
mount handed the label to the element as its profile; a display name
there would have mounted the shell with a profile it does not have.
2026-09-24 20:49:42 +00:00
doxa-maps-88655c70
8bb7f47e53 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 20:48:39 +00:00
doxa-maps-88655c70
fac00c00bf Adopt the research map's region colours, and pin the one unit that wasn't px
THE DRIVER RULED ON THE PALETTE. He compared the two screens himself: "when i
look on teh color ffor th regiosn tab of the simpel research map i don tsee the
you using htes colors on the reigosn dashbarod hwen you shoudl". Two surfaces
disagreeing about what Africa's colour means is a defect whichever palette is
prettier, and he chose which one wins. library/colors/wagf-region.js now
carries doxa-research-map/src/colors/doxa-region.js's values.

The blast radius turned out to be nil, and my earlier escalation overstated it:
wagf-region.js is imported by exactly two files, both mine. I had reported it as
needing a cross-agent decision on the grounds that seven surfaces would be
repainted. They would not have been — nobody else imports it.

WHAT THE CHANGE COST, measured with the same validator and surface the previous
palette was stepped against:

    lightness band   FAIL   #1ee05f .793 · #FFEB3B .928 · #00e0bd .810 sit
                            above the .43–.77 band
    chroma floor     PASS   all 7 >= .1
    CVD separation   PASS   worst adjacent #FFEB3B/#1ee05f dE 8.7 protan
    normal vision    PASS   worst adjacent dE 21.0
    contrast         WARN   under 3:1 vs surface: #1ee05f 1.72 · #FFEB3B 1.19
                            · #00e0bd 1.65 · #ff7f11 2.47

The palette this replaced passed all five. The contrast warning is survivable
here for a reason and not by dismissal: it obligates relief through a secondary
encoding, and both surfaces already carry one — the chooser renders a legend of
labelled chips in these colours, the dashboard renders the same seven as a
table. Identity never rests on hue alone. The lightness failure has no such
relief: three of these read faintly as pins on a light basemap. Darkening those
three by about 10% L would enter the band with the hue still recognisable, and
that is the Driver's call, so it is asked rather than done quietly.

_chk-region-palette-agrees.mjs is what stops the two drifting apart again. It
compares them value by value and names both files on a disagreement. Ours is
imported; theirs is parsed, because doxa-region.js imports a Vite alias and node
refuses the module — so the parse asserts its own shape first, requiring the
eight entries that file declares, and fails rather than comparing against an
empty object if anyone refactors it to computed values. Negative-tested: flip
one hex and it names that region and both paths.

AND ONE UNIT IN THIS PORT WAS NOT PX. The chevron's gap was a literal 1rem,
ported from their `.with-icon .icon.right { margin-inline-start: 1rem }` — 10px
at their 10px root. `rem` resolves against the document root wherever the
element lives, which is the whole reason dxlife-tokens.js resolves their scale
to px, so ours rendered 16px on our own leaf and would render 10px by accident
on a host with the common 62.5% reset. Pinned to 10px. No diff caught it: the
list diff's link spec reads colour, decoration, weight, display and alignItems,
and never a margin. It reads the icon's margin now.
2026-09-24 20:48:27 +00:00
doxa-maps-88655c70
e3df4677d3 Plugin studio: fold every composite, so the rail starts as a plain list
Some checks are pending
build-gate / build (push) Waiting to run
With every composition tree open, the rail was too long to find your way
around. Each composite now carries a chevron and starts folded, so the
rail first reads as the 35 plugins by capability, each composite saying
how many parts it has. Open one and you see its direct parts; a part that
is itself a composite folds again, so doxa-maps-spa opens to its tabs and
each tab opens to what it is made of, one level at a time. Choosing a
composite also opens it; only the chevron closes it. The same child folds
independently wherever it appears.

VerticalTabsLayout gains two optional row fields: `branch` makes a row
foldable, `ancestors` hides a row until every branch above it is open.
Rows are wrapped so the chevron can sit beside the tab rather than
inside it. doxa-maps-spa sets neither field and renders unchanged.
2026-09-24 20:45:19 +00:00
doxa-maps-88655c70
edccc8ee05 Filter the map to the groups a translator would go to next
The pin filter could separate nothing / something / both, which are
tiers. It could not ask the question a translation team actually
starts from: who can already HEAR the gospel — a recording, the JESUS
Film, a Bible video, Words of Life — but has no text Scripture in
their language? That set cuts across all three tiers, so no tier
button could select it, and it is the largest actionable group on this
map: 2,289 people groups across 1,147 languages.

The claim is bounded in the button, the caption and the tooltip: no
text Scripture FOUND IN THE POOLS CHECKED, never a claim that none
exists. It matters here more than usual, because the largest entries
are Western Punjabi, Shanghainese and Egyptian Arabic — varieties
where a recording plainly exists and a text in that same variety is
genuinely contested.

The flag is baked into the pin features so the filter stays a GL-side
predicate like the others; recomputing it per toggle would re-upload
12,379 features to answer a yes/no already known at build time. The
button carries no colour swatch, because it names a question rather
than one of the three colours.

Measured in the browser: 12,379 pins rendered, 2,289 after the filter,
matching the offline count exactly.
2026-09-24 20:41:49 +00:00
doxa-maps-88655c70
1257ab575b Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 20:33:58 +00:00
doxa-maps-88655c70
e944132933 Fix an illegible active filter button, and the comment that defended it
Apply a filter on the country page and the button you just pressed went white
text on 5%-alpha blue over a white ground — an invisible label. Theirs is white
on #6d766e. We had copied their DEAD FALLBACK:

    background-color: var(--color-surface-brand-lighter, rgba(0,80,160,0.05));
    color: var(--color-white);

and --color-surface-brand-lighter is defined, at tokens/semantic/_colors.scss
:64, as $color-green-300. The fallback never applies on their site.

What kept it alive was a comment of mine sitting right above the rule. It said
their active state is white-on-white on purpose, that the token "is NOT DEFINED
in their token files", and that the white-on-white had been "MEASURED on their
live page". The first two are contradicted by a file in the read-only clone.
The third could not have been what it claimed: getComputedStyle returns the
specified colour and never the composite, so neither candidate can serialise as
white — the token is #6d766e, the fallback is rgba(0,80,160,0.05). The only
rule on that button that yields white is .input's var(--color-surface-white),
which is the inactive state. The measurement had read a button without
data-active set.

Measured properly, by clicking the Adopted filter on both sides: theirs is
background rgb(109,118,110), colour rgb(255,255,255). Ours now matches, and so
does Clear All — theirs is `.button compact link`, which blocks/_buttons.scss
:64-69 gives --button-text-color: var(--color-brand) = #3b463d, where ours had
used the body ink #2a332a. 30 of 30 properties identical to live, 0 differ.

That second one was found twice over, independently: by this diff, and by a
static sweep of their markup's classes that flagged `.link` as a rule our CSS
had no equivalent for at all. The sweep is worth reusing — list every class and
tag a component's markup uses, resolve each against their scoped block and
their global sheet, then check ours has something for it.

The probe exists because _probe-country-vs-live.mjs printed one line everybody
had read past: "NOT MEASURED (absent on both sides, so neither identical nor
different): chip". A filter chip only exists once a filter is applied. Absent
on both sides is not a pass. It is the second diff here that clicks something
before measuring, after the pin popup, and its own first run was a probe bug
worth recording: it waited thirty seconds for the three filter buttons and then
reported that Adopted could not be found. The panel is collapsed by default on
both sides and the buttons sit behind a v-if, so they did not exist yet — a
probe failure dressed as a product finding.
2026-09-24 20:33:03 +00:00
doxa-maps-88655c70
25e3edec8f upg-100: no mapbox stylesheet under maplibre, and assert the preview exists
Some checks are pending
build-gate / build (push) Waiting to run
Two things, both found by other agents' reports rather than by my own gates.

rega found upg-100-list.vue:1464 injecting mapbox-gl.css v3.3.0 into the shadow
root unconditionally, so a keyless maplibre embed still pulled a stylesheet from
api.mapbox.com. Token-free and harmless to render, but wrong-engine leakage on a
page whose point is having no Mapbox dependency. Gated on isMaplibre, which
already existed at line 119.

This commit is ATOMIC by design: the fix, the rebuilt bundle, and the deletion
of rega's MAPBOX_KNOWN exemption land together. Either half alone leaves a
window — delete first and main red-gates on its own known bug; fix first and a
stale exemption sits there hiding the next one.

Proven before deleting, because the exemption is keyed on a URL STRING while the
defect is a network REQUEST: the built bundle still contains the v3.3.0 literal
(the constant remains; the guard stops the fetch), so a source-grep gate would
have been misled. rega's gate measures requests, and it reported
"provider=maplibre made 0 request(s) to api.mapbox.com" in BOTH js and iframe
modes with no KNOWN line printed at all — i.e. the exemption was already dead
code. Re-proven after deletion: still 0 requests, still PASS.

The leaf-head preloads are explicitly NOT covered and the reason is recorded in
the gate: mapbox-gl.css v3.24.0 and mapbox-gl-geocoder v5.0.0 are a different
version and a different owner (generate-tree, bundler-1's lane), already tracked
separately.

Second: the Driver's report had two halves — "it is supposed to show the
previews" and the print options overlapping. I fixed and gated the overlap and
left the first unasserted. The over-preview check was written as `if (img)`,
which means a card with NO preview satisfies it perfectly: nothing can overlap
an image that is not there.

The gate now counts previews. Mutation-tested by aborting every previews/**
request: the product degrades well — all 27 cards swap to a "poster not
generated yet" placeholder with no broken-image icons and no <img> at all, which
is precisely why an image-health check cannot see it. So the fallback itself is
asserted, and all-cards-placeholder now fails.

A first version of that assertion was wrong and the reason is recorded in the
code: it called 18 lazy-loaded previews broken. Every img carries loading=lazy,
the first unpainted one sat 3926px down an 844px viewport, and no HTTP response
failed. Deferred is not broken; it now judges only in-viewport images.
2026-09-24 20:28:45 +00:00
doxa-maps-88655c70
460ccb564b Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 20:26:59 +00:00
doxa-maps-88655c70
728d3cbaef Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 20:12:15 +00:00
doxa-maps-88655c70
29eaf3a62a Port their pin popup, and close the last English out of the regions clone
THE POPUP WAS A STUB. Click a pin on their region page and you get a card: the
people group's photo, the name, "country · primary ethnolinguistic group", a
definition list of Population / Religion / Intercessors-out-of-100, up to three
clamped lines of location description, and two buttons — Pray, which is the
point of the entire site, and Full Profile. Click one on ours and you got four
lines of hardcoded English with en-US number grouping in all eleven locales.

No diff here could ever have found it. Every vs-live probe reads the page at
rest, and a popup only exists after a click. So there is a new one, and the
popup now matches theirs on 37 of 37 measured properties with the same stat
labels, the same two actions, and their ?source=doxalife attribution on the
Pray link — which is invisible on screen and exactly what a re-implementation
drops silently.

Noticing it was not enough to fix it: the DATA was not on the page. Their
PIN_FIELDS asks for rop1, religion, location_description, image_url and
has_photo; ours asked for none of them, so a faithful popup would have rendered
empty rows. Five more columns on the same single request. Their row mapping
comes with it, including the has_photo gate on the image — a row can carry a
placeholder image_url and they only show it when the flag says there is a real
photo — as does their fuller escape, which also escapes both quote characters,
and that matters because these strings are interpolated into href and alt.

AND THAT FILE HAD NO t() AT ALL. Its prayer legend rendered four hardcoded
English strings on a page whose every other word is translated into eleven
languages — and three of those four were already in the catalogue, translated
for the Prayer Coverage card and hardcoded here anyway.

LOCALISATION, re-measured honestly. The old 97.4% was counting strings
identical to English as untranslated, which is wrong: es/fr/pt/ro share
"Total", and French shares "Population" and "Religion". Those are cognates. The
real question is coverage of their surfaces, so: every t() argument in
RegionDetail.vue, pages/regions/index.vue, CountryDetail.vue and CountryMap.vue
— 35 strings — checked against our catalogue. Two were missing, both
CountryMap's: "Outside this region" and "Pray". Added in all eleven, the eight
DX-life translates copied from their own common.json and it/ro/zh composed from
words already attested in those locales' own files rather than invented. Then
the reverse direction: a scan for capitalised text nodes outside {{ }} found
two of our own strings left in English — the dashboard's loading state and the
list's empty state, neither of which DX-life has an equivalent for because
their page is server-rendered. Both translated. The scan now returns zero in
all three components, and coverage of their surfaces is 35/35.

Verified alongside: all seven regions at 107 properties identical to live each
(not just Africa), mobile parity still 44/0 at 390 and 768 after the CSS
surgery, and first paint of the list measured at 3766ms with 1863KB of it a map
engine that leaf never calls — carded to the build machine, not worked around.
2026-09-24 20:11:44 +00:00