diff --git a/internal/check-hardcoded-english.mjs b/internal/check-hardcoded-english.mjs
new file mode 100644
index 00000000..820bc13b
--- /dev/null
+++ b/internal/check-hardcoded-english.mjs
@@ -0,0 +1,514 @@
+#!/usr/bin/env node
+/**
+ * check-hardcoded-english — user-visible English that never reaches a translator. Headless.
+ *
+ * ONE GATE INSTEAD OF FOUR PRIVATE SCANNERS. sr, upg100, gospel and regions each wrote one of these
+ * on 2026-09-28 and each found real defects; four copies means four sets of blind spots and four
+ * allow-lists drifting apart. This is the merge, and every rule below earns its place from a defect
+ * one of those scanners actually caught.
+ *
+ * ★ PARSED, NEVER REGEXED. A regex for string literals matches from the CLOSING quote of one to the
+ * OPENING quote of the next, so a file with adjacent imports yields phantom "strings" — the first
+ * version of regions' scanner reported 137 of them. Templates go through @vue/compiler-sfc and
+ * scripts through @babel/parser.
+ *
+ * ★ ALIASES ARE DISCOVERED, NOT LISTED. `const t2 = t` defeats a callee-name check, and that cost a
+ * false positive. The alias set is read out of each file (`const X = t`), so a rename cannot make
+ * translated text look hardcoded.
+ *
+ * ★ te()-GUARDED FALLBACKS ARE NOT LEAKS. `te(key) ? t(key) : 'Some English'` is the documented way
+ * to fall back when a key is absent; the English is reachable only when no translation exists. A
+ * literal inside a conditional whose test calls te() is therefore allowed (sr's pattern).
+ *
+ * ⚠ SECOND BLIND SPOT, AND IT BIT ON THE FIRST DAY. A guarded fallback is judged by its SHAPE, not
+ * by whether the lookup it guards actually works. `msg !== 'messages.engineMissing' ? msg : 'English'`
+ * is treated as a fallback — but if the key is looked up with the wrong prefix, the lookup ALWAYS
+ * misses and the English arm becomes the NORMAL path, in every locale, while this gate stays quiet.
+ * That is exactly what exp-shepherd found in cities-internet after I triaged the same line as "not a
+ * leak". A hardcoded-English scan cannot see a misconfigured lookup; check:i18n-keys-exist and a
+ * locale render are what catch that. Treat a guarded fallback as "not a leak HERE", never as "this
+ * string is not reaching users".
+ *
+ * ⚠ KNOWN BLIND SPOT, STATED SO NOBODY READS A GREEN RUN AS MORE THAN IT IS: THIS CANNOT SEE
+ * SINGLE-WORD STRINGS. A bare 'Other' used as a fallback label is user-visible English and this gate
+ * will not report it — regions found eight of them by grep, not with this tool. Single words carry
+ * too many non-prose meanings (ids, class fragments, enum values) to flag without drowning the
+ * signal. If that trade is ever revisited, the missing piece is context (what the value is ASSIGNED
+ * to), not a longer word list.
+ *
+ * Exit 0 clean, 1 with findings, 2 on a parse error. `--self-test` runs the planted cases only.
+ */
+import { readFileSync, readdirSync, existsSync } from 'node:fs'
+import { join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { parse } from '@vue/compiler-sfc'
+import * as babel from '@babel/parser'
+
+// ── what counts as user-visible ─────────────────────────────────────────────────────────────────
+const VISIBLE_ATTRS = new Set(['title', 'placeholder', 'aria-label', 'alt', 'label'])
+const isProse = (s) => /[A-Za-z]/.test(s) && /[A-Za-z]{2,}/.test(s.trim())
+
+// ── the allow-list: SHAPES first, names only where a shape cannot express it ─────────────────────
+// Each entry records the class and why it is not a leak. A shape is preferred over a literal so the
+// list does not need editing every time a new font or status code appears.
+const ALLOW = [
+ { why: 'CSS: declarations, selectors, at-rules — read by the DOM, not by a person',
+ test: (s) => /[{}]/.test(s) || /[a-z-]+\s*:\s*[^;]+;/.test(s) || /^\s*@(media|supports|keyframes|font-face)/.test(s) || /^\s*[.#][a-zA-Z]/.test(s) || /^\s*\[[a-z-]+[~|^$*]?=/.test(s) },
+ { why: 'HTML fragments: these plugins build popup markup in template literals, so a quasi is often '
+ + 'a tag, or the seam between two attributes ("\" alt=\"" / "\" loading=\"lazy\">")',
+ test: (s) => /<\/?[a-zA-Z][^>]*>?/.test(s)
+ || /\b(class|href|src|alt|loading|width|height|style|id|target|rel)\s*=/.test(s)
+ || /^["'>\s]*$/.test(s) || /^<\/|^["']?\s*>/.test(s) },
+ { why: 'URLs and url fragments, absolute OR relative (gospel: a relative URL was reported as prose)',
+ test: (s) => /^(https?:|\/\/|\/|\?|&|#)/.test(s)
+ || /^\.{0,2}\/[^\s]*$/.test(s)
+ || (/^[^\s]+\.(json|js|css|svg|png|jpe?g|webp|geojson|csv|html?)$/i.test(s)) },
+ { why: 'CSS class/id SUFFIXES built by interpolation (gospel: `${x}-fill`, `${x}-line` read as prose)',
+ test: (s) => /^[-_][a-z0-9][a-z0-9-]*$/.test(s) },
+ { why: 'custom-element tag names: lowercase and hyphenated, a tag is not a sentence',
+ test: (s) => /^\/?[a-z][a-z0-9]*(-[a-z0-9]+)+>?$/.test(s) },
+ { why: 'BCP-47 locale extensions and language tags (e.g. the latin-digits override)',
+ test: (s) => /^-?[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(s) || /-u-[a-z]{2}-/.test(s) },
+ { why: 'developer error text: a bracketed module prefix or an HTTP status, for the console',
+ test: (s) => /^\[[a-z0-9-]+\]/i.test(s) || /^HTTP\b/.test(s) },
+ { why: 'DOM id / class / data-key fragments: hyphenated or BEM identifiers, alone or as a class LIST',
+ // Every token must carry a separator. That is what tells `rd-card is-open` from `the map`:
+ // prose words do not contain hyphens, and this codebase's classes always do. A class list with
+ // no hyphen anywhere (`card open`) is deliberately NOT allowed — flagging is the safe direction.
+ // A trailing separator is allowed because that is the shape of an id PREFIX built by
+ // interpolation — `id="country-${code}"` leaves the quasi "country-".
+ test: (s) => s.split(/\s+/).every((tok) => /^[a-z][a-z0-9]*(?:[-_]{1,2}[a-z0-9]*)+$/.test(tok)) },
+ { why: 'font family names: typeface identifiers, never translated',
+ test: (s) => /^(Bebas Neue|Poppins|Oswald|Figtree|Segoe UI|Helvetica Neue|Times New Roman)$/.test(s) },
+ { why: 'engine/provider tokens and well-known proper nouns that are not prose',
+ test: (s) => /^(mapbox|maplibre|mercator|globe|Mapbox|MapLibre)$/.test(s) },
+]
+const allowedBy = (s) => ALLOW.find((a) => a.test(s))
+
+// ── localisation calls: base names plus aliases discovered per file ──────────────────────────────
+function localizedNames(code) {
+ const names = new Set(['t', 'tf', '$t'])
+ for (const m of code.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*(t|tf)\b\s*(?![(.])/g)) names.add(m[1])
+ return names
+}
+// A literal that is an argument to one of these never reaches a reader, whatever it says.
+function isDeveloperSite(parent) {
+ if (!parent) return false
+ if (parent.type === 'CallExpression' && parent.callee?.object?.name === 'console') return true
+ if (parent.type === 'NewExpression' && /^[A-Z]\w*Error$|^Error$/.test(parent.callee?.name || '')) return true
+ if (parent.type === 'CallExpression' && /^[A-Z]\w*Error$|^Error$/.test(parent.callee?.name || '')) return true
+ return false
+}
+const calleeName = (node) => node?.callee?.type === 'Identifier' ? node.callee.name
+ : node?.callee?.type === 'MemberExpression' ? (node.callee.property?.name || '') : ''
+
+// ── CROSS-AREA KEYS THROUGH A NAMESPACED TRANSLATOR ─────────────────────────────────────────────
+// ★ A REAL BUG, FOUND TWICE IN ONE DAY (pplr a56d06c8, cities-internet 7b3fb78d5), that no gate saw.
+// `const t = useOptionalT('connectivity')` then `t('messages.engineMissing')` — qualify() makes that
+// `connectivity.messages.engineMissing`, which cannot exist, so the English fallback is the NORMAL
+// path in every locale. check:i18n-keys-exist passes it because the key AS WRITTEN does exist; the
+// key as QUALIFIED is what misses. The user-visible outcome is identical to a hardcoded string,
+// which is why it belongs in this gate: a reader sees English where their language was promised.
+// Detected by SHAPE — qualified key absent AND unqualified key present — so it cannot fire on an
+// ordinary missing key, which is check:i18n-keys-exist's job, not this one.
+const qualifyKey = (ns, key) => (ns && !key.startsWith(`${ns}.`) ? `${ns}.${key}` : key)
+
+let CATALOGUE = null
+function catalogue(root = '.') {
+ if (CATALOGUE) return CATALOGUE
+ CATALOGUE = {}
+ const dir = join(root, 'library/i18n/locales/en')
+ if (!existsSync(dir)) return CATALOGUE
+ for (const f of readdirSync(dir)) {
+ if (!f.endsWith('.json')) continue
+ try { CATALOGUE[f.replace(/\.json$/, '')] = JSON.parse(readFileSync(join(dir, f), 'utf8')) } catch { /* skip */ }
+ }
+ return CATALOGUE
+}
+// `a.b.c` resolves as area `a` then path b.c inside it — the shape library/i18n/index.js builds.
+function resolveKey(key, root = '.') {
+ const cat = catalogue(root)
+ const [area, ...rest] = String(key).split('.')
+ let node = cat[area]
+ if (node === undefined) return undefined
+ for (const seg of rest) {
+ if (node === null || typeof node !== 'object') return undefined
+ node = node[seg]
+ if (node === undefined) return undefined
+ }
+ return node
+}
+
+// ★ WRAPPER FACTORIES CARRY THE NAMESPACE ONE INDIRECTION AWAY, and missing that is why the first
+// version of this check caught cities-internet but NOT pplr — the bug it was written for. pplr does
+// `const t = usePplrT()` with NO argument, and usePplrT() is `return useOptionalT('pplr')`. A check
+// that only reads a literal argument sees no namespace and stays silent on the exact defect twice
+// over. So wrappers are resolved from source first.
+let WRAPPERS = null
+function wrapperNamespaces(root = '.') {
+ if (WRAPPERS) return WRAPPERS
+ WRAPPERS = {}
+ const roots = [join(root, 'library/composables'), join(root, 'plugin-designs')]
+ const visit = (d, depth = 0) => {
+ if (depth > 4 || !existsSync(d)) return
+ for (const e of readdirSync(d, { withFileTypes: true })) {
+ const f = join(d, e.name)
+ if (e.isDirectory()) { if (!/node_modules|dist/.test(e.name)) visit(f, depth + 1); continue }
+ if (!e.name.endsWith('.js')) continue
+ let src = ''
+ try { src = readFileSync(f, 'utf8') } catch { continue }
+ for (const m of src.matchAll(/export\s+function\s+(use[A-Za-z]*T)\s*\([^)]*\)\s*\{[^}]*useOptionalT\s*\(\s*['"]([^'"]+)['"]/g)) {
+ WRAPPERS[m[1]] = m[2]
+ }
+ }
+ }
+ roots.forEach((r) => visit(r))
+ return WRAPPERS
+}
+
+// `const t = useOptionalT('connectivity')` → { t: 'connectivity' }
+// `const t = usePplrT()` → { t: 'pplr' }, via the wrapper map above
+function namespacedTranslators(code, root = '.') {
+ const map = {}
+ for (const m of code.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*use[A-Za-z]*T\s*\(\s*['"]([^'"]+)['"]/g)) map[m[1]] = m[2]
+ const wrappers = wrapperNamespaces(root)
+ for (const m of code.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*(use[A-Za-z]*T)\s*\(\s*\)/g)) {
+ if (wrappers[m[2]]) map[m[1]] = wrappers[m[2]]
+ }
+ return map
+}
+
+export function crossAreaKeys(code, file, root = '.') {
+ const out = []
+ const ns = namespacedTranslators(code, root)
+ if (!Object.keys(ns).length) return out
+ let ast
+ try { ast = babel.parse(code, { sourceType: 'module', plugins: ['jsx'], errorRecovery: true }) } catch { return out }
+ walkAst(ast, (node) => {
+ if (node.type !== 'CallExpression') return
+ const name = node.callee?.type === 'Identifier' ? node.callee.name : ''
+ if (!ns[name]) return
+ const arg = node.arguments?.[0]
+ if (arg?.type !== 'StringLiteral' || !arg.value.includes('.')) return
+ const qualified = qualifyKey(ns[name], arg.value)
+ if (resolveKey(qualified, root) !== undefined) return // resolves: fine
+ if (resolveKey(arg.value, root) === undefined) return // plain missing key: keys-exist's job
+ out.push({ file, kind: 'cross-area-key', text: `${name}('${arg.value}') → ${qualified} (never resolves; '${arg.value}' does)` })
+ })
+ return out
+}
+
+// ── the walk ────────────────────────────────────────────────────────────────────────────────────
+function walkAst(node, visit, parent = null, stack = []) {
+ if (!node || typeof node.type !== 'string') return
+ visit(node, parent, stack)
+ const next = [...stack, node]
+ for (const k of Object.keys(node)) {
+ if (k === 'loc' || k === 'leadingComments' || k === 'trailingComments') continue
+ const v = node[k]
+ if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') walkAst(c, visit, node, next) }
+ else if (v && typeof v.type === 'string') walkAst(v, visit, node, next)
+ }
+}
+// A literal is a GUARDED FALLBACK — reachable only when a translation is absent — in two shapes:
+// · te(key) ? t(key) : 'English' the documented existence check (sr)
+// · t.search || 'Search…' a PROXY t whose member read misses (people-groups-grid)
+// Both mean "the catalogue had nothing", so the English is the designed last resort rather than a
+// string that skipped translation. Ancestors are walked, not just the immediate parent, because the
+// fallback is often nested (`a ?? (te(k) ? t(k) : 'X')`).
+function guardedFallback(stack, names) {
+ return stack.some((n) => {
+ if (n.type !== 'ConditionalExpression' && n.type !== 'LogicalExpression') return false
+ let found = false
+ walkAst(n.test || n.left, (x) => {
+ if (x.type === 'CallExpression' && /^te$/.test(calleeName(x))) found = true
+ // a member read on a localisation object or alias: t.search, i18n.t.search
+ if (x.type === 'MemberExpression' && x.object?.type === 'Identifier' && names.has(x.object.name)) found = true
+ // THE "DID t() RETURN THE KEY?" IDIOM. vue-i18n returns the key itself when it is missing, so
+ // `msg !== 'messages.engineMissing' ? msg : 'English'` is a miss check and its English arm is a
+ // fallback. Matched on the KEY SHAPE (dotted, no spaces) rather than on any string comparison,
+ // so an ordinary `x !== 'yes'` is not mistaken for one. (cities-internet, triaged 2026-09-28.)
+ if (x.type === 'BinaryExpression' && /^[!=]==?$/.test(x.operator)) {
+ for (const side of [x.left, x.right]) {
+ if (side?.type === 'StringLiteral' && /^[a-z][\w-]*(\.[\w-]+)+$/.test(side.value)) found = true
+ }
+ }
+ })
+ return found
+ })
+}
+
+function scanScript(code, file, out) {
+ const names = localizedNames(code)
+ const ast = babel.parse(code, { sourceType: 'module', plugins: ['jsx'], errorRecovery: true })
+ walkAst(ast, (node, parent, stack) => {
+ const isStr = node.type === 'StringLiteral'
+ const isTpl = node.type === 'TemplateLiteral'
+ if (!isStr && !isTpl) return
+ if (parent && parent.type === 'CallExpression' && names.has(calleeName(parent))) return // t('…')
+ // ★ DEVELOPER STRINGS ARE EXEMPT BY CALL SITE, NOT BY CONTENT (upg100). `throw new Error('…')`
+ // and console.* never reach a reader, and judging them by their text means inventing a rule for
+ // every sentence a developer might write. The SITE is the fact; the wording is not.
+ if (isDeveloperSite(parent)) return
+ if (parent && /Import|Export/.test(parent.type)) return
+ if (parent && parent.type === 'TaggedTemplateExpression') return
+ if (guardedFallback(stack, names)) return
+
+ if (isStr) {
+ const text = node.value.trim()
+ if (!text.length || !isProse(text)) return // gospel: empty matches were being reported
+ if (!/\s/.test(text)) return // ⚠ the single-word blind spot, documented above
+ if (allowedBy(text)) return
+ out.push({ file, kind: 'script-string', text })
+ return
+ }
+ // ★ ONE TEMPLATE LITERAL IS ONE FINDING (upg100). Reporting each quasi separately turned a single
+ // string into 'polys ·', 'zones ·', 'px;' — three hits for one defect, which inflates every count
+ // and makes a triage list read as worse than the code is. Each quasi is still JUDGED on its own,
+ // so an allow-listed fragment cannot drag a whole literal in; only the REPORT is joined.
+ const quasis = node.quasis.map((q) => String(q.value.cooked ?? q.value.raw ?? ''))
+ const leaks = quasis.some((raw) => {
+ const text = raw.trim()
+ return text.length && isProse(text) && !allowedBy(text)
+ })
+ if (!leaks) return
+ const whole = quasis.map((q) => q.trim()).filter(Boolean).join(' {…} ').trim()
+ out.push({ file, kind: 'template-literal', text: whole })
+ })
+}
+
+// ★ BOUND ATTRIBUTES HIDE LEAKS FROM A TEMPLATE WALKER. `:title="`Sort by ${col}`"` is a DIRECTIVE
+// whose value is an EXPRESSION, not a static attribute, so a scan that reads only static attrs sees
+// nothing — a blind spot every one of the four private scanners shared (sr, 2026-09-28; real case
+// DashboardDataTable.vue:884). The expression is parsed with the same rules as a script block, so a
+// literal inside t()/tf(), or behind a te() guard, still passes.
+function scanBoundExpr(expr, file, attrName, out) {
+ const code = String(expr || '').trim()
+ if (!code) return
+ const found = []
+ try { scanScript(`(${code})`, file, found) } catch { return } // not parseable as an expression: skip
+ for (const f of found) out.push({ file, kind: `bind:${attrName}`, text: f.text })
+}
+
+// ★ dir="ltr" MARKS TEXT THAT IS MEANT TO STAY ENGLISH. gospel's plugins use it as the island
+// marker for content the agent ruled stays English (a BLUF, a source citation), so flagging it as a
+// leak would ask someone to "fix" a deliberate decision. Such findings are marked deliberate, listed
+// SEPARATELY, and excluded from the count a gate would fail on — visible, but not a defect.
+// Inherited down the subtree: a dir="ltr" wrapper covers everything inside it.
+const hasLtr = (node) => (node.props || []).some((p) =>
+ (p.type === 6 && p.name === 'dir' && p.value?.content === 'ltr')
+ || (p.type === 7 && p.name === 'bind' && p.arg?.content === 'dir' && /['"]ltr['"]/.test(p.exp?.content || '')))
+
+function walkTemplate(node, file, out, ltr = false) {
+ if (!node) return
+ const inLtr = ltr || (node.type === 1 && hasLtr(node))
+ if (node.type === 2) {
+ const raw = String(node.content || '').trim()
+ if (raw.length && isProse(raw) && !allowedBy(raw)) out.push({ file, kind: 'template-text', text: raw, deliberate: inLtr })
+ }
+ if (node.type === 1) for (const p of node.props || []) {
+ if (p.type === 6 && VISIBLE_ATTRS.has(p.name)) {
+ const v = p.value && p.value.content
+ if (v && isProse(v) && !allowedBy(v)) out.push({ file, kind: `attr:${p.name}`, text: v, deliberate: inLtr })
+ }
+ // (interpolations are handled below, on the node itself)
+ // type 7 = DIRECTIVE. v-bind carries the attribute name in `arg`.
+ if (p.type === 7 && p.name === 'bind' && p.exp?.content) {
+ const attrName = p.arg?.content || 'bind'
+ // :style and :class carry CSS and identifiers, never prose. Skipping them by ATTRIBUTE is
+ // better than allow-listing their contents: `background-image: url(` has no semicolon and so
+ // slipped the CSS shape test, and widening that test to catch it would start swallowing real
+ // sentences that happen to contain a colon.
+ if (attrName !== 'style' && attrName !== 'class') {
+ const before = out.length
+ scanBoundExpr(p.exp.content, file, attrName, out)
+ for (let i = before; i < out.length; i++) out[i].deliberate = inLtr
+ }
+ }
+ }
+ // type 5 = INTERPOLATION. `{{ cond ? 'Yes' : 'No' }}` is the same defect as a bound attribute —
+ // a literal the reader sees, assembled in an expression the template walker would otherwise skip.
+ // `{{ t('Foo') }}` stays quiet because the expression is parsed with the same translator rules.
+ if (node.type === 5 && node.content?.content) {
+ const before = out.length
+ scanBoundExpr(node.content.content, file, 'interpolation', out)
+ for (let i = before; i < out.length; i++) out[i].deliberate = inLtr
+ }
+ for (const c of node.children || []) walkTemplate(c, file, out, inLtr)
+}
+
+export function scanSfc(src, file) {
+ const out = []
+ const { descriptor, errors } = parse(src, { filename: file })
+ if (errors && errors.length) throw new Error(`${file}: ${errors[0].message}`)
+ if (descriptor.template && descriptor.template.ast) walkTemplate(descriptor.template.ast, file, out)
+ const code = [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join('\n')
+ if (code) { scanScript(code, file, out); out.push(...crossAreaKeys(code, file)) }
+ return out
+}
+
+// ── production plugins: the scope this gate defends ─────────────────────────────────────────────
+// Read from the built manifest so the list cannot drift from what actually ships.
+function productionPluginDirs(root = '.') {
+ const mf = join(root, 'public/dx-maps/manifest.json')
+ if (!existsSync(mf)) return []
+ const m = JSON.parse(readFileSync(mf, 'utf8'))
+ const names = m.bundles.filter((b) => b.stage === 'production').map((b) => b.bundle)
+ return names.map((n) => join(root, 'plugin-designs', n)).filter((d) => existsSync(d))
+}
+const sfcsIn = (dir) => {
+ const out = []
+ const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) {
+ const p = join(d, e.name)
+ if (e.isDirectory()) { if (!/node_modules|dist|snapshots|fixtures/.test(e.name)) walk(p) }
+ else if (e.name.endsWith('.vue')) out.push(p)
+ } }
+ walk(dir); return out
+}
+
+// ── self-test: the gate must be shown able to SEE, and able to stay quiet ────────────────────────
+const PLANTED_BAD = `
+ Counted BY LANGUAGE, so groups collapse.Prayer Coverage
+
+ h
+
+ {{ ok ? 'Everything loaded' : 'Nothing loaded' }}
+ {{ t('Prayer Coverage') }}
+ h
+ {{ t('Prayer Coverage') }}
+ {{ n }} {{ t('Countries') }}
+
- ${yt(r("popup.country.wagfRegion"))}: ${yt(Q)} + ${yt(i("popup.country.wagfRegion"))}: ${yt(Q)}
- ${yt(r("popup.country.wagfBlock"))}: ${yt(C)} + ${yt(i("popup.country.wagfBlock"))}: ${yt(C)}
- ${yt(r("popup.country.country"))}: ${yt(k)} (${yt(y)}) + ${yt(i("popup.country.country"))}: ${yt(k)} (${yt(y)})
- ${yt(r("popup.country.uupgCount"))}: ${yt(U)} + ${yt(i("popup.country.uupgCount"))}: ${yt(U)}
te&&(te=Z),D++)}if(D===0)return;if(D===1){l(T).flyTo({center:[be,$e],zoom:p,duration:h,essential:!0});return}const pe=R(j)||{center:0,minLng:0,maxLng:0,span:0},le=[[pe.minLng,q],[pe.maxLng,te]];if(X(T,le,pe.span)){U(T,pe.center,q,te);return}try{l(T).fitBounds(le,{padding:d,maxZoom:y,duration:h,essential:!0})}catch{}}return{getOffsetCenter:u,slowFlyToFamily:b,slowFlyToRegion:m,flyTo:x,flyToCoords:_,fitBounds:g,resetView:$,zoomToLegendRow:Q,calculateRegionCenterFromPolygon:L}}const Er="#1a1a1a",an="selected-pin-highlight";function nv(e=Er){const t=document.createElement("div");t.className="map-go-pin-wrapper";const o=`pinGrad-${Math.random().toString(36).slice(2,8)}`;return t.innerHTML=` + `}function b(h){const y=t();if(!y)return;const p=h||"language-family-pins";if(!y.getLayer(p))return;const k=f=>{try{const R=f.features[0],v=R.properties,U=R.geometry.coordinates.slice();for(;Math.abs(f.lngLat.lng-U[0])>180;)U[0]+=f.lngLat.lng>U[0]?360:-360;if(s&&s.legendMode!==void 0){s.selectPeopleGroup(R),s.isMobile&&s.legendState==="collapsed"&&s.setLegendState("open"),s.isMobile||window.dispatchEvent(new CustomEvent("dxm:openDesktopLegend",{detail:{mapId:o}}));return}new(xi(y)).Popup({maxWidth:"320px",className:"people-group-popup"}).setLngLat(U).setHTML(c(v)).addTo(y)}catch(R){console.error(` [${o}] Pin click handler error:`,R)}};y.on("click",p,k),l.value.push({event:"click",layer:p,handler:k})}function m(){const h=t();if(!h)return;const y="regions-fill";if(!h.getLayer(y))return;const p=k=>{const f=["language-family-pins","language-family-pins-hitbox","people-groups-pins"];for(const R of f)if(h.getLayer(R)){const v=h.queryRenderedFeatures(k.point,{layers:[R]});if(v&&v.length>0)return}if(k.features&&k.features.length>0){const R=k.features[0].properties;new(xi(h)).Popup().setLngLat(k.lngLat).setHTML(u(R)).addTo(h)}};h.on("click",y,p),l.value.push({event:"click",layer:y,handler:p})}function L(){const h=t();if(!h)return;const y="clusters";if(!h.getLayer(y))return;const p=k=>{const f=h.queryRenderedFeatures(k.point,{layers:[y]});if(!f.length)return;const R=f[0].properties.cluster_id,v=f[0].source;h.getSource(v).getClusterExpansionZoom(R,(U,X)=>{U||h.easeTo({center:f[0].geometry.coordinates,zoom:X})})};h.on("click",y,p),l.value.push({event:"click",layer:y,handler:p})}function x(h){const y=t();if(!y||!y.getLayer(h))return;const p=()=>{y.getCanvas().style.cursor="pointer"},k=()=>{y.getCanvas().style.cursor=""};y.on("mouseenter",h,p),y.on("mouseleave",h,k),l.value.push({event:"mouseenter",layer:h,handler:p}),l.value.push({event:"mouseleave",layer:h,handler:k})}function _(){b(),x("language-family-pins"),b("language-family-pins-hitbox"),x("language-family-pins-hitbox"),L(),x("clusters")}function g(){m(),x("regions-fill")}function $(){const h=t();h&&(h.getLayer("language-family-pins")&&_(),h.getLayer("regions-fill")&&g(),h.getLayer("people-groups-pins")&&(b(),x("people-groups-pins")))}function d(){const h=t();if(h){for(const{event:y,layer:p,handler:k}of l.value)try{h.off(y,p,k)}catch{}l.value=[]}}return{attachAllEventHandlers:$,attachLanguageFamilyEvents:_,attachRegionEvents:g,attachPinClickHandler:b,attachRegionClickHandler:m,attachClusterClickHandler:L,attachCursorHandlers:x,detachAllEventHandlers:d,generatePinPopupContent:c,generateRegionPopupContent:u,registeredHandlers:l}}const ot={Chamacoco:"Zamucoan",Ninam:"Yanomaman",Yagua:"Yaguan",Muinane:"Witotoan",Finnish:"Uralic","Hill Mari":"Uralic",Mansi:"Uralic","Meadow Mari":"Uralic","North Saami":"Uralic",Carabayo:"Unclassified",Kujarge:"Unclassified",Shabo:"Unclassified",Ainu:"Turkic",Kazakh:"Turkic","Khorasani Turkish":"Turkic",Kyrgyz:"Turkic","Northern Uzbek":"Turkic","South Azerbaijani":"Turkic",Turkish:"Turkic",Tuvan:"Turkic",Uyghur:"Turkic",Amundava:"Tupian",Araweté:"Tupian","Asurini of Xingú":"Tupian","Avá-Canoeiro":"Tupian",Awetí:"Tupian",Kamayurá:"Tupian",Karo:"Tupian",Morerebi:"Tupian",Tenharim:"Tupian","Uru-Eu-Wau-Wau":"Tupian","Uru-Pa-In":"Tupian",Wayampi:"Tupian","Zo’é":"Tupian",Evenki:"Tungusic",Xibe:"Tungusic",Carapana:"Tucanoan",Cubeo:"Tucanoan",Desano:"Tucanoan",Macuna:"Tucanoan",Piratapuyo:"Tucanoan",Siona:"Tucanoan",Siriano:"Tucanoan",Tucano:"Tucanoan",Tuyuca:"Tucanoan",Waimaha:"Tucanoan",Tay:"Trans-New Guinea",Adi:"Sino-Tibetan",Akha:"Sino-Tibetan","Amdo Tibetan":"Sino-Tibetan",Axi:"Sino-Tibetan",Biyo:"Sino-Tibetan","Boro (India)":"Sino-Tibetan",Brokkat:"Sino-Tibetan",Bugun:"Sino-Tibetan",Byangsi:"Sino-Tibetan","Central Lalo":"Sino-Tibetan","Central Tibetan":"Sino-Tibetan",Chak:"Sino-Tibetan",Chalikha:"Sino-Tibetan",Chaudangsi:"Sino-Tibetan",Chocangacakha:"Sino-Tibetan",Darlong:"Sino-Tibetan",Dhimal:"Sino-Tibetan","Digaro-Mishmi":"Sino-Tibetan",Dzalakha:"Sino-Tibetan","Eastern Nisu":"Sino-Tibetan",Ersu:"Sino-Tibetan",Gahri:"Sino-Tibetan",Gongduk:"Sino-Tibetan",Gyalsumdo:"Sino-Tibetan",Hani:"Sino-Tibetan","Idu-Mishmi":"Sino-Tibetan",Kaike:"Sino-Tibetan",Kanashi:"Sino-Tibetan",Katso:"Sino-Tibetan",Khamba:"Sino-Tibetan",Koireng:"Sino-Tibetan",Kucong:"Sino-Tibetan",Kurtokha:"Sino-Tibetan",Ladakhi:"Sino-Tibetan",Lakha:"Sino-Tibetan","Lang’e":"Sino-Tibetan",Lhokpu:"Sino-Tibetan",Lolopo:"Sino-Tibetan",Lunanakha:"Sino-Tibetan","Mandarin Chinese":"Sino-Tibetan",Meitei:"Sino-Tibetan","Miju-Mishmi":"Sino-Tibetan",Mpi:"Sino-Tibetan",Na:"Sino-Tibetan",Namuyi:"Sino-Tibetan",Narua:"Sino-Tibetan",Naxi:"Sino-Tibetan","Northern Muji":"Sino-Tibetan",Nuosu:"Sino-Tibetan",Nupbikha:"Sino-Tibetan",Nyenkha:"Sino-Tibetan",Olekha:"Sino-Tibetan",Pahari:"Sino-Tibetan",Pattani:"Sino-Tibetan","Phana’":"Sino-Tibetan",Phula:"Sino-Tibetan",Phunoi:"Sino-Tibetan",Puma:"Sino-Tibetan",Puroik:"Sino-Tibetan",Raji:"Sino-Tibetan",Rawat:"Sino-Tibetan",Sani:"Sino-Tibetan",Sherdukpen:"Sino-Tibetan",Sherpa:"Sino-Tibetan",Shuhi:"Sino-Tibetan",Sila:"Sino-Tibetan",Singpho:"Sino-Tibetan",Sunwar:"Sino-Tibetan","Tawang Monpa":"Sino-Tibetan",Tseku:"Sino-Tibetan",Tshangla:"Sino-Tibetan",Usoi:"Sino-Tibetan","Western Minyag":"Sino-Tibetan","Wusa Nasu":"Sino-Tibetan","Xiang Chinese":"Sino-Tibetan",Yakkha:"Sino-Tibetan","Youle Jinuo":"Sino-Tibetan","Yue Chinese":"Sino-Tibetan",Zakhring:"Sino-Tibetan","Afghan Sign Language":"Sign Language","Albanian Sign Language":"Sign Language","Algerian Sign Language":"Sign Language","American Sign Language":"Sign Language","Armenian Sign Language":"Sign Language","British Sign Language":"Sign Language","Bulgarian Sign Language":"Sign Language","Cambodian Sign Language":"Sign language","Cuban Sign Language":"Sign Language","Egyptian Sign Language":"Sign Language","Greek Sign Language":"Sign Language","Guinean Sign Language":"Sign Language","Hong Kong Sign Language":"Sign Language","Icelandic Sign Language":"Sign Language","Iranian Sign Language":"Sign Language","Israeli Sign Language":"Sign Language","Jamaican Sign Language":"Sign Language","Jordanian Sign Language":"Sign Language","Laos Sign Language":"Sign Language","Libyan Sign Language":"Sign Language","Maltese Sign Language":"Sign Language","Mauritian Sign Language":"Sign Language","Moroccan Sign Language":"Sign Language","Mozambican Sign Language":"Sign Language","Myanmar Sign Language":"Sign language","Natural Sign Language":"Sign Language","Pakistan Sign Language":"Sign Language","Papua New Guinean Sign Language":"Sign language","Russian Sign Language":"Sign Language","Saudi Arabian Sign Language":"Sign Language","Sign Language of the Netherlands":"Sign Language","Solomon Islands Sign Language":"Sign Language","Sri Lankan Sign Language":"Sign Language","Swedish Sign Language":"Sign Language","Turkish Sign Language":"Sign Language","Northern Conchucos Ancash Quechua":"Quechuan","Northern Pastaza Quichua":"Quechuan",Hupdë:"Puinavean",Amahuaca:"Panoan",Korubo:"Panoan",Sharanahua:"Panoan","Shipibo-Conibo":"Panoan",Yaminahua:"Panoan",Yawanawa:"Panoan",Afitti:"Nilo-Saharan",Assangori:"Nilo-Saharan","Beli (South Sudan)":"Nilo-Saharan",Berta:"Nilo-Saharan",Burun:"Nilo-Saharan","Dar Daju Daju":"Nilo-Saharan","Dar Fur Daju":"Nilo-Saharan","Dar Sila Daju":"Nilo-Saharan",Dazaga:"Nilo-Saharan","Dendi (Benin)":"Nilo-Saharan",Dilling:"Nilo-Saharan",Fur:"Nilo-Saharan",Gbaya:"Nilo-Saharan",Ghulfan:"Nilo-Saharan",Gwama:"Nilo-Saharan","Humburi Senni Songhay":"Nilo-Saharan",Jopadhola:"Nilo-Saharan",Kadaru:"Nilo-Saharan",Kanga:"Nilo-Saharan",Karanga:"Nilo-Saharan",Karko:"Nilo-Saharan",Kebu:"Nilo-Saharan",Keiga:"Nilo-Saharan",Keliko:"Nilo-Saharan",Kendeje:"Nilo-Saharan",Kibet:"Nilo-Saharan","Komo (Sudan)":"Nilo-Saharan",Korandje:"Nilo-Saharan",Kulfa:"Nilo-Saharan",Kwegu:"Nilo-Saharan",Laarim:"Nilo-Saharan",Logorik:"Nilo-Saharan",Majang:"Nilo-Saharan","Manga Kanuri":"Nilo-Saharan",Mango:"Nilo-Saharan",Mararit:"Nilo-Saharan",Marfa:"Nilo-Saharan",Midob:"Nilo-Saharan","Mo’da":"Nilo-Saharan",Morom:"Nilo-Saharan",Moru:"Nilo-Saharan",Naba:"Nilo-Saharan",Nara:"Nilo-Saharan",Njalgulgule:"Nilo-Saharan",Nuer:"Nilo-Saharan","Nyamusa-Molo":"Nilo-Saharan",Nyangatom:"Nilo-Saharan","Olu’bo":"Nilo-Saharan",Opo:"Nilo-Saharan",Päri:"Nilo-Saharan","Sara Kaba Naa":"Nilo-Saharan",Surbakhal:"Nilo-Saharan",Tadaksahak:"Nilo-Saharan","Tama (Chad)":"Nilo-Saharan",Temein:"Nilo-Saharan",Thuri:"Nilo-Saharan","Tirmaga-Chai Suri":"Nilo-Saharan",Toposa:"Nilo-Saharan",Tumtum:"Nilo-Saharan","Wali (Sudan)":"Nilo-Saharan","Yerwa Kanuri":"Nilo-Saharan",Yulu:"Nilo-Saharan",Zaghawa:"Nilo-Saharan","Adamawa Fulfulde":"Niger-Congo",Akaselem:"Niger-Congo","Amba (Uganda)":"Niger-Congo",Anii:"Niger-Congo",Baangi:"Niger-Congo",Badyara:"Niger-Congo","Bago-Kusuntu":"Niger-Congo",Bai:"Niger-Congo","Bainouk-Gunyaamolo":"Niger-Congo",Bamanankan:"Niger-Congo","Banda-Mbrès":"Niger-Congo",Bandial:"Niger-Congo",Bankagooma:"Niger-Congo","Belanda Viri":"Niger-Congo",Bemba:"Niger-Congo",Bolgo:"Niger-Congo","Bon Gula":"Niger-Congo",Bua:"Niger-Congo",Chichewa:"Niger-Congo","Chung (Cameroon)":"Niger-Congo","Ci Gbe":"Niger-Congo",Degema:"Niger-Congo",Dek:"Niger-Congo",Dhimba:"Niger-Congo",Dogoso:"Niger-Congo","East Limba":"Niger-Congo",Engenni:"Niger-Congo",Epie:"Niger-Congo",Éwé:"Niger-Congo",Fania:"Niger-Congo",Foodo:"Niger-Congo",Fwe:"Niger-Congo",Gciriku:"Niger-Congo",Gwa:"Niger-Congo",Herero:"Niger-Congo",Ibuoro:"Niger-Congo","Jenaama Bozo":"Niger-Congo",Jita:"Niger-Congo",Jula:"Niger-Congo",Kabiyè:"Niger-Congo",Kadung:"Niger-Congo",Kakabe:"Niger-Congo",Kalamsé:"Niger-Congo","Kami (Tanzania)":"Niger-Congo","Kango (Bas-Uélé District)":"Niger-Congo",Kasem:"Niger-Congo","Kelengaxo Bozo":"Niger-Congo",Khe:"Niger-Congo",Khisa:"Niger-Congo",Kim:"Niger-Congo",Kinyarwanda:"Niger-Congo",Kipfokomu:"Niger-Congo",Kitharaka:"Niger-Congo",Kiyombe:"Niger-Congo",Ko:"Niger-Congo",Koalib:"Niger-Congo",Koke:"Niger-Congo",Konongo:"Niger-Congo","Koro Nulu":"Niger-Congo","Koro Zuba":"Niger-Congo",Kuhane:"Niger-Congo",Kunda:"Niger-Congo",Kuranko:"Niger-Congo",Kuria:"Niger-Congo",Kwangali:"Niger-Congo",Lame:"Niger-Congo",Laru:"Niger-Congo",Lopa:"Niger-Congo","Makhuwa-Meetto":"Niger-Congo","Makhuwa-Shirima":"Niger-Congo","Malawi Nyiha":"Niger-Congo",Mandinka:"Niger-Congo",Mankanya:"Niger-Congo",Mansoanka:"Niger-Congo",Mbukushu:"Niger-Congo","Mende (Sierra Leone)":"Niger-Congo",Miyobe:"Niger-Congo",Mushungulu:"Niger-Congo","Mwali Comorian":"Niger-Congo","Mwera (Chimwera)":"Niger-Congo",Ndemli:"Niger-Congo",Ndengereko:"Niger-Congo",Ndumu:"Niger-Congo",Ndunga:"Niger-Congo","Ndzwani Comorian":"Niger-Congo",Ngile:"Niger-Congo","Northwest Gbaya":"Niger-Congo",Notre:"Niger-Congo",Nyamwanga:"Niger-Congo",Nyankore:"Niger-Congo",Nyengo:"Niger-Congo",Okpamheri:"Niger-Congo",Osatu:"Niger-Congo","Pana (Burkina Faso)":"Niger-Congo",Pulaar:"Niger-Congo",Pular:"Niger-Congo",Rufiji:"Niger-Congo","Sankaran Maninka":"Niger-Congo",Sapo:"Niger-Congo","Senara Sénoufo":"Niger-Congo",Shanga:"Niger-Congo",Sininkere:"Niger-Congo","Southern Ngbandi":"Niger-Congo",Swahili:"Niger-Congo","Syenara Sénoufo":"Niger-Congo",Tagbu:"Niger-Congo",Tagoi:"Niger-Congo",Tegali:"Niger-Congo",Tem:"Niger-Congo","Tiéyaxo Bozo":"Niger-Congo",Tiyaa:"Niger-Congo",Tocho:"Niger-Congo","Tonga (Zambia)":"Niger-Congo",Totela:"Niger-Congo","Toura (Côte d’Ivoire)":"Niger-Congo",Tswa:"Niger-Congo",Tumbuka:"Niger-Congo",Ukaan:"Niger-Congo",Uneme:"Niger-Congo",Vai:"Niger-Congo",Vute:"Niger-Congo",Waama:"Niger-Congo",Warnang:"Niger-Congo",Wolof:"Niger-Congo",Yao:"Niger-Congo",Yauma:"Niger-Congo",Yom:"Niger-Congo","Zan Gula":"Niger-Congo",Zande:"Niger-Congo",Zigula:"Niger-Congo",Mamaindê:"Nambikwara",Aghul:"Nakh-Daghestanian",Akhvakh:"Nakh-Daghestanian",Andi:"Nakh-Daghestanian",Avar:"Nakh-Daghestanian",Bagvalal:"Nakh-Daghestanian",Botlikh:"Nakh-Daghestanian",Chamalal:"Nakh-Daghestanian",Chechen:"Nakh-Daghestanian",Dargwa:"Nakh-Daghestanian",Kaitag:"Nakh-Daghestanian",Karata:"Nakh-Daghestanian",Khvarshi:"Nakh-Daghestanian",Tindi:"Nakh-Daghestanian",Pirahã:"Muran",Bonan:"Mongolic","China Buriat":"Mongolic",Daur:"Mongolic","Kalmyk-Oirat":"Mongolic","Peripheral Mongolian":"Mongolic","Russia Buriat":"Mongolic",Tagdal:"Mixed language",Wutunhua:"Mixed language","Ashéninka Perené":"Maipurean",Baniwa:"Maipurean",Cabiyarí:"Maipurean",Curripaco:"Maipurean","Enawené-Nawé":"Maipurean",Kanamarí:"Maipurean",Mehináku:"Maipurean","Pichis Ashéninka":"Maipurean",Waurá:"Maipurean",Bangime:"Language isolate",Burushaski:"Language isolate","Ai-Cham":"Kra-Dai",Aiton:"Kra-Dai","Dai Zhuang":"Kra-Dai",Duoluo:"Kra-Dai","Green Gelao":"Kra-Dai","Guibian Zhuang":"Kra-Dai",Hlai:"Kra-Dai",Jiamao:"Kra-Dai",Kang:"Kra-Dai",Khün:"Kra-Dai",Kuan:"Kra-Dai",Lachi:"Kra-Dai","Laha (Viet Nam)":"Kra-Dai",Lao:"Kra-Dai",Lü:"Kra-Dai","Mak (China)":"Kra-Dai","Northeastern Thai":"Kra-Dai","Northern Dong":"Kra-Dai","Northern Thai":"Kra-Dai",Nyaw:"Kra-Dai",Phake:"Kra-Dai","Phu Thai":"Kra-Dai",Qabiao:"Kra-Dai",Qau:"Kra-Dai","Qiubei Zhuang":"Kra-Dai",Shan:"Kra-Dai","Southern Dong":"Kra-Dai",Sui:"Kra-Dai","T’en":"Kra-Dai","Tai Daeng":"Kra-Dai","Tai Dam":"Kra-Dai","Tai Hongjin":"Kra-Dai","Tai Nüa":"Kra-Dai","Tai Ya":"Kra-Dai","Tai Yo":"Kra-Dai","Ts’ün-Lao":"Kra-Dai","Yang Zhuang":"Kra-Dai",Yerong:"Kra-Dai",Yong:"Kra-Dai",Yoy:"Kra-Dai",Khoekhoe:"Khoe-Kwadi",Karajá:"Karajá","Pará Gavião":"Jean",Suyá:"Jean",Japanese:"Japonic","Oki-No-Erabu":"Japonic","Toku-No-Shima":"Japonic",Yaeyama:"Japonic",Yoron:"Japonic","Adivasi Oriya":"Indo-European",Afrikaans:"Indo-European","Alviri-Vidari":"Indo-European",Angika:"Indo-European",Assamese:"Indo-European",Bagheli:"Indo-European",Bagri:"Indo-European",Bashkardi:"Indo-European",Bateri:"Indo-European",Bengali:"Indo-European",Bhojpuri:"Indo-European",Bhunjia:"Indo-European",Bilaspuri:"Indo-European",Bosnian:"Indo-European","Braj Bhasha":"Indo-European",Brokskat:"Indo-European",Bundeli:"Indo-European",Catalan:"Indo-European",Chambeali:"Indo-European",Chhattisgarhi:"Indo-European",Dameli:"Indo-European",Dari:"Indo-European",Dhundari:"Indo-European",Dogri:"Indo-European",Domari:"Indo-European",Dutch:"Indo-European","Eastern Balochi":"Indo-European","Eastern Panjabi":"Indo-European","Eastern Yiddish":"Indo-European",English:"Indo-European",French:"Indo-European",Gaddi:"Indo-European",Garhwali:"Indo-European",Gawri:"Indo-European",Gowro:"Indo-European",Gujarati:"Indo-European",Gujari:"Indo-European",Halbi:"Indo-European",Haroti:"Indo-European",Haryanvi:"Indo-European",Hindi:"Indo-European","Indus Kohistani":"Indo-European","Iranian Persian":"Indo-European",Ishkashimi:"Indo-European",Kacchi:"Indo-European",Kalkoti:"Indo-European",Kanauji:"Indo-European",Kangri:"Indo-European",Karingani:"Indo-European",Kashmiri:"Indo-European",Kati:"Indo-European",Khunsari:"Indo-European",Konkani:"Indo-European",Kudmali:"Indo-European",Kumaoni:"Indo-European",Kumzari:"Indo-European",Laki:"Indo-European",Lasi:"Indo-European",Luwati:"Indo-European",Magahi:"Indo-European",Maithili:"Indo-European",Maldivian:"Indo-European",Malvi:"Indo-European",Marathi:"Indo-European","Marwari (India)":"Indo-European",Mewari:"Indo-European",Munji:"Indo-European",Nepali:"Indo-European",Nimadi:"Indo-European","Northern Kurdish":"Indo-European",Odia:"Indo-European","Pahari-Potwari":"Indo-European",Palula:"Indo-European",Panchpargania:"Indo-European",Pangwali:"Indo-European",Parachi:"Indo-European",Portuguese:"Indo-European",Russian:"Indo-European",Sadri:"Indo-European",Sanglechi:"Indo-European",Sarikoli:"Indo-European",Savi:"Indo-European",Semnani:"Indo-European",Serbian:"Indo-European",Shabak:"Indo-European",Shahrudi:"Indo-European",Shekhawati:"Indo-European",Shina:"Indo-European",Shughni:"Indo-European",Shumashti:"Indo-European",Sindhi:"Indo-European",Sivandi:"Indo-European",Soi:"Indo-European","Southern Balochi":"Indo-European",Spanish:"Indo-European",Tajik:"Indo-European",Talysh:"Indo-European",Urdu:"Indo-European",Ushojo:"Indo-European",Vaghri:"Indo-European","Vlax Romani":"Indo-European",Wagdi:"Indo-European",Wakhi:"Indo-European",Waneci:"Indo-European","Western Balochi":"Indo-European",Yadgha:"Indo-European","Bu-Nao Bunu":"Hmong-Mien","Central Mashan Miao":"Hmong-Mien","Chuanqiandian Cluster Miao":"Hmong-Mien","Eastern Xiangxi Miao":"Hmong-Mien","Iu Mien":"Hmong-Mien","Luopohe Miao":"Hmong-Mien","Northern Qiandong Miao":"Hmong-Mien","Pa-Hng":"Hmong-Mien","Southwestern Huishui Miao":"Hmong-Mien","Wunai Bunu":"Hmong-Mien","Younuo Bunu":"Hmong-Mien",Amarakaeri:"Harákmbut",Huachipaeri:"Harákmbut",Cuiba:"Guajiboan",Macaguán:"Guajiboan",Tlicho:"Eyak-Athabaskan",Kannada:"Dravidian",Kodava:"Dravidian","Korra Koraga":"Dravidian","Kota (India)":"Dravidian",Kudiya:"Dravidian",Malapandaram:"Dravidian",Malayalam:"Dravidian",Muthuvan:"Dravidian",Ravula:"Dravidian",Sholaga:"Dravidian",Tamil:"Dravidian",Telugu:"Dravidian",Tulu:"Dravidian",Vishavan:"Dravidian","Andaman Hindi Creole":"Creole","Cocos Islands Malay":"Creole","Juba Arabic":"Creole",Nubi:"Creole",Barí:"Chibchan","Barro Negro Tunebo":"Chibchan","Western Tunebo":"Chibchan",Ikpeng:"Cariban","Kuikúro-Kalapálo":"Cariban",Pemon:"Cariban",Trió:"Cariban","Waimiri-Atroarí":"Cariban",Waiwai:"Cariban",Yukpa:"Cariban",Borôro:"Bororoan",Chachi:"Barbacoan","Abai Sungai":"Austronesian",Ambelau:"Austronesian",Asilulu:"Austronesian",Balaesang:"Austronesian","Batak Alas-Kluet":"Austronesian","Batak Dairi":"Austronesian","Batak Karo":"Austronesian",Bonerate:"Austronesian",Bonggi:"Austronesian",Brunei:"Austronesian","Brunei Bisaya":"Austronesian",Buol:"Austronesian","Central Malay":"Austronesian","Central Sama":"Austronesian","Coastal Konjo":"Austronesian",Dampelas:"Austronesian","Dibabawon Manobo":"Austronesian",Dondo:"Austronesian",Gane:"Austronesian","Geser-Gorom":"Austronesian","Highland Konjo":"Austronesian",Hitu:"Austronesian",Iban:"Austronesian",Indonesian:"Austronesian","Kadazan Dusun":"Austronesian",Kiput:"Austronesian",Komodo:"Austronesian",Kompane:"Austronesian",Lahanan:"Austronesian","Lampung Nyo":"Austronesian","Larike-Wakasihu":"Austronesian",Lola:"Austronesian",Luhu:"Austronesian",Malay:"Austronesian",Mariri:"Austronesian",Moronene:"Austronesian",Rukai:"Austronesian","Sabah Bisaya":"Austronesian","Seit-Kaitetu":"Austronesian",Sula:"Austronesian","Tae’":"Austronesian",Tajio:"Austronesian",Tomini:"Austronesian",Tutong:"Austronesian",Ujir:"Austronesian",Aheu:"Austro-Asiatic",Alak:"Austro-Asiatic",Bit:"Austro-Asiatic","Bo (Laos)":"Austro-Asiatic",Bondo:"Austro-Asiatic",Brao:"Austro-Asiatic",Bugan:"Austro-Asiatic",Chong:"Austro-Asiatic",Chut:"Austro-Asiatic",Danau:"Austro-Asiatic",Halang:"Austro-Asiatic","Halang Doan":"Austro-Asiatic",Hu:"Austro-Asiatic",Hung:"Austro-Asiatic",Ir:"Austro-Asiatic",Kensiu:"Austro-Asiatic",Khmu:"Austro-Asiatic",Khua:"Austro-Asiatic",Khuen:"Austro-Asiatic",Koda:"Austro-Asiatic","Kon Keu":"Austro-Asiatic",Kriang:"Austro-Asiatic",Kuay:"Austro-Asiatic",Laven:"Austro-Asiatic",Maleng:"Austro-Asiatic","Man Met":"Austro-Asiatic",Mang:"Austro-Asiatic",Minriq:"Austro-Asiatic",Mlabri:"Austro-Asiatic",Nguôn:"Austro-Asiatic",Nyaheun:"Austro-Asiatic",Nyahkur:"Austro-Asiatic","O’du":"Austro-Asiatic",Ong:"Austro-Asiatic",Oy:"Austro-Asiatic",Pacoh:"Austro-Asiatic","Phong-Kniang":"Austro-Asiatic",Prai:"Austro-Asiatic",Puoc:"Austro-Asiatic",Rengao:"Austro-Asiatic",Rmeet:"Austro-Asiatic","Rumai Palaung":"Austro-Asiatic",Sapuan:"Austro-Asiatic","Shwe Palaung":"Austro-Asiatic","So (Laos)":"Austro-Asiatic",Sou:"Austro-Asiatic","Tai Loi":"Austro-Asiatic",Talieng:"Austro-Asiatic",Tareng:"Austro-Asiatic",Tho:"Austro-Asiatic",U:"Austro-Asiatic","Upper Ta’oih":"Austro-Asiatic",Vietnamese:"Austro-Asiatic","Western Katu":"Austro-Asiatic",Suruahá:"Arauan",Jarawa:"Andamanese",Öñge:"Andamanese",Sentinel:"Andamanese",Afar:"Afro-Asiatic","Alaba-K’abeena":"Afro-Asiatic",Alagwa:"Afro-Asiatic",Ale:"Afro-Asiatic","Algerian Arabic":"Afro-Asiatic",Amharic:"Afro-Asiatic",Arbore:"Afro-Asiatic","Baharna Arabic":"Afro-Asiatic",Barein:"Afro-Asiatic",Bedawiyet:"Afro-Asiatic",Bidiyo:"Afro-Asiatic",Bilen:"Afro-Asiatic",Birgit:"Afro-Asiatic",Boor:"Afro-Asiatic","Borana-Arsi-Guji Oromo":"Afro-Asiatic",Buduma:"Afro-Asiatic","Central Atlas Tamazight":"Afro-Asiatic","Chadian Arabic":"Afro-Asiatic",Chenoua:"Afro-Asiatic",Ciwogai:"Afro-Asiatic",Daasanach:"Afro-Asiatic",Dabarre:"Afro-Asiatic",Dangaléat:"Afro-Asiatic",Deno:"Afro-Asiatic",Diri:"Afro-Asiatic",Duwai:"Afro-Asiatic","Eastern Egyptian Bedawi Arabic":"Afro-Asiatic","Eastern Oromo":"Afro-Asiatic",Garre:"Afro-Asiatic",Geruma:"Afro-Asiatic",Giiwo:"Afro-Asiatic",Gorowa:"Afro-Asiatic","Gulf Arabic":"Afro-Asiatic","Hadrami Arabic":"Afro-Asiatic",Harsusi:"Afro-Asiatic",Hassaniyya:"Afro-Asiatic",Hausa:"Afro-Asiatic",Hebrew:"Afro-Asiatic","Hijazi Arabic":"Afro-Asiatic",Hobyót:"Afro-Asiatic",Hozo:"Afro-Asiatic",Jiiddu:"Afro-Asiatic",Jimi:"Afro-Asiatic",Jina:"Afro-Asiatic","Jonkor Bourmataguil":"Afro-Asiatic","Judeo-Iraqi Arabic":"Afro-Asiatic","Judeo-Moroccan Arabic":"Afro-Asiatic",Kabalai:"Afro-Asiatic",Kajakse:"Afro-Asiatic",Kariya:"Afro-Asiatic","Karo (Ethiopia)":"Afro-Asiatic",Kutto:"Afro-Asiatic","Levantine Arabic":"Afro-Asiatic",Libido:"Afro-Asiatic","Libyan Arabic":"Afro-Asiatic","Lishana Deni":"Afro-Asiatic",Maaka:"Afro-Asiatic",Maay:"Afro-Asiatic",Majera:"Afro-Asiatic",Male:"Afro-Asiatic",Maslam:"Afro-Asiatic",Masmaje:"Afro-Asiatic",Mburku:"Afro-Asiatic",Mehri:"Afro-Asiatic","Mesopotamian Arabic":"Afro-Asiatic",Migaama:"Afro-Asiatic",Mogum:"Afro-Asiatic","Moroccan Arabic":"Afro-Asiatic",Mositacha:"Afro-Asiatic",Mubi:"Afro-Asiatic",Mukulu:"Afro-Asiatic",Nafusi:"Afro-Asiatic","Najdi Arabic":"Afro-Asiatic",Nyam:"Afro-Asiatic","Omani Arabic":"Afro-Asiatic","Pa’a":"Afro-Asiatic",Saba:"Afro-Asiatic",Saho:"Afro-Asiatic","Sanaani Arabic":"Afro-Asiatic",Seze:"Afro-Asiatic",Shehri:"Afro-Asiatic",Shekkacho:"Afro-Asiatic",Sheko:"Afro-Asiatic","Shihhi Arabic":"Afro-Asiatic",Siwi:"Afro-Asiatic",Sokoro:"Afro-Asiatic",Soqotri:"Afro-Asiatic","South Levantine Arabic":"Afro-Asiatic","Standard Arabic":"Afro-Asiatic","Sudanese Arabic":"Afro-Asiatic","Ta’izzi-Adeni Arabic":"Afro-Asiatic","Tahaggart Tamahaq":"Afro-Asiatic",Taznatit:"Afro-Asiatic",Tigré:"Afro-Asiatic",Toram:"Afro-Asiatic",Tsamai:"Afro-Asiatic",Tumzabt:"Afro-Asiatic",Tunni:"Afro-Asiatic",Waata:"Afro-Asiatic","West Central Oromo":"Afro-Asiatic",Xamtanga:"Afro-Asiatic",Zumbun:"Afro-Asiatic"},jl=[">",["to-number",["get","peoplePraying"],0],0],Ul={0:.25,8:.4,14:.5},fa={0:.1,8:.18,14:.22},ma={0:1.2,8:1.6,14:2},Wg=["interpolate",["linear"],["zoom"],0,["case",jl,Ul[0],fa[0]],8,["case",jl,Ul[8],fa[8]],14,["case",jl,Ul[14],fa[14]]],K0=["interpolate",["linear"],["zoom"],0,ma[0],8,ma[8],14,ma[14]],Vg=["==",["to-number",["get","peoplePraying"],0],0],Y0={prayer:[Vg],prayerProgress:[Vg],engagement:[["all",["!=",["get","engagementStatus"],!0],["!=",["get","engagementStatus"],1]]],adoption:[["all",["!=",["get","adoptionStatus"],!0],["!=",["get","adoptionStatus"],1]]]};function J0(e){const t=Y0[e]||[];if(!t.length)return K0;const o=t.length===1?t[0]:["any",...t];return["interpolate",["linear"],["zoom"],0,["case",o,fa[0],ma[0]],8,["case",o,fa[8],ma[8]],14,["case",o,fa[14],ma[14]]]}const Z0=[[/ sign language$/i,"Sign Language"]];function X0(e){if(!e||typeof e!="string")return null;const t=e.trim(),o=t.split(",").map(a=>a.trim()).filter(Boolean);if(o.length>=2){const a=[...o].reverse().join(" ");if(ot[a])return ot[a];if(o.length>=3){const s=[o[1],o[0]].join(" ");if(ot[s])return ot[s]}}if(ot[t])return ot[t];const n=t.replace(/\s*\(.*?\)\s*$/,"").trim();if(n!==t&&ot[n])return ot[n];for(const[a,s]of Z0)if(a.test(t))return s;return null}function Q0(e={}){const{getMap:t=()=>null,mapId:o="unknown",getLanguageFamilyColor:n=()=>"#999999",getNormalizedPeopleGroups:a=()=>[]}=e,s=()=>{const B=t();return B?Xe(B):null},i=qg({getMap:s,mapId:o,getLanguageFamilyColor:n,getNormalizedPeopleGroups:a}),r=ke({languageFamilyPins:!1,regions:!1,peopleGroups:!1,familyConnections:!1}),l=ke({});function c(B,K="languageFamily"){const ae=s();if(!ae||!(B!=null&&B.length))return;const _e=U0(K),T=B.map(G=>{var oe,ge,w,P,ee,V,ie,de,W,ue,Pe,Re,Ke,ut,nt,xt,Kt,Ee,Qe;return{type:"Feature",properties:{uniqueId:G.id||G.uniqueId,slug:G.slug||((oe=G._normalized)==null?void 0:oe.slug)||"",rop3:G.rop3||G.peopleGroupId,peopleId3:G.peopleId3||"",name:G.name,language:G.language,languageFamily:G.languageFamily||((ge=G._normalized)==null?void 0:ge.languageFamily)||X0(G.language)||"Unknown",affinityBlock:G.affinityBlock||G.affbloc||G.Affbloc||((w=G._normalized)==null?void 0:w.affinityBlock)||"Unknown",affinityBlockLabel:G.affinityBlockLabel||((P=G._normalized)==null?void 0:P.affinityBlockLabel)||"",cluster:G.cluster||((ee=G._normalized)==null?void 0:ee.cluster)||"",clusterLabel:G.clusterLabel||((V=G._normalized)==null?void 0:V.clusterLabel)||"",peopleGroup:G.peopleGroup||((ie=G._normalized)==null?void 0:ie.peopleGroup)||"",peopleGroupLabel:G.peopleGroupLabel||((de=G._normalized)==null?void 0:de.peopleGroupLabel)||"",doxaRegion:G.doxaRegion||((W=G._normalized)==null?void 0:W.doxaRegion)||"Unknown",wagfBlock:G.wagfBlock||((ue=G._normalized)==null?void 0:ue.wagfBlock)||"",wagfBlockLabel:G.wagfBlockLabel||((Pe=G._normalized)==null?void 0:Pe.wagfBlockLabel)||"",wagfRegionLabel:G.wagfRegionLabel||((Re=G._normalized)==null?void 0:Re.wagfRegionLabel)||"",country:G.country,countryIso:G.countryIso,countryIsoLabel:G.countryIsoLabel,countryName:G.countryName,religion:G.religion,religionCode:G.religionCode,religionLabel:G.religionLabel,religionName:G.religionName,population:G.population,status:G.status,description:G.description||"",imageUrl:G.imageUrl||"",peoplePraying:Number(G.peoplePraying??((Ke=G._normalized)==null?void 0:Ke.peoplePraying)??0),engagementStatus:G.engagementStatus===!0||G.engagementStatus===1||!1,adoptionStatus:G.adoptionStatus===!0||G.adoptionStatus===1||!1,bible:G.bible||((ut=G._normalized)==null?void 0:ut.bible)||"Not Available",jesusFilm:G.jesusFilm||((nt=G._normalized)==null?void 0:nt.jesusFilm)||"Not Available",radio:G.radio||((xt=G._normalized)==null?void 0:xt.radio)||"Not Available",gospel:G.gospel||((Kt=G._normalized)==null?void 0:Kt.gospel)||"Not Available",audio:G.audio||((Ee=G._normalized)==null?void 0:Ee.audio)||"Not Available",stories:G.stories||((Qe=G._normalized)==null?void 0:Qe.stories)||"Not Available",lng:parseFloat(G.longitude||G.lng)||0},geometry:{type:"Point",coordinates:[parseFloat(G.longitude||G.lng),parseFloat(G.latitude||G.lat)]}}}).filter(G=>!isNaN(G.geometry.coordinates[0])&&!isNaN(G.geometry.coordinates[1])),N=.04,F=G=>`${G.geometry.coordinates[0]},${G.geometry.coordinates[1]}`,Z=new Map;for(const G of T){const oe=F(G);Z.has(oe)||Z.set(oe,[]),Z.get(oe).push(G)}for(const G of Z.values()){if(G.length<2)continue;const[oe,ge]=G[0].geometry.coordinates,w=G.length;for(let P=0;P",["to-number",["get","peoplePraying"],0],0],"#ffffff","#1f2937"],"circle-opacity":1,"circle-stroke-opacity":1,"circle-opacity-transition":{duration:250}}}),ae.addLayer({id:"language-family-pins-hitbox",type:"circle",source:"language-families",paint:{"circle-radius":Bl("touchTarget"),"circle-color":"rgba(0,0,0,0)","circle-stroke-width":0,"circle-opacity":0}}),r.value.languageFamilyPins=!0,i.attachLanguageFamilyEvents()}function u(B,K="doxa-regions"){const ae=s();if(!ae)return;ae.getLayer("regions-fill")&&ae.removeLayer("regions-fill"),ae.getLayer("regions-border")&&ae.removeLayer("regions-border"),ae.getSource("regions")&&ae.removeSource("regions");const _e={africa:"#ff2e2e",asia:"#1a8cff",europe:"#1ee05f","latin_america_&_caribbean":"#FFEB3B",middle_east:"#a83dff","north_america_&_non-spanish_caribbean":"#00e0bd",oceania:"#ff7f11"},T="#cccccc",N=B&&B.isoToRegion?B.isoToRegion:{},F=[],Z={};for(const[ge,w]of Object.entries(N)){const P=_e[String(w).toLowerCase()];P&&F.push(ge,P),Z[w]||(Z[w]=[]),Z[w].push(ge)}l.value=Z;const ce=F.length?["match",["get","iso_3166_1_alpha_3"],...F,T]:T,G=ae.getLayer("language-family-pins-shadow")?"language-family-pins-shadow":ae.getLayer("language-family-pins")?"language-family-pins":void 0,oe=(()=>{var ge,w,P;try{return!!((P=(w=(ge=ae.getContainer)==null?void 0:ge.call(ae))==null?void 0:w.classList)!=null&&P.contains("maplibregl-map"))}catch{return!1}})();oe?(ae.addSource("regions",{type:"geojson",data:{type:"FeatureCollection",features:[]}}),fetch(`${ul(null)}/boundaries/world-adm0-web.geojson`).then(ge=>ge.json()).then(ge=>{for(const P of ge.features||[])P.properties&&P.properties.iso3&&(P.properties.iso_3166_1_alpha_3=P.properties.iso3);const w=ae.getSource("regions");w&&w.setData(ge)}).catch(ge=>console.warn("[map-layers] region boundaries failed to load",ge))):ae.addSource("regions",{type:"vector",url:"mapbox://mapbox.country-boundaries-v1"}),ae.addLayer({id:"regions-fill",type:"fill",source:"regions",...oe?{}:{"source-layer":"country_boundaries"},paint:{"fill-color":ce,"fill-opacity":K==="none"?.1:.7,"fill-antialias":!0,...Oo(ae,"fill"),"fill-opacity-transition":{duration:300}}},G),ae.addLayer({id:"regions-border",type:"line",source:"regions",...oe?{}:{"source-layer":"country_boundaries"},paint:{"line-color":ce,"line-width":1,"line-opacity":.9,...Oo(ae,"line")}},G),r.value.regions=!0,i.attachRegionEvents()}function b(B,K){const ae=s();if(!ae||!B||!(K!=null&&K.length))return;m();const _e=K.filter(ce=>{var oe;return(ce.languageFamily||((oe=ce._normalized)==null?void 0:oe.languageFamily)||"Unknown")===B});if(_e.length<2)return;const T=n(B),N=[],F=15,Z=[..._e].sort((ce,G)=>(ce.longitude||ce.lng)-(G.longitude||G.lng));for(let ce=0;ce =",["get","peoplePraying"],Do],"#15803d",[">",["get","peoplePraying"],0],"#d97706","rgba(0,0,0,0)"]}const I=[0,5,2,6,4,7,5,9,6,12,7,14,8,18,10,25,12,32,14,40];function j(B,K){const ae=["interpolate",["linear"],["zoom"]];for(let _e=0;_e ",["get","peoplePraying"],0],ae=z();if(!R.every(T=>B.getLayer(T))){for(const T of R)if(B.getLayer(T))try{B.removeLayer(T)}catch{}B.addLayer({id:p,type:"circle",source:"language-families",filter:K,paint:{"circle-radius":j(I,1),"circle-color":ae,"circle-blur":.3,"circle-opacity":.45,"circle-stroke-width":0,...Oo(B)}},"language-family-pins");for(const T of f)B.addLayer({id:T,type:"circle",source:"language-families",filter:K,paint:{"circle-radius":j(I,1),"circle-radius-transition":H,"circle-color":"rgba(0,0,0,0)","circle-blur":0,"circle-opacity":1,"circle-stroke-width":2.5,"circle-stroke-color":ae,"circle-stroke-opacity":0,"circle-stroke-opacity-transition":H,...Oo(B)}},"language-family-pins")}}function te(){const B=s();if(B)for(const K of R)B.getLayer(K)&&B.removeLayer(K)}function D(B){const K=s();if(!K)return;const ae=[">",["get","peoplePraying"],0],_e=B?["all",ae,B]:ae;for(const T of R)if(K.getLayer(T))try{K.setFilter(T,_e)}catch{}}function be(){const B=s();if(B){if(q(),d&&(cancelAnimationFrame(d),d=null),h=!0,B.getLayer("language-family-pins"))try{B.setLayoutProperty("language-family-pins","circle-sort-key",["case",[">=",["get","peoplePraying"],Do],1,[">",["get","peoplePraying"],0],1,3])}catch{}Ae(B),$e()}}function $e(){d&&(cancelAnimationFrame(d),d=null);const B=++y;let K=performance.now(),ae=-1/0;function _e(T){if(!h||B!==y)return;const N=s();if(!N||!R.some(Z=>N.getLayer(Z))){d=null;return}if(T-ae "u")return;const K=typeof B.getContainer=="function"?B.getContainer():null;if(K)try{A=new IntersectionObserver(ae=>{for(const _e of ae)E=_e.isIntersecting},{threshold:0}),A.observe(K)}catch{A=null,E=!0}}function pe(){if(A){try{A.disconnect()}catch{}A=null}E=!0}function le(){h=!1,d&&(cancelAnimationFrame(d),d=null),pe();try{te();const B=s();B&&B.getLayer("language-family-pins")&&B.setLayoutProperty("language-family-pins","circle-sort-key",void 0)}catch{}}return{layersAdded:r,regionIsoMap:l,addLanguageFamilyLayer:c,addRegionsLayer:u,addFamilyConnectionLines:b,removeFamilyConnectionLines:m,updateLayerSource:L,removeLayer:x,hasLayer:_,setPaintProperty:g,mapEvents:i,startPrayerGlow:be,stopPrayerGlow:le,syncGlowFilter:D,cleanup:$}}function Bt(e,t){if(!Array.isArray(e))return e;switch(e[0]){case"get":return t==null?void 0:t[e[1]];case"literal":return e[1];case"has":return t!=null&&e[1]in t;case"!has":return t==null||!(e[1]in t);case"!":return!Bt(e[1],t);case"coalesce":{for(let n=1;n 2){const s=Number(Bt(e[2],t));return Number.isFinite(s)?s:0}return NaN}case"to-string":{const n=Bt(e[1],t);return n==null?"":String(n)}case"to-boolean":return!!Bt(e[1],t);case"slice":{const n=Bt(e[1],t),a=Bt(e[2],t),s=e.length>3?Bt(e[3],t):void 0;return typeof n!="string"?"":s!=null?n.slice(a,s):n.slice(a)}default:return e}}function ba(e,t){if(e===!0)return!0;if(e===!1)return!1;if(!Array.isArray(e))return!!e;const o=e[0];if(o==="all"){for(let n=1;n "||o===">="||o==="<"||o==="<="){const n=typeof e[1]=="string"?t==null?void 0:t[e[1]]:Bt(e[1],t),a=Bt(e[2],t);switch(o){case"==":return n===a||n==null&&a==null;case"!=":return!(n===a||n==null&&a==null);case">":return n>a;case">=":return n>=a;case"<":return ni!=null&&i!==!0);for(const i of Ei(e,t)){const r=i&&i.properties;if(!r||s.length&&!s.every(c=>ba(c,r)))continue;const l=r[n];l!=null&&a.add(String(l))}return a}const tv=Object.freeze(Object.defineProperty({__proto__:null,matchesExpression:ba,readSourceFeatures:Ei,resolveExpr:Bt,selectedPinIds:ev},Symbol.toStringTag,{value:"Module"})),ko={style:"mapbox://styles/mapbox/standard",center:[20,10],zoom:1.8,minZoom:.5,maxZoom:22,pitch:20,bearing:0,projection:"mercator",renderWorldCopies:!0};function ov(e={}){const{getMap:t=()=>null,mapId:o="unknown",mapStore:n=null,regionsGeoData:a=null,defaultCenter:s=(ko==null?void 0:ko.center)||[20,10],defaultZoom:i=ko==null?void 0:ko.zoom}=e,r=()=>{try{return typeof window<"u"&&!!window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches}catch{return!1}};function l(C){const H=r(),E=(A={})=>{const z={};for(const I of["center","zoom","bearing","pitch","padding"])A[I]!==void 0&&(z[I]=A[I]);return C.jumpTo(z)};return{flyTo:A=>H?E(A):C.flyTo(A),easeTo:A=>H?E(A):C.easeTo(A),fitBounds:(A,z={})=>C.fitBounds(A,H?{...z,animate:!1,duration:0}:z)}}const c={speed:.5,essential:!0,curve:1.2};function u(C,H,E={x:150,y:0}){const A=t();if(!A)return C;try{const z=A.getCanvas(),I=z.width,j=z.height,q=A.project(C),te=window.devicePixelRatio||1,D=(E.x||0)*te,be=(E.y||0)*te,$e={x:q.x-D,y:q.y-be},Ae=A.unproject($e);return[Ae.lng,Ae.lat]}catch{return C}}function b(C,H=[]){const E=t();if(!E)return;let A=null,z=5;const I=H.filter(q=>q.languageFamily===C);if(I.length>0){let q=1/0,te=-1/0,D=1/0,be=-1/0;if(I.forEach($e=>{$e.longitude&&$e.latitude&&(q=Math.min(q,$e.longitude),te=Math.max(te,$e.longitude),D=Math.min(D,$e.latitude),be=Math.max(be,$e.latitude))}),q!==1/0){A=[(q+te)/2,(D+be)/2];const $e=te-q,Ae=be-D,pe=Math.max($e,Ae);pe>50?z=2:pe>20?z=3:pe>10?z=4:pe>5?z=5:z=6}}if(!A)return;const j=u(A);l(E).flyTo({center:j,zoom:z,...c})}function m(C){const H=t();if(!H)return;let E=null,A=4;const z=q0(C);if(z)E=z.center,A=z.zoom||4;else if(a!=null&&a.features){const j=a.features.find(q=>{var te,D;return((te=q.properties)==null?void 0:te.region)===C||((D=q.properties)==null?void 0:D.name)===C});j&&(E=L(j))}if(!E)return;const I=u(E);l(H).flyTo({center:I,zoom:A,...c})}function L(C){if(!C||!C.geometry)return null;const{type:H,coordinates:E}=C.geometry;let A=[];if(H==="Polygon"?A=E[0]||[]:H==="MultiPolygon"&&E.forEach(j=>{A=A.concat(j[0]||[])}),A.length===0)return null;let z=0,I=0;return A.forEach(j=>{z+=j[0],I+=j[1]}),[z/A.length,I/A.length]}function x(C,H={}){const E=t();if(!E)return;const A={center:C,...c,...H};l(E).flyTo(A)}function _(C,H,E={}){const A=t();if(!A||(C==null?void 0:C.longitude)==null||(C==null?void 0:C.latitude)==null)return;const z=H??Math.min(A.getZoom()+2,8);x([C.longitude,C.latitude],{...E,zoom:z})}function g(C,H={top:40,right:40,bottom:40,left:40},E={}){const A=t();if(!A||!C)return;const z=E.maxZoom??4,I=E.duration??900;l(A).fitBounds(C,{padding:H,maxZoom:z,duration:I,...c,...E})}function $(){const C=t();C&&l(C).flyTo({center:s,zoom:i,...c})}const d=80,h=800,y=5,p=5,k=110,f=1.2;function R(C){const H=C.filter(Number.isFinite).slice().sort((_e,T)=>_e-T),E=H.length;if(!E)return null;let A=-1,z=0;for(let _e=0;_e A&&(A=N,z=_e)}const I=(z+1)%E,j=new Array(E);for(let _e=0;_e 0&&T ((_e+180)%360+360)%360-180,Ae=$e(D),pe=24,le=360/pe,B=new Array(pe).fill(0);for(const _e of H){let T=Math.floor(((_e+180)%360+360)%360/le);T>=pe&&(T=pe-1),B[T]++}let K=0;for(let _e=1;_e B[K]&&(K=_e);return{center:$e(-180+(K+.5)*le),minLng:Ae,maxLng:Ae+(be-D),span:be-D}}function v(C){try{const H=C.getContainer&&C.getContainer().clientWidth||0,E=H>0?Math.log2(H/512):f,A=typeof C.getMinZoom=="function"?C.getMinZoom():E;return Math.max(A,E)}catch{return f}}function U(C,H,E,A){l(C).easeTo({center:[H,(E+A)/2],zoom:v(C),duration:h,essential:!0})}function X(C,H,E){if(E>k)return!0;try{const A=typeof C.cameraForBounds=="function"?C.cameraForBounds(H,{padding:d,maxZoom:y}):null;return A==null?!0:typeof A.zoom=="number"&&A.zoom<=v(C)+.05}catch{return!1}}function Q(C,H={}){var B,K;const E=t();if(!E||!C)return;if(H.regionPolygonSource&&E.getSource(H.regionPolygonSource)){const ae=Ei(E,H.regionPolygonSource),_e=C.id??C.label,T=ae.find(N=>{const F=(N==null?void 0:N.properties)||{};return F.region===_e||F.name===_e||F.id===_e});if(T){const N=[];let F=1/0,Z=-1/0;const ce=oe=>{for(const ge of oe)Array.isArray(ge[0])?ce(ge):Number.isFinite(ge[0])&&Number.isFinite(ge[1])&&(N.push(ge[0]),ge[1] Z&&(Z=ge[1]))};(B=T.geometry)!=null&&B.coordinates&&ce(T.geometry.coordinates);const G=R(N);if(G&&Number.isFinite(F)){const oe=[[G.minLng,F],[G.maxLng,Z]];X(E,oe,G.span)?U(E,G.center,F,Z):l(E).fitBounds(oe,{padding:d,maxZoom:y,duration:h,essential:!0});return}}}const A=H.sourceId??"language-families";if(!E.getSource(A))return;const z=H.matchExpr??C.filter;if(!z)return;const I=Ei(E,A);if(!I.length)return;const j=[];let q=1/0,te=-1/0,D=0,be=0,$e=0;const Ae=new Set;for(const ae of I){const _e=ae==null?void 0:ae.properties;if(!ba(z,_e))continue;const T=_e==null?void 0:_e.uniqueId;if(T!=null){if(Ae.has(T))continue;Ae.add(T)}const N=(K=ae.geometry)==null?void 0:K.coordinates;if(!Array.isArray(N)||N.length<2)continue;const F=N[0],Z=N[1];!Number.isFinite(F)||!Number.isFinite(Z)||(D===0&&(be=F,$e=Z),j.push(F),Z te&&(te=Z),D++)}if(D===0)return;if(D===1){l(E).flyTo({center:[be,$e],zoom:p,duration:h,essential:!0});return}const pe=R(j)||{center:0,minLng:0,maxLng:0,span:0},le=[[pe.minLng,q],[pe.maxLng,te]];if(X(E,le,pe.span)){U(E,pe.center,q,te);return}try{l(E).fitBounds(le,{padding:d,maxZoom:y,duration:h,essential:!0})}catch{}}return{getOffsetCenter:u,slowFlyToFamily:b,slowFlyToRegion:m,flyTo:x,flyToCoords:_,fitBounds:g,resetView:$,zoomToLegendRow:Q,calculateRegionCenterFromPolygon:L}}const Ti="#1a1a1a",an="selected-pin-highlight";function nv(e=Ti){const t=document.createElement("div");t.className="map-go-pin-wrapper";const o=`pinGrad-${Math.random().toString(36).slice(2,8)}`;return t.innerHTML=`