Commit graph

131 commits

Author SHA1 Message Date
doxa-maps-88655c70
ab83fd4ca7 Chart legends: the conventional pattern, nothing cleverer
Some checks are pending
build-gate / build (push) Waiting to run
Driver, after rejecting both experiments: "I cannot scroll right, and the
column numbers have this super white background -- it looks odd, it was to
be invisible... I am not impressed with that design. Use conventional
designs only."

He is right, and the two rejected variants earned it: scroll-to-read hid
the numbers' geometry behind a gesture, and the frozen column needed a
painted backing precisely BECAUSE something could slide beneath it. The
conventional row every chart library ships has neither problem: swatch,
label that ELLIPSIZES at the available width (full name in the row's
existing tooltip), count right-aligned in a fixed-width slot. No horizontal
scroll, no sticky, no background -- the number needs no backing because
nothing can ever slide beneath it.

Measured, both legends: transparent count background, position static,
right-aligned, ONE shared right edge, labels ellipsizing, zero horizontal
scroll. Screenshot read before pushing.
2026-09-23 22:14:33 +00:00
doxa-maps-88655c70
80d5981229 People-group links go to doxa.life from any host; stale layouts retire
Some checks are pending
build-gate / build (push) Waiting to run
THE LINK (Driver pasted localhost:5174/research/pitaguari/): the card's
"Full profile" base defaulted to root-relative /research/, which resolves
against whatever origin hosts the element -- right on doxa.life, wrong on
the dev server, an iframe, or a partner site. Same class as the ../../
child-src bug. Adopted the pattern the shared PeopleGroupDetail already
uses: window.DOXA_SITE_URL override, else the absolute canonical origin.

TWO owners had to move, and the first fix alone measured as a no-op: the
computed's fallback AND the prop's own default -- a non-empty prop default
is always truthy, so the canonical branch was unreachable until the prop
default became ''. Unset must mean unset.

Playwright on a non-doxa origin: all 24 rendered links now
https://doxa.life/research/<slug>, zero relative.

LAYOUT KEY v2: the Driver sees unequal sidebars because his browser
restores a layout saved under the OLD defaults (rail 264), and a restored
value outranks any default by design. Defaults changed materially today, so
the storage key is versioned: everyone starts once from the new symmetric
defaults; a fresh drag re-personalises from there.
2026-09-23 22:10:18 +00:00
doxa-maps-88655c70
8d41a8fdef Selection turns the table bar into a toolbar instead of a flood
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "when I click on a people group the header of the table is flooded
with buttons that overflow and don't make sense" -- and, on one of them:
"it does not need a Clear Selected button in the header because the footer
of the page already gives me that."

THE CLEAR X IS GONE from the header pill. Verified what actually clears
before repeating the premise: the thead master checkbox and the detail
drawer's close both call clearSelection, so the removal deletes a duplicate
trigger, not the capability.

THE FLOOD HAD TWO CAUSES, both measured at 1280 where the middle column is
686px: the centred hint, and -- after hiding it -- the in-flow title
squeezed to 48px and wrapped INTERNALLY ("People"/"Groups" on two lines),
growing the bar to 43px and tumbling the spacer. While a selection exists
the title and count duplicate what the pill and the section header already
say, so all three yield, state-scoped, and return on clear. The title also
gets white-space nowrap unconditionally: a title may never two-line itself.

Playwright, both 1280 and 1440, real dispatched clicks, full round trip:
select -> bar 32px, one line, zero overflowing controls, no clear x;
uncheck -> count 0, title and count return.
2026-09-23 21:46:21 +00:00
doxa-maps-88655c70
d9eca08c44 Chart counts are a real table column, on the Driver's own prescription
Some checks are pending
build-gate / build (push) Waiting to run
"The number column needs to use some type of TABLE css so the numbers
really are set apart as a column, uniform and justified right, so it does
not move when I scroll left and right."

He is right that table layout is the honest tool: display:table guarantees
every row's count cell shares ONE column edge in every browser ever
shipped. The previous subgrid attempt measured correct here but subgrid is
25 years younger than table layout; the table needs no support caveat.
Rows are table-rows, swatch/label/count are cells; the count cell keeps
sticky right:0 with an opaque background so a scrolling label slides under
a pinned column.

