Poster wall: the banner gets stickers, top and bottom
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

The card is a BANNER that displays a map, the way a label displays a
design on a bottle. A banner taped up gets a sticker at each end, and
each sticker straddles the edge it tapes: part on the banner's own white
space, part overhanging. Neither one touches the map.

MEASURED at 1440px before this change:
  banner 240-426, map 241-425  ->  1px of white space at each end
  header sticker  35px above the banner's top edge, -1px below
  footer sticker   1px above the bottom edge, 67px below

So the map filled the banner edge to edge and there was no banner
surface for a sticker to sit on. The footer row could only hang off into
space, which is what shipped, and every assertion in the gate was green
through all of it.

The banner now reserves white space at both ends (--banner-pad-top /
--banner-pad-bottom on .main), and each sticker centres on its edge:

  header sticker  17px on the banner / 17px above it
  footer sticker  35px on the banner / 33px overhanging
  map covered by either: 0px

The print row also moved INSIDE .main. It was a sibling, so top:100%
resolved against the CARD, 27px past the banner - the re-parenting and
07d3341c's centring transform are one fix, and neither works alone.
That commit's per-row localStorage preference is preserved.

Card Deck is no longer a peer button with a 162px <select> beside it.
The stock picker is a disclosure: press Card Deck, choose a size, and
that choice prints. A native dropdown in the row collides with the host
page's own dropdowns, which is why it is buttons rather than a <select>.

Print-button labels WRAP instead of clipping. With the per-row default
now 2, a 390px viewport halves the card to ~170px, where Romanian's
"Pachet de cartonașe" cannot share a line: measured 54 of 81 controls
clipped at ro/390px, en and zh clean. The ruling on this wall is that a
long label wraps; nowrap was forcing a clip.

Also fixes the transparent strip above a pinned section header. Sticky
pins to the scroll container's PADDING box, so with .wall's 22px
padding-top the header rested 22px down and posters scrolled visibly
through the gap. A ::before extends the header's opaque fill upward by
exactly that padding, read from the same var so the two cannot drift.
Measured by sampling the strip's pixels: 1 colour, 100% header
background, where before it was 1338-2831 colours of poster artwork.

The MFE shell's own chrome is translated, in all 11 locales. Measured
with ?lang=ar: "Select View", "Views", "Prayer Resources" and "Map"
rendered English while the poster wall's buttons inside the panes
translated correctly, because each pane installs i18n and the wrapper
around them never did - the chrome a reader meets FIRST was the
untranslated part. The wall's own heading, tag and intro go through t()
for the same reason, with the <strong> inside the translated string so
each language emphasises its own words.

The gate grew four assertions, each mutation-tested to fail on the
defect it was written for: off-frame (row floated off the banner),
head-off (header sticker not straddling), map-covered (a sticker on the
poster), and selects (a native dropdown back in the row).

Includes a rebuild of the four PosterWall consumers, which also picks up
studio's shared useMapInstance unmount guard and exp-shepherd's
translated LegendMobile labels.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 05:55:15 +00:00
parent 28fd8aae70
commit 89bd9c5d79
41 changed files with 830 additions and 450 deletions

View file

