Commit graph

472 commits

Author SHA1 Message Date
doxa-maps-88655c70
dfb4f755be Carry the map selection into the resource table
The table listed every link but had no idea what you had just tapped, so answering
"what exists for THIS group" meant reading a language name off the map and typing it
into a filter box. The map and the table are separate PAGES, so the selection travels
as a URL: ?tab=links&iso=<code> from a people group, &country=<ISO3> from a country.

FILTERED IN THE PROFILE, NOT IN THE SHARED COMPONENT. DataTable has other consumers
and none of them need a filter prop; teaching it one would put a change with repo-wide
blast radius behind a feature only this bundle uses. The rows are filtered before they
are handed over, which is invisible to every other caller.

COUNTRY FILTERING WITHOUT A SECOND FETCH. Rows carry country_codes, the ISO3 set the
language is spoken in, so a country filter is a membership test on data already loaded
rather than pulling the 679KB pin table just to resolve which languages a country has.
+23KB for the column, against 679KB avoided.

THE EMPTY CASE SAYS WHY. A selection with no match renders "nothing found for this
selection in the pools checked, which is not proof that nothing exists" rather than an
empty grid, which would read as a broken filter or, worse, as proof of absence.
Switching to a tab that has no language key says so too instead of showing zero rows.

Verified end to end on the served tree: clicked a real pin, followed the link THE SHEET
RENDERED rather than one built in the test, and landed on 2 rows both carrying cmn with
"Showing 2 of 581 resources for Chinese, Mandarin". Country path checked the same way —
Nepal 51, Chad 22, unfiltered 581 with no banner, and an unmatched code showing the
honest empty state.
2026-09-24 03:35:35 +00:00
doxa-maps-88655c70
410f101846 A table of every resource and its link, keyed by the resource not the group
Every table in this bundle is keyed by PEOPLE GROUP and answers "who has nothing".
None of them answered the question a mobiliser asks first: what exists, what is it
called, and where do I open it. Answering it from a group-grained table means reading
the same JESUS Film link off forty rows.

581 rows, one per language x resource, across 413 languages, together reaching 5,349
people groups — the same 5,349 the pin build counts as carrying a real URL, so the two
tables cannot drift without one of them being wrong.

Reach is MEASURED, not assumed: people_groups, speakers and countries are aggregated
across the pins that actually speak that language, so a row says how many people that
one link serves. Sorted by reach, because the link serving the most people is the one
to open first.

The column is named resource_url because DataTable keys on /_url$/ to render a real
anchor instead of printed text. That is a contract with the shared component, not a
naming preference — rename it and every link silently becomes a string.

TYPES STAY HONEST. 146 Scripture, 306 Film, 129 Bible Stories. Open Bible Stories is
typed as Bible Stories and never as Scripture, and the meta says why. The pools caveat
and the doctrine boundary both render under the table rather than living in a file
nobody opens: only 66-book-canon publishers are included, and the excluded ones are
named so their absence reads as a rule rather than an oversight.

Verified at 1280x900 on the served tree: the tab loads first, renders all ten columns
and 50 rows a page, and 50 rows carry real anchors to ebible.org, jesusfilm.org and
door43 — clicked through from the page, not read out of the JSON.
2026-09-24 03:25:13 +00:00
doxa-maps-88655c70
af10d727c4 Add Door43 as a third pool, so 240 groups stop reading "nothing found"
Two pools had ever been checked — eBible and JESUS Film — and the map said "nothing
found" for everyone outside them. For 240 people groups that claim was simply untrue:
unfoldingWord publishes a released resource in their language and we were not looking.

WHAT WAS ADDED. A dated snapshot of the Door43 catalog (stage=prod only) keyed by ISO
639-3, 165 codes across Open Bible Stories, Bible and Aligned Bible. Cached rather than
fetched live so the pin table is reproducible offline and does not depend on someone
else's uptime; the source URLs travel in the file.

stage=prod IS THE WHOLE CARE. Door43 LISTS more languages than it HOSTS, and this repo
already recorded that trap — "roster membership is not coverage". Released entries only,
and the result reconciles cleanly against what was already known: of 38 languages
previously flagged as having Open Bible Stories, 38 are confirmed and 0 contradicted.
It also found 11 flagged false that do have a published resource.

THE BIGGER HALF WAS INVISIBLE. 116 of the 165 codes have no row in
people-group-resources.json at all, so there was no record to hang a link on and their
pins stayed "nothing found" no matter what Door43 published. Those codes are now created
from the catalog, which is where 213 of the 240 lifted pins come from.

COVERAGE: pins carrying a real URL 5,109 -> 5,349 (41.3% -> 43.2%); codes 862 -> 978.
Tier 1 now means SOMETHING was found rather than "one of scripture/film", because that
is what the colour has to mean once a third pool exists. Tier 2 still means Scripture
AND film — a story set does not get promoted into a Bible.

OBS IS LABELLED AS STORIES. The chip reads "Bible Stories · Door43", never Scripture:
Open Bible Stories is 50 illustrated narratives drawn from the canon, and telling a
reader they have a translation when they have a story set is the one lie this map
cannot afford. 2-letter and region-suffixed tags are DROPPED rather than guessed into
639-3 — a wrong language match ships a real link to the wrong people.

Verified on the served tree: the Kir people group (Mandeali, mjl) previously showed
nothing and now renders a real anchor to git.door43.org/OBS-TLF/mjl_obs, which answers
200. The existing pin probe still passes 12,379 rendered with both Han Chinese links
at 200, so the new pool added coverage without disturbing the old.
2026-09-24 03:18:27 +00:00
doxa-maps-88655c70
0ebde03046 Say what a dot means, and that it is not what the country means
The pins landed with no key, so the map shipped two colour systems using the SAME
four hexes for two different claims and nothing telling them apart. A country is an
AVERAGE across its groups; a dot is one group's own language. Red-on-green is the
most interesting case on the map — a group with nothing found inside a well-supplied
country — and with no key it reads as a rendering fault instead of a finding.

The key says so in those words, and the SHAPE now carries the level: squares for the
country average, circles for a single group. Shape is the honest separator here,
because the hexes are deliberately shared — a dot and its country agreeing on what
"nothing found" looks like is the property worth keeping.

Counts are computed from the layer rather than written down: 12,379 dots, 5,109 with
a link. A number typed into a legend rots the first time the data moves.

Verified at 1280x800 on the served tree: the legend renders both key blocks, quotes
12,379/5,109 matching the generator's own tally, and the pin probe still passes
12,379 rendered with both Han Chinese links answering 200.
2026-09-24 03:05:22 +00:00
doxa-maps-88655c70
b05d1f13b4 Pin every people group, and link what exists in their own language
The map shaded COUNTRIES and stopped there, so the one question it could not answer
was the only one that matters in the field: for THIS group, what exists right now?
12,379 people-group pins now sit on it, each one clickable into the resources for
its language.

THE JOIN. people-groups-slim.json carries Lat/Long per people-group-in-country;
people-group-resources.json carries the real scripture_url/film_url per LANGUAGE.
The key is ISO 639-3 (slim.LanguageCode == resources.iso639_3), never the group
name — a group is reached in its language, not its nationality, and names collide
across unrelated languages.

MEASURED at build time: 5,981 of 12,379 pins (48.3%) join a resource record and
5,109 (41.3%) join one carrying a real URL, across 862 language codes. The other
59% is not a gap in the data to be hidden — it IS the subject. A pin with nothing
found sorts ABOVE a covered one, so the gap wins the z-fight at world zoom rather
than disappearing under a green dot.

NO URL IS INVENTED. Every link is copied from the resources table, which records
its own checked_pools/unchecked_pools, and the sheet keeps saying "none found
means absent from the pools checked, never proof that nothing exists". has_audio
keeps its third state: absent means the pool was never queried, not a no.

PIN COLOURS READ OFF BANDS rather than declaring a fourth palette, so a pin and
the country under it can never disagree about what "nothing found" looks like.

The table is generated into data/tables/ — the REPO ROOT copy, which is what
generate-tree publishes. The plugin-local data/tables/ is a stale divergent
duplicate that nothing serves; building into it ships a file no page fetches.

