Commit graph

43 commits

Author SHA1 Message Date
Doxa Maps
2b342f8cbc Pane elements opt out of the viewport floor: --profile-min-h:0 on the pane mount
The root of the paint-over and of the keyboard regression rega's A/B found.
ProfileLoader's wrapper is min-height: var(--profile-min-h, 100dvh), and a
host that already sizes the element sets --profile-min-h:0 (the Gallery's
.dms-pane, VerticalTabs, plugin-studio). The pane column sizes each element
(inset:0 in its pane) but never set it, so on the standalone leaf each
pane's wrapper was 100dvh = 900 px inside an 843 px pane: the wall's
scroller ran 57 px past its pane, its first row's tapes painted over the
map, and Tab into those tapes scrolled the pane COLUMN (and, with the
clip, onto an invisible control). The Gallery was pane-sized only because
its own host set the variable. The clip stays as belt and braces.
2026-09-29 14:09:23 +00:00
Doxa Maps
1e91067527 Clip the pane with overflow:clip, hidden only as the fallback
overflow:hidden alone leaves each pane a scroll container: focus() or
scrollIntoView() on content in the overflowing first card row (a Tab
into the tapes) would scroll the PANE and shift the wall inside it.
overflow:clip clips paint without making a scroll container, and like
hidden makes no containing block for position:fixed, so the modal scrims
still cover the viewport. hidden stays first as the fallback for engines
without clip (dx-shepherd's review).
2026-09-29 13:28:48 +00:00
Doxa Maps
561b86f217 A pane never paints outside its own box
The UPG-100 wall's first card row runs past the list pane's bottom into
the map area on every surface (rega's census on ca07fc3c8: cards to y 142
against a pane ending at 57; 14 wall elements inside the map area at
1440, 6 at 390, leaf and Gallery, both engines). On the standalone leaf
the tapes' z-index painted that overflow OVER the map pane: the 'PRINT
OPTIONS' fieldsets across the top of the map, the reported defect. The
Gallery hid it only by a stacking difference.

.pane gets overflow:hidden, so content outside a pane is clipped at the
pane, whatever the host's stacking contexts. Not contain or isolation:
either would make each pane the containing block or stacking context for
position:fixed modal scrims and trap them under a host's nav rail, the
regression VerticalTabsLayout records. The map roots are inset:0, so the
map and its attribution are pane-sized and unaffected.
2026-09-29 13:21:56 +00:00
Doxa Maps
34fd21dd5d The last library reference drops the speaker, and the baseline shrinks to what is left
library/layouts/app-wrapper-material/index.html carried the one library
reference the sweep's file filter missed (it looked at scripts, components
and docs, not .html): reworded the same way, comment only.

The reference baseline then shrinks from 505 to 147 lines, so nothing
reworded in this landing can come back. What remains is in the owners'
plugins, documentation and skills.
2026-09-29 05:26:26 +00:00
Doxa Maps
bfc99ff827 library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver
Every reference to the Driver in library/ is reworded: the engineering reason
stays, the speaker and the quote go, and a decision still open reads
"provisional, pending a product decision". Comments only: each changed file
is identical after stripping comments (JavaScript, template and CSS, including
CSS comments inside the style strings shadow-DOM components inject).

Those style-string comments do ship, so several bundles change by exactly
those comment bytes.
2026-09-29 05:26:25 +00:00
Doxa Maps
36f2818b37 Every animating library component now honours prefers-reduced-motion itself
17 of 25 library components that ship motion carried no reduced-motion rule, so
they animated regardless of the reader's OS setting.

The reason it cannot be fixed once, centrally: a @media (prefers-reduced-motion)
rule in the host document does not cross a shadow boundary, and these render
inside one. Measured while surveying: NO generated leaf page ships such a rule
at all — 0 occurrences across the whole built tree — so there was not even a
page rule to inherit. Each component owns its own.

CSS-ONLY AND ADDITIVE. Nothing but new @media blocks; no existing rule changed.
Verified by diff: 0 real removals across 17 files. The two lines git reports as
removed are a trailing comma moving as a new string was chained onto an existing
concatenation, and both original rules are still present — checked by grepping
for them, not by reading the diff.

WHERE MOTION CARRIES MEANING, it is kept rather than hidden:
  · SemanticTreeLegend's spinner MEANS "loading". Stopped dead it is a static
    ring that says nothing, so it is slowed to a step crawl (2.4s, steps(8)) —
    visibly working, no smooth spin.
  · LegendProgressBar's only motion is a hover filter, so it simply stops; the
    bar's FILL is geometry, not animation, and still reports its value.

The rule goes where each component's styles actually go, and that varies more
than expected: 13 append into a useShadowStyles template literal, 3 into
string-concatenated sheets, and VerticalTabsLayout into its VTL_SHADOW_CSS
constant AND its duplicate <style> block, because its own header says the two
copies must stay identical.

check:reduced-motion asserts both halves, each mutation-tested to fail before I
trusted a pass: strip the rule from an animating component and it fails; move a
rule from the injected sheet into a <style> block and it fails too, because on a
useShadowStyles component that rule would never reach the shadow root. Both
mutated files restored byte-identical.

Two of my own errors caught by tooling rather than by review, and worth naming:
my first patch asserted useShadowStyles existed in every target and RAISED on
VerticalTabsLayout instead of silently skipping it — the assert-the-anchor habit
from the last registration failure working as intended. Then I appended CSS
after the label argument on two concat-built sheets, corrupting the injection
key; check:shadow-css-parses named both files and the fix was to insert before
the label. A gate I did not write caught what I got wrong.

Registered in all three places and verified by re-reading the parsed
package.json.
2026-09-29 01:38:07 +00:00
Doxa Maps
813a52ebbe One page loaded the same bundle twice and nothing said so
Every plugin ships as a self-contained IIFE, so library/utils/embedScript.js is
compiled separately into each one: eleven bundles on main each carry their own
copy. Its cache was a module-scoped const, which made "load exactly once" true
per BUNDLE rather than per page — the opposite of what the file's own header
claimed. Two shells nesting the same child (the Research Gallery and the Plugin
Studio both mounting doxa-research-map) each injected it.

Nothing failed, which is why it survived. The second request came from the HTTP
cache, so no failed request and no extra network row; the bundle then executed in
full a second time — a second Vue, a second vue-i18n, a second pinia, ~375 KiB
gzip of work — and its guarded customElements.define quietly declined at the very
end. Where the two URLs are different versions of one plugin, whichever executed
first owns the tag and the other is discarded in silence.

The cache now lives on globalThis, the only scope that matches the claim, under a
versioned key so a page mixing old and new bundles falls back to a cache each
rather than misreading a shape it does not expect. Entries are keyed by the
resolved href, because the callers do not pass the same string for one file.
A <script> already in the document is adopted, which is what covers bundles built
before this and host pages that write their own tag; that resolves immediately,
and it is safe only because every caller here follows with a wait on the element
itself — the note in the file says so, since a new caller must keep that pairing.

claimTag is a diagnostic, not a guard: by the time a second bundle arrives the
define is long since won, so the thing that was missing was anyone saying so.

The check imports the module twice under different URLs — two module instances
sharing one globalThis, the same shape as two IIFEs on one page. It asserts the
second caller receives the FIRST one's promise, not merely that one script tag
exists: mutation-testing showed the tag count alone is satisfied by the adoption
path, so it would have passed with the original bug restored.
2026-09-29 00:06:51 +00:00
Doxa Maps
1bb1ef03b6 Show a lost WebGL context instead of a blank pane
The browser's context budget is commonly 8 to 16 and this tree has nineteen
map-bearing profiles, so a reader who visits enough map tabs crosses it. What
the browser does then is the real problem: it kills the OLDEST context silently.
getContext never returns null, nothing fires on the new canvas, and a map that
worked a minute ago is simply blank when you go back to it. The reader who
notices is the one returning to something that used to work.

The pane now says so and offers a reload. That holds under either option on the
Driver's row 107, because it makes the failure visible rather than preventing it.

The listener goes on the canvas itself, not the pane. The map's canvas lives in
the plugin's shadow root and webglcontextlost does not bubble, so a listener on
the pane would never hear it — hence a walk that descends through shadow roots.
It binds after mount and again on every re-activation, because the canvas does
not exist when the element connects and a later one would otherwise go unwatched.
Binding is idempotent and marked per canvas.

Two string defects caught before this went anywhere. I wrote
T('actions.reloadMap'), and the string lives at buttons.reloadMap — it exists in
all eleven locales and my wrong namespace would have rendered English in every
one of them while the correct translation sat unused. And messages.mapContextLost
did not exist at all, which the tr() helper answers with a silent English
fallback. Both now resolve; keys-exist and parity are green.
2026-09-28 21:59:28 +00:00
Doxa Maps
63ce2d372c Wire the back-to-top on both surfaces, from the side that owns the scroller
The Driver asked for the arrow on the people-group grid and on the regions list.
Those needed different wiring, and the reason is worth recording because it is
not a preference.

The regions dashboard owns its scroller: .rd-scroll has overflow-y:auto, so the
plugin mounts the button itself and drives its own element. It deliberately does
not touch the page's scroll — this renders inside a shadow root in someone
else's page, and their scroll is not ours to move.

The people-group grid owns no scroller at all. .pgg-root sets no overflow and
.pgg-body has no rule; the grid simply grows and something outside it scrolls.
In this shell that something is .vtl-host, which lives in the layout, outside
every plugin's shadow root, so the grid could not reach it even if it should.
So the arrow for that surface belongs in the layout, beside the circle the
layout already owns, driving the pane it already owns. The grid is untouched.

.vtl-host is per tab, so the arrow follows `active` rather than binding one ref,
or it would scroll whichever pane rendered last.

One component, two consumers, and each drives a scroller it actually owns.
Checked that this adds nothing to the hardcoded-English census: 256 before, 256
after — the three regions entries there are PosterWall prop literals and predate
this.
2026-09-28 17:40:35 +00:00
Doxa Maps
e5140f0b41 A back-to-top that sits beside the hamburger circle, not on top of it
A back-to-top on mobile, next to the hamburger circle, for the people-group grid and the regions list. Both scroll their own container,
so the container is a prop and this component owns only the affordance.

It does not know where the circle is, and must not guess. .vtl-fab is 44px at a
12px inset today, and it lives in the layout outside every plugin's shadow root.
Hardcoding 64px here would put the two on top of each other the day that changes,
silently, on a phone. So the layout publishes --map-fab-beside alongside the
clearance it already published, and this reads it — falling back to the plain
inset when there is no circle, which is also the standalone case where nothing
sets it. The circle's geometry is stated once, by the component that owns it.

It also refuses to appear when it cannot work: visible tests that the container
actually scrolls, not only that scrollTop is large. An arrow that does nothing
when pressed is worse than no arrow, because the reader spends a tap finding out.
And it honours prefers-reduced-motion, since a long list jumping is exactly the
motion that rule exists for.

The failure mode here is a var NAME drifting between publisher and consumer:
nothing throws, the fallback applies, and the button quietly sits under the
circle again. Checked rather than assumed — every published name is consumed and
every consumed name is published.

NEEDS EYES: I cannot see 44px or a hover state. The logic is small; whether it
lands beside the circle rather than over it is a render question.
2026-09-28 17:40:35 +00:00
Doxa Maps
3a4d4db48a Reserve room for the floating hamburger under a scrolled mobile legend
The Driver scrolls a legend to the end on a phone and the last rows sit under
the hamburger circle. The circle is .vtl-fab: 44px, pinned bottom-left, rendered
only when narrow and headerless with the drawer shut.

It wins on paint for a reason no z-index can fix. The FAB lives in the LAYOUT,
outside every plugin's shadow root, so it and the legend sheet are in different
stacking contexts; the sheet is z-index 1000 and still loses. Raising the sheet
would not have worked, which is why this reserves space instead.

The layout now publishes --map-fab-clearance, and only while the button is
actually on screen; custom properties inherit through the flattened tree, so it
reaches inside each plugin's shadow root. The mobile legend's scroll area adds
that to its bottom padding, defaulting to zero. No circle, no reserved space,
and a plugin used without this layout is unchanged. The padding is additive, so
nothing that works today moves.

