Commit graph

958 commits

Author SHA1 Message Date
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
0d394e9924 Sweep GRN batch six: 701 groups reachable by recording alone
170 more gap codes probed, 84 with gospel audio. 511 codes now carry a
Words of Life link, and 701 people groups whose ONLY resource in any
pool is that recording — 77.6 million people — can hear the gospel in
their own language.

Tier 0 falls 2,270 to 2,172. It was 3,457 when this work began, so the
gap is down by more than a third, and the translation-priority table
grows in step: 2,556 groups now, because every code GRN reaches moves
a group out of "nothing found" and into "can hear it, no text".

1,027 of the gap's codes are decided. Provenance stays exact through
the change — 915 codes name GRN as checked, 915 have been decided,
none decided goes unnamed — and all new URLs were verified before
shipping.
2026-09-24 22:04:56 +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
66519288dc README: UPG-100 is the mobile app, shot at its owner's "ready at 88a566f"
Some checks are pending
build-gate / build (push) Waiting to run
Deployment method 2 now shows the UPG-100 prayer lists and posters at phone
size: one poster per row, print options wrapping in full. Shot from the
rebuilt bundle at 88a566f, not from source. The capability-4 shots are
retaken from the same build, so they show the print-option fix too.
2026-09-24 22:03:50 +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
608ef3dd56 poster cards: print options wrap to full words instead of an ellipsis
The Driver rejected the ellipsis by name: the print options must LINE WRAP so
every label stays readable, because a button that no longer says what it does
has not been fixed.

Three measurements drove the CSS:

  - a <fieldset> takes min-width:min-content from the UA stylesheet, which
    OVERRIDES width and max-width. .pfs read width:82% while computing 431px
    inside a 212px card. min-width:0 goes on the fieldset ITSELF, not only on
    its children.
  - the row is anchored at its TOP (top:100%;margin-top:-14px), not its bottom.
    At bottom:-14px every extra line grew UPWARD over the preview image, which
    is how the first overflow fix broke the card it was meant to repair.
  - wrapping took the row from 35px to 78px, so the grid's row gap moves 64px
    to 88px. The old gap was tuned when the row was a single line.

No text-overflow anywhere in .pfs. min-width:0 alone had let three 76-79px
labels share 172px, showing 40px each.

The gate gains the assertion that would have caught the ellipsis: a control is
clipped when scrollWidth exceeds clientWidth. Mutation-tested -- restoring
text-overflow:ellipsis reproduces 81 of 108 clipped and exits 1. Without that
assertion the gate passed a wall whose buttons all read "Pra...".

Measured at 390/768/1024/1440/1920px: 0 clipped, 0 overflow, 0 over-preview,
0 neighbour intrusions, 108 controls.
2026-09-24 21:59:34 +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
bf7fc46dcd check:shots compares bundle content, not dates; retake the stale shots
Some checks are pending
build-gate / build (push) Waiting to run
A retake that came out byte-identical left the PNG unchanged, git dated it by
its old commit, and the gate called a correct, just-taken picture stale. No
clock can see that case. readme-shots now writes shot-bundles.json (shot ->
sha256 of the bundle it was taken from), and check:shots compares that hash
with the bundle built now; dates are only the fallback for unrecorded shots.

Negative-tested: one byte appended to the doxa-simple-map bundle fails exactly
the three cap2 shots; restoring it passes.