@ -203,6 +203,8 @@ async function measure(width, lang) {
const boxes = cards.map((c) => ({ c, b: c.getBoundingClientRect() }))
let intrusions = 0, overflowing = 0, worst = 0, coveringPreview = 0, worstY = 0
let previewsPainted = 0, previewsBroken = 0, noPreview = 0, unnamedPreview = 0
let notTaped = 0, worstTape = 0, selectsInRow = 0
let headNotTaped = 0, worstHead = 0, mapCovered = 0, worstCover = 0
for (const { c, b } of boxes) {
const row = c.querySelector('.pfs')
if (!row) continue
@ -247,6 +249,69 @@ async function measure(width, lang) {
worstY = Math.max(worstY, Math.round(ib.bottom - rb.top))
}
}
// ★ A STICKER STRADDLES THE EDGE IT TAPES (Driver 2026-09-25, the sticker/banner brief).
// The card is a BANNER displaying a map; each sticker sits centred on a banner edge, part
// on the banner's own white space and part overhanging. Three things were each true while
// this gate was green, so all three are now asserted:
// 1. the row floated 26px BELOW the banner, touching nothing (the shipped state);
// 2. the row sat entirely below the edge, touching at one pixel;
// 3. the row straddled correctly but landed ON THE MAP — "offensive to the map".
// over-preview above catches only a row overlapping the <img> from above.
const frame = c.querySelector('.main')
if (frame) {
const fb = frame.getBoundingClientRect()
// STRADDLE: some of the sticker on the banner, some off it. Requiring a near-even split
// would encode a ratio the Driver explicitly left open ("probably an equal or more
// overlap"); requiring BOTH sides to be non-trivial is the property actually stated.
const onBanner = Math.round(fb.bottom - rb.top)
const offBanner = Math.round(rb.bottom - fb.bottom)
if (onBanner < 4 || offBanner < 4) {
notTaped++
const d = onBanner < 4 ? onBanner : -offBanner
worstTape = Math.abs(d) > Math.abs(worstTape) ? d : worstTape
}
}
// ★ NO NATIVE DROPDOWN IN THE DEFAULT ROW (same ruling). The Driver's reason is not
// cosmetic: "I don't want to add the dropdown for the card because it makes it hard for
// others to have dropdowns too" — a <select> here collides with the host page's own
// selects. The size picker is a disclosure, revealed by the Card Deck button. A <select>
// reappearing in .pfs is the regression this catches.
if (row.querySelector('select')) selectsInRow++
// ★ AND THE HEADER STICKER STRADDLES ITS EDGE TOO. Same requirement, other end of the
// banner, and it was ALSO wrong while everything here was green: MEASURED 35px above the
// banner's top edge and -1px below, i.e. floating over the card touching the banner
// nowhere. Asserting only the footer would have left that in place indefinitely.
const headSticker = c.querySelector('.tape-h')
if (frame && headSticker) {
const hb = headSticker.getBoundingClientRect()
const fb2 = frame.getBoundingClientRect()
const onB = Math.round(hb.bottom - fb2.top)
const offB = Math.round(fb2.top - hb.top)
if (onB < 4 || offB < 4) {
headNotTaped++
const d = onB < 4 ? onB : -offB
worstHead = Math.abs(d) > Math.abs(worstHead) ? d : worstHead
}
}
// ★ NOTHING TOUCHES THE MAP. The brief is unambiguous and is the reason the banner grew
// at all: "the preview banner to showcase a map should not have anything overlapping the
// map because the map needs to be fully visible." Measured on the first straddle attempt:
// header over the map by 6px, footer by 8px — a correct straddle onto a banner with too
// little white space reserved. THIS is the assertion that catches that, and neither the
// straddle checks above nor over-preview can: over-preview only sees a row whose top is
// above the image bottom, which is a different geometry from a sticker landing on it.
if (img) {
const ib3 = img.getBoundingClientRect()
if (ib3.height > 2) {
const byFoot = Math.round(ib3.bottom - rb.top)
if (byFoot > 0) { mapCovered++; worstCover = Math.max(worstCover, byFoot) }
if (headSticker) {
const hb2 = headSticker.getBoundingClientRect()
const byHead = Math.round(hb2.bottom - ib3.top)
if (byHead > 0) { mapCovered++; worstCover = Math.max(worstCover, byHead) }
}
}
}
for (const o of boxes) {
if (o.c === c) continue
const ox = Math.min(rb.right, o.b.right) - Math.max(rb.left, o.b.left)
@ -295,7 +360,8 @@ async function measure(width, lang) {
return { cards: cards.length, rows: cards.filter((c) => c.querySelector('.pfs')).length,
overflowing, worst, intrusions, coveringPreview, worstY,
previewsPainted, previewsBroken, noPreview, unnamedPreview, clipped: clipped.length,
controls: ctrls.length, tiny: tiny.length, perRow, cardW }
controls: ctrls.length, tiny: tiny.length, perRow, cardW,
notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover }
})()`)
await page.close()
return { width, lang, hover, reach, ...out }
@ -315,6 +381,10 @@ for (const r of results) {
+ ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}`
+ ` · neighbour ${r.intrusions}`
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
+ ` · off-frame ${r.notTaped}${r.worstTape ? ` (${r.worstTape > 0 ? '+' : ''}${r.worstTape}px)` : ''}`
+ ` · selects ${r.selectsInRow}`
+ ` · head-off ${r.headNotTaped}${r.worstHead ? ` (${r.worstHead > 0 ? '+' : ''}${r.worstHead}px)` : ''}`
+ ` · map-covered ${r.mapCovered}${r.worstCover ? ` (${r.worstCover}px)` : ''}`
+ ` · previews ${r.previewsPainted} painted`
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
+ ` · max ${r.perRow}/row @ ${r.cardW}px`
@ -362,6 +432,39 @@ for (const r of results) {
fails.push(`${at}: ${r.previewsBroken} preview image(s) failed to load (naturalWidth 0) — `
+ 'a broken preview is not something the geometry checks can see')
}
// ★ THE OTHER EDGE OF over-preview. That assertion catches a row sitting too HIGH; this one
// catches it sitting too LOW, which is the state that actually shipped and that nothing here
// could see: MEASURED frame bottom 426, row top 452, and a full green run.
if (r.notTaped) {
fails.push(`${at}: ${r.notTaped} print row(s) do not STRADDLE the banner's bottom edge `
+ `(worst ${r.worstTape > 0 ? '+' : ''}${r.worstTape}px on one side; positive = too little `
+ 'on the banner, negative = too little overhanging). The footer sticker sits centred on '
+ 'that edge, part on the banner\'s white space and part hanging onto the page below — '
+ '"slight overlap over the banner, but not over the content of the banner". A row '
+ 'floating clear of the banner is as wrong as one covering the map.')
}
// ★ THE ONE THE DRIVER CARES ABOUT MOST. "The preview banner to showcase a map should not
// have anything overlapping the map because the map needs to be fully visible."
if (r.mapCovered) {
fails.push(`${at}: ${r.mapCovered} sticker(s) OVERLAP the map preview (worst ${r.worstCover}px). `
+ 'The banner\'s header and footer stickers straddle the banner\'s edges; the map inside is '
+ 'never to be touched — "if I put a piece of tape over the text it would be offensive to '
+ 'the map". Reserve more banner white space via --banner-pad-top/--banner-pad-bottom on '
+ '.main rather than moving the sticker: a sticker centred on the edge puts HALF its '
+ 'height on the banner, so the padding must exceed that half.')
}
if (r.headNotTaped) {
fails.push(`${at}: ${r.headNotTaped} header sticker(s) do not straddle the banner's top edge `
+ `(worst ${r.worstHead > 0 ? '+' : ''}${r.worstHead}px on one side). A sticker sits ON the `
+ 'edge it tapes, part on the banner and part overhanging — measured 35px above / -1px '
+ 'below before this was asserted, i.e. floating free of the banner entirely.')
}
if (r.selectsInRow) {
fails.push(`${at}: ${r.selectsInRow} native <select>(s) in the print row. The card-stock `
+ 'picker is a DISCLOSURE behind the Card Deck button, not a control in the default row — '
+ '"I don\'t want to add the dropdown for the card because it makes it hard for others to '
+ 'have dropdowns too." A <select> here also collides with the host page\'s own dropdowns.')
}
// EVERY card falling back to the placeholder means the previews are gone. MEASURED by aborting
// every previews/** request: the product degrades WELL — all 27 cards swap to a .ph placeholder
// reading "poster not generated yet", with no broken-image icons and no <img> left at all. That

View file

