diff --git a/plugin-designs/gospel-resources/data-tables.vue b/plugin-designs/gospel-resources/data-tables.vue
index a86c13686..abf92053a 100644
--- a/plugin-designs/gospel-resources/data-tables.vue
+++ b/plugin-designs/gospel-resources/data-tables.vue
@@ -77,6 +77,21 @@ function clearFilter() {
const active = ref(TABS.some((t) => t.id === params.get('tab')) ? params.get('tab') : 'links')
const current = computed(() => TABS.find((t) => t.id === active.value) || TABS[0])
+// ═══ COMPOSED MODE ═══ when these tables are stacked under the map in the dashboard, the
+// map asks them to filter in place instead of linking to this page. Null on the standalone
+// page, where the URL remains the only source of a filter and nothing here changes.
+const composedFilter = inject('composedFilter', null)
+if (composedFilter) {
+ watch(composedFilter, (f) => {
+ if (!f) return
+ fIso.value = String(f.iso || '').trim().toLowerCase()
+ fCountry.value = String(f.country || '').trim().toUpperCase()
+ fLabel.value = String(f.label || '').trim()
+ // The links tab is the one that carries URLs; a filter is meaningless on the others.
+ if (TABS.some((t) => t.id === 'links')) active.value = 'links'
+ })
+}
+
// Fetched at runtime — see the note in cities-internet/data-tables.vue. These two
// tables alone are 2.8MB of JSON; inlining them blocked first paint.
const cache = ref({})
diff --git a/plugin-designs/gospel-resources/gospel-dashboard.vue b/plugin-designs/gospel-resources/gospel-dashboard.vue
index 21b04770d..ccbb72617 100644
--- a/plugin-designs/gospel-resources/gospel-dashboard.vue
+++ b/plugin-designs/gospel-resources/gospel-dashboard.vue
@@ -30,6 +30,20 @@ import StackLayout from '@map/layouts/StackLayout.vue'
import ResourcesMap from './resources-map.vue'
import DataTables from './data-tables.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
+import { ref, provide } from 'vue'
+
+// ═══ THE TWO HALVES TALK TO EACH OTHER ═══════════════════════════════════════════════
+// "Every resource in " is a LINK to the standalone tables page, which is right
+// when the map is alone. Inside this composite the tables are already on screen, directly
+// below, so following that link navigated AWAY from the dashboard — the filter applied
+// correctly (6 of 6,498 for German) but the map was gone and the composite destroyed.
+// Measured: the URL changed from gospel-dashboard/index.html to data-tables/index.html.
+//
+// Provided rather than propped because the two children are separate profiles that must
+// each still work standalone: a child that finds nothing injected keeps its old behaviour
+// exactly, so nothing changes for resources-map or data-tables on their own pages.
+const composedFilter = ref(null)
+provide('composedFilter', composedFilter)
const sections = [
{ key: 'map', title: '' },
diff --git a/plugin-designs/gospel-resources/resources-map.vue b/plugin-designs/gospel-resources/resources-map.vue
index e4bbc8025..e7d946154 100644
--- a/plugin-designs/gospel-resources/resources-map.vue
+++ b/plugin-designs/gospel-resources/resources-map.vue
@@ -709,6 +709,22 @@ const tableLink = (q) => {
return `../data-tables/index.html?${p.toString()}`
}
+// When this map is composed ABOVE its own tables, "see all of these" must not be a
+// navigation — the tables are already on screen and leaving the page throws the map away.
+// Null on the standalone page, where the link is exactly right.
+const composedFilter = inject('composedFilter', null)
+const composed = computed(() => !!composedFilter)
+function filterComposedTables(q) {
+ if (!composedFilter) return
+ composedFilter.value = { ...q, at: Date.now() }
+ // Bring the tables into view; without this the filter applies somewhere the reader
+ // cannot see and the click reads as having done nothing.
+ const host = document.querySelector('gospel-resources-dashboard, gospel-resources-map')
+ const root = host && host.shadowRoot
+ const tables = root && root.querySelector('.gd-tables')
+ if (tables && tables.scrollIntoView) tables.scrollIntoView({ behavior: 'smooth', block: 'start' })
+}
+
useShadowStyles(
'.gm{position:absolute;inset:0;overflow:hidden}' +
'.gm-map{position:absolute;inset:0}' +
@@ -1210,8 +1226,14 @@ useShadowStyles(