Also retook the eight shots a rebuild had made stale since e2beec9.
2026-09-24 21:54:08 +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
3cb182a783 Unescape the pipe in the prose line
Inside a code span a backslash renders literally, so the sentence read
"| tail" with a stray backslash. The escape inside the table is kept:
there a bare pipe would split the cell.
2026-09-24 21:51:11 +00:00
doxa-maps-88655c70
43e8d0e365 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed 2026-09-24 21:47:29 +00:00
doxa-maps-88655c70
b4dcf6fa6d Research Gallery: rebuild against f63d75e's VerticalTabsLayout rework
f63d75e (plugin studio's Explorer tree) rewrote library/layouts/
VerticalTabsLayout.vue, which is compiled into doxa-maps-spa.js, and did not
rebuild the gallery. This rebuilds it. check:gallery-embed and the maplibre
variant both pass, js 13 + iframe 13, with the new layout.
2026-09-24 21:47:26 +00:00
doxa-maps-88655c70
e2beec9a11 README: reshoot the Plugin Studio at f63d75e — the Explorer-tree rail
Some checks are pending
build-gate / build (push) Waiting to run
readme-shots now clicks the rail's first plugin row (.st-row[role=treeitem],
not a section heading), keeping .vtl-tab as a fallback. Without that, the walk
fell through to the nested gallery's own tabs.
2026-09-24 21:42:37 +00:00
doxa-maps-88655c70
561d9a42e0 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 21:41:30 +00:00
doxa-maps-88655c70
88e4fea550 Research Gallery: rebuild the bundle against the shared layout it compiles in
e3df467 changed library/layouts/VerticalTabsLayout.vue, which is compiled
into doxa-maps-spa.js, but did not rebuild the gallery. So the bundle on main
was stale: new source, old bytes. check:sri cannot see this, because it only
proves the leaf page and the bundle agree with each other.

Measured, not assumed. Rebuilding fd95518's source reproduces the landed
bundle byte-for-byte (sha256 2856a1e2…; the build is deterministic). So the
gallery landings themselves were current. After this rebuild the bundle
contains the new layout code (branchOpen: new 1, landed 0).
check:gallery-embed and its maplibre variant both pass, js 13 + iframe 13.
2026-09-24 21:41:24 +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
a9bad3ccf2 Merge main into bundler-2/work
# 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 21:39:35 +00:00
doxa-maps-88655c70
3cfe93c67e Rebuild the fleet so the translated hint actually reaches it
regions landed the pin hint's i18n — useI18n, messages.pinHint.tapDot in 11
locales, English fallback, pinHintLabel first. Thirteen bundles embed that
component and only ONE of them had been rebuilt, so twelve maps still shipped the
old hardcoded English literal in their served .js.

check:sri passed the whole time, and correctly: every leaf matched the bundle it
loads. A stale bundle and its stale leaf agree perfectly — consistency is not
freshness, and no gate in the chain compares either against source.

MEASURED after rebuilding the eleven that were stale (country-posters and
regions-dashboard were already current):
  es  Toca un punto de color: alguien todavía necesita oración
  fr  Touchez un point coloré — quelqu'un a encore besoin de prière
  ru  Нажмите на цветную точку — кому-то всё ещё нужна молитва
Every one of those was the English sentence before this commit.

The same rebuild carried the legend's a11y fix to the two bundles that had no
owner watching them. All four bundles that embed SemanticTreeLegend now carry
legend.a11y.collapseRow; on doxa-research-map the row carets read "Expand Asia
(Region)", "Expand Afro-Asiatic", "Expand Arab World" — 14, 92 and 34 named
controls across the three tree tabs, with ZERO raw i18n keys, which is the defect
the commit before it fixed. The nine bundles WITHOUT that key were checked
separately and embed no such legend, so their zero means not-applicable rather
than stale.

Verified not-stale the only way that works here: every one of the thirteen now
contains both current source markers (the tapDot key, and the sheet-clearance
logic — 71 hits in the four that also embed the mobile legend). A build that had
failed silently would show tapDot=0, which is exactly how the original
twelve-of-thirteen gap was found.

gates:pre 7/7, gates:post 42/44 with the two known precondition skips.
2026-09-24 21:39:05 +00:00
doxa-maps-88655c70
e680a9cde9 Write down that a pipe hides a failure
A pipeline's exit status is its last command's, so a build piped
through tail reports success when the build failed — and the chain
runs on, publishing a stale bundle with new source and an old .js.
Every gate stays green afterwards, because the SRI check proves the
leaf and the bundle agree with each other, not with the source.

