A collapsible section head is a <button> with width:100%, but it keeps the
2px side margins of .sec-head, so it overflowed the wall's content box by 4px.
Its full-bleed ::before (left/right -20px) then reached 2px past the wall on each
side, so every collapsible wall (Poster Design's Styles, Country Posters) had
scrollWidth = clientWidth + 4 at 390, 768 and 1024. The head is now
calc(100% - 4px), the width a non-collapsible head already gets.
Measured on the built poster-design and country-posters bundles (div.wall
scrollWidth vs clientWidth at 390/768/1024/1440): before, +4 at the first three
widths on both walls; after, equal at all four. UPG-100's wall (no toggles)
was already equal.
Measured on the 60x96 print: every photo beside a name rasterised as the browser's broken-image glyph. An SVG
rasterised through <img> is an isolated document that fetches no external resource, and none of the people-group
photo hosts (media.joshuaproject.net, joshuaproject.net, images.peoplegroups.org and its resizer,
peoplegroupspublic.blob.core.windows.net, s3.doxa.life) sends CORS, so a photo cannot be inlined as data: in the
browser either.
- poster-raster.js: rasterPoster drops every <image> whose href is not data: (the basemap tiles are data: and stay),
so a download shows the paper, never the glyph. hasExternalImages() says when a sheet carries such photos.
- PosterWall: printing a generated sheet that carries photos prints THE SHEET ITSELF through printDocument, where
the photos load, on its own @page (60in 96in), instead of a raster shrunk onto letter. Same HUD steps.
- check:side-lanes: photo arms (0 external images reach the raster; the wall prints such a sheet as a document),
each with a poison in its self-test.
- check:side-lane-photo-pixels: the pixel side, a browser gate (a data: photo draws red; 99/99 external slots stay
flat; without the strip 0/99 do). Declared on-demand until its first browser-seat run.
- library/components/WARNING.md, library/dashboards/README.md and
documentation/contributing/README.md taught "graduate into the library at the second
consumer". They now teach the plugin-designer skill's rule (section 3): a component that
could serve more than one map is built once in library/, driven by props, and landed through
the build owner; one-off or experimental pieces stay in a map's folder; a new pattern is a
pilot, then approved, then rolled out. The contributing guide's "full reasoning" link pointed
to an index without it; it now points to the skill.
- The app shell doxa-maps-spa is labelled Deep Research. Ten docs still called it the Research
Gallery (15 mentions): DEPLOY.md, the desktop how-to, embeddability, develop-a-plugin,
reference-universal-props, reference.md, the shell's EMBED.md, and the plugin-designer skill
with two of its references. Identifiers (doxa-maps-spa, branch and route names) are unchanged;
README.md is not touched.
DX Life's /pray at 1440 kept the pill at the viewport's bottom until the map's bottom came into view: the page
scrolls a container, not the document, so the document-only test saw a full-screen page and kept the fold.
pageScrolls now also walks out from the pill, across shadow roots, for an ancestor scroller whose content is
taller than it; a map wholly below or above the viewport is treated as on a scrolling page; and a lift is
cleared when the pill goes off screen, so a lift set while the map was half visible does not survive into the
next view. check:pin-hint-floor adds the inner-scroller, shadow-crossing and map-below-the-fold fixtures; its
self-test refuses 9 planted defects.
The case-insensitive census with every plugin agent's name found 30 more lines: library (ProfileLoader,
useMapProvider, ApproxPinNote, listSelection, colocation), the gallery's embed-contract card id, the gospel
plugins' source notes and build scripts, a regions script note and one gate description. Comments and notes
only, except the Gospel x Internet CSV export's provenance line, which no longer names who screened the links.
The Country Posters India section headed "855 people groups · 37
countries", which is untrue: its cards are India's own and one per state
and union territory. sectionItemCountKey (default null: every section
counted with item-count-key, as before) lets a wall answer, per section,
an i18n plural key, or { key, n } to count n rather than the section's
card count, or null to keep the default.
Walls that do not pass it render byte-identical trees before and after
(a differential render of an upg100-style and a country-posters-style
wall). check:poster-wall-host-seams arm K: a named section counts in its
own key and number, an unnamed one keeps the default; it fails with the
prop ignored.
A section head read "<n> people groups · <items>" always, so a wall whose
cards count no people (Poster Design's gallery of templates) showed
"0 people groups · 4 lists" on every head. The total and its " · " now
leave together when the total is 0, the rule the cards already follow with
v-if="item.count". The head is still one string in one interpolation, so a
section with a total renders exactly the text it always did, whitespace
included: a differential render of an upg100-style wall and a
country-posters-style wall, before and after, gives identical trees.
check:poster-wall-host-seams gains arm J (a head with people keeps its
total; one without reads its items alone); the old head line fails it.
A section's total (its header count, and what orders the sections) defaults to the sum of its cards' counts,
unchanged for every wall that does not pass the prop. The Country Posters India section holds India's own card
and one card per state and union territory (the same 855 groups twice), so it passes India's count instead.
Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
On a partner home page the map is a block in a scrolling page. The clamp floored the pill at the viewport
fold, so a map whose bottom was below the fold at load had its pill lifted to mid-map, re-set by the settle
timers and resize observers, and left there after a scroll, then dropped. The fold now counts only on a
page that does not scroll (a full-screen pane or leaf, where a phone's browser bar can cover the bottom);
the legend sheet and the licence credit still lift the pill everywhere. The floor is a pure function
(pinHintFloor.js), and check:pin-hint-floor proves both cases; its self-test refuses 5 planted defects.
VerticalTabsLayout, header off: on a phone a 44px bar (a real grid row) replaces the floating corner
circle, with the rail toggle at the inline start and a #phone-end slot at the end; on a desktop a
#rail-end slot sits at the bottom of the rail (it receives `collapsed`). Neither renders with a header
on, so header-on consumers and desktop layouts without a filler are unchanged. The --map-fab-* footprint
is no longer published; its readers fall back to their plain insets.
ShellLanguageMenu fills either slot: the switcher's languages plus the current one, a native select
under a globe face. A pick dispatches one cancelable, composed, bubbling doxa-language-request on the
host element (library/utils/languageRequest.js). A host that cancels keeps the language; otherwise the
menu emits 'switch' and writes ?lang= only on a page the tree generated.
check:shell-header (gates:pre, Vite SSR) asserts where the bar and the rail slot render, the event's
shape and count, cancel and no-cancel, the ?lang= rule and the language list; its self-test (gates:post)
refuses 11 planted defects. check:nav-reachable now presses the bar's toggle.
The shadow-styles id must be a plain lowercase identifier (check:shadow-css-parses
reads anything else as a CSS rule concatenated onto the id), so 'PanelSheets'
becomes 'panel-sheets'; nothing imports the component yet. The derived library
index gains panelSheets.js and PanelSheets.vue.
A poster too big for one sheet prints as several, each trimmed edge laid over the next sheet's join band:
60x96 is three 60 x 36 in sheets stacked with 6 in joins (the Bless 5 x 8 ft wall), 66x66 two 36 x 66 in
sheets side by side with a 6 in join (India). library/components/poster/panelSheets.js is pure:
- planPanels(sheets) takes the size table's sheets object as is (sheetW, sheetH, count, direction, joinIn),
plus the renderer's borderIn and joinFill;
- sliceFor(plan, s) gives the part of the finished poster that sheet s prints, one page per sheet;
- screenLayout, panelChromeHtml and panelCss give the sheets pulled apart and the marks.
A 'glue' join leaves the band blank and marked red; a 'bleed' join lets a map continue under it, so a
trim a few millimetres off shows map, never a white seam, and only a guide is printed there.
PanelSheets.vue previews it for Vue surfaces:
- a src (the finished poster as one image) sliced per sheet with CSS, or a #sheet slot;
- it fills its container's width, takes no pointer events, and emits one load or error per src;
- it always draws the red glue band and the gap, even for a print that bleeds.
Its label is poster.glue_here, new in all 11 catalogues.
check:poster-wall-host-seams gains arm H: a default wall's broken stored
webp retries the tree's own copy before the placeholder. Arm I: a generated
preview that fails shows the placeholder at once. The gate serves embedBase.js
as if from a built tree, because the second base is derived from that URL and
is empty from source, which let a mutant that skipped the retry pass.
previewSrc's observer now uses the wall's own scroller as its root (the
viewport when the wall is embedded and the page scrolls it). A pane wall
scrolls inside .wall, which clips its cards, so with the viewport as root the
200px margin never looked ahead. Arm E asserts the root.
PosterWall states the two limits of previewSrc: a timeout ends the wait, not
the render, and the page cache never revokes the URLs it holds.
A card's thumbnail was always the stored previews/<id>.webp. A gallery of
poster templates drawn from the library has no stored file, and must show a
library change the moment it lands, so the thumbnail can now come from the
host: previewSrc: async (item) => an image URL | null.
Default null keeps the stored webp and creates no observer, so every current
wall is unchanged. When set, a card renders only once it is in view, one
render at a time, cached on the page by instanceId + item id. Until it answers
the card shows its label; null, a throw or no answer within 20 s shows the
existing "preview unavailable" placeholder.
The header-action gate becomes check:poster-wall-host-seams and covers both
seams: arms D to G for previewSrc (default webp and no observer; lazy, one at
a time and the null fallback; the timeout; the cache). Six mutants of the new
code each fail it.
A template card (Poster Design) carries no people-group count, and the new
branch printed a bare ' UPG ·' before its label. The count and separator now
render only when item.count is set, in the headerAction branch only; the
default toggle is unchanged. check:poster-wall-header-action gains arm C for
it, which fails with the guard removed.
The sticker on every poster card was hard-wired to "Show on map". A wall with
no map beside it (a gallery of poster templates) needs the same card with a
different action, and a fork of the card would drift from the wall.
headerAction null (the default) renders the existing toggle unchanged: the
same markup, aria-pressed, the is-last highlight and the window event, so
every current wall is untouched. { labelKey, ariaKey } renders a plain button
labelled from those keys that emits 'header-action' with the item and
dispatches no window event.
check:poster-wall-header-action (gates:pre) mounts the real PosterWall
headlessly on a recording renderer and calls the sticker's onClick in both
modes. Four mutants fail it: no seam, a new branch that also dispatches, a
default that emits, and aria-pressed on the new branch.
The credit fold was guarded by uiStore.isMobile, which only uiStore.init()
sets; a host that never calls it (UPG-100's map) kept the credit expanded.
The fold now tests the phone breakpoint itself, with the phone CSS block's
own query.
A finger aimed at the open sheet's top edge often landed on the hint pill
just above it, and a drag there scrolled the pane instead of resizing the
sheet. A transparent 12px lip above the open sheet takes that gesture with
the sheet's own handlers (their grab zone is measured from the sheet's top),
above the pill; it exists only while the sheet is open.
The pill takes touch-action:none. With the legend open it sits just above
the sheet's top edge, so a drag meant to resize the sheet often began on the
pill's lower edge and scrolled the pane instead. Taps still fire, so its
dismiss button keeps working.
A control in a map's own column keeps its pointer at every width; a header-form control (the
rounded-square MapControlButton) sits in the strip the rule was measured on, so below 1200px its
pointer hides again. Rebuilt doxa-maps-spa and doxa-simple-map (MapInfo's importers), tree and SRI.
The Regions header takes Big Picture's header recipe: its own MapInfoButton and ShareButton in the header form
(shape rounded-square, the vertical tabs' radius), side by side at the header's end, 28 px with a 44 px tap
target; on a phone the ⓘ takes the header's start edge, since the centred view switcher leaves no room for
two at one end. The gallery hands the tab's MapInfo to that button (trigger 'control'), with the floating ⓘ as
the fallback. The map-print progress card no longer shows the renderer's English phase messages.
An engine can mark its credit compact only after the map is ready, and the
fold stopped polling at a credit that was present but not yet compact, so
on such a map the credit never folded. It now stops only once the credit is
compact and already folded (a drag or a tap got there first), and keeps
looking, for up to 30 s as before, while it is not yet compact.
- Two lines, each its own string, in a new catalogue area only PinHint names (line1 / line2 x 11,
taken from today's one-sentence text at each locale's own separator; that text is untouched).
- 44px tall like the ☰, 12px text, a smaller bob. Capped to the width left between two ☰ footprints
(--map-fab-beside), so it is centred with equal room and never reaches the ☰; with no ☰ it is
simply centred.
- In the ☰'s row: while the layout shows the ☰, the base is the ☰'s own 12px bottom, and the clamp
lifts it to 8px above the legend sheet, the gap the layout keeps for the ☰.
- It no longer reads --map-fab-clearance, which now carries the ☰'s scroll-tracked lift: inside the
map, beside the legend, the pill stays over it by itself, and positioned from that lift it moved
against the legend on every scroll.
The layout's round menu button now sits fixed in the bottom corner on the
start side and never moves, so the sheet no longer insets itself beside it:
it is always full width. The collapsed strip pads its start edge by
--map-fab-beside, the room the button takes, so its caret and title stay
clear of it (0 where there is no button); the detail footer, placed
absolutely, gets the same inset. Open, the sheet keeps its full width under
the button's small corner. The floating image returns to the start edge. The
'dxmaps:legend-sheet' event is still dispatched.