+ ready, so it never disappears before the pins populate. -->
diff --git a/plugin-designs/pplr-data-maps/src/views/pplr-rosetta-cards.vue b/plugin-designs/pplr-data-maps/src/views/pplr-rosetta-cards.vue
index 6b4636f1f..d03bf0266 100644
--- a/plugin-designs/pplr-data-maps/src/views/pplr-rosetta-cards.vue
+++ b/plugin-designs/pplr-data-maps/src/views/pplr-rosetta-cards.vue
@@ -99,9 +99,8 @@ onMounted(async () => {
// Rosetta sample (5 stars — slightly larger than base pins so they
// stand out, but zoom-interpolated so they don't dominate the whole
- // map at low zoom levels (per user 2026-05-02: "yellow pens are way
- // bigger than all the other pins... when you zoom out it makes the
- // whole map just yellow").
+ // map at low zoom levels: at full size they were far bigger than every
+ // other pin and turned the zoomed-out map yellow.
map.value.addLayer({
id: 'rosetta-pin',
type: 'circle',
diff --git a/plugin-designs/pplr-data-maps/src/views/pplr-semantic-trees.vue b/plugin-designs/pplr-data-maps/src/views/pplr-semantic-trees.vue
index 7039d5873..85575f506 100644
--- a/plugin-designs/pplr-data-maps/src/views/pplr-semantic-trees.vue
+++ b/plugin-designs/pplr-data-maps/src/views/pplr-semantic-trees.vue
@@ -4,7 +4,7 @@ import { inject, ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue
const profileConfig = inject('profileConfig', null)
const treeType = computed(() => profileConfig?.value?.treeType || null)
-// One tree per category (per user spec). Each parent has exactly 2 children to
+// One tree per category. Each parent has exactly 2 children to
// keep the visual pattern obvious. Lang tree intentionally shows one language
// WITH dialects and one WITHOUT (to convey "not every language has dialects").
const TREES = [
@@ -337,8 +337,8 @@ onBeforeUnmount(() => resizeObs?.disconnect())
.tree-cell::-webkit-scrollbar { width: 6px; height: 6px; }
.tree-cell::-webkit-scrollbar-thumb { background: #484f58; border-radius: 3px; }
-/* ── Table+SVG container: scrolls horizontally if columns don't fit (per user
- 2026-05-02: don't hide field columns just to keep cells slim) ── */
+/* ── Table+SVG container: scrolls horizontally if columns don't fit (field
+ columns are never hidden just to keep cells slim) ── */
.tree-table-wrap {
position: relative;
overflow-x: auto;
diff --git a/plugin-designs/research-dashboard/dashboard.config.js b/plugin-designs/research-dashboard/dashboard.config.js
index 933787490..3546670fd 100644
--- a/plugin-designs/research-dashboard/dashboard.config.js
+++ b/plugin-designs/research-dashboard/dashboard.config.js
@@ -183,9 +183,8 @@ export default {
// "Affinity Bloc" tree tier) that could never recognise each other: the tree stored
// affinity_bloc='A012' while the dropdown stored rop1='South Asian Peoples', and
// filteredRows byte-matches each key against its OWN accessor, so neither ever saw the
- // other's selection. Observed from behaviour: "there are duplicate affinity blocks, one
- // called People, one called Affinity Bloc… the People filter IS by affinity block. So they
- // are the same thing." He is right — the People filter always WAS the affinity-bloc filter.
+ // other's selection. Observed from behaviour: two filters, "People" and "Affinity Bloc", that are
+ // the same axis — the People filter always WAS the affinity-bloc filter.
//
// THE RULE THIS RESTORES: one axis = one filterable facet key. The CODE won as canonical
// because every other People tier (cluster / people_group) already keys by code, and
@@ -219,9 +218,9 @@ export default {
// `tree:'affinity'` tells the rail to resolve the People codes to human names via the shared
// useAffinityBlockLegendData composable (single source of truth with the map's semantic tree).
// ── THE FOUR STACKED TREES (card BUILD-STACKED-TREE-LEGEND) ──
- // The requirement: "if you had a team of people trying to fill the globe with the gospel… they need to be
- // able to search by people, place, language, religion, and find who has the gospel resources and
- // who doesn't." The legend is how a researcher navigates those FOUR axes, in THIS order.
+ // The purpose: a team working to bring the gospel to every place needs to search by people, place,
+ // language and religion, and to find who has gospel resources and who does not. The legend is how a
+ // researcher navigates those FOUR axes, in THIS order.
//
// `axis` names which cap-3 colour strategy paints the tree (see useTreeLegendColors.js) — the
// legend reads the map's palettes, so the two can never drift. Status is NOT one of the four
@@ -329,11 +328,10 @@ export default {
//
// WHY IT COSTS NOTHING TO STAY DUAL: MapLibre ignores accessToken rather than rejecting it,
// so the Mapbox credential stays threaded through untouched while MapLibre is live.
- // ★ MAPBOX FOR THIS MAP, by the 2026-09-23 description. Every wording of the
- // ask pointed at one thing: "like the Mapbox map", "a mirror or clone of Mapbox's globe space
- // style with the stars", "water like Mapbox style". Mapbox Standard's globe NATIVELY renders
+ // ★ MAPBOX FOR THIS MAP. The target look is Mapbox's own: its globe, the space style with the
+ // stars, and its water. Mapbox Standard's globe NATIVELY renders
// the starfield, the atmosphere and that water -- no clone can beat the original, and we hold
- // a working baked token (the explicit ruling accepted it as public). MapLibre support
+ // a working baked token (accepted as a public token). MapLibre support
// stays fully built and reversible: flip this one key back and the keyless VersaTiles path
// returns. Interoperability was the requirement; it is intact.
provider: 'mapbox',
@@ -357,8 +355,8 @@ export default {
// dataset of 200 groups, a 150 cutoff puts EVERY country in the bottom bin and the map renders
// uniformly pale: not an error, not a crash, just a dashboard that says nothing. That failure
// is invisible to every existing gate, because the code is perfectly correct — the ASSUMPTION
- // is what moved. The report: "that filter of 150 ... should be a variable. It shouldn't be a
- // static ... I need to be able to reuse this dashboard for other maps, other data sets."
+ // is what moved. The cutoff of 150 must be a variable, not a static, so this dashboard can be
+ // reused for other maps and other data sets.
//
// So the thresholds are declared HERE, in the dashboard's own config, where every other
// per-dataset fact already lives (facets, columns, kpis). Re-pointing this dashboard at a new
@@ -390,13 +388,10 @@ export default {
// ── COLOUR RAMP — a NAME, resolved in DashboardMapPanel.vue's RAMP_PRESETS ──────────────
// Same pattern as `mapStyle` above: a name in config, not a URL/hex-array a host has to
// construct. 'heartbeat-red' is the shipped default (unchanged if this key is omitted).
- // 'greyscale' is the requested swappable alternative ("grey scale as a backdrop
- // from grey to black, just in case I need to swap from red to black scale") — set this to
+ // 'greyscale' is the swappable alternative, a grey-to-black backdrop — set this to
// 'greyscale' to swap the whole choropleth, legend included, with no other edit required.
- // ★ SWITCHED TO GREYSCALE 2026-09-23 on the ruling: "instead of the red, we need to
- // use the grey scale from grey to black". He had asked for this preset to exist earlier as a
- // swappable alternative; this is him taking the swap. One key, and the choropleth AND its
- // legend both move — which is why it was built as config rather than scattered hex.
+ // ★ THE SHIPPED VALUE IS GREYSCALE: the grey-to-black ramp replaced the red one. One key, and the
+ // choropleth AND its legend both move — which is why it was built as config rather than scattered hex.
// The ramp is ['#e8e8e8','#a9a9a9','#6f6f6f','#3a3a3a','#060606'] (DashboardMapPanel.vue:467),
// measured worst-case pin visibility 1.96 vs red's 1.84. THE DURABLE FINDING: the real fix for
// pin contrast is the lightness-conditional STROKE, which holds whichever ramp is selected —
@@ -406,9 +401,8 @@ export default {
// ── charts ── (DashboardChartsRail)
charts: [
- // ★ HIGH-LEVEL RELIGIONS ONLY (reported 2026-09-23: "it should not mention Ethnoreligion and
- // Islam multiple times, just once -- just the high-level religions, not broken down into
- // smaller categories"). The rollup ALREADY EXISTED as the religion_family facet ("Islam",
+ // ★ HIGH-LEVEL RELIGIONS ONLY: each religion appears once, at the high level, not broken down into
+ // smaller categories (Islam once, not "Islam - Sunni" and "Islam - Folk"). The rollup ALREADY EXISTED as the religion_family facet ("Islam",
// "Hinduism", "Ethnoreligion" -- one row each); the chart was simply pointed at the
// fine-grained `religion` facet ("Islam - Sunni", "Islam - Folk", ...). One word moves it.
// religion_family is dashboardOnly by design (a derived value no row field carries), so a
@@ -446,8 +440,8 @@ export default {
// `prayer_level` listed FIRST so the config owns the order rather than the adapter forcing
// it. DashboardResearchFilters de-duplicates on that key, so naming it here cannot render
// it twice — and if the adapter's forcing is ever removed, the order still holds.
- // ★ EIGHT dropdowns. The spec named them: "people, region, block, country, language, religion,
- // prayer, engagement" — and separately asked for prayer FIRST. engagement_status was the one
+ // ★ EIGHT dropdowns: people, region, bloc, country, language, religion, prayer and engagement,
+ // with prayer FIRST. engagement_status was the one
// missing: it was already fully built (label + getter at :112, the Status tree section at :201,
// the Engagement table column at :256) and simply never registered here, so the rail rendered
// SEVEN. Measured live before this change: 7 filter buttons (Prayer, People, Region, Bloc,
diff --git a/plugin-designs/research-dashboard/research-dashboard.vue b/plugin-designs/research-dashboard/research-dashboard.vue
index 9973f7bf8..983f3281a 100644
--- a/plugin-designs/research-dashboard/research-dashboard.vue
+++ b/plugin-designs/research-dashboard/research-dashboard.vue
@@ -321,8 +321,8 @@ useShadowStyles(`
★ SCOPE: this law governs UI CHROME — panels, cards, type, borders, controls; everything
whose colour comes from a token above. It does NOT govern BASEMAP TILES. The map panel
- renders a dark basemap ('galaxy' — black ground, blue water, set in dashboard.config.js on
- the 2026-09-23 ruling), and that is DELIBERATE, not drift: a basemap is imagery the
+ renders a dark basemap ('galaxy' — black ground, blue water, set in dashboard.config.js),
+ and that is DELIBERATE, not drift: a basemap is imagery the
data is drawn ON, chosen for choropleth contrast, not a surface the UI is themed with. A
dark map inside light chrome is the intended composition. Do not "fix" it to match the
palette, and do not cite it as precedent for darkening any chrome.
@@ -342,19 +342,19 @@ useShadowStyles(`
--rd-primary:var(--dx-accent);
/* ── RAIL BALANCE (card BUILD-REBALANCE-THE-TWO-SIDEBARS) ────────────────────────────
The charts rail used to be 312px against the tree rail's 248px — the CHARTS rail was
- 64px WIDER than the tree, which is exactly what was reported ("it looks like the
- CHART sidebar is LARGER than the TREE sidebar"). That was backwards for the content:
+ 64px WIDER than the tree, so the CHART sidebar looked LARGER than the TREE sidebar.
+ That was backwards for the content:
the tree carries long multi-word labels (bloc/cluster/region names) that must read on
ONE line, while the charts carry a donut plus a legend that can wrap.
So the widths SWAP ROLES: the tree takes the wider column, the charts the narrower.
The pair still sums to 560px, so the centre map cell is unchanged. */
- /* ★ SYMMETRIC SIDEBARS (reported 2026-09-23: "the charts sidebar can be wider so it is the
- same width as the left sidebar, which will put the map in the exact center"). rail
- 264 -> 296 = facet width; equal flanks centre the middle column by construction. */
+ /* ★ SYMMETRIC SIDEBARS: the charts rail is as wide as the tree rail, which puts the map in the
+ exact centre. rail 264 -> 296 = facet width; equal flanks centre the middle column by
+ construction. */
--facet-w:296px; --rail-w:296px;
/* ── ★ ONE EDGE GUTTER FOR THE WHOLE HEADER STACK ────────────────────────────────────
- (card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK: "on the edges there should be padding
- so that everything lines up perfectly.")
+ (card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK: one edge padding for every band, so
+ everything lines up.)
MEASURED BEFORE at 1600x900: topbar padding 14px, filters band 14px, KPI strip 16px. The
KPI band was inset 2px further than the two bands above it, so the stack's left edge was a
2px staircase rather than a line. Nobody chose that — each band picked its own number
@@ -443,7 +443,7 @@ useShadowStyles(`
.rd-rz::after{ content:''; position:absolute; background:transparent; border-radius:var(--r-pill,999px); transition:background .15s; }
/* Logical margins: the grid mirrors under dir=rtl (grid-template-columns + justify-self both
follow the inline direction), so a physical margin-right put the handle's 4px overhang on the
- wrong side of its own column in Arabic. go-int's board already used the logical pair. */
+ wrong side of its own column in Arabic. the gospel-internet board already used the logical pair. */
.rd-rz-facet{ grid-area:facets; justify-self:end; align-self:stretch; width:8px; margin-inline-end:-4px; cursor:col-resize; }
.rd-rz-rail{ grid-area:rail; justify-self:start; align-self:stretch; width:8px; margin-inline-start:-4px; cursor:col-resize; }
.rd-rz-table{ grid-area:map; align-self:end; justify-self:stretch; height:8px; margin-bottom:-4px; cursor:row-resize; }
@@ -451,7 +451,7 @@ useShadowStyles(`
.rd-rz-table::after{ top:50%; left:50%; transform:translate(-50%,-50%); width:34px; height:3px; }
/* ── SHELL HANDLE (page-scroll mode only) — the boundary between the WHOLE research-dashboard
element and whatever a host stacks below it (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER: this is
- the control actually asked for — "resize the height of the page" — which a previous
+ the control that resizes the height of the page, which a previous
pass mistakenly bound to .rd-rz-table above instead). Positioned OUTSIDE the grid (the shell is
already position:relative) rather than given a grid-area, because it must span all THREE
columns (facets + map/table + rail) regardless of their widths, and no single grid-area does
@@ -546,9 +546,8 @@ useShadowStyles(`
SERVER, not this CSS. Verified this time against a live server with a screenshot. */
.rd-shell.rd-page-scroll{ height:auto; --table-h:auto;
grid-template-rows: var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px) var(--rd-map-h,340px) auto; }
- /* ★ COLLAPSE GIVES ITS SPACE TO THE MAP (reported 2026-09-23: "collapsing the table is supposed to
- collapse down and increase the size of the map -- instead it collapsed up and introduced white
- space under the collapsed table"). The base .rd-table-collapsed template still says 1fr for the
+ /* ★ COLLAPSE GIVES ITS SPACE TO THE MAP: collapsing the table must collapse DOWN and grow the map.
+ It used to collapse UP and leave white space under the collapsed table. The base .rd-table-collapsed template still says 1fr for the
map row, and 1fr cannot grow in an auto-height shell -- there is no fixed total to divide -- so
the freed space pooled under the bar as white nothing. In page-scroll the collapsed map takes
the SAME shell budget the open state uses, minus only the bar: the one-owner derivation again,
@@ -614,11 +613,9 @@ useShadowStyles(`
table's real bottom edge, i.e. the sidebar grey-gap bug this card is fixing. --rd-table-foot-h
is a var (not a literal) for the same reason --rd-table-bar-h is: DashboardDataTable can correct
it if the footer's height ever changes, without this rule going stale. */
- /* ★ SEVEN DATA ROWS BY DEFAULT (2026-09-23, with the arithmetic done explicitly):
- "the table needs to only show SEVEN rows by default height. 7 people group rows. That's NOT
- including the header. If you include the field, there'll be 8 rows. If you include the footer,
- that'd be 9. If you include the table's header, 10. But 7 actual data people groups."
- That also settles an earlier ambiguity: he had said 77 and 7 in one breath; 7 is DATA ROWS.
+ /* ★ SEVEN DATA ROWS BY DEFAULT, with the arithmetic done explicitly: the table shows SEVEN
+ people-group rows at its default height, chrome NOT counted: adding the filter field makes 8 rows'
+ height, the footer 9, and the table's header 10. 7 is DATA ROWS.
★ STATED IN ROWS, NOT PIXELS, deliberately. The old formula derived the keyhole by subtraction
(shell - map - bar - foot), so the row count was whatever fell out — MEASURED 300px = 11 rows,
not 7. Deriving from --rd-table-rows x --rd-table-row-h means the promise is the declaration:
@@ -713,9 +710,9 @@ useShadowStyles(`
ONE QUANTITY, ONE OWNER applies to fallbacks too: same var, same fallback, everywhere. */
max-height:calc(var(--rd-shell-h, 562px) + 1px); }
- /* ★ THE ELEMENT MUST SIZE TO ITS CONTENT IN PAGE-SCROLL (reported 2026-09-23: "massive grey gap
- between map and table and the map overflows into the people-group grid page — the dashboard
- element is not one unified element above the grid element, it is overflowing").
+ /* ★ THE ELEMENT MUST SIZE TO ITS CONTENT IN PAGE-SCROLL. The symptom: a large grey gap between the
+ map and the table, with the map overflowing into the people-group grid, so the dashboard did not
+ read as one element above the grid.
MEASURED in the cap-5 shell at 1440x900: INSIDE the dashboard everything was tight (map bottom
536, table top 537, gap 1px, table flush with .rd-shell at 899) while the HOST element measured
3510px tall — 2611px of empty element below the content. That empty tail is the grey gap, and
diff --git a/plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs b/plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs
index 2168d69d6..546fb3493 100644
--- a/plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs
+++ b/plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs
@@ -33,7 +33,7 @@
* failure here (check-composite-interop carries the same part B for the same reason): a gate that
* only reads source will pass while the reader gets last week's element.
*
- * NO BROWSER. Playwright is rega's alone (fleet policy). Nothing here launches one.
+ * NO BROWSER. Playwright runs only in the browser QA step (project policy). Nothing here launches one.
*
* NEGATIVE-TESTED 2026-09-26 — each of these makes it FAIL, naming the file and the reason:
* chips back to
-> A1 "chip is a , not a