Measured all three forms on this machine rather than repeating the
warning: the piped form reports success on a deliberately broken
build, set -o pipefail catches it, and redirecting instead of piping
catches it because the status is then the build's own.

The same shape already bit a merge here: "if ! git merge ... | tail"
tested the pipe, so a conflicted merge read as clean and the
resolution step was skipped.
2026-09-24 21:35:09 +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
6d4abe58cc 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:28:04 +00:00
doxa-maps-88655c70
db36397b93 Translate the last three English literals in components the clone renders
A dropdown every localised surface renders had no t() at all. Its two visible
strings — the clear button and the empty state — were English literals, and one
of them, "No options found", was ALREADY catalogued in eleven locales elsewhere
in this repo: the translation existed and the component was ignoring it. The
poster wall's export modal had the same shape, reassuring the reader that
nothing is uploaded in English only.

Three keys in buttons.json across eleven locales, and the literals replaced.
Both bundles that render these components — regions and country-posters,
whose cards are routed to me — are rebuilt. check:i18n-parity passes with no
locale regressed.

AND A BUILD HAZARD IN MY OWN COMMAND SHAPE, worth writing down because it is
exactly the failure the brief warns about. I ran

    BUNDLE=x bun x vite build 2>&1 | tail -2 && bun run tree && bun run fix:sri

The vite build FAILED on a syntax error — I had put a const inside
defineProps — and tree and fix:sri ran anyway, because the exit status of a
pipeline is the LAST command's, and `tail` succeeded. So the chain regenerated
the tree and re-hashed against a bundle that had not been rebuilt: the "tree
alone ships a stale .js" trap, opened by the pipe I added to keep the output
short. Caught because the next build's byte count moved, not because the chain
complained. Any agent using `| tail` after a build needs pipefail or a separate
status check.
2026-09-24 21:26:49 +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
810d346fd8 Sweep GRN batch five, and verify the map actually drives the table
170 more gap codes, 84 with gospel audio. 427 codes now carry a Words
of Life link; 603 people groups whose only resource in any pool is
that recording — 76.4 million people — can hear the gospel in their
own language. Tier 0 falls 2,373 to 2,270, from 3,457 when this began.
859 of the gap's codes decided, provenance exact at 752 claiming GRN,
752 decided, none decided-but-unnamed.

Also measured, rather than assumed: the map really does filter the
table beneath it. Composed on the dashboard, opening Foodo and
pressing "Every resource in Foodo ↓" cuts the table from 7,762 rows to
1 — Foodo has exactly one resource, the Scripture Earth PDF. That was
claimed by the brief and had never been checked end to end.

The translation-priority table tracks the sweep: 2,458 groups now,
up from 2,355, because every code GRN reaches moves a group out of
"nothing found" and into "can hear it, no text".
2026-09-24 21:25:17 +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
1fe0c3e036 Regenerate the tree index after merging main 2026-09-24 21:15:59 +00:00
doxa-maps-88655c70
fa975abd29 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 21:15:09 +00:00
doxa-maps-88655c70
9ba6ad7a7a Name the legend's carets, and translate a hint that had never been translated
TWO SHARED-COMPONENT DEFECTS, both routed to me, both a step away from where
the routing pointed.

THE 14 NAMELESS BUTTONS ARE ONE COMPONENT. check:a11y-button-names reported 14
visible buttons with no accessible name across two country-posters leaves. The
routing named countries-map.vue, which contains exactly one <button> and it has
visible text — the gate prints the LEAF, so the file was an inference. Running
the gate and then a probe that prints what a developer can grep for — class,
innerHTML, box size, the custom-element host chain — named the real one:
library/components/legends/SemanticTreeLegend.vue:1228, seven
<button class="stl-caret">, 14x14, a 9x9 chevron and nothing else. The 14 is
those 7 counted twice, because country-posters-mfe composes countries-map;
worth saying, or the next reader hunts a second defect that does not exist.
"(no title either)" in the gate's output is the diagnostic half: it rules out
the title-only control this gate was built for, and it rules out Mapbox's own
controls, which ship aria-labels. These were ours and they were empty —
assistive technology said "button" once per expandable branch.

