[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"docs-nav":3,"docs-page:development":22},[4,7,10,13,16,19],{"route":5,"label":6},"\u002F","Home",{"route":8,"label":9},"\u002Fapi","1. API",{"route":11,"label":12},"\u002Fdevelopment","2. Development",{"route":14,"label":15},"\u002Finstances","3. Instances",{"route":17,"label":18},"\u002Fmap","4. Map",{"route":20,"label":21},"\u002Fdata","5. Data",{"route":11,"title":23,"html":24,"prev":25,"next":26},"Development","\u003Ch1 id=\"development\">Development\u003C\u002Fh1>\n\u003Cblockquote>\n\u003Cp>Contributor workflow and verification protocol for Waymark JS.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"contributor-workflow\">\u003Ca class=\"heading-anchor\" href=\"#contributor-workflow\">Contributor workflow\u003C\u002Fa>\u003C\u002Fh2>\n\u003Col>\n\u003Cli>Install dependencies: \u003Ccode>npm install\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Start dev server: \u003Ccode>npm run dev\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Run formatting: \u003Ccode>npm run format\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Verify formatting: \u003Ccode>npm run format:check\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Regenerate \u003Ccode>docs\u002Fllms-full.txt\u003C\u002Fcode> and run docs\u002FAPI sync checks: \u003Ccode>npm run docs:sync\u003C\u002Fcode> (\u003Ccode>test:ci\u003C\u002Fcode> first runs \u003Ccode>npm run check:llms\u003C\u002Fcode>, which fails if the committed bundle is stale)\u003C\u002Fli>\n\u003Cli>Preview and build the docs site: \u003Ccode>npm run docs:dev\u003C\u002Fcode> (Nuxt dev server; defaults to \u003Ccode>http:\u002F\u002Flocalhost:3000\u003C\u002Fcode>) and \u003Ccode>npm run docs:generate\u003C\u002Fcode> (builds the deployable static site to \u003Ccode>docs\u002F.output\u002Fpublic\u003C\u002Fcode>)\u003C\u002Fli>\n\u003Cli>Run unit tests: \u003Ccode>npm test\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Build and verify the dist output: \u003Ccode>npm run build &amp;&amp; npm run check:dist\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Run functional browser tests: \u003Ccode>npm run test:browser\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Regenerate the committed screenshots after an intentional visual change: \u003Ccode>npm run screenshots\u003C\u002Fcode>; verify byte-identity without touching the committed set: \u003Ccode>npm run screenshots:verify\u003C\u002Fcode>. Refresh the vendored basemap fixture — a deliberate, network-touching act — with \u003Ccode>npm run fixtures:refresh\u003C\u002Fcode> (see \u003Ca href=\"#screenshots\">Screenshots\u003C\u002Fa>).\u003C\u002Fli>\n\u003Cli>Run the full CI gate locally: \u003Ccode>npm run test:ci\u003C\u002Fcode> (format check → llms bundle check → docs sync → build → dist check → docs generate → unit tests → browser tests).\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Ccode>npm run test:browser\u003C\u002Fcode> boots a dedicated Playwright Vite server on \u003Ccode>127.0.0.1:4173\u003C\u002Fcode> with \u003Ccode>--strictPort\u003C\u002Fcode>, so browser tests are deterministic even when your normal dev port is occupied. Browser tests import \u003Ccode>src\u002Fentry.js\u003C\u002Fcode> directly — the built bundle is verified separately by \u003Ccode>npm run check:dist\u003C\u002Fcode> and the Vitest dist smoke test. \u003Ccode>playwright.config.js\u003C\u002Fcode> pins \u003Ccode>workers: 1\u003C\u002Fcode> and \u003Ccode>retries: 1\u003C\u002Fcode> because the suite includes WebGL\u002Fshader map tests that flake under parallel workers; single-worker execution was verified fully green. \u003Ccode>npm run test:browser\u003C\u002Fcode> runs the \u003Ccode>functional\u003C\u002Fcode> project only — committed pixels are captured by the separate \u003Ccode>screenshots\u003C\u002Fcode> project (see \u003Ca href=\"#screenshots\">Screenshots\u003C\u002Fa>) — and both projects run fully offline against the vendored basemap fixture.\u003C\u002Fp>\n\u003Cp>\u003Ccode>npm run docs:sync\u003C\u002Fcode> chains \u003Ccode>node scripts\u002Fllms-full.mjs\u003C\u002Fcode>, \u003Ccode>npm run docs:contract\u003C\u002Fcode>, and \u003Ccode>npm run docs:api-sync\u003C\u002Fcode> (which runs \u003Ccode>api:contract\u003C\u002Fcode> first). The generator step rebuilds \u003Ca href=\"\u002Fwaymark-js\u002Fllms-full.txt\">\u003Ccode>docs\u002Fllms-full.txt\u003C\u002Fcode>\u003C\u002Fa> — the full content of every doc linked in \u003Ca href=\"\u002Fwaymark-js\u002Fllms.txt\">\u003Ccode>docs\u002Fllms.txt\u003C\u002Fcode>\u003C\u002Fa>, concatenated into one file for single-fetch consumption. Both \u003Ccode>docs\u002Fllms.txt\u003C\u002Fcode> and \u003Ccode>docs\u002Fllms-full.txt\u003C\u002Fcode> are also published as tracked, byte-for-byte copies under \u003Ccode>docs\u002Fpublic\u002F\u003C\u002Fcode> for repository indexing; \u003Ccode>docs:sync\u003C\u002Fcode> refreshes those copies and \u003Ccode>npm run check:llms\u003C\u002Fcode> verifies them. \u003Ccode>test:ci\u003C\u002Fcode> runs the generator in check mode first (\u003Ccode>npm run check:llms\u003C\u002Fcode>), so a stale committed bundle fails the gate before \u003Ccode>docs:sync\u003C\u002Fcode> regenerates it. No root \u003Ccode>SKILL.md\u003C\u002Fcode> is generated; consumer-facing agent docs follow the llms.txt convention and are distributed with the docs GitHub Pages deploy (see \u003Ca href=\"#docs-site\">Docs site\u003C\u002Fa>).\u003C\u002Fp>\n\u003Cp>Vite config is split by role: \u003Ccode>vite.config.dev.js\u003C\u002Fcode> for local dev server, \u003Ccode>vite.config.lib.js\u003C\u002Fcode> for library builds, and \u003Ccode>vitest.config.js\u003C\u002Fcode> for tests.\u003C\u002Fp>\n\u003Cp>\u003Ccode>vite.config.lib.js\u003C\u002Fcode> defines \u003Ccode>process.env.NODE_ENV\u003C\u002Fcode> as a build-time replacement for the dist bundle, preventing browser runtime errors like \u003Ccode>process is not defined\u003C\u002Fcode>. It also keeps \u003Ccode>maplibre-gl\u003C\u002Fcode> external (\u003Ccode>rollupOptions.external\u003C\u002Fcode>), so the build emits a single ESM bundle — \u003Ccode>dist\u002Fwaymark.js\u003C\u002Fcode> (~31.5 kB, ~8.8 kB gzip) with a bare \u003Ccode>maplibre-gl\u003C\u002Fcode> import, no CSS and no inlined worker. \u003Ccode>npm run check:dist\u003C\u002Fcode> guards that output shape.\u003C\u002Fp>\n\u003Ch2 id=\"conventions-and-definitions\">\u003Ca class=\"heading-anchor\" href=\"#conventions-and-definitions\">Conventions and definitions\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Instance\u003C\u002Fstrong>: public object returned by \u003Ccode>createInstance({ map, document? })\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Map injection\u003C\u002Fstrong>: the consumer creates and owns the MapLibre map; Waymark duck-types and mounts its overlay onto it.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Runtime core\u003C\u002Fstrong>: internal lifecycle object in \u003Ccode>src\u002Fruntime\u002FcreateInstanceCore.js\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Runtime registry\u003C\u002Fstrong>: internal \u003Ccode>WeakMap\u003C\u002Fcode> from map → core in \u003Ccode>src\u002Fruntime\u002FruntimeRegistry.js\u003C\u002Fcode> (one runtime per map).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>InstanceDocument\u003C\u002Fstrong>: canonical serialisable payload from \u003Ccode>instance.toJSON()\u003C\u002Fcode> (the \u003Ccode>document\u003C\u002Fcode> option).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>GeoJSON\u003C\u002Fstrong>: map data format and symbol naming (\u003Ccode>createGeoJSONModule\u003C\u002Fcode>, \u003Ccode>geoJSON\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Data layers\u003C\u002Fstrong>: canonical shape is \u003Ccode>data.layers: Array&lt;{ type?: &quot;geojson&quot;, data: object }&gt;\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Feature interaction\u003C\u002Fstrong>: click\u002Fhover\u002Fleave event resolution in \u003Ccode>src\u002Fgeojson\u002FcreateFeatureInteractionModule.js\u003C\u002Fcode>; popups and cursor styling are consumer-owned.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Type icons\u003C\u002Fstrong>: \u003Ccode>config.types[].icon\u003C\u002Fcode> is a name hook for a consumer-registered map image (\u003Ccode>map.addImage\u003C\u002Fcode>); Waymark ships no icon assets and never registers images itself. The dev playground registers an inline \u003Ccode>photo-marker\u003C\u002Fcode> PNG this way and maps it to the \u003Ccode>photo\u003C\u002Fcode> type.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Keep runtime internals internal. Consumer docs and tests should target public API behaviour.\u003C\u002Fp>\n\u003Ch2 id=\"naming-conventions\">\u003Ca class=\"heading-anchor\" href=\"#naming-conventions\">Naming conventions\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Use camelCase identifiers, with acronyms kept uppercase.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>instanceDocument\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>InstanceDocument\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>geoJSON\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Use \u003Ccode>instanceDocument\u003C\u002Fcode> as the canonical variable\u002Fparameter name for the document payload, and \u003Ccode>map\u003C\u002Fcode> for the injected MapLibre map.\u003C\u002Fp>\n\u003Cp>Prefer these canonical forms in source, tests, docs, and fixtures.\u003C\u002Fp>\n\u003Cp>For data-layer ordering, use these canonical rules in docs\u002Ftests\u002Ffixtures:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>data.layers\u003C\u002Fcode> is the canonical data envelope.\u003C\u002Fli>\n\u003Cli>layer input shape is \u003Ccode>{ type?: &quot;geojson&quot;, data }\u003C\u002Fcode> and runtime \u003Ccode>instance.data.addLayer(...)\u003C\u002Fcode> uses the same contract.\u003C\u002Fli>\n\u003Cli>data-layer stack order is top-first: \u003Ccode>layers[0]\u003C\u002Fcode> is visually on top.\u003C\u002Fli>\n\u003Cli>insertion position is below the first \u003Ccode>symbol\u003C\u002Fcode> layer already present in the injected map style (appended when none exists).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>For full data\u002FGeoJSON contract details, prefer \u003Ca href=\"\u002Fwaymark-js\u002Fdata\">\u003Ccode>docs\u002F5.data.md\u003C\u002Fcode>\u003C\u002Fa> rather than duplicating rules in multiple docs.\u003C\u002Fp>\n\u003Ch2 id=\"cross-module-sync-pattern-runtime-state-owned\">\u003Ca class=\"heading-anchor\" href=\"#cross-module-sync-pattern-runtime-state-owned\">Cross-module sync pattern (runtime-state owned)\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Use this pattern when runtime modules must stay in sync.\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Runtime state owns mutable state\u003C\u002Fstrong>\u003Cul>\n\u003Cli>Keep authoritative mutable state in \u003Ccode>src\u002Fruntime\u002Fstate\u002FcreateInstanceState.js\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>createInstanceCore(...)\u003C\u002Fcode> owns lifecycle orchestration and commands, not duplicate mutable state copies.\u003C\u002Fli>\n\u003Cli>Modules and adapters consume snapshots and never become a second source of truth.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Dispatch named commands\u003C\u002Fstrong>\u003Cul>\n\u003Cli>Route mutations through explicit dispatch paths (for example \u003Ccode>types.visibility.set\u003C\u002Fcode>, \u003Ccode>types.visibility.reset\u003C\u002Fcode>, \u003Ccode>debug.set\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>Runtime commands dispatch state changes, then adapters apply map-side effects.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Emit canonical state events\u003C\u002Fstrong>\u003Cul>\n\u003Cli>Every real state mutation emits \u003Ccode>waymark:state.changed\u003C\u002Fcode> plus a scoped state event (\u003Ccode>waymark:state.debug.changed\u003C\u002Fcode>, \u003Ccode>waymark:state.types.visibility.changed\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>No-op dispatches emit nothing.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Emit data-layer events only where useful\u003C\u002Fstrong>\u003Cul>\n\u003Cli>The GeoJSON module emits \u003Ccode>waymark:data.layer.added\u003C\u002Fcode>, \u003Ccode>waymark:data.layer.mounted\u003C\u002Fcode>, and \u003Ccode>waymark:data.layer.error\u003C\u002Fcode> for layer lifecycle observability.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Emit feature interaction events from layer observation\u003C\u002Fstrong>\u003Cul>\n\u003Cli>The feature interaction module emits \u003Ccode>waymark:feature.click\u003C\u002Fcode>, \u003Ccode>waymark:feature.hover\u003C\u002Fcode>, and \u003Ccode>waymark:feature.leave\u003C\u002Fcode> with \u003Ccode>{ id, feature, layerId, lngLat, originalEvent }\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>These are observability events only: they do not mutate runtime state, the map cursor, or the camera.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Guidance for future sync features:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>keep mutation logic in runtime core, not in map adapters\u003C\u002Fli>\n\u003Cli>keep commands idempotent and no-op on invalid\u002Funchanged input\u003C\u002Fli>\n\u003Cli>emit events only after state mutation succeeds\u003C\u002Fli>\n\u003Cli>prefer one aggregate event per mutation path over multiple partial events\u003C\u002Fli>\n\u003Cli>ensure \u003Ccode>instance.toJSON()\u003C\u002Fcode> reflects the same post-mutation state as emitted events\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"testing-strategy\">\u003Ca class=\"heading-anchor\" href=\"#testing-strategy\">Testing strategy\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ccode>tests\u002Fdocs\u002F*.test.js\u003C\u002Fcode> (Vitest + jsdom): API contract tests without WebGL.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fdocs\u002Fruntime\u002FcreateInstanceState.test.js\u003C\u002Fcode>: runtime-state module contract (\u003Ccode>getSnapshot\u003C\u002Fcode>, command dispatch, scoped\u002Fglobal state events, no-op behaviour).\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fdocs\u002Fdist.smoke.test.js\u003C\u002Fcode>: imports the built \u003Ccode>dist\u002Fwaymark.js\u003C\u002Fcode> with \u003Ccode>maplibre-gl\u003C\u002Fcode> mocked, asserting \u003Ccode>createInstance\u003C\u002Fcode> exists and constructs \u003Ccode>LngLatBounds\u003C\u002Fcode> through the mock. It skips cleanly when \u003Ccode>dist\u002F\u003C\u002Fcode> is absent and runs for real in \u003Ccode>test:ci\u003C\u002Fcode>, which builds first.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fbrowser\u002F*.test.js\u003C\u002Fcode> (Playwright, \u003Ccode>functional\u003C\u002Fcode> project): browser API and dev-page functional smoke. \u003Ccode>1.api.test.js\u003C\u002Fcode> covers feature interaction events (click\u002Fhover\u002Fleave detail, hover suppression, leave semantics, destroy cleanup) and the \u003Ccode>Screenshot scenes\u003C\u002Fcode> assertions, which mount the capture scenes and assert render evidence (feature counts, polygon framing, per-family filters) without writing pixels; its specs fetch the shared fixtures (\u003Ccode>\u002Fgeojson\u002Fmarkers.json\u003C\u002Fcode>, \u003Ccode>\u002Fgeojson\u002Ftrack-port-hardy-cape-scott.geojson\u003C\u002Fcode>, \u003Ccode>\u002Fgeojson\u002Fshapes.json\u003C\u002Fcode>, \u003Ccode>\u002Fgeojson\u002Fsample-point.geojson\u003C\u002Fcode>) from the dev server. \u003Ccode>2.development.test.js\u003C\u002Fcode> covers the dev page: initial render (including ODbL attribution visibility), adding a layer, the consumer-side property-inspector popup (title, image, property rows), the marker\u002Fline\u002Fshape image fallbacks (thumbnail → medium → large), line and shape popup anchoring at the click location, and type visibility toggling.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fbrowser\u002Fscreenshots.spec.js\u003C\u002Fcode> (Playwright, \u003Ccode>screenshots\u003C\u002Fcode> project): the seven committed captures, rendered under the fixed-point capture protocol — see \u003Ca href=\"#screenshots\">Screenshots\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fbrowser\u002Fhelpers\u002Fscenes.js\u003C\u002Fcode>: shared page-driven scene builders used by both the functional specs and the screenshots spec, so assertions and committed pixels exercise identical scenes.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fbrowser\u002Fhelpers\u002Ftest.js\u003C\u002Fcode>: Playwright \u003Ccode>test\u003C\u002Fcode>\u002F\u003Ccode>expect\u003C\u002Fcode> extended with the external-request guard — every test context blocks non-localhost http(s) requests and fails the test naming the offending URL.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fbrowser\u002Fhelpers\u002Fscreenshot.js\u003C\u002Fcode>: fixed-point capture helper (\u003Ccode>screenshot(page, name, selector?)\u003C\u002Fcode>), the settle gate (\u003Ccode>waitForSettled\u003C\u002Fcode>), the safe-output guard and \u003Ccode>SCREENSHOT_OUT_DIR\u003C\u002Fcode> redirection — see \u003Ca href=\"#screenshots\">Screenshots\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fdocs\u002Fbasemap\u002F*.test.js\u003C\u002Fcode>: unit tests pinning the vendored basemap fixture — tile enumeration, generator host policy, manifest integrity, and the serving middleware contract.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Ccode>npm run check:dist\u003C\u002Fcode> statically asserts the built output: \u003Ccode>dist\u002F\u003C\u002Fcode> contains only \u003Ccode>waymark.js\u003C\u002Fcode>, the bundle imports \u003Ccode>maplibre-gl\u003C\u002Fcode> as a bare external, and there is no base64 worker blob, no CSS and no Vue markers. \u003Ccode>test:ci\u003C\u002Fcode> runs it after \u003Ccode>npm run build\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Use jsdom tests to validate API contract behaviour, not as proof of non-browser runtime support. Browser tests are the runtime source of truth for instance behaviour.\u003C\u002Fp>\n\u003Ch2 id=\"dev-page-setup\">\u003Ca class=\"heading-anchor\" href=\"#dev-page-setup\">Dev page setup\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The development playground lives under \u003Ccode>dev\u002F\u003C\u002Fcode> as a vanilla ES module app (\u003Ccode>dev\u002Findex.html\u003C\u002Fcode>, \u003Ccode>dev\u002Fdev.js\u003C\u002Fcode>). Its data is the shared fixture set under \u003Ccode>tests\u002Ffixtures\u002F\u003C\u002Fcode> — the single source of truth for the dev harness and the browser tests. \u003Ccode>vite.config.dev.js\u003C\u002Fcode> points \u003Ccode>publicDir\u003C\u002Fcode> at \u003Ccode>tests\u002Ffixtures\u003C\u002Fcode>, so the dev server serves the north Vancouver Island demo fixtures at \u003Ccode>\u002F\u003C\u002Fcode>: \u003Ccode>route.json\u003C\u002Fcode> (config) and \u003Ccode>geojson\u002F\u003C\u002Fcode> data. \u003Ccode>dev\u002Fdev.js\u003C\u002Fcode> imports \u003Ccode>maplibre-gl\u002Fdist\u002Fmaplibre-gl.css\u003C\u002Fcode> (MapLibre requires its stylesheet for popup positioning), creates its own MapLibre map on \u003Ccode>#map\u003C\u002Fcode> (the vendored OpenGIS basemap style served at \u003Ccode>\u002Fbasemap\u002Fstyle.json\u003C\u002Fcode>, initial centre on Holberg, \u003Ccode>fadeDuration: 0\u003C\u002Fcode>), and injects it via \u003Ccode>createInstance({ map, document })\u003C\u002Fcode>, booting one instance from \u003Ccode>\u002Froute.json\u003C\u002Fcode> plus four GeoJSON layers (\u003Ccode>track-north-island.geojson\u003C\u002Fcode>, \u003Ccode>track-port-hardy-cape-scott.geojson\u003C\u002Fcode>, \u003Ccode>markers.json\u003C\u002Fcode>, \u003Ccode>shapes.json\u003C\u002Fcode>) from \u003Ccode>\u002Fgeojson\u002F\u003C\u002Fcode>. It also fetches \u003Ccode>\u002Fgeojson\u002Fsample-point.geojson\u003C\u002Fcode> for the add-layer control. The vendored style and the \u003Ccode>\u002F__basemap__\u002F**\u003C\u002Fcode> middleware make the dev harness fully offline — no live basemap requests (see \u003Ca href=\"#screenshots\">Screenshots\u003C\u002Fa>).\u003C\u002Fp>\n\u003Cfigure>\n\u003Cimg src=\"\u002Fwaymark-js\u002Fscreenshots\u002Fdev-page.png\" alt=\"Development page with the four initial fixture layers\">\n\u003Cfigcaption>Development page with the four initial fixture layers\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp>Before \u003Ccode>createInstance(...)\u003C\u002Fcode>, the harness registers an inline PNG data URL as the \u003Ccode>photo-marker\u003C\u002Fcode> image (\u003Ccode>map.loadImage\u003C\u002Fcode> + \u003Ccode>map.addImage\u003C\u002Fcode>), so the \u003Ccode>photo\u003C\u002Fcode> type&#39;s symbol layers resolve the icon from first mount. It also demonstrates the consumer-owned feature interaction pattern: a \u003Ccode>Popup\u003C\u002Fcode> property inspector on \u003Ccode>waymark:feature.click\u003C\u002Fcode>, and cursor changes on \u003Ccode>waymark:feature.hover\u003C\u002Fcode> \u002F \u003Ccode>waymark:feature.leave\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>The click inspector renders a title heading, a single \u003Ccode>&lt;img class=&quot;dev-popup__image&quot;&gt;\u003C\u002Fcode> when any image URL property is present, and the remaining properties as key\u002Fvalue rows. The image URL is picked in preference order \u003Ccode>image_thumbnail_url\u003C\u002Fcode> → \u003Ccode>image_medium_url\u003C\u002Fcode> → \u003Ccode>image_large_url\u003C\u002Fcode>, the image \u003Ccode>alt\u003C\u002Fcode> is the feature title (or \u003Ccode>Feature image\u003C\u002Fcode>), and all three \u003Ccode>image_*_url\u003C\u002Fcode> keys are omitted from the rows whenever an image is displayed. The popup anchors at \u003Ccode>feature.geometry.coordinates\u003C\u002Fcode> for Point geometry, and at the click \u003Ccode>lngLat\u003C\u002Fcode> for everything else. The harness emits no \u003Ccode>[waymark:dev]\u003C\u002Fcode> diagnostic logs during normal operation — interaction feedback lives in the inspector.\u003C\u002Fp>\n\u003Cp>Vite dependency pre-bundling breaks MapLibre&#39;s worker: MapLibre resolves its worker relative to its own module URL, and Vite&#39;s pre-bundled copy under \u003Ccode>node_modules\u002F.vite\u002Fdeps\u002F\u003C\u002Fcode> has no worker file alongside it, so GeoJSON source processing silently stalls (\u003Ccode>map.loaded()\u003C\u002Fcode> never becomes \u003Ccode>true\u003C\u002Fcode>). \u003Ccode>vite.config.dev.js\u003C\u002Fcode> sets \u003Ccode>optimizeDeps.exclude: [&quot;maplibre-gl&quot;]\u003C\u002Fcode> so the package is served as-is. Vite consumers of Waymark should do the same in their own config.\u003C\u002Fp>\n\u003Cp>\u003Ccode>dev\u002Findex.html\u003C\u002Fcode> renders a restrained toolbar — the Waymark JS title plus a &quot;click a feature to inspect its properties&quot; cue — with four grouped controls:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>#dev-add-layer\u003C\u002Fcode> calls \u003Ccode>instance.data.addLayer({ data: samplePoint }, { fitBounds: true })\u003C\u002Fcode>; \u003Ccode>samplePoint\u003C\u002Fcode> carries \u003Ccode>waymarkType: &quot;beer&quot;\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>#dev-fit-bounds\u003C\u002Fcode> calls \u003Ccode>instance.data.fitBounds()\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>#dev-toggle-beer\u003C\u002Fcode> toggles \u003Ccode>instance.types.setVisibility(&quot;beer&quot;, ...)\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>#dev-debug\u003C\u002Fcode> (checkbox) toggles \u003Ccode>instance.debug.setEnabled(...)\u003C\u002Fcode> console logging; it initialises checked from \u003Ccode>config.debug\u003C\u002Fcode> in \u003Ccode>route.json\u003C\u002Fcode> (\u003Ccode>true\u003C\u002Fcode>)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cfigure>\n\u003Cimg src=\"\u002Fwaymark-js\u002Fscreenshots\u002Fdev-layer-added.png\" alt=\"A layer added from the toolbar, with the view fitted to its bounds\">\n\u003Cfigcaption>A layer added from the toolbar, with the view fitted to its bounds\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp>Fixture notes:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>tests\u002Ffixtures\u002Froute.json\u003C\u002Fcode> gives every type a \u003Ccode>title\u003C\u002Fcode>, maps the \u003Ccode>photo\u003C\u002Fcode> type to \u003Ccode>icon: &quot;photo-marker&quot;\u003C\u002Fcode> with \u003Ccode>iconSize\u003C\u002Fcode>, defines the \u003Ccode>area\u003C\u002Fcode> fill type (\u003Ccode>fill-color: &quot;#2f855a&quot;\u003C\u002Fcode>), and enables debug logging.\u003C\u002Fli>\n\u003Cli>The GeoJSON fixtures under \u003Ccode>tests\u002Ffixtures\u002Fgeojson\u002F\u003C\u002Fcode> (\u003Ccode>markers.json\u003C\u002Fcode>, \u003Ccode>track-north-island.geojson\u003C\u002Fcode>, \u003Ccode>track-port-hardy-cape-scott.geojson\u003C\u002Fcode>, \u003Ccode>shapes.json\u003C\u002Fcode>, \u003Ccode>sample-point.geojson\u003C\u002Fcode>) carry \u003Ccode>waymarkType\u003C\u002Fcode> only — the legacy duplicated \u003Ccode>type\u003C\u002Fcode> property has been removed.\u003C\u002Fli>\n\u003Cli>\u003Ccode>shapes.json\u003C\u002Fcode> holds a single \u003Ccode>waymarkType: &quot;area&quot;\u003C\u002Fcode> Polygon (&quot;Cape Scott trail area&quot;) that carries only \u003Ccode>image_medium_url\u003C\u002Fcode>, exercising the medium fallback in the popup.\u003C\u002Fli>\n\u003Cli>\u003Ccode>sample-point.geojson\u003C\u002Fcode> sits in-scene at \u003Ccode>[-127.998, 50.661]\u003C\u002Fcode>; the add-layer browser test asserts that centre after \u003Ccode>fitBounds\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Holberg (the pub &quot;The Scarlet Ibis&quot; at \u003Ccode>[-128.009734, 50.653923]\u003C\u002Fcode>) is the demo-closeup anchor, and the big route line is \u003Ccode>track-port-hardy-cape-scott\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The page exposes \u003Ccode>window.waymarkInstance\u003C\u002Fcode>, \u003Ccode>window.waymarkInstances\u003C\u002Fcode> and \u003Ccode>window.waymarkMaps\u003C\u002Fcode> (both keyed by the dev map key \u003Ccode>map\u003C\u002Fcode>), and \u003Ccode>window.createWaymarkInstance\u003C\u002Fcode> for manual console testing.\u003C\u002Fp>\n\u003Cp>This gives a stable baseline for browser smoke coverage in \u003Ccode>tests\u002Fbrowser\u002F2.development.test.js\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>the toolbar controls, \u003Ccode>#map\u003C\u002Fcode>, and the map canvas render with zero console errors\u003C\u002Fli>\n\u003Cli>adding a layer increases \u003Ccode>data.layers\u003C\u002Fcode> and fits bounds to the sample point\u003C\u002Fli>\n\u003Cli>clicking a marker opens the property-inspector popup (title, image, property rows) and loads the \u003Ccode>image_thumbnail_url\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>clicking a track line opens the popup anchored at the click location and loads the \u003Ccode>image_large_url\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>clicking a shape opens the popup with its properties and loads the \u003Ccode>image_medium_url\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>image URL keys never render as raw URL text rows in any popup\u003C\u002Fli>\n\u003Cli>toggling the &quot;beer&quot; type hides and restores typed visibility\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Run:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"hljs language-bash\">npm run format:check\nnpm run docs:\u003Cspan class=\"hljs-built_in\">sync\u003C\u002Fspan>\nnpm run docs:generate\nnpm \u003Cspan class=\"hljs-built_in\">test\u003C\u002Fspan>\nnpm run build &amp;&amp; npm run check:dist\nnpm run \u003Cspan class=\"hljs-built_in\">test\u003C\u002Fspan>:browser\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"docs--tests-sync-protocol\">\u003Ca class=\"heading-anchor\" href=\"#docs--tests-sync-protocol\">Docs ↔ tests sync protocol\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Treat docs and tests as one contract. Change both in the same slice.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\u003Cth>Docs page\u003C\u002Fth>\u003Cth>Unit tests\u003C\u002Fth>\u003Cth>Browser tests\u003C\u002Fth>\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\u003Ctd>\u003Ccode>docs\u002F1.api.md\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>\u003Ccode>tests\u002Fdocs\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>\u003Ccode>tests\u002Fbrowser\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>docs\u002F3.instances.md\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Internal-boundary assertions in \u003Ccode>tests\u002Fdocs\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Dev smoke in \u003Ccode>tests\u002Fbrowser\u002F2.development.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>docs\u002F4.map.md\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Map injection assertions in \u003Ccode>tests\u002Fdocs\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Map API coverage in \u003Ccode>tests\u002Fbrowser\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>docs\u002F5.data.md\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Data-layer contract assertions in \u003Ccode>tests\u002Fdocs\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Data-layer API coverage in \u003Ccode>tests\u002Fbrowser\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>Automation guards:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>npm run docs:contract\u003C\u002Fcode> checks required docs headings and mapped test \u003Ccode>describe(...)\u003C\u002Fcode> blocks.\u003C\u002Fli>\n\u003Cli>\u003Ccode>npm run docs:api-sync\u003C\u002Fcode> checks API sections in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode> against generated source contract data.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Keep \u003Ca href=\"\u002Fwaymark-js\u002Fllms.txt\">\u003Ccode>docs\u002Fllms.txt\u003C\u002Fcode>\u003C\u002Fa>&#39;s links and descriptions in step when the docs set changes; \u003Ca href=\"\u002Fwaymark-js\u002Fllms-full.txt\">\u003Ccode>docs\u002Fllms-full.txt\u003C\u002Fcode>\u003C\u002Fa> regenerates from them via \u003Ccode>npm run docs:sync\u003C\u002Fcode> and is never hand-edited. After editing any doc linked from \u003Ccode>docs\u002Fllms.txt\u003C\u002Fcode>, run \u003Ccode>npm run docs:sync\u003C\u002Fcode> so the committed bundle stays fresh — otherwise \u003Ccode>npm run check:llms\u003C\u002Fcode> (and therefore \u003Ccode>test:ci\u003C\u002Fcode>) fails.\u003C\u002Fp>\n\u003Ch2 id=\"hard-sync-workflow-docs--source--tests\">\u003Ca class=\"heading-anchor\" href=\"#hard-sync-workflow-docs--source--tests\">Hard sync workflow (docs ↔ source ↔ tests)\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>npm run docs:sync\u003C\u002Fcode> is intentionally strict and should be treated as a hard gate before tests. CI runs \u003Ccode>npm run check:llms\u003C\u002Fcode> before regenerating, so a stale committed \u003Ccode>docs\u002Fllms-full.txt\u003C\u002Fcode> fails the gate instead of being silently overwritten.\u003C\u002Fp>\n\u003Cp>Execution order:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Ccode>node scripts\u002Fllms-full.mjs\u003C\u002Fcode>\u003Cul>\n\u003Cli>Reads the \u003Ccode>.md\u003C\u002Fcode> links from \u003Ccode>docs\u002Fllms.txt\u003C\u002Fcode> in order and regenerates \u003Ccode>docs\u002Fllms-full.txt\u003C\u002Fcode> (stripping frontmatter and api-contract markers).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Ccode>npm run docs:contract\u003C\u002Fcode>\u003Cul>\n\u003Cli>Reads \u003Ccode>scripts\u002Fdoc-test-contract.json\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Verifies required headings in tracked docs pages (\u003Ccode>docs\u002F1.api.md\u003C\u002Fcode>, \u003Ccode>docs\u002F4.map.md\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>Verifies mapped \u003Ccode>describe(...)\u003C\u002Fcode> block names in \u003Ccode>tests\u002Fdocs\u002F1.api.test.js\u003C\u002Fcode> and \u003Ccode>tests\u002Fbrowser\u002F1.api.test.js\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Ccode>npm run docs:api-sync\u003C\u002Fcode>\u003Cul>\n\u003Cli>Runs \u003Ccode>npm run api:contract\u003C\u002Fcode> first, generating \u003Ccode>docs\u002F.generated\u002Fapi-contract.json\u003C\u002Fcode> from \u003Ccode>src\u002Fruntime\u002FcreateInstanceEvents.js\u003C\u002Fcode> (event constants) and \u003Ccode>src\u002Fconfig\u002Fdefaults.js\u003C\u002Fcode> (\u003Ccode>defaultConfig\u003C\u002Fcode>), together with the fixed \u003Ccode>createInstance({ map, document? })\u003C\u002Fcode> signature.\u003C\u002Fli>\n\u003Cli>Compares marker blocks in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode> (\u003Ccode>signature\u003C\u002Fcode>, \u003Ccode>defaults\u003C\u002Fcode>, \u003Ccode>lifecycle-events\u003C\u002Fcode>, \u003Ccode>data-layer-events\u003C\u002Fcode>, \u003Ccode>feature-events\u003C\u002Fcode>) against generated contract values.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>If this gate fails, fix docs headings\u002Fmarker blocks and corresponding test \u003Ccode>describe(...)\u003C\u002Fcode> names in the same change.\u003C\u002Fp>\n\u003Ch2 id=\"adding-or-changing-api-sections-safely\">\u003Ca class=\"heading-anchor\" href=\"#adding-or-changing-api-sections-safely\">Adding or changing API sections safely\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Use this convention whenever you add, rename, or remove a section in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch3 id=\"api-section-naming\">\u003Ca class=\"heading-anchor\" href=\"#api-section-naming\">API section naming\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>Use stable heading text in sentence case (for example, \u003Ccode>## InstanceDocument shape\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>Treat each heading name as automation-backed contract text, not editorial copy.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"one-to-one-heading-and-describe-mapping\">\u003Ca class=\"heading-anchor\" href=\"#one-to-one-heading-and-describe-mapping\">One-to-one heading and \u003Ccode>describe(...)\u003C\u002Fcode> mapping\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>Each API section heading in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode> must have matching \u003Ccode>describe(...)\u003C\u002Fcode> blocks in:\u003Cul>\n\u003Cli>\u003Ccode>tests\u002Fdocs\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>tests\u002Fbrowser\u002F1.api.test.js\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>The \u003Ccode>describe(...)\u003C\u002Fcode> names must be identical to the heading text.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"manifest-updates\">\u003Ca class=\"heading-anchor\" href=\"#manifest-updates\">Manifest updates\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>When adding or renaming an API section, update \u003Ccode>scripts\u002Fdoc-test-contract.json\u003C\u002Fcode> in the same change:\u003Cul>\n\u003Cli>\u003Ccode>requiredHeadings\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>describeMappings\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"marker-blocks-for-source-derived-facts\">\u003Ca class=\"heading-anchor\" href=\"#marker-blocks-for-source-derived-facts\">Marker blocks for source-derived facts\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>Source-derived API facts in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode> must be wrapped in marker blocks:\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"hljs language-html\">...\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>Keep \u003Ccode>&lt;name&gt;\u003C\u002Fcode> stable once introduced.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"extending-source-derived-contract-fields\">\u003Ca class=\"heading-anchor\" href=\"#extending-source-derived-contract-fields\">Extending source-derived contract fields\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>If you add a new source-derived contract field, update all three in the same change:\u003Col>\n\u003Cli>\u003Ccode>scripts\u002Fgenerate-api-contract.mjs\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>scripts\u002Fcheck-api-doc-sync.mjs\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>The matching marker block in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"practical-checklist\">\u003Ca class=\"heading-anchor\" href=\"#practical-checklist\">Practical checklist\u003C\u002Fa>\u003C\u002Fh3>\n\u003Col>\n\u003Cli>Add or rename the API section heading in \u003Ccode>docs\u002F1.api.md\u003C\u002Fcode> (stable sentence case).\u003C\u002Fli>\n\u003Cli>Add or rename matching \u003Ccode>describe(...)\u003C\u002Fcode> blocks in both API test files with identical names.\u003C\u002Fli>\n\u003Cli>Update \u003Ccode>scripts\u002Fdoc-test-contract.json\u003C\u002Fcode> (\u003Ccode>requiredHeadings\u003C\u002Fcode> and \u003Ccode>describeMappings\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>If the section includes source-derived facts, add or update the \u003Ccode>api-contract\u003C\u002Fcode> marker block.\u003C\u002Fli>\n\u003Cli>If you introduced a new source-derived field, update both sync scripts and the marker block together.\u003C\u002Fli>\n\u003Cli>Run \u003Ccode>npm run format:check\u003C\u002Fcode> and \u003Ccode>npm run docs:sync\u003C\u002Fcode> before handing off.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Sync checklist:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Update the relevant docs headings and examples.\u003C\u002Fli>\n\u003Cli>Update matching \u003Ccode>describe(...)\u003C\u002Fcode> blocks and assertions.\u003C\u002Fli>\n\u003Cli>Run \u003Ccode>npm run docs:sync\u003C\u002Fcode>, \u003Ccode>npm test\u003C\u002Fcode>, and \u003Ccode>npm run test:browser\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Ensure old filenames\u002Fheadings are removed.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"screenshots\">\u003Ca class=\"heading-anchor\" href=\"#screenshots\">Screenshots\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The browser suite commits seven PNG screenshots as visual evidence — three API-page captures and four dev-page captures — under \u003Ccode>screenshots\u002F\u003C\u002Fcode> at the repo root (tracked in git and published with the docs site). Playwright splits them from the functional suite with two projects (\u003Ccode>playwright.config.js\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\u003Cth>Project\u003C\u002Fth>\u003Cth>Scope\u003C\u002Fth>\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\u003Ctd>\u003Ccode>functional\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>All assertions (\u003Ccode>1.api.test.js\u003C\u002Fcode>, \u003Ccode>2.development.test.js\u003C\u002Fcode>); renders no committed pixels\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>screenshots\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>The seven committed captures (\u003Ccode>screenshots.spec.js\u003C\u002Fcode>) under the pinned capture protocol\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cul>\n\u003Cli>\u003Ccode>npm run test:browser\u003C\u002Fcode> runs the \u003Ccode>functional\u003C\u002Fcode> project only.\u003C\u002Fli>\n\u003Cli>\u003Ccode>npm run screenshots\u003C\u002Fcode> regenerates the committed set into \u003Ccode>screenshots\u002F\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>npm run screenshots:verify\u003C\u002Fcode> regenerates into the throwaway \u003Ccode>test-results\u002Fscreenshots-verify\u002F\u003C\u002Fcode>, SHA-256-compares every file against the committed set (writing \u003Ccode>report.json\u003C\u002Fcode>), and exits non-zero on any mismatch, keeping the captures for inspection. It is deliberately not part of \u003Ccode>test:ci\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>A safe-output guard in \u003Ccode>tests\u002Fbrowser\u002Fhelpers\u002Fscreenshot.js\u003C\u002Fcode> refuses to write into the tracked \u003Ccode>screenshots\u002F\u003C\u002Fcode> directory from a headed run or the wrong project; setting \u003Ccode>SCREENSHOT_OUT_DIR\u003C\u002Fcode> redirects captures elsewhere (the verify script uses this).\u003C\u002Fp>\n\u003Ch3 id=\"capture-protocol\">\u003Ca class=\"heading-anchor\" href=\"#capture-protocol\">Capture protocol\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Capture is fixed-point, not timed. \u003Ccode>screenshot(page, name, selector?)\u003C\u002Fcode> first awaits the shared \u003Ccode>waitForSettled(page)\u003C\u002Fcode> gate — bounded waits for network idle, every registered \u003Ccode>window.waymarkMaps\u003C\u002Fcode> map to be idle with tiles loaded, every \u003Ccode>&lt;img&gt;\u003C\u002Fcode> complete and decoded, \u003Ccode>document.fonts.ready\u003C\u002Fcode>, and two animation frames. It then accepts a frame only when three consecutive lossless PNG frames are byte-identical. Each round forces a render (\u003Ccode>map.redraw()\u003C\u002Fcode> plus the \u003Ccode>render\u003C\u002Fcode> event, then two animation frames) and toggles a full-viewport magenta probe overlay: a frame that fails to reflect the probe proves the compositor served a stale surface, and the round is discarded. Rounds are bounded; on exhaustion the helper throws rather than writing an unstable image — no tolerance and no fallback.\u003C\u002Fp>\n\u003Cp>The scene builders in \u003Ccode>tests\u002Fbrowser\u002Fhelpers\u002Fscenes.js\u003C\u002Fcode> gate capture readiness: the dev page waits until every mounted Waymark symbol layer&#39;s \u003Ccode>icon-image\u003C\u002Fcode> is registered, and the API scenes poll until their features have painted and the map is idle. Both the functional specs and the screenshots spec reuse the same builders, so committed pixels and functional assertions cannot drift apart.\u003C\u002Fp>\n\u003Ch3 id=\"deterministic-rendering\">\u003Ca class=\"heading-anchor\" href=\"#deterministic-rendering\">Deterministic rendering\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>The \u003Ccode>screenshots\u003C\u002Fcode> project pins the render path in \u003Ccode>playwright.config.js\u003C\u002Fcode>: bundled-Chromium SwiftShader launch flags, locale \u003Ccode>en-CA\u003C\u002Fcode>, timezone \u003Ccode>America\u002FVancouver\u003C\u002Fcode>, \u003Ccode>colorScheme: &quot;light&quot;\u003C\u002Fcode>, \u003Ccode>reducedMotion: &quot;no-preference&quot;\u003C\u002Fcode>, a 1280×720 viewport and \u003Ccode>deviceScaleFactor: 1\u003C\u002Fcode>. Captured maps pass \u003Ccode>fadeDuration: 0\u003C\u002Fcode> so symbol\u002Ficon fade cannot settle mid-transition. MapLibre popups are stabilised against a compositor promotion quirk — the stylesheet&#39;s \u003Ccode>will-change: transform\u003C\u002Fcode> promotes the popup to its own compositing layer at a fractional offset, which resamples non-deterministically — by overriding it to \u003Ccode>auto\u003C\u002Fcode> before capture. Re-verify this on any \u003Ccode>maplibre-gl\u003C\u002Fcode> upgrade.\u003C\u002Fp>\n\u003Cp>A single-thread \u003Ccode>SwiftShader.ini\u003C\u002Fcode> pin is written by the Playwright global setup and removed by the teardown for invocations that include the \u003Ccode>screenshots\u003C\u002Fcode> project; functional-only runs never receive it. Keep it as a precaution, but note it was empirically inert on the baseline machine: byte stability comes from the capture protocol and harness fixes above, not from the pin.\u003C\u002Fp>\n\u003Ch3 id=\"offline-test-environment-vendored-basemap\">\u003Ca class=\"heading-anchor\" href=\"#offline-test-environment-vendored-basemap\">Offline test environment (vendored basemap)\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>The dev harness and both browser suites run fully offline. \u003Ccode>tests\u002Ffixtures\u002Fbasemap\u002F\u003C\u002Fcode> holds a generated, origin-free slice of the OpenGIS basemap: a canonical \u003Ccode>style.json\u003C\u002Fcode>, a \u003Ccode>site.manifest.json\u003C\u002Fcode> recording per-file provenance (source, bytes, sha256, licence note), and the \u003Ccode>site\u002F**\u003C\u002Fcode> asset tree. The style is served by the existing \u003Ccode>publicDir\u003C\u002Fcode> mapping at \u003Ccode>\u002Fbasemap\u002Fstyle.json\u003C\u002Fcode>; tiles, glyphs and sprite are served from \u003Ccode>\u002F__basemap__\u002F**\u003C\u002Fcode> by a small Vite middleware — a missing tile or glyph returns an empty 200 so MapLibre stays quiet, while a missing sprite returns a loud 404.\u003C\u002Fp>\n\u003Cp>\u003Ccode>npm run fixtures:refresh\u003C\u002Fcode> regenerates the fixture as a deliberate act: it fetches the live style from the canonical host, allow-lists committable hosts (aborting loudly on anything else), rewrites asset URLs to root-relative local form, inlines the tile template and zoom range (no runtime descriptor fetch), vendors glyph range \u003Ccode>0-255\u003C\u002Fcode> for the declared fontstacks plus the 1× sprite, records provenance in the manifest, and prunes stale files. The full z0–14 pyramid is kept by explicit decision, bounded by the demo-area envelope (the seven capture viewports plus one tile of padding) and a \u003Ccode>MAX_TILE_COUNT = 5000\u003C\u002Fcode> generator guard; the tree is around 22 MB. Attribution is preserved verbatim and carried from the TileJSON onto the inlined source — it must remain visible in rendered captures.\u003C\u002Fp>\n\u003Cp>The suite enforces the guarantee: every test context installs a context-level route (\u003Ccode>tests\u002Fbrowser\u002Fhelpers\u002Ftest.js\u003C\u002Fcode>) that blocks every non-localhost http(s) request and fails the test naming the offending URL, so a live basemap dependency cannot creep back in.\u003C\u002Fp>\n\u003Ch3 id=\"byte-identity-scope-and-re-baselining\">\u003Ca class=\"heading-anchor\" href=\"#byte-identity-scope-and-re-baselining\">Byte-identity scope and re-baselining\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Regeneration is byte-identical for the same macOS major version, CPU architecture and pinned configuration. Cross-platform (Linux) identity is explicitly not targeted; the macOS identity workflow is the environment-drift canary (see \u003Ca href=\"#cicd-automation\">CI\u002FCD automation\u003C\u002Fa>).\u003C\u002Fp>\n\u003Cp>Re-baselining is a deliberate act: run \u003Ccode>npm run screenshots\u003C\u002Fcode>, review the image diff, then run \u003Ccode>npm run screenshots:verify\u003C\u002Fcode>. A macOS major upgrade, a \u003Ccode>SwiftShader\u003C\u002Fcode>-affecting browser change, or a move of the identity workflow to another architecture is a re-baseline event, not a bug.\u003C\u002Fp>\n\u003Ch3 id=\"capture-set\">\u003Ca class=\"heading-anchor\" href=\"#capture-set\">Capture set\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>API captures (\u003Ccode>\u002Fbrowser-api.html\u003C\u002Fcode>) load the shared fixtures over the dev server (\u003Ccode>markers.json\u003C\u002Fcode>, \u003Ccode>track-port-hardy-cape-scott.geojson\u003C\u002Fcode>, \u003Ccode>shapes.json\u003C\u002Fcode>) and frame a Holberg closeup (centre \u003Ccode>[-128.0145, 50.6525]\u003C\u002Fcode>, zoom 14) on the 640×360 \u003Ccode>#map\u003C\u002Fcode> element so the point, line, and polygon all sit in frame.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\u003Cth>Screenshot\u003C\u002Fth>\u003Cth>What it shows\u003C\u002Fth>\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\u003Ctd>\u003Ccode>api-quick-start\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Injected map with a GeoJSON layer (demo markers) rendered on the vendored basemap\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>api-geojson-families\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Circle (point), line, and fill (polygon) geometry families rendered\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>api-geojson-mixed\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Point, line, and polygon sublayers from one mixed FeatureCollection\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>Dev-page captures (\u003Ccode>\u002F\u003C\u002Fcode>) render the full 1280×720 viewport:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\u003Cth>Screenshot\u003C\u002Fth>\u003Cth>What it shows\u003C\u002Fth>\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\u003Ctd>\u003Ccode>dev-page\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Default dev page state — toolbar, map, and the four initial fixture layers\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>dev-layer-added\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>GeoJSON layer added from the toolbar, bounds fitted to \u003Ccode>[-127.998, 50.661]\u003C\u002Fcode>\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>dev-popup\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Feature inspector popup open after clicking the &quot;The Scarlet Ibis&quot; marker\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Ccode>dev-popup-line\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>Line popup anchored at the click location, with the feature&#39;s image\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Ch2 id=\"docs-site\">\u003Ca class=\"heading-anchor\" href=\"#docs-site\">Docs site\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The docs site is a Nuxt static-site app that lives entirely under \u003Ccode>docs\u002F\u003C\u002Fcode>: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FOpenGIS\u002FWaymark-JS\u002Fblob\u002Fmaster\u002Fdocs\u002Fnuxt.config.js\">\u003Ccode>docs\u002Fnuxt.config.js\u003C\u002Fcode>\u003C\u002Fa> configures it, source lives in \u003Ccode>docs\u002Fapp\u002F\u003C\u002Fcode>, the build module in \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FOpenGIS\u002FWaymark-JS\u002Fblob\u002Fmaster\u002Fdocs\u002Fmodules\u002Fdocs-pages.js\">\u003Ccode>docs\u002Fmodules\u002Fdocs-pages.js\u003C\u002Fcode>\u003C\u002Fa>, and the internal \u003Ccode>\u002Fapi\u002Fdocs\u002F*\u003C\u002Fcode> routes in \u003Ccode>docs\u002Fserver\u002F\u003C\u002Fcode>. \u003Ccode>npm run docs:dev\u003C\u002Fcode> (\u003Ccode>nuxt dev docs\u003C\u002Fcode>) serves it locally for previewing prose changes; \u003Ccode>npm run docs:generate\u003C\u002Fcode> (\u003Ccode>nuxt generate docs\u003C\u002Fcode>) prerenders every route and writes the deployable site to the generated \u003Ccode>docs\u002F.output\u002Fpublic\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>At build time the module packages the site: it copies the tracked \u003Ccode>screenshots\u002F\u003C\u002Fcode> directory, \u003Ccode>llms.txt\u003C\u002Fcode>, \u003Ccode>llms-full.txt\u003C\u002Fcode> and \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FOpenGIS\u002FWaymark-JS\u002Fblob\u002Fmaster\u002Fdocs\u002Fassets\u002Fwaymark-logo.svg\">\u003Ccode>docs\u002Fassets\u002Fwaymark-logo.svg\u003C\u002Fcode>\u003C\u002Fa> (as the site favicon) into the generated \u003Ccode>docs\u002Fpublic\u002F\u003C\u002Fcode> (served verbatim), then renders each page once with \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FOpenGIS\u002FWaymark-JS\u002Fblob\u002Fmaster\u002Fdocs\u002Fapp\u002Flib\u002Fdocs-content.mjs\">\u003Ccode>docs\u002Fapp\u002Flib\u002Fdocs-content.mjs\u003C\u002Fcode>\u003C\u002Fa> into JSON assets under \u003Ccode>docs\u002Fserver\u002Fassets\u002Fdocs-pages\u002F\u003C\u002Fcode>, which the \u003Ccode>\u002Fapi\u002Fdocs\u002Fnav\u003C\u002Fcode> and \u003Ccode>\u002Fapi\u002Fdocs\u002F[slug]\u003C\u002Fcode> routes read. The \u003Ccode>docs\u002Fpublic\u002Fllms.txt\u003C\u002Fcode> and \u003Ccode>docs\u002Fpublic\u002Fllms-full.txt\u003C\u002Fcode> copies are tracked in version control for repository indexing; \u003Ccode>npm run docs:sync\u003C\u002Fcode> refreshes them and \u003Ccode>npm run check:llms\u003C\u002Fcode> verifies they match their \u003Ccode>docs\u002F\u003C\u002Fcode> sources.\u003C\u002Fp>\n\u003Cp>Pages, routes and the prev\u002Fnext pagers follow the markdown links in \u003Ca href=\"\u002Fwaymark-js\u002Fllms.txt\">\u003Ccode>docs\u002Fllms.txt\u003C\u002Fcode>\u003C\u002Fa>: \u003Ccode>README.md\u003C\u002Fcode> is the home page, and the numbered docs keep their reading order.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Pages render with \u003Ccode>marked\u003C\u002Fcode> (GitHub-flavoured markdown) and \u003Ccode>highlight.js\u003C\u002Fcode> at build time; content headings get self-links and GitHub alerts become branded callouts.\u003C\u002Fli>\n\u003Cli>Embedded screenshots render as semantic \u003Ccode>&lt;figure&gt;\u003C\u002Fcode> elements with descriptive captions — the markdown image title when present, otherwise the alt text.\u003C\u002Fli>\n\u003Cli>Fenced code blocks are syntax-highlighted at build time and get a client-side copy button; wide tables scroll inside their own wrappers.\u003C\u002Fli>\n\u003Cli>Relative \u003Ccode>.md\u003C\u002Fcode> links become site routes; other repo-relative links are rewritten to \u003Ccode>https:\u002F\u002Fgithub.com\u002FOpenGIS\u002FWaymark-JS\u002Fblob\u002Fmaster\u002F...\u003C\u002Fcode>; llms bundle links point at the site root.\u003C\u002Fli>\n\u003Cli>Layout and theming come from the hosted OpenGIS branding stylesheet plus the \u003Ccode>background-map.js\u003C\u002Fcode> header module; the favicon and header mark come from \u003Ccode>docs\u002Fassets\u002Fwaymark-logo.svg\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Site configuration is via environment variables, documented in \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FOpenGIS\u002FWaymark-JS\u002Fblob\u002Fmaster\u002Fdocs\u002F.env.example\">\u003Ccode>docs\u002F.env.example\u003C\u002Fcode>\u003C\u002Fa>; copy it to \u003Ccode>docs\u002F.env\u003C\u002Fcode> (gitignored) to override — values are read from the app root (\u003Ccode>docs\u002F\u003C\u002Fcode>) when the dev server starts or the site is generated, so restart or regenerate after changing them. \u003Ccode>NUXT_PUBLIC_PRODUCTION_URL\u003C\u002Fcode> is the single deployment value: it sets both the absolute site URL used for \u003Ccode>&lt;link rel=&quot;canonical&quot;&gt;\u003C\u002Fcode> and the asset base path (derived from its pathname; default \u003Ccode>https:\u002F\u002Fwww.ogis.org\u002Fwaymark-js\u003C\u002Fcode>, and the root path when unset). \u003Ccode>NUXT_PUBLIC_BRANDING_URL\u003C\u002Fcode> (default \u003Ccode>https:\u002F\u002Fwww.ogis.org\u002Fbranding\u003C\u002Fcode>) sets the two branding asset links in the head — the theme stylesheet and the \u003Ccode>background-map.js\u003C\u002Fcode> module — and nothing else: the logo is a local asset and the Waymark v1 dependencies are resolved inside the hosted module.\u003C\u002Fp>\n\u003Cp>\u003Ccode>docs\u002Fassets\u002F\u003C\u002Fcode> holds the hand-authored logo SVG. SVG assets are formatted via \u003Ccode>@prettier\u002Fplugin-xml\u003C\u002Fcode>, registered in \u003Ccode>.prettierrc.json\u003C\u002Fcode>, so the logo is covered by \u003Ccode>npm run format:check\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>The \u003Cstrong>Deploy docs\u003C\u002Fstrong> workflow (\u003Ccode>.github\u002Fworkflows\u002Fdocs.yml\u003C\u002Fcode>) publishes the site: pushes to \u003Ccode>master\u003C\u002Fcode> touching \u003Ccode>docs\u002F**\u003C\u002Fcode>, \u003Ccode>screenshots\u002F**\u003C\u002Fcode>, \u003Ccode>scripts\u002Fllms-full.mjs\u003C\u002Fcode>, \u003Ccode>package.json\u003C\u002Fcode>\u002F\u003Ccode>package-lock.json\u003C\u002Fcode> or the workflow itself — plus manual \u003Ccode>workflow_dispatch\u003C\u002Fcode> — run \u003Ccode>npm run check:llms\u003C\u002Fcode>, configure Pages (whose \u003Ccode>base_url\u003C\u002Fcode> output supplies the deploy target), then \u003Ccode>npm run docs:generate\u003C\u002Fcode> on Node 24 (\u003Ccode>.nvmrc\u003C\u002Fcode>) with \u003Ccode>NUXT_PUBLIC_PRODUCTION_URL\u003C\u002Fcode> derived at deploy time from that output — it drives both the canonical URLs and the asset paths — and upload \u003Ccode>docs\u002F.output\u002Fpublic\u003C\u002Fcode> via GitHub Pages. One-time setup: Settings → Pages → Source = &quot;GitHub Actions&quot;. Published at \u003Ca href=\"https:\u002F\u002Fwww.ogis.org\u002Fwaymark-js\u002F\">https:\u002F\u002Fwww.ogis.org\u002Fwaymark-js\u002F\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"cicd-automation\">\u003Ca class=\"heading-anchor\" href=\"#cicd-automation\">CI\u002FCD automation\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Four GitHub Actions workflows automate the release pipeline:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>CI\u003C\u002Fstrong> (\u003Ccode>.github\u002Fworkflows\u002Fci.yml\u003C\u002Fcode>) — runs \u003Ccode>npm run test:ci\u003C\u002Fcode> on PRs and pushes to \u003Ccode>master\u003C\u002Fcode>. The browser leg is the \u003Ccode>functional\u003C\u002Fcode> project only: it renders no committed pixels and the suite has no live basemap dependency.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Release\u003C\u002Fstrong> (\u003Ccode>.github\u002Fworkflows\u002Frelease.yml\u003C\u002Fcode>) — runs on every push to \u003Ccode>master\u003C\u002Fcode>; when the current \u003Ccode>package.json\u003C\u002Fcode> version has no existing git tag, it creates the tag and a GitHub Release with auto-generated release notes.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Deploy docs\u003C\u002Fstrong> (\u003Ccode>.github\u002Fworkflows\u002Fdocs.yml\u003C\u002Fcode>) — builds the Nuxt docs site and deploys it to GitHub Pages on \u003Ccode>master\u003C\u002Fcode> pushes that touch docs inputs (see \u003Ca href=\"#docs-site\">Docs site\u003C\u002Fa>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Screenshots identity\u003C\u002Fstrong> (\u003Ccode>.github\u002Fworkflows\u002Fscreenshots.yml\u003C\u002Fcode>) — runs \u003Ccode>npm run screenshots:verify\u003C\u002Fcode> on \u003Ccode>macos-15-intel\u003C\u002Fcode> (the baseline machine&#39;s architecture) for path-filtered pushes to \u003Ccode>master\u003C\u002Fcode> and manual dispatch, and uploads \u003Ccode>test-results\u002Fscreenshots-verify\u002F\u003C\u002Fcode> on failure. It is the environment-drift canary: if the report shows all files differing, suspect environment drift; a subset differing more likely indicates a genuine rendering change. \u003Ccode>screenshots:verify\u003C\u002Fcode> is deliberately not part of \u003Ccode>test:ci\u003C\u002Fcode>. Note the runner caveat: GitHub&#39;s x86_64 macOS images are deprecated upstream and retire in August 2027; moving the job to arm64 will be a deliberate re-baseline event (see \u003Ca href=\"#byte-identity-scope-and-re-baselining\">Byte-identity scope and re-baselining\u003C\u002Fa>).\u003C\u002Fli>\n\u003C\u002Ful>\n",{"route":8,"label":9},{"route":14,"label":15},1792013384663]