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:
commit
a1c74004a0
416 changed files with 3140 additions and 20305 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ''')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).')
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
102
internal/check-no-fleet-names.mjs
Normal file
102
internal/check-no-fleet-names.mjs
Normal 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.`)
|
||||
|
|
@ -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)` : '') + '.')
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
109
internal/check-pin-hint-floor.mjs
Normal file
109
internal/check-pin-hint-floor.mjs
Normal 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().')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue