diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md
index c037a3d39..ee6647ff0 100644
--- a/documentation/reference-library-index.md
+++ b/documentation/reference-library-index.md
@@ -88,6 +88,8 @@ and a change here can break other maps.
`import … from '@map/components/map-controls/ZoomOutButton.vue'`
- **RegionCountryMap** — RegionCountryMap.vue — DX-life's CountryMap, rebuilt on the dx-maps map library.
`import … from '@map/components/maps/RegionCountryMap.vue'`
+- **MapPrintProgress** — MapPrintProgress.vue — SHARED. The loading screen of a map print (mapPrintTemplate.js).
+ `import … from '@map/components/poster/MapPrintProgress.vue'`
- **PosterDialog** — PosterDialog.vue — modal for previewing + rendering a poster export.
`import … from '@map/components/poster/PosterDialog.vue'`
- **PosterPreview** — PosterPreview.vue — scaled live preview of the poster composition.
@@ -139,6 +141,8 @@ and a change here can break other maps.
`import … from '@map/composables/legends/useLegendMapBridge.js'`
- **useWagfRegionsLegendData** — useWagfRegionsLegendData — 3-tier WAGF Regions legend for SemanticTreeLegend.
`import … from '@map/composables/legends/useWagfRegionsLegendData.js'`
+- **mapPrintTemplate** — mapPrintTemplate.js — SHARED (@map/composables/poster/mapPrintTemplate.js).
+ `import … from '@map/composables/poster/mapPrintTemplate.js'`
- **useMapPoster** — useMapPoster — render a Mapbox GL map to a poster-grade PNG or PDF blob.
`import … from '@map/composables/poster/useMapPoster.js'`
- **usePosterLayout** — usePosterLayout — slot positioning math for poster compositions.
diff --git a/library/components/info/MapInfo.vue b/library/components/info/MapInfo.vue
index fb54bd61f..c142a251e 100644
--- a/library/components/info/MapInfo.vue
+++ b/library/components/info/MapInfo.vue
@@ -257,7 +257,8 @@ function measureControl() {
shown = r.width > 0 && r.height > 0 && r.bottom > 0 && r.right > 0 && r.top < window.innerHeight && r.left < window.innerWidth
if (shown) {
const n = placeBeside(r), o = ctlAt.value
- if (!n || !o || n.side !== o.side || n.top !== o.top || n.left !== o.left || n.right !== o.right) ctlAt.value = n
+ if (n) n.strip = inStrip(c)
+ if (!n || !o || n.side !== o.side || n.top !== o.top || n.left !== o.left || n.right !== o.right || n.strip !== o.strip) ctlAt.value = n
}
}
controlShown.value = shown
@@ -268,6 +269,10 @@ function measureControl() {
// scrollTop). It goes on the control's physical inner side: a right-to-left map mirrors its column to
// the left edge, and that is read from where the control IS, never assumed from the page's direction.
const GAP = 10
+// A control in a tab's HEADER (MapControlButton's rounded-square form, the header-bar button) sits in the
+// tab's top strip, where the 1200px rule below measured the pointer covering the view switcher, so its
+// pointer takes that rule again (mi-ctl-strip). A control in a map's own column keeps its pointer at every width.
+const inStrip = (el) => el.classList.contains('mcb--rounded-square')
function placeBeside(r) {
const cb = rootEl.value && rootEl.value.offsetParent
if (!cb) return null
@@ -414,7 +419,8 @@ useShadowStyles(`
control's PHYSICAL side, measured, so the page direction must not flip the row, the glyph or the
nudge (the span keeps the text's own direction). (0,3,0) so the 1200px rule below does not hide it:
that rule measured the floating pointer crossing each tab's top strip, and this one is placed and
- measured beside one map's own control instead. */
+ measured beside one map's own control instead. A HEADER control is in that strip, so the rule
+ hides its pointer again at (0,4,0) (mi-ctl-strip). */
.mi .mi-pointer.mi-ctl{ display:flex; direction:ltr; translate:0 -50%; }
.mi .mi-pointer.mi-ctl-left{ flex-direction:row-reverse; animation-name:mi-nudge-left; }
.mi .mi-pointer.mi-ctl-right{ animation-name:mi-nudge; }
@@ -525,6 +531,7 @@ useShadowStyles(`
placement's business; this rule is only about the hint. */
@media (max-width:1199.98px){
.mi-pointer{ display:none; }
+ .mi .mi-pointer.mi-ctl.mi-ctl-strip{ display:none; }
}
/* A phone gets a BOTTOM SHEET, not a full screen. A full-screen dialog showed a few lines of text at the
@@ -573,7 +580,7 @@ useShadowStyles(`
-
+
{{ t('mapInfo.pointer') }}
@@ -30,7 +29,6 @@ const props = defineProps({
})
// Never an empty bar while the renderer warms up: 3% until it reports.
const pct = computed(() => Math.max(3, Math.min(100, Number(props.progress?.percent) || 0)))
-const message = computed(() => String(props.progress?.message || ''))
useShadowStyles(
'.mpp-scrim{position:fixed;inset:0;z-index:1000;background:rgba(8,12,16,.55);display:grid;place-items:center;padding:22px}' +
@@ -40,6 +38,6 @@ useShadowStyles(
'.mpp-title{margin:3px 0 14px;font-size:15px;font-weight:700;line-height:1.3}' +
'.mpp-bar{height:5px;border-radius:99px;background:rgba(0,0,0,.10);overflow:hidden}' +
'.mpp-fill{height:100%;background:#10b981;border-radius:99px;transition:width .25s ease}' +
- '.mpp-phase{margin-top:10px;font-size:12.5px;font-weight:600;min-height:1.2em}'
+ '@media (prefers-reduced-motion: reduce){.mpp-fill{transition:none}}'
)
diff --git a/plugin-designs/doxa-maps-spa/src/tabInfo.js b/plugin-designs/doxa-maps-spa/src/tabInfo.js
index 87bdfab61..cacd0ab7c 100644
--- a/plugin-designs/doxa-maps-spa/src/tabInfo.js
+++ b/plugin-designs/doxa-maps-spa/src/tabInfo.js
@@ -66,6 +66,10 @@ export const TAB_INFO = {
pos: { side: '56px', narrow: { placement: 'top-left', side: '8px' } },
leadKey: 'mapInfo.tab.regions-dashboard.lead',
strategyKey: 'mapStrategy.tab.regions-dashboard',
+ // ⓘ IN THE HEADER, NOT FLOATING (the header rollout, 2026-09-30): the Regions header draws its own rounded-square
+ // ⓘ beside Share (regions-mfe.vue), as Big Picture's does. trigger:'control' hands this MapInfo to it and
+ // suppresses the floating ⓘ while it is on screen; the pos above stays as the fail-open fallback.
+ trigger: 'control',
takeawayKey: 'mapInfo.tab.regions-dashboard.takeaway',
// The note names India as the one country without prints: no sheet, no geometry, and 855 people
// groups, too many to fit one poster legibly (library/data/country-posters.json carries that
diff --git a/plugin-designs/my-upg-100-list/upg-100-list.vue b/plugin-designs/my-upg-100-list/upg-100-list.vue
index 7207c0569..57479e255 100644
--- a/plugin-designs/my-upg-100-list/upg-100-list.vue
+++ b/plugin-designs/my-upg-100-list/upg-100-list.vue
@@ -499,7 +499,13 @@ import { resolveAssetsBase } from '@map/utils/embedBase.js'
// between MapLibre and Mapbox (see openPgDetail).
import { readSourceFeatures } from '@map/maps/pin-selection.js'
useShadowStyles(
- '.upg-root{position:absolute;inset:0;background:#0a1410;color:#e5e7eb;' +
+ // ★ isolation:isolate CONFINES THE MAP AREA'S LAYERS (2026-09-30), the containing layer Simple Research's and the
+ // country map's .rm-root already have. Without it the legend sheet's z-index 1000 competed directly with the
+ // gallery's round ☰ (z 15) in the shell's stacking context and painted over it on the Map view. Isolated,
+ // the whole map area stacks as one layer and the ☰ sits above it, sheet included, as on those maps. It is on
+ // the MAP root only: the poster wall's modal scrim lives in the other pane, outside this layer, and keeps
+ // covering the ☰ (raising the ☰ instead would have put it over that modal).
+ '.upg-root{position:absolute;inset:0;background:#0a1410;color:#e5e7eb;isolation:isolate;' +
"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
'.upg-map{position:absolute;inset:0}' +
// P3-1 graceful degrade: the map-cell error notice. Centred in the map cell and
diff --git a/plugin-designs/regions-dashboard/regions-mfe.vue b/plugin-designs/regions-dashboard/regions-mfe.vue
index bccb01811..1dff0e946 100644
--- a/plugin-designs/regions-dashboard/regions-mfe.vue
+++ b/plugin-designs/regions-dashboard/regions-mfe.vue
@@ -31,6 +31,9 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js'
import ViewSelect from '@map/components/ViewSelect.vue'
import ShareButton from '@map/components/map-controls/ShareButton.vue'
+// The header ⓘ ("What can this map do?"), Big Picture's header recipe: the map-control MapInfoButton in its HEADER
+// form, whose control bridge hands the tab's MapInfo to it (tabInfo.js trigger:'control').
+import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
// The SELECT VIEW strip is one of the sanctioned additions, so DX-life has no equivalent
// markup to clone — and until now its labels were hardcoded English, making it the one piece
// of chrome that never localised (visible at locale=ar: the panes and every heading go RTL
@@ -221,6 +224,16 @@ useShadowStyles(SHADOW_BOX_SIZING_RESET + PANE_GRID_STYLES + '\n' +
// POSITIONING is the caller's job — ViewSelect ships its own metrics but no placement, so the
// absolute centring lives here exactly as it does in the shell and in upg-100-mfe.
'.rmfe .hdr .viewsel{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); }' +
+ // ★ HEADER BUTTONS, Big Picture's recipe (design rule, 2026-09-30): the ⓘ and Share as rounded squares at the
+ // vertical tabs' radius, side by side at the header's end. The SHAPE comes from each button's
+ // shape="rounded-square" prop (--vtl-radius); this header only sizes them to 28 px and keeps a 44 px tap target.
+ '.rmfe .hdr .hdr-btns{ display:inline-flex; align-items:center; gap:8px; flex:0 0 auto; }' +
+ '.rmfe .hdr .hdr-btns .mcb{ --map-btn-size:28px; position:relative; }' +
+ '.rmfe .hdr .hdr-btns .mcb svg{ width:16px; height:16px; }' +
+ '.rmfe .hdr .hdr-btns .mcb::after{ content:""; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:44px; height:44px; }' +
+ // ON A PHONE the centred view switcher (about 290 px at 390) leaves no room for two buttons at one end, so the
+ // ⓘ takes the header's START edge, where the floating ⓘ sat before; Share keeps the end.
+ '@media (max-width: 600px){ .rmfe .hdr .hdr-btns > :first-child{ position:absolute; inset-inline-start:12px; top:50%; transform:translateY(-50%); } }' +
// Stage: the pane-wrapper column mounts here (its .pane-col is absolute).
'.rmfe .stage{ position:relative; flex:1 1 auto; min-height:0; }' +
'.pane > .pane-mount > *{ position:absolute; inset:0; width:100%; height:100%; display:block; }',
@@ -238,7 +251,10 @@ useShadowStyles(SHADOW_BOX_SIZING_RESET + PANE_GRID_STYLES + '\n' +
:label="t('Views')"
@select="go"
/>
-
+