diff --git a/internal/check-selection-kinds.mjs b/internal/check-selection-kinds.mjs
index 6022e3a53..9b0833545 100644
--- a/internal/check-selection-kinds.mjs
+++ b/internal/check-selection-kinds.mjs
@@ -166,6 +166,27 @@ for (const fn of ['onLegendExport', 'makeSelectionPacket', 'makeSelectionPoster'
}
}
+// ── 4. A TAPPED FEATURE RESOLVES AT ITS REAL LEVEL, FROM ITS REAL PROPERTIES ──────────────────────
+// Sections 1-2 start from hand-made KEYS ('adm0:IND'), so they could not see the resolver itself
+// misfile a country: once the world ADM0 boundaries carried a shapeName, "shapeName → ADM1" turned
+// every tapped country into a province (key 'adm1:Sudan', count null, 0 UUPG), and the Prayer List
+// refused it (reported from a phone, 2026-09-30). These are the properties the map ACTUALLY hands the
+// click handler (read off the live tapestry), so a resolver that keys on the wrong field fails here.
+{
+ const { resolveTerritory, COUNTRY_COUNTS, ADM1_COUNTS } = await import(resolve(ROOT, 'plugin-designs/my-upg-100-list/data/territory-counts.js'))
+ const country = resolveTerritory({ shapeGroup: 'SDN', shapeName: 'Sudan', iso3: 'SDN', zoneKey: 'sudan', zoneLabel: 'Sudan' })
+ ok(country && country.kind === 'adm0' && country.key === 'adm0:SDN',
+ `a tapped COUNTRY (shapeName + iso3, no listId) resolved as ${JSON.stringify(country)}, not adm0:SDN — every country tap would count 0 UUPG`)
+ ok(country && country.count === COUNTRY_COUNTS.SDN && country.count > 0,
+ `a tapped country's count is ${country && country.count}, not its COUNTRY_COUNTS entry (${COUNTRY_COUNTS.SDN})`)
+ const state = resolveTerritory({ iso3: 'IND', shapeGroup: 'IND', listId: 'asia-india-madhya-pradesh', shapeName: 'Madhya Pradesh', zoneKey: 'asia-india-madhya-pradesh' })
+ ok(state && state.kind === 'adm1' && state.key === 'adm1:Madhya Pradesh' && state.count === ADM1_COUNTS['Madhya Pradesh'],
+ `a tapped India STATE (listId + shapeName + iso3) resolved as ${JSON.stringify(state)}, not adm1 with its ADM1_COUNTS entry`)
+ const nepal = resolveTerritory({ shapeGroup: 'NPL', shapeName: 'Nepal', iso3: 'NPL', zoneKey: 'asia-india-uttarakhand' })
+ ok(nepal && nepal.kind === 'adm0' && nepal.key === 'adm0:NPL',
+ `a country drawn inside an Asia zone (zone.adm0, no listId) resolved as ${JSON.stringify(nepal)}, not adm0:NPL`)
+}
+
if (fails.length) {
console.error('SELECTION-KINDS: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)
diff --git a/library/components/info/MapInfo.vue b/library/components/info/MapInfo.vue
index 2e5285e7f..75d080f30 100644
--- a/library/components/info/MapInfo.vue
+++ b/library/components/info/MapInfo.vue
@@ -478,9 +478,19 @@ useShadowStyles(`
.mi-pointer{ display:none; }
}
+ /* A phone gets a BOTTOM SHEET, not a full screen. A full-screen dialog showed a few lines of text at the
+ top of an empty white page, and read as a new page rather than a panel over the map. The sheet sizes to
+ its content up to 70% of the SMALL viewport (svh: a collapsing URL bar must not resize it), scrolls
+ inside with its scroll kept to itself, keeps a 44px close button in view, and leaves the dimmed map
+ above it, where a tap closes it (the scrim's @click.self), like Escape does. */
+ @media (max-width:767.98px){
+ .mi-scrim{ padding:0; align-items:flex-end; }
+ .mi-dialog{ width:100%; max-height:70vh; max-height:70svh; border-radius:16px 16px 0 0; }
+ .mi-head{ align-items:center; padding:10px 10px 10px 20px; padding-inline:20px 10px; }
+ .mi-close{ width:44px; height:44px; }
+ .mi-body{ overscroll-behavior:contain; padding-bottom:calc(20px + env(safe-area-inset-bottom, 0px)); }
+ }
@media (max-width:600px){
- .mi-scrim{ padding:0; align-items:stretch; }
- .mi-dialog{ width:100%; max-height:none; height:100%; border-radius:0; }
.mi-pointer{ font-size:12px; padding:7px 10px; }
.mi-deck{ height:220px; }
.mi-deck .mi-card{ width:136px; height:190px; }
diff --git a/library/layouts/VerticalTabsLayout.vue b/library/layouts/VerticalTabsLayout.vue
index 1989dc3e4..d6fbc02db 100644
--- a/library/layouts/VerticalTabsLayout.vue
+++ b/library/layouts/VerticalTabsLayout.vue
@@ -103,7 +103,7 @@
sit NEXT TO the circle rather than on top of it (12px inset + 44px button + 8px gap). A
consumer that hardcoded 64px would silently overlap the day the circle resizes; reading it
means the circle's geometry is stated once, here, by the component that owns it. -->
-