From 0756c8cc5c92f2c172110af53f5792557a1607e8 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 14:11:52 +0000
Subject: [PATCH 01/69] Collect the gospel set-chart layout check in gates:pre
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The check itself shipped with the plugin modules it covers, but the line that
runs it existed only in a message, which is not a place code lives. So it sat in
internal/ and would never have run — the same failure it was written to prevent,
one layer up.
This is the whole registration in one commit: the file, the script entry and the
word in the gates:pre list. It imports boundedSet.js and centeredSet.js, so it
must land after the plugin commits that add them; landing it first fails
collection and takes gates:pre red for everyone.
---
internal/check-gospel-set-charts.mjs | 88 ++++++++++++++++++++++++++++
package.json | 3 +-
2 files changed, 90 insertions(+), 1 deletion(-)
create mode 100644 internal/check-gospel-set-charts.mjs
diff --git a/internal/check-gospel-set-charts.mjs b/internal/check-gospel-set-charts.mjs
new file mode 100644
index 000000000..5756e6722
--- /dev/null
+++ b/internal/check-gospel-set-charts.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/**
+ * check-gospel-set-charts.mjs — the bounded-set and centered-set LAYOUT maths.
+ *
+ * Both modules are pure, which is why they can be checked here at all: a pure function's
+ * inputs ARE its domain, so these assertions are evidence about the real module and not
+ * about a fixture that resembles one. What they are NOT evidence about is the shape of
+ * near-neighbors.json — that binding is checked where the data is read, not here.
+ *
+ * The properties worth pinning are the ones where a wrong answer still LOOKS like a chart:
+ *
+ * · bounded set — nothing may sit ON the boundary. A dot on the line is the one thing a
+ * bounded set cannot express, and it would read as a group that is somehow both.
+ * · bounded set — a group of UNKNOWN reached-ness is its own outcome. If it were folded
+ * into `outside`, a join failure would render as "unreached" and inflate the count with
+ * something that is not a fact about the group.
+ * · centered set — a group with no ring is UNPLACED, never defaulted to the outer ring.
+ * Ring 10 is a real answer ("beyond"), so it must not double as a bin for missing data.
+ * · centered set — a REACHED group (ring 0) is the CENTRE, not unplaced. An earlier build
+ * spelled "reached" and "no relation found" with the same 0; treating that value as
+ * missing data would report the best outcome in the set as an absence. The three states
+ * are asserted apart, and a reached group is asserted to reach NO ring at all.
+ * · centered set — the arrow stops exactly at innerR. An arrow that overshoots covers the
+ * cross it is meant to point at.
+ * · both — layout is deterministic, so a group does not move between renders.
+ */
+import { layoutBoundedSet, boundedSetTally } from '../plugin-designs/gospel-internet/src/boundedSet.js'
+import { layoutCenteredSet, ringTally, inwardArrow, ringRadii, RING_MAX, RING_REACHED, RING_NO_RELATION } from '../plugin-designs/gospel-internet/src/centeredSet.js'
+
+const fail = []
+const ok = (label, cond, detail = '') => {
+ if (cond) console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`)
+ else { console.log(` ✗ ${label}${detail ? ` — ${detail}` : ''}`); fail.push(label) }
+}
+
+console.log('GOSPEL-SET-CHARTS')
+
+// ── bounded set ─────────────────────────────────────────────────────────────
+const bg = [{ id: 'a', reached: true }, { id: 'b', reached: false }, { id: 'c' }, { id: 'd', reached: null }]
+const B = layoutBoundedSet(bg, { r: 100 })
+const bt = boundedSetTally(B)
+ok('bounded: reached inside, unreached outside, unknown neither', bt.inside === 1 && bt.outside === 1 && bt.unknown === 2,
+ `in ${bt.inside} out ${bt.outside} unknown ${bt.unknown}`)
+ok('bounded: unknown is NOT folded into outside', bt.outside === 1 && bt.unknown === 2)
+ok('bounded: total is conserved', bt.total === bg.length)
+const dist = (p) => Math.hypot(p.x, p.y)
+ok('bounded: nothing sits ON the boundary', dist(B.inside[0]) < 100 && dist(B.outside[0]) > 100,
+ `in r=${dist(B.inside[0]).toFixed(1)} out r=${dist(B.outside[0]).toFixed(1)}`)
+ok('bounded: deterministic between renders', layoutBoundedSet(bg, { r: 100 }).inside[0].x === B.inside[0].x)
+
+// ── centered set ────────────────────────────────────────────────────────────
+const cg = [{ id: 'a', ring: 1, neighborId: 'r1' }, { id: 'b', ring: RING_MAX, neighborId: 'r2' },
+ { id: 'c', ring: RING_REACHED }, { id: 'd' }, { id: 'e', ring: RING_MAX + 1 }, { id: 'f', ring: 5 }]
+const C = layoutCenteredSet(cg)
+const ct = ringTally(C)
+ok('centered: valid rings placed, unusable ones unplaced, reached at the centre',
+ ct.placed === 3 && ct.unplaced === 2 && ct.centre === 1,
+ `placed ${ct.placed} centre ${ct.centre} unplaced ${ct.unplaced}`)
+ok('centered: a missing ring is NOT defaulted onto the outer ring',
+ C.placed.filter((p) => p.ring === RING_MAX).length === 1)
+ok('centered: every unplaced group carries a reason', C.unplaced.every((u) => typeof u.reason === 'string' && u.reason))
+ok('centered: rings are ordered inward to outward', ringRadii().length === RING_MAX &&
+ C.placed.find((p) => p.ring === 1).r < C.placed.find((p) => p.ring === RING_MAX).r)
+const p1 = C.placed.find((p) => p.ring === 1)
+ok('centered: a point lies exactly on its own ring', Math.abs(Math.hypot(p1.x, p1.y) - p1.r) < 1e-9)
+const arrow = inwardArrow(C.placed.find((p) => p.ring === RING_MAX))
+ok('centered: the inward arrow stops at innerR and does not cover the cross',
+ arrow && Math.abs(Math.hypot(arrow.x2, arrow.y2) - 26) < 1e-9, `ends at r=${Math.hypot(arrow.x2, arrow.y2).toFixed(2)}`)
+ok('centered: no recorded neighbour means NO arrow, not a guessed one', inwardArrow({ x: 10, y: 0 }) === null)
+ok('centered: deterministic between renders', layoutCenteredSet(cg).placed[0].x === C.placed[0].x)
+
+// the three ring states are three different facts and must not collapse into one another
+const sg = [{ id: 'p', ring: 1, neighborId: 'r' }, { id: 'reached', ring: RING_REACHED },
+ { id: 'norel', ring: RING_NO_RELATION }, { id: 'noring' }]
+const S = layoutCenteredSet(sg)
+const st = ringTally(S)
+ok('centered: reached (ring 0) is the CENTRE, not unplaced', st.centre === 1 && S.centre[0].id === 'reached')
+ok('centered: no-relation (ring -1) is unplaced with its OWN reason',
+ S.unplaced.find((u) => u.id === 'norel')?.reason === 'no relation found')
+ok('centered: the three states stay apart', st.placed === 1 && st.centre === 1 && st.unplaced === 2,
+ `placed ${st.placed} centre ${st.centre} unplaced ${st.unplaced}`)
+ok('centered: a reached group reaches NO ring', [...st.counts.values()].reduce((a, b) => a + b, 0) === st.placed)
+
+if (fail.length) {
+ console.log(`\nGOSPEL-SET-CHARTS: FAIL — ${fail.length}: ${fail.join(', ')}`)
+ process.exit(1)
+}
+console.log('\nGOSPEL-SET-CHARTS: PASS — both layouts pinned on the properties that fail silently')
diff --git a/package.json b/package.json
index b5b22adcb..908e9c725 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"check:experimental-shelf": "node internal/check-experimental-shelf.mjs",
"check:export-poster-prints": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs",
"check:export-poster-prints:maplibre": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs --provider maplibre",
+ "check:gospel-set-charts": "node internal/check-gospel-set-charts.mjs",
"check:facet-cascade": "node internal/check-facet-cascade.mjs",
"check:facet-cascade:offline": "node internal/check-facet-cascade.mjs --offline",
"check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test",
@@ -267,7 +268,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels",
- "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields",
+ "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:gospel-set-charts check:hardcoded-english:self-test check:list-selection check:selector-fields",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
From 7308d4312a3f33e7c1e28d0d010e9504f730239f Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 16:41:13 +0000
Subject: [PATCH 02/69] Shape both set charts as tiered points, and leave the
projection to the caller
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The shared whiteboard layer draws a point collection where each feature carries
an id, a label, a tier and a priority, and stages its label layers by tier and
zoom. This decides which features exist, what tier each belongs to and how they
rank — which is this chart's business — and it does not convert to degrees.
Ring and band labels are placed on the radii the chart already draws rather than
on a second calculation, so the label and the ring it names cannot drift apart.
Groups rank by population, so when two names collide the larger people wins: a
reader zoomed out should meet the names carrying the most people, not whichever
was drawn first. Bands outrank rings outrank groups, because that is the order a
reader zooming in meets them.
Coordinates out are abstract on purpose. Converting them is a projection, not a
scale — the whiteboard shrinks degree height outward so the map's own stretch
cancels it, and a linear scale skips the cancellation. Measured on a ring of 720
points: with a linear scale the rendered aspect is 0.9968 at the inner ring and
0.7952 at the outer, so the target draws as ovals that worsen outward, worst
exactly where the ring labels sit. With the projection it is 1.0000 at every
ring.
That measurement also settles where the guard belongs: at the inner ring the
linear error is three parts in a thousand and would pass any sane tolerance. An
assertion placed there alone would be inert against the one mistake it exists to
catch. It goes at the outer ring, and at both.
---
.../gospel-internet/src/setChartFeatures.js | 82 +++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 plugin-designs/gospel-internet/src/setChartFeatures.js
diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js
new file mode 100644
index 000000000..d88dfba91
--- /dev/null
+++ b/plugin-designs/gospel-internet/src/setChartFeatures.js
@@ -0,0 +1,82 @@
+/**
+ * setChartFeatures.js — the set charts as TIERED POINTS, in abstract coordinates.
+ *
+ * The shared whiteboard layer draws a point FeatureCollection where each feature carries an id,
+ * a label, a tier and a priority `k`, and stages its label layers by tier and zoom. This turns
+ * my two layouts into that shape. It decides WHICH features exist, what tier each belongs to and
+ * how they rank against each other — which is mine — and it does NOT project.
+ *
+ * ★ COORDINATES OUT ARE ABSTRACT (u, v), NOT DEGREES, AND THAT IS DELIBERATE. Converting to
+ * lng/lat is a PROJECTION, not a scale: the whiteboard's inverse-Mercator shrinks the degree
+ * height as you move out so the map's own 1/cos(lat) stretch cancels it. A linear scale skips
+ * that cancellation and the error grows outward — 1.0154x at 10 degrees, 1.0642x at 20, 1.1547x
+ * at 30. For concentric rings that renders as vertical OVALS, worst at the outer ring where the
+ * labels sit, and it looks plausible enough to waste an afternoon on. So the caller passes these
+ * through the shared projection with the chart's extent; this module never guesses at degrees.
+ *
+ * Extents (the caller's one number): centered set 228 = innerR 30 + 9 x ringGap 22; bounded 260.
+ */
+import { ringInfo } from './ringInfo.js'
+
+/** Tier names, outermost-first in label priority: bands read zoomed out, groups zoomed in. */
+export const TIERS = { BAND: 'band', RING: 'ring', GROUP: 'group' }
+
+/**
+ * The centered-set target as tiered points.
+ *
+ * `placed` are laid-out groups ({ id, label, ring, x, y }); `radii` is ringRadii()'s output, so
+ * the ring and band labels sit on the SAME radii the chart draws rather than on a second
+ * calculation that could drift from it.
+ *
+ * `k` is the priority the shared layer sorts labels by. Groups rank by population, so when two
+ * names collide the bigger people wins — a reader zoomed out should see the names that carry the
+ * most people, not whichever happened to be drawn first. Bands and rings outrank every group.
+ */
+export function centeredSetFeatures(placed, radii, { bandLabel, ringLabel, population } = {}) {
+ const out = []
+ for (const p of placed || []) {
+ out.push({ id: p.id, label: p.label, tier: TIERS.GROUP, ring: p.ring,
+ k: Number(population?.(p) ?? 0), u: p.x, v: p.y })
+ }
+ // One label per ring, placed on the ring itself at a fixed angle so it does not wander between
+ // renders. Rings outrank groups; the inner rings outrank the outer, because a reader zooming in
+ // meets them in that order.
+ for (const { ring, r } of radii || []) {
+ const info = ringInfo(ring)
+ if (!info) continue
+ out.push({ id: `ring-${ring}`, label: ringLabel?.(ring) ?? `Ring ${ring}`, tier: TIERS.RING,
+ ring, k: 1e9 - ring, u: 0, v: -r })
+ }
+ // One label per band, at the outer edge of the band's own rings.
+ const bands = new Map()
+ for (const { ring, r } of radii || []) {
+ const info = ringInfo(ring)
+ if (!info) continue
+ const cur = bands.get(info.band)
+ if (!cur || r > cur) bands.set(info.band, r)
+ }
+ for (const [band, r] of bands) {
+ out.push({ id: `band-${band}`, label: bandLabel?.(band) ?? band, tier: TIERS.BAND,
+ k: 2e9, u: 0, v: -r })
+ }
+ return out
+}
+
+/**
+ * The bounded set as tiered points, plus the boundary circle as a dish.
+ *
+ * ★ THE GAP EITHER SIDE OF THE BOUNDARY IS THE CHART'S ONE CLAIM and it must survive projection:
+ * inside points sit within 0.92r, outside beyond 1.12r, and nothing may land ON the line — a dot
+ * on the boundary is the one thing a bounded set cannot express. The dish radius is the boundary
+ * itself, so the caller projects all three consistently or the gap closes on one side.
+ */
+export function boundedSetFeatures(layout, { label, population } = {}) {
+ const out = []
+ for (const side of ['inside', 'outside']) {
+ for (const p of layout?.[side] || []) {
+ out.push({ id: p.id, label: label?.(p) ?? p.label, tier: TIERS.GROUP, side,
+ k: Number(population?.(p) ?? 0), u: p.x, v: p.y })
+ }
+ }
+ return { features: out, dish: { u: layout?.cx ?? 0, v: layout?.cy ?? 0, r: layout?.r ?? 0 } }
+}
From 1755694a087e7068425ba322eb251df4c21f91ae Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 16:51:39 +0000
Subject: [PATCH 03/69] Project the set-chart features onto the whiteboard, and
report the real ceiling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The projection is passed in rather than built here, so the chart and its checks
share one instance and this module still owes nothing to a coordinate system it
does not define. The dish takes its radius from the projection's exposed scale:
deriving it by projecting two points and subtracting would be right on the
equator and wrong everywhere else.
The guard reports the outermost latitude, because that is the real limit and
shape is not. The projection is the exact inverse of the renderer's transform, so
a circle stays a circle at every extentFraction — measured at 0.000% aspect
error. An earlier reading of mine said otherwise and was wrong: it computed
screen x as longitude times cos(latitude), a plate-carree correction Web Mercator
never applies, which manufactured a distortion the renderer does not produce.
What does bite is the pole. The outermost point reaches 52 degrees at the default
0.17, 77 at 0.35, 83 at 0.45, and Web Mercator is undefined at the poles with
engines clamping near 85.
That ceiling is a property of extentFraction alone, not of a chart's extent:
the scale is extentFraction over extent, so the outermost point is at
extentFraction whatever the extent, and every chart reaches the same latitude.
Measured across extents 100, 228, 260 and 1000 — identical to three decimals. So
a consumer cannot buy headroom by choosing a bigger extent, and a check written
against one chart's ceiling holds for all of them.
---
.../gospel-internet/src/setChartFeatures.js | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js
index d88dfba91..14c85bb7e 100644
--- a/plugin-designs/gospel-internet/src/setChartFeatures.js
+++ b/plugin-designs/gospel-internet/src/setChartFeatures.js
@@ -80,3 +80,41 @@ export function boundedSetFeatures(layout, { label, population } = {}) {
}
return { features: out, dish: { u: layout?.cx ?? 0, v: layout?.cy ?? 0, r: layout?.r ?? 0 } }
}
+
+/**
+ * Project abstract (u, v) features into a GeoJSON point collection for the whiteboard layer.
+ *
+ * `project` is the shared whiteboardProjection for this chart's extent. It is passed in rather
+ * than built here so the chart and its checks use ONE projection instance, and so this module
+ * still owes nothing to a coordinate system it does not define.
+ *
+ * ⚠ THE LATITUDE CEILING IS THE REAL LIMIT, not shape. The projection is the exact inverse of
+ * the renderer's transform, so a circle stays a circle at every extentFraction — measured at
+ * 0.000% aspect error. What DOES bite is how close to the pole the drawing lands: at extent 260
+ * the outermost point reaches 52 degrees at the default 0.17, 77 at 0.35, 83 at 0.45, and Web
+ * Mercator is undefined at the poles with engines clamping near 85. So a caller that widens
+ * extentFraction to let readers zoom out further runs into the clamp, not into distortion.
+ * `maxAbsLat` reports it so a check can assert the drawing stays inside that range.
+ */
+export function toFeatureCollection(features, project) {
+ let maxAbsLat = 0
+ const out = []
+ for (const f of features || []) {
+ const [lng, lat] = project(f.u, f.v)
+ if (Math.abs(lat) > maxAbsLat) maxAbsLat = Math.abs(lat)
+ const { u, v, ...props } = f
+ out.push({ type: 'Feature', id: f.id, geometry: { type: 'Point', coordinates: [lng, lat] }, properties: props })
+ }
+ return { collection: { type: 'FeatureCollection', features: out }, maxAbsLat }
+}
+
+/**
+ * A radius in DEGREES for the dish, from a radius in layout units.
+ *
+ * The dish is a circle-radius in the paint, so it needs the projection's scale rather than a
+ * projected point. project.K is exposed for exactly this; deriving it by projecting two points
+ * and subtracting would be right along the equator and wrong everywhere else.
+ */
+export function dishRadiusDegrees(r, project) {
+ return Number(r || 0) * (project?.K ?? 0) * 360
+}
From f75fbbd4a2bcaef2763cbb8c77b582945833c465 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 16:55:07 +0000
Subject: [PATCH 04/69] Draw the centered set on the whiteboard, so it pans,
zooms and labels by tier
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The ask changed and so did the call. A static target is fine in SVG; a zoomable
one carrying ~2,100 labels is not. The engine tiles the collection per zoom so it
draws less when zoomed out, and its symbol layers do label collision, which is
the part that would take longest to do badly by hand.
The label staging is one layer per tier with its own minzoom: band names when
zoomed out, ring names in the middle, group names in close. None of that is
tree-shaped, which is why it is a shared layer rather than a copy of one.
Coordinates go through the shared projection, never a scale. It is the exact
inverse of the renderer's transform, so a circle stays a circle; a linear scale
would draw concentric rings as ovals worsening outward. The only number this
chart owns is its extent.
Dimming a deselected ring is a paint expression rather than a filter, for the
reason the SVG version dims instead of hiding: a ring emptied of its dots reads
as a ring with no groups, which is the opposite of what a reader selecting it
just asked for.
The layer id is read back from the handle rather than written from memory. The
first version reached for a name the module does not use; inside a try/catch that
does not throw, so the dimming would simply never have happened — a legend that
appears to do nothing rather than an error. Asking the handle means a rename
cannot silently disable it.
---
.../src/components/GiCenteredSetMap.vue | 137 ++++++++++++++++++
1 file changed, 137 insertions(+)
create mode 100644 plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
new file mode 100644
index 000000000..8642734cc
--- /dev/null
+++ b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+ {{ T('gospelInternet.centered.failed', 'The neighbour table could not be loaded, so this chart is not drawn.') }}
+ {{ failed }}
+
+
+
From 540bcd999e20c1792964e59f1555624094c9e5e6 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 16:57:40 +0000
Subject: [PATCH 05/69] Show the centered set as the whiteboard map rather than
a static figure
The panel now mounts the zoomable chart in place of the SVG. Pan, zoom and
labels that appear by tier as the reader moves in were the ask; the engine
supplies tiling and label collision that an SVG version would have to
reimplement, and badly.
The printed component stays SVG, which is not an inconsistency: a sheet has no
zoom and no hover, so paper and screen genuinely want different renderers. What
they share is the ring vocabulary, so the two cannot drift apart in what they
call a ring even though they draw it differently.
---
.../src/components/GiCenteredSet.vue | 23 ++++++-------------
1 file changed, 7 insertions(+), 16 deletions(-)
diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
index c48c14589..b60a377b5 100644
--- a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
@@ -26,6 +26,7 @@ import { useGiT } from '../useGiT.js'
import { loadNearRows, indexFor } from '../nearRows.js'
import { layoutCenteredSet, ringTally, inwardArrow, ringRadii } from '@map/components/charts/centeredSet.js'
import GiRingLegend from './GiRingLegend.vue'
+import GiCenteredSetMap from './GiCenteredSetMap.vue'
const T = useGiT()
const board = useBoardState()
@@ -112,22 +113,12 @@ useShadowStyles(
-
-
- {{ T('gospelInternet.centered.caption', 'Each group sits at its distance from a reached neighbour; arrows point at the one that places it.') }}
- {{ fmt(tally.placed) }}
-
+
+
Date: Mon, 28 Sep 2026 17:02:03 +0000
Subject: [PATCH 06/69] Put the bounded set on the whiteboard too, with its
boundary in map units
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both set charts now pan and zoom. The chart still makes one claim — which side
of the line a group is on — so nothing here encodes distance or rank; what zoom
buys is reading the names, because at two thousand dots the inside is a cluster
until you can get close to it.
The boundary is the shared dish layer, which is the same layer a language
family's circle uses, so it is not a new type for either of us. Its radius takes
an exponential-base-2 zoom interpolation rather than a fixed pixel value.
circle-radius is screen pixels: a constant would hold its size while the dots
spread out from under it, leaving the circle correct at exactly one zoom and
wrong at every other — and for a dot near the edge the inside/outside reading
would silently invert, which is the only thing this chart says. Pixels per degree
double with each zoom level, measured at exactly 2.000000 between adjacent
levels, so the interpolation tracks map units exactly rather than approximately.
The boundary is drawn as a line with a transparent fill. A filled disc would say
the inside is a region, when what the chart means is a threshold.
The cross rides in the point collection as its own tier rather than as an
overlay, so it pans and zooms with the drawing instead of floating above it.
---
.../src/components/GiBoundedSet.vue | 30 ++---
.../src/components/GiBoundedSetMap.vue | 108 ++++++++++++++++++
.../gospel-internet/src/setChartFeatures.js | 17 +++
3 files changed, 136 insertions(+), 19 deletions(-)
create mode 100644 plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue
diff --git a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
index 5c622642c..5f4777e1a 100644
--- a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
@@ -28,7 +28,12 @@ import { formatCount } from '@map/utils/formatCount.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { loadNearRows, indexFor } from '../nearRows.js'
+<<<<<<< HEAD
import { layoutBoundedSet, boundedSetTally } from '@map/components/charts/boundedSet.js'
+=======
+import { layoutBoundedSet, boundedSetTally } from '../boundedSet.js'
+import GiBoundedSetMap from './GiBoundedSetMap.vue'
+>>>>>>> ea8dbe45 (Put the bounded set on the whiteboard too, with its boundary in map units)
const T = useGiT()
const board = useBoardState()
@@ -73,7 +78,7 @@ useShadowStyles(
// can never push the figure out of its pane the way the legend did on the centered set.
'.gbs{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +
'justify-content:center;padding:46px 10px 10px;box-sizing:border-box;overflow:hidden;background:#f3f3f1}' +
- '.gbs svg{max-width:100%;max-height:100%}' +
+ '.gbs-fig{position:relative;flex:1 1 auto;align-self:stretch;min-height:0;min-width:0}' +
'.gbs-ring{fill:none;stroke:#3b463d;stroke-width:1.5}' +
'.gbs-in{fill:#92b195}.gbs-out{fill:#6d766e;opacity:.65}' +
'.gbs-cross-halo{stroke:var(--gbs-bg,#f3f3f1);stroke-width:7;stroke-linecap:square}' +
@@ -91,24 +96,11 @@ useShadowStyles(
{{ failed }}
-
+
+
+ {{ T('gospelInternet.bounded.failed', 'The reached/unreached table could not be loaded, so this chart is not drawn.') }}
+ {{ failed }}
+
+
+
diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js
index 14c85bb7e..66bc15a47 100644
--- a/plugin-designs/gospel-internet/src/setChartFeatures.js
+++ b/plugin-designs/gospel-internet/src/setChartFeatures.js
@@ -118,3 +118,20 @@ export function toFeatureCollection(features, project) {
export function dishRadiusDegrees(r, project) {
return Number(r || 0) * (project?.K ?? 0) * 360
}
+
+/**
+ * A zoom expression that keeps a radius fixed in MAP units while the paint wants PIXELS.
+ *
+ * ★ circle-radius IS SCREEN PIXELS, and for the bounded set's boundary that is the wrong unit.
+ * A fixed pixel radius stays the same size on screen while the dots spread out from under it as
+ * the reader zooms — the circle would be right at exactly one zoom level and wrong at every
+ * other, with the inside/outside reading silently inverting for dots near the edge.
+ *
+ * The world is 512 x 2^zoom pixels across 360 degrees, so pixels-per-degree doubles with every
+ * zoom level — measured exactly 2.000000 between adjacent levels. An exponential-base-2
+ * interpolation therefore tracks map units exactly rather than approximately, with two stops.
+ */
+export function degreesToPixelRadius(degrees, { minZoom = 0, maxZoom = 22 } = {}) {
+ const px = (z) => Number(degrees || 0) * 512 * Math.pow(2, z) / 360
+ return ['interpolate', ['exponential', 2], ['zoom'], minZoom, px(minZoom), maxZoom, px(maxZoom)]
+}
From 957ae50ed085b86327ddeb8cce9f3707a5fdf91d Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 17:04:03 +0000
Subject: [PATCH 07/69] Pin the whiteboard geometry, and record what the poison
arm disproved
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three properties, each with the arm that catches its own defect: the bounded
set's gap survives projection, the drawing stays inside the Mercator clamp, and
a radius expressed in map units doubles with every zoom level.
The first arm taught me something I had assumed the opposite of. It narrowed the
gap to a tenth of a percent expecting the margins to go negative. They stayed
positive: the projection is conformal, so it preserves the ratio exactly, and a
narrow gap survives just as an eight percent one does. There is no distortion for
it to be eaten by.
So "the gap survives projection" is close to a tautology, and a check resting on
it alone would look like a guard while testing almost nothing. What can actually
fail is the ordering of the constants, and that is what the arm now poisons. The
narrowing case is kept as a passing assertion rather than deleted, because the
fact that it is safe is worth stating: the boundary cannot be eroded by the
projection, only by someone editing the numbers.
The clamp arm holds for every chart at once. The ceiling belongs to
extentFraction alone, not to a chart's extent — the scale is extentFraction over
extent and the outermost point sits at the extent, so the product is always the
fraction. Asserted across four extents, identical to three decimals.
---
internal/check-gospel-whiteboard.mjs | 111 +++++++++++++++++++++++++++
1 file changed, 111 insertions(+)
create mode 100644 internal/check-gospel-whiteboard.mjs
diff --git a/internal/check-gospel-whiteboard.mjs b/internal/check-gospel-whiteboard.mjs
new file mode 100644
index 000000000..4659b4b5a
--- /dev/null
+++ b/internal/check-gospel-whiteboard.mjs
@@ -0,0 +1,111 @@
+#!/usr/bin/env node
+/**
+ * check-gospel-whiteboard — the geometry the set charts depend on once they are drawn on a map.
+ *
+ * Three properties, each with the arm that would catch its own defect. None of them is about
+ * whether the chart renders; all three are about it rendering something TRUE.
+ *
+ * 1. THE BOUNDED SET'S GAP SURVIVES PROJECTION. Inside points sit within 0.92r and outside
+ * beyond 1.12r, and nothing may land on the boundary — a dot on the line is the one thing a
+ * bounded set cannot express. Asserted on PROJECTED coordinates. Note what the arm below
+ * found: the projection is conformal, so it preserves the ratio EXACTLY and a narrowed gap
+ * survives just as a wide one does. The boundary cannot be eroded by the projection, only by
+ * someone editing the constants — which is therefore what the arm poisons.
+ * 2. THE DRAWING STAYS INSIDE THE MERCATOR CLAMP. Shape does not degrade with extentFraction —
+ * the projection is the exact inverse of the renderer's transform, measured at 0.000% aspect
+ * error, and an earlier claim of mine that it did was an artefact of my own harness. What
+ * does bite is latitude: engines clamp near 85 degrees.
+ * 3. A RADIUS IN MAP UNITS DOUBLES WITH EVERY ZOOM LEVEL. circle-radius is screen pixels, so a
+ * boundary that does not scale is correct at one zoom and wrong at every other.
+ *
+ * ★ THE CEILING IS A PROPERTY OF extentFraction ALONE, not of a chart's extent: the scale is
+ * extentFraction/extent and the outermost point is at u = extent, so the product is always
+ * extentFraction. A consumer cannot buy headroom against the clamp by choosing a bigger extent,
+ * and one assertion covers every chart.
+ */
+import { layoutBoundedSet } from '../plugin-designs/gospel-internet/src/boundedSet.js'
+import { degreesToPixelRadius, dishRadiusDegrees, toFeatureCollection, centeredSetFeatures }
+ from '../plugin-designs/gospel-internet/src/setChartFeatures.js'
+import { layoutCenteredSet, ringRadii } from '../plugin-designs/gospel-internet/src/centeredSet.js'
+import { whiteboardProjection } from '../library/maps/whiteboard-projection.js'
+
+const fail = []
+const ok = (label, cond, detail = '') => {
+ if (cond) console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`)
+ else { console.log(` ✗ ${label}${detail ? ` — ${detail}` : ''}`); fail.push(label) }
+}
+const R2D = 180 / Math.PI
+// TRUE Web Mercator screen space: x is proportional to LONGITUDE ALONE. An earlier version of
+// this used lng*cos(lat), a plate-carree correction the renderer never applies, and it
+// manufactured a distortion that does not exist. Keep x = lng.
+const screen = (lng, lat) => [lng, Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)) * R2D]
+
+console.log('GOSPEL-WHITEBOARD')
+
+// ── 1. the boundary gap, on projected coordinates ───────────────────────────
+const R = 120, SPREAD = 260
+function gapMargins(extentFraction, inner = 0.92, outer = 1.12) {
+ const p = whiteboardProjection({ extent: SPREAD, extentFraction })
+ const d = (u, v) => { const [lng, lat] = p(u, v); const [x, y] = screen(lng, lat); return Math.hypot(x, y) }
+ let bMin = Infinity, bMax = -Infinity, inMax = -Infinity, outMin = Infinity
+ for (let i = 0; i < 720; i++) {
+ const a = i / 720 * 2 * Math.PI, c = Math.cos(a), s = Math.sin(a)
+ const b = d(c * R, s * R); bMin = Math.min(bMin, b); bMax = Math.max(bMax, b)
+ inMax = Math.max(inMax, d(c * R * inner, s * R * inner))
+ outMin = Math.min(outMin, d(c * R * outer, s * R * outer))
+ }
+ return { inside: bMin - inMax, outside: outMin - bMax }
+}
+const m = gapMargins(0.17)
+ok('bounded: no inside point reaches the boundary', m.inside > 0, `margin ${m.inside.toFixed(3)}`)
+ok('bounded: no outside point reaches the boundary', m.outside > 0, `margin ${m.outside.toFixed(3)}`)
+// ★ THE ARM, AND WHAT WRITING IT TAUGHT ME. My first version narrowed the gap to 0.999/1.001 and
+// expected the margins to go negative. THEY DID NOT — they stayed positive at 0.028. The
+// projection is conformal, so it preserves the RATIO exactly; a gap of a tenth of a percent
+// survives projection just as a gap of eight percent does. There is no distortion for a narrow
+// gap to be eaten by.
+// That means "the gap survives projection" is very nearly a tautology, and a check resting on it
+// alone would look like a guard while testing almost nothing. What CAN actually fail is the
+// ORDERING of the constants — someone editing 0.92/1.12 to the wrong side of 1 — so that is what
+// the arm poisons. Recorded rather than quietly swapped, because the fact that narrowing is safe
+// is itself worth knowing: it says the boundary cannot be eroded by the projection, only by us.
+const inverted = gapMargins(0.17, 1.01, 0.99)
+ok('bounded: inverting the constants FAILS (so the guard is not inert)',
+ inverted.inside <= 0 || inverted.outside <= 0,
+ `inverted margins ${inverted.inside.toFixed(3)} / ${inverted.outside.toFixed(3)}`)
+const narrow = gapMargins(0.17, 0.999, 1.001)
+ok('bounded: a narrowed gap still survives, because the projection preserves ratios exactly',
+ narrow.inside > 0 && narrow.outside > 0, `margins ${narrow.inside.toFixed(3)} / ${narrow.outside.toFixed(3)}`)
+
+// ── 2. the latitude ceiling ─────────────────────────────────────────────────
+const EXTENT_C = 30 + 9 * 22
+const feats = centeredSetFeatures(
+ layoutCenteredSet([{ id: 'a', label: 'A', ring: 10 }], { innerR: 30, ringGap: 22 }).placed,
+ ringRadii({ innerR: 30, ringGap: 22 }), {})
+const { maxAbsLat } = toFeatureCollection(feats, whiteboardProjection({ extent: EXTENT_C }))
+ok('centered: the drawing stays inside the Mercator clamp', maxAbsLat < 85, `max |lat| ${maxAbsLat.toFixed(2)}`)
+// THE ARM: the ceiling is extentFraction's, so a wide one must breach it
+const wide = whiteboardProjection({ extent: EXTENT_C, extentFraction: 0.95 })(0, EXTENT_C)[1]
+ok('the clamp CAN be breached by extentFraction alone (so the guard is not inert)',
+ Math.abs(wide) > 85, `|lat| ${Math.abs(wide).toFixed(2)} at fraction 0.95`)
+// and it does not depend on the extent
+const lats = [100, 228, 260, 1000].map((e) => Math.abs(whiteboardProjection({ extent: e })(0, e)[1]))
+ok('the ceiling is extentFraction\'s alone, not the extent\'s',
+ Math.max(...lats) - Math.min(...lats) < 1e-6, lats.map((l) => l.toFixed(3)).join(' '))
+
+// ── 3. a map-unit radius doubles per zoom ───────────────────────────────────
+const deg = dishRadiusDegrees(R, whiteboardProjection({ extent: SPREAD }))
+const expr = degreesToPixelRadius(deg)
+const [, , , z0, r0, z1, r1] = expr
+ok('dish: the radius is a zoom expression, not a constant', Array.isArray(expr) && expr[0] === 'interpolate')
+ok('dish: it uses exponential base 2, which is what doubling per zoom requires',
+ expr[1][0] === 'exponential' && expr[1][1] === 2)
+ok('dish: its stops scale by exactly 2^(z1-z0)',
+ Math.abs(r1 / r0 - Math.pow(2, z1 - z0)) / Math.pow(2, z1 - z0) < 1e-9,
+ `${r0.toFixed(2)}px at z${z0}`)
+
+if (fail.length) {
+ console.log(`\nGOSPEL-WHITEBOARD: FAIL — ${fail.length}: ${fail.join(', ')}`)
+ process.exit(1)
+}
+console.log('\nGOSPEL-WHITEBOARD: PASS — the gap, the clamp and the scaling all hold, and each arm can fail')
From e3cdfe61d9dc4ebb267d0fed46337a93257f6ada Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 18:10:38 +0000
Subject: [PATCH 08/69] Point the set charts at the promoted modules, and clear
a committed conflict
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The layout modules moved to library when they were promoted, and three of my
files still named the old paths: the two new whiteboard components, which were
written before the move landed, and the geometry check, which was written after
it and still copied the old import from its neighbours.
GiBoundedSet also carried an unresolved conflict — markers and all — committed by
a cherry-pick loop of mine that resolved generated output automatically and swept
the source conflict along with it. The file had both imports of the same symbol,
one of them pointing at a deleted path. It parsed, so the SFC check passed; only
the bundler would have caught it, and only at the point where it could not
resolve the file.
The lesson is about the loop, not the file: resolving one class of conflict
automatically is fine, but `git add -A` afterwards does not distinguish the
conflicts you meant to resolve from the ones you did not. A loop that stages
everything needs to refuse when a marker is still present.
---
internal/check-gospel-whiteboard.mjs | 4 +-
.../src/components/GiBoundedSet.vue | 6 +--
.../src/components/GiBoundedSetMap.vue | 2 +-
.../src/components/GiCenteredSetMap.vue | 2 +-
public/dx-maps/doxa-maps-spa/manifest.json | 4 +-
.../gospel-internet/board/board/index.html | 2 +-
.../dx-maps/gospel-internet/board/index.html | 2 +-
.../board/india-china/index.html | 2 +-
.../board/language-tree/index.html | 2 +-
.../board/platforms/index.html | 2 +-
.../board/project-700/index.html | 2 +-
.../gospel-internet/board/zero/index.html | 2 +-
.../gospel-internet/gospel-internet.js | 46 +++++++++----------
public/dx-maps/manifest.json | 4 +-
14 files changed, 39 insertions(+), 43 deletions(-)
diff --git a/internal/check-gospel-whiteboard.mjs b/internal/check-gospel-whiteboard.mjs
index 4659b4b5a..2b40d0b27 100644
--- a/internal/check-gospel-whiteboard.mjs
+++ b/internal/check-gospel-whiteboard.mjs
@@ -23,10 +23,10 @@
* extentFraction. A consumer cannot buy headroom against the clamp by choosing a bigger extent,
* and one assertion covers every chart.
*/
-import { layoutBoundedSet } from '../plugin-designs/gospel-internet/src/boundedSet.js'
+import { layoutBoundedSet } from '../library/components/charts/boundedSet.js'
import { degreesToPixelRadius, dishRadiusDegrees, toFeatureCollection, centeredSetFeatures }
from '../plugin-designs/gospel-internet/src/setChartFeatures.js'
-import { layoutCenteredSet, ringRadii } from '../plugin-designs/gospel-internet/src/centeredSet.js'
+import { layoutCenteredSet, ringRadii } from '../library/components/charts/centeredSet.js'
import { whiteboardProjection } from '../library/maps/whiteboard-projection.js'
const fail = []
diff --git a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
index 5f4777e1a..6a7951514 100644
--- a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
@@ -5,7 +5,7 @@
* The chart makes exactly one claim — which side of the line a group is on — so position
* carries no other meaning: no gradient, no ranking, no distance. The centered-set target
* is the chart that makes the opposite claim, and keeping them visually distinct is the
- * point of having both. Layout maths is in ../boundedSet.js, pure and testable without
+ * point of having both. Layout maths is in @map/components/charts/boundedSet.js, pure and testable without
* mounting this.
*
* SVG RATHER THAN THE MAP ENGINE, which is the opposite of GiTree's call and for a reason
@@ -28,12 +28,8 @@ import { formatCount } from '@map/utils/formatCount.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { loadNearRows, indexFor } from '../nearRows.js'
-<<<<<<< HEAD
import { layoutBoundedSet, boundedSetTally } from '@map/components/charts/boundedSet.js'
-=======
-import { layoutBoundedSet, boundedSetTally } from '../boundedSet.js'
import GiBoundedSetMap from './GiBoundedSetMap.vue'
->>>>>>> ea8dbe45 (Put the bounded set on the whiteboard too, with its boundary in map units)
const T = useGiT()
const board = useBoardState()
diff --git a/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue b/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue
index 7c0369a3c..0724c469c 100644
--- a/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue
+++ b/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue
@@ -25,7 +25,7 @@ import { addTieredPointLayers } from '@map/charts/whiteboard/tiered-points.js'
import { createBoardMap } from '../boardEngine.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
-import { layoutBoundedSet } from '../boundedSet.js'
+import { layoutBoundedSet } from '@map/components/charts/boundedSet.js'
import { boundedSetFeatures, toFeatureCollection, dishRadiusDegrees, degreesToPixelRadius, TIERS } from '../setChartFeatures.js'
const props = defineProps({ groups: { type: Array, default: () => [] } })
diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
index 8642734cc..8c5c0210a 100644
--- a/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
+++ b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
@@ -27,7 +27,7 @@ import { createBoardMap } from '../boardEngine.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { ringInfo, CENTRE_COLOR } from '../ringInfo.js'
-import { layoutCenteredSet, ringRadii, RING_MAX } from '../centeredSet.js'
+import { layoutCenteredSet, ringRadii, RING_MAX } from '@map/components/charts/centeredSet.js'
import { centeredSetFeatures, toFeatureCollection, TIERS } from '../setChartFeatures.js'
const props = defineProps({
diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json
index a25fb9f3e..fa5f74826 100644
--- a/public/dx-maps/doxa-maps-spa/manifest.json
+++ b/public/dx-maps/doxa-maps-spa/manifest.json
@@ -615,7 +615,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
- "integrity": "sha384-vaQ5uTfFZa29SikosXm3SCtKbf2UvuoEswinq/qnmAeHDXjaiIPvqJGDZvqENhjf",
+ "integrity": "sha384-Ok30q6/8Z0WNu0q0qgxYln/censxh2mhSUbEvBKLDm4pvEWeCIPlJs8LyWSRZ6iD",
"elements": [
"gospel-internet-board"
],
@@ -635,7 +635,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "",
- "script": "\n\n\n\n"
+ "script": "\n\n\n\n"
},
"instances": [
{
diff --git a/public/dx-maps/gospel-internet/board/board/index.html b/public/dx-maps/gospel-internet/board/board/index.html
index a352a3c0a..3847c9b38 100644
--- a/public/dx-maps/gospel-internet/board/board/index.html
+++ b/public/dx-maps/gospel-internet/board/board/index.html
@@ -36,7 +36,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
\n\n"
+ "script": "\n\n\n\n"
},
"instances": [
{
From c45194fd0f3b85be4de1cd64fafe20f430291e4f Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 18:13:20 +0000
Subject: [PATCH 09/69] Say the table is loading, rather than drawing a
confident zero
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The neighbour table is 2.6 MB. Until it arrives the joined set is legitimately
empty, so both panels drew a chart of nothing and captioned it with a count of
zero — indistinguishable from a real answer of none, which is the same defect as
a join failure reported as a fact about a people.
Found by sweeping my own plugins for the Driver's bug classes rather than by
anyone reporting it, which is the point of the sweep: the newest code was the
code with the defect, because it had been read least.
The loading line is announced politely for a screen reader, since the change it
describes is a region of the page becoming real rather than an alert.
---
.../src/components/GiBoundedSet.vue | 9 ++++-
.../src/components/GiCenteredSet.vue | 9 ++++-
public/dx-maps/doxa-maps-spa/manifest.json | 4 +-
.../gospel-internet/board/board/index.html | 2 +-
.../dx-maps/gospel-internet/board/index.html | 2 +-
.../board/india-china/index.html | 2 +-
.../board/language-tree/index.html | 2 +-
.../board/platforms/index.html | 2 +-
.../board/project-700/index.html | 2 +-
.../gospel-internet/board/zero/index.html | 2 +-
.../gospel-internet/gospel-internet.js | 38 +++++++++----------
public/dx-maps/manifest.json | 4 +-
12 files changed, 46 insertions(+), 32 deletions(-)
diff --git a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
index 6a7951514..fd7164abe 100644
--- a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
@@ -91,7 +91,14 @@ useShadowStyles(
{{ T('gospelInternet.bounded.failed', 'The reached/unreached table could not be loaded, so this chart is not drawn.') }}
{{ failed }}
-
+
+
+ {{ T('gospelInternet.loadingTable', 'Loading the table…') }}
+
+
+
+ {{ T('gospelInternet.loadingTable', 'Loading the table…') }}
+
+
\n\n\n\n"
+ "script": "\n\n\n\n"
},
"instances": [
{
diff --git a/public/dx-maps/gospel-internet/board/board/index.html b/public/dx-maps/gospel-internet/board/board/index.html
index 3847c9b38..be0e45e64 100644
--- a/public/dx-maps/gospel-internet/board/board/index.html
+++ b/public/dx-maps/gospel-internet/board/board/index.html
@@ -36,7 +36,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
\n\n"
+ "script": "\n\n\n\n"
},
"instances": [
{
From 699662708141050c10648b9a3d950a08f7712047 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 18:13:20 +0000
Subject: [PATCH 10/69] One key for the set charts' loading line, in all eleven
locales
Authored here and landed by studio, per the library boundary. The English is
byte-identical to the call-site fallback; each script takes its own ellipsis and
spacing convention rather than the English one.
---
library/i18n/locales/ar/gospelInternet.json | 1 +
library/i18n/locales/de/gospelInternet.json | 1 +
library/i18n/locales/en/gospelInternet.json | 1 +
library/i18n/locales/es/gospelInternet.json | 1 +
library/i18n/locales/fr/gospelInternet.json | 1 +
library/i18n/locales/hi/gospelInternet.json | 1 +
library/i18n/locales/it/gospelInternet.json | 1 +
library/i18n/locales/pt/gospelInternet.json | 1 +
library/i18n/locales/ro/gospelInternet.json | 1 +
library/i18n/locales/ru/gospelInternet.json | 1 +
library/i18n/locales/zh/gospelInternet.json | 1 +
11 files changed, 11 insertions(+)
diff --git a/library/i18n/locales/ar/gospelInternet.json b/library/i18n/locales/ar/gospelInternet.json
index 383b51aaa..407960baa 100644
--- a/library/i18n/locales/ar/gospelInternet.json
+++ b/library/i18n/locales/ar/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "قائمة المجموعات العرقية التي اختارتها هذه الصفحة",
"loadError": "لم يتم تحميل بيانات اللوحة.",
+ "loadingTable": "جارٍ تحميل الجدول…",
"map": {
"aria": "خريطة المجموعات العرقية، مظلّلة بحسب نسبة استخدام الإنترنت في كل بلد"
},
diff --git a/library/i18n/locales/de/gospelInternet.json b/library/i18n/locales/de/gospelInternet.json
index 8e7a5d4e6..f31796482 100644
--- a/library/i18n/locales/de/gospelInternet.json
+++ b/library/i18n/locales/de/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Volksgruppenliste, von dieser Seite gewählt",
"loadError": "Die Daten der Übersicht wurden nicht geladen.",
+ "loadingTable": "Die Tabelle wird geladen …",
"map": {
"aria": "Karte der Volksgruppen, schattiert danach, wie viele Menschen im Land das Internet nutzen"
},
diff --git a/library/i18n/locales/en/gospelInternet.json b/library/i18n/locales/en/gospelInternet.json
index 6c84695eb..51499e7a0 100644
--- a/library/i18n/locales/en/gospelInternet.json
+++ b/library/i18n/locales/en/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "People-group list, chosen by this page",
"loadError": "The board's data did not load.",
+ "loadingTable": "Loading the table…",
"map": {
"aria": "Map of people groups, shaded by how much of each country uses the internet"
},
diff --git a/library/i18n/locales/es/gospelInternet.json b/library/i18n/locales/es/gospelInternet.json
index 7f2f621e4..2d9edc5b1 100644
--- a/library/i18n/locales/es/gospelInternet.json
+++ b/library/i18n/locales/es/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Lista de grupos étnicos elegida por esta página",
"loadError": "No se cargaron los datos del tablero.",
+ "loadingTable": "Cargando la tabla…",
"map": {
"aria": "Mapa de grupos étnicos, sombreado según cuánto usa internet cada país"
},
diff --git a/library/i18n/locales/fr/gospelInternet.json b/library/i18n/locales/fr/gospelInternet.json
index 7a2a1e5de..28bb49f42 100644
--- a/library/i18n/locales/fr/gospelInternet.json
+++ b/library/i18n/locales/fr/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Liste de groupes ethniques choisie par cette page",
"loadError": "Les données du tableau de bord n’ont pas été chargées.",
+ "loadingTable": "Chargement de la table…",
"map": {
"aria": "Carte des groupes ethniques, ombrée selon la part de chaque pays qui utilise internet"
},
diff --git a/library/i18n/locales/hi/gospelInternet.json b/library/i18n/locales/hi/gospelInternet.json
index fcea19303..c5c2b4958 100644
--- a/library/i18n/locales/hi/gospelInternet.json
+++ b/library/i18n/locales/hi/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "इस पेज द्वारा चुनी गई जन-समूह सूची",
"loadError": "बोर्ड का डेटा लोड नहीं हुआ।",
+ "loadingTable": "तालिका लोड हो रही है…",
"map": {
"aria": "जन-समूहों का मानचित्र, हर देश में इंटरनेट उपयोग के अनुसार रंगा हुआ"
},
diff --git a/library/i18n/locales/it/gospelInternet.json b/library/i18n/locales/it/gospelInternet.json
index d1b03116b..0da518418 100644
--- a/library/i18n/locales/it/gospelInternet.json
+++ b/library/i18n/locales/it/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Elenco di gruppi etnici scelto da questa pagina",
"loadError": "I dati del pannello non sono stati caricati.",
+ "loadingTable": "Caricamento della tabella…",
"map": {
"aria": "Mappa dei gruppi etnici, ombreggiata secondo quanto ogni paese usa internet"
},
diff --git a/library/i18n/locales/pt/gospelInternet.json b/library/i18n/locales/pt/gospelInternet.json
index 2638a0946..ddf38a658 100644
--- a/library/i18n/locales/pt/gospelInternet.json
+++ b/library/i18n/locales/pt/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Lista de povos escolhida por esta página",
"loadError": "Os dados do painel não carregaram.",
+ "loadingTable": "A carregar a tabela…",
"map": {
"aria": "Mapa dos povos, sombreado pelo quanto cada país usa a internet"
},
diff --git a/library/i18n/locales/ro/gospelInternet.json b/library/i18n/locales/ro/gospelInternet.json
index 60cf3bf6a..331fe220b 100644
--- a/library/i18n/locales/ro/gospelInternet.json
+++ b/library/i18n/locales/ro/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Listă de grupuri etnice aleasă de această pagină",
"loadError": "Datele panoului nu s-au încărcat.",
+ "loadingTable": "Se încarcă tabelul…",
"map": {
"aria": "Harta grupurilor etnice, colorată după cât folosește internetul fiecare țară"
},
diff --git a/library/i18n/locales/ru/gospelInternet.json b/library/i18n/locales/ru/gospelInternet.json
index b2905dd62..278ccc27f 100644
--- a/library/i18n/locales/ru/gospelInternet.json
+++ b/library/i18n/locales/ru/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "Список народов, выбранный этой страницей",
"loadError": "Данные панели не загрузились.",
+ "loadingTable": "Загрузка таблицы…",
"map": {
"aria": "Карта народов, закрашенная по доле пользователей интернета в каждой стране"
},
diff --git a/library/i18n/locales/zh/gospelInternet.json b/library/i18n/locales/zh/gospelInternet.json
index 898ad882e..cfd800670 100644
--- a/library/i18n/locales/zh/gospelInternet.json
+++ b/library/i18n/locales/zh/gospelInternet.json
@@ -192,6 +192,7 @@
},
"listChip": "本页面选定的族群名单",
"loadError": "看板的数据未能加载。",
+ "loadingTable": "正在载入数据表…",
"map": {
"aria": "族群地图,按各国互联网使用程度着色"
},
From 6cab26c79731078cf13855be6d95b619e3ff6369 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 18:32:31 +0000
Subject: [PATCH 11/69] Make the legend's full-screen button do something
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The ring legend declared a fullscreen emit and rendered a labelled, focusable
button for it. The parent bound select and not fullscreen, so the button looked
right, tabbed right, announced itself right, and did nothing. It is a Driver ask,
and it is the half-wired shape exactly: the visible half worked, so nothing
appeared broken.
The fix is not to bind the emit. The button is now the shared control, so there
is one implementation instead of two — and it brings the part a second one would
have missed. iOS Safari does not implement requestFullscreen on arbitrary
elements and fails silently, so the shared control carries a pseudo-fullscreen
fallback. A hand-rolled handler would have worked everywhere I could test it and
nowhere I could not.
A first attempt kept the hand-rolled button and called the shared component's
toggle through a template ref. That component has no defineExpose, so under
script setup the method is private and the call would have resolved to undefined
— silently reintroducing the same defect while appearing to fix it. Checked
before committing rather than after.
The panel element is passed in, because full screen should show the chart and the
legend together, and the legend does not own the panel it sits in.
Found by sweeping for the half-wired-config class rather than by anyone reporting
it, which is what that class is for: nothing about it looks wrong from outside.
---
.../src/components/GiCenteredSet.vue | 18 +++++++-
.../src/components/GiRingLegend.vue | 16 +++++--
public/dx-maps/doxa-maps-spa/manifest.json | 4 +-
.../gospel-internet/board/board/index.html | 2 +-
.../dx-maps/gospel-internet/board/index.html | 2 +-
.../board/india-china/index.html | 2 +-
.../board/language-tree/index.html | 2 +-
.../board/platforms/index.html | 2 +-
.../board/project-700/index.html | 2 +-
.../gospel-internet/board/zero/index.html | 2 +-
.../gospel-internet/gospel-internet.js | 44 +++++++++----------
public/dx-maps/manifest.json | 4 +-
12 files changed, 62 insertions(+), 38 deletions(-)
diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
index d0193ed70..aef6f5d4a 100644
--- a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
@@ -27,6 +27,7 @@ import { loadNearRows, indexFor } from '../nearRows.js'
import { layoutCenteredSet, ringTally, inwardArrow, ringRadii } from '@map/components/charts/centeredSet.js'
import GiRingLegend from './GiRingLegend.vue'
import GiCenteredSetMap from './GiCenteredSetMap.vue'
+import FullscreenButton from '@map/components/map-controls/FullscreenButton.vue'
const T = useGiT()
const board = useBoardState()
@@ -34,6 +35,9 @@ const near = ref(null)
// Which ring the reader has asked about. null = none; the chart dims the others rather than
// hiding them, because a ring with no dots left would read as a ring with no groups.
const activeRing = ref(null)
+// The panel element, so full screen shows the chart AND its legend rather than one of them.
+const panelEl = ref(null)
+
const failed = ref('')
onMounted(async () => {
@@ -106,7 +110,7 @@ useShadowStyles(
-
+
{{ T('gospelInternet.centered.failed', 'The neighbour table could not be loaded, so this chart is not drawn.') }}
{{ failed }}
@@ -128,8 +132,18 @@ useShadowStyles(
@select="activeRing = $event" />
{{ T('gospelInternet.centered.reached', 'Already reached, so at the centre:') }} {{ fmt(tally.centre) }}.
diff --git a/plugin-designs/gospel-internet/src/components/GiRingLegend.vue b/plugin-designs/gospel-internet/src/components/GiRingLegend.vue
index af6f646b6..50af2a835 100644
--- a/plugin-designs/gospel-internet/src/components/GiRingLegend.vue
+++ b/plugin-designs/gospel-internet/src/components/GiRingLegend.vue
@@ -19,17 +19,21 @@
* Emits: select(ring | null), fullscreen
*/
import { ref, computed } from 'vue'
+import FullscreenButton from '@map/components/map-controls/FullscreenButton.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { formatCount } from '@map/utils/formatCount.js'
import { useGiT } from '../useGiT.js'
import { BANDS, CULTURES, CENTRE_COLOR, ringInfo, ringFor, ringSummary } from '../ringInfo.js'
const props = defineProps({
+ // The element full screen should fill — the whole panel, so the chart and this legend go
+ // together. Passed in because the legend does not own the panel it sits in.
+ panel: { type: Object, default: null },
groups: { type: Array, default: () => [] },
unmatched: { type: Number, default: 0 },
activeRing: { type: Number, default: null },
})
-const emit = defineEmits(['select', 'fullscreen'])
+const emit = defineEmits(['select'])
const T = useGiT()
// formatCount, not toLocaleString(): a bare toLocaleString follows the READER'S BROWSER,
// so one page could print 2,074 here and 2.074 in the panel beside it. formatCount holds
@@ -115,8 +119,14 @@ useShadowStyles(
{{ T('gospelInternet.rings.title', 'What the rings mean') }}
-
+
+
{{ T('gospelInternet.rings.intro', 'Each unreached group sits on a ring by how close it is to its nearest reached group. The closer the ring, the more naturally the gospel can travel.') }}
diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json
index 4247415a6..cb8e21ed7 100644
--- a/public/dx-maps/doxa-maps-spa/manifest.json
+++ b/public/dx-maps/doxa-maps-spa/manifest.json
@@ -615,7 +615,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
- "integrity": "sha384-4Bp7rtVKWkP6qtL3MU3dMZB0xo4Ljekc3a+Ujfr0Ck6ypeSqOLOcq+n8Zt1kAI7v",
+ "integrity": "sha384-nDV/E/glwOpIW6Y+Rc8wn43M6J7gIKY00mirO8gEqaHWbaNNPHQp3tnkog+uuaNw",
"elements": [
"gospel-internet-board"
],
@@ -635,7 +635,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "",
- "script": "\n\n\n\n"
+ "script": "\n\n\n\n"
},
"instances": [
{
diff --git a/public/dx-maps/gospel-internet/board/board/index.html b/public/dx-maps/gospel-internet/board/board/index.html
index be0e45e64..a7ca029ba 100644
--- a/public/dx-maps/gospel-internet/board/board/index.html
+++ b/public/dx-maps/gospel-internet/board/board/index.html
@@ -36,7 +36,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
\n\n"
+ "script": "\n\n\n\n"
},
"instances": [
{
From 2583d39d2d6f122b8bc4ae83ab12a419828eb0bd Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 19:37:25 +0000
Subject: [PATCH 12/69] Keep the reader's legend choice across a resize
Every width change reset the legend from the breakpoint, so opening the decoder
on a phone lost it on rotation, and closing it on a wide window reopened it by
dragging the edge. The resize was undoing the reader's decision rather than
responding to it.
The width now decides only until the reader does. After that their choice holds
at both widths.
This is the same defect fixed in the other plugin's legend earlier today, written
again here a few hours later, because the two legends are separate
implementations of one control and only one of them had learned. That is the
argument for promoting the control, not for being more careful next time.
---
.../src/components/GiCenteredSet.vue | 16 ++++++++++--
public/dx-maps/doxa-maps-spa/manifest.json | 4 +--
.../gospel-internet/board/board/index.html | 2 +-
.../dx-maps/gospel-internet/board/index.html | 2 +-
.../board/india-china/index.html | 2 +-
.../board/language-tree/index.html | 2 +-
.../board/platforms/index.html | 2 +-
.../board/project-700/index.html | 2 +-
.../gospel-internet/board/zero/index.html | 2 +-
.../gospel-internet/gospel-internet.js | 26 +++++++++----------
public/dx-maps/manifest.json | 4 +--
11 files changed, 38 insertions(+), 26 deletions(-)
diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
index a75f02dd3..d6f7540e2 100644
--- a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue
@@ -42,7 +42,19 @@ const activeRing = ref(null)
const narrow = ref(false)
const legendOpen = ref(true)
let mq = null
-function applyWidth(m) { narrow.value = m.matches; legendOpen.value = !m.matches }
+// ★ A DELIBERATE CHOICE SURVIVES A RESIZE. The first version of this set legendOpen from the
+// breakpoint on EVERY width change, so a reader who opened the decoder on a phone lost it on
+// rotation, and one who closed it on desktop had it reopened by dragging the window. The resize
+// would be undoing their decision rather than responding to it.
+// This is the same defect I had already fixed in the other plugin's legend today and then wrote
+// again here, because the two legends are separate implementations of one control. That is the
+// case for promoting the control rather than a reason to be more careful.
+let chosen = null // null = the reader has not decided yet
+function applyWidth(m) {
+ narrow.value = m.matches
+ legendOpen.value = chosen == null ? !m.matches : chosen
+}
+function setLegendOpen(v) { chosen = v; legendOpen.value = v }
onMounted(() => {
try {
mq = window.matchMedia('(max-width: 900px)')
@@ -151,7 +163,7 @@ useShadowStyles(
- What “no text” means. It is the TEXT Scripture gap, and nothing wider: a row
+ {{ tr('gospel.list.noTextMeans') }} It is the TEXT Scripture gap, and nothing wider: a row
pilled “no text” may still show audio Scripture, a film or a Bible video beside it, and
several do. Not found in the resource pools checked so far —
currently eBible.org ({{ fmt(stats.ebible_translations) }} translations across
@@ -416,7 +416,7 @@ useShadowStyles(
this build does not have. A pool nobody has queried shows as unknown, never as “no”.
Languages are joined to people groups by exact ISO 639-3 code.
- This page lists published resource availability only. It carries no information
+ {{ tr('gospel.list.pageLists') }} published resource availability only. It carries no information
about churches, believers or gatherings, by design.
Showing 12 of {{ sameLanguage.length }} groups sharing this language.
From cd9a54e1d969ff98ea0dd38af4a17912435bed23 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Tue, 29 Sep 2026 07:30:06 +0000
Subject: [PATCH 45/69] The legend toggle, the chart titles and "no code" were
never translated
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Found while classifying the remainder rather than by the gate pointing at them:
the search box's aria-label was hardcoded English — screen-reader text, the same
class as the resize-handle bug that check:i18n-keys-exist documents — along with
the legend's open/close label, the three gap-chart titles, "unnamed language",
the unchecked-link notice, and "no code", which appears at six call sites and is
now one key.
Nine keys, sixteen call sites. 102 -> 88.
These were hiding inside the 102 I had described as mostly-deliberate. The
classification was worth doing for its own sake: four of the five classes really
are deliberate, but the fifth had real defects in it, and I would not have found
them by reading the count.
---
library/i18n/locales/ar/gospel.json | 13 +++++++++-
library/i18n/locales/de/gospel.json | 13 +++++++++-
library/i18n/locales/en/gospel.json | 13 +++++++++-
library/i18n/locales/es/gospel.json | 13 +++++++++-
library/i18n/locales/fr/gospel.json | 13 +++++++++-
library/i18n/locales/hi/gospel.json | 13 +++++++++-
library/i18n/locales/it/gospel.json | 13 +++++++++-
library/i18n/locales/pt/gospel.json | 13 +++++++++-
library/i18n/locales/ro/gospel.json | 13 +++++++++-
library/i18n/locales/ru/gospel.json | 13 +++++++++-
library/i18n/locales/zh/gospel.json | 13 +++++++++-
.../gospel-resources/resources-map.vue | 26 +++++++++----------
12 files changed, 145 insertions(+), 24 deletions(-)
diff --git a/library/i18n/locales/ar/gospel.json b/library/i18n/locales/ar/gospel.json
index 32ca90f0d..c57fcebce 100644
--- a/library/i18n/locales/ar/gospel.json
+++ b/library/i18n/locales/ar/gospel.json
@@ -11,7 +11,8 @@
"clear": "مسح البحث",
"none": "لا شيء يطابق هذا الاسم أو اللغة أو الرمز.",
"placeholderShort": "شعب أو لغة أو رمز ISO",
- "showingOf": "يُعرض {shown} من أصل {total} نتيجة — واصل الكتابة لتضييق البحث."
+ "showingOf": "يُعرض {shown} من أصل {total} نتيجة — واصل الكتابة لتضييق البحث.",
+ "aria": "ابحث عن مجموعة بشرية أو لغة أو رمز ISO"
},
"legend": {
"title": "شعوب غير مبلَّغة ولا يوجد لها أي مورد إنجيلي",
@@ -409,5 +410,15 @@
"hero": "{groups} مجموعة بشرية {growing} {noFound} {pop} نسمة، بتقدير {deaths} وفاة يوميًا.",
"growing": "في تزايد",
"found": "دون كتاب مقدس ودون فيلم في المجموعات التي جرى فحصها —"
+ },
+ "ui": {
+ "noCode": "بلا رمز",
+ "unnamed": "لغة بلا اسم",
+ "hideKey": "إخفاء المفتاح والمرشِّحات",
+ "showKey": "المفتاح والمرشِّحات",
+ "largest": "أكبر المجموعات التي لم يُعثر لها على شيء",
+ "nobodyPraying": "لا شيء موجود، ولا أحد يصلّي",
+ "concentrates": "أين تتركّز الفجوة",
+ "linkUnchecked": "لم يتم فحص هذا الرابط."
}
}
diff --git a/library/i18n/locales/de/gospel.json b/library/i18n/locales/de/gospel.json
index 28497f902..4d23116f4 100644
--- a/library/i18n/locales/de/gospel.json
+++ b/library/i18n/locales/de/gospel.json
@@ -11,7 +11,8 @@
"clear": "Suche löschen",
"none": "Nichts passt zu diesem Namen, dieser Sprache oder diesem Code.",
"placeholderShort": "Gruppe, Sprache oder ISO-Code",
- "showingOf": "{shown} von {total} Treffern angezeigt — weitertippen, um einzugrenzen."
+ "showingOf": "{shown} von {total} Treffern angezeigt — weitertippen, um einzugrenzen.",
+ "aria": "Volksgruppe, Sprache oder ISO-Code suchen"
},
"legend": {
"title": "Unerreichte Volksgruppen ohne gefundene Evangeliums-Ressource",
@@ -409,5 +410,15 @@
"hero": "{groups} Volksgruppen {growing} {noFound} {pop} Menschen, geschätzt {deaths} Todesfälle pro Tag.",
"growing": "wachsen",
"found": "ohne Bibel und ohne Film in den geprüften Beständen —"
+ },
+ "ui": {
+ "noCode": "kein Code",
+ "unnamed": "unbenannte Sprache",
+ "hideKey": "Legende & Filter ausblenden",
+ "showKey": "Legende & Filter",
+ "largest": "Größte Gruppen ohne jeden Fund",
+ "nobodyPraying": "Nichts gefunden, und niemand betet",
+ "concentrates": "Wo sich die Lücke ballt",
+ "linkUnchecked": "Dieser Link wurde nicht geprüft."
}
}
diff --git a/library/i18n/locales/en/gospel.json b/library/i18n/locales/en/gospel.json
index ef1e0eb31..cd392ab1e 100644
--- a/library/i18n/locales/en/gospel.json
+++ b/library/i18n/locales/en/gospel.json
@@ -11,7 +11,8 @@
"clear": "Clear search",
"none": "Nothing matches that name, language or code.",
"placeholderShort": "Group, language or ISO code",
- "showingOf": "Showing {shown} of {total} matches — keep typing to narrow."
+ "showingOf": "Showing {shown} of {total} matches — keep typing to narrow.",
+ "aria": "Find a people group, language or ISO code"
},
"legend": {
"title": "Unengaged groups with no gospel resource found",
@@ -409,5 +410,15 @@
"hero": "{groups} people groups are {growing} {noFound} {pop} people, an estimated {deaths} deaths per day.",
"growing": "growing",
"found": "with no scripture and no film found in the pools checked —"
+ },
+ "ui": {
+ "noCode": "no code",
+ "unnamed": "unnamed language",
+ "hideKey": "Hide key & filters",
+ "showKey": "Key & filters",
+ "largest": "Largest groups with nothing found",
+ "nobodyPraying": "Nothing found, and nobody praying",
+ "concentrates": "Where the gap concentrates",
+ "linkUnchecked": "This link has not been checked."
}
}
diff --git a/library/i18n/locales/es/gospel.json b/library/i18n/locales/es/gospel.json
index d802ac94d..87fe44e26 100644
--- a/library/i18n/locales/es/gospel.json
+++ b/library/i18n/locales/es/gospel.json
@@ -11,7 +11,8 @@
"clear": "Borrar búsqueda",
"none": "Nada coincide con ese nombre, idioma o código.",
"placeholderShort": "Pueblo, idioma o código ISO",
- "showingOf": "Se muestran {shown} de {total} coincidencias; siga escribiendo para acotar."
+ "showingOf": "Se muestran {shown} de {total} coincidencias; siga escribiendo para acotar.",
+ "aria": "Buscar un pueblo, lengua o código ISO"
},
"legend": {
"title": "Pueblos no alcanzados sin ningún recurso del evangelio",
@@ -409,5 +410,15 @@
"hero": "{groups} pueblos están {growing} {noFound} {pop} personas, unas {deaths} muertes por día.",
"growing": "creciendo",
"found": "sin Escritura ni película en los repositorios consultados:"
+ },
+ "ui": {
+ "noCode": "sin código",
+ "unnamed": "lengua sin nombre",
+ "hideKey": "Ocultar clave y filtros",
+ "showKey": "Clave y filtros",
+ "largest": "Pueblos mayores sin nada encontrado",
+ "nobodyPraying": "Nada encontrado, y nadie orando",
+ "concentrates": "Dónde se concentra la brecha",
+ "linkUnchecked": "Este enlace no ha sido verificado."
}
}
diff --git a/library/i18n/locales/fr/gospel.json b/library/i18n/locales/fr/gospel.json
index ffcc5b959..bda7a21c9 100644
--- a/library/i18n/locales/fr/gospel.json
+++ b/library/i18n/locales/fr/gospel.json
@@ -11,7 +11,8 @@
"clear": "Effacer la recherche",
"none": "Rien ne correspond à ce nom, cette langue ou ce code.",
"placeholderShort": "Peuple, langue ou code ISO",
- "showingOf": "{shown} résultats affichés sur {total} — continuez à taper pour affiner."
+ "showingOf": "{shown} résultats affichés sur {total} — continuez à taper pour affiner.",
+ "aria": "Rechercher un peuple, une langue ou un code ISO"
},
"legend": {
"title": "Peuples non atteints sans aucune ressource de l'Évangile",
@@ -409,5 +410,15 @@
"hero": "{groups} peuples sont en {growing} {noFound} {pop} personnes, environ {deaths} décès par jour.",
"growing": "croissance",
"found": "sans Écriture ni film dans les fonds consultés —"
+ },
+ "ui": {
+ "noCode": "sans code",
+ "unnamed": "langue sans nom",
+ "hideKey": "Masquer la légende et les filtres",
+ "showKey": "Légende et filtres",
+ "largest": "Plus grands peuples sans aucune ressource",
+ "nobodyPraying": "Rien trouvé, et personne ne prie",
+ "concentrates": "Où l’écart se concentre",
+ "linkUnchecked": "Ce lien n’a pas été vérifié."
}
}
diff --git a/library/i18n/locales/hi/gospel.json b/library/i18n/locales/hi/gospel.json
index 71f1623ba..3fe3b907d 100644
--- a/library/i18n/locales/hi/gospel.json
+++ b/library/i18n/locales/hi/gospel.json
@@ -11,7 +11,8 @@
"clear": "खोज साफ़ करें",
"none": "इस नाम, भाषा या कोड से कुछ भी मेल नहीं खाता।",
"placeholderShort": "जनसमूह, भाषा या ISO कोड",
- "showingOf": "{total} मिलानों में से {shown} दिखाए जा रहे हैं — सीमित करने के लिए टाइप करते रहें।"
+ "showingOf": "{total} मिलानों में से {shown} दिखाए जा रहे हैं — सीमित करने के लिए टाइप करते रहें।",
+ "aria": "जनसमूह, भाषा या ISO कोड खोजें"
},
"legend": {
"title": "ऐसे जनसमूह जिन तक सुसमाचार नहीं पहुँचा और कोई संसाधन नहीं मिला",
@@ -409,5 +410,15 @@
"hero": "{groups} जनसमूह {growing} {noFound} {pop} लोग, अनुमानित {deaths} मृत्यु प्रतिदिन।",
"growing": "बढ़ रहे हैं",
"found": "जाँचे गए स्रोतों में न पवित्रशास्त्र, न फ़िल्म —"
+ },
+ "ui": {
+ "noCode": "कोई कोड नहीं",
+ "unnamed": "अनाम भाषा",
+ "hideKey": "कुंजी और फ़िल्टर छिपाएँ",
+ "showKey": "कुंजी और फ़िल्टर",
+ "largest": "सबसे बड़े समूह जिनके लिए कुछ नहीं मिला",
+ "nobodyPraying": "कुछ नहीं मिला, और कोई प्रार्थना नहीं कर रहा",
+ "concentrates": "अंतर कहाँ केंद्रित है",
+ "linkUnchecked": "यह लिंक जाँचा नहीं गया है।"
}
}
diff --git a/library/i18n/locales/it/gospel.json b/library/i18n/locales/it/gospel.json
index e52132969..0373b725a 100644
--- a/library/i18n/locales/it/gospel.json
+++ b/library/i18n/locales/it/gospel.json
@@ -11,7 +11,8 @@
"clear": "Cancella ricerca",
"none": "Nessuna corrispondenza per quel nome, lingua o codice.",
"placeholderShort": "Popolo, lingua o codice ISO",
- "showingOf": "Mostrati {shown} di {total} risultati: continua a digitare per restringere."
+ "showingOf": "Mostrati {shown} di {total} risultati: continua a digitare per restringere.",
+ "aria": "Cerca un popolo, una lingua o un codice ISO"
},
"legend": {
"title": "Popoli non raggiunti senza alcuna risorsa del vangelo",
@@ -409,5 +410,15 @@
"hero": "{groups} popoli sono in {growing} {noFound} {pop} persone, circa {deaths} morti al giorno.",
"growing": "crescita",
"found": "senza Scrittura né film nei repertori consultati —"
+ },
+ "ui": {
+ "noCode": "senza codice",
+ "unnamed": "lingua senza nome",
+ "hideKey": "Nascondi legenda e filtri",
+ "showKey": "Legenda e filtri",
+ "largest": "Popoli più grandi senza nulla di trovato",
+ "nobodyPraying": "Nulla trovato, e nessuno in preghiera",
+ "concentrates": "Dove si concentra il divario",
+ "linkUnchecked": "Questo collegamento non è stato verificato."
}
}
diff --git a/library/i18n/locales/pt/gospel.json b/library/i18n/locales/pt/gospel.json
index d6c480a44..2f79a6d81 100644
--- a/library/i18n/locales/pt/gospel.json
+++ b/library/i18n/locales/pt/gospel.json
@@ -11,7 +11,8 @@
"clear": "Limpar busca",
"none": "Nada corresponde a esse nome, idioma ou código.",
"placeholderShort": "Povo, idioma ou código ISO",
- "showingOf": "Mostrando {shown} de {total} resultados — continue digitando para refinar."
+ "showingOf": "Mostrando {shown} de {total} resultados — continue digitando para refinar.",
+ "aria": "Procurar um povo, língua ou código ISO"
},
"legend": {
"title": "Povos não alcançados sem nenhum recurso do evangelho",
@@ -409,5 +410,15 @@
"hero": "{groups} povos estão {growing} {noFound} {pop} pessoas, cerca de {deaths} mortes por dia.",
"growing": "crescendo",
"found": "sem Escritura e sem filme nos acervos consultados —"
+ },
+ "ui": {
+ "noCode": "sem código",
+ "unnamed": "língua sem nome",
+ "hideKey": "Ocultar legenda e filtros",
+ "showKey": "Legenda e filtros",
+ "largest": "Maiores povos sem nada encontrado",
+ "nobodyPraying": "Nada encontrado, e ninguém orando",
+ "concentrates": "Onde a lacuna se concentra",
+ "linkUnchecked": "Este link não foi verificado."
}
}
diff --git a/library/i18n/locales/ro/gospel.json b/library/i18n/locales/ro/gospel.json
index 3b6f723ba..3541599c1 100644
--- a/library/i18n/locales/ro/gospel.json
+++ b/library/i18n/locales/ro/gospel.json
@@ -11,7 +11,8 @@
"clear": "Șterge căutarea",
"none": "Nimic nu se potrivește cu acel nume, limbă sau cod.",
"placeholderShort": "Popor, limbă sau cod ISO",
- "showingOf": "Se afișează {shown} din {total} rezultate — continuați să tastați pentru a restrânge."
+ "showingOf": "Se afișează {shown} din {total} rezultate — continuați să tastați pentru a restrânge.",
+ "aria": "Caută un grup etnic, o limbă sau un cod ISO"
},
"legend": {
"title": "Popoare neatinse, fără nicio resursă a Evangheliei",
@@ -409,5 +410,15 @@
"hero": "{groups} grupuri etnice sunt în {growing} {noFound} {pop} persoane, aproximativ {deaths} decese pe zi.",
"growing": "creștere",
"found": "fără Scriptură și fără film în colecțiile verificate —"
+ },
+ "ui": {
+ "noCode": "fără cod",
+ "unnamed": "limbă fără nume",
+ "hideKey": "Ascunde legenda și filtrele",
+ "showKey": "Legendă și filtre",
+ "largest": "Cele mai mari grupuri fără nimic găsit",
+ "nobodyPraying": "Nimic găsit și nimeni nu se roagă",
+ "concentrates": "Unde se concentrează golul",
+ "linkUnchecked": "Acest link nu a fost verificat."
}
}
diff --git a/library/i18n/locales/ru/gospel.json b/library/i18n/locales/ru/gospel.json
index a3ea3fc75..d0303423a 100644
--- a/library/i18n/locales/ru/gospel.json
+++ b/library/i18n/locales/ru/gospel.json
@@ -11,7 +11,8 @@
"clear": "Очистить поиск",
"none": "Ничего не найдено по этому названию, языку или коду.",
"placeholderShort": "Народ, язык или код ISO",
- "showingOf": "Показано {shown} из {total} совпадений — продолжайте вводить, чтобы сузить поиск."
+ "showingOf": "Показано {shown} из {total} совпадений — продолжайте вводить, чтобы сузить поиск.",
+ "aria": "Найти народ, язык или код ISO"
},
"legend": {
"title": "Недостигнутые народы, для которых не найдено ни одного ресурса",
@@ -409,5 +410,15 @@
"hero": "{groups} народов {growing} {noFound} {pop} человек, примерно {deaths} смертей в день.",
"growing": "растут",
"found": "без Писания и без фильма в проверенных источниках —"
+ },
+ "ui": {
+ "noCode": "нет кода",
+ "unnamed": "язык без названия",
+ "hideKey": "Скрыть легенду и фильтры",
+ "showKey": "Легенда и фильтры",
+ "largest": "Крупнейшие народы, для которых ничего не найдено",
+ "nobodyPraying": "Ничего не найдено, и никто не молится",
+ "concentrates": "Где сосредоточен разрыв",
+ "linkUnchecked": "Эта ссылка не проверялась."
}
}
diff --git a/library/i18n/locales/zh/gospel.json b/library/i18n/locales/zh/gospel.json
index fb39d272f..4112a416b 100644
--- a/library/i18n/locales/zh/gospel.json
+++ b/library/i18n/locales/zh/gospel.json
@@ -11,7 +11,8 @@
"clear": "清除搜索",
"none": "没有符合该名称、语言或代码的结果。",
"placeholderShort": "族群、语言或 ISO 代码",
- "showingOf": "显示 {total} 个匹配中的 {shown} 个 — 继续输入以缩小范围。"
+ "showingOf": "显示 {total} 个匹配中的 {shown} 个 — 继续输入以缩小范围。",
+ "aria": "查找族群、语言或 ISO 代码"
},
"legend": {
"title": "尚未接触且未找到任何福音资源的族群",
@@ -409,5 +410,15 @@
"hero": "{groups} 个族群正在{growing}{noFound}{pop} 人,估计每天 {deaths} 人离世。",
"growing": "增长",
"found": "在已查询的资源库中既无圣经也无影片——"
+ },
+ "ui": {
+ "noCode": "无代码",
+ "unnamed": "未命名语言",
+ "hideKey": "隐藏图例和筛选",
+ "showKey": "图例和筛选",
+ "largest": "未找到任何资源的最大族群",
+ "nobodyPraying": "一无所获,也无人祷告",
+ "concentrates": "差距集中在哪里",
+ "linkUnchecked": "此链接尚未核查。"
}
}
diff --git a/plugin-designs/gospel-resources/resources-map.vue b/plugin-designs/gospel-resources/resources-map.vue
index abd4f0cc8..87a470c13 100644
--- a/plugin-designs/gospel-resources/resources-map.vue
+++ b/plugin-designs/gospel-resources/resources-map.vue
@@ -1215,7 +1215,7 @@ const VSTATE = {
}
// A URL with no verification entry is UNCHECKED — never quietly shown as working.
const vchip = (url) => VSTATE[verifyOf(url)?.state || 'unknown'] || VSTATE.unknown
-const vtitle = (url) => verifyOf(url)?.detail || 'This link has not been checked.'
+const vtitle = (url) => verifyOf(url)?.detail || t('gospel.ui.linkUnchecked')
// A big field (India carries 856 groups) would otherwise render hundreds of buttons
// into a phone sheet on the first tap. Capped, with the real total always stated and
@@ -2244,7 +2244,7 @@ useShadowStyles(
accepts; the aria-label stays long, because a screen reader has no width. -->
+ :aria-label="t('gospel.search.aria')">
×
Showing 8 of {{ fmt(matches.length) }} matches — keep typing to narrow.
@@ -2270,7 +2270,7 @@ useShadowStyles(
so on a phone the whole gap workflow sat behind a label that sounded like
documentation. Reaching the ranked gap list means opening this first, and a
reader with no reason to open "notes" never finds it. -->
- {{ legendOpen ? 'Hide key & filters' : 'Key & filters' }}
+ {{ legendOpen ? t('gospel.ui.hideKey') : t('gospel.ui.showKey') }}
- {{ gapBy === 'groups' ? 'Largest groups with nothing found'
- : gapBy === 'noprayer' ? 'Nothing found, and nobody praying'
+ {{ gapBy === 'groups' ? t('gospel.ui.largest')
+ : gapBy === 'noprayer' ? t('gospel.ui.nobodyPraying')
: gapBy === 'hnr' ? 'Can hear the gospel, no text to read'
- : 'Where the gap concentrates' }}
+ : t('gospel.ui.concentrates') }}{{ t('gospel.sheet.close') }}
{{ t('connectivity.list.sectionMeta', { countries: num(sec.items.length), groups: num(sec.groups) }) }}
· {{ t('connectivity.list.unmeasured', { n: num(sec.unknown) }) }}
From 4dd11718ff1273d717f3e40303274d8d15875d37 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Tue, 29 Sep 2026 11:56:47 +0000
Subject: [PATCH 56/69] Rudder's corner panel kept its notices above the draft,
and they never yielded
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two defects, one cause: on a short panel the engine-offline notice took the room
the draft needed.
FIRST, A PHONE RULE THAT NEVER RAN. bac2431d0 (19:59) put notices below the
draft "so the draft stays in view". 4e94e9216 (21:58) added the corner widget
with `.compact .notice { order: 0 }`. A media query adds NO specificity, so
`.compact .notice` (0,2,0) beat `@media { .notice }` (0,1,0) at every width and
the phone rule has been dead since the day it landed. Ordering the compact rule
to 1 fixes the widget at every width, not only under 480px, because the corner
panel is short regardless of the viewport.
SECOND, ORDER ALONE WAS NOT ENOUGH, which I only found by doing the arithmetic
rather than trusting the first fix. .notice is flex:0 1 auto and .compact
.compose is flex:1 1 0, so the draft yields whatever the notice's content asks
for no matter where it sits. The offline notice is ~132px at 375 and ~179px at
390 — reference text a reader needs once, holding a third of a short panel
forever. Capping it at 96px with its own scroll gives the draft 58px at 375x450
and 124px at 390x560, against rega's measured 22px and 41px.
The panel is 60vh (plugin-studio.vue:466), NOT the viewport. Sizing off the
viewport is what made my earlier estimate wrong by 58px; the model that uses the
real panel reproduces rega's 22px exactly.
No floor anywhere: a floor is what clipped the draft to begin with.
---
plugin-designs/rudder-voice/rudder-voice.vue | 21 +++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/plugin-designs/rudder-voice/rudder-voice.vue b/plugin-designs/rudder-voice/rudder-voice.vue
index 16903cfb1..78978ed84 100644
--- a/plugin-designs/rudder-voice/rudder-voice.vue
+++ b/plugin-designs/rudder-voice/rudder-voice.vue
@@ -1148,7 +1148,26 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
.compact .bottom .mic.sm { padding: 9px 18px; font-size: 15px; gap: 6px; border-radius: 999px; }
.compact .bottom .mic.sm:not(.on) { background: var(--accent); color: #04121c; border-color: var(--accent); }
.compact .bottom .mic.sm.on { background: var(--rec); color: #fff; border-color: var(--rec); }
-.compact .notice, .compact .hint { order: 0; font-size: 12.5px; padding: 8px 10px; }
+/* ★ NOTICES SIT BELOW THE DRAFT IN THE CORNER PANEL, for the same reason they do on a phone: the
+ panel is short, and an engine-offline notice is several lines, so above the draft it eats the room
+ the draft needs. This used to say `order: 0`, which SILENTLY CANCELLED the phone rule below —
+ `.compact .notice` is (0,2,0) and the media query's `.notice` is (0,1,0), and a media query adds no
+ specificity, so the more specific rule won at every width and the notice stayed on top. The phone
+ rule (bac2431d0, 19:59) was already there; the corner widget (4e94e9216, 21:58) overrode it two
+ hours later without meaning to. Ordering it here fixes the widget at EVERY width, not only under
+ 480px, because the corner panel is short regardless of the viewport. No floor is involved: a floor
+ is what clipped the draft in the first place. */
+.compact .notice, .compact .hint { order: 1; font-size: 12.5px; padding: 8px 10px; }
+/* ★ AND THE NOTICE YIELDS. Ordering it below the draft fixes the reading order but NOT the height:
+ .notice is flex:0 1 auto and .compact .compose is flex:1 1 0, so the draft still gives up whatever
+ the notice's content asks for. rega measured the result at 375x450 — a 20px draft whose wheel moves
+ nothing. The engine-offline notice is the long one (apiBase, Go 1.22, the setup command), ~132px at
+ 375 and ~179px at 390, and it is REFERENCE text: a reader needs it once, not a third of a short
+ panel forever. Capping it and letting it scroll gives the draft 58px at 375x450 and 124px at
+ 390x560, measured against the 60vh panel the corner widget actually gets (plugin-studio.vue:466),
+ not the viewport — sizing off the viewport is what made my first estimate wrong by 58px.
+ Again: no floor anywhere. A floor is what clipped the draft to begin with. */
+.compact .notice { max-height: 96px; overflow-y: auto; overscroll-behavior: contain; }
.compact .debug { order: 4; }
/* A phone-sized panel: less padding, and a shorter draft so it stays in view below the controls. */
@media (max-width: 480px) {
From 79cbb41bc3382a8c4cd67d16f60e8e547ea52f0b Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Tue, 29 Sep 2026 11:56:47 +0000
Subject: [PATCH 57/69] Rudder's corner panel kept its notices above the draft,
and they never yielded
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two defects, one cause: on a short panel the engine-offline notice took the room
the draft needed.
FIRST, A PHONE RULE THAT NEVER RAN. bac2431d0 (19:59) put notices below the
draft "so the draft stays in view". 4e94e9216 (21:58) added the corner widget
with `.compact .notice { order: 0 }`. A media query adds NO specificity, so
`.compact .notice` (0,2,0) beat `@media { .notice }` (0,1,0) at every width and
the phone rule has been dead since the day it landed. Ordering the compact rule
to 1 fixes the widget at every width, not only under 480px, because the corner
panel is short regardless of the viewport.
SECOND, ORDER ALONE WAS NOT ENOUGH, which I only found by doing the arithmetic
rather than trusting the first fix. .notice is flex:0 1 auto and .compact
.compose is flex:1 1 0, so the draft yields whatever the notice's content asks
for no matter where it sits. The offline notice is ~132px at 375 and ~179px at
390 — reference text a reader needs once, holding a third of a short panel
forever. Capping it at 96px with its own scroll gives the draft 58px at 375x450
and 124px at 390x560, against rega's measured 22px and 41px.
The panel is 60vh (plugin-studio.vue:466), NOT the viewport. Sizing off the
viewport is what made my earlier estimate wrong by 58px; the model that uses the
real panel reproduces rega's 22px exactly.
No floor anywhere: a floor is what clipped the draft to begin with.
---
plugin-designs/rudder-voice/rudder-voice.vue | 21 +++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/plugin-designs/rudder-voice/rudder-voice.vue b/plugin-designs/rudder-voice/rudder-voice.vue
index 16903cfb1..78978ed84 100644
--- a/plugin-designs/rudder-voice/rudder-voice.vue
+++ b/plugin-designs/rudder-voice/rudder-voice.vue
@@ -1148,7 +1148,26 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
.compact .bottom .mic.sm { padding: 9px 18px; font-size: 15px; gap: 6px; border-radius: 999px; }
.compact .bottom .mic.sm:not(.on) { background: var(--accent); color: #04121c; border-color: var(--accent); }
.compact .bottom .mic.sm.on { background: var(--rec); color: #fff; border-color: var(--rec); }
-.compact .notice, .compact .hint { order: 0; font-size: 12.5px; padding: 8px 10px; }
+/* ★ NOTICES SIT BELOW THE DRAFT IN THE CORNER PANEL, for the same reason they do on a phone: the
+ panel is short, and an engine-offline notice is several lines, so above the draft it eats the room
+ the draft needs. This used to say `order: 0`, which SILENTLY CANCELLED the phone rule below —
+ `.compact .notice` is (0,2,0) and the media query's `.notice` is (0,1,0), and a media query adds no
+ specificity, so the more specific rule won at every width and the notice stayed on top. The phone
+ rule (bac2431d0, 19:59) was already there; the corner widget (4e94e9216, 21:58) overrode it two
+ hours later without meaning to. Ordering it here fixes the widget at EVERY width, not only under
+ 480px, because the corner panel is short regardless of the viewport. No floor is involved: a floor
+ is what clipped the draft in the first place. */
+.compact .notice, .compact .hint { order: 1; font-size: 12.5px; padding: 8px 10px; }
+/* ★ AND THE NOTICE YIELDS. Ordering it below the draft fixes the reading order but NOT the height:
+ .notice is flex:0 1 auto and .compact .compose is flex:1 1 0, so the draft still gives up whatever
+ the notice's content asks for. rega measured the result at 375x450 — a 20px draft whose wheel moves
+ nothing. The engine-offline notice is the long one (apiBase, Go 1.22, the setup command), ~132px at
+ 375 and ~179px at 390, and it is REFERENCE text: a reader needs it once, not a third of a short
+ panel forever. Capping it and letting it scroll gives the draft 58px at 375x450 and 124px at
+ 390x560, measured against the 60vh panel the corner widget actually gets (plugin-studio.vue:466),
+ not the viewport — sizing off the viewport is what made my first estimate wrong by 58px.
+ Again: no floor anywhere. A floor is what clipped the draft to begin with. */
+.compact .notice { max-height: 96px; overflow-y: auto; overscroll-behavior: contain; }
.compact .debug { order: 4; }
/* A phone-sized panel: less padding, and a shorter draft so it stays in view below the controls. */
@media (max-width: 480px) {
From 3386988dc5ac61b3afbdf1a4ad56ce58c97692c6 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Tue, 29 Sep 2026 13:40:57 +0000
Subject: [PATCH 58/69] Two lists joined with an English comma in every locale
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
formatList (R29) replaces join(', ') at the two sites that a reader sees. The
separator was never a missing key — no key can fix punctuation between
interpolated values — which is why both survived every i18n gate.
connectivity-map.vue:365, CONJUNCTION. The Indian states with no boundary are
ALL missing one. The names land inside a translated sentence at :595, and the ar
message reads "لا توجد بعد حدود ولاية لـ {names}: …", so English commas were
being printed inside Arabic prose.
GiCombos.vue:49, DISJUNCTION, decided from the matcher and not from what reads
better: useSelectionBus.js:299 tests cur.includes(value) and toggleFilterValue
adds to the list, so two values on one facet WIDEN the result set. "Africa and
Asia" would state the opposite of what the filter does. This changes the English
too — "Africa, Asia" becomes "Africa or Asia" — which is a gain, because a comma
silently reads as ALL.
Measured, the same three names and two facet values through each type:
en "Ladakh, Puducherry, and Chandigarh" "Africa or Asia"
ar "Ladakh وPuducherry وChandigarh" "Africa أو Asia"
zh "Ladakh、Puducherry和Chandigarh" "Africa或Asia"
was "Ladakh, Puducherry, Chandigarh" "Africa, Asia"
ar joins with و and no comma at all; zh uses 、. Neither is reachable by keying.
Not converted, and deliberately: useBoard.js:126 is a console.warn for
developers, and rudder-voice.vue:971 is in a component with zero i18n imports and
no catalogue area — English by construction, so it converts with the rest of
Rudder if Rudder is ever localized.
Source only. The rebuilds are regenerated after this rebases onto landed R29.
---
plugin-designs/cities-internet/connectivity-map.vue | 8 ++++++--
.../gospel-internet/src/components/GiCombos.vue | 8 +++++++-
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/plugin-designs/cities-internet/connectivity-map.vue b/plugin-designs/cities-internet/connectivity-map.vue
index 9d5470e8f..fab04a86a 100644
--- a/plugin-designs/cities-internet/connectivity-map.vue
+++ b/plugin-designs/cities-internet/connectivity-map.vue
@@ -41,7 +41,7 @@ function setGlobal(name, value) {
*/
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue'
import { useOptionalT, fillParams } from '@map/composables/useOptionalT.js'
-import { tStatic } from '@map/i18n/index.js'
+import { tStatic, formatList } from '@map/i18n/index.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { addMapAttribution } from '@map/composables/useMapAttribution'
@@ -362,7 +362,11 @@ function showAdm1Sex(sex) {
const map = mapRef.value
if (map?.getLayer('ci-adm1-fill')) map.setPaintProperty('ci-adm1-fill', 'fill-color', adm1Fill(sex))
}
-const adm1Missing = computed(() => (adm1.value?.meta?.boundaries?.IND?.missing || []).join(', '))
+// CONJUNCTION: these states are ALL missing a boundary, so "A, B, and C" is the honest reading. The
+// names land inside a translated sentence (connectivity.adm1.missing, :595), and ar reads
+// "لا توجد بعد حدود ولاية لـ {names}: …" — so a hardcoded ", " put English separators inside Arabic
+// prose. ar joins with و and no comma, zh with 、; no key could have fixed that.
+const adm1Missing = computed(() => formatList(adm1.value?.meta?.boundaries?.IND?.missing || [], { type: 'conjunction' }))
// ── HONOUR THE PROVIDER SWITCH: this profile builds its map RAW ─────────────
// Every bundle that goes through useMapInstance gets the switch for free. This one calls
diff --git a/plugin-designs/gospel-internet/src/components/GiCombos.vue b/plugin-designs/gospel-internet/src/components/GiCombos.vue
index 3895bf40d..69b8fc8b0 100644
--- a/plugin-designs/gospel-internet/src/components/GiCombos.vue
+++ b/plugin-designs/gospel-internet/src/components/GiCombos.vue
@@ -17,6 +17,7 @@ import { computed, provide } from 'vue'
import { createSelectionBus } from '@map/composables/useSelectionBus.js'
import DashboardFilterChips from '@map/components/dashboards/DashboardFilterChips.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
+import { formatList } from '@map/i18n/index.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { filterRows, label, FACET_RAIL } from '../model.js'
@@ -45,7 +46,12 @@ function labelFor(key, value) {
const n = board.tree.value.byId.get(board.treeNode.value)
if (n) return n.n
}
- return [].concat(value).map((v) => labelOne(key, v)).join(', ')
+ // DISJUNCTION, from the matcher rather than from what reads better: useSelectionBus.js:299 tests
+ // `cur.includes(value)` and toggleFilterValue ADDS to the list, so two values on one facet WIDEN the
+ // result set. "Africa and Asia" would claim the opposite of what the filter does — and a chip saying
+ // ALL while the data shows ANY is worse than a comma, because a comma is merely ambiguous where
+ // "and" is confidently wrong.
+ return formatList([].concat(value).map((v) => labelOne(key, v)), { type: 'disjunction' })
}
provide('selectionBus', bus)
// ★ THE FACET LABELS ARE A COMPUTED, NOT A LITERAL. Evaluated once inside provide() they froze in
From 02b1e707c77f03c7853755160acbea0d3ae46c8a Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Tue, 29 Sep 2026 14:10:04 +0000
Subject: [PATCH 59/69] The list and the table painted thousands of pixels
below their host box
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Neither element had a :host rule at all, so each fell back to the custom-element
default — display:inline, no height of its own — while its root div carried
position:absolute;inset:0. Absolute positioning resolves against the nearest
POSITIONED ancestor, so on a partner page both resolved against the PARTNER's
layout rather than against this element.
rega measured it on a partner page with a 420px host box: connectivity-list
painted to y=16,799 and data-tables to y=1,698 — 16,355px and 1,254px below the
box, over whatever the partner had underneath.
The same fix as connectivity-map's (60cb4ff18), for the same reason:
:host{display:block;position:relative;width:100%;height:100%;min-height:320px}
display:block gives the element a box; position:relative makes that box the
containing block; height:100% lets a definite-height slot decide; min-height is
the floor so an auto-height host grows to fit instead of collapsing.
NO NEW SCROLL CONTAINER WAS NEEDED. .ci and .ct already carry overflow-y:auto —
the scroller existed, it simply had no host to be bounded by. Once contained,
the list and the table scroll inside the box, which is what an embed should do
in a definite-height slot.
min-height is 320px here, not the map's 420px: regions-list.vue:408 is the house
pattern for list profiles, and a list is legible shorter than a map is.
---
plugin-designs/cities-internet/connectivity-list.vue | 11 +++++++++++
plugin-designs/cities-internet/data-tables.vue | 6 ++++++
2 files changed, 17 insertions(+)
diff --git a/plugin-designs/cities-internet/connectivity-list.vue b/plugin-designs/cities-internet/connectivity-list.vue
index 4720b7ba6..72baeb8bd 100644
--- a/plugin-designs/cities-internet/connectivity-list.vue
+++ b/plugin-designs/cities-internet/connectivity-list.vue
@@ -150,6 +150,17 @@ const band = (a) => a === 'yes' ? 'ok' : a === 'partial' ? 'mid' : a === 'no' ?
const bandLabel = (a) => t('connectivity.list.band.' + (['yes', 'partial', 'no'].includes(a) ? a : 'unknown'))
useShadowStyles(
+ // ★ THE LIST MUST STAY INSIDE ITS HOST BOX. This element had NO :host rule, so a custom element
+ // falls back to display:inline with no height of its own, and .ci below — position:absolute;inset:0
+ // — resolved against whatever the PARTNER page happened to have positioned. rega measured the result
+ // on a partner page: content painting to y=16,799, which is 16,355px BELOW a 420px host box.
+ // display:block gives it a box; position:relative makes that box the containing block .ci resolves
+ // against; height:100% lets a definite-height slot decide; min-height is the floor for an AUTO-height
+ // host, which grows to fit instead of collapsing to nothing. .ci already carries overflow-y:auto, so
+ // once it is contained the list SCROLLS inside the box rather than overflowing it — no new scroll
+ // container is needed, only a host for the one that is already there.
+ // Pattern: regions-list.vue:408, plus the position:relative that connectivity-map needed (60cb4ff18).
+ ':host{display:block;position:relative;width:100%;height:100%;min-height:320px}' +
'.ci{position:absolute;inset:0;overflow-y:auto;padding:20px 18px 44px;background:#f6f7f9;color:#0f1419;' +
"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
'.ci.is-dark{background:#0a1410;color:#e5e7eb}' +
diff --git a/plugin-designs/cities-internet/data-tables.vue b/plugin-designs/cities-internet/data-tables.vue
index 7a9c8f207..bb2606369 100644
--- a/plugin-designs/cities-internet/data-tables.vue
+++ b/plugin-designs/cities-internet/data-tables.vue
@@ -88,6 +88,12 @@ watch(current, (t) => t && ensure(t.file), { immediate: true })
const table = computed(() => cache.value[current.value?.file] || null)
useShadowStyles(
+ // ★ THE TABLE MUST STAY INSIDE ITS HOST BOX — the same defect and the same fix as connectivity-list.
+ // With no :host rule the element is inline with no height, and .ct (position:absolute;inset:0)
+ // resolved against the partner page's positioned ancestor instead of this element. rega measured
+ // content painting to y=1,698, which is 1,254px BELOW a 420px host box.
+ // .ct already has overflow-y:auto, so containing it is enough to make the table scroll in the box.
+ ':host{display:block;position:relative;width:100%;height:100%;min-height:320px}' +
'.ct{position:absolute;inset:0;overflow-y:auto;padding:18px;background:#f6f7f9;color:#0f1419;' +
"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
'.ct.is-dark{background:#0a1410;color:#e5e7eb}' +
From af2d4cc5f88b032cad2ca2daba1856e2e933f134 Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Tue, 29 Sep 2026 14:10:33 +0000
Subject: [PATCH 60/69] Rebuild cities-internet for the contained list and
table
BUNDLE=cities-internet bun x vite build, then bun run tree and fix:sri.
One integrity hash moves, in the cities-internet rows only; the CDN pins are
byte-identical on both sides. Verified in the BUILT output rather than the
source: the shipped bundle carries both new :host rules.
---
.../cities-internet/cities-internet.js | 54 +++++++++----------
.../connectivity-list/index.html | 2 +-
.../connectivity-map/index.html | 2 +-
.../cities-internet/data-tables/index.html | 2 +-
public/dx-maps/doxa-maps-spa/manifest.json | 10 ++--
public/dx-maps/manifest.json | 10 ++--
6 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/public/dx-maps/cities-internet/cities-internet.js b/public/dx-maps/cities-internet/cities-internet.js
index 9b4d921a2..c91b90db4 100644
--- a/public/dx-maps/cities-internet/cities-internet.js
+++ b/public/dx-maps/cities-internet/cities-internet.js
@@ -2,51 +2,51 @@
* @vue/shared v3.5.42
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
-**/function wr(e){const t=Object.create(null);for(const o of e.split(","))t[o]=1;return o=>o in t}const je={},Xo=[],Gt=()=>{},Ei=()=>!1,la=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ca=e=>e.startsWith("onUpdate:"),et=Object.assign,kr=(e,t)=>{const o=e.indexOf(t);o>-1&&e.splice(o,1)},vd=Object.prototype.hasOwnProperty,De=(e,t)=>vd.call(e,t),ke=Array.isArray,vo=e=>kn(e)==="[object Map]",ua=e=>kn(e)==="[object Set]",Ii=e=>kn(e)==="[object Date]",Ae=e=>typeof e=="function",We=e=>typeof e=="string",Ct=e=>typeof e=="symbol",Be=e=>e!==null&&typeof e=="object",Ri=e=>(Be(e)||Ae(e))&&Ae(e.then)&&Ae(e.catch),Di=Object.prototype.toString,kn=e=>Di.call(e),wd=e=>kn(e).slice(8,-1),pa=e=>kn(e)==="[object Object]",da=e=>We(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,xn=wr(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ga=e=>{const t=Object.create(null);return o=>t[o]||(t[o]=e(o))},kd=/-\w/g,st=ga(e=>e.replace(kd,t=>t.slice(1).toUpperCase())),xd=/\B([A-Z])/g,Tt=ga(e=>e.replace(xd,"-$1").toLowerCase()),ma=ga(e=>e.charAt(0).toUpperCase()+e.slice(1)),xr=ga(e=>e?`on${ma(e)}`:""),jt=(e,t)=>!Object.is(e,t),Cr=(e,...t)=>{for(let o=0;o{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:o})},Cd=e=>{const t=parseFloat(e);return isNaN(t)?e:t},zi=e=>{const t=We(e)?Number(e):NaN;return isNaN(t)?e:t};let Gi;const ha=()=>Gi||(Gi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function He(e){if(ke(e)){const t={};for(let o=0;o{if(o){const n=o.split(Pd);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function ye(e){let t="";if(We(e))t=e;else if(ke(e))for(let o=0;o!!(e&&e.__v_isRef===!0),G=e=>We(e)?e:e==null?"":ke(e)||Be(e)&&(e.toString===Di||!Ae(e.toString))?Ui(e)?G(e.value):JSON.stringify(e,Wi,2):String(e),Wi=(e,t)=>Ui(t)?Wi(e,t.value):vo(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((o,[n,a],r)=>(o[Ar(n,r)+" =>"]=a,o),{})}:ua(t)?{[`Set(${t.size})`]:[...t.values()].map(o=>Ar(o))}:Ct(t)?Ar(t):Be(t)&&!ke(t)&&!pa(t)?String(t):t,Ar=(e,t="")=>{var o;return Ct(e)?`Symbol(${(o=e.description)!=null?o:t})`:e};/**
+**/function wr(e){const t=Object.create(null);for(const o of e.split(","))t[o]=1;return o=>o in t}const je={},Xo=[],Gt=()=>{},Ei=()=>!1,la=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ca=e=>e.startsWith("onUpdate:"),et=Object.assign,kr=(e,t)=>{const o=e.indexOf(t);o>-1&&e.splice(o,1)},vd=Object.prototype.hasOwnProperty,De=(e,t)=>vd.call(e,t),ke=Array.isArray,vo=e=>kn(e)==="[object Map]",ua=e=>kn(e)==="[object Set]",Ii=e=>kn(e)==="[object Date]",Ae=e=>typeof e=="function",We=e=>typeof e=="string",Ct=e=>typeof e=="symbol",Be=e=>e!==null&&typeof e=="object",Ri=e=>(Be(e)||Ae(e))&&Ae(e.then)&&Ae(e.catch),Di=Object.prototype.toString,kn=e=>Di.call(e),wd=e=>kn(e).slice(8,-1),pa=e=>kn(e)==="[object Object]",da=e=>We(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,xn=wr(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ga=e=>{const t=Object.create(null);return o=>t[o]||(t[o]=e(o))},kd=/-\w/g,st=ga(e=>e.replace(kd,t=>t.slice(1).toUpperCase())),xd=/\B([A-Z])/g,Tt=ga(e=>e.replace(xd,"-$1").toLowerCase()),ha=ga(e=>e.charAt(0).toUpperCase()+e.slice(1)),xr=ga(e=>e?`on${ha(e)}`:""),jt=(e,t)=>!Object.is(e,t),Cr=(e,...t)=>{for(let o=0;o{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:o})},Cd=e=>{const t=parseFloat(e);return isNaN(t)?e:t},zi=e=>{const t=We(e)?Number(e):NaN;return isNaN(t)?e:t};let Gi;const ma=()=>Gi||(Gi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function He(e){if(ke(e)){const t={};for(let o=0;o{if(o){const n=o.split(Pd);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function ye(e){let t="";if(We(e))t=e;else if(ke(e))for(let o=0;o!!(e&&e.__v_isRef===!0),G=e=>We(e)?e:e==null?"":ke(e)||Be(e)&&(e.toString===Di||!Ae(e.toString))?Ui(e)?G(e.value):JSON.stringify(e,Wi,2):String(e),Wi=(e,t)=>Ui(t)?Wi(e,t.value):vo(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((o,[n,a],r)=>(o[Ar(n,r)+" =>"]=a,o),{})}:ua(t)?{[`Set(${t.size})`]:[...t.values()].map(o=>Ar(o))}:Ct(t)?Ar(t):Be(t)&&!ke(t)&&!pa(t)?String(t):t,Ar=(e,t="")=>{var o;return Ct(e)?`Symbol(${(o=e.description)!=null?o:t})`:e};/**
* @vue/reactivity v3.5.42
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
-**/let nt;class Ki{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&nt&&(nt.active?(this.parent=nt,this.index=(nt.scopes||(nt.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,o;if(this.scopes){const n=this.scopes.slice();for(t=0,o=n.length;t0&&--this._on===0){if(nt===this)nt=this.prevScope;else{let t=nt;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let o,n;for(o=0,n=this.effects.length;o0)return;if(An){let t=An;for(An=void 0;t;){const o=t.next;t.next=void 0,t.flags&=-9,t=o}}let e;for(;Cn;){let t=Cn;for(Cn=void 0;t;){const o=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=o}}if(e)throw e}function Ji(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Xi(e){let t,o=e.depsTail,n=o;for(;n;){const a=n.prevDep;n.version===-1?(n===o&&(o=a),Nr(n),Md(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=a}e.deps=t,e.depsTail=o}function Tr(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Qi(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Qi(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Pn)||(e.globalVersion=Pn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Tr(e))))return;e.flags|=2;const t=e.dep,o=Ke,n=Mt;Ke=e,Mt=!0;try{Ji(e);const a=e.fn(e._value);(t.version===0||jt(a,e._value))&&(e.flags|=128,e._value=a,t.version++)}catch(a){throw t.version++,a}finally{Ke=o,Mt=n,Xi(e),e.flags&=-3}}function Nr(e,t=!1){const{dep:o,prevSub:n,nextSub:a}=e;if(n&&(n.nextSub=a,e.prevSub=void 0),a&&(a.prevSub=n,e.nextSub=void 0),o.subs===e&&(o.subs=n,!n&&o.computed)){o.computed.flags&=-5;for(let r=o.computed.deps;r;r=r.nextDep)Nr(r,!0)}!t&&!--o.sc&&o.map&&o.map.delete(o.key)}function Md(e){const{prevDep:t,nextDep:o}=e;t&&(t.nextDep=o,e.prevDep=void 0),o&&(o.prevDep=t,e.nextDep=void 0)}let Mt=!0;const el=[];function Ht(){el.push(Mt),Mt=!1}function Ut(){const e=el.pop();Mt=e===void 0?!0:e}function tl(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const o=Ke;Ke=void 0;try{t()}finally{Ke=o}}}let Pn=0;class Fd{constructor(t,o){this.sub=t,this.dep=o,this.version=o.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Or{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Ke||!Mt||Ke===this.computed)return;let o=this.activeLink;if(o===void 0||o.sub!==Ke)o=this.activeLink=new Fd(Ke,this),Ke.deps?(o.prevDep=Ke.depsTail,Ke.depsTail.nextDep=o,Ke.depsTail=o):Ke.deps=Ke.depsTail=o,ol(o);else if(o.version===-1&&(o.version=this.version,o.nextDep)){const n=o.nextDep;n.prevDep=o.prevDep,o.prevDep&&(o.prevDep.nextDep=n),o.prevDep=Ke.depsTail,o.nextDep=void 0,Ke.depsTail.nextDep=o,Ke.depsTail=o,Ke.deps===o&&(Ke.deps=n)}return o}trigger(t){this.version++,Pn++,this.notify(t)}notify(t){Sr();try{for(let o=this.subs;o;o=o.prevSub)o.sub.notify()&&o.sub.dep.notify()}finally{Lr()}}}function ol(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)ol(n)}const o=e.dep.subs;o!==e&&(e.prevSub=o,o&&(o.nextSub=e)),e.dep.subs=e}}const ba=new WeakMap,zo=Symbol(""),Mr=Symbol(""),Sn=Symbol("");function dt(e,t,o){if(Mt&&Ke){let n=ba.get(e);n||ba.set(e,n=new Map);let a=n.get(o);a||(n.set(o,a=new Or),a.map=n,a.key=o),a.track()}}function oo(e,t,o,n,a,r){const s=ba.get(e);if(!s){Pn++;return}const i=l=>{l&&l.trigger()};if(Sr(),t==="clear")s.forEach(i);else{const l=ke(e),c=l&&da(o);if(l&&o==="length"){const u=Number(n);s.forEach((p,d)=>{(d==="length"||d===Sn||!Ct(d)&&d>=u)&&i(p)})}else switch((o!==void 0||s.has(void 0))&&i(s.get(o)),c&&i(s.get(Sn)),t){case"add":l?c&&i(s.get("length")):(i(s.get(zo)),vo(e)&&i(s.get(Mr)));break;case"delete":l||(i(s.get(zo)),vo(e)&&i(s.get(Mr)));break;case"set":vo(e)&&i(s.get(zo));break}}Lr()}function Ed(e,t){const o=ba.get(e);return o&&o.get(t)}function Qo(e){const t=Fe(e);return t===e?t:(dt(t,"iterate",Sn),At(e)?t:t.map(Ft))}function $a(e){return dt(e=Fe(e),"iterate",Sn),e}function Wt(e,t){return ro(e)?tn(ao(e)?Ft(t):t):Ft(t)}const Id={__proto__:null,[Symbol.iterator](){return Fr(this,Symbol.iterator,e=>Wt(this,e))},concat(...e){return Qo(this).concat(...e.map(t=>ke(t)?Qo(t):t))},entries(){return Fr(this,"entries",e=>(e[1]=Wt(this,e[1]),e))},every(e,t){return no(this,"every",e,t,void 0,arguments)},filter(e,t){return no(this,"filter",e,t,o=>o.map(n=>Wt(this,n)),arguments)},find(e,t){return no(this,"find",e,t,o=>Wt(this,o),arguments)},findIndex(e,t){return no(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return no(this,"findLast",e,t,o=>Wt(this,o),arguments)},findLastIndex(e,t){return no(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return no(this,"forEach",e,t,void 0,arguments)},includes(...e){return Er(this,"includes",e)},indexOf(...e){return Er(this,"indexOf",e)},join(e){return Qo(this).join(e)},lastIndexOf(...e){return Er(this,"lastIndexOf",e)},map(e,t){return no(this,"map",e,t,void 0,arguments)},pop(){return Ln(this,"pop")},push(...e){return Ln(this,"push",e)},reduce(e,...t){return nl(this,"reduce",e,t)},reduceRight(e,...t){return nl(this,"reduceRight",e,t)},shift(){return Ln(this,"shift")},some(e,t){return no(this,"some",e,t,void 0,arguments)},splice(...e){return Ln(this,"splice",e)},toReversed(){return Qo(this).toReversed()},toSorted(e){return Qo(this).toSorted(e)},toSpliced(...e){return Qo(this).toSpliced(...e)},unshift(...e){return Ln(this,"unshift",e)},values(){return Fr(this,"values",e=>Wt(this,e))}};function Fr(e,t,o){const n=$a(e),a=n[t]();return n!==e&&!At(e)&&(a._next=a.next,a.next=()=>{const r=a._next();return r.done||(r.value=o(r.value)),r}),a}const Rd=Array.prototype;function no(e,t,o,n,a,r){const s=$a(e),i=s!==e&&!At(e),l=s[t];if(l!==Rd[t]){const p=l.apply(e,r);return i?Ft(p):p}let c=o;s!==e&&(i?c=function(p,d){return o.call(this,Wt(e,p),d,e)}:o.length>2&&(c=function(p,d){return o.call(this,p,d,e)}));const u=l.call(s,c,n);return i&&a?a(u):u}function nl(e,t,o,n){const a=$a(e),r=a!==e&&!At(e);let s=o,i=!1;a!==e&&(r?(i=n.length===0,s=function(c,u,p){return i&&(i=!1,c=Wt(e,c)),o.call(this,c,Wt(e,u),p,e)}):o.length>3&&(s=function(c,u,p){return o.call(this,c,u,p,e)}));const l=a[t](s,...n);return i?Wt(e,l):l}function Er(e,t,o){const n=Fe(e);dt(n,"iterate",Sn);const a=n[t](...o);return(a===-1||a===!1)&&xa(o[0])?(o[0]=Fe(o[0]),n[t](...o)):a}function Ln(e,t,o=[]){Ht(),Sr();const n=Fe(e)[t].apply(e,o);return Lr(),Ut(),n}const Dd=wr("__proto__,__v_isRef,__isVue"),al=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Ct));function Bd(e){Ct(e)||(e=String(e));const t=Fe(this);return dt(t,"has",e),t.hasOwnProperty(e)}class rl{constructor(t=!1,o=!1){this._isReadonly=t,this._isShallow=o}get(t,o,n){if(o==="__v_skip")return t.__v_skip;const a=this._isReadonly,r=this._isShallow;if(o==="__v_isReactive")return!a;if(o==="__v_isReadonly")return a;if(o==="__v_isShallow")return r;if(o==="__v_raw")return n===(a?r?pl:ul:r?cl:ll).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const s=ke(t);if(!a){let l;if(s&&(l=Id[o]))return l;if(o==="hasOwnProperty")return Bd}const i=Reflect.get(t,o,Ze(t)?t:n);if((Ct(o)?al.has(o):Dd(o))||(a||dt(t,"get",o),r))return i;if(Ze(i)){const l=s&&da(o)?i:i.value;return a&&Be(l)?Rr(l):l}return Be(i)?a?Rr(i):en(i):i}}class sl extends rl{constructor(t=!1){super(!1,t)}set(t,o,n,a){let r=t[o];const s=ke(t)&&da(o);if(!this._isShallow){const c=ro(r);if(!At(n)&&!ro(n)&&(r=Fe(r),n=Fe(n)),!s&&Ze(r)&&!Ze(n))return c||(r.value=n),!0}const i=s?Number(o)e,ya=e=>Reflect.getPrototypeOf(e);function Ud(e,t,o){return function(...n){const a=this.__v_raw,r=Fe(a),s=vo(r),i=e==="entries"||e===Symbol.iterator&&s,l=e==="keys"&&s,c=a[e](...n),u=o?Ir:t?tn:Ft;return!t&&dt(r,"iterate",l?Mr:zo),et(Object.create(c),{next(){const{value:p,done:d}=c.next();return d?{value:p,done:d}:{value:i?[u(p[0]),u(p[1])]:u(p),done:d}}})}}function va(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Wd(e,t){const o={get(a){const r=this.__v_raw,s=Fe(r),i=Fe(a);e||(jt(a,i)&&dt(s,"get",a),dt(s,"get",i));const{has:l}=ya(s),c=t?Ir:e?tn:Ft;if(l.call(s,a))return c(r.get(a));if(l.call(s,i))return c(r.get(i));r!==s&&r.get(a)},get size(){const a=this.__v_raw;return!e&&dt(Fe(a),"iterate",zo),a.size},has(a){const r=this.__v_raw,s=Fe(r),i=Fe(a);return e||(jt(a,i)&&dt(s,"has",a),dt(s,"has",i)),a===i?r.has(a):r.has(a)||r.has(i)},forEach(a,r){const s=this,i=s.__v_raw,l=Fe(i),c=t?Ir:e?tn:Ft;return!e&&dt(l,"iterate",zo),i.forEach((u,p)=>a.call(r,c(u),c(p),s))}};return et(o,e?{add:va("add"),set:va("set"),delete:va("delete"),clear:va("clear")}:{add(a){const r=Fe(this),s=ya(r),i=Fe(a),l=!t&&!At(a)&&!ro(a)?i:a;return s.has.call(r,l)||jt(a,l)&&s.has.call(r,a)||jt(i,l)&&s.has.call(r,i)||(r.add(l),oo(r,"add",l,l)),this},set(a,r){!t&&!At(r)&&!ro(r)&&(r=Fe(r));const s=Fe(this),{has:i,get:l}=ya(s);let c=i.call(s,a);c||(a=Fe(a),c=i.call(s,a));const u=l.call(s,a);return s.set(a,r),c?jt(r,u)&&oo(s,"set",a,r):oo(s,"add",a,r),this},delete(a){const r=Fe(this),{has:s,get:i}=ya(r);let l=s.call(r,a);l||(a=Fe(a),l=s.call(r,a)),i&&i.call(r,a);const c=r.delete(a);return l&&oo(r,"delete",a,void 0),c},clear(){const a=Fe(this),r=a.size!==0,s=a.clear();return r&&oo(a,"clear",void 0,void 0),s}}),["keys","values","entries",Symbol.iterator].forEach(a=>{o[a]=Ud(a,e,t)}),o}function wa(e,t){const o=Wd(e,t);return(n,a,r)=>a==="__v_isReactive"?!e:a==="__v_isReadonly"?e:a==="__v_raw"?n:Reflect.get(De(o,a)&&a in n?o:n,a,r)}const Kd={get:wa(!1,!1)},Vd={get:wa(!1,!0)},qd={get:wa(!0,!1)},Yd={get:wa(!0,!0)},ll=new WeakMap,cl=new WeakMap,ul=new WeakMap,pl=new WeakMap;function Zd(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function en(e){return ro(e)?e:ka(e,!1,zd,Kd,ll)}function Jd(e){return ka(e,!1,jd,Vd,cl)}function Rr(e){return ka(e,!0,Gd,qd,ul)}function Nw(e){return ka(e,!0,Hd,Yd,pl)}function ka(e,t,o,n,a){if(!Be(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const r=a.get(e);if(r)return r;const s=Zd(wd(e));if(s===0)return e;const i=new Proxy(e,s===2?n:o);return a.set(e,i),i}function ao(e){return ro(e)?ao(e.__v_raw):!!(e&&e.__v_isReactive)}function ro(e){return!!(e&&e.__v_isReadonly)}function At(e){return!!(e&&e.__v_isShallow)}function xa(e){return e?!!e.__v_raw:!1}function Fe(e){const t=e&&e.__v_raw;return t?Fe(t):e}function wo(e){return!De(e,"__v_skip")&&Object.isExtensible(e)&&Bi(e,"__v_skip",!0),e}const Ft=e=>Be(e)?en(e):e,tn=e=>Be(e)?Rr(e):e;function Ze(e){return e?e.__v_isRef===!0:!1}function ie(e){return dl(e,!1)}function ko(e){return dl(e,!0)}function dl(e,t){return Ze(e)?e:new Xd(e,t)}class Xd{constructor(t,o){this.dep=new Or,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=o?t:Fe(t),this._value=o?t:Ft(t),this.__v_isShallow=o}get value(){return this.dep.track(),this._value}set value(t){const o=this._rawValue,n=this.__v_isShallow||At(t)||ro(t);t=n?t:Fe(t),jt(t,o)&&(this._rawValue=t,this._value=n?t:Ft(t),this.dep.trigger())}}function H(e){return Ze(e)?e.value:e}const Qd={get:(e,t,o)=>t==="__v_raw"?e:H(Reflect.get(e,t,o)),set:(e,t,o,n)=>{const a=e[t];return Ze(a)&&!Ze(o)?(a.value=o,!0):Reflect.set(e,t,o,n)}};function gl(e){return ao(e)?e:new Proxy(e,Qd)}function eg(e){const t=ke(e)?new Array(e.length):{};for(const o in e)t[o]=hl(e,o);return t}class tg{constructor(t,o,n){this._object=t,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0,this._key=Ct(o)?o:String(o),this._raw=Fe(t);let a=!0,r=t;if(!ke(t)||Ct(this._key)||!da(this._key))do a=!xa(r)||At(r);while(a&&(r=r.__v_raw));this._shallow=a}get value(){let t=this._object[this._key];return this._shallow&&(t=H(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&Ze(this._raw[this._key])){const o=this._object[this._key];if(Ze(o)){o.value=t;return}}this._object[this._key]=t}get dep(){return Ed(this._raw,this._key)}}class og{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function ml(e,t,o){return Ze(e)?e:Ae(e)?new og(e):Be(e)&&arguments.length>1?hl(e,t,o):ie(e)}function hl(e,t,o){return new tg(e,t,o)}class ng{constructor(t,o,n){this.fn=t,this.setter=o,this._value=void 0,this.dep=new Or(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Pn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!o,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&Ke!==this)return Zi(this,!0),!0}get value(){const t=this.dep.track();return Qi(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function ag(e,t,o=!1){let n,a;return Ae(e)?n=e:(n=e.get,a=e.set),new ng(n,a,o)}const Ca={},Aa=new WeakMap;let Go;function rg(e,t=!1,o=Go){if(o){let n=Aa.get(o);n||Aa.set(o,n=[]),n.push(e)}}function sg(e,t,o=je){const{immediate:n,deep:a,once:r,scheduler:s,augmentJob:i,call:l}=o,c=k=>a?k:At(k)||a===!1||a===0?so(k,1):so(k);let u,p,d,w,y=!1,m=!1;if(Ze(e)?(p=()=>e.value,y=At(e)):ao(e)?(p=()=>c(e),y=!0):ke(e)?(m=!0,y=e.some(k=>ao(k)||At(k)),p=()=>e.map(k=>{if(Ze(k))return k.value;if(ao(k))return c(k);if(Ae(k))return l?l(k,2):k()})):Ae(e)?t?p=l?()=>l(e,2):e:p=()=>{if(d){Ht();try{d()}finally{Ut()}}const k=Go;Go=u;try{return l?l(e,3,[w]):e(w)}finally{Go=k}}:p=Gt,t&&a){const k=p,C=a===!0?1/0:a;p=()=>so(k(),C)}const g=Vi(),b=()=>{u.stop(),g&&g.active&&kr(g.effects,u)};if(r&&t){const k=t;t=(...C)=>{const S=k(...C);return b(),S}}let x=m?new Array(e.length).fill(Ca):Ca;const $=k=>{if(!(!(u.flags&1)||!u.dirty&&!k))if(t){const C=u.run();if(k||a||y||(m?C.some((S,L)=>jt(S,x[L])):jt(C,x))){d&&d();const S=Go;Go=u;try{const L=[C,x===Ca?void 0:m&&x[0]===Ca?[]:x,w];x=C,l?l(t,3,L):t(...L)}finally{Go=S}}}else u.run()};return i&&i($),u=new qi(p),u.scheduler=s?()=>s($,!1):$,w=k=>rg(k,!1,u),d=u.onStop=()=>{const k=Aa.get(u);if(k){if(l)l(k,4);else for(const C of k)C();Aa.delete(u)}},t?n?$(!0):x=u.run():s?s($.bind(null,!0),!0):u.run(),b.pause=u.pause.bind(u),b.resume=u.resume.bind(u),b.stop=b,b}function so(e,t=1/0,o){if(t<=0||!Be(e)||e.__v_skip||(o=o||new Map,(o.get(e)||0)>=t))return e;if(o.set(e,t),t--,Ze(e))so(e.value,t,o);else if(ke(e))for(let n=0;n{so(n,t,o)});else if(pa(e)){for(const n in e)so(e[n],t,o);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&so(e[n],t,o)}return e}/**
+**/let nt;class Ki{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&nt&&(nt.active?(this.parent=nt,this.index=(nt.scopes||(nt.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,o;if(this.scopes){const n=this.scopes.slice();for(t=0,o=n.length;t0&&--this._on===0){if(nt===this)nt=this.prevScope;else{let t=nt;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let o,n;for(o=0,n=this.effects.length;o0)return;if(An){let t=An;for(An=void 0;t;){const o=t.next;t.next=void 0,t.flags&=-9,t=o}}let e;for(;Cn;){let t=Cn;for(Cn=void 0;t;){const o=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=o}}if(e)throw e}function Ji(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Xi(e){let t,o=e.depsTail,n=o;for(;n;){const a=n.prevDep;n.version===-1?(n===o&&(o=a),Nr(n),Md(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=a}e.deps=t,e.depsTail=o}function Tr(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Qi(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Qi(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Pn)||(e.globalVersion=Pn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Tr(e))))return;e.flags|=2;const t=e.dep,o=Ke,n=Mt;Ke=e,Mt=!0;try{Ji(e);const a=e.fn(e._value);(t.version===0||jt(a,e._value))&&(e.flags|=128,e._value=a,t.version++)}catch(a){throw t.version++,a}finally{Ke=o,Mt=n,Xi(e),e.flags&=-3}}function Nr(e,t=!1){const{dep:o,prevSub:n,nextSub:a}=e;if(n&&(n.nextSub=a,e.prevSub=void 0),a&&(a.prevSub=n,e.nextSub=void 0),o.subs===e&&(o.subs=n,!n&&o.computed)){o.computed.flags&=-5;for(let r=o.computed.deps;r;r=r.nextDep)Nr(r,!0)}!t&&!--o.sc&&o.map&&o.map.delete(o.key)}function Md(e){const{prevDep:t,nextDep:o}=e;t&&(t.nextDep=o,e.prevDep=void 0),o&&(o.prevDep=t,e.nextDep=void 0)}let Mt=!0;const el=[];function Ht(){el.push(Mt),Mt=!1}function Ut(){const e=el.pop();Mt=e===void 0?!0:e}function tl(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const o=Ke;Ke=void 0;try{t()}finally{Ke=o}}}let Pn=0;class Fd{constructor(t,o){this.sub=t,this.dep=o,this.version=o.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Or{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Ke||!Mt||Ke===this.computed)return;let o=this.activeLink;if(o===void 0||o.sub!==Ke)o=this.activeLink=new Fd(Ke,this),Ke.deps?(o.prevDep=Ke.depsTail,Ke.depsTail.nextDep=o,Ke.depsTail=o):Ke.deps=Ke.depsTail=o,ol(o);else if(o.version===-1&&(o.version=this.version,o.nextDep)){const n=o.nextDep;n.prevDep=o.prevDep,o.prevDep&&(o.prevDep.nextDep=n),o.prevDep=Ke.depsTail,o.nextDep=void 0,Ke.depsTail.nextDep=o,Ke.depsTail=o,Ke.deps===o&&(Ke.deps=n)}return o}trigger(t){this.version++,Pn++,this.notify(t)}notify(t){Sr();try{for(let o=this.subs;o;o=o.prevSub)o.sub.notify()&&o.sub.dep.notify()}finally{Lr()}}}function ol(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)ol(n)}const o=e.dep.subs;o!==e&&(e.prevSub=o,o&&(o.nextSub=e)),e.dep.subs=e}}const ba=new WeakMap,zo=Symbol(""),Mr=Symbol(""),Sn=Symbol("");function dt(e,t,o){if(Mt&&Ke){let n=ba.get(e);n||ba.set(e,n=new Map);let a=n.get(o);a||(n.set(o,a=new Or),a.map=n,a.key=o),a.track()}}function oo(e,t,o,n,a,r){const s=ba.get(e);if(!s){Pn++;return}const i=l=>{l&&l.trigger()};if(Sr(),t==="clear")s.forEach(i);else{const l=ke(e),c=l&&da(o);if(l&&o==="length"){const u=Number(n);s.forEach((p,d)=>{(d==="length"||d===Sn||!Ct(d)&&d>=u)&&i(p)})}else switch((o!==void 0||s.has(void 0))&&i(s.get(o)),c&&i(s.get(Sn)),t){case"add":l?c&&i(s.get("length")):(i(s.get(zo)),vo(e)&&i(s.get(Mr)));break;case"delete":l||(i(s.get(zo)),vo(e)&&i(s.get(Mr)));break;case"set":vo(e)&&i(s.get(zo));break}}Lr()}function Ed(e,t){const o=ba.get(e);return o&&o.get(t)}function Qo(e){const t=Fe(e);return t===e?t:(dt(t,"iterate",Sn),At(e)?t:t.map(Ft))}function $a(e){return dt(e=Fe(e),"iterate",Sn),e}function Wt(e,t){return ro(e)?tn(ao(e)?Ft(t):t):Ft(t)}const Id={__proto__:null,[Symbol.iterator](){return Fr(this,Symbol.iterator,e=>Wt(this,e))},concat(...e){return Qo(this).concat(...e.map(t=>ke(t)?Qo(t):t))},entries(){return Fr(this,"entries",e=>(e[1]=Wt(this,e[1]),e))},every(e,t){return no(this,"every",e,t,void 0,arguments)},filter(e,t){return no(this,"filter",e,t,o=>o.map(n=>Wt(this,n)),arguments)},find(e,t){return no(this,"find",e,t,o=>Wt(this,o),arguments)},findIndex(e,t){return no(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return no(this,"findLast",e,t,o=>Wt(this,o),arguments)},findLastIndex(e,t){return no(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return no(this,"forEach",e,t,void 0,arguments)},includes(...e){return Er(this,"includes",e)},indexOf(...e){return Er(this,"indexOf",e)},join(e){return Qo(this).join(e)},lastIndexOf(...e){return Er(this,"lastIndexOf",e)},map(e,t){return no(this,"map",e,t,void 0,arguments)},pop(){return Ln(this,"pop")},push(...e){return Ln(this,"push",e)},reduce(e,...t){return nl(this,"reduce",e,t)},reduceRight(e,...t){return nl(this,"reduceRight",e,t)},shift(){return Ln(this,"shift")},some(e,t){return no(this,"some",e,t,void 0,arguments)},splice(...e){return Ln(this,"splice",e)},toReversed(){return Qo(this).toReversed()},toSorted(e){return Qo(this).toSorted(e)},toSpliced(...e){return Qo(this).toSpliced(...e)},unshift(...e){return Ln(this,"unshift",e)},values(){return Fr(this,"values",e=>Wt(this,e))}};function Fr(e,t,o){const n=$a(e),a=n[t]();return n!==e&&!At(e)&&(a._next=a.next,a.next=()=>{const r=a._next();return r.done||(r.value=o(r.value)),r}),a}const Rd=Array.prototype;function no(e,t,o,n,a,r){const s=$a(e),i=s!==e&&!At(e),l=s[t];if(l!==Rd[t]){const p=l.apply(e,r);return i?Ft(p):p}let c=o;s!==e&&(i?c=function(p,d){return o.call(this,Wt(e,p),d,e)}:o.length>2&&(c=function(p,d){return o.call(this,p,d,e)}));const u=l.call(s,c,n);return i&&a?a(u):u}function nl(e,t,o,n){const a=$a(e),r=a!==e&&!At(e);let s=o,i=!1;a!==e&&(r?(i=n.length===0,s=function(c,u,p){return i&&(i=!1,c=Wt(e,c)),o.call(this,c,Wt(e,u),p,e)}):o.length>3&&(s=function(c,u,p){return o.call(this,c,u,p,e)}));const l=a[t](s,...n);return i?Wt(e,l):l}function Er(e,t,o){const n=Fe(e);dt(n,"iterate",Sn);const a=n[t](...o);return(a===-1||a===!1)&&xa(o[0])?(o[0]=Fe(o[0]),n[t](...o)):a}function Ln(e,t,o=[]){Ht(),Sr();const n=Fe(e)[t].apply(e,o);return Lr(),Ut(),n}const Dd=wr("__proto__,__v_isRef,__isVue"),al=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Ct));function Bd(e){Ct(e)||(e=String(e));const t=Fe(this);return dt(t,"has",e),t.hasOwnProperty(e)}class rl{constructor(t=!1,o=!1){this._isReadonly=t,this._isShallow=o}get(t,o,n){if(o==="__v_skip")return t.__v_skip;const a=this._isReadonly,r=this._isShallow;if(o==="__v_isReactive")return!a;if(o==="__v_isReadonly")return a;if(o==="__v_isShallow")return r;if(o==="__v_raw")return n===(a?r?pl:ul:r?cl:ll).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const s=ke(t);if(!a){let l;if(s&&(l=Id[o]))return l;if(o==="hasOwnProperty")return Bd}const i=Reflect.get(t,o,Ze(t)?t:n);if((Ct(o)?al.has(o):Dd(o))||(a||dt(t,"get",o),r))return i;if(Ze(i)){const l=s&&da(o)?i:i.value;return a&&Be(l)?Rr(l):l}return Be(i)?a?Rr(i):en(i):i}}class sl extends rl{constructor(t=!1){super(!1,t)}set(t,o,n,a){let r=t[o];const s=ke(t)&&da(o);if(!this._isShallow){const c=ro(r);if(!At(n)&&!ro(n)&&(r=Fe(r),n=Fe(n)),!s&&Ze(r)&&!Ze(n))return c||(r.value=n),!0}const i=s?Number(o)e,ya=e=>Reflect.getPrototypeOf(e);function Ud(e,t,o){return function(...n){const a=this.__v_raw,r=Fe(a),s=vo(r),i=e==="entries"||e===Symbol.iterator&&s,l=e==="keys"&&s,c=a[e](...n),u=o?Ir:t?tn:Ft;return!t&&dt(r,"iterate",l?Mr:zo),et(Object.create(c),{next(){const{value:p,done:d}=c.next();return d?{value:p,done:d}:{value:i?[u(p[0]),u(p[1])]:u(p),done:d}}})}}function va(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Wd(e,t){const o={get(a){const r=this.__v_raw,s=Fe(r),i=Fe(a);e||(jt(a,i)&&dt(s,"get",a),dt(s,"get",i));const{has:l}=ya(s),c=t?Ir:e?tn:Ft;if(l.call(s,a))return c(r.get(a));if(l.call(s,i))return c(r.get(i));r!==s&&r.get(a)},get size(){const a=this.__v_raw;return!e&&dt(Fe(a),"iterate",zo),a.size},has(a){const r=this.__v_raw,s=Fe(r),i=Fe(a);return e||(jt(a,i)&&dt(s,"has",a),dt(s,"has",i)),a===i?r.has(a):r.has(a)||r.has(i)},forEach(a,r){const s=this,i=s.__v_raw,l=Fe(i),c=t?Ir:e?tn:Ft;return!e&&dt(l,"iterate",zo),i.forEach((u,p)=>a.call(r,c(u),c(p),s))}};return et(o,e?{add:va("add"),set:va("set"),delete:va("delete"),clear:va("clear")}:{add(a){const r=Fe(this),s=ya(r),i=Fe(a),l=!t&&!At(a)&&!ro(a)?i:a;return s.has.call(r,l)||jt(a,l)&&s.has.call(r,a)||jt(i,l)&&s.has.call(r,i)||(r.add(l),oo(r,"add",l,l)),this},set(a,r){!t&&!At(r)&&!ro(r)&&(r=Fe(r));const s=Fe(this),{has:i,get:l}=ya(s);let c=i.call(s,a);c||(a=Fe(a),c=i.call(s,a));const u=l.call(s,a);return s.set(a,r),c?jt(r,u)&&oo(s,"set",a,r):oo(s,"add",a,r),this},delete(a){const r=Fe(this),{has:s,get:i}=ya(r);let l=s.call(r,a);l||(a=Fe(a),l=s.call(r,a)),i&&i.call(r,a);const c=r.delete(a);return l&&oo(r,"delete",a,void 0),c},clear(){const a=Fe(this),r=a.size!==0,s=a.clear();return r&&oo(a,"clear",void 0,void 0),s}}),["keys","values","entries",Symbol.iterator].forEach(a=>{o[a]=Ud(a,e,t)}),o}function wa(e,t){const o=Wd(e,t);return(n,a,r)=>a==="__v_isReactive"?!e:a==="__v_isReadonly"?e:a==="__v_raw"?n:Reflect.get(De(o,a)&&a in n?o:n,a,r)}const Kd={get:wa(!1,!1)},Vd={get:wa(!1,!0)},qd={get:wa(!0,!1)},Yd={get:wa(!0,!0)},ll=new WeakMap,cl=new WeakMap,ul=new WeakMap,pl=new WeakMap;function Zd(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function en(e){return ro(e)?e:ka(e,!1,zd,Kd,ll)}function Jd(e){return ka(e,!1,jd,Vd,cl)}function Rr(e){return ka(e,!0,Gd,qd,ul)}function Nw(e){return ka(e,!0,Hd,Yd,pl)}function ka(e,t,o,n,a){if(!Be(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const r=a.get(e);if(r)return r;const s=Zd(wd(e));if(s===0)return e;const i=new Proxy(e,s===2?n:o);return a.set(e,i),i}function ao(e){return ro(e)?ao(e.__v_raw):!!(e&&e.__v_isReactive)}function ro(e){return!!(e&&e.__v_isReadonly)}function At(e){return!!(e&&e.__v_isShallow)}function xa(e){return e?!!e.__v_raw:!1}function Fe(e){const t=e&&e.__v_raw;return t?Fe(t):e}function wo(e){return!De(e,"__v_skip")&&Object.isExtensible(e)&&Bi(e,"__v_skip",!0),e}const Ft=e=>Be(e)?en(e):e,tn=e=>Be(e)?Rr(e):e;function Ze(e){return e?e.__v_isRef===!0:!1}function ie(e){return dl(e,!1)}function ko(e){return dl(e,!0)}function dl(e,t){return Ze(e)?e:new Xd(e,t)}class Xd{constructor(t,o){this.dep=new Or,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=o?t:Fe(t),this._value=o?t:Ft(t),this.__v_isShallow=o}get value(){return this.dep.track(),this._value}set value(t){const o=this._rawValue,n=this.__v_isShallow||At(t)||ro(t);t=n?t:Fe(t),jt(t,o)&&(this._rawValue=t,this._value=n?t:Ft(t),this.dep.trigger())}}function H(e){return Ze(e)?e.value:e}const Qd={get:(e,t,o)=>t==="__v_raw"?e:H(Reflect.get(e,t,o)),set:(e,t,o,n)=>{const a=e[t];return Ze(a)&&!Ze(o)?(a.value=o,!0):Reflect.set(e,t,o,n)}};function gl(e){return ao(e)?e:new Proxy(e,Qd)}function eg(e){const t=ke(e)?new Array(e.length):{};for(const o in e)t[o]=ml(e,o);return t}class tg{constructor(t,o,n){this._object=t,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0,this._key=Ct(o)?o:String(o),this._raw=Fe(t);let a=!0,r=t;if(!ke(t)||Ct(this._key)||!da(this._key))do a=!xa(r)||At(r);while(a&&(r=r.__v_raw));this._shallow=a}get value(){let t=this._object[this._key];return this._shallow&&(t=H(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&Ze(this._raw[this._key])){const o=this._object[this._key];if(Ze(o)){o.value=t;return}}this._object[this._key]=t}get dep(){return Ed(this._raw,this._key)}}class og{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function hl(e,t,o){return Ze(e)?e:Ae(e)?new og(e):Be(e)&&arguments.length>1?ml(e,t,o):ie(e)}function ml(e,t,o){return new tg(e,t,o)}class ng{constructor(t,o,n){this.fn=t,this.setter=o,this._value=void 0,this.dep=new Or(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Pn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!o,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&Ke!==this)return Zi(this,!0),!0}get value(){const t=this.dep.track();return Qi(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function ag(e,t,o=!1){let n,a;return Ae(e)?n=e:(n=e.get,a=e.set),new ng(n,a,o)}const Ca={},Aa=new WeakMap;let Go;function rg(e,t=!1,o=Go){if(o){let n=Aa.get(o);n||Aa.set(o,n=[]),n.push(e)}}function sg(e,t,o=je){const{immediate:n,deep:a,once:r,scheduler:s,augmentJob:i,call:l}=o,c=k=>a?k:At(k)||a===!1||a===0?so(k,1):so(k);let u,p,d,w,y=!1,h=!1;if(Ze(e)?(p=()=>e.value,y=At(e)):ao(e)?(p=()=>c(e),y=!0):ke(e)?(h=!0,y=e.some(k=>ao(k)||At(k)),p=()=>e.map(k=>{if(Ze(k))return k.value;if(ao(k))return c(k);if(Ae(k))return l?l(k,2):k()})):Ae(e)?t?p=l?()=>l(e,2):e:p=()=>{if(d){Ht();try{d()}finally{Ut()}}const k=Go;Go=u;try{return l?l(e,3,[w]):e(w)}finally{Go=k}}:p=Gt,t&&a){const k=p,C=a===!0?1/0:a;p=()=>so(k(),C)}const g=Vi(),b=()=>{u.stop(),g&&g.active&&kr(g.effects,u)};if(r&&t){const k=t;t=(...C)=>{const S=k(...C);return b(),S}}let x=h?new Array(e.length).fill(Ca):Ca;const $=k=>{if(!(!(u.flags&1)||!u.dirty&&!k))if(t){const C=u.run();if(k||a||y||(h?C.some((S,L)=>jt(S,x[L])):jt(C,x))){d&&d();const S=Go;Go=u;try{const L=[C,x===Ca?void 0:h&&x[0]===Ca?[]:x,w];x=C,l?l(t,3,L):t(...L)}finally{Go=S}}}else u.run()};return i&&i($),u=new qi(p),u.scheduler=s?()=>s($,!1):$,w=k=>rg(k,!1,u),d=u.onStop=()=>{const k=Aa.get(u);if(k){if(l)l(k,4);else for(const C of k)C();Aa.delete(u)}},t?n?$(!0):x=u.run():s?s($.bind(null,!0),!0):u.run(),b.pause=u.pause.bind(u),b.resume=u.resume.bind(u),b.stop=b,b}function so(e,t=1/0,o){if(t<=0||!Be(e)||e.__v_skip||(o=o||new Map,(o.get(e)||0)>=t))return e;if(o.set(e,t),t--,Ze(e))so(e.value,t,o);else if(ke(e))for(let n=0;n{so(n,t,o)});else if(pa(e)){for(const n in e)so(e[n],t,o);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&so(e[n],t,o)}return e}/**
* @vue/runtime-core v3.5.42
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
-**/const Tn=[];let Dr=!1;function Ow(e,...t){if(Dr)return;Dr=!0,Ht();const o=Tn.length?Tn[Tn.length-1].component:null,n=o&&o.appContext.config.warnHandler,a=ig();if(n)on(n,o,11,[e+t.map(r=>{var s,i;return(i=(s=r.toString)==null?void 0:s.call(r))!=null?i:JSON.stringify(r)}).join(""),o&&o.proxy,a.map(({vnode:r})=>`at <${hc(o,r.type)}>`).join(`
+**/const Tn=[];let Dr=!1;function Ow(e,...t){if(Dr)return;Dr=!0,Ht();const o=Tn.length?Tn[Tn.length-1].component:null,n=o&&o.appContext.config.warnHandler,a=ig();if(n)on(n,o,11,[e+t.map(r=>{var s,i;return(i=(s=r.toString)==null?void 0:s.call(r))!=null?i:JSON.stringify(r)}).join(""),o&&o.proxy,a.map(({vnode:r})=>`at <${mc(o,r.type)}>`).join(`
`),a]);else{const r=[`[Vue warn]: ${e}`,...t];a.length&&r.push(`
`,...lg(a)),console.warn(...r)}Ut(),Dr=!1}function ig(){let e=Tn[Tn.length-1];if(!e)return[];const t=[];for(;e;){const o=t[0];o&&o.vnode===e?o.recurseCount++:t.push({vnode:e,recurseCount:0});const n=e.component&&e.component.parent;e=n&&n.vnode}return t}function lg(e){const t=[];return e.forEach((o,n)=>{t.push(...n===0?[]:[`
-`],...cg(o))}),t}function cg({vnode:e,recurseCount:t}){const o=t>0?`... (${t} recursive calls)`:"",n=e.component?e.component.parent==null:!1,a=` at <${hc(e.component,e.type,n)}`,r=">"+o;return e.props?[a,...ug(e.props),r]:[a+r]}function ug(e){const t=[],o=Object.keys(e);return o.slice(0,3).forEach(n=>{t.push(...fl(n,e[n]))}),o.length>3&&t.push(" ..."),t}function fl(e,t,o){return We(t)?(t=JSON.stringify(t),o?t:[`${e}=${t}`]):typeof t=="number"||typeof t=="boolean"||t==null?o?t:[`${e}=${t}`]:Ze(t)?(t=fl(e,Fe(t.value),!0),o?t:[`${e}=Ref<`,t,">"]):Ae(t)?[`${e}=fn${t.name?`<${t.name}>`:""}`]:(t=Fe(t),o?t:[`${e}=`,t])}function on(e,t,o,n){try{return n?e(...n):e()}catch(a){Nn(a,t,o)}}function Et(e,t,o,n){if(Ae(e)){const a=on(e,t,o,n);return a&&Ri(a)&&a.catch(r=>{Nn(r,t,o)}),a}if(ke(e)){const a=[];for(let r=0;r>>1,a=ft[n],r=On(a);r=On(o)?ft.push(e):ft.splice(dg(t),0,e),e.flags|=1,bl()}}function bl(){Pa||(Pa=_l.then(vl))}function gg(e){if(!ke(e))xo&&e.id===-1?xo.splice(an+1,0,e):e.flags&1||(nn.push(e),e.flags|=1);else for(let t=0;tOn(o)-On(n));if(nn.length=0,xo){for(let o=0;oe.id==null?e.flags&2?-1:1/0:e.id;function vl(e){try{for(Kt=0;Kt{n._d&&Da(-1);const r=Sa(t),s=lo.length;let i;try{i=e(...a)}finally{for(let l=lo.length;l>s;l--)ns();Sa(r),n._d&&Da(1)}return i};return n._n=!0,n._c=!0,n._d=!0,n}function zr(e,t){if(lt===null)return e;const o=ja(lt),n=e.dirs||(e.dirs=[]);for(let a=0;a1)return o&&Ae(t)?t.call(n&&n.proxy):t}}function mg(){return!!(Xt()||Uo)}const hg=Symbol.for("v-scx"),fg=()=>Pe(hg);function kl(e,t){return Gr(e,null,t)}function Ue(e,t,o){return Gr(e,t,o)}function Gr(e,t,o=je){const{immediate:n,deep:a,flush:r,once:s}=o,i=et({},o),l=t&&n||!t&&r!=="post";let c;if(ln){if(r==="sync"){const w=fg();c=w.__watcherHandles||(w.__watcherHandles=[])}else if(!l){const w=()=>{};return w.stop=Gt,w.resume=Gt,w.pause=Gt,w}}const u=ct;i.call=(w,y,m)=>Et(w,u,y,m);let p=!1;r==="post"?i.scheduler=w=>{bt(w,u&&u.suspense)}:r!=="sync"&&(p=!0,i.scheduler=(w,y)=>{y?w():Br(w)}),i.augmentJob=w=>{t&&(w.flags|=4),p&&(w.flags|=2,u&&(w.id=u.uid,w.i=u))};const d=sg(e,t,i);return ln&&(c?c.push(d):l&&d()),d}function _g(e,t,o){const n=this.proxy,a=We(e)?e.includes(".")?xl(n,e):()=>n[e]:e.bind(n,n);let r;Ae(t)?r=t:(r=t.handler,o=t);const s=Hn(this),i=Gr(a,r.bind(n),o);return s(),i}function xl(e,t){const o=t.split(".");return()=>{let n=e;for(let a=0;ae.__isTeleport,Ho=e=>e&&(e.disabled||e.disabled===""),bg=e=>e&&(e.defer||e.defer===""),Al=e=>typeof SVGElement<"u"&&e instanceof SVGElement,Pl=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,jr=(e,t)=>{const o=e&&e.to;return We(o)?t?t(o):null:o},$g={name:"Teleport",__isTeleport:!0,process(e,t,o,n,a,r,s,i,l,c){const{mc:u,pc:p,pbc:d,o:{insert:w,querySelector:y,createText:m,createComment:g,parentNode:b}}=c,x=Ho(t.props);let{dynamicChildren:$}=t;const k=(L,F,N)=>{L.shapeFlag&16&&u(L.children,F,N,a,r,s,i,l)},C=(L=t)=>{const F=Ho(L.props),N=L.target=jr(L.props,y),I=Hr(N,L,m,w);N&&(s!=="svg"&&Al(N)?s="svg":s!=="mathml"&&Pl(N)&&(s="mathml"),a&&a.isCE&&(a.ce._teleportTargets||(a.ce._teleportTargets=new Set)).add(N),F||(k(L,N,I),Mn(L,!1)))},S=L=>{const F=()=>{if(Co.get(L)===F){if(Co.delete(L),Ho(L.props)){const N=b(L.el)||o;k(L,N,L.anchor),Mn(L,!0)}C(L)}};Co.set(L,F),bt(F,r)};if(e==null){const L=t.el=m(""),F=t.anchor=m("");if(w(L,o,n),w(F,o,n),bg(t.props)||r&&r.pendingBranch){S(t);return}x&&(k(t,o,F),Mn(t,!0)),C()}else{t.el=e.el;const L=t.anchor=e.anchor,F=Co.get(e);if(F){F.flags|=8,Co.delete(e),S(t);return}t.targetStart=e.targetStart;const N=t.target=e.target,I=t.targetAnchor=e.targetAnchor,j=Ho(e.props),M=j?o:N,q=j?L:I;if(s==="svg"||Al(N)?s="svg":(s==="mathml"||Pl(N))&&(s="mathml"),$?(d(e.dynamicChildren,$,M,a,r,s,i),ts(e,t,!0)):l||p(e,t,M,q,a,r,s,i,!1),x)j?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Ta(t,o,L,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const ge=jr(t.props,y);ge&&(t.target=ge,Ta(t,ge,null,c,0))}else j&&Ta(t,N,I,c,1);Mn(t,x)}},remove(e,t,o,{um:n,o:{remove:a}},r){const{shapeFlag:s,children:i,anchor:l,targetStart:c,targetAnchor:u,target:p,props:d}=e,w=Ho(d),y=r||!w,m=Co.get(e);if(m&&(m.flags|=8,Co.delete(e)),p&&(a(c),a(u)),r&&a(l),!m&&(w||p)&&s&16)for(let g=0;g1){for(const o of e)if(o.type!==Yt){t=o;break}}return t}function Sl(e){if(!Ma(e))return La(e.type)&&e.children?wg(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:o}=e;if(o){if(t&16)return o[0];if(t&32&&Ae(o.default))return o.default()}}function Wr(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;const o=e.component.subTree;Wr(La(o.type)&&Sl(o)||o,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Ao(e,t){return Ae(e)?et({name:e.name},t,{setup:e}):e}function Kr(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function Ll(e,t){let o;return!!((o=Object.getOwnPropertyDescriptor(e,t))&&!o.configurable)}const Na=new WeakMap;function Fn(e,t,o,n,a=!1){if(ke(e)){e.forEach((m,g)=>Fn(m,t&&(ke(t)?t[g]:t),o,n,a));return}if(rn(n)&&!a){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&Fn(e,t,o,n.component.subTree);return}const r=n.shapeFlag&4?ja(n.component):n.el,s=a?null:r,{i,r:l}=e,c=t&&t.r,u=i.refs===je?i.refs={}:i.refs,p=i.setupState,d=Fe(p),w=p===je?Ei:m=>Ll(u,m)?!1:De(d,m),y=(m,g)=>!(g&&Ll(u,g));if(c!=null&&c!==l){if(Tl(t),We(c))u[c]=null,w(c)&&(p[c]=null);else if(Ze(c)){const m=t;y(c,m.k)&&(c.value=null),m.k&&(u[m.k]=null)}}if(Ae(l))on(l,i,12,[s,u]);else{const m=We(l),g=Ze(l);if(m||g){const b=()=>{if(e.f){const x=m?w(l)?p[l]:u[l]:y()||!e.k?l.value:u[e.k];if(a)ke(x)&&kr(x,r);else if(ke(x))x.includes(r)||x.push(r);else if(m)u[l]=[r],w(l)&&(p[l]=u[l]);else{const $=[r];y(l,e.k)&&(l.value=$),e.k&&(u[e.k]=$)}}else m?(u[l]=s,w(l)&&(p[l]=s)):g&&(y(l,e.k)&&(l.value=s),e.k&&(u[e.k]=s))};if(s){const x=()=>{b(),Na.delete(e)};x.id=-1,Na.set(e,x),bt(x,o)}else Tl(e),b()}}}function Tl(e){const t=Na.get(e);t&&(t.flags|=8,Na.delete(e))}const Nl=e=>e.nodeType===8;ha().requestIdleCallback,ha().cancelIdleCallback;function kg(e,t){if(Nl(e)&&e.data==="["){let o=1,n=e.nextSibling;for(;n;){if(n.nodeType===1){if(t(n)===!1)break}else if(Nl(n))if(n.data==="]"){if(--o===0)break}else n.data==="["&&o++;n=n.nextSibling}}else t(e)}const rn=e=>!!e.type.__asyncLoader;function xg(e){Ae(e)&&(e={loader:e});const{loader:t,loadingComponent:o,errorComponent:n,delay:a=200,hydrate:r,timeout:s,suspensible:i=!0,onError:l}=e;let c=null,u,p=0;const d=()=>(p++,c=null,w()),w=()=>{let y;return c||(y=c=t().catch(m=>{if(m=m instanceof Error?m:new Error(String(m)),l)return new Promise((g,b)=>{l(m,()=>g(d()),()=>b(m),p+1)});throw m}).then(m=>y!==c&&c?c:(m&&(m.__esModule||m[Symbol.toStringTag]==="Module")&&(m=m.default),u=m,m)))};return Ao({name:"AsyncComponentWrapper",__asyncLoader:w,__asyncHydrate(y,m,g){const b=y.isConnected;let x=!1;(m.bu||(m.bu=[])).push(()=>x=!0);const $=()=>{x||!y.parentNode||b&&!y.isConnected||g()},k=r?()=>{const C=r($,S=>kg(y,S));C&&(m.bum||(m.bum=[])).push(C)}:$;u?k():w().then(()=>!m.isUnmounted&&k())},get __asyncResolved(){return u},setup(){const y=ct;if(Kr(y),u)return()=>Oa(u,y);const m=C=>{c=null,Nn(C,y,13,!n)};if(i&&y.suspense||ln)return w().then(C=>()=>Oa(C,y)).catch(C=>(m(C),()=>n?Ie(n,{error:C}):null));const g=ie(!1),b=ie(),x=ie(!!a);let $,k;return Ea(()=>{$!=null&&clearTimeout($),k!=null&&clearTimeout(k)}),a&&(k=setTimeout(()=>{y.isUnmounted||(x.value=!1)},a)),s!=null&&($=setTimeout(()=>{if(!y.isUnmounted&&!g.value&&!b.value){const C=new Error(`Async component timed out after ${s}ms.`);m(C),b.value=C}},s)),w().then(()=>{y.isUnmounted||(g.value=!0,y.parent&&Ma(y.parent.vnode)&&y.parent.update())}).catch(C=>{if(y.isUnmounted){c=null;return}m(C),b.value=C}),()=>{if(g.value&&u)return Oa(u,y);if(b.value&&n)return Ie(n,{error:b.value});if(o&&!x.value)return Oa(o,y)}}})}function Oa(e,t){const{ref:o,props:n,children:a,ce:r}=t.vnode,s=Ie(e,n,a);return s.ref=o,s.ce=r,delete t.vnode.ce,s}const Ma=e=>e.type.__isKeepAlive;function Cg(e,t){Ol(e,"a",t)}function Ag(e,t){Ol(e,"da",t)}function Ol(e,t,o=ct){const n=e.__wdc||(e.__wdc=()=>{let a=o;for(;a;){if(a.isDeactivated)return;a=a.parent}return e()});if(Fa(t,n,o),o){let a=o.parent;for(;a&&a.parent;)Ma(a.parent.vnode)&&Pg(n,t,o,a),a=a.parent}}function Pg(e,t,o,n){const a=Fa(t,e,n,!0);Ea(()=>{kr(n[t],a)},o)}function Fa(e,t,o=ct,n=!1){if(o){const a=o[e]||(o[e]=[]),r=t.__weh||(t.__weh=(...s)=>{Ht();const i=Hn(o),l=Et(t,o,e,s);return i(),Ut(),l});return n?a.unshift(r):a.push(r),r}}const io=e=>(t,o=ct)=>{(!ln||e==="sp")&&Fa(e,(...n)=>t(...n),o)},Ml=io("bm"),qt=io("m"),Sg=io("bu"),Lg=io("u"),Rt=io("bum"),Ea=io("um"),Tg=io("sp"),Ng=io("rtg"),Og=io("rtc");function Mg(e,t=ct){Fa("ec",e,t)}const Fg="components",Fl=Symbol.for("v-ndc");function El(e){return We(e)?Eg(Fg,e,!1)||e:e||Fl}function Eg(e,t,o=!0,n=!1){const a=lt||ct;if(a){const r=a.type;{const i=mc(r,!1);if(i&&(i===t||i===st(t)||i===ma(st(t))))return r}const s=Il(a[e]||r[e],t)||Il(a.appContext[e],t);return!s&&n?r:s}}function Il(e,t){return e&&(e[t]||e[st(t)]||e[ma(st(t))])}function Xe(e,t,o,n){let a;const r=o,s=ke(e);if(s||We(e)){const i=s&&ao(e);let l=!1,c=!1;i&&(l=!At(e),c=ro(e),e=$a(e)),a=new Array(e.length);for(let u=0,p=e.length;u