Measured, both legends: display table, cells right-aligned, exactly ONE
unique right edge at rest AND while scrolled. And with the rail widened to
296 in the previous commit, neither legend currently overflows at all --
the wider sidebar bought the words their room, as intended.
2026-09-23 21:35:40 +00:00
doxa-maps-88655c70
564af57c4e Symmetric sidebars: the charts rail matches the left sidebar, centring the map
Some checks are pending
build-gate / build (push) Waiting to run
Driver: 'the charts sidebar can be wider so it is the same width as the
left sidebar, which will put the map in the exact centre.' rail 264 -> 296
everywhere the number lives (inline var, both CSS fallbacks, the
composable's DEFAULTS, the doc comment) -- one quantity, every owner moved
together this time.

Measured: facet 296, rail 296, map margins 297 both sides, centred true.
2026-09-23 21:33:07 +00:00
doxa-maps-88655c70
e315487e2b Reclaim the fold: dashboard 849 -> 623, and the people are visible
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "it is a priority to reclaim height... so I can see half of the
first row of the people groups grid." Measured after: shell bottom 623,
first card top 640, half the card inside a 900px viewport -- the faces show.

THE HEIGHT: map 360 -> 260 and shell follows the same row arithmetic
(260 + 32 + 7x26 + 28 = 502); headers slim again (topbar 44 -> 38, stats
26 -> 22). Seven data rows unchanged.

THE ZOOM: a shorter map now shows MORE world, not less. Default zoom
1.45 -> 1.1 opens on the whole sphere, minZoom 1 -> 0.5, and the mercator
"no duplicate world" floor is skipped on the globe -- a sphere has no
second world, so the floor only stole zoom-out range.

TWO MORE SECOND-OWNER BUGS FOUND ON THE WAY, both now dead:
- research-dashboard.vue passed its own defaults {mapH:360, shellH:720} to
  useDashboardLayout, silently overriding the composable's DEFAULTS -- the
  bundle carried BOTH and the stale copy won, so two reclaim passes changed
  nothing. The override is deleted; one owner.
- one grid-template-rows fallback spelled 40svh WITHOUT a space and
  survived the fallback sweep -- at 900px that IS 360, re-pinning the row.

ALSO: the chart count column is now a real GRID column (label 1fr, count
auto, rows joined via subgrid), because per-row flex put each count at its
own row's right edge the moment the legend scrolled -- "less like a column,
more like cards". Measured: one shared right edge before and after scroll.
2026-09-23 21:31:05 +00:00
doxa-maps-88655c70
11bd2bcd5c Religion chart shows high-level religions once; count columns align
Some checks are pending
build-gate / build (push) Waiting to run
RELIGION ROLLUP. "It should not mention Ethnoreligion and Islam multiple
times -- just the high-level religions." The rollup already existed as the
religion_family facet; the donut was simply pointed at the fine-grained
religion facet. One word moves it. Measured after: Hinduism 693, Islam 650,
Ethnoreligion 610, Buddhism 83 -- each named once. The full Sunni/Folk
breakdown still lives in the facet rail's Religion tree. religion_family is
dashboardOnly by design, so a slice click narrows the dashboard but not the
grid below -- documented, and the right trade for a glanceable donut.

COUNT COLUMN. Uniform, right-justified, immovable, in BOTH donut legends:
min-width 3.4em + text-align right on the shared .rd-li rule, on top of the
existing sticky right:0 and tabular-nums. Measured: every count 42px wide,
right-aligned, digits in one scannable column.

Known nit, deliberately left: Religion can show two "Other" rows -- one is
a real family label, one is the topN overflow bucket. Worth a rename of the
overflow bucket later, not a silent merge of different things.
2026-09-23 21:20:59 +00:00
doxa-maps-88655c70
64f1f2bf7b Table fullscreen: native Fullscreen API, so exit and Export CSV are reachable
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "there is no way to exit full screen or export csv when you enter
full screen on the table."

The bar carrying both controls IS inside the wrap, but the overlay used
position:fixed, which inside a shadow root resolves against any
transformed or contained ANCESTOR rather than the viewport. In the cap-5
shell that pinned the wrap to a pane whose top sat above the visible area,
so the bar -- and its exit and export controls -- rendered off screen. The
leaf host has no such ancestor, which is why every leaf measurement passed
while the shell was broken: the two-host rule again.

element.requestFullscreen() is the wheel the platform ships: guaranteed
viewport coverage regardless of ancestor transforms, native ESC, and a
fullscreenchange event so the button state follows every exit path. Two
shadow-boundary details mattered and are documented in the code:
document.fullscreenElement retargets to the shadow HOST (and
host.contains(inner) is false across the boundary), so the sync reads
getRootNode().fullscreenElement; and our ESC handler delegates to
document.exitFullscreen() so all consumers stay in sync. The .is-full
overlay stays as styling hook and API-less fallback.

Verified in the shell with a screenshot read before pushing: native
fullscreen engages, "Exit full screen" and "Export CSV" both visible
top-right, full column headers, ESC exits.
2026-09-23 21:14:29 +00:00
doxa-maps-88655c70
df1ce0feed Collapsing the table gives its space to the map, in page-scroll too
Some checks are pending
build-gate / build (push) Waiting to run
Driver: collapsing the table is supposed to increase the size of the map;
instead it introduced white space under the collapsed bar.

The collapsed template still used 1fr for the map row, and 1fr cannot grow
in an auto-height shell -- no fixed total to divide -- so the freed space
pooled under the bar. The page-scroll collapsed state now derives from the
same shell budget as the open state: map = shell minus the bar, on both the
row and the element. One owner, every state.
2026-09-23 21:06:00 +00:00
doxa-maps-88655c70
a9655e319e One fallback, one owner: the sidebar cap now agrees with the table about the shell
Some checks are pending
build-gate / build (push) Waiting to run
Two rules read --rd-shell-h with DIFFERENT fallbacks: the table keyhole fell
back to 602px (seven rows) while the sidebar cap fell back to 80svh = 720px.
Nothing sets the var at rest, so the sidebars believed the shell was 119px
taller than the table did. The spanning side columns stretched the auto
table row to fit, and the 118px surplus painted as the grey band under and
around the table -- and made divider drags look broken, because the row
would not follow; the slack just moved.

The one-quantity-one-owner rule applies to FALLBACKS too: same var, same
fallback, everywhere. Cap unified at 602px.

Measured through real drags, screenshot read before pushing:
  at rest        gap 1px, 7 rows, table dead-space 118 -> 2px, sidebars -2px of table bottom
  divider up 80  gap 1px, 10 rows, sidebars still flush
  shell +120     gap 1px, 15 rows, shell 733 -> 855, sidebars follow
2026-09-23 21:02:15 +00:00
doxa-maps-88655c70
510af26fb5 Close the grey gap: pin the map ROW to the same var as the map element
Some checks are pending
build-gate / build (push) Waiting to run
In page-scroll the map row was 1fr, which stretches to the implied content
height while the map element stays pinned at --rd-map-h -- the row grew,
the element did not, and the difference painted as dead space above the
table. Reported three times. The row now uses the same var as the element,
so they are one quantity and the slack cannot exist.

This exact rule was applied earlier today and reverted on a false signal --
a probe said no shell, but the real cause was a dead dev server, not the
CSS. Re-verified this time against a live server: map bottom 488, table top
489, gap 1px, seven data rows, with a screenshot read before pushing.
2026-09-23 20:56:41 +00:00
doxa-maps-88655c70
ae01086662 The galaxy the way Mapbox does it: engine fog with stars, black space
Some checks are pending
build-gate / build (push) Waiting to run
Mapbox renders space and the starfield engine-side through the fog API
(documented in their globe guide: stars and atmosphere are customized with
the fog property). Applied their documented example values with one change:
space-color is pure black on the Driver's instruction, star-intensity 0.6.

The container behind the sphere is plain black now, no gradient -- on
Mapbox the engine paints its own space so it never shows; on an engine
without fog (MapLibre) black is the honest fallback.

Verified by reading the rendered screenshot: stars in black space,
atmosphere glow at the limb, vivid blue oceans, the choropleth legible.
2026-09-23 20:54:57 +00:00
doxa-maps-88655c70
3bd2a7bebf Dashboard globe runs on Mapbox Standard: the look the Driver kept naming
Some checks are pending
build-gate / build (push) Waiting to run
Every wording of the ask described Mapbox itself -- the globe space style
with stars, the atmosphere, water like the Mapbox style. Mapbox Standard
renders all of that natively; no found clone matches the original. The
working baked token is already accepted as public by explicit ruling.

One key changed. The MapLibre path (VersaTiles Colorful, keyless) stays
fully built and reversible by flipping provider back.
2026-09-23 20:51:22 +00:00
doxa-maps-88655c70
1fbc29e6e1 Galaxy corrected: day-side Earth in space, not a dark basemap
Some checks are pending
build-gate / build (push) Waiting to run
The Driver, correcting the first attempt: "I don't want a dark basemap, I
didn't ask for one. I asked for a spherical globe with a black background --
the galaxy, like the Mapbox map -- with blue oceans like the Mapbox map, and
the land was not to be black."

The first galaxy tinted dark tiles: black land, recoloured water -- the
night side of Earth. What was asked for is the DAY side seen from space.

So the style entry is now a normal-coloured basemap with no paint overrides
at all (mapbox standard / OpenFreeMap liberty), and the blackness lives
where it belongs: the map CONTAINER behind the sphere, a deep radial space
gradient scoped to [data-projection="globe"] so flat mercator keeps its
neutral ground.

Verified by reading the rendered screenshot before pushing: a real sphere,
blue oceans, normal land, the greyscale choropleth legible on top, black
void behind the globe, and the old caption gone.
2026-09-23 20:42:26 +00:00
doxa-maps-88655c70
1dec28b2d8 Remove the map caption, and make the globe projection reach MapLibre
Some checks are pending
build-gate / build (push) Waiting to run
The caption "UPG choropleth - click a country to filter - click a pin to
select" is gone, along with its now-dead .rd-maptag rule and a comment that
claimed the bottom-left corner was occupied.

GLOBE: the projection default was already 'globe', but the request was not
arriving. Mapbox GL takes setProjection('globe') -- a bare string. MapLibre
v5 takes setProjection({type:'globe'}) -- an object. Passing a string to
MapLibre throws, an empty catch swallowed it, and the map silently fell back
to flat mercator with no console error. That is why it appeared exactly when
the dashboard moved to provider maplibre.

Now tries the object form first, then the string, so one call serves both
engines. The catch stays so an engine with no projection support degrades
rather than breaking the panel -- but it now catches a real unsupported case
instead of masking a shape mismatch.
2026-09-23 20:39:18 +00:00
doxa-maps-88655c70
d9fbff14bb Galaxy style on MapLibre, and restore both dashboard resizes
Some checks are pending
build-gate / build (push) Waiting to run
GALAXY + MAPLIBRE. New 'galaxy' style in the shared registry -- deep-space
ground, blue oceans -- with both a mapbox and a keyless MapLibre URL, and a
paint override that is applied only if the loaded style actually has the
layer, so a miss is a silent no-op rather than a break. The dashboard now
runs provider 'maplibre' and mapStyle 'galaxy'. Reversible by two config
keys; the Mapbox path is untouched.

BOTH RESIZES RESTORED. My seven-row change had hard-capped the table's
keyhole at 7 x 26px, which read like a default and was not: nothing writes
--rd-table-rows, while the drag handles write --rd-shell-h and --rd-map-h.
So the cap was unreachable by any gesture and the Driver lost both resizes
at once, plus a grey gap where the freed pixels had no consumer.

Fixed by restoring the derivation and moving the default instead: the
keyhole is once again shell - map - bar - foot, and DEFAULTS.shellH drops
720 -> 602 (map 360 + bar 32 + 7 rows x 26 + foot 28) so the table OPENS at
seven rows while staying fully draggable. CSS fallback matched to 602.

Measured before the revert: shell handle 849 -> 731 with rows 7, table
divider map 360 -> 280 with rows 10, gap 1px after each.

Also carries the charts' frozen count column (sticky right:0 with an opaque
background, so a scrolled label cannot slide under the number) and the
ViewSelect library-index entry.
2026-09-23 20:35:38 +00:00
doxa-maps-88655c70
378ecb9ae6 UPG-100: lead with Prayer Resources, map second
Some checks are pending
build-gate / build (push) Waiting to run
The first tab a new user meets is now the poster/resources pane, not the
map. That pane already explains what a UPG-100 list is at the top and shows
the posters, so it needs no explanation; a choropleth of zones does. Maps
need not be primary — a familiar list or grid is the on-ramp people use
before they transition into the map.