@ -409,6 +409,31 @@ const prints = usePosterPrints({
const canPrintRows = computed(() => typeof props.records === 'function')
const previewIsControl = computed(() => props.previewAction === 'print')
// ── CARD-DECK DISCLOSURE ────────────────────────────────────────────────────
// Which card currently has its size row revealed, by item id. Holding an ID
// rather than a boolean-per-card means opening one CLOSES the others for free:
// two size rows open at once would overlap in the grid gap, and there is no
// reading of the Driver's request where two are wanted.
const deckOpenFor = ref(null)
function toggleDeck(id, ev) {
deckOpenFor.value = deckOpenFor.value === id ? null : id
// Tell the size row how far down to start: the print row's REAL height, which is
// one line in en and two in the long-label locales. Walking up from the clicked
// button to its own .pfs keeps this correct per card rather than per wall.
const pfs = ev?.currentTarget?.closest?.('.pfs')
if (pfs) pfs.parentElement?.style.setProperty('--pstocks-gap', `${pfs.offsetHeight}px`)
}
// Choosing a size IS the print command — the reader already said "print the
// cards" by opening this row, so a second confirm step would be a click with
// nothing behind it. prints.stock reads pickedStock through a getter, so setting
// it here is what the deck prints at.
function printDeckAt(item, stockId) {
pickedStock.value = stockId
deckOpenFor.value = null
prints.printCardDeck(item)
}
function downloadPacket(item) {
// Still the honest "not yet" when this wall has no records provider: NOT an
// error — unbuilt, not broken. phase:'error' reads to a user as "something went
@ -479,12 +504,21 @@ function download(blob, name) {
function dismissExport() { exportState.value = null }
useShadowStyles(
'.wall{position:absolute;inset:0;overflow-y:auto;padding:22px 18px 44px;' +
// --wall-pad-top is read back by .sec-head::before to cover the scroll-padding strip
// above a pinned header (see there). Declared as a var so the two can never drift:
// typing 22px in both places is how that gap would silently re-open.
'.wall{--wall-pad-top:22px;position:absolute;inset:0;overflow-y:auto;' +
'padding:var(--wall-pad-top) 18px 44px;' +
"background:#f6f7f9;color:#0f1419;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
'.wall.is-dark{background:#0a1410;color:#e5e7eb}' +
// Embedded: normal flow, no pane background, no pane padding. inset:auto matters as much
// as position:static — leaving inset set keeps the box stretched even once static.
'.wall--embedded{position:static;inset:auto;overflow-y:visible;padding:0;background:transparent}' +
// ⚠ --wall-pad-top MUST go to 0 here too, not just the padding. Embedded, the wall is not
// the scroll container — the HOST page is — so there is no scroll-padding strip to cover,
// and a 22px cover bar would paint the wall's background over the host's own content
// above each header. The var and the padding are one decision, set in one place.
'.wall--embedded{--wall-pad-top:0px;position:static;inset:auto;overflow-y:visible;' +
'padding:0;background:transparent}' +
'.wall--embedded.is-dark{background:transparent}' +
'.wall-h{margin:0 0 6px;font-size:19px;font-weight:700;text-align:center}' +
'.wall-tag{margin:0 auto 12px;width:fit-content;padding:3px 11px;border-radius:999px;' +
@ -513,8 +547,13 @@ useShadowStyles(
'border-radius:6px;background:#fff;color:inherit;cursor:pointer}' +
'.is-dark .perrow__label{color:#9fb3a6}' +
'.is-dark .perrow__select{background:#132019;border-color:rgba(110,231,183,.20)}' +
'.card{position:relative;background:#fff;border:1px solid #e3e6ea;border-radius:3px;' +
'padding:24px 12px 26px;box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
// --card-pad-top IS the banner's top edge in card coordinates, and .tape-h reads it to
// centre itself there. Declared as a var so the two cannot drift apart the way a second
// hand-typed 24px would.
'.card{--card-pad-top:24px;position:relative;background:#fff;border:1px solid #e3e6ea;' +
'border-radius:3px;' +
'padding:var(--card-pad-top) 12px 26px;' +
'box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
'transition:transform .16s ease,box-shadow .16s ease}' +
// ★ THE BLUE OUTLINE IS A HOVER STATE, NOT A MISSING STATIC BORDER. Several reports have asked
// for "the blue outline around the poster" to be restored, and MEASURED 2026-09-24 there has
@ -546,7 +585,16 @@ useShadowStyles(
'font-size:10.5px;font-weight:600;letter-spacing:.02em;cursor:pointer;' +
'border:1px solid rgba(0,0,0,.08);background:#eef2f6;color:#23303c}' +
'.is-dark .tape{background:#1d2b23;color:#cfe9d9;border-color:rgba(255,255,255,.10)}' +
'.tape-h{top:-11px;width:82%;padding:4px 8px;border-radius:999px;' +
// ★ THE HEADER STICKER STRADDLES THE BANNER'S TOP EDGE, for the same reason the footer one
// straddles the bottom. MEASURED before: 35px above the banner's top edge, -1px below — it
// floated over the card, touching the banner nowhere. The Driver reads the rounded top
// sticker as the one that "looks beautiful and is really well spaced", so this keeps its
// size and shape exactly and only corrects WHERE it sits: centred on the edge it is taping.
// --banner-pad-top reserves the banner surface it lands on, so it never reaches the map.
// .tape-h is positioned against .card (it is a direct child), and the banner's top is the
// card's padding-top — hence top:24px, read from the card rather than restated.
'.tape-h{top:var(--card-pad-top,24px);transform:translate(-50%,-50%);' +
'width:82%;padding:4px 8px;border-radius:999px;' +
'display:flex;align-items:center;justify-content:center;gap:5px}' +
'.tape-h:hover{background:#e2e8ee}' +
'.hcount{font-weight:800;font-variant-numeric:tabular-nums}' +
@ -570,19 +618,30 @@ useShadowStyles(
// a vertical one the driver saw. Anchoring at top:100% pins it just under the card, so the
// second line grows DOWN into empty space. MEASURED: 92px between the preview's bottom and
// the next card, of which the single-line row used 35 — 51px spare.
// ★ THE FOOTER IS TAPE TOO (Driver): "see how the header for show on map is inline on the top of
// the poster box? but the footer print options are not inline on the border or outline of the
// box... we want the footer to imitate the header, by being inline like a piece of tape".
// The header does that with .tape-h{top:-11px}: a fixed offset that works because its height is
// fixed. The footer's height is NOT fixed -- it holds two to four controls plus a select, and it
// grows with translated labels -- so a fixed margin-top:-14px left it hanging BELOW the edge
// instead of sitting on it, and any height change moved it again.
// translate(-50%,-50%) at top:100% centres it on the bottom border at ANY height, which is the
// same "piece of tape" the header achieves and is self-correcting when a locale makes it taller.
// That also answers the rest of the Driver's rule -- "if the box is too tall to centre we just
// let it take up an overlap of the poster box, but not so far that it overlaps the poster blue
// outline": half the tape overlaps the card and half hangs below, so the overlap grows only half
// as fast as the tape does and never reaches the preview.
// ★ THE FOOTER STICKER STRADDLES THE BANNER'S BOTTOM EDGE (Driver): "see how the header for
// show on map is inline on the top of the poster box? but the footer print options are not
// inline on the border or outline of the box... we want the footer to imitate the header, by
// being inline like a piece of tape".
//
// top:100% + translate(-50%,-50%) centres it on that edge at ANY height, which matters because
// the row's height is NOT fixed: it holds two to four controls and grows with translated
// labels (MEASURED 68px in en/ro, 54px in zh). A fixed margin-top left it hanging below the
// edge and moved again whenever the height changed. Centring is self-correcting, so no
// constant here has to track the locale.
//
// ⚠ top:100% IS THE BANNER'S BOTTOM EDGE ONLY BECAUSE .pfs LIVES INSIDE .main. As a SIBLING
// of the banner it resolved against the CARD instead — MEASURED card bottom 453 vs banner
// bottom 426, so the row centred on a line 27px past the banner, touching it nowhere. The
// re-parenting and the centring are one fix; neither works alone.
//
// Half the sticker lands on the banner, so the banner must reserve white space for it or the
// sticker sits on the map: that is --banner-pad-bottom on .main, and the gate asserts the
// result (map-covered 0) rather than trusting the arithmetic.
//
// ⚠ DO NOT ADD margin-top HERE. `margin:0` in the shorthand below RESETS any margin-top
// declared above it — a shorthand always beats an earlier longhand in the same block. A
// margin-top:-1px sat here computing to 0px while I believed it was doing the positioning;
// a mutation test (set it to 26px, expect the row to move) proved it inert.
'.pfs{position:absolute;top:100%;left:50%;transform:translate(-50%,-50%);' +
// ⚠ min-width:0 ON THE FIELDSET ITSELF. A <fieldset> gets `min-width:min-content` from the
// UA stylesheet, which OVERRIDES width and max-width — measured: computed width 431px
@ -599,13 +658,30 @@ useShadowStyles(
// of 108 controls clipped. Fitting a row by hiding what its buttons say is not fitting it.
// flex-basis:auto + wrap lets each control take the width its LABEL needs and flow to a
// second line, which there is room for now that the row grows downward.
'.pfs > .pbtn{flex:0 1 auto;min-width:0;white-space:nowrap}' +
// ⚠ THE <select> IS THE ONE THAT WILL NOT SHRINK. Measured: with min-width:0 applied to
// every child, .pstock still rendered 188px inside a 198px row because a <select> sizes to
// its WIDEST OPTION and `flex:0 1 auto` keeps that as its basis. flex-basis:0 makes it share
// the row like the buttons do; a cap keeps it from dominating the three text controls.
'.pfs > .pstock{flex:0 1 auto;min-width:0;max-width:100%}' +
// ⚠ nowrap HERE IS A CLIPPING BUG IN THE NARROW x LONG-LABEL CORNER, and it only appeared once
// the per-row default became 2: at 390px that halves the card to ~170px, and Romanian's
// "Pachet de cartonașe" (133px) plus two more controls cannot share it on one line.
// MEASURED after the merge: 54 of 81 controls clipped at ro/390px, en and zh clean — long
// labels and a narrow card, exactly the pair each change was individually fine with.
// The standing ruling on this wall is that a label too long for its box WRAPS; nowrap forced
// the browser to clip instead. Wrapping costs a second line in the one corner that needs it.
'.pfs > .pbtn{flex:0 1 auto;min-width:0;white-space:normal;overflow-wrap:break-word;' +
'text-align:center;line-height:1.2}' +
'.is-dark .pfs{background:#132019;border-color:rgba(110,231,183,.24)}' +
// ── THE REVEALED SIZE ROW ────────────────────────────────────────────────────
// Sits directly under the print row, still hanging into the grid's 88px row-gap.
// ⚠ THE OFFSET IS MEASURED, NOT GUESSED. Both rows share .main as containing block, so
// "under .pfs" means top:100% + .pfs's own height — and that height is NOT a constant:
// the row wraps to a second line in long-label locales (ro/ru measured at 2 lines).
// A hard-coded 20px would tuck the size row under the print row in exactly those
// locales. toggleDeck() reads the live offsetHeight and writes it to --pstocks-gap.
// Hangs below the footer sticker, which is itself half-overhanging the banner edge — so the
// offset starts at HALF the print row's height, not all of it.
'.pstocks{top:100%;margin-top:calc(var(--pstocks-gap,20px) / 2);width:92%;max-width:92%;' +
'box-sizing:border-box;padding:3px;border-radius:2px;background:#fff;' +
'display:flex;flex-wrap:wrap;gap:3px;justify-content:center;' +
'box-shadow:0 1px 4px rgba(0,0,0,.10);cursor:default}' +
'.is-dark .pstocks{background:#132019;border-color:rgba(110,231,183,.24)}' +
'.pfs > legend{margin:0 auto;padding:0 5px;font-size:8px;text-transform:uppercase;' +
'letter-spacing:.6px;font-weight:700;color:#8a93a0}' +
'.is-dark .pfs > legend{color:#7f9c8c}' +
@ -616,13 +692,27 @@ useShadowStyles(
'border-radius:2px;background:transparent;transition:background .15s}' +
'.pbtn:hover{color:#10b981}' +
'.pbtn:hover::after{background:#10b981}' +
// Stock picker (cardStocks). Sized a shade smaller than .pbtn so it reads as a
// SETTING for the neighbouring Card Deck button rather than a fourth action, and
// `flex:0 1 auto` keeps it from taking an equal share of the row the way .pbtn does.
'.pstock{flex:0 1 auto;max-width:100%;border:1px solid #d8dde3;border-radius:3px;' +
// The deck button is a DISCLOSURE, so it says so while open — a caret that flips,
// plus the green the row already uses for "active", so the reader can see which
// card's sizes are showing without reading aria-expanded.
'.pbtn-deck[aria-expanded="true"]{color:#10b981}' +
'.pbtn-deck::before{content:"\\25B8";display:inline-block;margin-right:3px;font-size:8px;' +
'transition:transform .15s}' +
'.pbtn-deck[aria-expanded="true"]::before{transform:rotate(90deg)}' +
// Size choices. These are BUTTONS, not <option>s — the Driver ruled out a native
// dropdown here because it collides with the other dropdowns on the host page.
// Each one both picks the size and prints, so it is styled as an action.
// ⚠ NO nowrap HERE, and this is a MEASURED correction rather than a preference. With
// white-space:nowrap the two landscape labels rendered 161px inside a 169px row and CLIPPED
// ("Card 3.5 x 2.5 in (landscape)", "Postcard 6 x 4 in (landscape)"). The standing ruling on
// this wall is that a label too long for its box WRAPS — never an ellipsis, never a crop. So
// each size takes a full row of its own when it needs one; flex-wrap on .pstocks gives it the
// line, and min-width:0 lets the text actually break instead of forcing the box open.
'.pstock{flex:0 1 auto;min-width:0;max-width:100%;border:1px solid #d8dde3;border-radius:3px;' +
'background:#fff;color:#5b6672;font:inherit;font-size:9.5px;font-weight:600;' +
'padding:2px 4px;cursor:pointer;line-height:1.25}' +
'padding:2px 5px;cursor:pointer;line-height:1.25;white-space:normal;overflow-wrap:break-word}' +
'.pstock:hover{border-color:#10b981;color:#10b981}' +
'.pstock.is-on{border-color:#10b981;color:#0b7a5b;background:rgba(16,185,129,.10)}' +
// previewAction='print' only. The zoom-in cursor says "this opens something", and
// the focus-visible outline is not optional: it is how a keyboard user sees where
// they are on a control that is otherwise just a picture.
@ -665,6 +755,31 @@ useShadowStyles(
'box-shadow:0 6px 10px -8px rgba(0,0,0,.35);border-radius:4px 4px 0 0}' +
'.is-dark .sec-head{border-bottom-color:rgba(255,255,255,.12);background:#0a1410;' +
'box-shadow:0 6px 10px -8px rgba(0,0,0,.7)}' +
// ★ THE TRANSPARENT STRIP ABOVE A PINNED HEADER (Driver 2026-09-25): "there is a transparent
// section above the header, so when you're scrolling the posters are visible above the header
// ... in the white space between the header and the top of the page."
//
// CAUSE, MEASURED at 1440px: `position:sticky; top:0` pins to the scroll container's PADDING
// BOX, not its border box. .wall has padding-top:22px, so a pinned header comes to rest 22px
// down and the cards keep scrolling through those 22px. Probed at scrollTop 400/900/1600 the
// element painting in that strip was IMG.img every time — literally posters flying past above
// the header, exactly as reported.
//
// WHY A PSEUDO-ELEMENT AND NOT `top:-22px`. Pulling the header up by the padding does pin it
// flush, but sticky clips nothing: the header's own first 22px would then sit outside the
// container and its text would be cut off. And padding-top on the header itself would move the
// text down instead of covering anything. A ::before that extends the SAME opaque fill upward
// covers the strip while leaving both layout and the header's own position untouched — the
// `outline` vs `border` distinction again: paint outside the box, do not re-lay-out.
//
// Height comes from --wall-pad-top (set on .wall), never a second literal 22px.
// Full-bleed on x: the header is inset 2px by its own margin and the strip has to cover the
// container's 18px side padding too, or the posters show through at the edges.
'.sec-head::before{content:"";position:absolute;left:-20px;right:-20px;' +
'bottom:100%;height:var(--wall-pad-top,0px);background:inherit;pointer-events:none}' +
// Nothing to cover when the wall is not the scroller (--wall-pad-top is 0px there), but be
// explicit: an embedded wall must never paint a bar over its host.
'.wall--embedded .sec-head::before{display:none}' +
// A collapsed row is a plain bar — no bottom rule reaching for content that is
// not there.
'.sec-head.is-shut{border-bottom-color:transparent;margin-bottom:6px}' +
@ -684,7 +799,46 @@ useShadowStyles(
'.sec-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums}' +
// Poster aspect is 3ft x 3ft SQUARE — the real print ratio, so the wall previews
// the actual artefact rather than an arbitrary box.
'.main{position:relative;aspect-ratio:1/1;border:1px solid #dfe3e7;overflow:hidden;' +
// ★ THE BANNER IS NOT THE MAP — and until now it effectively was.
// Driver 2026-09-25, the sticker/olive-oil-bottle brief: this box is a BANNER that DISPLAYS
// the map, the way a label displays a design. A banner taped to a wall gets a sticker at the
// top and one at the bottom, each straddling its edge — "slight overlap over the banner, but
// not over the content of the banner... if I put a piece of tape over the text it would be
// offensive to the map."
//
// MEASURED before this change, and it is the whole problem: banner 240-426, map 241-425.
// whiteSpaceTopOfBanner 1px, whiteSpaceBottomOfBanner 1px. The map filled the banner edge to
// edge, so there was NO banner surface for a sticker to sit on — the bottom sticker could
// only ever hang off into space (measured straddle: 1px on, 67px off) or cover the map.
// Both wrong, and no amount of moving the sticker fixes it; the BANNER had to grow.
//
// --banner-pad is that room. .img is object-fit:contain, which fits the CONTENT box, so
// padding reserves banner surface and the map shrinks to sit inside it rather than spilling.
// The bottom gets MORE than the top because the footer sticker is taller than the header one
// and, per the brief, should overhang the page BELOW the banner more than it covers the
// banner itself ("probably an equal or more overlap").
//
// ⚠ aspect-ratio applies to the BORDER box here (box-sizing is border-box in this root), so
// the square is the BANNER's shape, not the map's. The map inside is correspondingly shorter
// than it is wide, which is correct: the banner is the thing being framed.
//
// overflow IS VISIBLE, DELIBERATELY. .pfs lives INSIDE this banner so its top:100% resolves
// here; under `hidden` the print row was clipped to nothing. Nothing else needs the clip:
// .img is object-fit:contain (never paints past its box) and .ph is a smaller flow box.
// ⚠ DERIVED FROM THE STICKER HEIGHTS, NOT CHOSEN — and derived with SLACK, because the
// heights are fractional. A sticker centred on the banner edge puts HALF its height onto the
// banner, so the reserved white space must exceed that half.
// attempt 1: 10px/26px by eye vs 34px/68px stickers -> map covered by 6px and 8px.
// attempt 2: exactly half + 4px (21px/38px) -> STILL 1px of overlap on 27 cards in en and
// ro but NOT zh, where the print row is 54px rather than 68px. The rects are
// fractional (68.x), so half of them rounds past an integer constant; and the
// row's height is LOCALE-DEPENDENT, which no fixed number can track.
// Hence generous slack rather than a tighter constant: 12px past the widest measured half.
// The gate asserts the relationship (map-covered 0), so if a future locale wraps the row to
// three lines this fails loudly instead of quietly shaving the map.
'.main{--banner-pad-top:29px;--banner-pad-bottom:50px;' +
'position:relative;aspect-ratio:1/1;border:1px solid #dfe3e7;overflow:visible;' +
'padding:var(--banner-pad-top) 10px var(--banner-pad-bottom);' +
'background:#fbfcfd;display:flex;align-items:center;justify-content:center}' +
'.is-dark .main{background:#0c1712;border-color:rgba(255,255,255,.08)}' +
// CONTAIN, not cover: `cover` crops to fill, which on a square sheet sliced off
@ -798,26 +952,44 @@ useShadowStyles(
the two beside it. -->
<div class="ph-note">preview unavailable</div>
</div>
</div>
<fieldset class="tape pfs">
<legend>{{ t('buttons.printOptions') }}</legend>
<button v-if="!(hidePrayerListWhenSoon && !canPrintRows)" type="button" class="pbtn"
@click="downloadPacket(item)">{{ t('buttons.prayerList') }}</button>
<!-- CARD DECK — only when this wall can supply rows. A button that
cannot work is worse than an absent one, so it is gated on the
records provider rather than shown and then failing at click. -->
<button v-if="canPrintRows" type="button" class="pbtn"
@click="prints.printCardDeck(item)">{{ t('buttons.cardDeck') }}</button>
<!-- Stock picker, only where the wall offers one AND the deck is printable.
@click.stop so choosing a size does not also fire the card's own
show-on-map handler. Labels come from card-render.js STOCK. -->
<select v-if="canPrintRows && stockOptions.length" class="pstock"
v-model="pickedStock" :aria-label="t('buttons.cardStockAria')" @click.stop>
<option v-for="s in stockOptions" :key="s.id" :value="s.id">{{ s.label }}</option>
</select>
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
</fieldset>
<!-- ★ THE PRINT ROW LIVES INSIDE .main, NOT BESIDE IT (Driver 2026-09-25).
It is `position:absolute; top:100%`, which resolves against the nearest
positioned ANCESTOR. As a SIBLING of .main that ancestor was .card, whose
bottom sits 26px past the frame — measured: frame bottom 426, row top 452.
That 26px of nothing is why it read as a detached row instead of tape.
Moved INSIDE .main, top:100% is the frame's own bottom edge, so the row
starts exactly on the border it is taping over. -->
<fieldset class="tape pfs">
<legend>{{ t('buttons.printOptions') }}</legend>
<button v-if="!(hidePrayerListWhenSoon && !canPrintRows)" type="button" class="pbtn"
@click="downloadPacket(item)">{{ t('buttons.prayerList') }}</button>
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
<!-- ★ CARD DECK IS NOT A PEER OPTION, AND ITS SIZE PICKER IS HIDDEN UNTIL ASKED
FOR (Driver 2026-09-25): "card should not be second option, and I don't want
to add the dropdown for the card because it makes it hard for others to have
dropdowns too... only let you click on the dropdown when you click on print
the cards, which will show you a hidden dropdown to let you choose the print
size option you want."
So the deck is a DISCLOSURE, not a sibling button: clicking it reveals this
card's own size row, and the size buttons there are what actually print.
A 162px <select> was the single widest control in a 172px row — removing it
from the default row is most of why the row now fits on one line. -->
<button v-if="canPrintRows" type="button" class="pbtn pbtn-deck"
:aria-expanded="String(deckOpenFor === item.id)"
@click.stop="toggleDeck(item.id, $event)">{{ t('buttons.cardDeck') }}</button>
</fieldset>
<!-- The revealed size row. Rendered as SIBLINGS in a second tape rather than a
<select> so there is no native dropdown to collide with other dropdowns —
which is the constraint the Driver gave, not a styling preference. -->
<div v-if="canPrintRows && stockOptions.length && deckOpenFor === item.id"
class="tape pstocks" role="group" :aria-label="t('buttons.cardStockAria')">
<button v-for="s in stockOptions" :key="s.id" type="button" class="pstock"
:class="{ 'is-on': pickedStock === s.id }"
@click.stop="printDeckAt(item, s.id)">{{ s.label }}</button>
</div>
</div>
</div>
</div>
</section>

View file

@ -2,5 +2,8 @@
"menuComingSoon": "القائمة قادمة قريباً",
"pinHint": {
"tapDot": "اضغط على نقطة ملوّنة — لا يزال هناك من يحتاج إلى الصلاة"
}
},
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
"upg100WallTag": "تعبئة للصلاة",
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "الدولة",
"upg100Region": "المنطقة",
"upg100Zone": "قوائم UPG-100",
"upg100Group": "مجموعة شعبية"
}
"upg100Group": "مجموعة شعبية",
"selectView": "اختر العرض",
"views": "العروض",
"prayerResources": "موارد الصلاة",
"map": "الخريطة"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Menü folgt in Kürze",
"pinHint": {
"tapDot": "Tippe auf einen farbigen Punkt — jemand braucht noch Gebet"
}
},
"upg100WallHeading": "Meine UPG-100-Liste — Gebetslisten und Poster",
"upg100WallTag": "Gebetsmobilisierung",
"upg100WallIntro": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie <strong>mit dem Namen ihrer Volksgruppe</strong> beten."
}

View file

@ -19,5 +19,9 @@
"country": "Land",
"upg100Region": "Region",
"upg100Zone": "UPG-100-Listen",
"upg100Group": "Volksgruppe"
}
"upg100Group": "Volksgruppe",
"selectView": "Ansicht wählen",
"views": "Ansichten",
"prayerResources": "Gebetsressourcen",
"map": "Karte"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Menu coming soon",
"pinHint": {
"tapDot": "Tap a coloured dot — someone still needs prayer"
}
},
"upg100WallHeading": "My UPG 100 List — Prayer Lists & Posters",
"upg100WallTag": "Prayer Mobilization",
"upg100WallIntro": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them <strong>by their people group name</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "Country",
"upg100Region": "Region",
"upg100Zone": "UPG-100 Lists",
"upg100Group": "People Group"
}
"upg100Group": "People Group",
"selectView": "Select View",
"views": "Views",
"prayerResources": "Prayer Resources",
"map": "Map"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Menú próximamente",
"pinHint": {
"tapDot": "Toca un punto de color: alguien todavía necesita oración"
}
},
"upg100WallHeading": "Mi lista UPG 100 — Listas de oración y carteles",
"upg100WallTag": "Movilización de oración",
"upg100WallIntro": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos <strong>por el nombre de su pueblo</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "País",
"upg100Region": "Región",
"upg100Zone": "Listas UPG-100",
"upg100Group": "Grupo étnico"
}
"upg100Group": "Grupo étnico",
"selectView": "Seleccionar vista",
"views": "Vistas",
"prayerResources": "Recursos de oración",
"map": "Mapa"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Menu bientôt disponible",
"pinHint": {
"tapDot": "Touchez un point coloré — quelqu’un a encore besoin de prière"
}
},
"upg100WallHeading": "Ma liste UPG 100 — Listes de prière et affiches",
"upg100WallTag": "Mobilisation pour la prière",
"upg100WallIntro": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux <strong>par le nom de leur peuple</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "Pays",
"upg100Region": "Région",
"upg100Zone": "Listes UPG-100",
"upg100Group": "Groupe de peuples"
}
"upg100Group": "Groupe de peuples",
"selectView": "Choisir la vue",
"views": "Vues",
"prayerResources": "Ressources de prière",
"map": "Carte"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "मेनू शीघ्र आ रहा है",
"pinHint": {
"tapDot": "किसी रंगीन बिंदु पर टैप करें — किसी को अभी भी प्रार्थना की आवश्यकता है"
}
},
"upg100WallHeading": "मेरी UPG 100 सूची — प्रार्थना सूचियाँ और पोस्टर",
"upg100WallTag": "प्रार्थना लामबंदी",
"upg100WallIntro": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए <strong>उनके जनसमूह के नाम से</strong> प्रार्थना करते हुए।"
}

