Commit graph

407 commits

Author SHA1 Message Date
doxa-maps-88655c70
c7942ae7f7 Merge commit '28b0e505b788e47e028287144bb8039c3a68fc25' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 02:50:22 +00:00
doxa-maps-88655c70
baa5e72e5c The title and strapline sat 10px tight, because we recreated a reset as a class
Their base/_reset.scss zeroes margins with
`body,h1,h2,h3,h4,p,figure,blockquote,dl,dd { margin: 0 }` -- a bare ELEMENT
selector, specificity 0-0-1. Their `.stack > * + *` is 0-1-0, so the stack wins
and their heading gets its 10px.

We recreated that reset as class rules, `.rd-title{margin:0}` and
`.rd-summary{margin:0}`. Those are ALSO 0-1-0, because `*` contributes nothing
to specificity, so `.rd-head > * + *` and `.rd-title` tied and source order
decided it -- the later class rule won and the stack margin was discarded.

Measured on live /regions/africa, their head is a(0px), h1(10px), p(10px).
Ours was h1(0px), p(0px): the title and strapline sat flush against the back
link, 10px tight, on every region and country page. Porting the reset as the
element selector it actually is, and deleting the two class-level margin:0
declarations, restores their cascade exactly -- both now read 10px.

Found by sweeping for the defect CLASS rather than the instance: for every
`> * + *` container in our sheets, any child that is not the first and still
computes margin-top 0 has lost the same race. That turned up three candidates,
and measuring THEIR page settled the third -- their .region-goal is 0px too, so
our .rd-goal matching at 0px was never a defect. A heuristic found the
suspects; only the live measurement could tell a defect from a faithful copy.

This is the fourth of their global rules that the shadow boundary changed the
behaviour of, after box-sizing, the list bullet and prefers-reduced-motion --
but the first where the rule DID reach us and the port altered its specificity.
Copying a global element rule as a scoped class rule is not a faithful port even
when the declaration is identical.

reset-vs-live now reports 0 differences; region-vs-live 4/4, list-vs-live 21/21,
table rows 4/4, SRI passes.
2026-09-25 02:50:05 +00:00
doxa-maps-88655c70
43e3102f47 Check 5fish links too, and say why YouVersion stays out
The rot sampler now covers 5fish by where its redirect LANDS — a published code
goes to its numbered language page, one they do not carry is swept to /regions —
the same rule the verifier uses, with both arms proved in band. Six links a run,
ten seconds apart, because their robots.txt asks for it. That leaves 94 of 8,510
shipped links outside every re-check, down from 5,760 this morning.

YouVersion is the largest catalogue the brief names and it is now recorded as
rejected, with the measurement: nodejs.bible.com serves it without a key but its
robots.txt is Disallow: / , www.bible.com answers even robots.txt with a bot
wall, and the unauthenticated slice returns 2 English versions against the dozens
that exist — so taking it would be both discourteous and wrong. It is named in
the unchecked list on 5,413 codes instead of being silently absent, because a
reader deciding that a group has nothing is entitled to know which pools nobody
searched.
2026-09-25 02:44:37 +00:00
doxa-maps-88655c70
2253d893b7 Finish the GRN sweep: every reachable pool has now been checked
Batch fourteen closed it — 2,347 language codes decided, 1,022 carrying Words
of Life. Forty groups leave the gap: 1,653 with nothing becomes 1,613, and the
gap breakdown drops from four situations to three, because "GRN not yet swept"
no longer describes anybody. What is left is 1,429 groups where every pool this
build can reach has been searched, 183 sign languages behind key-gated sources,
and one group with no language code at all.

Two things the sweep exposed. 5fish answers 200 for any path, so its 982 links
were being verified by a status that means nothing there; they are now judged by
where the redirect LANDS — a real code goes to its numbered language page, one
they do not carry is swept to /regions — with both arms proved in band before
any link is classified. And country-gap reads a file that another script
refreshes from the pins, so running them out of order publishes the previous
build's numbers: it said 1,653 while the map said 1,613. It now refuses to write
a table that disagrees with the index the map draws.
2026-09-25 02:44:37 +00:00
doxa-maps-88655c70
d0ae01e0c6 Honour prefers-reduced-motion: their reset never reached our shadow roots
base/_reset.scss:66-79 suppresses motion site-wide with
`@media (prefers-reduced-motion: reduce) { *, *::before, *::after {
animation-duration:.01ms!important; transition-duration:.01ms!important; ... } }`
-- a DOCUMENT-level universal selector, so it styles nothing inside a shadow
root. The same boundary that swallowed their box-sizing reset.

It matters because we had ported their animation faithfully: their
RegionDetail.vue:209 and our .rd-seg / .rd-bar__slider both carry
`transition: width 1s ease-out`. Measured with emulateMedia on the segment that
actually carries the transition, in both states, because a value that is 1s in
BOTH states is the whole claim:

  theirs  1s -> 1e-05s under reduce
  ours    1s -> 1s

So a vestibular visitor got a full one-second sliding bar from us that their own
site suppresses -- a clone difference and an accessibility regression in one.
After porting the block into .rd-root and .rl-root, ours reads 1s -> 1e-05s,
matching theirs, and the unreduced case is still 1s.

Their `html:focus-within { scroll-behavior: auto }` half is deliberately NOT
ported: a shadow root cannot style the host document's html element, and the
leaf page owns that.