Fixed at the source, which is why it lives in library/: an aria-label naming
the branch the caret discloses, and aria-expanded, because a name tells a
screen-reader user what the control is for and still not whether the branch is
open. Three keys across eleven locales rather than a hardcoded label. And the
same defect one element over, latent: .stl-x-chip carried only a `title` —
precisely what this gate exists to reject — and hardcoded English besides. It
escaped the sweep because it renders only while a row is selected.

Seven surfaces render that legend, so one fix reaches all of them. The
artifacts are per-bundle though: country-posters is rebuilt here because its
cards are routed to me, and my own bundle with it. The other five stay stale
until their owners rebuild, and I have not touched them.

AND THE PIN HINT HAD NEVER BEEN TRANSLATED. The ask was to audit eleven locales
for a stale "red dot". There is nothing stale, because there is nothing: no
locale file contains "Tap a", "pinHint" or "coloured dot"; PinHint.vue imports
no useI18n; the sentence is a JavaScript string literal. So every map that
renders the hint has been showing an English sentence to readers in Arabic,
Russian and Chinese.

Which is worth noticing about the fix that came before it. The colour half was
found and fixed carefully — the comment above it records the Driver reporting it
five or more times, and why a shared component must not name a colour it cannot
know. All correct. Nobody asked whether the sentence was in the catalogue. Same
shape as the Russian headings next door: wrong only for readers who do not file
bug reports in English, and so invisible to everyone who could.

Eleven locales, every one saying coloured and never red. The pinHintLabel
override still comes first, so a profile that does want to name its colour can.
And the key falls back to the English sentence when a locale lacks it, so a
missing translation degrades to today's behaviour rather than rendering the
literal "messages.pinHint.tapDot" — the failure mode of a naive t() swap.

Flagged and not taken, because they are shared components with owners:
FilterDropdown renders a hardcoded English "Clear selection" and "No options
found" (the latter already exists translated in the regions catalogue, so the
component is ignoring a string that is right there), and PosterWall's export
modal renders its reassurance line untranslated.
2026-09-24 21:14:47 +00:00
doxa-maps-88655c70
d5aaa900e6 README: reshoot the Plugin Studio at 0740a94 — folding rail, Research Gallery first
Some checks are pending
build-gate / build (push) Waiting to run
Shot from the rebuilt studio bundle the studio agent marked stable (0740a94),
not from source: the redesign was on main for half an hour before its bundle was.
2026-09-24 21:14:35 +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
0740a945c4 Rebuild the plugin studio bundle with its folding rail and Research Gallery name
The served studio (public/doxa-maps/plugin-studio) was built before the
rail folded its composites and before the shell was called Research
Gallery, so the built tree still showed the old studio. Rebuilt that one
bundle and re-baked its SRI in the manifest and its two leaf pages;
check:sri passes on all 57 pages. No other bundle was rebuilt.
2026-09-24 21:13:21 +00:00
doxa-maps-88655c70
53308c546c Delegation diagram as rings of circles; stop showing Simple Research as the mobile app
Some checks are pending
build-gate / build (push) Waiting to run
- The diagram is redrawn as concentric rings of uniform circles, not filled
  bands: the Plugin Studio ring labels itself, the seven plugins sit on the
  plugin ring, one agent per plugin sits outside it joined by a line and an
  arrow ("each plugin has one agent responsible for building it"), and one
  shepherd sits outside the agent ring. The race framing is gone. SVG source
  updated with the PNG.
- Deployment method 2: Simple Research was never built for phones, so it is
  no longer the mobile example. UPG-100 is; its screenshot lands when its
  phone layout is finished. The shot is removed from readme-shots, with the
  reason and the planned replacement in a comment.
- Retook nine screenshots that the latest bundle rebuilds had made stale.
2026-09-24 21:08:52 +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