diff --git a/library/composables/printInPlace.js b/library/composables/printInPlace.js
new file mode 100644
index 000000000..eb95fe4fb
--- /dev/null
+++ b/library/composables/printInPlace.js
@@ -0,0 +1,47 @@
+/**
+ * printInPlace.js — SHARED (@map/composables/printInPlace.js).
+ *
+ * ★ THE ONE print mechanism, graduated VERBATIM from my-upg-100-list/poster-wall.vue
+ * openPrintWindow() (commit e957cbe "Print the poster in place, on a portrait sheet") on the
+ * Driver's ruling that killed the pop-up path: print must happen IN PLACE — no new browser
+ * window, no pop-up blocker, the dialog rises over the page the user is looking at.
+ *
+ * WHY IT GRADUATES (library high bar: reusable + parameterized, 2+ consumers): the
+ * prayer-walk-planner's print still used window.open(), and that is not just the rejected UX --
+ * it is AGENT-HOSTILE. window.open() without a user gesture is pop-up-blocked, so
+ * PrayerWalkPlanner.print() called from an agent (the Driver: "the agent can also create
+ * polygons and trigger the print function") silently did nothing. An iframe cannot be blocked
+ * and needs no gesture, which is what makes the same call work for a human click AND an agent.
+ *
+ * Mechanics preserved from the poster-wall original, each for a measured reason:
+ * - OFF-SCREEN 1x1, not display:none — a display:none iframe does not lay out in every
+ * engine, and a zero-height layout can print a blank first page.
+ * - autoprint appends the print bootstrap AFTER the caller's markup so it runs once the
+ * document is parsed (matches timing).
+ * - afterprint + a 60s floor reclaim the frame, so repeated prints never leak nodes.
+ *
+ * Returns the frame's contentWindow, or null when the host is sandboxed/detached (caller
+ * keeps its own fallback).
+ */
+export function openPrintFrame(bodyHtml, { autoprint = false } = {}) {
+ const frame = document.createElement('iframe')
+ frame.setAttribute('aria-hidden', 'true')
+ frame.style.cssText = 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0'
+ document.body.appendChild(frame)
+
+ const doc = frame.contentDocument
+ if (!doc) { frame.remove(); return null }
+ doc.open()
+ doc.write(bodyHtml)
+ if (autoprint) {
+ doc.write(
+ '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ c.label }}
+
+
+
+
+
+
+
+ {{ filtered.length.toLocaleString('en-US') }} people groups in {{ countryName }}
+
+
+
+ People groups could not be loaded. This is a connection problem, not a filter.
+