Found by finishing an audit I had already called complete. The previous pass
enumerated 21 conditional rules but only actually checked the [lang] and [dir]
ones -- the four :has() rules and the @media set were reasoned about, not
measured. Of those, @media print turns out to be moot for us (it hides header,
footer and nav, which an embed does not have, plus orphans/widows on p, blocked
by the same boundary and marginal), and this one was a real defect.

list-vs-live still 21 of 21, chooser 15 of 15, parity and SRI pass.
2026-09-25 02:37:08 +00:00
doxa-maps-88655c70
2fa09852ab Let the gap breakdown survive the sweep finishing
The breakdown hardcoded "Four different situations share it" and gave
every line an unconditional row. The GRN sweep has 302 codes left, so
within an hour or two that bucket empties — and the sheet would have
rendered "· 0 (0) — Global Recordings Network has not been swept for
them yet" beside a sentence insisting there were four. Two falsehoods
at once: the count, and the implication that some remain unswept.

Each line is now conditional and the opening counts the situations
that actually have rows.

Tested by simulating completion in the data the page really loads:
with every code marked swept it renders "Three different situations",
the GRN line is gone, and the real translation gap absorbs them,
1,220 → 1,531. Restored afterwards and both asset copies verified
identical and back to 311.

Worth recording from that test: the page reads public/assets/tables/,
NOT public/doxa-maps/assets/tables/. Editing the second one changes
nothing, which cost two browser runs to discover — the first
"simulation" verified the file it edited and still watched the old
number render, which looked like a broken guard rather than a wrong
path.
2026-09-25 01:37:30 +00:00
doxa-maps-88655c70
f4595325fa Stop the charts listing GRN as both checked and unchecked
Two defects, both found by looking at the rendered page rather than
the data behind it.

The footer read "Pools CHECKED: … grn. Pools NOT checked: …, grn (not
yet swept)." GRN is swept a batch at a time, so some codes carry it as
checked and the rest as unchecked, and unioning across the corpus
printed it on both sides. To a reader that is a flat contradiction,
and it is the kind that quietly discredits every other number on the
page. It is neither fully checked nor untouched, so it now says so
once: "grn (partly — the sweep is still running)", on the checked side
only.

And three of the eleven bars — Audio, Gospel presentation,
Discipleship training — are drawn in a green the legend never named,
so a reader had no key for them at all. The legend now carries it.

Neither was visible in the markup or the numbers. The pool strings are
assembled from per-code provenance, and the legend was three entries
that all looked reasonable until you counted the colours actually on
screen against the colours explained.
2026-09-25 01:14:48 +00:00
doxa-maps-88655c70
1465e3a7bb 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 01:02:29 +00:00
doxa-maps-88655c70
a87c80c9ad upg-100 legend: drop the People Group tab, which could never render anything
Some checks are pending
build-gate / build (push) Waiting to run
The Driver said "it doesn't need to have a People Group tab inside the legend".
The reason is stronger than preference: clicking it showed nothing at all.

SemanticTreeLegend renders tab N as collectAtDepth(nodes, N) -- a tab's INDEX
IS a tree depth. This legend's tree is exactly TWO levels: 9 regions -> 28
lists. MEASURED by expanding every caret on the Region tab: 37 rows, 9 at
indent 0 and 28 at indent 1, deepest level 1. There is no level 2, so tab index
2 resolved to an empty set and the panel rendered its "No data at this level"
empty state with the tab ACTIVE and selected.

★ THIS IS WHY PART (c) WAS GATED AND NO LONGER IS. The card asks "what is the
legend's intended tree once Region/Zone/People Group are removed" -- a real
question, still the Driver's. But removing a tab that is STRUCTURALLY INCAPABLE
of content needs no ruling about intent. It is a defect, not a design choice.

AFTER: two tabs, both with content -- Region 9 rows (expandable to the 28) and
UPG-100 Lists 28 rows. Clicking every remaining tab and info button, 6 elements
in total, produces ZERO empty states.

⚠ THE i18n KEYS ARE DELIBERATELY LEFT IN PLACE. 22 locale files carry
upg100Group; deleting them costs 11 languages of re-translation if a third
level ever arrives, while an unused key costs nothing at runtime and the parity
gates assert presence rather than usage. Removing them would be the riskier
edit, not the tidier one.

⚠ AND A THIRD TAB IS NOT THE MECHANISM if people-group rows are ever wanted.
The tree would need a third level (list -> group); re-adding the tab without
deepening the data reproduces exactly this empty state.

Verified: check:legend-export-reachable PASS, check:legend-matches-source PASS,
check:a11y-button-names PASS, and check:legend-info-localized PASS on this leaf
-- 4 info popups, all differing from English in es/ar/zh including RTL Arabic,
0 blank.
2026-09-25 00:59:50 +00:00
doxa-maps-88655c70
b18679e2e9 Stop telling readers the audio pools were never searched
A footnote under every country sheet still said "No resource found
means not present in eBible or the JESUS Film catalogue; audio pools
are key-blocked and unchecked". Both halves stopped being true long
ago: eight pools are checked, not two, and audio is searched at Faith
Comes By Hearing and at GRN — which needed no key at all.

That is not a harmless leftover. It understates what the map knows, to
a reader deciding whether "nothing found" means anything, and it is
the single caveat most likely to be quoted back.

The file's own header was wrong in three ways and is corrected with
it: the band is not "no scripture and no film" but no resource of ANY
kind, since it is computed from the tier; the pools are named in full
rather than as "eBible + Arclight"; and GRN is not key-blocked.

Verified in the browser rather than in the markup — the footnote is
inside the country sheet, which only appears after a pin and a back
button, so a grep would not have told me it renders.
2026-09-25 00:54:09 +00:00
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