The button's v-if and the clearance are now ONE computed rather than two copies
of the same condition. Two copies is the bug waiting to happen: drift one way
and the legend ends under the circle again, drift the other and there is a strip
of dead whitespace with no circle to justify it. Neither has a symptom any gate
can see, so disagreement is made impossible rather than tested for.

Measured, not guessed: the earlier reading of this card found the top-left map
control at top:8px and concluded the geometry did not match the report. It did
not — that is a second, real collision in two plugins, and a different bug.
2026-09-28 17:40:34 +00:00
Doxa Maps
b2142ac0b0 DataTable + StackLayout: physical left/right -> logical, where the rule mirrors
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Six conversions, one deliberately kept physical. Same method as the legends.

CONVERTED
  DataTable.vue:238  .tabulator-cell border-right -> border-inline-end. A per-cell
    trailing border is the COLUMN SEPARATOR between adjacent cells; in RTL the
    separator has to sit on the other side or it draws on the table's outer edge.
  DataTable.vue:269  .dt-actions   margin-left:auto -> margin-inline-start:auto
  DataTable.vue:293  .dt-close     margin-left:auto -> margin-inline-start:auto
    Both are "push me to the trailing end of a flex row", which is the left in RTL.
  DataTable.vue:299  .dt-dl ul padding-left -> padding-inline-start. A list's
    marker indent follows the reading direction.
  StackLayout.vue:161,305  .stl-meta margin-left:auto -> margin-inline-start:auto
    (twice: the light and dark theme blocks).

★ KEPT PHYSICAL, AND IT WOULD HAVE BEEN A REGRESSION TO CONVERT
  DataTable.vue:245  '.dt:dir(rtl) … .tabulator-cell{text-align:right}'
  It is already SCOPED TO :dir(rtl), so right IS the trailing edge wherever it
  applies. More importantly the cells carry unicode-bidi:plaintext, which makes
  `end` resolve PER CELL from that cell's own first strong character -- which is
  the exact behaviour this line exists to override (English cells left-aligning
  inside an rtl table and clipping at its left edge, rega L20). text-align:end
  would silently undo the fix. Comment added at the site so the next reader does
  not "correct" it.

No consumer overrides any of the four classes: .dt-actions, .dt-close, .dt-dl and
.stl-meta appear in no other file in library/ or plugin-designs/, so a physical
override cannot defeat the logical rule and produce a half-mirrored table.

LTR is unchanged: inline-start/inline-end resolve to left/right under dir=ltr.
check:sfc-parse PASS. 13 plugin files use DataTable, 9 use StackLayout.

library/ is the build owner's under rule 5B -- source sha to studio; rega eyeballs ar.
2026-09-28 09:48:45 +00:00
Doxa Maps
4ce8604d42 Both library layouts load nested bundles through the shared embedScript, which fixes VerticalTabsLayout on the dev server
VerticalTabsLayout kept a private script loader without the dev-server ES-module
rule, so a bundle nested through it under bun run dev loaded as a classic script,
reported success, and never defined its tag. It and StackLayout now call
library/utils/embedScript.js loadScriptOnce; StackLayout keeps its own
resolveBundleSrc step first. loadScriptOnce gains an empty-url no-op (a
<script src=""> re-fetched the page) and carries StackLayout's stack-demo
measurement beside the gallery's. One copy of the pattern and one cache.
2026-09-28 07:07:33 +00:00
Doxa Maps
63dbacc068 The nav-row comment shipped a role noun too
doxa-maps-spa.js carried "internal role noun (Driver)" twice. It was not the
SPA's own file: the text is a CSS comment inside VerticalTabsLayout's style
literal, so it is string content and ships to every bundle using that layout.

Rewritten in BOTH synced copies (lines 355 and 844) to state the decision
rather than who made it. check:synced-css passes, so the copies stayed
identical. Verified against output: doxa-maps-spa.js goes 2 occurrences to 0.

Located by matching the SHIPPED text back to source, after two wrong guesses
worth recording. A backtick-parity test over a whole .vue file said all 25 of
that file's "Driver" mentions were outside a template literal, while 2
demonstrably shipped -- the crude lexer again. And the leak was not in
doxa-maps-spa.vue at all, which is where the bundle name pointed.
2026-09-28 00:20:34 +00:00
Doxa Maps
dec7059e87 Decouple the library's prose: no host-platform names in the QA widget, StackLayout or script notes
The QA widget comments (QaCorner, ProfileLoader, app-wrapper-material) say our own surfaces and the host's
session instead of naming one host; StackLayout describes a normal host page's flow; the live-cors script
note says it was measured on the live test host. Comments and notes only; no behaviour changes.
2026-09-26 17:28:45 +00:00
Doxa Maps
fef8c7b0fd The last of the shared-library English: legend, poster preview and tab loading states
Third and final batch of the shared-library strings, in the shepherd's order:
SemanticTreeLegend (4), PosterPreview (2), PosterWall (2) and
VerticalTabsLayout (2), plus PosterWall's "people groups" which already had a
key and was still typed in.

These are all LOADING and OVERFLOW states — "Loading data…", "Showing top 500 of
2,107", "use the geocoder above to find the rest", "Loading map snapshot…",
"preview unavailable", "Could not load X". Text a reader meets exactly when
something is slow or has gone wrong, which is the worst moment to be handed a
language they do not read. PosterWall's own source called one of them "i18n
DEBT" in a comment; it is paid now.

The counted ones take a placeholder rather than being split around an
interpolation — legend.showingTop is "Showing top {n} of {m}" — so a translator
can move the numbers within the sentence. Languages do not agree about where a
count goes, and a sentence assembled from fragments cannot be reordered at all.

VerticalTabsLayout needed a different shape: it is an options-API component, so
a module-level helper compiles fine and then renders nothing, because its
template can only see what the component returns. T is exposed from setup().