Renames the tab from "Prayer Lists" to "Prayer Resources".

Pane order lives in at[0], not in array order: buildPaneColumn sorts by
at[0] while ViewSelect renders the array verbatim, so both are swapped
together or the tab strip desyncs from the scroll column. The active view
is now derived from that same sort instead of a hardcoded 'map'.

In the wrapper shell the map pane is resolved by PROFILE rather than by
"pane 0 is the map": under the positional rule, reordering mounted the map
element into the poster pane, and "Show on map" scrolled to the wall you
clicked from. The shell's tab strip also resets to a profile's first view
on switch, since a stale 'map' id is valid in nearly every profile.

ViewSelect keeps each tab on one line (nowrap + ellipsis), ported from the
shell's own mobile rule that the original extraction left behind. Measured
at 390px: "Prayer Resources" wrapped inside its button and grew the strip
45 -> 60px; it now holds 45px at every width from 1280 down to 390.
2026-09-23 20:33:00 +00:00
doxa-maps-88655c70
1d4fe185e3 Full table headers via horizontal scroll, and a full-screen table
Some checks are pending
build-gate / build (push) Waiting to run
Driver, on the people-groups table under the map: "I would like to add a
horizontal scroll bar so that instead of ABBREVIATING the headers, we can have
the FULL headers in the table", and "I would also like to add a FULL SCREEN
button to the table, so that if you want you can open up the table to be full
screen."

These ship together on purpose. The table now shows seven data rows by default
because we are reclaiming vertical space; a deliberately small table is only an
honest trade if the reader can get a big one on demand. The cap and its release
are one change.

HEADERS ARE NO LONGER ABBREVIATED. text-overflow:ellipsis is gone from .rd-th.
Ellipsis destroys information with no way to recover it -- "Engageme..." is not
a column name, and unlike a truncated VALUE it has no menu behind it. Horizontal
scroll preserves it: the full text always exists and is always reachable. This
is the same mechanism already applied to the chart legends (nowrap +
overflow-x:auto), deliberately reused rather than a second invention.

THE FLOOR IS MEASURED, NOT ESTIMATED. Columns are flex:<w> 1 0, which can never
overflow -- flex-basis 0 plus shrink means every column collapses to whatever
the container offers, so scrollWidth always equalled clientWidth and overflow-x
had nothing to scroll. Each column now carries a min-width derived from its own
header. A first pass guessed 7.1px/char, which put the summed floor at 675px
against an 878px scrollport, so it never bound and no scrollbar could appear at
any width. Measured live by cloning each header out of flex flow: worst case is
9.75px/char (Language 78px/8ch), so 9.9 covers every label, +12px reserves the
sort indicator. Summed floor 742px + 30px checkbox.

That floor sits BELOW the 1440px scrollport on purpose. At 1440 the eight
columns genuinely fit and there is nothing to scroll; forcing a scrollbar there
would invent a defect. It binds at 1280 and below, which is exactly where the
truncation being complained about was happening.

ROW WIDTH IS SHARED, NEVER INTRINSIC. An intermediate pass used width:max-content
to create the overflow. It did -- and sized every row to ITS OWN content, so a
row holding "Lao People's Democratic Republic" laid out wider than the row above
it. That is not a table but a stack of independent flex lines: screenshot-
verified ragged, with Country/Region/Language stepping left and right on
consecutive rows while the header, correctly aligned, disagreed with all of
them. The measurement said so too and was read past: scrollWidth was 969 at the
top of the list and 1051 after scrolling, i.e. the table's width depended on
which rows happened to be rendered. Head, rows and footer now share one explicit
width (summed floor as min-width, width:100%), so nothing is intrinsic and the
column grid holds at every scroll offset.

The sticky header stays pinned on the vertical axis only, which is correct: it
must travel sideways with the body or the labels detach from the columns they
name. The footer is a SIBLING of the scrollport, so it is translated in lockstep
from the scroll handler -- otherwise the population total slides out from under
its own column.

FULL SCREEN is CSS pseudo-fullscreen, not requestFullscreen(). The native API
promotes the element to the browser's top layer, detaching it from the cascade
this table is built on, and needs allow="fullscreen" to work in the iframe embed
this dashboard actually ships in. Pinning the wrapper with position:fixed keeps
the element where it is in the tree, so inherited tokens and ancestor rules keep
applying and only the two rules we mean to override are overridden. Esc exits.

The seven-row cap is released with max-height:none !important, and !important is
the honest tool rather than a specificity war: the first attempt lost 0-2-0 to
the shell's 0-3-0 selector and rendered a 900px box containing a 182px table --
the exact failure its own comment warned about. That shell rule is also being
rewritten concurrently, so matching its selector would break on the next edit.

library/components/map-controls/FullscreenButton.vue was read first and does not
fit: it resolves its target as the SHADOW HOST, so pointed here it would
fullscreen the whole dashboard rather than the table; it renders MapControlButton,
a circular control built to float over a canvas; and it depends on vue-i18n,
which this bundle does not use. Making it fit means adding a target prop and a
chromeless variant to a component with seven map consumers to serve one non-map
caller, so this stays local until a second table wants it.

Verified at 1280 and 1440, real browser, live data: headers complete with
text-overflow clip and no cell clipped; scrollWidth 867 vs clientWidth 718 with
the last column's left edge moving 1045 -> 896; rows recycle (abai-sungai ->
deaf-comorans) with the rendered count bounded at 14; zero ragged columns and
zero head-vs-row drift at every scroll offset; fullscreen 244 -> 900px with the
keyhole 182 -> 838 and 45 rows rendered, restoring exactly on exit.
check:fits-screen 17/17.
2026-09-23 20:25:08 +00:00
doxa-maps-88655c70
afe6abe556 Plugin studio: show what a composite is made of, in one panel
Some checks are pending
build-gate / build (push) Waiting to run
A "Composed of" panel now sits under the plugin title, above the live
preview. For a composite it lists one row per directly-nested child:
name, element tag, and a capability badge, each row a link to that
child's own entry. A leaf plugin shows no panel.