Verified on the served tree at 1280x800: 12,379 pins loaded AND rendered, layer
and source present, and a click on Han Chinese returns ebible.org/cmn2006/ plus
jesusfilm.org languageId=53352, both rendered as real anchors in the sheet and
both answering 200. window.__grState() exposes those counts so the next check does
not have to guess at screen coordinates to hit a 3px dot.
2026-09-24 03:02:06 +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
d01951bdd6 dev server: answer the production embed URL instead of silently serving the studio
Some checks are pending
build-gate / build (push) Waiting to run
A copied embed URL — <bundle>/<profile>[/<instance>]/index.html, the shape the
manifest advertises and a person actually pastes — had no route on the dev
server, which served its leaf stand-in only at /__leaf/<bundle>/<profile>.

It failed silently, which is the expensive part: with no route, Vite's SPA
fallback answers an unknown path with the studio's HTML at status 200. The
reader asks for one map and receives a different, working page, with no 404 and
nothing in the console.

leafRewrite() maps the embed shape onto the existing /__leaf/ renderer before
any branch runs, so there is one renderer, not two that can drift — the point of
the route is that one URL renders the same thing in dev and in the built tree.
Both the bare tree-relative form and the /<treeName>/-prefixed form are accepted;
the tree name is read from the build config rather than hardcoded. Traversal,
encoded traversal and malformed escapes fall through untouched, and an unknown
bundle keeps the leaf route's 404 JSON rather than becoming HTML at 200.

Verified: /doxa-simple-map/doxa-simple-map/index.html and
/__leaf/doxa-simple-map/doxa-simple-map return byte-identical bodies (742 B);
the prayer URL mounts doxa-map at 1280x800 with a live canvas in a real browser.
2026-09-23 19:53:56 +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
3e95c6bbee Dashboard height handle follows the cursor instead of snapping 144px
The code for this landed inside the preceding commit -- a concurrent worktree
staged it before this commit could run -- so this records what changed and how
it was measured.

Dragging the bar at the dashboard's bottom edge added about three inches of
height on the first pixel of movement, then tracked correctly afterwards.

--rd-shell-h is not the element's height. The shell is height:auto when it is
page-scrolling, and the variable's only consumer is the table grid's
max-height: calc(shell-h - map-h - bar - foot), so it budgets the map and the
table and excludes the 141px of chrome stacked above them. The drag treated the
cursor's Y position as the new height, which published a budget with the chrome
counted twice: the grid's ceiling jumped 300px to 444px on the first frame and
the element grew 144px with it.

The drag now anchors on the budget the stylesheet is actually resolving and
adds the pointer's delta, so the first frame re-publishes the value already in
effect and every later frame moves 1:1 with the cursor. That anchor reads the
computed variable when one is set, because the budget is a ceiling rather than
a floor and the element can render smaller than it -- a restored 983px budget
measured 743px on screen, which had the first frame snapping 239px the other
way. It falls back to measuring map + table only when no variable is set, since
the stylesheet's 80svh default cannot be read off the stored number at any
viewport other than 900px tall.

Measured at 1440x900 with dispatched mouse events, with and without a stored
layout: 1px moves the edge 1-3px, 120px moves it 120-122px, dragging back up
100px shrinks it 100px, and the height holds after the button is released.
2026-09-23 19:07:55 +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
988b1cef87 Share menu shows the whole embed snippet, with no scrollbar
Some checks are pending
build-gate / build (push) Waiting to run
The code box was max-height:90px + overflow-y:auto, which clipped a ~4-line
embed snippet to about two and a half lines and put a scrollport inside a
popover. Wrong trade for this content: the box exists to be read and copied
in one glance, and user-select:all means a single click selects the whole
snippet -- a scrollport fights that gesture, because the selection extends
into content the reader never saw.

height:auto lets the snippet size itself. The popover is short-lived and
anchored, so the extra lines cost nothing.

Measured on the demo-map leaf after opening the "Add to Your Website" tab:
max-height none, scrollHeight 110 == clientHeight 110, scrolls false.

Library change, so it reaches every bundle that mounts the share control on
the next full build.
2026-09-23 19:01:14 +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
22ceabd4b5 Stop the pin hint naming a colour the reader may not see
Some checks are pending
build-gate / build (push) Waiting to run
The default label was "Tap a red dot -- someone still needs prayer" and it
shipped on every map: pinHintLabel is an override no profile has ever set,
and pinHint:false is an opt-out no profile has ever used (verified: zero
occurrences of either across plugin-designs/).

So cities-with-internet and gospel-resources, which have no red dots,
invited readers to tap one; and UPG-100, whose dots are a four-colour
reached/unreached scale, named the wrong colour. A hint describing
something the reader cannot see makes a working map look broken.

Fixed at the default rather than per profile, because the defect was a
default asserting more than it could know. A shared component cannot know
what colour a given map's dots are, so it no longer claims one. Profiles
that do want to name a colour still can, via pinHintLabel.

Reported 5+ times by the Driver.
2026-09-23 18:06:25 +00:00
doxa-maps-88655c70
c9d547c039 Update the clause-C self-test poison to a defect the code can still have
Some checks are pending
build-gate / build (push) Waiting to run
The original poison froze map.scrollZoom.disable() to a no-op, reproducing
syncScrollZoomToHost's old conditional-disable defect. That code path is now
dead: the panel calls .enable() unconditionally and nothing calls .disable(),
so poisoning it exercised nothing and the self-test could not fail for
clause C.

A negative test that cannot fail is the same hazard as a gate that cannot
fail -- it certifies the gate as working while proving nothing about it.

Self-test passes: the poisoned build is correctly rejected (clause=A),
14/16 clause-checks PASS.
2026-09-23 17:58:26 +00:00
doxa-maps-88655c70
7917cbdd7e Publish composes edges into manifest.json at the bundle level
Some checks are pending
build-gate / build (push) Waiting to run
Bundles that declare composition in their profile meta.json (composes:
[...]) now carry that array on the manifest's bundle object, so a
consumer like the plugin studio can render the composition tree without
re-deriving it. 7 bundles affected: big-picture, dashboard-plus-grid,
research-map-plus-grid, my-upg-100-list, stack-demo, doxa-maps-spa,
plugin-studio. Omitted for bundles with no composed children.
2026-09-23 17:51:17 +00:00
doxa-maps-88655c70
eb95d02597 Make bun run build refuse by default instead of asking politely
Some checks are pending
build-gate / build (push) Waiting to run
"Agents never run the full build" is already a top-level heading in
AGENT-BRIEF.md, and the research that established it was three hours old.
It was violated anyway -- by a dispatch prompt that said `bun run build`
while telling the agent to read the brief first. The clean step then wiped
public/doxa-maps mid-probe and an unrelated bundle read back as "did not
mount", one step from a fictional defect in a file that was fine.

Documentation cannot fix that; an explicit instruction beats a standing rule
at the point of work. So the build refuses with exit 75 and prints the
cheaper paths (bun run tree, BUNDLE= vite build, settled-tree-gate) unless
BUILD_ALLOW=1 is set.

Opt-in rather than caller-detection, deliberately: the first draft keyed on
two environment variables that do not exist in a spawned process -- a guard
that could never fire. A probe then reported a third variable as the
discriminator; it is set in the parent too, so it marks any session rather
than a child. No environment variable distinguishes them.

Negative-tested both directions: no flag exits 75 with the refusal,
BUILD_ALLOW=1 reaches "build lock acquired".
2026-09-23 17:48:27 +00:00
doxa-maps-88655c70
0b58ab4560 Commit the rebuilt tree so integrity hashes match the shipped bundles
29 built files were rebuilt but uncommitted, including manifest.json -- the
file the cap-5 shell fetches at runtime to decide what to mount. Committed
source and committed output disagreeing on an SRI hash makes the browser
refuse the script, which presents as a blank pane and reads exactly like a
mount failure.

Diff is hash churn only, no structural change.
check:sri PASS 45/45, check:composite PASS 23 edges, settled-tree PASS.
2026-09-23 17:34:12 +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
8a51dd061c Raise the settled-tree floors, on the gate's own advice
Some checks are pending
build-gate / build (push) Waiting to run
The gate had been printing a NOTE on every green run: "the tree has outgrown its
floors (leaves 46 vs floor 40, dirs 23 vs floor 17) -- they still catch a
CATASTROPHICALLY gutted tree but no longer a partial one." It was right. At a
floor of 17 it would have passed a tree missing eight directories.