★ THE NEW GATE CAUGHT SOMETHING I WOULD HAVE MISSED, on its first real batch.
Adding these keys flipped two strings in
plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue from "no
catalogue entry" to "has a key and is still hardcoded" — that file is a COPY of
the library component I just fixed, carrying the same debt. It is another lane's
plugin so I have not touched it, and it is reported rather than quietly
baselined away. The baseline is refreshed to today's truth so the gate guards
the new line: 285 → 273 without a key, and the two copies recorded as known debt
rather than as a regression.
2026-09-26 13:04:16 +00:00
Doxa Maps
1795fb4740 Plugin Studio rail laid out like plugin-designs/: one folder per bundle, its elements inside
The Driver's ask: one folder per BUNDLE with its ELEMENTS inside, bundles sorted by capability, highest
first. The rail listed every element flat under its own capability, so a bundle's elements were
scattered (gospel-resources' five screens sat in three sections). Now each capability section (5 → 1)
holds the bundles whose HIGHEST capability it is, in name order; a bundle with several elements is one
folder (country-posters, gospel-resources, my-upg-100-list, regions-dashboard, cities-internet), and a
single-element bundle is shown as its element. Composites keep their folded composition trees.

StudioTree identifies a plugin's row by a `canonical` flag rather than "depth 0", so auto-reveal opens
the bundle folder above an element chosen by URL or restored tab. New folder icon; 'elementsCount' and
'elements' in all 11 studio catalogues.
check:studio-rail (headless, gates:post) runs the studio's real rail code on the built manifest and
asserts the layout and one canonical node per plugin (21 bundles, 33 elements); it fails on the previous
rail. Also: poster.json:dpi is declared identical on purpose in check:i18n-never-translated (a unit
symbol kept as-is by project decision), which had turned that gate red on main.
2026-09-26 12:23:49 +00:00
Doxa Maps
0c7f4af47b Engine and token props: a token-service URL is honoured, Clear clears, and a pending token is awaited
(cherry picked from commit 6e4ddceced)
2026-09-26 03:34:15 +00:00
doxa-maps-88655c70
09014f8a03 Rename the output folder to dx-maps
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Driver decision: no embeddable URL carries "doxa" in its folder. The tree's name is one line in
vite.doxa-maps-build-config.json ("name": "dx-maps"); everything else follows it.

- __DOXA_TREE_NAME__ (the vite define rega landed, from MFE_NAME = internal/_tree-name.mjs) bakes the name into the
  bundles for the runtime code that must recognise the tree in a URL. StackLayout.vue's
  resolveBundleSrc anchored on a literal /doxa-maps/ and would have broken every composite's nested
  bundles on a renamed tree; it now anchors on the define. The Research Gallery's dev fallback uses
  it (one reader: rega's define, not a second one).
- The native shells follow via internal/sync-app-shells.mjs: capacitor webDir public/dx-maps, Tauri
  frontendDist ../../public/dx-maps (its window url is tree-relative since the one-folder landing).
- The WordPress contract test reads the name from config instead of carrying three literals.
- check:locale-reachable extracted detectLocale alone and died with "localeFromUrl is not defined"
  after 09d03c60 factored a helper out; it now follows the calls it makes (already red on main).
- generate-tree removes its temp asset pool even when it crashes (a failed build leaked ~50 MB into
  /tmp each time; measured on a full disk).
- Capability exemplars regenerated for the new paths.

Proof: rm -rf public && bun run build -> public/ = README.md + dx-maps/; the 25 built bundles carry
zero 'doxa-maps/' paths (scanned); check:sri, check:drop, check:parse, check:public-hygiene PASS.
Tag names (doxa-* -> dx-*) are the next, separate landing.
2026-09-25 19:26:14 +00:00
doxa-maps-88655c70
4d773d6ee8 Move internal files out of the repo; fix five dead links; a fuller delegation diagram
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Moved out (kept outside the repo, in the team's internal tracker):
- AGENT-BRIEF.md: operating rules for the team's own agents
- documentation/slide-deck-outline.md, documentation/README-longform.md:
  internal pitch material; the README and why.md cover the same ground
- plugin-designs/gospel-resources/SOURCES-CHECKED-AND-REJECTED.md: research log
- .claude/ (an editor status line), .forgejo/ (a CI workflow that never ran),
  _archive/ (a retired staging page; already gitignored but still tracked)
Seven code comments that named these files now describe the rule instead.

Fixed five dead links left from the docs/ -> documentation/ move, including
the one in template-bundle that every new plugin inherits.

The delegation diagram now shows the whole model: the Plugin Studio with the
shared library and the merge queue into main, seven production plugins with
capability badges and the Experimental shelf, one agent per plugin (a roaming
agent for the shelf), the shepherd's oversight ring, a legend, and the five
ways a build ships.
2026-09-25 05:14:10 +00:00
doxa-maps-88655c70
5e7f73d0be VerticalTabsLayout mirrors for right-to-left, and the gallery sets its direction
Arabic did not flip the shell (reported by studio for matrix cell E). The
layout is written in physical terms: rail at left:0, content at left:256px,
translateX(-…) slides. So an Arabic gallery kept its rail on the left with
LTR geometry.

The layout now carries an additive :dir(rtl) mirror block, in BOTH copies
of its CSS (VTL_SHADOW_CSS and the style block). The rail anchors right,
content right:256px, the collapse and drawer slides go positive, the
collapsed paddings, auto-margins and text-align mirror, and the arrow SVGs
flip. translateX has no logical form, so this is a mirror block rather than
logical properties. LTR consumers match none of it.

The gallery sets dir (and lang) on the layout from its own language: rtl
for Arabic, ltr otherwise. So an English gallery on some other RTL page
keeps a Latin rail unmirrored.

Measured on the served built tree:
  1400px ar: rail [944,1200], content [0,944], text right; collapsed =
             a 72px rail on the right
  390px ar:  drawer parked off-canvas right, slides in to [944,1200]
  LTR:       studio, stack-demo and the gallery at 1400px geometry
             identical before and after. The 390px differences seen once
             were transition timing and did not reproduce.

The shared layout is compiled into doxa-maps-spa and plugin-studio, so
both bundles are rebuilt here. stack-demo's bundle came out byte-identical:
it does not compile the layout.
2026-09-25 04:41:01 +00:00
doxa-maps-88655c70
814124b3f6 Plugin studio: its own words in every locale
Some checks are pending
build-gate / build (push) Waiting to run
Choosing a language in the studio translated the plugins it mounts but
left the studio itself in English: the title, the count, the filter box,
Collapse all, the section names, Copy embed, the loading and error
messages, and the rail's own Collapse button and aria labels.

The studio's strings now live in library/i18n/locales/<lc>/studio.json
for all 11 locales, held to key parity by check:i18n-parity like every
other catalog. The studio has no vue-i18n instance, so it looks strings
up directly (locale, then English, per key). The Explorer tree takes
them through a `strings` prop. VerticalTabsLayout gains an optional
`labels` prop for its five words; hosts that do not pass it keep the
English defaults, so doxa-maps-spa is unchanged until it opts in.

Verified by switching the studio through every language its switcher
offers (en, es, fr, pt, ar, ru): no English left in the studio chrome.
Technical names (CAP n, element tags, tier machine names in tooltips)
stay as they are.
2026-09-25 04:10:49 +00:00
doxa-maps-88655c70
d5c283f474 Mobile drawer: an open rail sits above the plugin's own layers
Some checks are pending
build-gate / build (push) Waiting to run
On a phone, opening the studio's rail over the research dashboard left
the dashboard's filter row painted across the drawer. Mounted content
shares the drawer's stacking context, and the dashboard raises its
filter bar to z-index 55 while the drawer sat at 20.

While the drawer is open on a phone it now sits at z-index 1000: at
that moment it is the only thing you can use. Desktop and plugin modal
scrims keep their existing order. Checked at 390px on dashboard-plus-grid
in the studio: the drawer covers the dashboard completely. doxa-maps-spa
shares this layout and gets the same fix.
2026-09-25 03:47:16 +00:00
doxa-maps-88655c70
f63d75ed80 Plugin studio: an Explorer tree for the rail, imitating VS Code
Some checks are pending
build-gate / build (push) Waiting to run
The studio's rail was hand-rolled vertical tabs stretched into a tree.
It is now an Explorer tree modelled on the VS Code Explorer, keyboarded
per the WAI-ARIA tree pattern:

- CAP 5 -> CAP 1 sections with sticky headers; one-line rows with the
  name and a muted element tag; twisty on the left; indent guides.
- Composites fold; their parts hang beneath, one level at a time, each
  part opening the plugin's one canonical entry.
- A filter box on top shows matches together with the path to them.
- One view action, Collapse all.
- Auto-reveal: opening a plugin by click, URL hash or remembered tab
  scrolls its row into view and opens its section.
- Arrow keys walk the tree, Right/Left open, close and move between
  parent and child, Enter opens, a letter jumps to the next name.
- The slim rail becomes an activity bar with one icon per section.
- Fold state is remembered per page.

The tree lives in plugin-designs/plugin-studio/src/StudioTree.vue and
fills VerticalTabsLayout's new #rail slot, so the layout still owns the
collapse toggle, mobile drawer and slim strip. The alias, depth and
branch row fields added to the layout earlier today are removed again:
nothing else used them.

The studio host now has a default height (85vh, min 480px), as
doxa-maps-spa does. Pasting the published embed snippet into a plain
page mounted a complete studio 0px tall. A page's own height or
--plugin-studio-height still wins.

The studio gates that click tabs by text now also match tree rows.
Rebuilt the plugin-studio bundle and re-baked its SRI.
2026-09-24 21:40:48 +00:00
doxa-maps-88655c70
e3df4677d3 Plugin studio: fold every composite, so the rail starts as a plain list
Some checks are pending
build-gate / build (push) Waiting to run
With every composition tree open, the rail was too long to find your way
around. Each composite now carries a chevron and starts folded, so the
rail first reads as the 35 plugins by capability, each composite saying
how many parts it has. Open one and you see its direct parts; a part that
is itself a composite folds again, so doxa-maps-spa opens to its tabs and
each tab opens to what it is made of, one level at a time. Choosing a
composite also opens it; only the chevron closes it. The same child folds
independently wherever it appears.

VerticalTabsLayout gains two optional row fields: `branch` makes a row
foldable, `ancestors` hides a row until every branch above it is open.
Rows are wrapped so the chevron can sit beside the tab rather than
inside it. doxa-maps-spa sets neither field and renders unchanged.
2026-09-24 20:45:19 +00:00
doxa-maps-88655c70
f9146d5105 Plugin studio: every plugin by capability, and a composition tree per composite
Some checks are pending
build-gate / build (push) Waiting to run
The rail now has two labelled parts. "Every plugin, by capability" keeps
one node per plugin in collapsible CAP 5 -> CAP 1 sections. "What each
composite is made of" adds one collapsible section per composite, headed
by the composite, with every element nested into it drawn as an indented
tree at full depth, ordered by capability at each level. A child repeats
in every composite that nests it, as specified.

Repeats are aliases, not new nodes: clicking people-groups-grid under
big-picture selects the same canonical entry as clicking it in CAP 2 and
highlights every copy, so each plugin still has exactly one name. Arrow
keys walk only the canonical rows. A declared child that is published
nowhere (big-picture's bp-research-header) shows as a "part" row, so the
header/grid split inside Big Picture can be named.

The dev server's manifest never carried `composes` edges, so under
`bun x vite` the Composed-of panel was empty for every composite. It now
publishes them from the same meta.json source as the built manifest.

Also: bare sibling profile refs (gospel-resources composes "data-tables")
now resolve, and rail rows use local SVG icons per capability instead of
emoji that rendered as empty boxes without an emoji font.

VerticalTabsLayout gains two optional row fields (`selects`, `depth`) and
shows badge/count on folder headings; existing consumers set neither.
2026-09-24 18:49:20 +00:00
doxa-maps-88655c70
be77ea954c Merge main into bundler-2/work 2026-09-23 23:48:10 +00:00
doxa-maps-88655c70
ae388d6da8 cap-5 shell: drop contain:layout from .vtl-content -- fixed scrims cover the viewport
Some checks are pending
build-gate / build (push) Waiting to run
contain:paint was removed earlier for the scroll bug, but LAYOUT
containment also makes .vtl-content the containing block for every
position:fixed descendant, so all modal scrims hosted in the shell
resolved inset:0 to the CONTENT PANE: measured, a fixed inset:0 probe
read x=256 w=1184 on a 1440 viewport -- the 256px nav rail stayed
clickable behind an open modal. Visibly almost right, interactively
wrong.

The perf rationale for keeping it does not hold: a nav collapse
changes this element's own width, and a width change re-lays-out the
subtree with or without containment.

Verified: fixed probe now 0,0 1440x900; check:scrim-viewport PASS
(3 scrims cover the viewport in the shell); check:fits-screen PASS
(scroll/zoom/reachability contract intact); nav collapse round-trips.
2026-09-23 23:40:13 +00:00
doxa-maps-88655c70
3685b196cb Show the cap-4 composite in the studio's Simple Research slot
The cap-5 shell's "Simple Research" tab rendered an error panel instead of
the map, in both trees:

  Could not load <doxa-research-map> from ../../doxa-research-map/
  doxa-research-map.js

StackLayout already anchored sibling-bundle srcs to the tree root, but it
anchored on the /doxa-maps/ segment of window.location.pathname. The studio
mounts at the SERVER ROOT (/index.html), which has no such segment, so the
lookup missed and the literal '../../' was returned untouched. The browser
resolved it against '/', where '../' cannot climb, and requested
/doxa-research-map/doxa-research-map.js — 404, both children, empty stack.

That is why it survived: every gate and every screenshot used a LEAF url,
and the studio is the one host that is not one. The same composite at its
own leaf renders perfectly.

resolveBundleSrc now falls back to bundleTreeRoot() — the bundle's OWN
script URL, which rollup rewrites to document.currentScript.src in the IIFE
build. That is a fact about where the code came from, not a guess about
where the page is, which is the lesson stack-demo.vue wrote down when this
defect appeared for the third time. It returns null in dev ESM and for any
non-<name>/<name>.js layout, so it never guesses. The pathname rule stays
FIRST and the untouched-string fallback stays LAST, so every path that
worked before is byte-identical: this can still only fix a path, never
break one.

research-map-plus-grid also adopts stack-demo's bundleSrc() dev/prod
helper. In dev, vite serves entries at /plugin-designs/<bundle>/index.js and
there is no <bundle>/<bundle>.js to reach, so the dev surface 404'd too.
import.meta.env.DEV is statically replaced, so the built output is
byte-identical to the literal it replaces.

Verified in a browser against the built tree, reading the screenshots:
studio root -> Simple Research paints the world map with its prayer legend,
on the dev server too. Negative-tested: disabling the new fallback and
rebuilding restores the exact 404 and the empty canvas.

No regression across every StackLayout consumer — rmpg shallow leaf, rmpg
deep default/ leaf (the case the pathname rule was written for),
spa -> Simple Research, dashboard-plus-grid, stack-demo — and all seven
studio dashboards. 7/7 pre, 22/22 bundles, 18/18 post.
2026-09-23 23:26:40 +00:00
doxa-maps-88655c70
378ecb9ae6 UPG-100: lead with Prayer Resources, map second
Some checks are pending
build-gate / build (push) Waiting to run
The first tab a new user meets is now the poster/resources pane, not the
map. That pane already explains what a UPG-100 list is at the top and shows
the posters, so it needs no explanation; a choropleth of zones does. Maps
need not be primary — a familiar list or grid is the on-ramp people use
before they transition into the map.

Renames the tab from "Prayer Lists" to "Prayer Resources".

Pane order lives in at[0], not in array order: buildPaneColumn sorts by
at[0] while ViewSelect renders the array verbatim, so both are swapped
together or the tab strip desyncs from the scroll column. The active view
is now derived from that same sort instead of a hardcoded 'map'.

In the wrapper shell the map pane is resolved by PROFILE rather than by
"pane 0 is the map": under the positional rule, reordering mounted the map
element into the poster pane, and "Show on map" scrolled to the wall you
clicked from. The shell's tab strip also resets to a profile's first view
on switch, since a stale 'map' id is valid in nearly every profile.

ViewSelect keeps each tab on one line (nowrap + ellipsis), ported from the
shell's own mobile rule that the original extraction left behind. Measured
at 390px: "Prayer Resources" wrapped inside its button and grew the strip
45 -> 60px; it now holds 45px at every width from 1280 down to 390.
2026-09-23 20:33:00 +00:00
doxa-maps-88655c70
1cf4e89dab Resolve a composite's child bundles against the tree root, not a guessed depth
Some checks are pending
build-gate / build (push) Waiting to run
Driver-reported console text:
  Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js

A composite declares children as src: '../../<bundle>/<bundle>.js'. That literal
is right from a one-deep leaf and wrong from a two-deep one, and the generator
emits both:
  .../research-map-plus-grid/index.html          ../../ -> /doxa-maps/          200
  .../research-map-plus-grid/default/index.html  ../../ -> /doxa-maps/rmpg/     404

So the path encoded an assumption about the host's depth that the host does not
control and cannot see. It survived because every gate exercised the shallow
leaf, which is genuinely fine.

StackLayout now derives the tree root from the document's own pathname (up to
and including the /doxa-maps/ segment) and rebuilds the path from there --
depth-independent by construction, so a third leaf depth cannot reintroduce it.
Absolute, protocol-relative and root-relative srcs pass through untouched, and
a missing /doxa-maps/ segment returns the original string, so this can only fix
a path, never break a working one.

Measured, both leaves, real browser with live data:
  one-deep  mapEl=true mapH=792 canvasH=856 0 4xx
  two-deep  mapEl=true mapH=792 canvasH=856 0 4xx   (was 404, map absent)

Also carries source that had been left behind by its own built output:
the cities/gospel legend bounding (shipped in 5008a6f as output only) and the
per-profile config carrier in generate-tree.mjs, plus a new fits-screen clause
that wheels inside every column rather than one.
2026-09-23 19:34:28 +00:00
doxa-maps-88655c70
d97cf9eab5 Say why a tab could not load instead of painting an empty grey pane
Some checks are pending
build-gate / build (push) Waiting to run
The Simple Research tab rendered as a featureless grey rectangle with no
message, no overlay and nothing in the console. It reads as a broken app,
and it hid a plain missing-file condition through four separate reports.

Two silent failure paths produced it, one layer apart:

- doxa-maps-spa swallowed a failed manifest.json fetch. Every tab except
  Big Picture is composed from that manifest (Big Picture alone is baked),
  so losing it leaves each tab with no scriptUrl. mountTab then returns
  early and the pane stays in state 'idle' -- which renders neither the
  loading hint nor the error hint. The manifest fetch now tries the built
  tree location as a second candidate and, if every candidate fails, sets a
  message the pane displays.

- StackLayout's per-section catch deliberately left a failed section's host
  empty so the rest of the stack still renders. Keeping the stack alive was
  right; leaving the band blank and silent was not. A failed section now
  shows which element failed and which URL it came from, and still does not
  abort its siblings.

Tree-relative scriptUrls are also resolved against the manifest they came
from rather than the current document. The two roots are identical in the
built tree and differ elsewhere, which turned every composed tab's bundle
into a 404 against a path that exists nowhere.

Measured, built tree: research-map-plus-grid mounts both children, 4778px
of content, live canvas, zero 4xx -- unchanged. dashboard-plus-grid,
stack-demo and research-map-plus-grid all render with a painted canvas and
no failure cards. Where the bundles genuinely cannot be reached, the pane
now names the element and the URL instead of showing grey.
2026-09-23 19:14:27 +00:00
doxa-maps-88655c70
d4a8184ad7 Match the tab radius to the rest of the app, and open on the tab you were last on
The vertical tabs were a 999px stadium on a 48px row. That was a real earlier
instruction -- imitate the YouTube active row -- and it shipped, but measured
against the product it is the only thing shaped that way: the collapse button is
8px, the filter toggles are 8px, and 8px is already the most common radius in
the layout file. They now read var(--vtl-radius,8px), so a consumer has one knob
and the three surfaces cannot drift apart. The 48px Material row height is
untouched. The YouTube rationale stays in the comment as history with an explicit
note not to restore the pill from it, because a superseded rationale that still
reads as current is how a reversed decision gets quietly undone.

The .vtl-cap badge keeps its pill. The complaint was about the ratio of radius to
size -- a 16px chip at 999px is a chip, a 48px row at 999px is a stadium.

The studio opened on nothing and said "Choose an element from the list." It now
resolves a tab in priority order: the URL hash, since an explicit shareable
request should beat a remembered one; then localStorage, so a reload is not a
fresh start; then the first row, so the pane is never empty. This lives in the
studio rather than the layout because VerticalTabsLayout is controlled via
v-model:active and deliberately holds no selection state.

Storage is keyed by pathname so two shells on one origin do not fight over one
key, every access is wrapped because localStorage throws in private mode and an
exception in setup would blank the whole subtree, and the key is only written
after the row is confirmed to exist so a bad key cannot poison the next load.

Verified in the built shell on a settled tree: first load auto-selects with no
hint and stores the key; clicking big-picture moves the selection and updates
storage; a reload restores big-picture; and a deliberately poisoned key falls
back to the first tab and rewrites itself rather than rendering an empty pane.
2026-09-23 01:03:15 +00:00
doxa-maps-88655c70
00bf294d41 Compact the pane-scroll comments, keeping both declarations
Some checks are pending
build-gate / build (push) Waiting to run
An agent collapsed the multi-line notes on .ps-pane and .vtl-host into
one-liners. Both declarations survive in both synced copies -- overflow-y:auto,
overscroll-behavior:contain, and --profile-min-h:0 -- and check:shell-scroll
still passes on the built tree: 2611px scrolled, grid on screen, no map-tab
scrollbar.

Committing straight away rather than leaving it in the working tree. These are
the exact two files where the scroll fix was silently lost last tick: it
existed only uncommitted, a gate passed against those bytes, and a second
agent's edit overwrote them before anything reached git.

Worth noting a false alarm in my own check: grepping for a formatting-
sensitive string reported one copy where there are two, because the compaction
left them with different whitespace. The semantics are identical, which is why
check:synced-css compares property maps rather than text.
2026-09-22 19:18:11 +00:00
doxa-maps-88655c70
043b40ddbd Give the shell pane its scrollport back
check:shell-scroll went back to "wheel moved 0px, grid UNREACHABLE" with
2586px of grid mounted. The cause is not a later edit undoing a committed fix:
git history shows .vtl-host never had overflow-y:auto in any commit. Seven
commits, all `position:absolute; inset:0`.

The fix only ever existed in a working tree. An agent edited the file live,
the gate passed against those uncommitted bytes, and a second agent editing
the same file overwrote them before anything was committed. Every observation
was true and none of it was durable. Worse, 762953a was committed while the
tree was in the reverted state and described as landing the scroll fix.

Restored on .vtl-host and .ps-pane, both synced copies of each, and the reason
is now in the file rather than in a session transcript: the composite reports
scrollHeight 2838 against clientHeight 769 here, 2069px of mounted grid with
nowhere to go, while the document measures 900 === 900 so a wheel anywhere has
nothing to move. It is a host gap, not a plugin bug -- the same composite is
perfect standalone because its generated leaf gives the document the scroll.

The comment says the line has been lost once and points at the gate, because
`inset:0` looks like the simpler form and someone will reach for it again.

Committing before re-verifying, deliberately. A gate result is only as durable
as the bytes it ran against, and this bug is the proof.
2026-09-22 19:04:21 +00:00
doxa-maps-88655c70
762953a054 Land the swarm's dashboard work: colour, rails, cascade, scroll
Some checks are pending
build-gate / build (push) Waiting to run
Sixteen source files from five agents, all verified against source gates
before commit: template-literal, sfc-parse, kpi-unit, composite-reg,
facet-cascade, one-palette, script-flags, no-public-binds, no-orphan-gates and
quoted-scripts all pass. public-hygiene reports 0 findings.

Colour. The choropleth ramp is now derived by interpolating the product's own
tokens (surface #f3f3f1 to accent #92b195) rather than adding a sixth grey, and
that fixes a pincer the earlier research had not named: on the old grey-to-black
ramp the navy dot and the white ring failed at OPPOSITE ends, so no fixed pin
colour could win. Worst-case dot contrast moves 1.13 to 7.29. Second-order
breaks fixed with it -- the country outline would have measured 1.08-1.82 as
white on the pale fill, so it is #6d766e at 2.59-4.34, and the legend swatch
border.

Religion colours no longer key by array index. colorFor takes no index at all;
colour comes from the registry by facet plus value. All 31 distinct live
religion labels now match the map, 0 mismatches. Tree-rail grey rows 142 to 0
against the live 2,107-row API.

Cascade. Clearing a parent facet now clears its descendants, in the selection
bus rather than in any component, because every clear path already funnels
through setFilter/clearFilters -- a component fix would have fixed one button
and left four. The hierarchy is derived from config.facetRail[].drill, so it
cannot drift from the tiers the rail renders. Place turned out to be three deep
(region, block, country), not two.

Rails. Tree rows wrapping 18/66 to 3/66; the tree rail is now the wider of the
two at 296 vs 264, with the map cell unchanged because both pairs sum to 560.

Also lands the shell scroll fix and the facet-cascade gate with its committed
fixture -- 73 sampled rows, no network, so the assertion that reproduces the
empty map runs on a fresh clone instead of silently skipping.
2026-09-22 18:57:39 +00:00
doxa-maps-88655c70
5725f5b9d9 Let a stacked composite find its siblings in either tree
Some checks are pending
build-gate / build (push) Waiting to run
stack-demo rendered two empty sections in dev. Its sibling bundles were addressed with
a tree-relative path, which the browser resolves against the document URL, so no host
and no attribute can redirect it — correct in the built tree, a 404 in dev. Measured:
the dev entry answers 200 while the path the relative form resolves to answers 404. The
sources now resolve at runtime, so the same file is right in both trees, and the
production branch compiles away to the string it replaces.

That was the third place this defect appeared. Both capability-5 shells carried the same
hardcoded paths and were cured the same way, and each instance was found separately
after the previous one was fixed — because a literal parent-directory path reads as
correct in review and is invisible to a gate that only ever checks the built tree.

Removing the 404 was not enough: the sections were still empty with zero failed
requests. The shared layout injected each bundle with a classic script, and a classic
script pointed at a dev ES module fires its load handler successfully and only then
throws, so the promise resolved while the tag never registered and the wait for
definition quietly timed out. A load handler cannot detect that.

The module rule now lives in the shared layout rather than in a fourth private copy of
the same loader, so the two other composites that inject through it are covered too.
Verified after: both elements define, sixty-six facet rows and twenty-four cards on
stack-demo and on dashboard-plus-grid, zero failed requests on each.
2026-09-22 15:37:41 +00:00
doxa-maps
684b209704 Show the research-map composite instead of Big Picture on a phone
Some checks are pending
build-gate / build (push) Waiting to run
The Driver's ordering: desktop opens on the Big Picture dashboard; mobile opens
on the research map and does not show Big Picture at all. Both capability-4
composites already existed, so this is the shell choosing between them. A row in
the nav model can now declare onlyWhen desktop or mobile; everything below the
first rung is identical on both, which is why only the first two rows carry it.

One nav model rather than two, so the teaching order lives in one place. Two
parallel models would be two things to keep in sync and the failure would be
silent: a row added to one and forgotten in the other simply never appears.

Crossing the breakpoint can remove the row that is currently active, which would
leave the user on a mounted but unlisted tab with no highlight. The shell now
snaps to the first surviving row, and establishes the viewport class before
opening the first tab so a phone never opens Big Picture for a frame.

Also fixes a pre-existing bug this exposed: with no header there was no way to
open the rail on mobile. The drawer sits off-canvas, the only toggle lives inside
the header, and the collapse control is inside the drawer itself — measured
off-screen at x:-244. Every tab except the one that happened to open was
unreachable. A floating button now appears only when narrow, headerless and shut,
so no existing consumer can see a change.
2026-09-22 08:13:41 +00:00
doxa-maps-88655c70
9dc4dda8cf consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.

The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.

Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
  - join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
  - tauri frontendDist '../public' -> '../../public', since src-tauri is now one
    level deeper; check:tauri caught this exactly as the card predicted
  - the WordPress contract test's dirname(__DIR__,2) -> 3, same reason

Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00
doxa-maps-88655c70
af73dfb3f9 adopt Material nav metrics in VerticalTabsLayout
Driver: "the sidebar is supposed to look like Material UI, like what people are used to...
people are used to the YouTube layout, Google layout is just way better. We do not need
to create custom styles people are not used to."

Measured before: app bar 51px, nav rows 44px with an 8px radius, collapsed rail 60px.
Now app bar 64px desktop and 56px mobile, rows 48px, a full pill active row, and a 72px
mini-variant rail. Nav width was already 256px and is unchanged.

Nothing vendored. The card's law is to imitate the metrics in our own CSS rather than add
Quasar or MUI, which is the same clone-don't-edit rule the library follows elsewhere.

Two traps worth recording. box-sizing:border-box is load-bearing on the app bar: without
it the element is content-box, so min-height:64px measured 81px live once padding and
border were added. And the rail collapse is a translate rather than a width change, so
four numbers are coupled — the nav translate, the content pane's left, and two padding
compensations that keep icons inside the visible strip.

Verified live: app bar 64 desktop and 56 mobile, rows 48 with a 999px radius, rail 256
expanded to 72 collapsed with the content pane starting at exactly 72.
2026-09-21 16:25:43 +00:00
doxa-maps-88655c70
981cc40633 Shared layout and legend tools on the DX palette; keep dark-surface ink ramps 2026-09-20 06:15:37 +00:00
doxa-maps-88655c70
85014aad11 Session work: strobe fix, grid layout, stacked tree legend, selection camera, chip bounding, asset inheritance, gates 2026-09-20 04:49:25 +00:00
doxa-maps-88655c70
ea0c47b277 Brand-neutral shared VerticalTabsLayout with lazy nested-element hosting; fix cap-5 prayer tab leaf path; neutralize library identifiers; add Forgejo CI build-gate workflow
Some checks failed
build-gate / build (push) Has been cancelled
2026-09-19 17:24:30 +00:00