Composition is no longer navigation. The rail had carried a collapsible
folder per composite, which re-listed children that already had their
own home — people-groups-grid held six nav nodes, so there was no single
name to call it by when reporting a problem. The rail is back to one
node per plugin in its own capability tier (31 nodes, 31 manifest rows,
none repeated) and the relationship lives on the plugin's own page.

Depth is one. A nested composite shows as a single row and does not
expand its own children; its breakdown is one click away, on its page.

The badge reads the manifest's declared profile type, so retyping a
profile moves its badge with no code change here. A declared child that
is published nowhere is counted as "not published" beside the heading
rather than dropped in silence — big-picture names bp-research-header,
which is an internal part, not a plugin. The list caps its own height:
the layout's header row is auto-sized, so ten uncapped rows would have
taken height from the preview instead of overflowing.

Rows are buttons with descriptive aria-labels, reachable and activatable
by keyboard. No emoji glyph on a row: this box has no emoji font, so
every one rendered as an empty box, and the badge says it better.
2026-09-23 20:21:48 +00:00
doxa-maps-88655c70
a69386582c Reclaim height: seven data rows, slimmer headers, charts scroll sideways
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "we are trying to reclaim height."

TABLE = SEVEN DATA ROWS, stated in ROWS rather than pixels. The old formula
derived the keyhole by subtraction (shell - map - bar - foot), so the row
count was whatever fell out -- measured 300px = 11 rows. Now
calc(var(--rd-table-rows,7) * var(--rd-table-row-h,26px)), so the promise is
the declaration: change the row height and seven rows stays seven rows.
His own arithmetic settles an earlier ambiguity (he had said both 77 and 7):
seven is DATA rows, excluding field, footer and table header.

HEADERS 137px -> 125px: topbar 52 -> 44, stats 30 -> 26, both now vars so a
host tunes them in one place instead of three grid-template-rows that must
agree.

CHARTS scroll sideways instead of wrapping. This is the third pass on that
trade and it resolves rather than reverses it: pass 1 was nowrap + ellipsis +
overflow-x HIDDEN, so labels were truncated and unrecoverable -- the defect
he rejected. This is nowrap + overflow-x AUTO, so the full label still exists
and is reachable. Legend cap 176 -> 120.

Measured at 1440x900: grid-template-rows 44/55/26/476/244, keyhole 182px,
exactly 7 data rows, shell 863 -> 849 with the map gaining 116px.
2026-09-23 20:03:52 +00:00
doxa-maps-88655c70
e5c66272c2 Tighten the vertical legend's row spacing without touching the type
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "the spacing between the rows is too thick, they take up way too much
height -- the padding should be decreased per row."

Row vertical padding 3px -> 1px, min-height 22px -> 18px, section header
padding 8px -> 5px.

Font-size is deliberately untouched at 12px. It is not a lever here: the
Driver previously rejected shrinking it ("super tiny text that cannot be
read"), and the note beside .fr-lbl records that the width for the count came
from the bar and the gutters for the same reason. Only whitespace moved.
min-height 18px still clears a 12px glyph at line-height 1.3, so no descender
is clipped.

Measured live at 1440x900 with four sections open, 69 rows rendered:
  row height 24px -> 20px
  computed padding 1px/1px, font-size 12px unchanged
  rail content height 1966px -> 1424px (542px saved)
2026-09-23 19:58:39 +00:00
doxa-maps-88655c70
02baa2d42b Switch the choropleth from heartbeat-red to the greyscale ramp
Some checks are pending
build-gate / build (push) Waiting to run
Driver ruling: "instead of the red, we need to use the grey scale from grey
to black." The greyscale preset was built earlier at his request as a
swappable alternative; this is him taking the swap.

One config key moves both the choropleth and its legend together, which is
why it was built as config rather than scattered hex. Ramp is
#e8e8e8 / #a9a9a9 / #6f6f6f / #3a3a3a / #060606.

Verified in the shipped bundle: colorRamp "greyscale", the five stops
present, sfc-parse passes.

Worth knowing for the follow-up: the colour research measured the ramp
change as roughly neutral for pin visibility (worst case 1.96 greyscale vs
1.84 red). The real fix for pin contrast is the lightness-conditional stroke
-- dark ring under a light pin, white under a dark one -- which lifts the
worst case to 4.51 and works on either ramp. Not done here; that is its own
change.
2026-09-23 19:57:08 +00:00
doxa-maps-88655c70
ca4343c25d Regions mounts its cap-4 composite; UPG-100 drops its duplicate share and shrinks its dots
Some checks are pending
build-gate / build (push) Waiting to run
REGIONS WAS RENDERING AS A CAP-1. The regions-mfe composite (nested-elements-mfe
over regions-list + region-dashboards, with the shared Select View strip) had
shipped, but the shell entry still named the bare `regions-list` leaf -- the
cap-1 half -- so one flat list rendered and the composite sat unused. The cause
was in the shell, not the regions bundle. Compare the UPG-100 entry, which names
`upg-100-mfe` and is why that tab has tabs.
Measured in the shell after: Select View present, options "Regions List" /
"Region Dashboards".

DUPLICATE SHARE BUTTON. The map carried its own ShareButton while the composite
header already mounts one. Removed the map-level control here rather than in the
shared component, because sharing is per-map policy: Prayer Progress is the map
meant for embedding elsewhere and keeps its own button; a map only ever seen
inside a composite does not need one. Unused import dropped.

DOTS WERE OVERCORRECTED. An earlier pass raised z=1 radius 4 -> 6 to fix "hard to
see". Sound reasoning, wrong amount: it made this the largest-dotted map in the
set (all-people-groups is 2.2 at z=0). Now 5 at z=1, keeping a deliberate step
above the dense map without leaving the family; the halo carries visibility.

Gates: template-literal, sfc-parse, sri all pass.
2026-09-23 19:37:55 +00:00
doxa-maps-88655c70
1cf4e89dab Resolve a composite's child bundles against the tree root, not a guessed depth
Some checks are pending
build-gate / build (push) Waiting to run
Driver-reported console text:
  Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js

A composite declares children as src: '../../<bundle>/<bundle>.js'. That literal
is right from a one-deep leaf and wrong from a two-deep one, and the generator
emits both:
  .../research-map-plus-grid/index.html          ../../ -> /doxa-maps/          200
  .../research-map-plus-grid/default/index.html  ../../ -> /doxa-maps/rmpg/     404

So the path encoded an assumption about the host's depth that the host does not
control and cannot see. It survived because every gate exercised the shallow
leaf, which is genuinely fine.

StackLayout now derives the tree root from the document's own pathname (up to
and including the /doxa-maps/ segment) and rebuilds the path from there --
depth-independent by construction, so a third leaf depth cannot reintroduce it.
Absolute, protocol-relative and root-relative srcs pass through untouched, and
a missing /doxa-maps/ segment returns the original string, so this can only fix
a path, never break a working one.

Measured, both leaves, real browser with live data:
  one-deep  mapEl=true mapH=792 canvasH=856 0 4xx
  two-deep  mapEl=true mapH=792 canvasH=856 0 4xx   (was 404, map absent)

Also carries source that had been left behind by its own built output:
the cities/gospel legend bounding (shipped in 5008a6f as output only) and the
per-profile config carrier in generate-tree.mjs, plus a new fits-screen clause
that wheels inside every column rather than one.
2026-09-23 19:34:28 +00:00
doxa-maps-88655c70
d97cf9eab5 Say why a tab could not load instead of painting an empty grey pane
Some checks are pending
build-gate / build (push) Waiting to run
The Simple Research tab rendered as a featureless grey rectangle with no
message, no overlay and nothing in the console. It reads as a broken app,
and it hid a plain missing-file condition through four separate reports.

Two silent failure paths produced it, one layer apart:

- doxa-maps-spa swallowed a failed manifest.json fetch. Every tab except
  Big Picture is composed from that manifest (Big Picture alone is baked),
  so losing it leaves each tab with no scriptUrl. mountTab then returns
  early and the pane stays in state 'idle' -- which renders neither the
  loading hint nor the error hint. The manifest fetch now tries the built
  tree location as a second candidate and, if every candidate fails, sets a
  message the pane displays.

- StackLayout's per-section catch deliberately left a failed section's host
  empty so the rest of the stack still renders. Keeping the stack alive was
  right; leaving the band blank and silent was not. A failed section now
  shows which element failed and which URL it came from, and still does not
  abort its siblings.

Tree-relative scriptUrls are also resolved against the manifest they came
from rather than the current document. The two roots are identical in the
built tree and differ elsewhere, which turned every composed tab's bundle
into a 404 against a path that exists nowhere.

Measured, built tree: research-map-plus-grid mounts both children, 4778px
of content, live canvas, zero 4xx -- unchanged. dashboard-plus-grid,
stack-demo and research-map-plus-grid all render with a painted canvas and
no failure cards. Where the bundles genuinely cannot be reached, the pane
now names the element and the URL instead of showing grey.
2026-09-23 19:14:27 +00:00
doxa-maps-88655c70
bca3d58c3e Regions element gets the Select View tabs, shared with UPG-100
Some checks are pending
build-gate / build (push) Waiting to run
The Regions bundle shipped only tab 1, so the Select View switcher the
UPG-100 list shows had nowhere to live. Adds the composite that carries
it over two panes -- Regions List and Region Dashboards -- plus the
Region Dashboards tab itself, which summarises a region or a country
from the same live endpoint and the same rollup tab 1 uses.

Clicking a region heading or a country row on tab 1 now forwards that
selection and swaps to tab 2 with it applied.

The switcher itself is extracted into library/components/ViewSelect.vue
rather than copied a third time: it had two implementations already, in
the app shell and hand-copied into upg-100-mfe. It takes the views and
the current id as props and knows nothing about maps or panes, so the
placement stays with each header. UPG-100 now renders from the shared
component and is unchanged on screen -- same 45px height, 13px labels,
8px radius.
2026-09-23 19:08:00 +00:00
doxa-maps-88655c70
5008a6ff5f Bound the cities and gospel legends so the map controls stay clear
Some checks are pending
build-gate / build (push) Waiting to run
Both maps drew their legend as a full-width strip (left:8px right:8px --
measured 1264px of a 1280px map, 98.75%), which ran underneath the floating
control rail. MapToolbar sits at inset-inline-end:3px with 32px buttons, so
any box reaching right:8px intersects it at every viewport; the rail's higher
z-index kept the buttons clickable, which is why the defect survived three
reports as "looks broken but works". Gospel ran under four of five buttons,
cities under two, the difference being only legend height.

The legend is now a bounded, left-anchored panel imitating the shared
SemanticTreeLegend: left:8px with width var(--map-legend-width,380px). Being
bounded on the left, it cannot reach the right rail at all -- a geometric
guarantee rather than a tuned z-index. Below 480px it keeps a 46px gap so the
rail still clears. Measured after: legend 380px of 1280 (29.7%), zero
intersection with all five buttons, 857px clearance, and a dispatched click on
the top control landing on button.mcb via composedPath. Mobile at 390px: 336px
wide, 11px clearance, zero intersections. Both files kept byte-identical.

Both maps also showed the shared "Tap a coloured dot" hint while shading
country polygons and drawing no dots at all -- a hint pointing at something the
reader cannot see. PinHint documents a pinHint:false opt-out, but the
standalone leaf page built its config from a hardcoded object, so no profile
could reach that key. <profile>.meta.json now carries an optional `config` map
merged into the leaf's BASE_CONFIG, with generator-owned keys reserved and the
carrier stripped before the manifest is written so its published shape is
unchanged. Both maps opt out; the hint is gone from both.
2026-09-23 19:07:13 +00:00
doxa-maps-88655c70
5520b32173 Size the UPG-100 dots for a hundred marks, not twelve thousand
Some checks are pending
build-gate / build (push) Waiting to run
At world zoom the dots were radius 4 with a 1.2px halo -- the same scale
all-people-groups uses for ~12,379 marks, where density itself carries the
signal. This map draws ONE HUNDRED, alone on a whole planet, so the same
radius reads as almost nothing.

Radius 4 -> 6 at z=1 (+50%), tapering back to the original 9 by z=8 where
geography supplies the context. Halo widened 1.2 -> 1.8 in step, so it stays
a frame rather than a smudge: a 1.2px ring around a 6px dot is
proportionally thinner than around a 4px one, which would have spent the
size increase and lost the contrast that made small dots findable at all.

Verified in the shipped bundle: new interpolation present, old values gone,
0 data 404s, legend mounts, canvas at real height.
2026-09-23 18:47:36 +00:00
doxa-maps-88655c70
1bf42e2110 Let the dashboard element size to its content when it is page-scrolling
Some checks are pending
build-gate / build (push) Waiting to run
:host carries height:100%, which the fixed-viewport host needs and a
page-scroll host must not have: in a tall scroller, 100% is thousands of px,
so the element claimed a large empty tail below its own content. The shell
inside already switched to height:auto for this reason; the host never did.

A host cannot be matched by a class on its own descendant, so the pageScroll
flag is reflected outward as a data attribute and :host([data-page-scroll])
switches to height:auto with the min-height dropped.

Labelled UNPROVEN in the file, deliberately: dashboard-plus-grid imports this
as a plain Vue component rather than mounting the custom element, so the rule
is not reachable from there -- and that composite was measured stacking
correctly regardless (dashboard 0..864, grid 864..3031, gap 0). The rule
applies where research-dashboard is mounted as its own element.
2026-09-23 18:41:07 +00:00
doxa-maps-88655c70
6ba3ad76aa Legend rises into the removed search bar's space -- but only where it was removed
Some checks are pending
build-gate / build (push) Waiting to run
The UPG-100 legend was pinned 58px below the map top by a profile !important
rule whose job was to clear the geocoder pill. Measured with a real token in
both hosts: the standalone leaf DOES mount the geocoder (top 12, h 35), so 58
is correct there; the cap-5 shell mounts NO geocoder at all, so the same rule
stranded the legend below a pill that was not there -- the dead gap reported
repeatedly.

One host must not be fixed by breaking the other, so the offset is now
conditional on the pill existing: default 8px, pushed to 58px only when
:has(.mapboxgl-ctrl-geocoder) finds a rendered pill in the tree.

Also parameterizes the shared SemanticTreeLegend's offset as
var(--stl-top, 60px) with the max-height mirroring it, so any profile that
drops its search bar can raise its legend without an !important arms race.

Measured after: leaf legendOffsetInMap=58 with geocoder present; shell
legendOffsetInMap=8 with no geocoder. Full authoritative rebuild included --
composite freshness PASS, SRI PASS 45/45.
2026-09-23 18:30:00 +00:00
doxa-maps-88655c70
cb07c556e1 Plugin studio: collapsible composition sections per composite bundle
Some checks are pending
build-gate / build (push) Waiting to run
Every composite bundle (one whose manifest entry carries `composes`) now
gets its own collapsible folder in the rail, with the composite itself as
the top row and its composed children underneath, grouped/labelled the
same way the existing capability-tier rail already is. Reuses the
existing folder collapse/expand mechanism (VerticalTabsLayout, the same
one Experimental already uses) rather than inventing a second
interaction. Driven entirely from manifest.json's composes edges, so a
future composite needs no code change to appear. A child can legitimately
appear in more than one section (e.g. people-groups-grid nests into four
different composites) — that duplication is the point, not a bug.
2026-09-23 18:18:04 +00:00
doxa-maps-88655c70
eaa2c35286 Give the dashboard its own height handle, separate from the map/table split
Some checks are pending
build-gate / build (push) Waiting to run
Dragging the map/table divider in page-scroll mode was also resizing the
whole element, because --rd-map-h alone answered both "how is the map/table
space split" and "how tall is the element" -- Driver: "you improperly bound
that control to the separator line between the map and the table... that's
not the request I was making to resize the height of the page."

