Commit graph

96 commits

Author SHA1 Message Date
Doxa Maps
2f163e04c1 Large side-lane sheets: photos print in the sheet's own document, and a raster never draws a broken image
Measured on the 60x96 print: every photo beside a name rasterised as the browser's broken-image glyph. An SVG
rasterised through <img> is an isolated document that fetches no external resource, and none of the people-group
photo hosts (media.joshuaproject.net, joshuaproject.net, images.peoplegroups.org and its resizer,
peoplegroupspublic.blob.core.windows.net, s3.doxa.life) sends CORS, so a photo cannot be inlined as data: in the
browser either.
- poster-raster.js: rasterPoster drops every <image> whose href is not data: (the basemap tiles are data: and stay),
  so a download shows the paper, never the glyph. hasExternalImages() says when a sheet carries such photos.
- PosterWall: printing a generated sheet that carries photos prints THE SHEET ITSELF through printDocument, where
  the photos load, on its own @page (60in 96in), instead of a raster shrunk onto letter. Same HUD steps.
- check:side-lanes: photo arms (0 external images reach the raster; the wall prints such a sheet as a document),
  each with a poison in its self-test.
- check:side-lane-photo-pixels: the pixel side, a browser gate (a data: photo draws red; 99/99 external slots stay
  flat; without the strip 0/99 do). Declared on-demand until its first browser-seat run.
2026-10-01 09:45:33 +00:00
Doxa Maps
a1c74004a0 Merge development (353bf078c) into the whole-India landing: package.json gates unioned (check:side-lanes added), poster-render re-pinned on the merged file 2026-10-01 08:24:45 +00:00
Doxa Maps
6d0253d909 countrySheet: a requested large size wins; LARGE_SHEETS only supplies India's 60x96 default
India can print at 60x96 (the default), 66x66 or 36x60. makeCountrySheet used to map any non-packet request to the
default, so a 66x66 or 36x60 request drew the 60x96 sheet. sheetFormatFor(item, format) (exported, pure) now keeps
a requested size the country can print at, falls back to its default otherwise, keeps a packet page a page, and
returns exactly what was asked for every other country.
check:country-sheet-one-path asserts that table (India 66x66/36x60/60x96/letter/3x4/packet; Nepal 3x3/letter;
China 3x4), with two poisons: India ignoring a requested 66x66, and a packet page turned into a large sheet.
2026-10-01 06:07:48 +00:00
Doxa Maps
1af359ac42 poster-render: side-lane CSS adds no byte to other formats; photos only from the known photo hosts
5B fixes on the side-lane mode:
- The side-lane styles' template slot sat on its own line, so every OTHER format's page gained one newline. It is
  now joined onto </style>, emitting nothing at all off side lanes.
- A photo is drawn only over https from the hosts that serve people-group photos (media.joshuaproject.net,
  joshuaproject.net, images.peoplegroups.org, peoplegroupspublic.blob.core.windows.net), never the generic
  placeholder; anything else is dropped. Re-pinned.
check:side-lanes' seeded data now also carries photos on a host that is NOT allowed; none may be drawn.
2026-10-01 06:03:48 +00:00
Doxa Maps
455fa8ab7d One rule for countries too large for paper: countrySheet.js's LARGE_SHEETS; whole-India list, packet and cards on a desktop
CORRECTS the previous commit's "no prayer list, packet or cards for whole India": the current direction is that
whole India's poster, list, packet and cards (about a thousand pages) are offered DESKTOP-ONLY, and a phone shows
the existing poster.phoneLimit sentence instead of failing.
countrySheet.js now exports LARGE_SHEETS ({ IND: 60x96, desktopOnly }) and largeSheet(item), and makeCountrySheet
itself prints such a country in its large format (a packet page stays a page) with its "Administrative Areas"
cell from the shared state table ("36 states and union territories"). So every caller gets the same sheet: the
Regions list's India no longer has to disagree with Country Posters (regions-list's own lines follow, from regions).
The wall drops its India-only code and follows the same rule.
2026-10-01 05:55:55 +00:00
Doxa Maps
1bb5b90984 Country sheets: photos reach the renderer, and a caller can name the "Administrative Areas" cell
pinsFrom carries image_url (only side-lane sheets draw it; every other sheet ignores it). countryZone and makeCountrySheet accept an optional adminAreas for the facts row (India: "36 states and union territories"); without it the cell shows the country's name as before.
2026-10-01 05:52:47 +00:00
Doxa Maps
ef7ace486e poster-render: large side-lane sheets (60x96, 66x66, 36x60) that name a whole country's groups with crossing-free leaders
A new layout mode, ADDITIVE: three new FORMATS with `sideLanes`; every existing format takes exactly its old path.
Every name horizontal, on the left and right of the map in two staggered columns per side (rows alternate, so each
column keeps a full line pitch and each row holds one name). Co-located groups form a cluster with one leader and
a bracketed block. Crossing-free by construction: one vertical split sends each cluster to the nearer side; each
leader is a short stub to its own lane, a horizontal run to the map edge, and a fan to its block, with lanes placed
farthest-from-the-edge first and clamped between their neighbours, and fans in the lanes' order. The renderer
measures and returns its crossing count. A real photo (never the placeholder) sits outside its name where the
format allows. The type is the largest that fits the rows and columns, capped per format (nameMaxPt).
On these sheets only: pins stay at their true positions (no settle pass), the map frame takes the country's own
shape centred in the band with a thin margin, and the side-lane styles are emitted.
India (855 groups, live data): 60x96 20.7 pt names + 7.9 mm photos; 66x66 14.6 pt + 5.5 mm; 36x60 12.6 pt, no
photos; 0 crossings on all three; 4 s per render where the numbered path took 65 s. Re-pinned.
2026-10-01 05:52:47 +00:00
Doxa Maps
a204752597 P: merge 2c397d25e 2026-10-01 00:55:59 +00:00
Doxa Maps
2c397d25ef Source comments state design history and measurements as facts, without quoting or naming anyone
Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
2026-10-01 00:37:17 +00:00
Doxa Maps
51524b7897 A poster series for India's states and union territories; series-isolation sees shared wording and poster text correctly
series.json: a new `indiaState` series, modelled on `country`, so a state poster names itself honestly
("India State Maps", "What is a State Prayer Map?", one sheet for each state and union territory of India,
"which state God has put on their heart"). The country series' text would have called Gujarāt one of the
~170 Country Maps. Approved as the default wording; shown as editable.

check:series-isolation, two blind spots found by adding a third series:
- Shared wording was dropped from only the EARLIER series (sets were mutated while still being compared),
  so the later one kept it as a fingerprint and the earlier failed for carrying it. The shared set is now
  found on the original sets. Self-test: three series in any order; the old loop fails it.
- The contamination scan never read a series' `poster` block, where a country poster once printed the
  UPG-100 side box. It does now. The phrases that NAME country and indiaState are hand-listed, so a
  verbatim copy of one's framing into the other is caught even though symmetric dropping treats it as shared.
  Self-test 9/9, including both directions of a country/state copy.

countrySheet.js: the seriesFor comment no longer says "map 1 of 0" was printed; it was latent (the map-only
packet page prints no series line, and the India poster is gated), as a rendered check showed.
2026-09-30 23:33:34 +00:00
Doxa Maps
b0d0fcaf91 seriesFor: state that every no-sheet country shares one series, and gate it
A second country without a sheet size would silently join India's oversized
series and print India as "map 1 of 2". The comment now says so, and
check:country-sheet-one-path fails when more than one country has no sheet,
naming them, so that change is a decision rather than drift. Its self-test
plants a second one (China) and requires the arm to fail.
2026-09-30 21:57:25 +00:00
Doxa Maps
ea9211a5f9 seriesFor: India (sheet null) is its own 1-of-1 series
seriesFor skipped any country without a sheet size, so India's packet page 1
from the Regions tab printed "map 1 of 0", while the Country Posters wall's
own inline copy printed "map 1 of 1". A null sheet is now a tier like any
other. A country absent from the list (a host's code with no poster) still
gets no series, so the renderer keeps its default rather than a made-up
position.
2026-09-30 21:56:56 +00:00
Doxa Maps
f963d90bc3 Packet: the lightweight worksheet shows every group's photo and short description too (a photo cell and a two-line description per row, 16 + 18 rows a page); the variant gate asserts both headless, the render gate counts the rows' photos and follows the new pagination 2026-09-30 12:04:20 +00:00
Doxa Maps
3211236014 Packet: point 4's reference names what prints, Mark 5:18–20, in all 11 locales (the Gerasene reading keeps 5:12–20), re-pinned; the passage gets a 700-char ceiling beside its 250 floor, in the render gate and, headless, in the structure gate for every locale 2026-09-30 11:55:51 +00:00
Doxa Maps
b0c4ef1897 Re-pin packet-strings.json and rebuild the five packet bundles from the merged source, and regenerate the tree and SRI 2026-09-30 11:43:33 +00:00
Doxa Maps
f5fa9e05b1 WIP packet: Mark 5:18-20 back on the points page (source) 2026-09-30 11:38:02 +00:00
Doxa Maps
2dbd567945 WIP packet 10:58 (the dictated points, point 4's statement + lead-in, the wheel turned with Start here; re-pinned; 5 bundles rebuilt): measured, reads pending 2026-09-30 11:17:25 +00:00
Doxa Maps
23b96dfe6c Prayer packet: point 4's sheet carries no Metacamp footer
The new READ THE WORD sheet reused the points page's footer, so a packet
printed three Metacamp attributions where check:packet-browser counts
exactly two (the wheel and the points). The sheet already carries its
own credit line, the scripture's; the footer goes. Rebuilt the five
bundles that compile the renderer; the tree and SRI regenerated.
2026-09-30 09:59:29 +00:00
Doxa Maps
69e0e67075 Prayer packet: point 4 gets its own sheet, Mark 5:1-20 in full with verse 19 in bold
Point 4 (READ THE WORD) again prints Mark 5:1-20 in full, the texts the
packet shipped before a80f4fd6e (the public-domain versions on file for
en, es, fr, de, ru and ar; the English WEB text elsewhere, as before).
Verse 19 is in bold: point4_passage_bold holds it verbatim from the same
text, so the bold is a substring match and never a second rendering of
scripture. THE HEAVENLY FATHER'S HEART follows the passage.

The passage at a readable size (24px) and the twelve points at theirs do
not share one sheet, so point 4's reading has its own page right after the
points ("4. READ THE WORD"), and the points page gains more space between
points. Both sheets fit in all 11 locales.

The gates count the new sheet (packet-render +3, poster-prints 5 sheets,
a1-structure "... points · word") and assert the passage in full again,
verse 19 in bold and the seven readings. packet-strings.json is re-pinned,
and the five bundles that compile the pin are rebuilt (country-posters,
doxa-research-map, my-upg-100-list, regions-dashboard, upg-neighbor),
with the tree and SRI regenerated.
2026-09-30 09:41:43 +00:00
Doxa Maps
86034174b0 WIP Mark 5 page (source) 2026-09-30 09:32:33 +00:00
Doxa Maps
0daf6db225 Re-pin packet-strings.json: non-English packet prints load their language again
The pair (a80f4fd6e) changed library/scripts/posters/packet-strings.json
(point 4's reading list) without re-running the pin script, so the served
copy (assets/lib/packet-strings.json) no longer matched
PACKET_STRINGS_SHA384. The print then refused it and fell back to the
bundled English block: every non-English packet printed in English.
check:poster-render-pinned has failed on development since.

Re-pinned (library/packet/poster-render.integrity.js, packet-strings only;
the generator and places pins are unchanged), and rebuilt the five bundles
that compile the pin in: country-posters, doxa-research-map,
my-upg-100-list, regions-dashboard, upg-neighbor. The tree and SRI
regenerated. check:poster-render-pinned: FAIL (2) on 7744a8418, PASS here.
2026-09-30 09:20:22 +00:00
Doxa Maps
8105b1e76a Prayer packet: point 4 prints a short reading list, and the 12 points are larger
Point 4 (READ THE WORD) printed Mark 5:1-20 in full at 14px, the longest
and smallest text in the packet. It now prints THE HEAVENLY FATHER'S
HEART: one heading, one framing line and seven short readings (Noah,
Lot, Rahab, the Gerasene, Cornelius, Lydia, the jailer), each a
household the Father saved through one person. The names come from each
locale's block; the addresses stay canonical English in the data and the
book is named in the packet's language, like every other reference
(Genesis and Joshua added to the 11 books tables).

That room goes to the twelve points: body 17 -> 22px, heading 19 -> 24px,
references 14 -> 17px, and 13 -> 16px between points. The page still fits
one sheet in all 11 locales (the tightest, de, ends 59px above the
bottom).

The gates that asserted the full passage (packet-render, packet-browser,
poster-prints) now assert the list: a .p4-heart block with its seven
readings. Rebuilt: my-upg-100-list, country-posters, regions-dashboard,
doxa-research-map, upg-neighbor; the tree and SRI regenerated.
2026-09-30 08:29:54 +00:00
Doxa Maps
42db98180f A tiny series prints its exact count, not a rounded ~0
The SPARSE series line rounds its total to tens -- 173 reads as ~170, 28 as ~30
-- because a series that large is read as a size, not counted. Below ten the
rounding breaks: China is the only 3x4 country sheet, so its tier has one member,
and Math.round(1 / 10) * 10 = 0 printed "1 of ~0 maps".

Under ten the line now states the exact count, "map 1 of 1" -- the same text
the dense layout already prints for China, so the move to SPARSE is not a
regression for it on the print.

PROVED: China rendered SPARSE reads "— map 1 of 1" with 0 occurrences of "~0";
and Sudan rendered after this change is BYTE-IDENTICAL to Sudan rendered before
it, so no series of ten or more changes -- the 173 country sheets and the
UPG-100 zones are unaffected.

Not decided here: China's thumbnail says 174 of 174 and its print 1 of 1. That
is per-tier against whole-series numbering, the pre-existing R-CHN-SERIES-TIER,
left for a product decision after the demo.

poster-render.js is sha384-pinned; re-pinned so check:poster-render-pinned
accepts it.
2026-09-30 03:05:49 +00:00
Doxa Maps
16dca4d266 Posters drawn at the click use the SPARSE layout, like their stored thumbnails
The bold header on a poster -- the UPG count and population set large, a
bigger map, and no "Jesus' Strategy to Feed the 5000" / "What is a Prayer List"
columns -- is the SPARSE layout, the "thumbnail-worthy" design. The `posters`
script generates with SPARSE=1, so every stored thumbnail has it. But the env
handed to the generator in the browser carried only FORMAT and POSTER_DATE, so
EVERY sheet drawn at the click -- UPG-100 and country alike -- fell back to the
dense layout.

MEASURED on FINAL (two UPG-100 zones, Northwest and South-Central India): the
thumbnail shows the SPARSE header; the print the reader actually gets shows the
small facts table, both vision columns and a smaller map. That is the reported
"the country poster looks worse than the UPG-100 poster" -- the comparison was a
SPARSE thumbnail against a dense sheet.

(a) posterSheets.js passes SPARSE for every POSTER format and NOT for 'packet'.
    That is exactly the split the two build scripts make: `posters` sets
    SPARSE=1, `packets` does not. The renderer's formats are 3x4, 3x3, letter and
    packet, so 'packet' alone keeps the full layout its pages were built around.

(b) check:poster-browser-equiv's poster case now runs the Node side with
    SPARSE=1 as well. It used to compare dense with dense, so it passed while no
    reader ever saw that layout on a card. The packet case stays dense on both
    sides. PASS: country-npl 909026 bytes on each; asia-india-gujarat (packet)
    835047 on each.

The stored UPG-100 thumbnails are untouched by this. What changes is the PRINT,
which now matches them.
2026-09-30 02:53:15 +00:00
Doxa Maps
072b1ff216 Regions-tab print: pass the same series as the Country Posters wall
Every country poster printed from the Regions tab read "map 1 of 0" and "This
map is one of a 0-map set called the Country Maps" -- nonsense on a printed
poster, on the demo's showcase flow (Regions -> a country -> Poster Map). Found
by the served check on FINAL.

WHY: makeCountrySheet called generateCountrySheet with no series, so
generateSheet fell back to its default { totalGroups:0, totalLists:0, index:1 }.

NOT A REGRESSION from the one-definition change, and worth recording because it
was first reported as one. The plugin-local copy regions-list used before that
change passed no series either; across it regions-list.vue changed by exactly
one line, the import. The defect has been in the Regions print since 168b1f1bd.
The Country Posters wall was never affected because it computes its own.

THE FIX computes the series exactly as the wall does (country-poster-wall.vue):
the tier is every country printed at the same sheet size, the total is that
tier's length, and the index is the country's position in it by id. It lives in
ONE place, seriesFor() in the library, rather than as a second inline copy of the
wall's formula -- the same duplication this branch's parent removed for the sheet
itself. regions-list passes the FULL country list, not a region's sublist:
indexing a region's own list would print "map 3 of 12" beside a thumbnail that
says "map 57 of 173".

PROVED, rendered with the values seriesFor returns:
  Sudan  index 1 of 173  ->  "map 1 of 173"  |  "one of a 170-map set"
  Chad   index 4 of 173  ->  "map 4 of 173"  |  "one of a 170-map set"
which is exactly the text the wall prints for the same two countries on FINAL,
measured on the served page, and what their thumbnails say. A row with no sheet
tier gets undefined, keeping the renderer's own default rather than a fabricated
position.

KNOWN, NOT CHANGED: China is the only 3x4 sheet, so its tier has one member and
it prints "map 1 of 1" / "a 0-map set" -- here AND on the wall, identically,
because this matches the wall by design. Its thumbnail says "174 of 174". Fixing
it means choosing whole-series numbering over per-tier, which is the carded
174-vs-173 question, not a freeze-night change.
2026-09-30 00:00:00 +00:00
Doxa Maps
baa60ba647 Merge remote-tracking branch 'origin/development' into regions/country-sheet-onedef
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-29 22:59:27 +00:00
Doxa Maps
190e7665a7 The packet's low pages fill the sheet: measured, then re-shaped
Measured in a browser (content's bounding box against each sheet, en/de/ru):
the knows-Christ grid ended 72% of the way down its sheet (478px of blank
paper below), and the two single-chart sets pages 60% and 69% (702px and
525px). The same 5,200 boxes now lay out 65 x 80 in 14.5px squares, which
spends the height on bigger boxes; the sets charts (SVG, so they scale) grow
from 960px to 1320px (bounded, nothing below it) and 1240px (centered, room
for its ring legend, which widens with it). Now 88%, 79% and 84% of the
height, with no page clipped in en, de or ru. The width classes are hung on
the row (set-solo-<kind>), not on the page, so no page class name enters the
shared stylesheet.
2026-09-29 21:34:14 +00:00
Doxa Maps
614c3248d3 Card names hyphenate only where a word cannot fit
hyphens:auto on every name let the browser split ANY word at a line end, so
multi-word names could break mid-word ("Northern Conchu-/cos"), which is
worse on a people group's name than the unhyphenated break it replaced. The
previous commit is reverted; hyphenation now applies only to names holding a
14+ letter word (12 of 2,107), with hyphenate-limit-chars:10 4 4 so a short
word is never split: the only multi-word ones are "Deaf <X>", and Deaf
stays whole.
2026-09-29 21:04:26 +00:00
Doxa Maps
1000a7ab5c Revert "A card name that must break mid-word gets a hyphen"
This reverts commit 7cdf72bfd4.
2026-09-29 21:03:46 +00:00
Doxa Maps
a73b672cc6 Merge remote-tracking branch 'origin/development' into regions/country-sheet-onedef
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-29 21:02:52 +00:00
Doxa Maps
520973c1d7 One country-sheet definition: the Regions tab's poster was still teal
exp's second read of 1ab8524cf found a real defect on the demo path, and a census
of the tree found the cause was worse than the symptom.

THE SYMPTOM: the Regions tab printed a TEAL country poster and logged a warn,
while Country Posters printed the same country HUED. That is precisely the "two
different posters for one country" the hue change exists to refuse.

THE CAUSE: regions-list.vue:73 imported a plugin-local copy,
src/country-sheet-print.js, byte-identical to library/packet/countrySheet.js
except for the region key. The region forwarding was added to the LIBRARY file,
so countryRegionColor(undefined) returned the old flat teal and warned.

★ AND THE LIBRARY FILE HAD NO IMPORTERS AT ALL. Its own comment claimed two
consumers -- the country-poster wall and <doxa-country-map> -- and a census found
NEITHER was true: regions-list imported the local copy, and
country-poster-wall.vue:94 defines its own inline countrySheet that calls
generateCountrySheet directly (passing the whole record, which is why THAT path
was correctly hued). So the module was an orphan wearing a comment that said
otherwise, and the region key was added where nothing read it. A move that was
never finished, and the duplicate stayed invisible until one copy was edited.

regions-list now imports the library module, the copy is deleted, and the
comment says what is true. Not re-copied: a duplicate of that file is
indistinguishable from it until one side is edited, and then it is a silent
divergence.

CHAIN VERIFIED TO SOURCE, since the hue is only right if the region survives
every hop: POSTER_BY_ISO3 holds country-posters.json records, which all carry a
region; localisedPoster SPREADS the record in both its branches, so the key
survives a locale switch; postersFor passes it; makeCountrySheet forwards it.
ARTIFACT PROOF: the region forwarding in the rebuilt regions-dashboard bundle
goes from 1 occurrence to 2, and POSTER_REGIONS plus the full region-name list
are present in it.

The no-poster fallback branch in postersFor still builds an object without a
region, deliberately: it has no sheet to draw, and it already carries
unsupportedReason.

NOTE ON HISTORY, not rewritten: f6a3f85a2 is titled "Rebuild the three
consumers…" but it also carries SOURCE changes -- the ink .pin-num-bg in the
shared renderer and the warn-once in posterSheets.js. The title understates it.
It is pushed, so it stands; recorded here instead.
2026-09-29 20:55:27 +00:00
Doxa Maps
be3184207c Merge remote-tracking branch 'origin/development' into upg100/page-fill
# Conflicts:
#	public/dx-maps/country-posters/countries-map/index.html
#	public/dx-maps/country-posters/country-poster-wall/index.html
#	public/dx-maps/country-posters/country-posters-mfe/index.html
#	public/dx-maps/country-posters/country-posters.js
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/my-upg-100-list/my-upg-100-list.js
#	public/dx-maps/my-upg-100-list/poster-wall/index.html
#	public/dx-maps/my-upg-100-list/upg-100-list/index.html
#	public/dx-maps/my-upg-100-list/upg-100-mfe/index.html
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-29 20:54:55 +00:00
Doxa Maps
42537f11c9 The knows-Christ grid fills the page: the same 5,200 boxes, 80 x 65
52 columns exist only because a year has 52 weeks; on the knows-Christ page
a box is a share of people, and at 52 x 11px the grid used about half the
sheet's width. Without axes the same 5,200 boxes are laid out 80 x 65 (both
divide 5,200, so the rectangle is whole), the columns stretch to the frame
and the cells stay square, still shaded from the top in reading order. The
height this frees goes to a larger legend. The life calendar keeps its 52
week columns and its axes.
2026-09-29 20:41:29 +00:00
Doxa Maps
1980c9000e Merge remote-tracking branch 'origin/development' into regions/country-poster-hue
# Conflicts:
#	public/dx-maps/country-posters/countries-map/index.html
#	public/dx-maps/country-posters/country-poster-wall/index.html
#	public/dx-maps/country-posters/country-posters-mfe/index.html
#	public/dx-maps/country-posters/country-posters.js
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/my-upg-100-list/my-upg-100-list.js
#	public/dx-maps/my-upg-100-list/poster-wall/index.html
#	public/dx-maps/my-upg-100-list/upg-100-list/index.html
#	public/dx-maps/my-upg-100-list/upg-100-mfe/index.html
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-29 20:37:20 +00:00
Doxa Maps
f6a3f85a2c Rebuild the three consumers of the shared country-hue rule
The rule lives in library/packet/posterSheets.js so the browser and the Node
generator cannot disagree, which puts it in every bundle that generates a country
sheet. landing-preconditions step 4 named country-map, country-posters and
regions-dashboard; each built with BUNDLE, then tree, then fix:sri.

No source of country-posters changed -- only its compiled bundle catches up with
the library it already imported.
2026-09-29 20:32:09 +00:00
Doxa Maps
7cdf72bfd4 A card name that must break mid-word gets a hyphen
hyphens:auto on the name, with the deck's <html lang> naming the language,
so a 16-letter name like Liechtensteiners that overflow-wrap has to split
breaks with a hyphen rather than silently mid-word.
2026-09-29 20:28:51 +00:00
Doxa Maps
7795eba447 The knows-Christ totals come from the unrounded share
Only the printed percentage rounds. A large list at 0.004% is tens of
thousands of people, which printed as 0 when the total was computed from
the rounded figure; a share below the last printed digit now reads
"<0.01%" beside its real total. check:evangelical-share asserts it.
2026-09-29 20:20:02 +00:00
Doxa Maps
d78e485643 "Who among them knows Christ" counts people, not weeks
The page reused the life calendar's grid and counted its shaded boxes in
weeks ("0.61% · 32 weeks"), under a week axis and a year axis, with a lede
calling it "one human life in weeks". On this page a box is an equal share
of the list's PEOPLE, so the same 5,200 boxes are now drawn with no week or
year axis, and the legend states each side in people: how many follow
Jesus and how many do not yet, each with its share (the two percentages
sum to 100 as printed), plus how many people one box stands for. Numbers
are formatted in the reader's locale.

The labels become "Followers of Jesus" / "Not yet following Jesus" and the
lede drops the weeks, in all eleven languages; the served strings file is
re-pinned. check:evangelical-share asserted the weeks figure; it now
asserts the people totals, the bare percentage and the absence of weeks
and axes, with the life page (which keeps its axes) as the negative
control. check:packet-variants found the grid by an exact class string
and now matches the class prefix.
2026-09-29 20:08:55 +00:00
Doxa Maps
a2da3fe97b Colour each country poster by its region, instead of one teal for all 173
★ THE REPORT WAS THAT THE COUNTRY POSTER'S CSS LOOKS WORSE THAN THE UPG-100
POSTER'S, AND IT IS NOT A CSS DIFFERENCE. Measured before editing anything: a
country sheet (Sudan) and a 100-list sheet (europe) generated from the same tree
have <style> blocks of 253 lines each, 18,475 vs 18,489 bytes, byte-identical
EXCEPT one custom property --

    country : --zc:#2bb
    upg100  : --zc:hsl(103, 72%, 56%)

poster-render.js has exactly ONE <style> block and both series render through it,
so typography, margins, header, legend and sheet fill are literally the same
rules. There was no second stylesheet and nothing to port; a CSS rewrite would
have been wasted work and a collision with the sheet-fill work in flight.

--zc drives the zone fill, the pin fill and the pin-number background -- the
three most visible things on a sheet -- and all 173 country posters set it to the
same hardcoded teal while each 100-list sheet carried its own hue. That is the
whole of the difference.

★ THE RULE LIVES IN library/packet/posterSheets.js BECAUSE BOTH PATHS MUST
AGREE, and getting that wrong is the most useful thing that happened here. A
first attempt defined the region-to-hue mapping in the Node generator alone and
broke check:poster-browser-equiv by exactly 26 bytes on country-npl -- the length
of 'hsl(45, 72%, 56%)' over '#2bb', twice, because --zc is emitted at two places.
The browser generates these sheets at click time from posterSheets.js, so a rule
one side knows and the other does not is two different posters for the same
country, which is precisely what that gate exists to refuse. One definition,
imported by the Node generator; countryZone takes the region, and countrySheet.js
forwards it so the print wall's path carries it too.

The formula mirrors leafColor in the 100-list's legend-tree.js -- an even hue
spread at 72%/56%, comma-HSL because Mapbox paint expressions reject the slash
syntax. It is restated rather than imported because library/ must not depend on
plugin-designs/; the dependency runs the other way. If that rule changes there,
this changes in the same commit.

★ BY REGION, NOT BY COUNTRY INDEX. 173 countries over 360 degrees is ~2 degrees
apart: indistinguishable, and meaningless on a poster seen ALONE with no sibling
to be told apart from. A region is a grouping the reader already has, so Sudan
and Chad reading as related is true rather than decorative.

★ THE LIST IS DECLARED, NOT DERIVED FROM WHAT A RUN CONTAINS. Indexing into the
regions present in one run would give a country one hue in a single-sheet render
and another in a full build, so re-rendering would change the colour of a poster
somebody had already printed. APPEND a new region; never reorder, which would
change the hue of every region after the insertion. An unrecognised region keeps
the old teal rather than being folded into 'Other' -- folding would silently give
two regions the same hue and hide that the list needs a line.

PROVED BY ARTIFACT, after the refactor:
  Sudan   BEFORE --zc:#2bb   AFTER hsl(180, 72%, 56%)   Middle East
  Chad    hsl(0, 72%, 56%)     Nigeria hsl(0, 72%, 56%)   both Africa -- same region, same hue
  Nepal   hsl(45, 72%, 56%)                               Asia -- differs
  check:poster-browser-equiv: country-npl browser 921548 = node 921548, byte-identical.

★ A LEGIBILITY PROBLEM THIS INHERITS AND DOES NOT CAUSE, reported rather than
silently absorbed: .pin-num is #FFFFFF over .pin-num-bg, filled with --zc. At
72%/56% white text fails WCAG 4.5:1 on SIX of the eight region hues (hue 0 reads
4.26, 45 1.91, 90 1.63, 135 1.74, 180 1.62, 315 3.79; only 225 and 270 pass).
It is PRE-EXISTING and already shipping on the 100-list's own sheets -- europe's
hsl(103) gives white 1.70:1 -- so matching the rule exactly is right for the
reported complaint and makes legibility no worse than it already is. The fix is a
line in poster-render.js's shared <style> block, which is not this change's to
make; it is with that file's owner.

Also folds in a pre-existing punctuation nit found in second read: the hi value
of "Loading results..." ended '..।' -- two ASCII dots plus U+0964 DANDA, a full
stop -- neither an ellipsis nor correct after one. Now '...', matching the
English. Keyset unchanged, so parity is untouched and still 0 debt.

gates:pre PASS 107 FAIL 0 NM 1 of 108.
2026-09-29 19:59:32 +00:00
Doxa Maps
e018ec47b3 A long people-group name is never cut off on a prayer card
The name now sits in a narrow box beside the photo, and the dataset has
16-letter single words (Pagativeshagallu, Liechtensteiners) that would have
overflowed it sideways and been clipped without a sign. The size steps by
the longest word as well as the length, and overflow-wrap:anywhere is the
last resort.
2026-09-29 19:56:47 +00:00
Doxa Maps
b6fe075e92 Prayer-card photos print sharp: sized from their pixels, not stretched
The people-group photos are small: every one sampled across the dataset is
at most 200 px wide, typically 200x250, and the API offers no larger
variant. The card stretched that across its full top band with
object-fit:cover, so a poker card printed a face at about 80 ppi and a
postcard at about 50: visibly blurred, on the one part of the card that
is a person.

The photo is now a framed portrait at the source's own 4:5 shape, sized
from its pixels at 180 ppi (about 1.1 x 1.4 in), on a warm band with the
name beside it (below it on a landscape card). Every stock prints a
200 px photo at about 190 ppi inside its frame. A bigger card gets more
room around the face, never a bigger, blurrier face.
2026-09-29 19:43:58 +00:00
Doxa Maps
bf07e0eb61 Merge branch 'regions/country-map-posters' into regions/handoff-integration
# Conflicts:
#	plugin-designs/regions-dashboard/regions-list.vue
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-29 14:53:10 +00:00
Doxa Maps
a3e58ac7ca Serve the packet text at print time instead of bundling all 11 languages
A single-file build inlines every import, dynamic ones included, so
packet-strings.json (~115 KB minified: every language's prayer points,
captions and headings) rode in every bundle that can print a packet, list
or card deck. With the wall's print messages translated, country-posters
crossed the 1.5 MB ceiling (1,502,226 bytes).

The table is now served at <assets>/lib/packet-strings.json and fetched at
the first print by library/packet/packetStrings.js, the same move as the
sheet generator and ne-places.json:

- Pinned like them: PACKET_STRINGS_SHA384 in poster-render.integrity.js
  (pin-poster-render.mjs); the text is hashed before it is parsed, and an
  unmatched table is not used. These strings are laid into the print
  document, so an unverified copy never reaches it.
- English is bundled as the fallback (a named import of the JSON, so only
  the en block ships): with no asset base, on file://, on a network error or
  an integrity mismatch the print comes out in English and the console says
  why. A print never fails for want of this file.
- generate-tree copies it into the pool AND the served allowlist; verified
  by regenerating the tree (the served copy is byte-identical to source).
- check:poster-render-pinned, two-sided: the pin matches source and served
  copy, the genuine copy loads all 11 languages, a tampered copy falls back
  to English (mutation-tested: disabling the hash check fails it), and an
  unserved one falls back rather than failing.
- sha384 is one shared helper (library/packet/sha384.js).

Sizes: country-posters 1,378,463 (was 1,484,053), my-upg-100-list 1,143,456
(was 1,243,509), regions-dashboard 973,802 (was 1,079,448).
2026-09-29 08:33:44 +00:00
0bedb889ba PARKED: the country map's own poster surface, opt-in, waiting on on-demand print code
<doxa-country-map> gains the poster, prayer list, card deck and packet that the
regions wall has, for the one country it draws. The surface is opt-in through
profile-config (showPosters, read only as literal true or 'true'), off by
default, behind v-if so an embed that never asks for it emits no markup, and
loaded through retryableAsyncComponent rather than a static import.

NOT LANDABLE YET, and the measurement is why. Built element, default OFF:

  baseline          528,901 B
  with the surface  930,692 B
  delta            +401,791 B

The lazy load does not help. This repo builds each bundle as a single IIFE with
inlineDynamicImports, which flattens an async component exactly as it flattens a
plain import(): PosterWall, packet-render and createPosterGenerator all appear in
the built element with the flag off. my-upg-100-list's lazy detail panel shows
the same pattern, so this is the build shape rather than this wiring.

So a country page that never turns the surface on would still carry ~400 KB of
printing code. This element exists precisely so such a page need not load the
regions dashboard, so that trade is the wrong way round. It waits for the print
subsystem to be served on demand, the same change that took the regions wiring
from +208 KB to +17 KB once the sheet generator moved to a pinned asset.

countrySheet.js moves to library/packet/ HERE rather than in the wiring commit:
its second consumer is this parked surface, so until this lands the file has one
live consumer and stays plugin-local. The wiring lands plugin-only.

Holds either way: records and sheet come from the shared providers, never a copy;
the single poster item resolves from country-posters.json, with an
unsupported-reason row for a code that has none; poster-action is print
throughout; no host is switched on.
2026-09-29 07:48:18 +00:00
Doxa Maps
f18980f641 Pages whose security policy forbids blob: scripts can still print generated sheets, verified natively
The served generator was run by importing its VERIFIED text from a blob: URL, which fails closed on a
partner page whose Content-Security-Policy forbids blob: scripts: safe, but no generated sheet there
(dx-shepherd's FIX-POSTER-RENDER-LOAD-WITHOUT-BLOB-FOR-STRICT-CSP).

The tree now also serves a CLASSIC-SCRIPT copy, lib/poster-render.global.js: the same file under a
mechanical transform (strict mode kept; the one export becomes window.DoxaPosterRender), written by
generate-tree with the function the pin is computed from (globalVariant, pin-poster-render.mjs) and on
its served allowlist — produced by regenerating the tree, not copied in by hand. posterSheets keeps the
proven path first; only when the blob import itself is refused does it load the classic copy with
<script integrity=sha384-…>, so the browser verifies it natively. An integrity FAILURE never falls back:
it throws before any import is tried.

check:poster-render-pinned adds: the classic copy matches its pin and the served copy, and — run as a
real classic script — draws the module's sheet byte for byte. The <script> path under a strict policy
needs a browser (rega). country-posters 1,473,964 bytes (ceiling 1,500,000); verify-bundles-shipped
PASS; gates:pre 74/74.
2026-09-29 07:31:17 +00:00
Doxa Maps
3268105643 The served capitals-and-cities table is integrity-pinned too, and a forged one is not drawn
Following the generator's pin (studio's follow-up): the Natural Earth table the sheets fetch at print
time (boundaries/ne-places.json) is data, not code, but pinned the same way — its sha384 sits beside the
generator's in poster-render.integrity.js (bun run pin:poster-render writes both), and posterSheets hashes
the fetched text before parsing it. A table that is not the pinned one is not used: the sheet draws no
city dots and says so in the console, rather than plotting places it cannot vouch for.

check:poster-render-pinned, two-sided for the table as for the generator: the pin matches the source
and the served copy; a FORGED table (every name replaced) yields exactly the sheet drawn with no places;
the genuine table IS used (its sheet differs) — the probe's pins now span Chad, because a single pin
framed too small an area for any place to fall inside it and the check could see nothing.
check:poster-browser-equiv still byte-identical; gates:pre green.
2026-09-29 07:31:17 +00:00
Doxa Maps
0be5f07ab6 The served sheet generator is integrity-pinned: a browser runs it only when its sha384 matches
Code loaded at print time executes on partner pages, so it is pinned like every embed. The bundle
carries the generator's sha384 (library/packet/poster-render.integrity.js, written by
`bun run pin:poster-render`); posterSheets fetches <assets>/lib/poster-render.js as TEXT, hashes it
with WebCrypto, and on a match imports the verified text from a blob: URL — never the network URL a
second time. A mismatch refuses to run, with a message naming the URL and both hashes; the print falls
back to what it can do without the generator.

The page base is captured when posterSheets evaluates, and every URL it resolves (the generator, the
boundaries, the places) resolves against it — not the bundle's location, and not a URL a single-page
host changed after load.

check:poster-render-pinned (gates:pre), two-sided: the pin equals the sha384 of the source and of the
served copy (a stale pin names the command — proven by an unpinned edit); a TAMPERED copy is refused
and its planted side effect never runs; the genuine copy loads and draws. check:poster-browser-equiv
still passes byte-identical through the pinned path. country-posters 1,472,734 bytes (ceiling
1,500,000), my-upg-100-list 1,232,508, both carrying the pin; verify-bundles-shipped PASS; gates:pre 74/74.
2026-09-29 06:23:20 +00:00
Doxa Maps
7987f100c1 The map-sheet generator is served and imported at print time, so country-posters is back under the 1.5 MB ceiling
development was RED on gates:post (verify bundles-shipped): country-posters.js had grown to 1.66 MB, over
the 1.5 MB ceiling, when the in-browser sheet generator landed. Measured by building without it: the
generator alone was 64.9 KB of the bundle (single-file builds inline any import), and Natural Earth's
places another ~170 KB (moved to a fetch in 8167415c5).

library/packet/poster-render.js is a pure module with no imports, so the tree now serves it verbatim
at assets/lib/poster-render.js (one generate-tree line) and posterSheets imports it BY URL at the first
print — resolved against the page, not the bundle, and kept out of the build with @vite-ignore. Every map
that can print a sheet sheds it; it is downloaded once, by a reader who prints, and cached across maps.

Measured on real builds: country-posters 1,662,962 -> 1,472,015 bytes (ceiling 1,500,000);
my-upg-100-list 1,231,789; verify-bundles-shipped passes; the bundle holds the runtime import and no
generator code. check:poster-browser-equiv now also asserts the served copy is byte-identical to its
source, and still passes byte-identical through makeZoneSheet and generateCountrySheet, which import the
served copy. gates:pre 73/73.
2026-09-29 06:23:20 +00:00
Doxa Maps
6c80284b0b The generated sheets fetch their capitals and cities at print time, instead of every page bundling them
Generating map sheets in the browser inlined Natural Earth's capitals and cities (170 KB) into every
bundle that can print one: single-file builds inline lazy imports, so the whole page paid for a table
only a reader who prints needs. The tree now serves it beside the boundaries (boundaries/
ne-places.json, one line in generate-tree's copy list; public domain, no credit to carry) and
posterSheets fetches it at the first print, with the boundaries. No asset base, or a failed fetch,
draws no city dots — the generator's own rule for absent data. The Node generator reads the same file
from its source.

Measured: my-upg-100-list 482 -> 432 KB gzipped, country-posters 547 -> 496 KB. check:poster-browser-
equiv still passes byte-identical through makeZoneSheet and generateCountrySheet, which now fetch the
served table — so the fetch path draws the same dots Node does. gates:pre 73/73.
2026-09-29 06:23:20 +00:00
Doxa Maps
7067c0ae96 Fix what an adversarial review of the print standard confirmed: 21 defects, each re-checked against the code
Ten agents read today's branch (five finders, one skeptic per area who had to confirm each finding
against the current code); 21 distinct defects survived. All are fixed here.

Generator and its proof
- generateCountrySheet fed only ADM0, so a country beside India or China printed without their
  provinces (Nepal: 251 KB against Node's 921 KB). It loads the full boundary set, as the driver does.
- check:poster-browser-equiv rebuilt its own feed, so it could not see that. It now calls what the
  browser calls (makeZoneSheet, generateCountrySheet — Nepal), deletes the target before the Node run,
  runs Node with a clean environment, and FAILS (not "not measured") when the Node generator breaks.
  Proven two-sided: it fails with the ADM1 fix reverted.
- The facts table's country list sorted in the reader's browser locale; pinned to en-US (Node's
  default, so Node's output still hashes to the baseline).
- zone-sheet-print.js imports the library relatively, so bun, Node and the bundler resolve it alike.

Print seams and wiring
- The wall's Poster Map button used its own stored-file fetch and never the generated sheet, so every
  country poster 404'd on the published tree; it now prints the generated sheet first.
- Country Posters wall: card stocks passed (its Card Deck could never print), iso3-names passed (zh/ar
  page 1 kept English names), the stored fallback reads previews/ (where the tree serves them).
- Card-stock labels render at render time, so a locale change relabels them.
- The records provider caches by language AND list, and does not keep a failed membership load.
- The UPG-100 legend's Card Deck returns quietly on a parent row instead of an evaluator error; the
  selection fetch uses PRINT_FIELDS.

Packet
- The centered-set caption counts what is DRAWN (a ring too full to separate reports its remainder).
- Each ring's number lane is sized to its own radius (ring 1 put a dot under its badge).
- Per-box line keeps a decimal below 10 people ("≈ 0" for small lists); worksheet rows name the country;
  the localized poster kicker is fitted against the poster's real 19px, not 22.

Plurals and gates
- regions.countryCount / regionCount were authored in English zero|one|other order in de, es, fr, hi, it
  and pt, so CLDR picked "27 Land", "1 países". Rewritten in each locale's own order; check:plural-forms
  now fails a string in English order (proven on the old German string).
- check:optional-t-equivalence (gates:post) crashed on the new plural fallback's imports; it injects the
  real cldrChoiceIndex and asserts the plural path (ru 21/3/5, en 0/1/7).
- check:packets-all and check-packet-render counted people as cards only, so every lightweight packet
  read as "a packet of nobody"; worksheet rows count, and the browser gate checks the grid's top fill
  (the renamed .lc-frame) instead of the retired scatter. check:packet-variants now reads the grid itself
  — 5,200 boxes, Jesus' 1,716 from the top, the % shading from the top — proven: it fails on a renderer
  that shades the last boxes.
gates:pre 68/68, check:optional-t-equivalence PASS, both bundles build.
2026-09-29 06:23:19 +00:00