diff --git a/internal/check-cdn-pins.mjs b/internal/check-cdn-pins.mjs
new file mode 100644
index 000000000..20ff43da6
--- /dev/null
+++ b/internal/check-cdn-pins.mjs
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+/**
+ * check-cdn-pins — every runtime third-party script/stylesheet is an EXACT version with an integrity hash.
+ *
+ * CRITIC-5 (2026-09-26): maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime
+ * loader set `integrity`, so a CDN could serve any bytes to every partner's visitors unnoticed.
+ * library/constants/cdn.js is now the one registry; loaders set integrity + crossorigin from it.
+ *
+ * OFFLINE (default, in gates:post): in shipped code (library/, plugin-designs/ except scripts/,
+ * internal/generate-tree.mjs) no jsDelivr/unpkg/esm.sh/api.mapbox.com JS or CSS URL is a floating
+ * range (@7, @1.2, latest), and every such URL is either in the registry or on the reviewed list below.
+ * --online: also re-fetches every registry URL and requires its sha384 to equal the pinned hash.
+ * EXIT 0 pass · 1 fail · 75 (online) network unavailable
+ */
+import { execFileSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import { resolve, dirname } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { CDN } from '../library/constants/cdn.js'
+
+const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+const ONLINE = process.argv.includes('--online')
+// Loaded through something other than a `
+
+ `
: `\n\n` + scriptEmbed
+ ? `\n\n` + scriptEmbed
: scriptEmbed,
},
...(instances.length ? { instances } : {}),
diff --git a/library/components/map-controls/GeocoderComponent.vue b/library/components/map-controls/GeocoderComponent.vue
index 08388ff66..0df933cc5 100644
--- a/library/components/map-controls/GeocoderComponent.vue
+++ b/library/components/map-controls/GeocoderComponent.vue
@@ -62,6 +62,7 @@
geocoder — the raw MapboxGeocoder instance (for advanced usage)
-->