--rd-shell-h is now the independent total-height budget; the new bottom
handle (.rd-rz-shell) is the only thing that writes it. The existing
map/table divider still drives --rd-map-h, but only as the map's share of
that fixed budget, so it redistributes between map and table without
changing the sum. Table/facets/rail max-height all re-derive from
--rd-shell-h so the sidebars keep spanning exactly map+table at every drag.

Also closes a 28-30px sidebar gap the re-derivation exposed: the table
budget only ever subtracted the header bar's height, never the summary
footer's, so facets/rail undershot the table's real bottom edge.
2026-09-23 18:16:30 +00:00
doxa-maps-88655c70
5d87bd9db1 Poster print options: two per line, clear of the blue hover outline
Some checks are pending
build-gate / build (push) Waiting to run
The fieldset is anchored bottom:-26px and grows UPWARD as content wraps, so
at one-button-per-line (three lines tall) its top reached into the image --
which is where the blue hover outline lives. Widened the row from 82% to 96%
so two labels share a line.

Measured on all three poster cards at 1440x900: 2 button lines, height 50px,
top 3px BELOW the image bottom, zero overlap with the outline.

Includes the tree regeneration so every leaf's SRI matches its bundle
(a scoped BUNDLE= build alone leaves leaves stale -- check:sri PASS 45/45).
2026-09-23 18:10:35 +00:00
doxa-maps-88655c70
2a8318433f Remove a second stray backtick from research-dashboard shadow-CSS
Some checks are pending
build-gate / build (push) Waiting to run
Shadow CSS is emitted inside a JS template literal, so a backtick in a comment
terminates the string and the bundle stops compiling. Second instance in this
file; the first one already cost a broken build this session.
2026-09-23 17:25:55 +00:00
doxa-maps-88655c70
0ebb3e34ac Sidebars span map AND table; facet rail opens with its own scrollbar
Some checks are pending
build-gate / build (push) Waiting to run
The side columns stopped at the map's bottom edge, leaving the people-groups
table outside them. Cause was a single rule capping both side columns at the
map's height in page-scroll mode:

  .rd-shell.rd-page-scroll > [data-region="facets"],
  .rd-shell.rd-page-scroll > [data-region="rail"]{ max-height:var(--rd-map-h); }