View file

@ -19,5 +19,9 @@
"country": "देश",
"upg100Region": "क्षेत्र",
"upg100Zone": "UPG-100 सूचियाँ",
"upg100Group": "जनसमूह"
}
"upg100Group": "जनसमूह",
"selectView": "दृश्य चुनें",
"views": "दृश्य",
"prayerResources": "प्रार्थना संसाधन",
"map": "मानचित्र"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Menu in arrivo",
"pinHint": {
"tapDot": "Tocca un punto colorato — qualcuno ha ancora bisogno di preghiera"
}
},
"upg100WallHeading": "La mia lista UPG 100 — Liste di preghiera e poster",
"upg100WallTag": "Mobilitazione alla preghiera",
"upg100WallIntro": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro <strong>con il nome del loro popolo</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "Paese",
"upg100Region": "Regione",
"upg100Zone": "Liste UPG-100",
"upg100Group": "Gruppo etnico"
}
"upg100Group": "Gruppo etnico",
"selectView": "Seleziona vista",
"views": "Viste",
"prayerResources": "Risorse di preghiera",
"map": "Mappa"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Menu em breve",
"pinHint": {
"tapDot": "Toque num ponto colorido — alguém ainda precisa de oração"
}
},
"upg100WallHeading": "A minha lista UPG 100 — Listas de oração e cartazes",
"upg100WallTag": "Mobilização de oração",
"upg100WallIntro": "Reparta a tarefa da oração em grupos de cinquenta e de cem — como Jesus alimentou os 5.000 (Lucas 9:14). Cada cartaz é uma lista de 100 que pode orar, imprimir e pôr na parede — orando por eles <strong>pelo nome do seu povo</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "País",
"upg100Region": "Região",
"upg100Zone": "Listas UPG-100",
"upg100Group": "Grupo étnico"
}
"upg100Group": "Grupo étnico",
"selectView": "Selecionar vista",
"views": "Vistas",
"prayerResources": "Recursos de oração",
"map": "Mapa"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Meniu în curând",
"pinHint": {
"tapDot": "Atinge un punct colorat — cineva are încă nevoie de rugăciune"
}
},
"upg100WallHeading": "Lista mea UPG 100 — Liste de rugăciune și afișe",
"upg100WallTag": "Mobilizare pentru rugăciune",
"upg100WallIntro": "Împarte sarcina rugăciunii în grupuri de câte cincizeci și o sută — așa cum Isus a hrănit cei 5.000 (Luca 9:14). Fiecare afiș este o listă de 100 pe care o poți ruga, tipări și pune pe perete — rugându-te pentru ei <strong>pe numele poporului lor</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "Țară",
"upg100Region": "Regiune",
"upg100Zone": "Liste UPG-100",
"upg100Group": "Grup etnic"
}
"upg100Group": "Grup etnic",
"selectView": "Alege vizualizarea",
"views": "Vizualizări",
"prayerResources": "Resurse de rugăciune",
"map": "Hartă"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "Меню скоро появится",
"pinHint": {
"tapDot": "Нажмите на цветную точку — кому-то всё ещё нужна молитва"
}
},
"upg100WallHeading": "Мой список UPG 100 — молитвенные списки и плакаты",
"upg100WallTag": "Молитвенная мобилизация",
"upg100WallIntro": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них <strong>по имени их народа</strong>."
}

View file

@ -19,5 +19,9 @@
"country": "Страна",
"upg100Region": "Регион",
"upg100Zone": "Списки UPG-100",
"upg100Group": "Народ"
}
"upg100Group": "Народ",
"selectView": "Выбрать вид",
"views": "Виды",
"prayerResources": "Молитвенные ресурсы",
"map": "Карта"
}

View file

@ -2,5 +2,8 @@
"menuComingSoon": "菜单即将推出",
"pinHint": {
"tapDot": "点按一个彩色圆点——仍有人需要祷告"
}
},
"upg100WallHeading": "我的 UPG 100 名單 — 禱告名單與海報",
"upg100WallTag": "禱告動員",
"upg100WallIntro": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — <strong>按著他們族群的名字</strong>為他們禱告。"
}

View file

@ -19,5 +19,9 @@
"country": "国家",
"upg100Region": "区域",
"upg100Zone": "UPG-100 名单",
"upg100Group": "族群"
}
"upg100Group": "族群",
"selectView": "選擇檢視",
"views": "檢視",
"prayerResources": "禱告資源",
"map": "地圖"
}

View file

@ -39,6 +39,7 @@
* IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF.
*/
import { computed, inject, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import PosterWall from '@map/components/poster/PosterWall.vue'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
@ -151,9 +152,20 @@ async function fetchListRecords(list) {
// The wall's own copy. v-html in the shared component, so the <strong> lands as markup —
// the emphasis is the point: praying for them BY NAME is the whole brief.
const intro = 'Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed '
+ 'the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — '
+ 'praying for them <strong>by their people group name</strong>.'
//
// ★ THROUGH t(), NOT A LITERAL. rega measured ?lang=ar and found this intro, the heading and
// the tag rendering in English while the wall's buttons beside them were translated — the
// copy a reader reads FIRST was the untranslated part. The <strong> travels inside the
// translated string so each language can emphasise its own words, which is not something a
// wrapper around an English sentence could do.
// Guarded like every other useI18n() here: it throws on a host without vue-i18n.
const humanizeKey = (k) => String(k).split('.').pop()
.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
let t = (k) => humanizeKey(k)
try { t = useI18n().t } catch (_) { /* no i18n on this host */ }
const intro = computed(() => t('messages.upg100WallIntro'))
const heading = computed(() => t('messages.upg100WallHeading'))
const wallTag = computed(() => t('messages.upg100WallTag'))
</script>
@ -165,8 +177,8 @@ const intro = 'Hand away the prayer task in clusters of fifties and hundreds —
:is-dark="isDark"
:assets-base="assetsBase"
event-name="doxa-upg-100-list:show-on-map"
heading="My UPG 100 List — Prayer Lists &amp; Posters"
tag="Prayer Mobilization"
:heading="heading"
:tag="wallTag"
:intro="intro"
unit="UPG"
item-noun="list"

View file

@ -34,11 +34,25 @@
* whole MFE (both panes) is what gets shared/embedded.
*/
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import ViewSelect from '@map/components/ViewSelect.vue'
import ShareButton from '@map/components/map-controls/ShareButton.vue'
// ★ THE SHELL'S OWN CHROME WAS ENGLISH IN EVERY LOCALE. rega measured ?lang=ar and found
// 'Select View', 'Prayer Resources' and 'Map' rendering in English while the poster wall's own
// buttons inside the panes translated correctly — because the panes each install i18n and this
// wrapper never did. The chrome a reader sees FIRST was the untranslated part.
//
// ⚠ GUARDED, like every other useI18n() call in this codebase: it THROWS on a host that has not
// installed vue-i18n, and this component is mounted by hosts we do not control. The fallback
// humanises the key so a bare host shows "Select View", never a blank or a raw dotted key.
const humanizeKey = (k) => String(k).split('.').pop()
.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
let t = (k) => humanizeKey(k)
try { t = useI18n().t } catch (_) { /* no i18n on this host */ }
const cfg = inject('profileConfig', ref(null))
const tk = inject('mapboxToken', ref(''))
const uiStore = inject('uiStore', null)
@ -66,9 +80,11 @@ const spec = computed(() => ({
// only one of the two desynchronises the tab strip from the scroll column — the
// strip would read "Prayer Resources | Map" while scroll position 0 still held the
// map. Both are swapped here, and `view` below is initialised from the same sort.
// Labels through t(). `spec` is a computed, so switching locale re-labels the tab strip
// rather than needing a remount — which is the behaviour the locale switcher assumes.
panes: [
{ id: 'list', label: 'Prayer Resources', at: [0, 0], config: { profile: 'poster-wall' } },
{ id: 'map', label: 'Map', at: [1, 0], config: { profile: 'upg-100-list' } },
{ id: 'list', label: t('tabs.prayerResources'), at: [0, 0], config: { profile: 'poster-wall' } },
{ id: 'map', label: t('tabs.map'), at: [1, 0], config: { profile: 'upg-100-list' } },
],
// The View switcher lives in this element's own header, so the layout's
// "… below ↓" pill would be a second, redundant control (shell design note 2026-08-01).
@ -222,8 +238,8 @@ useShadowStyles(PANE_GRID_STYLES + `
<ViewSelect
:views="spec.panes"
:current="view"
legend="Select View"
label="Views"
:legend="t('tabs.selectView')"
:label="t('tabs.views')"
@select="go"
/>
<ShareButton :is-dark="isDark" :embed-path="SHARE_EMBED_PATH" />

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -158,7 +158,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "doxa-maps/country-posters/country-posters.js",
"integrity": "sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52",
"integrity": "sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc",
"elements": [
"country-posters-map"
],
@ -178,7 +178,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -192,7 +192,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -206,7 +206,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/4epY9l6XoXC9zO7TRH+ctgitb5yu9+75TCl9wN61uQzvkv2aq7D5RP7mpl2Ou52\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-AKXIPE4uMxP9Nou703e0Sf9fzKVfzjwizcRYnZmQFSKnyidPg+vFffu6LQ6UMCPc\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -681,7 +681,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO",
"integrity": "sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL",
"elements": [
"my-upg-100-list-map"
],
@ -701,7 +701,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -715,7 +715,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -729,7 +729,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -879,7 +879,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX",
"integrity": "sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0",
"elements": [
"regions-dashboard"
],
@ -899,7 +899,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -913,7 +913,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -927,7 +927,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-XFPAGBZYjRapaYMLXiWWqBr9Y9dQusnU+9ycMDgEDI5t397zeVocYIw7oMePtBAL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-q/kWsyT3AKpEKfSRhZcoxRGZ5a/CpLd2DXF3MhbtxWr9g6Jimhfua2I9lsdzTsqX" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-B9L+nAm6/g86Pbb70XLrlhwr/BQcuXRaJ604mhCTfDHzTDtJpcvEQtIEov9zZ1o0" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: