Merge development (353bf078c) into the whole-India landing: package.json gates unioned (check:side-lanes added), poster-render re-pinned on the merged file

This commit is contained in:
Doxa Maps 2026-10-01 08:24:45 +00:00
commit a1c74004a0
416 changed files with 3140 additions and 20305 deletions

View file

@ -423,7 +423,7 @@ func (e *engine) startWith(lang, device string, src *recorderProc, sink func(eve
recMu.Unlock()
writeMu.Lock()
// The streaming model holds back the last fraction of a second (its right context) until more audio
// or a pause arrives, and a commit alone does not flush it (rega: a Stop mid-sentence lost "what
// or a pause arrives, and a commit alone does not flush it (measured: a Stop mid-sentence lost "what
// your country…"). 1.2 s of silence first lets it finish the words already spoken.
silence := make([]byte, 3200)
for i := 0; i < 12; i++ {
@ -750,7 +750,7 @@ func (e *engine) startWith(lang, device string, src *recorderProc, sink func(eve
e.logs.add("speech server error: %s", string(msg))
emit(event{"type": "error", "message": string(msg)})
case t == "input_audio_buffer.committed":
// The commit acknowledges Stop; the words for the audio before it can still follow (rega: a
// The commit acknowledges Stop; the words for the audio before it can still follow (measured: a
// Stop mid-word lost "what your country…"). Keep reading until the final text or 6 s of quiet (a loaded server can take seconds per backlog chunk).
committed = true
e.logs.add("speech server committed the stream; reading its last words")

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="512" width="840" height="1208" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="489" width="840" height="1231" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="627" width="840" height="1093" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -194,7 +194,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -214,8 +214,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -286,15 +286,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="351" width="840" height="1333" fill="var(--sea)"
@ -318,7 +316,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -342,8 +340,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1925" x2="1474" y2="1925" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="512" width="840" height="1208" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="443" width="840" height="1277" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

View file

@ -184,7 +184,7 @@
.hseries{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:14px;fill:#7E7562;letter-spacing:.01em}
.hseries-em{font-weight:700;fill:#6B4A78}
.hseries-dim{fill:#A79E8A}
/* The site reference on its OWN line — Brief: "so that it is less wide". A
/* The site reference on its OWN line, so the series block is less wide. A
single long line was forcing the series block wider than the map. */
.hseries-dim2{font-family:Lato,'Segoe UI',system-ui,sans-serif;font-size:12px;
fill:#A79E8A;letter-spacing:.02em}
@ -204,8 +204,8 @@
the map's width (~1100px); the band is now 1388px, so the type follows.
15px measures ~1263px — fills the band while keeping headroom over the width
estimator's error rather than sitting one glyph from overflow. */
/* 13.5px bold. Brief: "this is too large, it should not span full width —
there needs to be gaps." At 15px bold it measured 1396px of the 1448px band,
/* 13.5px bold. At full width it was too large: the line needs gaps at both
ends. At 15px bold it measured 1396px of the 1448px band,
i.e. touching both rules; 13.5 measures ~1256px and leaves ~96px of paper at
each end, so the line reads as SET INSIDE the band rather than filling it.
Bold + dark ink still carry the weight. */
@ -276,15 +276,13 @@
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#2A2721" flood-opacity="0.34"/>
</filter>
</defs>
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating."
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating.
A soft drop shadow under a crisp 1.5px border lifts the map off the paper,
so it reads as an object ON the sheet rather than a window cut INTO it —
which is also what makes the surrounding text feel like a mount around it. -->
<!-- THE MAP FLOATS. Brief: "a border around the map itself like a border line
and a shadow around the outside of that line that makes the map itself look
like it is floating." The white mat was tried and rejected — the shadow-only
<!-- THE MAP FLOATS: a border line around the map itself, with a shadow outside
that line, makes the map look like it is floating. The white mat was tried and rejected — the shadow-only
border reads better, and the mat also cut every leader line with a band of
white where the lines crossed it. -->
<rect x="330" y="466" width="840" height="1254" fill="var(--sea)"
@ -308,7 +306,7 @@
down to where the region name stops being the loudest thing on the poster.
Splitting it lets the CATEGORY sit quietly above and the REGION stay big:
the region is what a passer-by uses to find their sheet on a wall of 30. -->
<!-- TITLE, two-tone. Brief: the title reads "Unengaged of <Region>", with
<!-- TITLE, two-tone. The title reads "Unengaged of <Region>", with
"Unengaged of" in a DIFFERENT COLOUR from the region name — so a passer-by
reads at a glance that it is these PEOPLE who are unengaged, while the
region stays the loudest thing on the sheet. The overline above it is now
@ -332,8 +330,8 @@
shadow now define that edge far more clearly, so the rule just floated as a
stray horizontal line above the map.) -->
<line x1="26" y1="1936" x2="1474" y2="1936" class="rule"/>
<!-- FOOTER SCRIPTURE — TIER 1. Brief: "should be larger text bold and not two
lines but one line with scripture reference on the right." It is one of the
<!-- FOOTER SCRIPTURE — TIER 1: larger, bold, on ONE line, with the scripture
reference on the right. It is one of the
three things that must read when a reader steps closer, alongside the title
and the people-group names, so it carries display weight. The reference is
right-aligned to the map's own edge, which is what keeps it on ONE line. -->

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -11,7 +11,6 @@
AGENT-BRIEF.md
_archive
data-private
library/scripts/posters/snapshots/_granular-experiment.mjs.shelved
plugin-designs/key-cities
name:*field-access*
name:*visa*.json

View file

@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
"build": "bash internal/build-lock.sh",
"build:unlocked":"bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post",
"gates:pre":"node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
"gates:post":"node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
"gates:post":"node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
"preview": "bun x vite preview",
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production

View file

@ -16,7 +16,7 @@ served folder". The licence travels with the asset; the explanation travels with
| `boundaries/gb-CHN-ADM1.geojson` | geoBoundaries gbOpen, boundaryID `CHN-ADM1-43563684` | **Public Domain**, licenceSource `commons.wikimedia.org`, year 2019 | recorded in `boundaries/ATTRIBUTION` | none — upstream records no attribution duty |
| `boundaries/world-adm0*.geojson` (2 files, 162 countries) | geoBoundaries gbOpen, assembled per country (its features carry geoBoundaries' own `iso3`/`shapeGroup`/`shapeName`; corrected — this register first guessed Natural Earth) | **MIXED, and ESTABLISHED per country as of 2026-09-28**: 162 upstream licences. **32** duty-free (28 Public Domain + 4 CC0) · **65** attribution-only · **65 SHARE-ALIKE CLASS** (58 ODbL 1.0 + 7 explicit ShareAlike: AGO GAB GEO GIN MMR NER TUR) | **Yes** — the per-country table (licence + licenseSource + boundaryID + boundarySource, fetched verbatim from the geoBoundaries API, 162 of 162, 0 failures) ships at `plugin-designs/my-upg-100-list/data/adm0/PROVENANCE.json`. `boundaries/ATTRIBUTION` references it by path rather than copying it | **OPEN, legal — not a measurement**: whether a redistributed boundary file is a "Derivative Database" under ODbL is unanswered, and half the duty-bearing countries turn on it. It needs an owner's decision on data licensing |
| `people-groups/*` (3 files, 8 MB) | peoplegroups.org | **not established** | no terms ship | **OPEN, judgment** — redistribution terms unknown |
| `pplr/*` (6 files, 16 MB) | **PPLR ArcGIS Online org** `services2.arcgis.com/S4ydGgujXcif36k3`, aggregating IMB (peoplegroups.org), Joshua Project, the ROP registry, and carrying WCD (World Christian Database) IDs. Built from a local 78 MB extract not present in this repo — evidence from exp-shepherd | **UNRESOLVED.** `aCountry` and `aIMBPeople` publish no `copyrightText`, description or licence (queried `f=json`) | nothing ships | **OPEN, judgment** — redistribution not established. WCD is a proprietary Brill database, so even the IDs need a check |
| `pplr/*` (6 files, 16 MB) | **PPLR ArcGIS Online org** `services2.arcgis.com/S4ydGgujXcif36k3`, aggregating IMB (peoplegroups.org), Joshua Project, the ROP registry, and carrying WCD (World Christian Database) IDs. Built from a local 78 MB extract not present in this repo | **UNRESOLVED.** `aCountry` and `aIMBPeople` publish no `copyrightText`, description or licence (queried `f=json`) | nothing ships | **OPEN, judgment** — redistribution not established. WCD is a proprietary Brill database, so even the IDs need a check |
| `tables/*` (304 files, 36 MB) | derived by `library/scripts/research/fetch-*.mjs` from several upstream APIs | **varies, not established per table** | nothing ships; there is no source or licence doc in `tables/` | **OPEN, judgment** — the largest gap by volume |
| `images/worldmap.svg` | recorded in `images/PROVENANCE` | see that file | the PROVENANCE file ships and explains why no licence file sits beside it | none claimed; reasoning recorded rather than assumed |
| `previews/*` (378 files) | our own poster previews | ours | n/a | none |

View file

@ -42,7 +42,6 @@ the app is rebuilt.**
| `build-gallery.mjs` | `out/index.html` — browsable preview grid, sectioned by region. |
| `build-hd-gallery.mjs` | `out/hd.html` — full-resolution review + copy-to-clipboard. |
| `export-public.mjs` | Copies PNGs into region folders OUTSIDE the repo (see below). |
| `snapshots/` | Frozen template versions. v5 is the approved current; v1–v4 are the lineage. |
## Environment

View file

@ -1,10 +1,7 @@
# needs-eyes — partner-embed fixtures
Under the team's Playwright policy (2026-09-25) only **rega** runs a browser on the VPS, and a
maintainer's laptop is the other pair of eyes. So a check that can only be seen has to be *openable by
someone else* — not a construct that lives inside one agent's gate process.
These pages are generated, not hand-written:
A check that can only be seen has to be *openable by someone else*, not a construct that lives inside
one gate's process. These pages are generated on demand and are not committed; write them with:
```
node internal/check-embedded-rtl.mjs --emit

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>partner</title>
<style>html,body{margin:0}#w{height:900px}</style></head>
<body><h1 id="partner-heading">Partner page</h1><div id="w"></div>
<!-- A REAL PARTNER EMBED SETS THIS. The generated leaf pages bake window.MAP_APP_API_URL, and
without it every data fetch resolves same-origin against the partner and fails, so the wall
renders zero cards and the locale has nothing to translate. The first run of this gate
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<script src="http://127.0.0.1:47395/dx-maps/my-upg-100-list/my-upg-100-list.js" crossorigin="anonymous"></script>
<script>
var el = document.createElement("my-upg-100-list-map");
el.setAttribute('profile-config', JSON.stringify({ profile: "poster-wall",
assetsBase: "http://127.0.0.1:47395/dx-maps/assets" }));
el.style.cssText = 'display:block;width:100%;height:900px';
document.getElementById('w').appendChild(el);
</script></body></html>

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>partner</title>
<style>html,body{margin:0}#w{height:900px}</style></head>
<body><h1 id="partner-heading">Partner page</h1><div id="w"></div>
<!-- A REAL PARTNER EMBED SETS THIS. The generated leaf pages bake window.MAP_APP_API_URL, and
without it every data fetch resolves same-origin against the partner and fails, so the wall
renders zero cards and the locale has nothing to translate. The first run of this gate
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<script src="http://127.0.0.1:47395/dx-maps/my-upg-100-list/my-upg-100-list.js" crossorigin="anonymous"></script>
<script>
var el = document.createElement("my-upg-100-list-map");
el.setAttribute('profile-config', JSON.stringify({ profile: "poster-wall", locale: 'ar',
assetsBase: "http://127.0.0.1:47395/dx-maps/assets" }));
el.style.cssText = 'display:block;width:100%;height:900px';
document.getElementById('w').appendChild(el);
</script></body></html>

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>partner</title>
<style>html,body{margin:0}#w{height:900px}</style></head>
<body><h1 id="partner-heading">Partner page</h1><div id="w"></div>
<!-- A REAL PARTNER EMBED SETS THIS. The generated leaf pages bake window.MAP_APP_API_URL, and
without it every data fetch resolves same-origin against the partner and fails, so the wall
renders zero cards and the locale has nothing to translate. The first run of this gate
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<script src="http://127.0.0.1:47395/dx-maps/my-upg-100-list/my-upg-100-list.js" crossorigin="anonymous"></script>
<script>
var el = document.createElement("my-upg-100-list-map");
el.setAttribute('profile-config', JSON.stringify({ profile: "upg-100-mfe", locale: 'ar',
assetsBase: "http://127.0.0.1:47395/dx-maps/assets" }));
el.style.cssText = 'display:block;width:100%;height:900px';
document.getElementById('w').appendChild(el);
</script></body></html>

View file

@ -158,7 +158,7 @@ the IMB / peoplegroups.org dataset (never a hand-kept roster). Aliases name them
## Open points found while writing this down
Each is a defect or a decision, routed through dx-shepherd.
Each is a defect or a decision, still open.
1. **`profile-config.tokenUrl` has no reader.** It is written by the studio and the gallery and
read by nothing. The reader's own URL is also never forwarded as `tokenUrl`, only as the token

View file

@ -356,8 +356,6 @@ The measurements behind the rules above, kept for whoever needs the reasoning.
- **Undocumented gates (2026-09-22).** 26 gates lived outside the build and only nine were listed; two
unlisted ones had been failing for two sessions. A standalone gate nobody knows about does not run —
which is why the table above is the list to run before publishing.
- **`check:dxlife`.** It exits 2 ("clone not found") on any machine without a sibling DX-life clone — a
deliberate third value that `bun run` reports as a failure. It is not in the build, so a clone still builds.
- **Screenshot distinctness.** Two README slots once showed the same view under two captions: the shot
chased a "Next" control that did not exist inside a `try/catch`, and the PNGs differed by 0.16% render
noise, so no checksum caught them.

View file

@ -194,7 +194,7 @@ export function backendDevPlugin(root, env = process.env) {
name: 'dx-backend-dev',
apply: 'serve',
configureServer(server) {
// ENGINE NOT RUNNING, QUIETLY (dx-shepherd, R500): with no backend process the proxy answered
// ENGINE NOT RUNNING, QUIETLY (R500): with no backend process the proxy answered
// 500 ECONNREFUSED, which every page showed as console errors. While a backend is down its routes get
// well-formed answers instead: /events streams ONE status event saying the engine is offline (the
// plugin renders its own "engine not running" state from it), anything else returns an empty JSON

View file

@ -168,7 +168,7 @@ done
# Everything busy: keep POLLING EVERY SLOT until the deadline, and report a timeout as UNMEASURED
# rather than as a verdict. This used to queue on slot 1 alone (flock -w on slot 1), so a slot that
# freed up was never taken: measured 2026-09-25 (dx-shepherd), ten waiters were piled on slot 1 while
# freed up was never taken: measured 2026-09-25, ten waiters were piled on slot 1 while
# slot 2 sat free, and with slot 1 held 12 s and slot 2 freed at ~3 s a caller waited 11.5 s — the
# two-slot semaphore had collapsed into ONE queue. Polling takes the first slot that frees (~4 s in
# the same test). Trade-off, accepted: polling is not first-come-first-served, so under heavy

View file

@ -30,7 +30,7 @@ check('a GeoJSON feature is matched to rows on the same point', colocatedWith(id
check('a lone point shares with nobody', colocatedWith(idx, rows[3]).length === 0, colocatedWith(idx, rows[3]))
check('a row without a position is ignored', colocatedWith(idx, rows[4]).length === 0, colocatedWith(idx, rows[4]))
// A click hands over the RENDERED feature: the engine rebuilt its geometry from tile coordinates, so it is
// near the roster point but not equal to it: ~0.001 degree at low zoom (rega, row 46, a shared point that never matched).
// near the roster point but not equal to it: ~0.001 degree at low zoom (a browser check, row 46: a shared point that never matched).
const rendered = { type: 'Feature', properties: { name: 'Koraga' }, geometry: { type: 'Point', coordinates: [75.2486, 12.8431] } }
check('a rendered (tile-rounded) feature still finds its co-located groups', colocatedWith(idx, rendered).sort().join() === 'Godda,Maila', colocatedWith(idx, rendered))
// The same people in two countries: the nearest roster row wins, not the first.
@ -39,7 +39,7 @@ const renderedFar = { type: 'Feature', properties: { name: 'Koraga' }, geometry:
check('a repeated name resolves to the nearest roster point', colocatedWith(twin, renderedFar).join() === 'Other', colocatedWith(twin, renderedFar))
// The rows a map DRAWS are normalized people groups (name + latitude/longitude, no geometry), and every
// map built on useMapData must offer them to its legends: the prayer map did not, so its legends read the
// raw API cache and neither the shared-point sentence nor the standing note appeared (rega, row 46 at 835ec1ac).
// raw API cache and neither the shared-point sentence nor the standing note appeared (a browser check, row 46 at 835ec1ac).
const drawn = [{ name: 'A', latitude: 12.85, longitude: 75.25 }, { name: 'B', latitude: '12.85', longitude: '75.25' }]
const clickedA = { type: 'Feature', properties: { name: 'A' }, geometry: { type: 'Point', coordinates: [75.2504, 12.8497] } }
check('normalized rows (no geometry) index and match a clicked pin', colocatedWith(buildColocationIndex(drawn), clickedA).join() === 'B', colocatedWith(buildColocationIndex(drawn), clickedA))

View file

@ -1,101 +0,0 @@
#!/usr/bin/env node
/**
* check-backlog-refs.mjs — a card may not point at a card that does not exist.
*
* THE DEFECT THIS CATCHES. `depends_on` is the board's readiness mechanism: a card is ready when
* its dependencies are done. A dependency naming a card that is NOT IN THE BACKLOG can never be
* done, so the card can never become ready — and nothing says so. It reads as ordinary blocked
* work forever.
*
* MEASURED 2026-09-24, three dangling references across the builder backlog:
* IDEA-SLIM-MAP-SEARCHBAR-HOW-DO-FILTERS-FIT <- BUILD-PEOPLE-GROUP-SEARCH-…depends_on
* IDEA-REGIONS-DASHBOARD-REVERSE-ENGINEER-DXLIFE <- two regions cards' depends_on
* PRINCIPLE-MAPS-ARE-SECONDARY-LISTS-AND-GRIDS-ARE-PRIMARY <- two cards' parent
* The first one matters most: that card is status:todo and waiting on something unfindable.
* The last is cited by the loop prompt itself as an open card ("PRINCIPLE-MA"), so a reader is
* being pointed at work that has no record.
*
* ★ SEVERITY IS NOT UNIFORM, so this gate grades it. A dangling `depends_on` on an OPEN card is a
* FAILURE — that card is unreachable. A dangling `parent`, or a dangling anything on a card that is
* already done, is REPORTED but does not fail: losing a parent is a filing problem, not a blocked
* one, and failing on it would only teach people to delete the reference rather than restore the
* card. Naming the difference is the point.
*
* ⚠ IT ONLY CHECKS REFERENCES THAT LOOK LIKE CARD IDS. Fields like `parent` also legitimately hold
* prose in some cards ("the regions epic"), and treating those as broken links would drown the
* real ones. The shape is SCREAMING-KEBAB, 9+ chars — the convention every id in this board uses.
*
* Usage: node internal/check-backlog-refs.mjs
* SKIPs when _qa-cycle is absent (it lives outside every repo, by design).
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const QA = process.env.QA_CYCLE || resolve(HERE, '..', '..', '_qa-cycle')
if (!existsSync(QA)) {
console.log('BACKLOG-REFS: SKIP — _qa-cycle not present (expected in CI; it is in no repo).')
console.log(' This gate reads the AGENTIC BACKLOGS, which live outside the repo and are not')
console.log(' cloned with it. Absence is expected; it is NOT a pass.')
process.exit(0)
}
const PHASES = ['01-ideation-research', '02-builder-cycle', '03-deployment-cycle']
const REF_FIELDS = ['parent', 'depends_on', 'blocks', 'supersedes', 'supersedes_mechanism_of']
// The id convention every card in this board follows. Prose in a `parent` field must NOT match.
const ID_SHAPE = /^[A-Z][A-Z0-9-]{8,}$/
const cards = []
for (const phase of PHASES) {
const p = join(QA, phase, 'backlog.json')
if (!existsSync(p)) continue
const j = JSON.parse(readFileSync(p, 'utf8'))
for (const c of j.cards || []) cards.push({ ...c, _phase: phase })
}
if (!cards.length) {
console.log('BACKLOG-REFS: SKIP — no cards found in any phase backlog.')
process.exit(0)
}
const ids = new Set(cards.map((c) => c.id))
const OPEN = new Set(['todo', 'doing', 'in-progress', 'blocked'])
const blocking = [] // dangling depends_on on an OPEN card — the card can never become ready
const noted = [] // everything else dangling — filing debt, reported not failed
for (const c of cards) {
const f = c.fields || {}
for (const key of REF_FIELDS) {
const raw = f[key] !== undefined ? f[key] : c[key]
if (!raw) continue
for (const ref of (Array.isArray(raw) ? raw : [raw])) {
if (typeof ref !== 'string' || !ID_SHAPE.test(ref)) continue
if (ids.has(ref)) continue
const entry = { from: c.id, key, ref, status: c.status || '(none)' }
if (key === 'depends_on' && OPEN.has(String(c.status || '').toLowerCase())) blocking.push(entry)
else noted.push(entry)
}
}
}
console.log(`BACKLOG-REFS: ${cards.length} card(s) across ${PHASES.length} backlog(s), `
+ `${REF_FIELDS.length} reference field(s) checked.`)
if (noted.length) {
console.log(` ${noted.length} dangling reference(s) that do NOT block anything:`)
for (const n of noted) console.log(` ${n.from} [${n.status}] .${n.key} -> ${n.ref}`)
console.log(' Filing debt: a lost parent or a stale link on a finished card. Restore the card')
console.log(' or drop the reference — but do not delete it just to quiet this line.')
}
if (blocking.length) {
console.error(`\nBACKLOG-REFS: FAIL — ${blocking.length} OPEN card(s) depend on a card that does not exist.`)
for (const b of blocking) {
console.error(` ✗ ${b.from} [${b.status}] depends_on ${b.ref}`)
}
console.error('\n A dependency that is not in the backlog can never be done, so these cards can')
console.error(' never become ready — and nothing says so. They read as ordinary blocked work.')
console.error(' Fix: restore the missing card, or remove the dependency if it is obsolete.')
process.exit(1)
}
console.log('BACKLOG-REFS: PASS — every open card\'s dependencies name a card that exists.')

View file

@ -1,121 +0,0 @@
#!/usr/bin/env node
/**
* check-backlog-status-enum.mjs — a card's `status` must be a machine-readable enum, not prose.
*
* THE DEFECT THIS CATCHES, and it was MY OWN reporting error for an entire flight. Cards were being
* written with explanatory statuses:
*
* "done — reconciled: big-picture + dashboard-plus-grid are current, unified-research-dashboard…"
* "blocked — Steps 1+3 DONE and re-verified 2026-09-22 at 19/19 bundles…"
* "ready"
*
* The prose is genuinely valuable — several of those strings carry measurements — but it made
* `status` unqueryable. Every count I reported used `status !== 'done'`, which is FALSE for
* `"done — reconciled: …"`, so finished cards kept being counted as open.
*
* MEASURED 2026-09-22 across the three phase backlogs: 130 of 395 cards carried a prose status, and
* 71 of those were ALREADY DONE. Reported open was 176; the real number was 105. The P0 figure I had
* been quoting every tick was 21; it was actually 14.
*
* ★ WHY THIS IS WORSE THAN AN ORDINARY OFF-BY-N. A backlog is a decision instrument — it answers
* "what is left" and "is the remaining work human-gated". Inflating the open count makes a drained
* board look stalled, which pushes toward manufacturing work rather than holding honestly. And the
* error is self-concealing: every individual card reads correctly to a human, because the prose says
* exactly what is true. Only the AGGREGATE is wrong.
*
* THE FIX PRESERVES THE PROSE. Migration moved it to `fields.status_detail` and normalised `status`
* to the enum; 130/130 preserved, 0 lost. The explanation was never the problem — putting it in the
* field that tooling reads was.
*
* WHAT IT ASSERTS: every card's `status` is ONE WORD from a closed set — the five workflow
* states plus four terminal ones (superseded / deferred / decided / verified-already-true).
* The point is not the particular words: it is that `status` stays machine-readable so the
* open-count is right. Prose belongs in fields.status_detail.
*
* It deliberately does NOT require `status_detail` to exist. Most cards need no explanation, and
* demanding one would turn a real check into paperwork that gets satisfied with filler.
*
* NEGATIVE-TESTED: setting one card's status back to "done — reconciled: …" fails by name.
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const QA = join(ROOT, '../_qa-cycle')
const PHASES = ['01-ideation-research', '02-builder-cycle', '03-deployment-cycle']
// ★ THE ENUM WAS TOO NARROW, not the cards wrong. MEASURED 2026-09-24 across 633 cards:
// EIGHT distinct status values are in use and EVERY ONE IS A SINGLE WORD — there is no
// prose left anywhere in the field. The nine this gate was failing carried legitimate
// TERMINAL states the original enum simply predated: superseded (5), deferred (2),
// decided, verified-already-true.
//
// This gate exists to keep the AGGREGATE honest — "a prose status makes every open-count
// wrong while each card still READS correctly". A one-word terminal state serves that
// purpose perfectly; it is exactly what the 130-card migration was FOR. Rewriting nine
// accurate statuses to 'done' would have destroyed real distinctions (a SUPERSEDED card
// was replaced by another; a DEFERRED one was consciously postponed; neither was
// finished) to satisfy a list — losing information to make a checker green.
//
// OPEN vs CLOSED is the property that matters, and all four additions are CLOSED:
// open = todo + doing + in-progress (66), closed = done + these four (507), blocked (60).
const TERMINAL_EXTRA = ['superseded', 'deferred', 'decided', 'verified-already-true']
const ALLOWED = new Set(['todo', 'doing', 'in-progress', 'blocked', 'done', ...TERMINAL_EXTRA])
if (!existsSync(QA)) {
// _qa-cycle lives outside every repo the CI clones, so absence is EXPECTED there. Say so loudly
// rather than passing silently — a gate that reports success when it never ran is the failure
// mode this repo has already been bitten by.
console.log('BACKLOG-STATUS: SKIP — _qa-cycle not present (expected in CI; it is in no repo).')
process.exit(0)
}
const offenders = []
let total = 0
const counts = {}
for (const ph of PHASES) {
const p = join(QA, ph, 'backlog.json')
if (!existsSync(p)) continue
let cards
try {
cards = JSON.parse(readFileSync(p, 'utf8')).cards || []
} catch (e) {
console.error(`BACKLOG-STATUS: FAIL — ${ph}/backlog.json does not parse: ${e.message}`)
process.exit(1)
}
for (const c of cards) {
total++
const s = String(c.status)
counts[s] = (counts[s] || 0) + 1
if (!ALLOWED.has(s)) offenders.push({ ph, id: c.id, s })
}
}
if (!total) {
console.error('BACKLOG-STATUS: FAIL — found 0 cards across three phase backlogs.')
console.error(' A gate with nothing to assert is not passing; it is not running.')
process.exit(1)
}
if (offenders.length) {
console.error(`BACKLOG-STATUS: FAIL — ${offenders.length} of ${total} card(s) carry a non-enum status.\n`)
for (const o of offenders.slice(0, 12)) {
console.error(` ${o.ph} ${o.id}`)
console.error(` status: "${o.s.slice(0, 84)}${o.s.length > 84 ? '…' : ''}"`)
}
if (offenders.length > 12) console.error(` …and ${offenders.length - 12} more`)
console.error('\n Allowed: ' + [...ALLOWED].join(' / '))
console.error(' Keep the explanation — move it to fields.status_detail. The prose is valuable and')
console.error(' often carries measurements; it just must not live in the field tooling filters on.')
console.error(' A prose status makes every open-count wrong while each card still READS correctly,')
console.error(' so the error hides in the aggregate: 130 of 395 cards once carried one, 71 of them')
console.error(' already done, and the reported open count was 176 against a real 105.')
process.exit(1)
}
console.log(`BACKLOG-STATUS: PASS — ${total} card(s), every status is a queryable enum.`)
const order = ['todo', 'doing', 'in-progress', 'blocked', 'done']
console.log(' ' + order.filter((k) => counts[k]).map((k) => `${k} ${counts[k]}`).join(' · '))
console.log(' Explanations live in fields.status_detail, which is not required — most cards need')
console.log(' none, and demanding one would turn this into paperwork satisfied with filler.')

View file

@ -112,6 +112,38 @@ ok(!noCountry.length && resolveTerritory({ shapeName: 'Mahārāshtra' }).country
ok(!disagree.length, `Copy's regroup warning disagrees with apply-grouping: ${disagree.join('; ')}`,
`the regroup warning matches apply-grouping's own verdict in all ${Object.keys(REGROUP_CASES).length} cases (India, China, countries, and the three it refuses)`)
// ── A6. A STATE-ONLY selection titles its Prayer list / Card deck ───────────────────────────────────────
// Development threw "Reduce of empty array" for a state-only selection: the title named zones and countries only, so
// a selection of Gujarāt alone had NO names. On the REAL served table: Gujarāt alone is titled "Gujarāt"; a state +
// a country names both; a province with no table beside it is not named; Mizoram (no groups listed) alone gives the
// keyed "no people groups" text. NEGATIVE CONTROL: the old naming (zones + countries) gives [] and reduce() throws.
{
const { keyedError, errorText } = await import(pathToFileURL(P('library/i18n/keyedError.js')).href)
const wallEn = JSON.parse(readFileSync(P('library/i18n/locales/en/wall.json'), 'utf8'))
const tr = (k) => k.split('.').slice(1).reduce((o, x) => (o == null ? o : o[x]), wallEn) ?? k
const states = JSON.parse(readFileSync(P('data/people-groups/india-states.json'), 'utf8')).states
const st = (shape) => states.find((x) => x.shapeName === shape)
const guj = st('Gujarāt'), miz = st('Mizoram')
const fromOf = (pairs) => { const m = new Map(); for (const [slugs, name] of pairs) for (const s of slugs) m.set(s, [...(m.get(s) || []), name]); return m }
const noRecords = () => keyedError('wall.hud.noRecords', 'that list has no people groups to print')
const join = (a, b) => `${a} + ${b}`
const T = (kind, name) => ({ kind, name })
const alone = X.selectionTitleNames([], [T('adm1', 'Gujarāt')], fromOf([[guj.slugs, 'Gujarāt']]), noRecords)
const both = X.selectionTitleNames([], [T('adm0', 'Nepal'), T('adm1', 'Gujarāt')], fromOf([[['npl-a', 'npl-b'], 'Nepal'], [guj.slugs, 'Gujarāt']]), noRecords)
const withProvince = X.selectionTitleNames([], [T('adm1', 'Guizhou Province'), T('adm1', 'Gujarāt')], fromOf([[guj.slugs, 'Gujarāt']]), noRecords)
let empty = ''
try { X.selectionTitleNames([], [T('adm1', 'Mizoram')], fromOf([[miz.slugs, 'Mizoram']]), noRecords); empty = 'no throw' } catch (e) { empty = errorText(e, tr) }
let oldThrows = false
try { [].reduce(join) } catch (_) { oldThrows = true }
const oldNames = [...[], ...[T('adm1', 'Gujarāt')].filter((t) => t.kind === 'adm0').map((t) => t.name)]
ok(guj.count === 90 && miz.count === 0 && oldNames.length === 0 && oldThrows,
`NEGATIVE CONTROL: Gujarāt ${guj.count} / Mizoram ${miz.count} groups, the old naming gave ${JSON.stringify(oldNames)}`,
'negative control: on the old naming a Gujarāt-only selection has no names, and reduce() throws')
ok(alone.length === 1 && alone.reduce(join) === 'Gujarāt' && both.join('|') === 'Nepal|Gujarāt' && withProvince.join('|') === 'Gujarāt' && empty === tr('wall.hud.noRecords'),
`titles: alone ${JSON.stringify(alone)}, with a country ${JSON.stringify(both)}, with a province ${JSON.stringify(withProvince)}, Mizoram alone "${empty}"`,
`a state-only selection is titled "Gujarāt"; a state + a country names both; a province with no table is not named; Mizoram alone reads "${empty}"`)
}
// ── B. the CSV ───────────────────────────────────────────────────────────────────────────────────
function parseCsv(s) { // RFC 4180: quoted fields, doubled quotes, CRLF records
const rows = []; let row = [], cell = '', q = false
@ -151,7 +183,7 @@ const wired = {
'one detection for every action': (vue.match(/await selectionPeople\(\)/g) || []).length >= 2 && /selectionPeople\(\)\.then/.test(vue),
// India's states resolve through the served table (library/data/indiaStates.js), fetched only once a state is selected
// In batch mode a tap on a pin opens NO detail sheet (it covered the whole HUD on a phone); the tapestry
// listener under it toggles the territory. The real-tap proof is rega's; this pins the guard in the source.
// listener under it toggles the territory. The real-tap proof is a browser check; this pins the guard in the source.
'a pin tap in batch mode opens no sheet': /map\.on\('click', 'upg-pins-hit', \(e\) => \{[\s\S]{0,700}?if \(batchMode\.value\) return\s*\n\s*const f = e && e\.features/.test(vue),
// …and that tap toggles the territory EXACTLY ONCE: the tapestry listener already does it, so the pin handler
// must not toggle a second time (select-then-deselect in one tap).
@ -161,6 +193,9 @@ const wired = {
return !/toggleTerritory|toggleByProps|toggleZoneSelection/.test(body)
})(),
'the regroup warning on Copy': /const warn = regroupable\(sel\) \? '' : t\('upgMap\.copyNotRegroupable'\)/.test(vue),
// The packet / card-deck title goes through selectionTitleNames (arm A6 proves it), with the keyed empty case.
'the title names come from selectionTitleNames': /const names = selectionTitleNames\(zoneNames, selectedTerritories\.value, from,\s*\n\s*\(\) => keyedError\('wall\.hud\.noRecords'/.test(vue)
&& !/const names = \[\.\.\.zoneNames, \.\.\.countryNames\]/.test(vue),
'India states detected from the served table': /import \{ loadIndiaStates \} from '@map\/data\/indiaStates\.js'/.test(vue)
&& /india\.byShapeName\.get\(t\.name\)/.test(vue) && /t\.kind === 'adm1'\) indiaStates\(\)\.catch/.test(vue),
}

View file

@ -111,8 +111,8 @@ if (!targets.length) {
// The token to hand the partner page is the one the tree already bakes into its own leaves: this
// gate must not invent a key, and a token that is not this tree's would 401 and look like our bug.
// The tree bakes no token since the auto engine (the repo ships none), so every tk case SKIPPED and every
// provider case went UNMEASURED: a hollow PASS (rega, ded4b3c1). A seat that has a real public token passes
// it as TEST_MAPBOX_TOKEN (as rega's checks do); otherwise a baked one is still used if a tree has it.
// provider case went UNMEASURED: a hollow PASS (measured at ded4b3c1). A seat that has a real public token passes
// it as TEST_MAPBOX_TOKEN (as the browser checks do); otherwise a baked one is still used if a tree has it.
const bakedToken = process.env.TEST_MAPBOX_TOKEN || (() => {
for (const b of manifest.bundles || []) {
for (const p of b.profiles || []) {
@ -170,10 +170,10 @@ try {
lines.push(` · ${t.element} [${c.knob}] SKIPPED — no baked token in this tree to hand the host`)
continue
}
// The composite's OWN profile rides with the knob under test (rega, 1a761741: gospel-resources,
// The composite's OWN profile rides with the knob under test (measured at 1a761741: gospel-resources,
// my-upg-100-list, regions-dashboard and two more drew no map under a knob-only config, because a
// multi-profile element needs to be told which profile is the composite). A knob still wins.
// A composite that opens on a non-map view must be told to show its map (exp-shepherd: country-posters-mfe
// A composite that opens on a non-map view must be told to show its map (country-posters-mfe
// reads profile-config `view`), or there is no canvas to measure.
const VIEW = { 'country-posters-mfe': { view: 'map' } }
const cfg = JSON.stringify({ profile: t.profile, ...(VIEW[t.element] || {}), ...c.config(bakedToken) }).replace(/'/g, '&#39;')

View file

@ -11,7 +11,7 @@
*
* ★ WHY IT NOW REBUILDS AND COMPARES BYTES, NOT MTIMES (2026-09-25). The first version compared
* the newest child SOURCE mtime with the parent BUNDLE mtime. File times are not build facts:
* a git merge or checkout rewrites sources with checkout-time mtimes, so rega's publish of
* a git merge or checkout rewrites sources with checkout-time mtimes, so a publish of
* beb13da3 stopped on "doxa-maps-spa / plugin-studio / stack-demo STALE vs gospel-resources /
* research-dashboard" although a rebuild reproduced the committed bytes exactly — and two of
* those parents mount their children BY TAG at runtime, so they compile none of the child in
@ -61,7 +61,7 @@ const TREE = join(ROOT, 'public', TREE_NAME)
const argv = process.argv.slice(2)
const oi = argv.indexOf('--only')
const ONLY = oi >= 0 && argv[oi + 1] ? new Set(argv[oi + 1].split(',')) : null
// NOT ONLY COMPOSITES (rega, 2026-09-25): a library change merged into main reached prayer-walk-planner,
// NOT ONLY COMPOSITES (measured 2026-09-25): a library change merged into main reached prayer-walk-planner,
// which composes nothing, so this gate could not see it was stale. --all checks EVERY bundle; --bundles a,b
// checks those (the landing precondition passes the ones a landing can have changed).
const ALL = argv.includes('--all')

View file

@ -26,7 +26,7 @@
*
* NOT A SUBSTITUTE FOR A BROWSER. Both ends existing is necessary, not sufficient: UPG-100's
* select-zone listener exists and still loses the first click, because the lazy Map pane attaches it
* too late (rega, 2026-09-26). The runtime half is check:gallery-embed's `linked` phase.
* too late (measured 2026-09-26). The runtime half is check:gallery-embed's `linked` phase.
*
* NEGATIVE-TESTED (2026-09-26, main 521f8b18, PASS with 17 links and the 3 LATE_KNOWN sites), each FAILS:
* a receiver renamed in source ('map-filtered' -> 'map-filter' in research-map-plus-grid.vue);

View file

@ -19,7 +19,7 @@
* `tk` to a baked-in child, at production stage. See the comment at the scan below.
*
* Source-level and headless BY DESIGN: the behavioural twin (check:composite-embed-knobs) needs a
* browser and only rega may run one. Exit 0 pass · 1 fail.
* browser, and this gate runs without one. Exit 0 pass · 1 fail.
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'

View file

@ -10,7 +10,7 @@
* and Simple Research, both in the Research Gallery). Every structural gate passed. This gate runs
* the thing that failed: the composite's own setup, and its mounted hooks.
*
* HOW. No browser (fleet policy: only rega launches one). Vite loads each composite .vue through
* HOW. No browser (by design: this gate runs headless). Vite loads each composite .vue through
* ssrLoadModule with the repo's alias and the same vue({ customElement: true }) plugin, and
* @vue/runtime-core mounts it on a no-op renderer. The SSR compile gives the component an
* ssrRender rather than a render, so the renderer draws nothing and no CHILD is set up: this

View file

@ -1,149 +0,0 @@
#!/usr/bin/env node
/**
* check-done-evidence.mjs — a card may not reach status:done with NO evidence.
*
* WHY (2026-09-21)
* Two quality cards were status:done with an EMPTY bluf. Both turned out genuinely done — but
* only because I opened a browser and read the source. A board-wide count then found 18 done
* cards carrying NEITHER a bluf NOR any fields: a status word and nothing else.
*
* ★ THE HAND-OFF IS THE POINT. The Doxa team will read this board to learn what exists. A card
* reading "done" with no evidence cannot be reconciled against the code at all — they cannot
* tell a shipped deliverable from an abandoned one someone closed. This flight found SIX cards
* whose stated status disagreed with reality; an evidence-less card is the unfalsifiable case.
*
* WHAT COUNTS AS EVIDENCE (deliberately generous — this is a floor, not a style guide)
* EITHER a non-empty `bluf` OR a non-empty `fields` object. Both are free to write at the moment
* the work is fresh, which is the only moment the evidence is cheap and accurate.
*
* BASELINE, NOT A CLIFF
* The 10 pre-existing offenders are allow-listed by id. A gate that fails on day one for history
* nobody can fix gets disabled, and then it protects nothing. This fails ONLY on a NEW one — the
* same shape as check-label-joins, which allow-lists two known occurrences.
* ★ TO CLEAR AN ENTRY: add real evidence to the card, then delete its id here. The list should
* only ever shrink; a PR that grows it is doing the thing this gate exists to stop.
*
* NEGATIVE-TESTED: adding a fresh done card with no bluf/fields makes this FAIL naming it,
* EXIT=1; removing it returns PASS.
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
// ★ DEPTH 2, NOT 1 — and that is deliberate but load-bearing. Every sibling gate in internal/
// resolves ROOT one level up (the repo). This one goes TWO levels up, because the backlogs it
// checks live in `_qa-cycle/`, which is AGENTIC SCRATCH OUTSIDE THE REPO — tracked by no git
// repository, and never cloned with the studio.
//
// THE HAND-OFF DEFECT THIS GUARD FIXES (measured 2026-09-22). On this machine _qa-cycle happens
// to sit beside the repo, so the gate passes. An inheritor who clones ONLY the studio has no
// sibling _qa-cycle — and the gate did NOT fail there. It printed
// "0 card(s) across 3 backlog(s), 0 done" ... "PASS"
// and exited 0. A VACUOUSLY GREEN gate is worse than no gate: it certifies nothing while looking
// healthy, which is exactly the failure shape this repo keeps re-learning.
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
const QA = join(ROOT, '_qa-cycle')
if (!existsSync(QA)) {
console.log('DONE-EVIDENCE: SKIP — no _qa-cycle/ beside this repo, so there are no backlogs to check.')
console.log(' This gate reads the AGENTIC BACKLOGS, which live outside the repo and are not cloned')
console.log(' with it. Absence is expected for anyone who checked out the studio on its own; it is')
console.log(' NOT a pass. Run it from a workspace that has _qa-cycle/ as a sibling of this repo.')
process.exit(0)
}
const BACKLOGS = [
'01-ideation-research/backlog.json',
'02-builder-cycle/backlog.json',
'03-deployment-cycle/backlog.json',
]
// Pre-existing, from before the rule. Verified 2026-09-21. SHRINK ONLY.
// ★ THE BASELINE IS NOW EMPTY, and that is the strongest state this gate can be in.
// All ten exemptions had since gained evidence — the gate reported every one of them
// as STALE on 2026-09-24 — so the list was pure dead weight: ten ids that exempted
// nothing while implying ten cards still owed a record.
// An EMPTY set is not an unset one: every done card must now carry evidence, with no
// exemptions, and the gate fails outright for a single card that does not. Keeping
// `{ ... }` full of satisfied ids would mean the same today and quietly permit a
// regression the moment someone "reused" an entry.
// If a genuinely un-reconstructable card ever needs an exemption, add its id here WITH
// the reason — and the gate will tell you the day it becomes stale.
const BASELINE = new Set([
])
let checked = 0, done = 0
const offenders = []
const staleBaseline = []
// What counts as evidence on a done card, by schema family:
// 1. `bluf` or a non-empty `fields` object (the original schema);
// 2. `one_line` or `verify` (the second schema; see the note in the loop below);
// 3. a non-empty `evidence` LIST with at least one non-blank entry. 44 cards record their proof
// this way, and 12 of them carry none of the other fields, so each false-failed on closing
// (DESIGN-TEAM-ROLES-MEMBER-DESIGNER-ADMIN did, with its proof sitting in the list).
function carriesEvidence(c) {
const str = (v) => String(v || '').trim().length > 0
if (str(c.bluf) || str(c.one_line) || str(c.verify)) return true
if (c.fields && Object.keys(c.fields).length > 0) return true
if (Array.isArray(c.evidence) && c.evidence.some((e) => str(typeof e === 'string' ? e : JSON.stringify(e ?? '')))) return true
return false
}
// Self-test, both sides: a non-empty list passes; an empty list, or one of blanks, still fails.
{
const ok = carriesEvidence({ status: 'done', evidence: ['check:x PASS 12/12'] })
const empty = carriesEvidence({ status: 'done', evidence: [] })
const blanks = carriesEvidence({ status: 'done', evidence: ['', ' '] })
if (!ok || empty || blanks) {
console.error('DONE-EVIDENCE: SELF-TEST FAILED — an evidence list is not judged by its content')
process.exit(1)
}
}
for (const rel of BACKLOGS) {
const p = join(QA, rel)
if (!existsSync(p)) continue
let cards
try { cards = JSON.parse(readFileSync(p, 'utf8')).cards || [] } catch { continue }
for (const c of cards) {
checked++
if (c.status !== 'done') continue
done++
// ★ THE BOARD USES TWO CARD SCHEMAS, and this gate only knew one. Older cards carry
// `bluf` + `fields`; a second family carries `one_line` (what was done), `why`, and
// `verify` (the artifact and how it was checked). MEASURED 2026-09-24 across 443 done
// cards: 40 carry `verify` and 41 carry `one_line`, and ALL THREE cards this gate was
// failing had one or both — with named probes, assertion counts and negative tests.
// They were among the best-evidenced cards on the board.
// `verify` is if anything the STRONGER signal: it names how the claim was checked,
// which is exactly what "evidence" means here. Demanding a redundant `bluf` alongside
// it would have taught the next agent to pad the field the checker reads rather than
// write the record — the gate training the behaviour it exists to prevent.
if (carriesEvidence(c)) {
// A baselined id that now HAS evidence is stale — report it so the list shrinks.
if (BASELINE.has(c.id)) staleBaseline.push(`${c.id} (${rel})`)
continue
}
if (BASELINE.has(c.id)) continue
offenders.push(`${c.id} (${rel})`)
}
}
console.log(`DONE-EVIDENCE: ${checked} card(s) across ${BACKLOGS.length} backlog(s), ${done} done, ` +
`${BASELINE.size} baselined.`)
for (const s of staleBaseline) {
console.log(` i BASELINE NOW STALE — ${s} has evidence; remove its id from check-done-evidence.mjs`)
}
if (offenders.length) {
console.error(`\nDONE-EVIDENCE: FAIL — ${offenders.length} card(s) are done with NO evidence in any field:`)
for (const o of offenders) console.error(` ✗ ${o}`)
console.error(
'\n Add ONE of: `bluf` or `one_line` saying what was done, `verify` naming how it was checked,\n' +
' a `fields` entry naming the artifact, or an `evidence` list. Any ONE is enough — the point is a\n' +
' record a reader can reconcile against the code, not a particular field name.\n' +
' it was verified. Write it now, while the work is fresh — reconstructing it later produces a\n' +
' plausible story indistinguishable from a real record.'
)
process.exit(1)
}
console.log('DONE-EVIDENCE: PASS — every done card carries evidence (bluf, one_line, verify, fields or an evidence list).')

View file

@ -1,101 +0,0 @@
#!/usr/bin/env node
/**
* check-dxlife-untouched.mjs — evidence for the ZERO DX-LIFE EDITS standing law.
*
* ★ WHY THIS IS NOT `git status` (a real false-pass, 2026-09-20)
* I verified the invariant for several ticks with:
* cd root-dx-life && git status --porcelain | wc -l # -> 0
* and reported that 0 as proof. root-dx-life IS NOT A GIT REPOSITORY — it is a
* plain directory clone. `git status` prints "fatal: not a git repository" to STDERR and nothing
* to stdout, so `wc -l` counts 0. THE CHECK RETURNED THE PASS VALUE *BECAUSE IT FAILED*.
* Identical in shape to the settled-tree churn bug (bfs rejected the argument, stderr discarded,
* wc -l counted an empty stream): A COMMAND'S ERROR PATH PRODUCING THE PASS VALUE THROUGH AN
* UNCHECKED PIPE. A safety invariant is the worst possible place for that.
*
* ★ WHAT THIS DOES INSTEAD — a VCS-INDEPENDENT WITNESS.
* Walks the clone and records (path, size, mtime) for every file into a baseline JSON. On later
* runs it DIFFS against that baseline and reports added / removed / modified. No VCS required,
* so it cannot be defeated by the tree not being a repo.
*
* FIRST RUN writes the baseline and says so plainly — it does NOT claim the tree is unmodified,
* because a baseline taken now can only prove "unchanged SINCE NOW". Honest framing matters more
* than a green line: this gate earns its value over time, not on its first invocation.
*
* USAGE
* node internal/check-dxlife-untouched.mjs # create or verify the baseline
* DXLIFE=/path/to/clone node internal/check-dxlife-untouched.mjs
* ★ WHAT IT WALKS: the DX Life CLONES, not every directory under the umbrella (2026-09-29). root-dx-life
* also holds git WORKTREES of doxa-life (wt-rg-*, today's research-gallery branches) and a DANGLING
* one (research-map-clean, whose .git points at a path that no longer exists). Walking them counted
* sanctioned work and generated output as "added" and held this gate red, while the clones themselves
* were untouched. A directory whose .git is a FILE beginning `gitdir:` is a worktree and is skipped,
* keyed on that file rather than on `git worktree list`, which does not show a dangling one. Skipped,
* never deleted: research-map-clean's parent repo is gone, so it may hold the only copy of its work.
* Generated dirs are skipped too, and the baseline is compared only WITHIN this scope, so narrowing
* the scope reads as no change rather than as thousands of removals.
*
* EXIT 0 unchanged (or baseline created) · 1 drift detected · 75 not measured (no clone on this machine)
*/
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const CLONE = process.env.DXLIFE || resolve(HERE, '..', '..', '..', 'root-dx-life')
const STORE = resolve(HERE, '..', '..', '_qa-cycle', '_scratch-coordinator', 'dxlife-baseline.json')
if (!existsSync(CLONE)) {
// Not measured, not failed: a machine without the DX Life umbrella has nothing this law covers.
console.log(`DXLIFE-UNTOUCHED: NOT MEASURED — no DX Life clone at ${CLONE} on this machine.`)
process.exit(75)
}
const SKIP = new Set(['node_modules', '.git', '.output', '.nuxt', '.cache', 'dist'])
const isWorktree = (dir) => {
try { const g = join(dir, '.git'); return statSync(g).isFile() && readFileSync(g, 'utf8').startsWith('gitdir:') } catch { return false }
}
const worktrees = [] // relative paths skipped as worktrees, reported below
const snap = {}
;(function walk(dir) {
if (dir !== CLONE && isWorktree(dir)) { worktrees.push(dir.slice(CLONE.length + 1)); return }
let entries
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
for (const e of entries) {
if (SKIP.has(e.name)) continue
const p = join(dir, e.name)
if (e.isDirectory()) { walk(p); continue }
try {
const st = statSync(p)
snap[p.slice(CLONE.length + 1)] = `${st.size}:${Math.round(st.mtimeMs)}`
} catch { /* unreadable file — skip rather than fail the invariant check */ }
}
})(CLONE)
const n = Object.keys(snap).length
if (!existsSync(STORE)) {
mkdirSync(dirname(STORE), { recursive: true })
writeFileSync(STORE, JSON.stringify({ created: new Date().toISOString(), clone: CLONE, files: snap }, null, 0))
console.log(`DXLIFE-UNTOUCHED: BASELINE CREATED — ${n} files recorded at ${CLONE}.`)
console.log(' This run proves NOTHING about the past; it establishes the reference point.')
console.log(' From here on, any add/remove/modify under that tree will be reported.')
process.exit(0)
}
// Compare within the walked scope: a baseline entry under a skipped worktree or generated dir is out of scope.
const inScope = (rel) => !worktrees.some((w) => rel === w || rel.startsWith(w + '/')) && !rel.split('/').some((seg) => SKIP.has(seg))
const base = Object.fromEntries(Object.entries(JSON.parse(readFileSync(STORE, 'utf8')).files).filter(([k]) => inScope(k)))
console.log(`DXLIFE-UNTOUCHED: skipped ${worktrees.length} worktree(s) (a .git FILE): ${worktrees.join(', ') || 'none'}`)
const added = Object.keys(snap).filter((k) => !(k in base))
const removed = Object.keys(base).filter((k) => !(k in snap))
const changed = Object.keys(snap).filter((k) => k in base && base[k] !== snap[k])
console.log(`DXLIFE-UNTOUCHED: ${n} files walked · +${added.length} -${removed.length} ~${changed.length}`)
if (added.length || removed.length || changed.length) {
console.error('DXLIFE-UNTOUCHED: FAIL — the read-only clone CHANGED:')
for (const f of added.slice(0, 5)) console.error(` ADDED ${f}`)
for (const f of removed.slice(0, 5)) console.error(` REMOVED ${f}`)
for (const f of changed.slice(0, 5)) console.error(` MODIFIED ${f}`)
console.error(' ZERO DX-LIFE EDITS is a standing law. Investigate before doing anything else.')
process.exit(1)
}
console.log('DXLIFE-UNTOUCHED: PASS — no file added, removed or modified since the baseline.')

View file

@ -12,7 +12,7 @@
*
* Nothing throws, because the t() shim falls back to its key. So it reads as "a few strings not
* translated yet" and it survived in country-posters, key-cities, gospel-resources, cities-internet and
* my-upg-100-list simultaneously — confirmed independently by regions-plugin, who censused all 19 entry
* my-upg-100-list simultaneously — confirmed independently by a census of all 19 entry
* files and found the same five and no sixth.
*
* ★ AND A CENSUS THAT COUNTED FUNCTION BODIES SAW FOUR OF SIXTEEN. That is why this gate greps for the

View file

@ -4,7 +4,7 @@
*
* THE DEFECT (2026-09-28, found testing live): the link copied from Simple Research was
* `research-map-plus-grid/research-map-plus-grid/index.html` while the page was
* `http://127.0.0.1:5192/#f=…` — "the perfect example of how embeddability is not working".
* `http://127.0.0.1:5192/#f=…`, a clear case of an embed that could not work.
*
* WHY IT SURVIVED SO LONG, and it is the reason this is a gate rather than a fix: a relative share link
* WORKS WHEN CLICKED. The browser resolves it against the page you copied it from, so the person testing

View file

@ -196,7 +196,7 @@ const run = async () => {
const f = __srcFeatures(m, 'rd-bubbles'); return !!(f && f.length > 0);
})()`, { timeout: 90000 })
} catch (e) {
// ★ DISTINGUISH "I CANNOT MEASURE THIS ENGINE" FROM "THE PRODUCT IS BROKEN".
// ★ DISTINGUISH "THIS ENGINE CANNOT BE MEASURED HERE" FROM "THE PRODUCT IS BROKEN".
// This gate reads the source's CURRENT GeoJSON off `_data`, a private Mapbox internal. MEASURED
// on one dashboard, one build, only the engine differing, by calling the app's own setData() to
// shrink 2107 features to 5:

View file

@ -38,7 +38,7 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { join, relative, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
// ★ SHARED WITH check:hardcoded-english (regions-plugin). Two gates must agree about WHICH files are
// ★ SHARED WITH check:hardcoded-english (the same file list). Two gates must agree about WHICH files are
// in scope even when they disagree about what they find in them; a second copy would drift on exactly
// the pair-form / transitive / cycle details below, and the drift would be silent.
import { effectiveStages, stageReason } from './lib/effective-stage.mjs'
@ -73,7 +73,7 @@ function findFrozen(ast, aliases) {
if (name && (TRANSLATORS.has(name) || aliases.has(name))) {
// tStatic(path, EXPLICIT_LOCALE) — the caller pinned it deliberately.
const pinned = name === 'tStatic' && node.arguments.length > 1
// ★ tStatic IS NEVER LIVE, WHEREVER IT SITS (exp, 2026-09-28). It resolves the locale via
// ★ tStatic IS NEVER LIVE, WHEREVER IT SITS (2026-09-28). It resolves the locale via
// getRuntimeLocale(), which reads a PLAIN window property — not a Vue ref — so a computed()
// wrapping it has NO reactive dependency and never invalidates. computed(() => tStatic(x))
// is strictly WORSE than the naked call: it looks like the fix, so nobody looks again.
@ -153,7 +153,7 @@ const DEAD_LOCALE_GETTER = /\b(getRuntimeLocale|detectLocale|localeFromUrl)\s*\(
/**
* Is offset `at` inside the parentheses of a computed()/watch()/watchEffect() that tracks the locale?
*
* ⚠ BRACE DEPTH, NOT A LOOKBACK WINDOW (gospel, GOS-FROZEN-SR). The first version of this asked
* ⚠ BRACE DEPTH, NOT A LOOKBACK WINDOW (GOS-FROZEN-SR). The first version of this asked
* whether a wrapper appeared in the preceding 260 characters. gospel hit exactly this in GiDrawer.vue
* — the enclosing `computed(` opens at line 35 and the call sites are at 95-100 — and reported SIX
* false positives on working code, nearly "fixing" it. Measured on the same shape here: 2,600
@ -206,7 +206,7 @@ function findStoredTreeFreeze(code) {
// function RETURNS A STRUCTURE whose values come from the translator — an options object or a
// node list — which a caller then stores. So require the translator call to sit inside a
// returned object/array literal, not merely somewhere in the body.
// ★ THE MECHANISM IS "BUILT CORRECTLY, THEN PARKED", not "built once at setup" (gospel, on
// ★ THE MECHANISM IS "BUILT CORRECTLY, THEN PARKED", not "built once at setup" (found on
// upg100's legendOpts). That function re-reads t() correctly on EVERY call — its own comment
// says the function shape is deliberate — and the freeze is one level out: the RESULT is
// assigned into a ref and nothing re-fills it. A rule that only looked for a translator outside
@ -282,7 +282,7 @@ function findDeadGetterComputed(code) {
}
/**
* SHAPE 3 — labels evaluated inside a provide() literal (gospel, GOS-FROZEN-SR; real in GiCombos.vue).
* SHAPE 3 — labels evaluated inside a provide() literal (GOS-FROZEN-SR; real in GiCombos.vue).
*
* provide('dashboardConfig', { facets: { access_level: { label: T('…') } } })
*
@ -388,7 +388,7 @@ const CASES = [
['GOOD inside an arrow', () => scanSource("const { t } = useI18n()\nconst f = () => t('a.b')").length, 0],
['GOOD tStatic with explicit locale', () => scanSource("const x = tStatic('a.b', 'en')").length, 0],
['GOOD watch(() => t())', () => scanSource("const { t } = useI18n()\nwatch(src, () => t('a.b'))").length, 0],
// ★ exp's false negative: a computed cannot make tStatic live.
// ★ A known false negative: a computed cannot make tStatic live.
['BAD computed(() => tStatic()) is STILL frozen', () => scanSource("const X = computed(() => tStatic('a.b'))").length, 1],
// A function BODY re-reads the global on every call (useOptionalT:74 returns such an arrow),
// so it is NOT frozen. Only a reactive wrapper caches it. I had this case backwards at first.

View file

@ -171,7 +171,7 @@ try {
the data was right and the feature was dead. */
/* ⚠ THE NAME, CASE-FOLDED, ANYWHERE IN THE CARD. This asserted `text.includes(slug.split('-')[0])`
— "abai" against a rendered "Abai Sungai" — and reported a false negative on a card that was
rendering correctly, which cost rega a round. A slug fragment is not the name; the roster has
rendering correctly, which cost a review round. A slug fragment is not the name; the roster has
the name, so the roster is what it compares. */
const anyRow = (JSON.parse(readFileSync(resolve(PUBLIC, TREE_NAME, 'assets/tables/doxa-people-groups.json'), 'utf8')).rows || [])
.find((r) => r.slug && r.mapped)

View file

@ -2,7 +2,7 @@
* check-gospel-engine-contract.mjs — the map must use the engine it was ASKED for, or say nothing
* at all. Never a different one.
*
* THE DEFECT THIS IS WRITTEN AGAINST (regions' C3 read + upg100's addendum, 2026-09-26).
* THE DEFECT THIS IS WRITTEN AGAINST (the C3 review and its addendum, 2026-09-26).
* resources-map resolved its engine as `engineGlobal(provider) || window.mapboxgl`. engineGlobal
* already defaults to mapbox, so that fallback could only fire for provider 'maplibre' — and then
* it returned MAPBOX. A page that chose the keyless engine silently got the one that needs a

View file

@ -1,7 +1,7 @@
/**
* check-gospel-english-leaks.mjs — a translated key must not render in English.
*
* THE METHOD, from rega by way of dx-shepherd. A string is a LEAK when the catalogue has a
* THE METHOD, taken from the browser review. A string is a LEAK when the catalogue has a
* DIFFERENT value for the locale and the page still shows the English one. That is a render
* or wiring fault, and it is a different thing from an untranslated string, which is a
* catalogue gap. Only the first is this check's business.
@ -75,7 +75,7 @@ try {
/* ═══ WHOLE LABELS, NOT SUBSTRINGS — the third correction, and the last ═══════════
* Substring matching over the rendered blob reported keys that were never rendered at
* all: "Language" matched inside the DATA — the people-group language "Sign Language" —
* and "Region" inside a region name. Those are exactly the data strings rega's method
* and "Region" inside a region name. Those are exactly the data strings the method
* excludes, and no amount of length filtering separates a label from a word that
* happens to sit inside one.
* So the unit is an ELEMENT'S OWN LABEL: the trimmed text a leaf element renders. A

View file

@ -39,7 +39,7 @@ import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const PORT = Number(process.env.RTL_SHAPE_PORT || 5393)
const POISON = process.argv.includes('--poison')
// ⚠ PARAMETERISED, not forked. dx-shepherd routed this gate to the other RAW-map owners
// ⚠ PARAMETERISED, not forked. This gate was routed to the other RAW-map owners
// (cities-internet, key-cities, upg-100-list, RegionCountryMap) — copying it would give each of
// them their own drifting copy of the pan-first logic and the poison arm, which are the two parts
// that took corrections to get right. The leaf is the only thing that differs.

View file

@ -6,7 +6,7 @@
* to zoom out past the point where ONE WORLD fills the canvas WIDTH. That puts a hard floor under the
* zoom at log2(canvasWidth/512) — and on a WIDE, SHORT figure that floor is too close for the
* drawing's own height, so the outermost groups fall outside the frame. minZoom does not help; the
* width constraint wins. rega measured it at 1440: 54 groups outside in ar and 97 in en, which is
* width constraint wins. A browser check measured it at 1440: 54 groups outside in ar and 97 in en, which is
* what proved it was the WIDTH and never RTL.
*
* WHAT IT ASSERTS. For every layout the charts actually get, the drawing's vertical extent at the
@ -45,11 +45,11 @@ export function neededHeightPx(extent, fraction, canvasWidth) {
return worldFraction * 512 * Math.pow(2, floorZoom(canvasWidth))
}
// The canvases the charts are actually given: rega's measured figures at 1440 and 1280, the embed
// The canvases the charts are actually given: the measured figures at 1440 and 1280, the embed
// card, and two narrow ones so a phone cannot regress unnoticed.
const LAYOUTS = [
{ name: '1440 board (rega measured)', w: 796, h: 249 },
{ name: '1280 board (rega measured)', w: 636, h: 220 },
{ name: '1440 board (measured)', w: 796, h: 249 },
{ name: '1280 board (measured)', w: 636, h: 220 },
{ name: 'embed card', w: 1198, h: 560 },
{ name: 'tablet', w: 380, h: 300 },
{ name: 'phone', w: 340, h: 240 },
@ -77,13 +77,13 @@ for (const chart of CHARTS) {
}
// ★ NEGATIVE CONTROL. The 0.17 default is the value that was measured FAILING on the page, so if
// this arm does not fail the calculation is not measuring what rega measured and a pass means
// this arm does not fail the calculation is not measuring what the browser measured and a pass means
// nothing. Asserted on the tightest real layout.
const poison = neededHeightPx(CENTERED_EXTENT, 0.17, 796)
ok(
'NEG CONTROL: the old 0.17 fraction still does NOT fit 796x249',
poison > 249 - 2 * FIT_PADDING,
`0.17 needs ${poison.toFixed(0)}px of ${249 - 2 * FIT_PADDING}px — the defect rega measured`,
`0.17 needs ${poison.toFixed(0)}px of ${249 - 2 * FIT_PADDING}px — the measured defect`,
)
// The two charts must share one footprint, or the same population reads as a different size when a

View file

@ -2,13 +2,13 @@
/**
* check-grid-image-loading — the people-group cards must not fetch 24 photos during boot.
*
* THE MEASUREMENT (rega, SR-LOAD on 21ebb4f26): a cold load of Simple Research downloaded 24
* THE MEASUREMENT (SR-LOAD on 21ebb4f26): a cold load of Simple Research downloaded 24
* people-group photos, 5.3 MB, DURING BOOT — competing with the map engine and the API data on the
* same connection. Nothing was wrong with any single image; there was simply no instruction about
* WHEN to fetch them, so the browser fetched them all at once.
*
* ★ WHY A GATE AND NOT JUST THE FIX. This component is being MERGED with the regions list right now
* (a settled decision, regions-plugin building it), and a merge rewrites markup. Four attributes on
* (a settled decision, built by the regions plugin), and a merge rewrites markup. Four attributes on
* one <img> are exactly the kind of thing that survives a review and not a rewrite — and losing them
* is invisible: the page still works, it just costs 5.3 MB again on every cold load.
*
@ -36,7 +36,7 @@ if (!m) {
const tag = m[0]
if (!/:loading=/.test(tag)) {
fails.push('the card image has no loading attribute, so every photo is fetched during boot — '
+ 'rega measured 24 of them, 5.3 MB, competing with the map and the API data.')
+ 'a cold load measured 24 of them, 5.3 MB, competing with the map and the API data.')
}
if (!/decoding="async"/.test(tag)) fails.push('the card image does not decode off the main thread (decoding="async").')
if (!/\bwidth=/.test(tag) || !/\bheight=/.test(tag)) {

View file

@ -24,8 +24,8 @@
* by whether the lookup it guards actually works. `msg !== 'messages.engineMissing' ? msg : 'English'`
* is treated as a fallback — but if the key is looked up with the wrong prefix, the lookup ALWAYS
* misses and the English arm becomes the NORMAL path, in every locale, while this gate stays quiet.
* That is exactly what exp-shepherd found in cities-internet after I triaged the same line as "not a
* leak". A hardcoded-English scan cannot see a misconfigured lookup; check:i18n-keys-exist and a
* That is exactly what a later review found in cities-internet after the same line had been triaged as
* "not a leak". A hardcoded-English scan cannot see a misconfigured lookup; check:i18n-keys-exist and a
* locale render are what catch that. Treat a guarded fallback as "not a leak HERE", never as "this
* string is not reaching users".
*
@ -59,7 +59,7 @@ const ALLOW = [
{ why: 'HTML fragments: these plugins build popup markup in template literals, so a quasi is often '
+ 'a tag, or the seam between two attributes ("\" alt=\"" / "\" loading=\"lazy\">")',
test: (s) => /<\/?[a-zA-Z][^>]*>?/.test(s)
// dir and lang join the list (exp, END-EXP-SR-DIRLANG): `'" dir="'` is the seam between two
// dir and lang join the list (SR-DIRLANG): `'" dir="'` is the seam between two
// attributes in a print page assembled by concatenation, and every RTL-aware page writes both.
// They are the same class as class= and href= — attribute NAMES, not text a reader sees.
|| /\b(class|href|src|alt|loading|width|height|style|id|target|rel|dir|lang)\s*=/.test(s)
@ -91,7 +91,7 @@ const ALLOW = [
test: (s) => /^[a-z-]+\($/.test(s)
|| /^[,)\s]*[a-z-]+\([^)]*\)[,)\s]*$/.test(s)
|| /^[,)\s]+$/.test(s) },
// ★ DECLARATION SEAMS, the other half of the same split (exp, I18N-EXP-PLAN). Splitting
// ★ DECLARATION SEAMS, the other half of the same split (I18N-EXP-PLAN). Splitting
// `width:${w}px;height:${h}px` leaves the quasi "px;height:" — a seam BETWEEN two declarations,
// carrying the tail of one value and the head of the next property. It has a colon but no value
// and no brace, so every CSS shape above misses it. exp measured these as false leaks in
@ -351,7 +351,7 @@ function isDataComparison(node, parent) {
// resource not found means NOT FOUND in the pools checked" is a sentence that happens to be used as
// a key, and that is a real leak — gospel found twelve defects hiding in exactly that kind of
// already-classified bucket. Anything with sentence punctuation stays loud whatever its value is.
// ★ REGION IDENTITIES, EXEMPT ONLY WHILE THEY ARE TRANSLATABLE (studio's judgement, R26).
// ★ REGION IDENTITIES, EXEMPT ONLY WHILE THEY ARE TRANSLATABLE (R26).
// legend-tree's REGION_OF maps a zone slug to a region NAME, and that name is an identity: node ids
// derive from it, and every surface that shows one translates it at render through REGION_KEY —
// upg-100-list.vue:440 `REGION_KEY[english] ? t(\`poster.${…}\`) : english`, poster-wall.vue:129 the
@ -1009,7 +1009,7 @@ const RENDERED = { africa: 'A sentence the reader actually sees here', asia: 'An
// names, values that are prose a reader sees: the keys must STILL count, because this is a
// label table, not a join table. Without this case the allData guard can be removed and every
// test still passes — measured by poisoning it.
// ★ REGION IDENTITIES (studio, R26): exempt ONLY while REGION_KEY names them. A region added
// ★ REGION IDENTITIES (R26): exempt ONLY while REGION_KEY names them. A region added
// without a key falls through `|| english` and renders English in eleven locales — the defect
// upg100 already shipped once with Americas and Europe. The conditional IS the rule.
{
@ -1068,7 +1068,7 @@ const a = greeting + 'Welcome to the prayer map'
</script>`
ok(scanSfc(CAT, 'concat.vue').some((f) => f.text === 'Welcome to the prayer map'),
'COUNTS a sentence built by CONCATENATION — only an equality test means "compared against data"')
// ★ exp's CSS DECLARATION SEAMS (I18N-EXP-PLAN), tested on the SHAPE rather than through a fixture.
// ★ CSS DECLARATION SEAMS (I18N-EXP-PLAN), tested on the SHAPE rather than through a fixture.
// A seam like "px;height:" only exists as a separate string if a quasi is judged alone, and this
// gate deliberately judges a split template literal as ONE string — so a .vue fixture can never
// produce one, and an assertion written that way passes whether or not the rule exists. I wrote
@ -1081,7 +1081,7 @@ const a = greeting + 'Welcome to the prayer map'
'prose ending in a colon is NOT — the seam shape needs a semicolon-separated declaration')
ok(!allowedBy('Pools checked; nothing found:'),
'prose with a semicolon AND a colon is NOT — the segments must be property:value pairs')
// ★ exp's HTML ATTRIBUTE SEAM (END-EXP-SR-DIRLANG). `'<html lang="' + l + '" dir="' + d + '">'`
// ★ HTML ATTRIBUTE SEAM (SR-DIRLANG). `'<html lang="' + l + '" dir="' + d + '">'`
// leaves the quasi `" dir="`, an attribute seam rather than a declaration seam. dir and lang were
// the only standard attributes missing from the HTML shape. Asserted on the shape for the same
// reason as the CSS seam: the gate judges a split literal as ONE string, so no fixture reaches it.

View file

@ -1,7 +1,7 @@
/**
* check-instance-config-keys.mjs — a preset's profile-config keys must be keys the profile READS.
*
* ★ THE DEFECT (rega RQ20): people-groups-grid's "driven" preset declares "show-search":"false" and
* ★ THE DEFECT (RQ20): people-groups-grid's "driven" preset declares "show-search":"false" and
* "show-filters":"false", and the page renders the search box and the filters bar anyway. Nothing
* errored — 0 console errors — because nothing was wrong at runtime. The key simply never reached
* the component:

View file

@ -2,7 +2,7 @@
/**
* check-leaf-engine-per-provider — a leaf loads the engine its PROVIDER asks for, and no other.
*
* THE BUG (card BUG-LEAF-PAGES-PRELOAD-MAPBOX-EVEN-UNDER-MAPLIBRE; measured by exp-shepherd at
* THE BUG (card BUG-LEAF-PAGES-PRELOAD-MAPBOX-EVEN-UNDER-MAPLIBRE; measured at
* 577149af). generate-tree chose a leaf's engine peer at BUILD time, from the instance config, and
* baked it into <head> as static tags: mapbox-gl.css, the geocoder css, mapbox-gl.js and the geocoder
* js, all from api.mapbox.com. The provider is chosen at RUNTIME (resolveMapProvider:

View file

@ -2,7 +2,7 @@
/**
* check-leaf-english-leaks — a translated key must not render in English, on ANY leaf.
*
* THE METHOD is rega's, by way of dx-shepherd, and is already proven in
* THE METHOD comes from the browser review and is already proven in
* check-gospel-english-leaks.mjs. A string is a LEAK when the catalogue has a DIFFERENT value
* for the locale and the page still shows the English one. That is a render or wiring fault,
* and it is a different thing from an untranslated string, which is a catalogue gap.
@ -145,7 +145,7 @@ for (const t of TARGETS) {
// {value:'unengaged', label:'Unengaged'}, 2 distinct values, 100% populated, and
// dashboard.config.js:132 counts 2,081 Unengaged / 27 Engaged. They are API VALUES
// that happen to equal a catalogue string.
// That is the population rega's method excludes by name ("DATA, the people-group and
// That is the population the method excludes by name ("DATA, the people-group and
// place names that come from the publishers"). Whole-label matching stops a substring
// impersonating a label; it cannot stop a DATA CELL rendering the same text. So the
// table body is excluded by ROLE, the way prose already is.

View file

@ -7,7 +7,7 @@
* fmtCount }. There is no `rows`. Two plugins independently read `legend.rows` anyway, and because
* JavaScript answers `undefined` rather than complaining, each failure wore a different costume:
*
* - countries-map: Export poster THREW (exp-shepherd's NE-17, fixed in d6916e05e)
* - countries-map: Export poster THREW (NE-17, fixed in d6916e05e)
* - research-map: provide('legendRows', legend.rows) provided undefined; the poster fallback
* `legend?.rows?.value || []` could only ever be []; and the packet's legend slot was
* `rows: legend?.rows?.value || []` with no second source, so that poster printed NO legend at

View file

@ -5,7 +5,7 @@
* WHY: the shared legend (.stl-panel, its reopen button, the detail buttons, the tree-guide hairlines) used
* `left: 8px`. MapToolbar uses `inset-inline-end`, which mirrors under dir="rtl"; a physical `left` does not.
* So in Arabic the controls moved left onto a legend that stayed left, on three production maps at once
* (Simple Research, Prayer Progress, UPG-100; rega's survey at 52eeb0db). The fix is inset-inline-start /
* (Simple Research, Prayer Progress, UPG-100; a browser survey at 52eeb0db). The fix is inset-inline-start /
* inset-inline-end; this gate keeps a physical left/right from coming back.
*
* WHAT IT READS: every CSS declaration in library/components/legends/*.vue (comments stripped). A

View file

@ -12,7 +12,7 @@
*
* ★ BUT showsFlyTools() DESCRIBES THE LEGEND TYPE, NOT WHETHER THE TOOLBAR EXISTS. Fly + Clusters are
* leaf-only: the composite — which is what a reader calls "Simple Research" — passes legendTools:false,
* so the toolbar is never drawn there and the 56px reserved a slot for nothing. rega measured it at
* so the toolbar is never drawn there and the 56px reserved a slot for nothing. A browser check measured it at
* 1440x900 on main e1d0bc20: the band from the map's top edge (y 44) to the legend card (y 100) is 56px
* of empty map on both tabs, no toolbar anywhere in the strip.
*

View file

@ -44,11 +44,11 @@ const MISSING = [
'asia-china-xinjiang-uyghur-autonomous-region',
'asia-india-bihar',
]
// asia-india-madhya-pradesh WAS here and is now FILED. upg100 hit this arm while filing it, and
// did the right thing with it: they reported the red instead of editing my expectation to make
// their change green — "editing someone else's pinned expectation to make my own change green is
// precisely the move that hollows out a guard". dx-shepherd then ruled that their data and this
// fixture land TOGETHER, which is the only ordering where neither of us is red in between.
// asia-india-madhya-pradesh WAS here and is now FILED. The filing change hit this arm, and the
// red was reported instead of the pinned expectation being edited to turn the change green: editing
// another owner's pinned expectation to make one's own change green is precisely the move that hollows
// out a guard. The data and this fixture landed TOGETHER, the only ordering where neither side is red
// in between.
ok(leavesOf(TREE[0]).map((l) => l.id).join(',') === 'z1,z2,z3', 'leavesOf walks NESTED children, not one level')
@ -85,7 +85,7 @@ const naive = TREE // what a region-grouped tree gives you, unaugmented
ok(!zoneIdsIn(naive).has('asia-china-xinjiang-uyghur-autonomous-region'),
'NEG CONTROL: the unaugmented tree really does omit xinjiang, so the bucket is not decoration')
// ── a region sweep is NOT a zone sweep (gospel, END-GOS-VLG2) ───────────────────────────────────
// ── a region sweep is NOT a zone sweep (gospel-resources) ───────────────────────────────────
// ★ The consequence of the bucket that neither of us should leave implicit: ticking every REGION
// still does not select every ZONE, because the unfiled zones have no region BY DEFINITION.
// Nothing on screen would say so — the reader just gets a smaller total, which is the exact
@ -113,7 +113,7 @@ ok(selectedId(leavesOf(EMPTYZONE[0])[0]) === 'z9', 'and it still emits its own i
ok(withUnassigned(EMPTYZONE, ['z9']).length === EMPTYZONE.length,
'presence is by ID, not by member count, so an unresolvable zone is not ALSO dumped in the bucket')
// ── the gap is REAL TODAY, not just real in my fixture (gospel, END-GOS-VLG3) ───────────────────
// ── the gap is REAL TODAY, not just real in a fixture (gospel-resources) ───────────────────
// ★ EVERY ASSERTION ABOVE RUNS AGAINST A FIXTURE. That proves the MODULE behaves, and it proves the
// fixture models a gap — it does not prove the gap still exists in the data we ship. So the negative
// control above could stay green forever while the bucket quietly became dead code, or while a FIFTH
@ -148,7 +148,7 @@ ok(realGap.join(',') === MISSING.join(','),
realGap.join(',') === MISSING.join(',')
? 'and it is exactly the four this check names, so the fixture above models the shipped data rather than a story about it'
: `THE GAP MOVED — re-derive. shipped now: [${realGap.join(', ')}] but this check asserts: [${MISSING.join(', ')}]. A zone ADDED here is groups that just became unreachable; a zone REMOVED got filed and the fixture is stale. Either way the numbers in the commit body are no longer true.`)
// ── the zones that got FILED stay filed (dx-shepherd RG12) ─────────────────────────────────────
// ── the zones that got FILED stay filed (RG12) ─────────────────────────────────────
// The gap arms watch the BAD direction. Filing is the good one, and it is exactly as revertible:
// drop a key from zone-regions and those groups leave the region tree again, silently, with the
// bucket the only thing still carrying them. Two of these three were never visible to the gap arms
@ -163,7 +163,7 @@ for (const z of FILED) {
ok(joinable.has('asia-china-xinjiang-uyghur-autonomous-region') && !filed.has('asia-china-xinjiang-uyghur-autonomous-region'),
'xinjiang specifically: joinable in admin-to-list, absent from zone-regions — its 100 groups are reachable ONLY through the bucket')
// ── the id ambiguity that is masked by a data coincidence (gospel, END-GOS-VLGC) ────────────────
// ── the id ambiguity that is masked by a data coincidence (gospel-resources) ────────────────
// ★ REGION IDS AND ZONE IDS SHARE ONE NAMESPACE. selectedId emits one bare string with no kind, and
// the two sets OVERLAP in the shipped data. gospel found it while wiring and I re-measured it here
// rather than taking the report: 33 zone ids, 7 region ids, intersection exactly {europe}, whose

View file

@ -2,7 +2,7 @@
/**
* check-map-attribution — the credit line names the basemap of the engine that actually drew the map.
*
* WHY (licence credit, rega's row 28 at ccc2b1bb): addMapAttribution always credited "Mapbox ·
* WHY (licence credit, row 28 at ccc2b1bb): addMapAttribution always credited "Mapbox ·
* © OpenStreetMap". Under MapLibre the tiles are OpenFreeMap's (OpenMapTiles on OpenStreetMap), so
* gospel-resources, key-cities and cities-internet credited Mapbox, which served nothing, and omitted
* the OpenMapTiles credit. Headless: the real module against stand-in map classes and a minimal DOM.

View file

@ -1,4 +1,4 @@
/* check-map-unmount-guard — the fast, browser-free half of the unmount-race proof (by gospel-plugin,
/* check-map-unmount-guard — the fast, browser-free half of the unmount-race proof (by the gospel-resources owner,
adopted into internal/ by the studio lane; the end-to-end half is check-map-unmount-mid-init.mjs).
Proves the guard by REPRODUCING the race against the real file: take the shipped source,

View file

@ -17,7 +17,7 @@
* (f) ProfileLoader stops mounting the notice, or the rule stops staying quiet inside the Gallery;
* (g) a flagged profile's element is mounted by neither ProfileLoader nor its own useScreenGate —
* dashboard-plus-grid defines its element without ProfileLoader, so its leaf carried
* "desktop-only" and mounted the map on a phone anyway (rega, 390px).
* "desktop-only" and mounted the map on a phone anyway (measured at 390px).
* `--self-test` plants one bad case per rule and proves each can fail before a pass is trusted.
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs'

View file

@ -2,7 +2,7 @@
/**
* check-no-credentials-omit.mjs — no data fetch in library/ or plugin-designs/ may use credentials:'omit'.
*
* WHY (dx-shepherd, 2026-09-27): behind any login gate (basic auth, a session cookie, OIDC) the Research
* WHY (2026-09-27): behind any login gate (basic auth, a session cookie, OIDC) the Research
* Gallery said "Could not load Big Picture", because doxa-maps-spa fetched its SAME-ORIGIN manifest with
* credentials:'omit', so the gate's cookie never went and the request was refused. 'same-origin' (the fetch
* default) sends credentials only to the page's own origin: a cross-origin CDN or token service still

View file

@ -0,0 +1,102 @@
#!/usr/bin/env node
/**
* check-no-fleet-names.mjs — source text names no build-team member, attributes no request to a user, and carries
* no dated QA-round tag.
*
* The tracked source of this repo is published. Comments are where provenance creeps in: who measured a thing,
* which request a choice answered, which QA round found it. The technical fact belongs in the comment; the person,
* the request and the date-stamped round do not. A sweep removed every such line; this gate keeps the count at 0.
*
* SCANS every tracked text file EXCEPT research data (data/, plugin-designs/<plugin>/data/) and build output
* (public/). The translation catalogues ARE scanned: a name can hide in a string as well as a comment, and a
* translation that ever collides with a short name is allowed by its line, not by skipping the folder. Names match
* in any case. Three classes:
* 1. a build-team member's name (the fleet's agent names);
* 2. a per-user attribution (the words "per" and "user" in a row: a request credited to a person);
* 3. a dated QA-round tag (a lowercase or uppercase QA label followed by a 20xx date).
* ALLOWED: named LINES only (ALLOW below: a file plus the exact line pattern, with the reason), never a whole file,
* so a new name added to an allowlisted file still fails. It is empty today: this gate builds its own patterns
* from parts, so it does not match itself.
* --self-test plants each class (and a clean line, and an allowed line) and must see exactly the planted ones.
*/
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
// Built from parts so this file does not match itself line by line.
const NAMES = ['dx-' + 'shepherd', 'exp-' + 'shepherd', 'go-' + 'int', 'msg-' + 'agent', 're' + 'ga']
// The plugin agents are named <plugin>-plugin; the folders and bundles never carry that suffix.
const PLUGIN_AGENTS = ['deep-research', 'prayer-progress', 'poster-design', 'country-posters', 'wall-of-people-groups',
'simpleresearch', 'regions', 'big-picture', 'bible-poster', 'upg100', 'gospel', 'cities'].map((p) => p + '-' + 'plugin')
const CLASSES = [
['a build-team member named', new RegExp('\\b(' + NAMES.concat(PLUGIN_AGENTS).join('|') + ')\\b', 'i')],
['a per-user attribution', new RegExp('\\bper ' + 'user\\b', 'i')],
['a dated QA tag', new RegExp('\\(q' + 'a:|\\bq' + 'a: ?20\\d\\d-|\\bQ' + 'A 20\\d\\d-\\d\\d')],
]
const SKIP = [/^data\//, /^public\//, /^plugin-designs\/[^/]+\/data\//, /\/node_modules\//]
const TEXT = /\.(m?js|cjs|ts|vue|html|css|json|md|sh|go|py|ya?ml|txt|svg)$|(^|\/)(Dockerfile|\.gitignore|\.gitattributes)$/
// { file, line: RegExp matching the WHOLE allowed line, why }. A detector that must name what it detects goes here,
// one line at a time. Empty today.
export const ALLOW = []
export function scanText(text, file = '', allow = ALLOW) {
const hits = []
text.split('\n').forEach((line, i) => {
if (allow.some((a) => a.file === file && a.line.test(line))) return
for (const [what, re] of CLASSES) if (re.test(line)) hits.push({ file, line: i + 1, what, text: line.trim().slice(0, 140) })
})
return hits
}
function trackedFiles() {
const out = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT, maxBuffer: 1 << 28 }).toString('utf8')
return out.split('\0').filter((f) => f && TEXT.test(f) && !SKIP.some((re) => re.test(f)))
}
if (process.argv.includes('--self-test')) {
const planted = [
['measured by ' + NAMES[4] + ' at 390', 1],
['(' + NAMES[0] + "'s ruling)", 1],
['// Per ' + 'user 2026-05-02: drop the prefix', 1],
['/* fits the band (q' + 'a: 2026-05-02 iter-9) */', 1],
['// Q' + 'A 2026-04-27: rows match desktop', 1],
['// the drawer opens from the right in ar', 0],
['// a user of the map can pick a language', 0],
['// ' + NAMES[4].toUpperCase().slice(0, 1) + NAMES[4].slice(1) + ' measured it at 390', 1],
['(' + NAMES[0].toUpperCase() + "'s call)", 1],
['// on ' + PLUGIN_AGENTS[9] + "'s argument", 1],
['// the ' + PLUGIN_AGENTS[3].replace('-' + 'plugin', '') + ' bundle', 0],
['// the irrigation (reg' + 'ar) field', 0],
]
let bad = 0
for (const [line, want] of planted) {
const got = scanText(line, 'x.js').length
const ok = got === want
console.log(` ${ok ? 'ok ' : 'BAD'} ${want ? 'caught' : 'clean '}: ${line}`)
if (!ok) bad++
}
// A name inside a translation catalogue is scanned like any other file.
const localeFile = 'library/i18n/locales/en/x.json'
if (SKIP.some((re) => re.test(localeFile)) || scanText('{ "note": "by ' + NAMES[0] + '" }', localeFile).length !== 1) { console.log(' BAD locales: a name in a catalogue was not caught'); bad++ }
// The allowlist is per LINE: an allowed line passes, a different flagged line in the SAME file still fails.
const allow = [{ file: 'd.mjs', line: /^const PATTERN = .*detects$/, why: 'self-test' }]
const allowedLine = 'const PATTERN = "' + NAMES[0] + '" // detects'
if (scanText(allowedLine, 'd.mjs', allow).length !== 0) { console.log(' BAD allowlist: an allowed line was flagged'); bad++ }
if (scanText(allowedLine + '\n// measured by ' + NAMES[4], 'd.mjs', allow).length !== 1) { console.log(' BAD allowlist: a new name in an allowlisted file passed'); bad++ }
if (scanText(allowedLine, 'other.mjs', allow).length !== 1) { console.log(' BAD allowlist: the allowed line passed in another file'); bad++ }
if (bad) { console.error(`NO-FLEET-NAMES SELF-TEST: FAIL — ${bad} line(s) judged wrong.`); process.exit(1) }
console.log(`NO-FLEET-NAMES SELF-TEST: PASS — ${planted.filter((p) => p[1]).length} planted lines caught, ${planted.filter((p) => !p[1]).length} clean lines passed, the per-line allowlist holds (3 cases), catalogues are scanned.`)
process.exit(0)
}
const files = trackedFiles()
const hits = []
for (const f of files) { let t; try { t = readFileSync(join(ROOT, f), 'utf8') } catch { continue } hits.push(...scanText(t, f)) }
if (hits.length) {
console.error(`NO-FLEET-NAMES: FAIL — ${hits.length} line(s) in ${new Set(hits.map((h) => h.file)).size} file(s). State the fact; drop the person, the request and the dated round:`)
for (const h of hits.slice(0, 40)) console.error(` ✗ ${h.file}:${h.line} (${h.what}): ${h.text}`)
process.exit(1)
}
console.log(`NO-FLEET-NAMES: PASS — ${files.length} tracked source files name no build-team member, attribute no request to a user and carry no dated QA-round tag.`)

View file

@ -1,28 +1,28 @@
#!/usr/bin/env node
/**
* check-no-new-driver-references — shipped text neither quotes nor cites the Driver, and the references
* already in it may only go away.
* check-no-internal-role-nouns — shipped text names no internal process role, and the references already
* in it may only go away.
*
* The rule: a comment, doc, UI string or commit says what the code does and why, never who said what.
* An open decision reads "provisional, pending a product decision". Measured on 2026-09-29, several
* hundred lines already referred to the Driver. Owners remove them in one sweep per area before the
* public giveaway; until then this gate holds the line FORWARD-ONLY:
* - a line that refers to the Driver and is not in the baseline FAILS (a new reference);
* An open decision reads "provisional, pending a product decision". The gate holds the line
* FORWARD-ONLY against a baseline:
* - a line that uses the role noun and is not in the baseline FAILS (a new reference);
* - a baselined line that is gone is fine (owners sweep in parallel); --shrink tidies the baseline.
*
* WHAT COUNTS: the role word as a whole word, written `Driver` or in capitals, `DRIVER` (not a GPU or
* device driver, not WebDriver, not an env name like DRIVER_MERGE). KEYED by file plus the line's
* normalised text, with a count, not by line number, so edits elsewhere in a file never trip it; a new
* line with the same text in the same file raises the count and fails.
* WHAT COUNTS: the role noun as a whole word, in title case or in capitals (not a GPU or device driver,
* not WebDriver, not an env name with the word in it). The word is assembled at runtime below, so this
* file does not itself carry it. KEYED by file plus the line's normalised text, with a count, not by
* line number, so edits elsewhere in a file never trip it; a new line with the same text in the same
* file raises the count and fails.
*
* ⚠ THE CAPITALS FORM WAS MISSING AT FIRST. The first pattern matched only `Driver`, so 42 lines written
* `DRIVER: "…"`, `THE DRIVER'S …` or `DRIVER-REPORTED` passed unseen, and a sweep measured by this gate
* read as finished while they stayed. The baseline was re-measured once with the wider pattern, the one
* time it has grown; it only shrinks from here.
* ⚠ THE CAPITALS FORM WAS MISSING AT FIRST. The first pattern matched only the title-case form, so lines
* written in capitals passed unseen, and a sweep measured by this gate read as finished while they
* stayed. The baseline was re-measured once with the wider pattern, the one time it has grown; it only
* shrinks from here.
*
* node internal/check-no-new-driver-references.mjs check
* node internal/check-no-new-driver-references.mjs --shrink drop what is gone (never adds)
* node internal/check-no-new-driver-references.mjs --init write the baseline, only if none exists
* node internal/check-no-internal-role-nouns.mjs check
* node internal/check-no-internal-role-nouns.mjs --shrink drop what is gone (never adds)
* node internal/check-no-internal-role-nouns.mjs --init write the baseline, only if none exists
*
* Node only. Exit 0 pass · 1 fail.
*/
@ -32,15 +32,18 @@ import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SELF = 'internal/check-no-new-driver-references.mjs'
const SELF = 'internal/check-no-internal-role-nouns.mjs'
// Files whose JOB is to name the word: a checker's pattern list is not a citation. Exempt by name, each with its
// reason, so no rule wider than one file is ever needed. Anything else in them is still that file's owner's to keep clean.
const PATTERN_FILES = new Set([
'internal/public-hygiene-check.mjs', // its egress patterns match the role word, so its pattern list must spell it
'internal/public-hygiene-check.mjs', // its egress patterns match the role noun
])
const BASELINE_REL = 'internal/fixtures/driver-references-baseline.json'
const BASELINE_REL = 'internal/fixtures/internal-role-nouns-baseline.json'
const BASELINE = join(ROOT, BASELINE_REL)
const ROLE = /\b(?:Driver|DRIVER)\b/
// The role noun, assembled from parts so this file does not carry it; title case and capitals.
const W = ['Dri', 'ver'].join('')
const WU = W.toUpperCase()
const ROLE = new RegExp(`\\b(?:${W}|${WU})\\b`)
const AREAS = ['library', 'plugin-designs', 'internal', 'documentation', 'skills', 'README.md']
export function referencesIn(text) {
@ -72,14 +75,14 @@ function tally(files, read) {
return { counts, where }
}
// ── self-test: the role word is found, a device driver is not, and a new line beside a baselined one is isolated
// ── self-test: the role noun is found, a device driver is not, and a new line beside a baselined one is isolated
{
const found = referencesIn('// the Driver ruled that numerals stay en-US\n// THE DRIVER\'S ASK\n// the GPU driver and WebDriver and DRIVER_MERGE\n')
if (found.length !== 2) { console.error('DRIVER-REFS: SELF-TEST FAILED — the role word (either case) is not told apart from other drivers'); process.exit(1) }
const { counts } = tally(['a.vue'], () => '// Driver: old\n// Driver: old\n// Driver: new\n')
const base = new Map([[keyOf('a.vue', '// Driver: old'), 2]])
const found = referencesIn(`// the ${W} ruled that numerals stay en-US\n// THE ${WU}'S ASK\n// the GPU driver and WebDriver and ${WU}_MERGE\n`)
if (found.length !== 2) { console.error('ROLE-NOUNS: SELF-TEST FAILED — the role noun (either case) is not told apart from other drivers'); process.exit(1) }
const { counts } = tally(['a.vue'], () => `// ${W}: old\n// ${W}: old\n// ${W}: new\n`)
const base = new Map([[keyOf('a.vue', `// ${W}: old`), 2]])
const fresh = [...counts].filter(([k, n]) => n > (base.get(k) || 0))
if (fresh.length !== 1 || !fresh[0][0].endsWith('new')) { console.error('DRIVER-REFS: SELF-TEST FAILED — a new reference beside baselined ones was not isolated'); process.exit(1) }
if (fresh.length !== 1 || !fresh[0][0].endsWith('new')) { console.error('ROLE-NOUNS: SELF-TEST FAILED — a new reference beside baselined ones was not isolated'); process.exit(1) }
}
const { counts, where } = tally(shippedFiles(), (f) => readFileSync(join(ROOT, f), 'utf8'))
@ -87,9 +90,9 @@ const args = process.argv.slice(2)
const toObj = (m) => Object.fromEntries([...m].sort(([a], [b]) => (a < b ? -1 : 1)))
if (args.includes('--init')) {
if (existsSync(BASELINE)) { console.error(`DRIVER-REFS: --init refused, ${BASELINE_REL} exists (use --shrink)`); process.exit(1) }
writeFileSync(BASELINE, JSON.stringify({ about: 'Lines in shipped text that referred to the Driver on 2026-09-29 (re-measured that day when the pattern learned the capitals form, DRIVER), keyed by file and text, to be removed by the owners\' sweep before the public giveaway. Forward-only: this list may only shrink (--shrink).', counts: toObj(counts) }, null, 1) + '\n')
console.log(`DRIVER-REFS: baseline written: ${counts.size} line(s) in ${new Set([...where.values()].map((w) => w.file)).size} file(s)`)
if (existsSync(BASELINE)) { console.error(`ROLE-NOUNS: --init refused, ${BASELINE_REL} exists (use --shrink)`); process.exit(1) }
writeFileSync(BASELINE, JSON.stringify({ about: 'Lines in shipped text that used the internal role noun when this gate began, keyed by file and text. Owners remove them; the list only shrinks (--shrink).', counts: toObj(counts) }, null, 1) + '\n')
console.log(`ROLE-NOUNS: baseline written: ${counts.size} line(s) in ${new Set([...where.values()].map((w) => w.file)).size} file(s)`)
process.exit(0)
}
@ -100,17 +103,17 @@ if (args.includes('--shrink')) {
const kept = new Map()
for (const [k, n] of base) { const now = counts.get(k) || 0; if (now > 0) kept.set(k, Math.min(n, now)) }
writeFileSync(BASELINE, JSON.stringify({ ...baseline, counts: toObj(kept) }, null, 1) + '\n')
console.log(`DRIVER-REFS: baseline shrunk ${base.size} -> ${kept.size} line(s) (it never grows)`)
console.log(`ROLE-NOUNS: baseline shrunk ${base.size} -> ${kept.size} line(s) (it never grows)`)
process.exit(0)
}
const fresh = [...counts].filter(([k, n]) => n > (base.get(k) || 0)).map(([k]) => where.get(k))
const gone = [...base.keys()].filter((k) => !counts.has(k)).length
if (fresh.length) {
console.error(`DRIVER-REFS: FAIL — ${fresh.length} new reference(s) to the Driver in shipped text:`)
console.error(`ROLE-NOUNS: FAIL — ${fresh.length} new use(s) of the internal role noun in shipped text:`)
for (const w of fresh.slice(0, 20)) console.error(` ✗ ${w.file}:${w.line} ${w.text.slice(0, 120)}`)
console.error(' Keep the engineering reason and drop the speaker; an open decision reads "provisional, pending a product decision".')
process.exit(1)
}
console.log(`DRIVER-REFS: PASS — no new reference to the Driver; ${base.size - gone} older line(s) still baselined for the sweep`
console.log(`ROLE-NOUNS: PASS — no new use of the internal role noun; ${base.size - gone} older line(s) still baselined for the sweep`
+ (gone ? `, ${gone} already swept (tidy with --shrink)` : '') + '.')

View file

@ -11,12 +11,12 @@
* a listening socket produces no output and no failure — the gate it served passed, and the process
* outlived it.
*
* ★ WHY THIS IS THE INTERESTING CLASS. The standing rule "keep the box closed" lived in prose, in
* ★ WHY THIS IS THE INTERESTING CLASS. The standing rule "keep the build machine closed" lived in prose, in
* instructions to an agent. The actual behaviour lived in a FLAG in package.json. Prose cannot
* constrain a flag. The rule was never violated by a decision — it was violated by a default that
* nobody re-read, and it stayed violated because nothing ever asked the question out loud. It was
* stated plainly: this box needs no open ports, because changes are pushed to Forgejo and pulled
* onto a laptop; Playwright is for internal testing, not for public access.
* stated plainly: a build machine needs no open ports, because changes are pushed to the git remote
* and pulled onto a laptop; Playwright is for internal testing, not for public access.
*
* Loopback is sufficient for the entire test suite: verify-browser-all, check:a11y-button-names,
* check:nav-reachable and smoke:share-all all already default to 127.0.0.1. The wide bind bought
@ -81,7 +81,7 @@ if (errs.length) {
console.error('NO-PUBLIC-BINDS: FAIL — a repo script would open a port to the network.\n')
errs.forEach((e) => console.error(e))
console.error('\n The whole test suite reaches its server over 127.0.0.1. Nothing here needs a public')
console.error(' bind: code moves between machines through Forgejo, not through an open port. Use')
console.error(' bind: code moves between machines through the git remote, not through an open port. Use')
console.error(' --bind 127.0.0.1, and pass BASE_URL/BASE if some caller genuinely needs another host.')
process.exit(1)
}

View file

@ -3,7 +3,7 @@
* check-no-silent-engine-substitution — a profile must never fall back to a DIFFERENT map engine than
* the one its provider asked for.
*
* THE DEFECT, found by regions' C3 read and routed by dx-shepherd (END-SHEP-C3C, 2026-09-26):
* THE DEFECT, found by the C3 review (2026-09-26):
*
* const gl = engineGlobal(provider) || window.mapboxgl
*
@ -72,7 +72,7 @@ function scan(text) {
// documented what they removed in a /* */ block whose second line begins with a BACKTICK:
// `engineGlobal(grProvider) || window.mapboxgl`. engineGlobal already defaults to mapbox, so
// The prefix test saw no comment marker, matched the quoted pattern, and reported gospel as the last
// remaining offender at main 9df086e5. I relayed that to dx-shepherd as fact and it was wrong: the
// remaining offender at main 9df086e5. That was relayed as fact and it was wrong: the
// line number had also moved three times (1490 -> 1505 -> 1509) purely because their file grew, which
// is what made a stale card look like a live defect.
// ★ A GATE THAT FAILS ON THE EXPLANATION OF A FIX IS WORSE THAN NO GATE. It tells the one person who

View file

@ -21,7 +21,7 @@
* (2) is the half that matters over time: a fixed shell plus a plugin that adds .foo{min-height:700px}
* next month is the same bug again, and nobody would connect it to this.
*
* Source-level and headless BY DESIGN. Only rega runs a browser here, and the browser proof is rega's
* Source-level and headless BY DESIGN. This gate runs no browser, and the browser proof is a separate
* scroll-container count. This is the cheap guard that keeps the class from coming back between runs.
* ★ THIS GATE IS RED ON PURPOSE AT LANDING, and is NOT wired into gates:pre yet. gospel-internet's
* .gb still carries min-height:640px; that fix belongs to its owner, not to me, and wiring a red gate
@ -103,7 +103,7 @@ for (const bundle of readdirSync(DESIGNS)) {
//
// ★ THIS IS GATED BECAUSE IT WAS ALREADY PORTED ONCE AND ARRIVED SHORT. doxa-maps-spa's .dms-pane was
// copied from VerticalTabsLayout's .vtl-host -- its own comment says "porting the same proven shape" --
// and took the first two lines. rega measured the result: 12 of 16 gallery tabs overflowing by exactly
// and took the first two lines. A browser check measured the result: 12 of 16 gallery tabs overflowing by exactly
// the DX-life header height, on plugins with no floor of their own. Two thirds of a fix reads as a
// whole one in review, which is precisely why a reviewer cannot be the check.
for (const [file, label] of [

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
/* check-optional-t-equivalence.mjs — `bun run check:optional-t-equivalence`. Source-only, headless.
EVERY LATER C4 WAVE EXTENDS THIS FILE (dx-shepherd, REGC4G): a wave that migrates a site without
EVERY LATER C4 WAVE EXTENDS THIS FILE (REGC4G): a wave that migrates a site without
adding its row here is a wave whose behaviour nobody re-checks. Sections 1-6 are the helper's own
contract; section 7 is the per-site register. */
// C4: does useOptionalT() behave like the twenty hand-written guarded translators it replaces?
@ -122,11 +122,11 @@ const SITES = [
['library/components/hints/PinHint.vue', 'useOptionalI18n', 'PINHINT_FALLBACK[key]'],
['plugin-designs/pplr-data-maps/src/composables/usePplrT.js', 'useOptionalT', null],
['plugin-designs/upg-neighbor/src/composables/useNeighborT.js', 'useOptionalT', null],
// wave 3a — gospel-resources (owner: gospel-plugin). All four fell back to enLookup, i.e. the
// wave 3a — gospel-resources. All four fell back to enLookup, i.e. the
// SHIPPED ENGLISH, not the locale-aware catalogue; each passes that enLookup in, so a bare host
// still renders English. Switching them to the catalogue default would make a bare host honour
// ?lang=ar — a real improvement, and gospel-plugin's to make, not this migration's.
// ★ gospel-plugin CHOSE the locale-honouring catalogue fallback (2026-09-26) rather than keeping
// ?lang=ar — a real improvement, and its owner's to make, not this migration's.
// ★ gospel-resources' owner CHOSE the locale-honouring catalogue fallback (2026-09-26) rather than keeping
// enLookup's English: "most partners will drop a custom element or an iframe and never install
// vue-i18n ... an Arabic ministry site embedding this map serves its readers English while the
// bundle already carries all 11 catalogues. That is a bug, not a decision." So these four take NO
@ -143,13 +143,13 @@ const SITES = [
// wave 3b — cities-internet. All three were byte-identical in the guard block; each keeps its own
// enLookup fallback, so a bare host renders the same English. Their local fill() used `k in params`,
// so the shared fillParams also removes a way for them to print the literal "null".
// ★ exp-shepherd, as cities-internet's owner, took the locale-honouring default (2026-09-26): "the
// bundle already carries all 11 catalogues, so a bare host getting English would be a bug". So these
// ★ cities-internet's owner took the locale-honouring default (2026-09-26): the bundle already
// carries all 11 catalogues, so a bare host getting English would be a bug. So these
// three pass NO fallback, and their English-only enLookup + per-area catalogue import are deleted.
['plugin-designs/cities-internet/connectivity-list.vue', 'useOptionalT', null],
['plugin-designs/cities-internet/connectivity-map.vue', 'useOptionalT', null],
['plugin-designs/cities-internet/data-tables.vue', 'useOptionalT', null],
// wave 3d — all-people-groups. Treated as UNOWNED (studio agreed), so the conservative reading wins:
// wave 3d — all-people-groups. Treated as UNOWNED, so the conservative reading wins:
// its inline EN map stays the fallback rather than switching to the catalogue, because nobody is
// available to decide that this bundle should start honouring the locale on a bare host -- THEN
// the locale-honouring fallback was RULED for everywhere (2026-09-27; the goal: a bare-host
@ -162,11 +162,11 @@ const SITES = [
// the inline EN map. Taking a SITES array wholesale from another branch re-asserts ITS view of every
// file in it, including the ones you changed; the gate caught it, which is the only reason it is right.
['plugin-designs/all-people-groups/all-people-groups.vue', 'useOptionalI18n', null],
// wave 3c — my-upg-100-list (owner: upg100-plugin). No namespace, since callers pass full keys.
// wave 3c — my-upg-100-list. No namespace, since callers pass full keys.
// ⚠ THE THIRD COLUMN IS null, NOT 'humanizeKey(k)', and that is a DEPARTURE from 4c05e589, which
// kept the fallback. Correct as regions-plugin wrote it; wrong the moment dx-shepherd took the
// kept the fallback. Correct as first written; wrong the moment the owner took the
// product call (UPGHK, 2026-09-27) to delete it. A row asserting a fallback the code no longer
// passes fails by design — the same hole regions flagged to me in the opposite direction.
// passes fails by design — the same hole flagged earlier in the opposite direction.
// ★ WHY IT WAS DELETED. humanizeKey rendered a key's last segment title-cased, failing two ways: on
// the messages keys it printed "Upg100 Wall Intro" where a whole sentence exists (wrong even in
// English), and on the tabs keys it printed the RIGHT English by coincidence — those keys are named
@ -175,7 +175,7 @@ const SITES = [
// someone changes locale.
// The premise for keeping it ("these keys are not fully catalogued") was MINE and was FALSE: seven
// keys across both files, all string literals, every one present in all eleven locales, 0 gaps.
// Same class of decision gospel-plugin took for its four sites above, and the OPPOSITE of the 3d
// Same class of decision gospel-resources' owner took for its four sites above, and the OPPOSITE of the 3d
// reading directly above — which is right, because that file is unowned and these two are not.
['plugin-designs/my-upg-100-list/poster-wall.vue', 'useOptionalT', null],
['plugin-designs/my-upg-100-list/upg-100-mfe.vue', 'useOptionalT', null],
@ -188,11 +188,11 @@ const SITES = [
// third bare call — a new unguarded translator — still fails this gate.
// ★ DELIBERATELY NOT IN THIS REGISTER, so nobody "completes" C4 by converting it:
// plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue
// It still hand-rolls the guard, and exp-shepherd asked for it to stay that way: the file is carded for
// It still hand-rolls the guard, and its owner asked for it to stay that way: the file is carded for
// retirement (RETIRE-PPLR-SEMANTICTREELEGEND) in favour of the library component, so converting it is
// work that gets thrown away. A migration whose value expires before it lands is not worth the diff.
// Two more sites are their owners' to cut, not absent by oversight: country-posters-mfe.vue
// (exp-shepherd, into its single posters landing) and key-cities/cities-map.vue (exp-shepherd, gated).
// (into its single posters landing) and key-cities/cities-map.vue (gated).
const ALLOWED_BARE = { 'plugin-designs/gospel-resources/resources-map.vue': 2 }
for (const [file, helper, fallbackFrag] of SITES) {
@ -221,13 +221,13 @@ for (const [file, helper, fallbackFrag] of SITES) {
// Which sites were MOVED to the catalogue by a decision, vs which always resolved there. Saying "as it
// always did" about a moved file is a false statement inside a gate message, the one place a reader has
// no reason to doubt. gospel-resources and cities-internet moved by their owners' decisions,
// my-upg-100-list by dx-shepherd's product call (UPGHK) deleting humanizeKey, and all-people-groups by
// my-upg-100-list by a product call (UPGHK) deleting humanizeKey, and all-people-groups by
// the embed-contract ruling (locale-honouring everywhere, 2026-09-27).
const MOVED_BY = {
'gospel-resources': "gospel-plugin moved it here from enLookup's English",
'cities-internet': "exp-shepherd moved it here from enLookup's English",
'my-upg-100-list': 'upg100-plugin moved it here from humanizeKey, a title-cased key segment',
'all-people-groups': "dx-shepherd's embed-contract ruling moved it here from its inline English map",
'gospel-resources': "its owner moved it here from enLookup's English",
'cities-internet': "its owner moved it here from enLookup's English",
'my-upg-100-list': 'its owner moved it here from humanizeKey, a title-cased key segment',
'all-people-groups': "the embed contract moved it here from its inline English map",
}
const mover = Object.keys(MOVED_BY).find((d) => file.includes(d))
ok(!/fallback:/.test(code), mover
@ -236,7 +236,7 @@ for (const [file, helper, fallbackFrag] of SITES) {
}
}
// ── 7b. gospel-plugin's ASKED-FOR ROW, against the REAL catalogue ───────────────────────────────
// ── 7b. gospel-resources' ASKED-FOR ROW, against the REAL catalogue ───────────────────────────────
// Their words: "a BARE host (no vue-i18n) with ?lang=ar must render pins.approximate in Arabic — not
// the key, not gospel.pins.approximate, not English." It covers their conditions 1 and 2 together: a
// namespace would corrupt the key, and an EN-only fallback would serve English to an Arabic reader.

View file

@ -5,7 +5,7 @@
* WHY: JSON.parse keeps the LAST of two equal keys and says nothing, so a duplicate silently kills the
* first entry. "check:religion-label-not-value" sat twice in //scripts, one description dead, while
* check:doc-scripts passed (it reads the parsed object, where only one survives). It was a merge artifact:
* the landing tooling unions both sides' package.json additions. Found by regions-plugin; only Bun's parser
* the landing tooling unions both sides' package.json additions. Found during a landing; only Bun's parser
* warned, while running something unrelated.
*
* HOW: a small JSON scanner that tracks every object's keys by position, so it sees what JSON.parse

View file

@ -129,7 +129,7 @@ ok(Math.round(per / 100) * 100 === 40200,
// it: 32 contiguous cells fill most of row 1 of 52, so the page printed as one solid bar across the
// header — the ordinary chart the life grid exists to avoid — and the existing gate asserts only
// that the shaded COUNT is between 0 and 5,200, which is equally true of a bar and of a scatter.
// A count cannot distinguish a distribution. rega's eyes caught it; nothing else could have.
// A count cannot distinguish a distribution. A visual review caught it; nothing else could have.
const picked = scatterCells(5200, 32, 'upg-100')
const spread = scatterSpread(picked, 52)
ok(spread.cells === 32, '32 shaded cells are picked, distinct')

View file

@ -33,7 +33,7 @@ import { pathToFileURL, fileURLToPath } from 'node:url'
import { requirePathInThisCheckout } from './_serves-this-tree.mjs'
// ★ --expect-funnel TURNS AN OPTIONAL PAGE INTO A DECLARED REQUIREMENT, and it exists because of a
// real near-miss (regions-plugin, 2026-09-26, reported unprompted). A merge resolved with
// real near-miss (2026-09-26, reported unprompted). A merge resolved with
// `git checkout --ours` silently deleted the printPacket call site that supplies the funnel data —
// 60 lines — AND EVERY GATE STAYED GREEN. It had to: the funnel page is optional by design
// (buildPacket omits it when the arg is null), so a packet with no page 2 is not malformed, just
@ -274,8 +274,8 @@ const r = await page.evaluate(() => {
})(),
funnelEq: document.querySelectorAll('.funnel-eq').length,
funnelSteps: document.querySelectorAll('.funnel-mine').length,
// ★ MEASURED SO THE GATE CAN REPORT WHAT IT ASSERTED. rega: "the gate prints no funnel line, even
// with --expect-funnel ... a reader cannot see WHAT it asserted about page 2." A silent pass is
// ★ MEASURED SO THE GATE CAN REPORT WHAT IT ASSERTED. A review found the gate printed no funnel line, even
// with --expect-funnel, so a reader could not see WHAT it asserted about page 2. A silent pass is
// the mirror of a printed-but-unasserted number: the check is real and the evidence is invisible.
// ⚠ ZERO POPULATION IS COUNTED ON PURPOSE. The reader's row printed "0 people" in every packet
// (Number(null) is 0, so the finite-number guard admitted a deliberate no-data marker). Fixed in
@ -302,10 +302,10 @@ const r = await page.evaluate(() => {
.filter((d) => /^0(\.0+)?\s+\D/.test(d.textContent.trim())).length
})(),
funnelShare: (document.querySelector('.funnel-share')?.textContent || '').trim().slice(0, 80),
// ★ LATIN RUNS ON A NON-LATIN SHEET, which is the one gap rega named and I left open: "the gate
// passed: it counts rows, not the row's language." It asserted the funnel page's STRUCTURE and
// nothing about whether its prose reached the reader's language — my own standing rule turned on
// my own gate.
// ★ LATIN RUNS ON A NON-LATIN SHEET, the one gap a review named and that was left open: the gate
// passed because it counted rows, not the rows' language. It asserted the funnel page's STRUCTURE
// and nothing about whether its prose reached the reader's language, the standing rule turned on
// this gate itself.
//
// ⚠ IT ONLY WORKS IN A NON-LATIN-SCRIPT LOCALE, and that is why it is worth having rather than
// aspiring to a general "is this translated?" test, which is not decidable. On a hi/ar/ru/zh sheet
@ -749,7 +749,7 @@ if (r.lifeSheets) {
}
}
// ⚠ AND THE SHADE MUST BE SCATTERED, NOT STACKED — the assertion that would have caught what three gates
// and a passing suite did not. rega's contact sheet (2026-09-28) showed page 3 reading as ONE SOLID BAR:
// and a passing suite did not. A contact sheet (2026-09-28) showed page 3 reading as ONE SOLID BAR:
// 32 contiguous cells fill most of row 1 of 52, so the page said "a big green block" where the
// intent is 32 boxes lost in 5,200. Every assertion passed, because they all measured the COUNT.
// Half the grid height is a deliberately loose floor: a genuine scatter of even a few cells spans most of
@ -793,7 +793,7 @@ if (r.funnel && !r.funnelSteps) {
// ★ AND IF THE CALLER DECLARED IT, ITS ABSENCE IS A FAILURE. See the --expect-funnel note at the top:
// a lost call site produces a packet that is CORRECT but missing page 2, which is why nothing caught
// a merge deleting one.
// ★ EVERY FUNNEL ROW MUST HAVE A BAR. rega measured the first version: the reader's step had no bar,
// ★ EVERY FUNNEL ROW MUST HAVE A BAR. A browser check measured the first version: the reader's step had no bar,
// so the funnel never visibly narrowed to "yours" — a funnel whose final term is not drawn as part of
// the funnel fails at the one thing it is for. Rows and bars are counted separately so the next
// regression is a mismatch rather than an invisible omission.
@ -805,8 +805,8 @@ if (r.funnel && r.funnelRows !== r.funnelBars) {
// because Number(null) is 0 and the renderer's guard tested finiteness rather than PRESENCE. A people
// group with no people is not a fact; it is missing data wearing a number's clothes.
// ★ AND ON A NON-LATIN SHEET, THE FUNNEL'S PROSE MUST BE IN THE READER'S SCRIPT. See funnelLatinRuns
// above for why this is only meaningful in hi/ar/ru/zh. This is the gap rega named after the ?list=
// render: "the gate passed: it counts rows, not the row's language", and it was right — the English
// above for why this is only meaningful in hi/ar/ru/zh. This is the gap a review named after the ?list=
// render (the gate passed because it counted rows, not the rows' language), and it was right — the English
// list label sat among Devanagari and every assertion here was green.
//
// ⚠ THE ALLOWLIST IS TWO ENTRIES AND BOTH ARE DECISIONS, NOT BOOKMARKS:

View file

@ -189,7 +189,7 @@ ok(count(light, /sheet-sets-(bounded|centered)/g) === 0, 'lightweight still carr
// "rows close on the bottom margin" one already deleted from check-profile-table. A printed sheet
// is simply much taller than gospel's constrained pane, which is why their bug is not ours.
// The real protection is the empty-ring filter asserted above (ten rows is the ceiling) plus
// rega's rendered check; a headless budget with 790px of slack would be decoration.
// the rendered browser check; a headless budget with 790px of slack would be decoration.
}
// An unknown variant must print the FULL packet. A typo that printed a SHORTER packet would look
@ -204,7 +204,7 @@ ok(/printPacketAs[\s\S]{0,600}prints\.printPacket\(/.test(WALL),
'printPacketAs does not call prints.printPacket — the Prayer List caret would print the columnar LIST again, not the packet')
ok(/variant/.test(PRINTS.match(/async function printPacket\([^)]*\)/)?.[0] || ''),
'usePosterPrints.printPacket does not accept a variant — the caret\'s choice would be silently dropped')
// printList itself STAYS (dx-shepherd: don't delete it, just stop the button calling it).
// printList itself STAYS (by decision: keep it, just stop the button calling it).
ok(/function printList\(/.test(PRINTS) && /printList,/.test(PRINTS),
'printList was removed — it must remain exported for any surface that still wants the columnar list')

View file

@ -132,7 +132,7 @@ const gate = join(HERE, 'check-packet-render.mjs')
// ★ THE FUNNEL PAGE IS REQUIRED HERE, and this line is the half of the fix that was missing.
// check-packet-render gained --expect-funnel after a merge deleted the call site supplying the funnel
// data and every gate stayed green (regions-plugin, 2026-09-26). But a DECLARABLE expectation that
// data and every gate stayed green (2026-09-26). But a DECLARABLE expectation that
// nothing declares is not a closed hole — it is an invisible defect traded for a visible obligation.
// I said as much to regions and then owed the second half.
//

View file

@ -3,7 +3,7 @@
* check-pane-config-reforward — a composite's panes keep following the outer config for the whole
* session, and a locale change never rewrites a filter into a display name.
*
* ★ THE BUG THIS GUARDS, measured by rega on development 92ca8b6e6 across FOUR composites
* ★ THE BUG THIS GUARDS, measured on development 92ca8b6e6 across FOUR composites
* (regions-mfe, upg-100-mfe, country-posters-mfe, research-map-plus-grid). The outer element's
* profile-config reached locale 'ar' by +1.5 s — attribute AND property, same node — while every
* NESTED host carried NO locale key at boot and still none at +15 s. Text per composite at +15 s:
@ -15,7 +15,7 @@
* ★ IT RUNS THE SHIPPED HELPER WITH REAL VUE. usePaneConfig is imported, not re-typed, and driven
* by real refs through a real watchEffect — the dependency-tracking hole this gate's case 3 covers
* (a plain array leaves a LATER entry's getter untracked) is invisible to any test that fakes
* reactivity, and it was found by studio only because their test used real Vue.
* reactivity, and it was found only because a test used real Vue.
*
* ★ AND IT ASSERTS THE DOM, NOT THE RETURN VALUE. Every case reads the attribute back off the
* element, because "the helper decided to write" and "the pane was told" are different claims and
@ -76,7 +76,7 @@ const cfgOf = (el) => {
paneConfig.dispose()
}
// ── 1b. NO LOCALE KEY AT BOOT IS THE REAL BEFORE-STATE, not a stale one. rega measured the nested
// ── 1b. NO LOCALE KEY AT BOOT IS THE REAL BEFORE-STATE, not a stale one. A browser check measured the nested
// hosts carrying no locale key at all, so the first re-forward is absent -> 'ar'.
{
const outer = ref({ apiBaseUrl: '/api' })
@ -225,7 +225,7 @@ const cfgOf = (el) => {
// gridLanguage() falls back to `linked.language.value || linked.language.label`, and `label` is a
// DISPLAY NAME. The grid's valueMatches() compares whole normalised values, so a name fetched in
// one language matches no row in another — the failure that took the grid 2,107 -> 0 and read as
// "no results" (rega, 2026-09-26). This asserts the CLASS, not today's instance.
// "no results" (measured 2026-09-26). This asserts the CLASS, not today's instance.
{
const locale = ref('en')
// A language whose display label follows the locale, as the API returns it, but whose code does not.

View file

@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* check-pin-hint-floor.mjs — the pin-hint pill is placed at the map's bottom on a page that scrolls, and
* still kept above the fold on a full-screen app page.
*
* The defect it guards: PinHint's clamp floored the pill at the viewport fold on EVERY page. On a partner
* home page, where the map is a block in a scrolling page, a map whose bottom was below the fold at load
* had its pill lifted to mid-map, re-set by the settle timers, and left there after a scroll.
*
* ASSERTS (headless, on library/components/hints/pinHintFloor.js and PinHint.vue's source):
* 1. scrolling page, map bottom below the fold → no lift (the pill stays at the map's bottom); also a map
* ENTIRELY below the fold, at load, with its bottom in view and scrolled past → no lift at any scroll;
* 2. full-screen page, pill under the fold → lifted above it (today's behaviour kept);
* 3. the legend sheet and the licence credit still lift the pill on a scrolling page;
* 4. pageScrolls() tells a longer page from a full-screen one, and fails safe (false) on bad input;
* 4b. an INNER scroller around the map (html/body at 100%) counts as a scrolling page, across shadow roots;
* a scroller whose content fits does not;
* 5. PinHint's clampIntoView takes its floor from pillFloor(), not from window.innerHeight, treats a map wholly
* above or below the viewport as on a scrolling page, and drops a stale lift when the pill goes off screen.
* --self-test plants each defect (the old fold-everywhere floor, a pageScrolls that never says yes, the
* old source line) and must see each one refused.
*/
import { readFileSync } from 'node:fs'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { dirname, join } from 'node:path'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const F = await import(pathToFileURL(join(ROOT, 'library/components/hints/pinHintFloor.js')).href)
const SRC = readFileSync(join(ROOT, 'library/components/hints/PinHint.vue'), 'utf8')
// The clamp's arithmetic, as PinHint.vue does it: lift = overflow + 8 when the pill's bottom passes the floor.
const liftFor = (pillBottom, floor) => { const o = Math.round(pillBottom - floor); return o > 0 ? o + 8 : 0 }
const doc = (scrollHeight) => ({ scrollingElement: { scrollHeight } })
function audit({ pillFloor, pageScrolls }, src) {
const bad = []
// 1. DX Life's measured case at 1440: fold 900, map 450..1230, the pill's bottom 12px above the map's.
const scrolls = pageScrolls(doc(3200), { innerHeight: 900 })
const l1 = liftFor(1218, pillFloor({ viewportH: 900, scrolls }))
if (l1 !== 0) bad.push(`a scrolling page lifts the pill ${l1}px off the map's bottom (the fold is not an obstacle there)`)
// 1b. DX Life /pray at 1440: the map ENTIRELY below the fold (map 1103..1867, fold 900). The pill's bottom
// (12px above the map's) must get NO lift from the fold at scrollY 0, with the map's bottom in view, or past it.
for (const [scrollY, label] of [[0, 'at load'], [967, 'with the map bottom in view'], [2067, 'scrolled past']]) {
const l = liftFor(1855 - scrollY, pillFloor({ viewportH: 900, scrolls }))
if (l !== 0) bad.push(`a map below the fold lifts the pill ${l}px ${label} (scrollY ${scrollY})`)
}
// 2. A full-screen app page at 390×844: the pill 16px under the fold is lifted clear of it.
const s2 = pageScrolls(doc(844), { innerHeight: 844 })
const l2 = liftFor(860, pillFloor({ viewportH: 844, scrolls: s2 }))
if (l2 !== 24) bad.push(`a full-screen page lifts the pill ${l2}px, expected 24 (16 over the fold + 8)`)
// 3. Obstacles still count on a scrolling page.
const l3 = liftFor(1218, pillFloor({ viewportH: 900, scrolls: true, sheetTop: 1150 }))
if (l3 !== 76) bad.push(`the legend sheet lifts the pill ${l3}px on a scrolling page, expected 76`)
const l3b = liftFor(1218, pillFloor({ viewportH: 900, scrolls: true, creditTop: 1200 }))
if (l3b !== 26) bad.push(`the licence credit lifts the pill ${l3b}px on a scrolling page, expected 26`)
// 4b. An INNER scroller: the document does not scroll (html/body at 100%), a container around the map does.
const node = (oy, sh, ch, parent = null, host = null) => { const n = { nodeType: 1, oy, scrollHeight: sh, clientHeight: ch, parentElement: parent }; n.getRootNode = () => (host ? { host } : n); return n }
const winStyle = { innerHeight: 900, getComputedStyle: (n) => ({ overflowY: n.oy }) }
const scroller = node('auto', 3200, 900)
const mapBox = node('hidden', 764, 764, scroller)
const pillEl = node('visible', 19, 19, mapBox)
if (pageScrolls(doc(900), winStyle, pillEl) !== true) bad.push('an inner scroller around the map is not seen as a scrolling page')
const fits = node('auto', 900, 900); const pill2 = node('visible', 19, 19, node('hidden', 900, 900, fits))
if (pageScrolls(doc(900), winStyle, pill2) !== false) bad.push('a scroller whose content fits is taken for a scrolling page')
const outerScroller = node('scroll', 2400, 900)
const shadowHost = node('visible', 900, 900, outerScroller)
const inShadow = node('visible', 19, 19, null, shadowHost)
if (pageScrolls(doc(900), winStyle, inShadow) !== true) bad.push('the walk does not cross a shadow root to the scroller outside it')
// The inner-scroller page at 1440 with the map below the fold: lift 0 at every scroll.
const s4 = pageScrolls(doc(900), winStyle, pillEl)
for (const y of [0, 967, 2067]) { const l = liftFor(1855 - y, pillFloor({ viewportH: 900, scrolls: s4 })); if (l !== 0) bad.push(`an inner-scroller page lifts the pill ${l}px at scroll ${y}`) }
// 4. pageScrolls on its own.
if (pageScrolls(doc(900), { innerHeight: 900 }) !== false) bad.push('pageScrolls says a full-screen page scrolls')
if (pageScrolls(doc(901), { innerHeight: 900 }) !== false) bad.push('pageScrolls counts a 1px rounding overflow as a scroll')
if (pageScrolls(null, null) !== false) bad.push('pageScrolls does not fail safe without a document')
// 5. The component uses it.
const body = (src.match(/function clampIntoView\(\)\s*\{([\s\S]*?)\n\}/) || [])[1] || ''
if (!body) bad.push('PinHint.vue has no clampIntoView() to read')
else {
if (!/pillFloor\(\{[^}]*scrolls:\s*offPage \|\| pageScrolls\(document, window, el\)/.test(body)) bad.push('clampIntoView does not take its floor from pillFloor({ … scrolls: offPage || pageScrolls(document, window, el) … })')
if (!/const offPage = !!hr && \(hr\.top >= window\.innerHeight \|\| hr\.bottom <= 0\)/.test(body)) bad.push('clampIntoView does not treat a map wholly above or below the viewport as on a scrolling page')
if (!/if \(!onScreen\.value \|\| !paneActive\.value\) \{ el\.style\.removeProperty\('--pin-hint-lift'\); return \}/.test(body)) bad.push('clampIntoView keeps a stale lift when the pill goes off screen')
if (/Math\.min\(\s*window\.innerHeight/.test(body)) bad.push('clampIntoView still floors on window.innerHeight directly')
}
return bad
}
const real = { pillFloor: F.pillFloor, pageScrolls: F.pageScrolls }
const problems = audit(real, SRC)
if (process.argv.includes('--self-test')) {
if (problems.length) { console.error('PIN-HINT-FLOOR SELF-TEST: FAIL — the real code already fails; fix that first.\n' + problems.map((p) => ' ✗ ' + p).join('\n')); process.exit(1) }
const poisons = [
['the fold on every page (the old floor)', { ...real, pillFloor: ({ viewportH, sheetTop = Infinity, creditTop = Infinity }) => Math.min(viewportH, sheetTop, creditTop) }, SRC],
['a page that never scrolls', { ...real, pageScrolls: () => false }, SRC],
['a page that always scrolls', { ...real, pageScrolls: () => true }, SRC],
['obstacles dropped on a scrolling page', { ...real, pillFloor: (o) => (o.scrolls ? Infinity : F.pillFloor(o)) }, SRC],
['an inner scroller ignored (document only)', { ...real, pageScrolls: (d, w) => F.pageScrolls(d, w) }, SRC],
['no shadow-root crossing', { ...real, pageScrolls: (d, w, el) => { let e = el; while (e) { const oy = w && w.getComputedStyle ? w.getComputedStyle(e).overflowY : ''; if ((oy === 'auto' || oy === 'scroll') && e.scrollHeight > e.clientHeight + 1) return true; e = e.parentElement } return F.pageScrolls(d, w) } }, SRC],
['a stale lift kept off screen', real, SRC.replace("if (!onScreen.value || !paneActive.value) { el.style.removeProperty('--pin-hint-lift'); return }", 'if (!onScreen.value || !paneActive.value) return')],
['the off-page rule dropped', real, SRC.replace('scrolls: offPage || pageScrolls(document, window, el)', 'scrolls: pageScrolls(document, window, el)')],
['the old source line', real, SRC.replace(/const floor = pillFloor\([^\n]*\n/, 'const floor = Math.min(window.innerHeight, sheetFloor(r), creditFloor(r))\n')],
]
let missed = 0
for (const [name, fns, src] of poisons) { const caught = audit(fns, src).length > 0; console.log(` ${name}: ${caught ? 'caught' : 'NOT CAUGHT'}`); if (!caught) missed++ }
if (missed) { console.error(`PIN-HINT-FLOOR SELF-TEST: FAIL — ${missed} poison(s) not caught.`); process.exit(1) }
console.log(`PIN-HINT-FLOOR SELF-TEST: PASS — all ${poisons.length} planted defects refused.`); process.exit(0)
}
if (problems.length) { console.error('PIN-HINT-FLOOR: FAIL\n' + problems.map((p) => ' ✗ ' + p).join('\n')); process.exit(1) }
console.log('PIN-HINT-FLOOR: PASS — a scrolling page keeps the pill at the map\'s bottom; a full-screen page still lifts it above the fold; sheet and credit still lift it; PinHint uses pillFloor().')

View file

@ -11,7 +11,7 @@
* this._data = typeof options.data === 'string' ? { url: options.data } : { geojson: options.data }
*
* so `data.features` was undefined and the reader returned an EMPTY LIST on MapLibre — the keyless
* engine the public builds use. Measured by rega on 2026-09-28: a language-family selection and a
* engine the public builds use. Measured on 2026-09-28: a language-family selection and a
* prayer selection both logged "0 pin(s) selected" with the pins plainly on screen.
*
* ★ THE FAILURE MODE IS WHY A TEST IS THE ONLY GUARD. Every caller is written to NO-OP when it finds no
@ -79,7 +79,7 @@ for (const [label, src] of [['mapbox', { _data: COLLECTION }], ['maplibre', { _d
eq('expressions are ANDed', matchesExpression(['all', ['==', ['get', 'uniqueId'], 'a'], ['==', ['get', 'languageFamily'], 'Indo-European']], FEATURES[0].properties), false)
// ── PACKETSEL: the prayer count is LIVE, so the packet must read feature-state ──────────────────
// rega, RQ3(b): the packet's "Has Prayer" selection showed 50 of 997 where the legend row said 699.
// RQ3(b): the packet's "Has Prayer" selection showed 50 of 997 where the legend row said 699.
// The legend is right. Polling writes each pin's current count with setFeatureState() and
// prayer-progress.js:94 colours from ['feature-state','peoplePraying']; the GeoJSON property is
// whatever was true at page load. Reading the property selects the wrong people to pray for, which is

View file

@ -108,9 +108,9 @@ async function measure(width, lang) {
await page.waitForTimeout(10000)
// ★ HOVER IS INVISIBLE TO A STATIC PROBE, which is exactly how the blue outline stayed
// unbuilt for so long. Repeated reports asked to "restore the blue outline around the poster";
// unbuilt for so long. A blue outline around the hovered poster was reported missing more than once;
// measuring the loaded page found `outline:none` on all 136 framed elements and concluded it had
// never existed — true, and useless, because the affordance being asked for only exists WHILE
// never existed — true, and useless, because the affordance in question only exists WHILE
// THE POINTER IS OVER THE CARD. The pointer has to actually move.
//
// ⚠ AND IT MUST NOT MOVE ANYTHING. The whole rest of this gate asserts where the print row sits.

View file

@ -3,7 +3,7 @@
* check-poster-layout-optin — the composed poster layout is OPT-IN, and every caller that does not
* ask for it gets byte-for-byte what it got before.
*
* WHY THE OPT-IN EXISTS (dx-shepherd's ruling, from upg100's check). The renderer used to raster the
* WHY THE OPT-IN EXISTS (a decision that followed upg-100-list's check). The renderer used to raster the
* map at FULL PAGE size and composite the title, legend and footer on top of it, so the legend lay
* over the map it describes. usePosterLayout had always computed the correct geometry and nothing
* consumed it. Fixing that unconditionally would have broken a different product: country-posters

View file

@ -83,7 +83,7 @@ ok(typeof html === 'string' && html.length > 10000 && /<svg\b/.test(html), `the
// THE CLASSIC-SCRIPT COPY (strict-CSP pages load it with <script integrity>): pinned, served as the tree
// writes it, and — run as a real classic script — the SAME generator: its sheet is byte-identical to the
// module's for the same inputs. (The <script> path itself needs a browser; that half is rega's.)
// module's for the same inputs. (The <script> path itself needs a browser; that half is a browser check.)
{
const classic = globalVariant(source)
const servedClassic = readFileSync(join(REPO, 'public/dx-maps/assets/lib/poster-render.global.js'), 'utf8')

View file

@ -3,7 +3,7 @@
* check-preset-config-merge — a preset page keeps its profile's defaults. Headless.
*
* A preset (<bundle>/<profile>/<instance>/index.html) bakes a config made of the profile's
* <profile>.meta.json `config` with the instance's `props` MERGED over it. It used to REPLACE it: go-int's
* <profile>.meta.json `config` with the instance's `props` MERGED over it. It used to REPLACE it: the
* upg-neighbor presets lost the profile's pinHint:false. For every built preset page this asserts each
* meta `config` key is present, with the instance's own value where the instance sets one.
* Rule stated in documentation/PRESET-PLUGIN-STANDARD.md.

View file

@ -7,7 +7,7 @@
* It runs in Node with no layout engine, so it asserts the things that are TRUE OF THE MODEL: the
* geometry closes, every group gets a row, the fit ladder picks the size it claims, the fallback
* fires in all three states. It CANNOT assert that the rendered row is 73px tall in a browser —
* that is rega's seat (internal/check-packet-render.mjs, which launches a browser and is exit-75
* that is the browser gate (internal/check-packet-render.mjs, which launches a browser and is exit-75
* NOT MEASURED anywhere else). A character-count budget is a MODEL of wrapping, not a measurement
* of it, and the honest place to say so is here rather than in a commit message nobody re-reads.
*
@ -61,7 +61,7 @@ const CH = (H - 2 * M - HEAD - GUT * (PER_PAGE - 1)) / PER_PAGE
// to fire: 20 rows (73px) and 21 rows (69px) pass because the content genuinely fits; 24 rows
// (60px) and 28 fail. That is the boundary this is meant to guard.
// ⚠ THE ROW IS A BORDER-BOX, so its padding and border come OUT of the declared height rather than
// being added to it. rega measured 81px against a 73.25px model before `box-sizing:border-box` was
// being added to it. A browser check measured 81px against a 73.25px model before `box-sizing:border-box` was
// set — the rows were rendering 7.75px taller than the geometry that positions them, overlapping
// their successors, while the stack still fitted the sheet (ending 8px INSIDE the bottom margin
// rather than on it). Nothing clipped and nothing ran off the page, which is exactly why only a
@ -156,7 +156,7 @@ ok(long.text.endsWith('…'), 'a clamped description is not marked with an ellip
}
// ── 5. EVERY SHIPPED LOCALE FITS, MEASURED AGAINST THE REAL CATALOGUE ──────────────────────────
// ★ This is the arm that would have caught a locale I cannot eyeball. The ladder is INSURANCE, not
// ★ This is the arm that would have caught a locale no reviewer can read by eye. The ladder is INSURANCE, not
// routine: if a real locale is routinely stepping down, the budget is wrong and the fix is the
// budget, not the ladder. Longest today is German at 178 against a 234-char budget.
{
@ -177,4 +177,4 @@ if (fails.length) {
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log('PROFILE-TABLE: PASS — the row is tall enough for a name plus two description lines at the smallest ladder size, every group gets a row across page boundaries, the fit ladder steps 17→15→14 and cuts at a whole word only as a last resort, a missing or empty describer degrades to the metadata line, and every shipped locale fits at full size. NOT covered here: the RENDERED row height and whether the description actually wraps to two lines — those need a browser and are rega\'s seat.')
console.log('PROFILE-TABLE: PASS — the row is tall enough for a name plus two description lines at the smallest ladder size, every group gets a row across page boundaries, the fit ladder steps 17→15→14 and cuts at a whole word only as a last resort, a missing or empty describer degrades to the metadata line, and every shipped locale fits at full size. NOT covered here: the RENDERED row height and whether the description actually wraps to two lines — those need a browser check.')

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
/* check-provider-for.mjs — run as `bun run check:provider-for`. Source-only, headless, ~0.1s.
Placed in internal/ with studio's explicit authorisation for this one gate (END-STU-C3AB): it spans
Placed in internal/ by explicit decision for this one gate: it spans
library/ plus three separate plugins, so no single plugin's scripts/ folder owns it. */
// C3: do the library helpers behave like the raw-global code they replace, on BOTH engine shapes AND
// on the ambiguous case that is neither?

View file

@ -2,7 +2,7 @@
/**
* check-published-tree-weight — the tree we publish has a weight, and nothing was watching it.
*
* THE DEFECT THIS CATCHES. A publish was ALREADY BLOCKED ONCE at a 74MB asset tree (Forgejo
* THE DEFECT THIS CATCHES. A publish was ALREADY BLOCKED ONCE at a 74MB asset tree (the git host
* rejected the pack). Nothing has measured the tree since. MEASURED 2026-09-25: public/assets is
* 134MB, against the 94MB recorded when BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB was written —
* it grew 40MB, and no one noticed, because size is the classic property that only becomes visible
@ -109,7 +109,7 @@ if (existsSync(posters)) {
if (total > MAX_MB * MB) {
console.error(`\nTREE-WEIGHT: FAIL — public/${TREE_NAME}/assets is ${mb(total)}MB, over the ${MAX_MB}MB ceiling.`)
console.error(' A publish was already blocked once at a 74MB asset tree (Forgejo rejected the pack),')
console.error(' A publish was already blocked once at a 74MB asset tree (the git host rejected the pack),')
console.error(' and nothing has measured it since; it grew from 94MB to 134MB unnoticed.')
console.error(' Before deleting anything, read the split above: the city posters are the heavy half')
console.error(' AND the half that cannot be regenerated. The reproducible half is worth less than')

View file

@ -10,7 +10,7 @@
* ★ NARROWED AFTER LANDING, and this is what the next reader needs: the ordering is not merely
* "not the cause" — it is UNCONSTRUCTIBLE today. region-dashboards.vue:1091 gates the entire map
* subtree behind v-if="loading", and loadRegions assigns points inside the try while loading=false
* is in the finally, so the map CANNOT mount with empty points. rega measured 0 grey in 36 mounts,
* is in the finally, so the map CANNOT mount with empty points. A browser check measured 0 grey in 36 mounts,
* including a run with the prayer API held back 20s: the map was constructed AFTER the data, every
* time. So this gate guards a FUTURE refactor that drops that v-if data gate — not a live path.
*
@ -27,7 +27,7 @@
*
* ★ WHAT THIS ASSERTS, and it is a MODEL not a mock: the four arrival orders, against the real
* repaint rule. It fails if any order can leave the fill flat-grey while points exist. A browser
* cannot be run here (that is rega's seat), so the ordering is modelled and the SOURCE is asserted
* cannot be run here (that is a browser check), so the ordering is modelled and the SOURCE is asserted
* to carry the repaint that makes the model's good cases reachable.
*/
import { readFileSync } from 'node:fs'

View file

@ -85,14 +85,14 @@ const LINKED_KNOWN = {
// - both fixes are ancestors of main: 12a64702 (release the retained request on success — the FIRST
// press was losing the zone) and 67e9ede5 (the wall clears its memory when its pane goes inert).
// - the receiving side still registers its listener.
// - rega proved it end to end on BOTH engines at 810ffa26.
// - a browser check proved it end to end on BOTH engines at 810ffa26.
}
const MAPBOX_KNOWN = {
// pplr-data-maps (new Experimental bundle, exp-shepherd) loads the Mapbox GEOCODER script even when
// pplr-data-maps (new Experimental bundle) loads the Mapbox GEOCODER script even when
// the host asked for maplibre: the only api.mapbox.com request in the whole keyless gallery, both
// modes, measured 2026-09-25 while that tab was open. Routed via dx-shepherd; delete this entry in
// modes, measured 2026-09-25 while that tab was open. Routed to its owner; delete this entry in
// the owner's fix commit so the gate enforces zero again.
'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js': 'exp-shepherd (pplr-data-maps loads the Mapbox geocoder under maplibre)',
'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js': 'pplr-data-maps loads the Mapbox geocoder under maplibre',
// EMPTY ON PURPOSE. The one entry here — my-upg-100-list/upg-100-list.vue:1464 pinning
// mapbox-gl.css v3.3.0 regardless of engine — was fixed and the exemption deleted in the SAME
// COMMIT as the fix, so main never had a window where the gate red-flagged its own known bug and

View file

@ -2,7 +2,7 @@
/**
* check-select-zone-race — "Show on map" works on the FIRST click, not the second.
*
* THE DEFECT (PRODUCTION-MATRIX:422, measured by rega): the composite's list pane dispatches
* THE DEFECT (PRODUCTION-MATRIX:422, measured in a browser): the composite's list pane dispatches
* 'doxa:select-zone' at the map pane, but the map pane attaches its listener in onMounted and the
* pane mounts lazily. So the first click landed before any listener existed, did nothing, and only
* a SECOND click selected the zone. A control you must press twice reads as broken.
@ -19,7 +19,7 @@
* Delivery and arrival both succeeding still lost the selection without this.
*
* Source-level and headless: the race is a lifecycle ordering between two custom elements, which a
* DOM-less runner cannot stage and which the browser seat measures end to end (rega owns that).
* DOM-less runner cannot stage and which the browser seat measures end to end.
* What this pins is that none of the three mechanisms quietly disappears.
*
* Exit 0 pass · 1 fail.
@ -58,7 +58,7 @@ ok(/addEventListener\('doxa:select-zone'/.test(LIST),
// ── 3. EFFECT: retained while unready, drained at the readiness signal ─────────────────────────
// ⚠ THE SUBTLEST LAYER. Delivery and arrival can both succeed and the selection still not hold,
// because onLegendSelect returns early while the tapestry layer is down. rega read that state
// because onLegendSelect returns early while the tapestry layer is down. A browser check read that state
// twice, 13 ms apart, with the correct id.
// ⚠ TWO DISTINCT RETAIN SITES, and a single /_pendingZoneId = zoneId/ match passes while ONE of
// them is deleted — measured: removing the not-found retain left the gate green on the other.

Some files were not shown because too many files have changed in this diff Show more