The grid already spanned both rows correctly ("facets map rail" / "facets table
rail") -- this max-height was overriding it, which is why earlier passes that
adjusted the rail's CONTENTS never moved the bound. Replaced with min-height:0,
letting the grid do the bounding it was already doing.

Facet rail sections people/place/language/religion open by default again, with
.fr-body's own overflow-y:auto providing the scrollbar.

Measured, both hosts, 1440x900, real browser:
  dashboard-plus-grid (page-scroll): facets 140..2158, table bottom 2158, delta 0
  research-dashboard (fixed-viewport): facets 140..900, table bottom 900, delta 0
  facet rail scrolls: scrollHeight 1966 in clientHeight 708
2026-09-23 17:24:43 +00:00
doxa-maps-88655c70
4bed933001 Keep map scroll-zoom enabled now that the page-scroll map row is bounded
Driver: "I can't zoom in and out on the map. The scrolling is causing that
to change." Scroll-zoom was force-disabled whenever an ancestor scrolled, a
fix from when the dashboard band had no bounded height and a wheel over the
map was the only gesture available in the lower two-thirds of the page.
--rd-map-h changed that precondition: the map row is now a fixed, bounded
height, so the cursor naturally leaves it once a user scrolls past. Verified
on the live mapboxgl.Map instance (not a pixel-diff proxy): zoom now moves
1.45 -> 1.95 on a wheel over the map in the cap-5 shell, standalone
dashboard-plus-grid leaf, and standalone research-dashboard leaf, with no
change to pane scroll position in any of them.
2026-09-23 16:57:36 +00:00
doxa-maps-88655c70
f53dbcea72 Drop the redundant verb from the print labels, and clear the artwork
Some checks are pending
build-gate / build (push) Waiting to run
TWO SMALL FIXES, both Driver-reported by eye.

LABELS. The fieldset's legend already reads "Print Options", so "Print a" in
every button was three redundant words competing for the row's width, and the
printer emoji beside that legend added no information. Labels are now the object
only -- "UPG-100 list" / "UPG prayer poster" / "card deck" -- which fits two on
the first row and one on the second, as asked.

The aria-label keeps the full sentence ("Print the <list> UPG card deck"): a
screen-reader user does not get the legend's framing as reliably as a sighted
user reading down the fieldset, and a control announced as "card deck" alone is
ambiguous. Short visible label, descriptive accessible name.

POSITION. The options row overlapped the poster artwork by 12px. My first
measurement said that 12px was letterboxed white margin, because the image is
object-fit:contain -- but these posters fill the card's WIDTH, so contain
letterboxes at the SIDES and the bottom 12px is real artwork. Reading the
screenshot showed the legend printed across the poster's own footer line.
Geometry said white space; the image said otherwise.

bottom:-14px -> -26px. Overlap measured 12px -> 0px, and the screenshot now
shows the poster's footer text fully legible with the legend on the card's white
edge below it. The negative offset is kept rather than moving the row into flow,
because the tape header at top:-11px uses the same overhang idiom and the 26px
row gap was sized for that pairing.
2026-09-23 16:54:54 +00:00
doxa-maps-88655c70
1a00cac2f7 Fix regions-list clone: real chevron SVG, correct title/link weights, grid indent
Some checks are pending
build-gate / build (push) Waiting to run
Second pass against Driver feedback. Measured getComputedStyle() on the live
doxa.life/regions page and diffed against ours:

- Chevron was a text glyph (&rsaquo;), not DX-life's real SVG icon
  (public/assets/icons/arrow-chevron.svg#chevron-up). Ported the actual path
  geometry and rotation.
- Region title was bold + letter-spaced + uppercase; the live page measures
  weight 400, letter-spacing normal, no text-transform — Bebas Neue's own
  glyphs carry the bold/condensed/caps look, DX-life applies nothing on top.
- Country links were weight 400; live page measures weight 500.
- countries-grid was missing the page's global list indent (35.2px
  padding-left, measured live).
- region-card__footer had a stray ~19.5px bottom margin from the <p>'s
  user-agent default; DX-life's rule is margin-top only.

Colours/fonts/radii/spacing here are an intentional exact clone of DX-life's
own design (Driver's instruction) — not this repo's material-light chrome
tokens. Do not "fix" this back to our house palette.
2026-09-23 16:36:35 +00:00
doxa-maps-88655c70
b71b4bbfa3 Fix UPG-100 poster print-options row to wrap instead of stretch
Some checks are pending
build-gate / build (push) Waiting to run
.pbtn had flex:1 + white-space:nowrap, forcing every print button to
stretch full-width and forbidding its label from wrapping — one very
wide button per line. Now .pfs wraps and .pbtn sizes to its own
content, so short labels sit two per row when they fit and long ones
wrap to their own line without stretching or clipping.
2026-09-23 16:35:45 +00:00
doxa-maps-88655c70
3ac4175e98 Land four agents' dashboard work: resize handle, slimmer stats row, closed rails
Some checks are pending
build-gate / build (push) Waiting to run
Four delegated agents left verified work uncommitted in shared files. Committing
it together because it is interdependent and all of it passes the gates; each
agent's own measurements are in _qa-cycle/02-builder-cycle/delegation/.

RESIZE HANDLE (resize-sections). A draggable separator between the dashboard
band and the grid, so the band's height is the user's choice rather than a fixed
compromise. Driver: "if I were to open this on a large monitor and I wanted to
resize the height of the dashboard compared to the grid, being able to do that
would be really cool." Verified with real pointer events: 522px -> 672px on a
150px drag, stable before/during/after, no snap-back; keyboard-operable with
aria-valuenow tracking the live value. A real bug was caught pre-ship: 200
localStorage writes on a single keypress.

TABLE/MAP DRAG FIX (table-resize). The handle did nothing in page-scroll mode --
measured 522px before AND after a real 200px drag, every frame. The shell is
height:auto there and grows with content, so the percent-of-shell formula was
meaningless. It now measures against the map element's own top edge captured at
drag start, re-deriving each frame rather than summing a delta. Four drags both
directions with persistence: 417.6->336, 336->283, 283->548, and 548->470 after
a reload that restored 548.

SLIMMER STATS ROW (slim-headers). The KPI strip's content is an 18px line inside
a row fixed at 42px -- 24px of empty space. Tokenized as --rd-stats-h:30px,
recovering 12px with no information loss, and tokenized rather than inlined
because 42px repeated in three row templates that must stay in sync.

CLOSED FACET SECTIONS (sidebar-collapse). Driver: "once the data fills all the
sidebars it causes the dashboard to become really tall... without data it looks
fine." Measured: five open sections rendered 69 rows, the rail's own scroller
never engaged (clientHeight === scrollHeight === 2117px), band 2325px against a
900px viewport. All five now start closed -- the default changes, not the
mechanism; per-section state and toggle() already existed.

Build exit 0, 21 bundles / 30 profiles. check:sri, check:settled, sfc-parse,
template-literal, one-palette, synced-css all pass.
2026-09-23 16:27:39 +00:00
doxa-maps-88655c70
79b12baee9 Add prayer-walk-planner: experimental polygon prayer-walking map
Some checks are pending
build-gate / build (push) Waiting to run
Draw/delete/batch-select polygons on a keyless MapLibre+OpenFreeMap basemap,
print a per-zone prayer-walking plan (title + coordinates) via an in-browser
print dialog, everything persisted to localStorage only — no backend, per
the JAMstack constraint. Root SFC mounts directly as the custom element
(no ProfileLoader — needs no token/dataSource/profile-switching), so
defineExpose() bridges addPolygon/deletePolygon/selectPolygon/print/list
straight onto the DOM element for agent-driven use; window.PrayerWalkPlanner
mirrors the same API for discovery. Researched keyless satellite imagery
(Esri World Imagery, NASA GIBS/MODIS, USGS orthophotos): none qualified —
Esri restricts free use to OSM tracing only, GIBS/MODIS tops out at 250m/px
(useless for a neighborhood walk), USGS is US-only — so this ships the
keyless vector basemap already proven in library/styles/registry.js.
2026-09-23 16:22:56 +00:00
doxa-maps-88655c70
4a8b872891 Slim the research-dashboard's KPI stats row from 42px to 30px
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for the three dashboard headers to read slimmer so the
people-groups grid peeks below the fold as a scroll hint. Measured each
header's actual content height: the KPI strip's content is 18px tall
(a single baseline row) inside a grid row hardcoded at 42px, so 24px was
dead space around it — the largest untouched margin of the three. The
topbar and research-filters bands are already at a Driver-fought floor
from prior cards, so they're left alone. Tokenized as --rd-stats-h since
the literal appeared in three grid-template-rows declarations that must
stay in sync.
2026-09-23 16:21:47 +00:00
doxa-maps-88655c70
2772486663 Replace poster-wall's Download/Prayer-List buttons with three real print actions
Some checks are pending
build-gate / build (push) Waiting to run
Driver, 2026-09-23: the UPG-100 list card should offer three PRINT options, each
opening the browser's own print dialog on a rendered preview (never a silent file
download), in this order: Print a UPG-100 list (the point of the app), Print a UPG
prayer poster, Print a UPG card deck.

- printList100() and printCardDeck() reuse printPoster()'s print-preview window
  mechanism (extracted to openPrintWindow()) rather than inventing a second one.
- Both fetch real per-people-group records via the same normalize.js +
  selector-evaluator.js pipeline the sibling map profile already uses, so the map,
  the list print and the card deck can never disagree about which PGs belong to a
  list. Found and fixed along the way: 12 of 27 lists carry a `pg_slugs` selector
  that selector-evaluator.js intentionally throws on (a documented special case in
  upg-100-list.vue) — my first pass called it unconditionally, which silently ate
  every click on the India/China lists before reaching window.open(). Caught only
  by driving a real click and polling the HUD, not by reading the code.
- Card deck is new code (no card/deck generator exists in this repo or in
  root-dx-life to port — searched first); one card per people group, print-paginated
  with `break-after:page`, styled after the poster-wall's own tape/card vocabulary.
- Removed the now-orphaned exportPoster()/downloadPacket()/download() — the raw
  Download button and the "Prayer List: coming soon" placeholder were exactly the
  two buttons the Driver objected to.

Verified live: instrumented window.open/window.print via Playwright, clicked all
three buttons for real. Each opens exactly one window and calls print() exactly
once (List: 1/1, Card Deck: 1/1; Poster: 1 window opened, print() correctly does
NOT fire when the full-res poster 404s in this default, bundlePosters:false tree —
the documented degraded state, confirmed by screenshot).
2026-09-23 16:20:04 +00:00
doxa-maps-88655c70
873a99d1a6 Pay the agent discovery tax once, in the repo
Some checks are pending
build-gate / build (push) Waiting to run
MEASURED: agents produced 21 report docs totalling 2,079 lines of prose and 27
scratch probe files against a handful of product commits. The cause is the spawn
prompts, which is to say me: each carried ~15 environment rules, several cited
lessons, a three-clause invariant, a mandatory screenshot read and a
what-I-could-not-verify section. Per report that is 7-12 headings and 10-16
verification mentions. The prompt made PROVING the job and BUILDING the side
effect.

Two costs compounded. Every agent re-derived the same environment from scratch
-- which port, is the tree settled, does stale SRI block the script, does this
box have WebGL. And several spent that discovery budget, hit a build queue they
could not control, and died on a session limit before committing; their work
survived only because it was found untracked (one 511-line gate was a `git
clean` away from being lost).

This file pays that tax once. Spawn prompts can now be short: what to build,
which files you own, and the one assertion that would falsify it.

It also states the rule that was missing: ship a commit and PUSH, then write two
sentences -- not the reverse. A long honest report attached to no commit is the
failure mode. And it sizes verification to blast radius explicitly, because
over-verifying is a real cost rather than a virtue.

check:doc-scripts passes, so every command quoted here exists in package.json.
2026-09-23 16:05:47 +00:00
doxa-maps-88655c70
6d9ace4cc7 Clone DX-life's /regions design exactly into regions-list.vue
Some checks are pending
build-gate / build (push) Waiting to run
The Driver's exact-clone complaint: our element kept DX-life's markup shape but
reinvented the styling — lighter header green, small radius, generic font, and
no footer link at all. This replaces every design value with DX-life's own,
resolved from their SCSS tokens and cross-checked against getComputedStyle()
on the live https://doxa.life/regions page.

Key finding: DX-life's <html> sets font-size:10px as its rem root, so every
rem token in their CSS resolves to 62.5% of the raw number (their 3.5rem
border-radius is 35px, not 56px). Ported: dark #3b463d header band, 35px
radius, bold/condensed/uppercase title (Bebas Neue approximated — their font
file has no CORS header so it can't be cross-origin loaded), and the missing
"See <Region> dashboard" footer link bottom-right with its permanent underline.
Also fixed a real wrap bug the port introduced: the countries-grid bullet was
flex/baseline-aligned and stranded the (count) beside line 2 on multi-line
country names; now a plain inline row with an absolutely-positioned dot.

Colours/fonts/radii here are an intentional, Driver-directed exception to this
repo's material-light chrome tokens — the clone wins for this element.

Full property-by-property table: _qa-cycle/02-builder-cycle/delegation/RESULT-regions-design-clone.md
2026-09-23 16:01:58 +00:00
doxa-maps-88655c70
b59fb66cc9 Add Regions as the third Dashboards row, completing the seven
Some checks are pending
build-gate / build (push) Waiting to run
The Driver named seven plugins across three sections. Six shipped; Regions was
the gap.

  Dashboards          Big Picture · Simple Research · Regions
  Prayer Mobilization UPG-100 List · Prayer Progress
  Gospel Research     Cities with Internet · Gospel Resources

What ships today is tab 1 of 2. regions-dashboard currently builds a single
profile, regions-list -- the WAGF region list refactored from DX-life's /regions
page, which emits region-selected / country-selected. The scrollable
region/country detail dashboard those events will drive lands as an ADDITIONAL
profile in the same bundle, and this row needs no edit when it does, because the
row names the BUNDLE and the shell resolves the profile from the manifest.

Listed now rather than held back: a working list that reaches the rail is worth
more than a complete feature nobody can open.

Verified in a browser on the built tree at 1440x900, recursive shadow-DOM walk:
four rail sections render as Dashboards / Prayer Mobilization / Gospel Research
/ Experimental; the seven curated rows appear in the Driver's order with Regions
third; 14 tabs total, 14 unique, zero duplicates, zero console errors.
check:sri PASS, check:settled PASS before and after.
2026-09-23 15:43:43 +00:00
doxa-maps-88655c70
3b08fb5962 Stop Big Picture showing the same total twice
The composite's own header renders "<N> shown" from @groups-filtered, and the
nested grid rendered its own "Total: <N>" line 139px below it. Measured live:
"2107 shown" and "Total: 2,107" — the same number twice, both above the fold.

That is the redundant-header shape the standing rule forbids (the hint lives in
the EXISTING header, never a second one). show-total=false here matches the
pattern already applied in dashboard-plus-grid.vue:279 for the same reason.

The grid keeps its own total when embedded ALONE, where it is the only count on
screen — the prop is host-controlled, not removed.

Work started by a delegated agent that hit its session limit mid-task; verified
the cited sibling pattern exists before committing. check:sfc-parse and
check:template-literal pass.
2026-09-23 15:32:28 +00:00
doxa-maps-88655c70
4c74bd717e Restore three-section tab structure: Dashboards / Prayer Mobilization / Gospel Research
Big Picture now lives in its own Dashboards section alongside a renamed Simple
Research (was Research Map, recategorised as the mobile-default dashboard).
Prayer Mobilization narrows to UPG-100 List + Prayer Progress; the renamed
Gospel Research section takes Cities with Internet + Gospel Resources.

The device rule changes shape to match: every row now stays in the rail on
every width (nothing is hidden any more), Big Picture is disabled rather than
removed on mobile with a "desktop only" badge and an explanatory toast on
click, and the auto-selected tab swaps to Simple Research on mobile / Big
Picture on desktop.

Added a loud, module-load-time assertion that no tab id is declared twice
across the curated sections, on top of the existing silent runtime dedupe --
this is the gate the duplicate-key regression needed and didn't have.
2026-09-23 15:21:33 +00:00
doxa-maps-88655c70
c48d1d401a Make the legend show the colour the map actually paints
Some checks are pending
build-gate / build (push) Waiting to run
The swatch rendered the raw bin hex. The map never paints that colour: the
choropleth composites at fill-opacity 0.78 over Positron's #f2f3f0, so every
swatch showed a colour that appears nowhere on the map.

Measured, chip vs actual fill:

    #f4cec4 -> #f4d6ce   dE  4.07
    #ecaea1 -> #edbdb2   dE  7.53
    #e08678 -> #e49e92   dE 11.93
    #ce544b -> #d6776f   dE 17.13
    #b10010 -> #bf3541   dE 21.14

The error grows with the bin, and at the top it is the size of a whole bin step:
the gap between the top two bins is 25.45 as painted, while the legend's own
error on the heartbeat bin is 21.14. A reader comparing the two darkest swatches
was comparing two colours neither of which was on the map -- a strong candidate
for the report that "60-149 red is the same red as the heartbeat zone",
independent of the ramp itself.

Composited in the legend rather than dropping the map's opacity: 0.78 is
deliberate, it lets country borders and labels read through the choropleth, and
this file's own contrast notes are measured at 0.78. The legend was the half
that was lying.

Verified: all five bins produce the measured target values, and non-hex and null
inputs pass through untouched. check:one-palette, check:synced-css,
check:template-literal, check:sfc-parse all pass.
2026-09-23 15:20:30 +00:00
doxa-maps-88655c70
f48cad8dbb Widen the red ramp, add a greyscale ramp, and gate cap-4 interoperability
Some checks are pending
build-gate / build (push) Waiting to run
THE RAMP. The Driver reported that 60-149 reads as the same red as the 150+
heartbeat bin. The old ramp was mathematically even and he was still right --
that tension was the finding, not a contradiction. Measured in plain sRGB
distance the top step (60.0) looked like the LARGEST in the ramp; that
measurement was lying, because RGB distance is not perceptual. Re-measured as
deltaE the old steps were 15.5 / 15.8 / 16.7 / 17.7 -- nearly flat, while chroma
collapses at the dark end so hue and saturation stop carrying information there.

The new ramp deliberately ACCELERATES toward the dark end: 14.1 / 17.9 / 21.4 /
24.9. Top-bin separation 17.67 -> 24.94, a 41% improvement on exactly the pair
he named, and total lightness range widens from 37.2 to 49.0 L*.

A named greyscale ramp ships alongside it, grey to black, as he asked ("just in
case i need to swap from red to black scale"). Its steps measure 22.8 / 22.4 /
22.4 / 22.8 -- near-perfectly even, every step well above the ~10 threshold for
clear distinguishability. It is an OPTION, selected by name; the red ramp
remains the default, so no prior decision is silently reversed.

Bin EDGES are untouched -- check:heartbeat passes, which replays the bin
assignment and fails if a default moves.

THE INTEROP GATE. verify:interop drives the actual compiled cap-4 composites in
a real browser against live data, closing the gap verify:gate1 leaves open. On
its first run it PASSED both desktop directions: a facet-rail click narrowed the
grid to "Showing 1,309", and selecting three rows moved the camera from
28.000,18.000@1.45 to 73.666,13.162@2.48 -- read back off the live map, not
inferred. It asserts the grid's live count line rather than card count, because
card count is a proven false-negative (always 24).

It ships with verify:interop:self-test, a two-mode negative test -- grid-prop
freeze and camera freeze -- each required to fire its own combination-named
failure. A harness that has only ever reported PASS is unproven.

Gates: sfc-parse, template-literal, one-palette, synced-css, mjs-parse,
heartbeat, doc-scripts all pass.
2026-09-23 14:53:51 +00:00