That matters more than it sounds. A totally wiped tree announces itself; a
PARTIAL one is the state that produces a confident false measurement -- "that
bundle is missing", "the dashboard tab does not exist" -- which this flight has
already paid for several times over.

MIN_DIRS 17 -> 21 (the manifest's current bundle count, the number that grows
when a plugin is added, leaving headroom against the directories on disk).
MIN_LEAVES 40 -> 44 (two leaves of headroom for a mid-build transient).

Deliberately NOT derived from the live tree: a floor computed from what is there
can never notice that something is missing.

Negative-tested 5/5: healthy 25/46 passes; the OLD floors exactly (17/40) now
FAIL as the partial wipe they are; losing 5 dirs fails; losing 3 leaves fails;
mid-clean fails. And it still exits 0 on the real tree, with the stale-floor
NOTE gone. check:mjs-parse passes.

These floors stay coarse by design -- check:embed-targets remains the
load-bearing assertion that every embed.iframe target actually resolves.
2026-09-23 17:00:40 +00:00
doxa-maps-88655c70
56c3b2700d Derive the gate's wheel point from the band, instead of hardcoding it
Some checks are pending
build-gate / build (push) Waiting to run
Clauses B and C(below-map) wheel-tested at hardcoded y=800 and y=850. Those were
tuned when the dashboard band measured ~1213px. A later fix dropped the
page-scroll map default from 58svh to 40svh to satisfy clause A, shrinking the
band to ~892px -- so both coordinates stopped being "below the band" and started
landing INSIDE it, on .rd-td, the table's own windowed scrollport, whose
~54,500px internal scroll a bounded gesture can never exhaust.

★ The product was fine the whole time. A real gesture from a natural cursor
position (y=870) moves the grid 2159px, fully reachable. The gate was failing on
its own staleness and reporting it as a product defect -- the most expensive kind
of wrong a gate can be, because it sends someone to fix working code.

Both points now derive from the band's measured bottom: 20px clear of it, never
past the fold, floored at 120px. Verified across four band heights including the
degenerate ones -- at the current 892px band it resolves to y=870, precisely the
position where the real gesture was measured; at a band taller than the viewport
it clamps to the fold, where clause A is the clause that should be failing
anyway; and it stays sane when the band cannot be found at all.

Found by two agents who cross-verified each other with elementFromPoint rather
than geometry inference, and who correctly declined to patch a shared gate file
a third agent had already touched today -- they routed it instead.

check:mjs-parse passes.
2026-09-23 16:57:44 +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
4d9b90f3e3 Stop the fitness gate reporting Mapbox's own wrappers as clipped
Some checks are pending
build-gate / build (push) Waiting to run
On its first real verdict, clause E failed on ALL EIGHT viewport/target
combinations with docOverflow=0 and the same two elements every time:
mapboxgl-canvas-container and mapboxgl-control-container. Both are GL-JS's own
wrappers, legitimately zero-height with children -- the canvas container
collapses because the canvas is absolutely positioned, the control container is
an empty positioning shell. Neither is a layout defect.

That was 8 of the gate's 12 failures: a false signal loud enough to drown the
one real finding underneath it (clause B -- a wheel below the map scrolls
nothing on the composite leaf). A gate that reports four-fifths noise teaches
its reader to skim the summary line, which is the failure this file was written
to end. So the exemption is a correctness fix, not a convenience.

Scoped by PREFIX rather than an exact class list, because GL-JS composes these
as multi-class strings ("mapboxgl-canvas-container mapboxgl-interactive") and
adds more across versions. Kept narrow to mapboxgl-/maplibregl- so it cannot
silently swallow a real defect in our own markup.

Negative-tested as a predicate, 7/7: both real false-positives and the maplibre
equivalent are exempt; rd-shell, pgg-root and region-card__footer are NOT; and
"my-mapboxgl-thing" does not match mid-string. check:mjs-parse passes.

The gate could not be re-run end-to-end for a before/after verdict -- the shared
build queue has been busy continuously (check:settled reported dirs=2/17
leaves=0/40 mid-clean). Predicate proven in isolation; the full verdict is owed.
2026-09-23 16:36:51 +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
8029282f46 Add the fitness-for-use gate, and wire its negative test
Some checks are pending
build-gate / build (push) Waiting to run
A green state suite said nothing about whether the product is usable. The suite
was 56/58, verify:gate1 13/13 on live data, verify:map-paint 11/11 with real GL,
verify:interop 4/4, 363 done cards -- and the Driver opened the product and
found five real defects in one sitting, several CAUSED by a change a gate had
certified. Not one was visible to any gate.

The gap is what they assert. Existing gates ask does it mount, does it fetch, do
the counts agree, does a canvas paint. The Driver asks: is it slim, can I see it
at once, does the wheel do what I expect, does it tell me what I am looking at.

check:fits-screen asserts five clauses on the built composite with a real
browser and live data:

  A  the dashboard band fits ONE 1440x900 viewport, and is shorter than its own
     tree-legend's scrollHeight
  B  the people-groups grid is still reachable by scrolling underneath -- a fix
     for A must not re-break this (AP-fixing-reachability-by-making-the-thing-taller)
  C  a wheel over the map changes zoom and does not scroll the pane; a wheel
     below it scrolls and does not zoom -- instrumented on the DISPATCHED EVENT
     via composedPath, never on geometry, per the four wrong diagnoses that
     rule already cost
  D  the grid's live count is visible and CHANGES when a facet narrows it
  E  a 1440/1280/768/390 sweep has no horizontal overflow and no element
     clipped to zero height

It ships with check:fits-screen:self-test -- one regression mode per clause,
because a gate that has only ever printed PASS is unproven.

It also skips honestly: exit 0 with an explanation if the tree is not settled at
START or is no longer settled at END, or if the live API is unreachable. Proven
on its first real run, which reported SKIP with dirs=3/17 while a build was
mid-clean rather than inventing a defect.

Started by a delegated agent that hit its session limit; the two hardcoded-port
probes it left beside this file were scratch and moved to _qa-cycle. Verified:
check:mjs-parse passes, the gate is wired in package.json, and its settled-tree
skip path works.
2026-09-23 15:50:27 +00:00
doxa-maps-88655c70
cba9de3adb Tell agents to use the dev server, and to push
Some checks are pending
build-gate / build (push) Waiting to run
TWO RULES THAT WERE COSTING REAL TIME.

1. A FULL BUILD IS NOT NEEDED TO SEE A CHANGE. Agents were running the minutes-
long `bun run build` for edits the dev server hot-reloads. Documented `bun run
dev`, plus the parallel-safe form.

   The trap that makes the parallel form mandatory: Vite's default behaviour on
   a busy port is to silently move to the next one, and FIVE gates have 5173
   written into them (check-facet-changes-map, plugin-gallery-smoke,
   smoke-studio, generate-gallery, check-scrim-covers-viewport). A second dev
   server without --strictPort therefore leaves those gates talking to somebody
   else's server, or to none, reporting a confident wrong answer with no error.
   Verified: `--port 5299 --strictPort` serves 200 on root and on the registry,
   and a duplicate on the same port exits 1 instead of drifting.

2. A LOCAL COMMIT IS INVISIBLE. The Driver runs a laptop watcher against the
   Forgejo repo and sees changes in the browser from THAT repo, so work that
   stops at `git commit` has not been delivered however well it was verified.
   Documented the push, with the credential kept out of .git/config and filtered
   from output.

   Also documented the follow-up nobody expects: after pushing to an inline URL,
   `git log @{u}..` still reports the commits as unpushed, because git only moves
   a remote-tracking ref when it pushes to a NAMED remote. That read as "63
   unpushed commits" on a branch that was fully pushed.

check:doc-scripts passes, so every command quoted here exists in package.json.
2026-09-23 15:45:24 +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
10fdda338b Rebuild the tree so every leaf's baked SRI matches its bundle
Some checks are pending
build-gate / build (push) Waiting to run
A single-bundle build regenerates the JS but not the leaf pages, so four
big-picture leaves carried a stale sha384 and the browser refused to execute the
script at all. That is why a nav probe measured 0 sections on a shell whose
source was correct, and it is the likeliest cause of a teammate's 'pggMounted:
false on big-picture everywhere' reading.

check:sri named both the cause and the fix; ran the full build it prescribes on
a settled tree. Now PASS: 44 script references across 45 leaf pages, every
leaf's integrity matching the bundle it loads.
2026-09-23 15:37:18 +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