diff --git a/docs/renderer-parity.md b/docs/renderer-parity.md
index cad8797..4534ffc 100644
--- a/docs/renderer-parity.md
+++ b/docs/renderer-parity.md
@@ -12,7 +12,7 @@ Android/Kotlin. Repixelizer is the first style-parity target.
| Renderer | Status | Proof | Gap |
| --- | --- | --- | --- |
-| Web reference | First generic app target | `web/` renders live VoidBot and fixture surfaces | Needs provider picker, Repixelizer fixture, visual regression fixtures, and command round-trip tests |
+| Web reference | First generic app target | `web/` renders live VoidBot and fixture surfaces through the provider picker | Needs visual regression fixtures and command round-trip tests |
| iOS / UIKit | Native proof | Renders Odin's fullscreen interface wall from `surface.root`, generic provider trees, and a custom VoidBot cockpit with avatar images | Needs provider picker, Repixelizer style token lowering, and visual parity fixtures |
| Android / Kotlin | Device-edge proof | APK builds and shows broker/sensor status | Needs provider picker, full surface tree rendering, style token lowering, asset image lowering, and command controls |
| Fensalir Direct2D | Documented landing zone | Existing `AquariumUiDocument` and `DirectWriteOverlay` path | Needs adapter from Eve surface document to `AquariumUiDocument` and DirectWrite/Direct2D token lowering |
@@ -20,19 +20,22 @@ Android/Kotlin. Repixelizer is the first style-parity target.
## Spawned Surfaces
-The browser reference runner currently exposes hardcoded fixture/live tabs:
+The browser reference runner now exposes local advertisements through the
+provider picker:
- `VoidBot Live`: connects to Mimir's `/eve/deck` broker and opens
`voidbot.swarm`.
+- `Repixelizer`: loads `web/fixtures/repixelizer.provider-advertisement.json`
+ and `web/fixtures/repixelizer.eve-surface.json`.
- `Fensalir Direct2D`: loads `web/fixtures/fensalir-client-surface.json`, a
recorded surface describing the Direct2D client lowering path.
- `Sai VN Surface`: loads `web/fixtures/sai-vn-surface.json`.
- `Huginn .cc`: compiles `web/fixtures/huginn-cc-surface.eve`.
- `Reactive DSL`: compiles `web/fixtures/reactive-composition.eve`.
-Those tabs are temporary probes. The runtime app target is a provider picker
-fed by `gamecult.eve.provider_advertisement.v1`, with fixtures represented as
-local advertisements.
+The remaining temporary part is the local provider list itself. The runtime app
+target is the same picker fed by live `gamecult.eve.provider_advertisement.v1`
+documents through CultMesh/Odin.
Start it with:
diff --git a/web/fixtures/repixelizer.eve-surface.json b/web/fixtures/repixelizer.eve-surface.json
new file mode 100644
index 0000000..ce73297
--- /dev/null
+++ b/web/fixtures/repixelizer.eve-surface.json
@@ -0,0 +1,558 @@
+{
+ "commands": [
+ {
+ "authority": "browser-lowering",
+ "command": "repixelizer.web.open",
+ "method": "GET",
+ "route": "/app/",
+ "surfaceId": "repixelizer.operator.surface",
+ "transport": "http"
+ },
+ {
+ "authority": "repixelizer-route-policy",
+ "command": "repixelizer.job.submit",
+ "method": "POST",
+ "route": "/api/jobs",
+ "surfaceId": "repixelizer.operator.surface",
+ "transport": "http"
+ },
+ {
+ "authority": "repixelizer-job-owner",
+ "command": "repixelizer.job.cancel_own",
+ "method": "DELETE",
+ "route": "/api/jobs/{job_id}",
+ "surfaceId": "repixelizer.operator.surface",
+ "transport": "http"
+ }
+ ],
+ "loweringComparison": {
+ "periwinkleAndroidKotlin": {
+ "keeps": [
+ "surface root traversal",
+ "pane/card/text hierarchy",
+ "TextView body content",
+ "basic ProgressBar metric rendering",
+ "node-bound command taps"
+ ],
+ "losesUntilRendererParity": [
+ "Press Start 2P and VT323 font selection",
+ "Repixelizer shell frame, scanlines, pixel grid, corner accents, and chunky border widths",
+ "landing/app image assets; current Android lowerer prints asset refs when images are not explicitly implemented",
+ "upload picker, comparison canvas, pan/zoom inspection, eyedropper, and paint controls",
+ "provider style tokens; current Kotlin toneColor/textColorFor/textSizeFor are hard-coded"
+ ],
+ "minimumParityCuts": [
+ "map surface.styles.tokens before toneColor/textColorFor/textSizeFor",
+ "add packaged pixel fonts or font download cache",
+ "render image assetUri/assetRef as ImageView with pixelated sampling",
+ "lower command controls to Android upload/document picker and job route calls"
+ ],
+ "source": "E:\\Projects\\Eve\\android\\app\\src\\main\\java\\org\\gamecult\\eve\\MainActivity.kt",
+ "status": "surface-tree-renderer-present-style-token-gap"
+ }
+ },
+ "providerId": "repixelizer",
+ "providerKind": "repixelizer.product",
+ "runtime": {
+ "auth": {
+ "appSlug": "repixelizer",
+ "enabled": false,
+ "loginUrl": null,
+ "logoutUrl": null,
+ "mode": "off",
+ "protectQueue": false,
+ "required": false
+ },
+ "hostedDemo": false,
+ "limits": {
+ "defaultSteps": 48,
+ "heartbeatIntervalSeconds": 10,
+ "maxInputDimension": 4096,
+ "maxOutputDimension": 1024,
+ "maxSteps": 256,
+ "maxUploadBytes": 16777216,
+ "queueCapacity": 32,
+ "staleAfterSeconds": 30
+ },
+ "queue": {
+ "activeStatus": null,
+ "hasActiveJob": false,
+ "queueCapacity": 32,
+ "queueDepth": 0,
+ "waitingCount": 0
+ }
+ },
+ "schema": "gamecult.eve.surface.v1",
+ "surface": {
+ "id": "repixelizer.operator.surface",
+ "root": {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.hero.copy",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "Force fake pixel art back onto a real grid, then inspect each solver step without sanding off the crunchy old-machine charm."
+ },
+ "role": "body",
+ "text": "Force fake pixel art back onto a real grid, then inspect each solver step without sanding off the crunchy old-machine charm."
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.hero.open.body",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "Browser lowering remains the full interactive app until native Eve gains upload and canvas editing parity."
+ },
+ "role": "body",
+ "text": "Browser lowering remains the full interactive app until native Eve gains upload and canvas editing parity."
+ },
+ {
+ "children": [],
+ "id": "repixelizer.hero.open.route",
+ "kind": "text",
+ "props": {
+ "method": "GET",
+ "role": "mono",
+ "route": "/app/",
+ "text": "GET /app/"
+ },
+ "role": "mono",
+ "text": "GET /app/"
+ }
+ ],
+ "commandId": "repixelizer.web.open",
+ "id": "repixelizer.hero.open",
+ "kind": "card",
+ "props": {
+ "commandId": "repixelizer.web.open",
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Open demo",
+ "transport": {
+ "kind": "http",
+ "method": "GET",
+ "route": "/app/"
+ }
+ },
+ "role": "group",
+ "style": {
+ "tone": "cool",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Open demo"
+ }
+ ],
+ "id": "repixelizer.hero",
+ "kind": "pane",
+ "layout": {
+ "direction": "vertical",
+ "padding": 10
+ },
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Repixelizer hosted demo"
+ },
+ "role": "section",
+ "style": {
+ "tone": "warm",
+ "variant": "repixelizer-retro-pane"
+ },
+ "text": "Repixelizer hosted demo"
+ },
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.workflow.submit.body",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "Submit a PNG/JPEG/WebP/GIF into the GUI job queue; queue ownership stays in repixelizer.gui."
+ },
+ "role": "body",
+ "text": "Submit a PNG/JPEG/WebP/GIF into the GUI job queue; queue ownership stays in repixelizer.gui."
+ },
+ {
+ "children": [],
+ "id": "repixelizer.workflow.submit.route",
+ "kind": "text",
+ "props": {
+ "method": "POST",
+ "role": "mono",
+ "route": "/api/jobs",
+ "text": "POST /api/jobs"
+ },
+ "role": "mono",
+ "text": "POST /api/jobs"
+ }
+ ],
+ "commandId": "repixelizer.job.submit",
+ "id": "repixelizer.workflow.submit",
+ "kind": "card",
+ "props": {
+ "commandId": "repixelizer.job.submit",
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Upload image",
+ "transport": {
+ "kind": "http",
+ "method": "POST",
+ "route": "/api/jobs"
+ }
+ },
+ "role": "group",
+ "style": {
+ "tone": "cool",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Upload image"
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.inspect.source",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "source, edge map, lattice overlay, phase field, cleaned output"
+ },
+ "role": "body",
+ "text": "source, edge map, lattice overlay, phase field, cleaned output"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.inspect.canvas",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "comparison canvas and editor lower to native only after Periwinkle gets image/control parity"
+ },
+ "role": "body",
+ "text": "comparison canvas and editor lower to native only after Periwinkle gets image/control parity"
+ }
+ ],
+ "id": "repixelizer.workflow.inspect",
+ "kind": "card",
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Inspect solver"
+ },
+ "role": "group",
+ "style": {
+ "tone": "panel",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Inspect solver"
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.workflow.cancel.body",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "The command intent delegates to the existing owner-bound DELETE route."
+ },
+ "role": "body",
+ "text": "The command intent delegates to the existing owner-bound DELETE route."
+ },
+ {
+ "children": [],
+ "id": "repixelizer.workflow.cancel.route",
+ "kind": "text",
+ "props": {
+ "method": "DELETE",
+ "role": "mono",
+ "route": "/api/jobs/{job_id}",
+ "text": "DELETE /api/jobs/{job_id}"
+ },
+ "role": "mono",
+ "text": "DELETE /api/jobs/{job_id}"
+ }
+ ],
+ "commandId": "repixelizer.job.cancel_own",
+ "id": "repixelizer.workflow.cancel",
+ "kind": "card",
+ "props": {
+ "commandId": "repixelizer.job.cancel_own",
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Cancel own job",
+ "transport": {
+ "kind": "http",
+ "method": "DELETE",
+ "route": "/api/jobs/{job_id}"
+ }
+ },
+ "role": "group",
+ "style": {
+ "tone": "cool",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Cancel own job"
+ }
+ ],
+ "id": "repixelizer.workflow",
+ "kind": "pane",
+ "layout": {
+ "direction": "vertical",
+ "padding": 10
+ },
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Single page app"
+ },
+ "role": "section",
+ "style": {
+ "tone": "warm",
+ "variant": "repixelizer-retro-pane"
+ },
+ "text": "Single page app"
+ },
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.runtime.hosted",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "hostedDemo: False"
+ },
+ "role": "mono",
+ "text": "hostedDemo: False"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.runtime.steps",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "defaultSteps: 48 maxSteps: 256"
+ },
+ "role": "mono",
+ "text": "defaultSteps: 48 maxSteps: 256"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.runtime.dimensions",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "maxOutputDimension: 1024px"
+ },
+ "role": "mono",
+ "text": "maxOutputDimension: 1024px"
+ }
+ ],
+ "id": "repixelizer.runtime.config",
+ "kind": "card",
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Runtime"
+ },
+ "role": "group",
+ "style": {
+ "tone": "panel",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Runtime"
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.queue.depth",
+ "kind": "text",
+ "props": {
+ "role": "strong",
+ "text": "0 active or waiting / 32 capacity"
+ },
+ "role": "strong",
+ "text": "0 active or waiting / 32 capacity"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.queue.waiting",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "waitingCount: 0"
+ },
+ "role": "mono",
+ "text": "waitingCount: 0"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.queue.active",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "hasActiveJob: False"
+ },
+ "role": "mono",
+ "text": "hasActiveJob: False"
+ }
+ ],
+ "id": "repixelizer.runtime.queue",
+ "kind": "card",
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Queue"
+ },
+ "role": "group",
+ "style": {
+ "tone": "panel",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Queue"
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "id": "repixelizer.auth.mode",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "mode: off"
+ },
+ "role": "mono",
+ "text": "mode: off"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.auth.required",
+ "kind": "text",
+ "props": {
+ "role": "mono",
+ "text": "required: False"
+ },
+ "role": "mono",
+ "text": "required: False"
+ },
+ {
+ "children": [],
+ "id": "repixelizer.auth.providers",
+ "kind": "text",
+ "props": {
+ "role": "body",
+ "text": "providers: none"
+ },
+ "role": "body",
+ "text": "providers: none"
+ }
+ ],
+ "id": "repixelizer.runtime.auth",
+ "kind": "card",
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Access"
+ },
+ "role": "group",
+ "style": {
+ "tone": "panel",
+ "variant": "repixelizer-retro-card"
+ },
+ "text": "Access"
+ }
+ ],
+ "id": "repixelizer.runtime",
+ "kind": "pane",
+ "layout": {
+ "direction": "vertical",
+ "padding": 10
+ },
+ "props": {
+ "styleProfile": "repixelizer.retro.pixel",
+ "title": "Live projection"
+ },
+ "role": "section",
+ "style": {
+ "tone": "warm",
+ "variant": "repixelizer-retro-pane"
+ },
+ "text": "Live projection"
+ }
+ ],
+ "id": "repixelizer.surface.root",
+ "kind": "surface",
+ "layout": {
+ "direction": "vertical",
+ "padding": 14
+ },
+ "props": {
+ "browserLowering": "https://repixelizer.gamecult.org/app/",
+ "frontPage": "https://repixelizer.gamecult.org/",
+ "styleProfile": "repixelizer.retro.pixel",
+ "subtitle": "Fake pixel art goes in. Real grid-aligned sprite art comes out.",
+ "title": "Repixelizer"
+ },
+ "role": "root",
+ "style": {
+ "tone": "warm",
+ "variant": "repixelizer-retro-shell"
+ },
+ "text": "Repixelizer"
+ },
+ "schema": "gamecult.eve.surface.v1",
+ "styles": {
+ "assets": {
+ "appUrl": "https://repixelizer.gamecult.org/app/",
+ "fontCss": "https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap",
+ "landingInput": "https://repixelizer.gamecult.org/app/landing-assets/character-input.png",
+ "landingOutput": "https://repixelizer.gamecult.org/app/landing-assets/character-repixelized.png"
+ },
+ "loweringHints": {
+ "android-native": "Load packaged or downloadable Press Start 2P and VT323 fonts, map style tokens before hard-coded tones, and render asset images instead of asset labels.",
+ "css": "Use the tokens as CSS custom properties and retain scanline/pixel-grid overlays.",
+ "tui": "Preserve title/body font distinction as role metadata; canvas editing lowers to artifact refs and command intents only."
+ },
+ "profile": "repixelizer.retro.pixel",
+ "tokens": {
+ "borderBottomWidthPx": 5,
+ "borderWidthPx": 4,
+ "buttonGlyph": ">",
+ "colorAccent": "#ffd84a",
+ "colorAccentStrong": "#ffae1a",
+ "colorBackgroundBottom": "#01050d",
+ "colorBackgroundMid": "#020813",
+ "colorBackgroundTop": "#01040b",
+ "colorCornerAccent": "#ffb72a",
+ "colorMuted": "#d3bb7f",
+ "colorPanel": "#0a2239",
+ "colorPanelAlt": "#061627",
+ "colorPanelBorder": "#0a2740",
+ "colorPanelBorderDeep": "#06192a",
+ "colorPixelGrid": "rgba(0, 0, 0, 0.3)",
+ "colorShadow": "rgba(0, 0, 0, 0.55)",
+ "colorShellSurfaceBottom": "#030b16",
+ "colorShellSurfaceTop": "#061424",
+ "colorText": "#efe5c9",
+ "colorTextBright": "#f7efd8",
+ "cornerAccentPx": 12,
+ "dashedInnerFrame": true,
+ "fontBody": "\"VT323\", \"Courier New\", monospace",
+ "fontMono": "\"VT323\", \"Courier New\", monospace",
+ "fontTitle": "\"Press Start 2P\", \"VT323\", monospace",
+ "imageRendering": "pixelated",
+ "scanlineOverlay": true
+ }
+ },
+ "title": "Repixelizer"
+ },
+ "title": "Repixelizer",
+ "type": "surface-state",
+ "updatedAt": "2026-06-04T00:00:00Z",
+ "version": 1
+}
diff --git a/web/fixtures/repixelizer.provider-advertisement.json b/web/fixtures/repixelizer.provider-advertisement.json
new file mode 100644
index 0000000..74cf930
--- /dev/null
+++ b/web/fixtures/repixelizer.provider-advertisement.json
@@ -0,0 +1,198 @@
+{
+ "commands": [
+ {
+ "authority": "repixelizer-route-policy",
+ "command": "repixelizer.job.submit",
+ "method": "POST",
+ "result": "accepted-denied-or-queued",
+ "route": "/api/jobs",
+ "surfaceId": "repixelizer.web.app",
+ "transport": "http"
+ },
+ {
+ "authority": "repixelizer-job-owner",
+ "command": "repixelizer.job.cancel_own",
+ "method": "DELETE",
+ "result": "accepted-denied-or-not-found",
+ "route": "/api/jobs/{job_id}",
+ "surfaceId": "repixelizer.web.app",
+ "transport": "http"
+ }
+ ],
+ "contacts": [
+ {
+ "kind": "repo",
+ "path": "E:\\Projects\\repixelizer"
+ }
+ ],
+ "freshness": {
+ "lastSeenAt": "2026-06-04T00:00:00Z",
+ "maxAgeMs": 15000,
+ "state": "fresh"
+ },
+ "kind": "service.product",
+ "nestedVerses": [
+ {
+ "authorityBoundary": "repixelizer-local-session-or-heimdall-account",
+ "carryRules": {
+ "commands": "repixelizer-route-policy-only",
+ "identity": "heimdall-claims-projected-when-enabled"
+ },
+ "kind": "session-space",
+ "parentVerseId": "gamecult.local",
+ "stateSchemas": [
+ "repixelizer.job.v0",
+ "repixelizer.job_event.v0",
+ "repixelizer.auth_projection.v0"
+ ],
+ "surfaceIds": [
+ "repixelizer.web.app"
+ ],
+ "verseId": "repixelizer.session.{sessionId}"
+ },
+ {
+ "authorityBoundary": "read-only-first-cut",
+ "kind": "hosted-operator-space",
+ "parentVerseId": "gamecult.local",
+ "stateSchemas": [
+ "repixelizer.job.v0",
+ "repixelizer.queue_snapshot.v0",
+ "repixelizer.job_event.v0",
+ "repixelizer.runtime_config.v0",
+ "repixelizer.auth_projection.v0"
+ ],
+ "surfaceIds": [
+ "repixelizer.operator.surface"
+ ],
+ "verseId": "repixelizer.operator"
+ }
+ ],
+ "providerId": "repixelizer",
+ "runtime": {
+ "auth": {
+ "enabled": false,
+ "mode": "off",
+ "protectQueue": false,
+ "providers": [],
+ "required": false
+ },
+ "hostedDemo": false,
+ "queueCapacity": 32,
+ "showQueuePanel": false,
+ "spoolPath": "C:\\Users\\Meta\\AppData\\Local\\Temp\\repixelizer-gui-spool"
+ },
+ "schema": "gamecult.eve.provider_advertisement.v1",
+ "schemas": [
+ {
+ "authority": "accepted",
+ "description": "Job id, owner binding, status, timestamps, input/output artifact refs, error summary, and retention state.",
+ "owner": "repixelizer.gui",
+ "portable": true,
+ "schema": "repixelizer.job.v0",
+ "storage": "memory-dev-only-with-cc-witness-reserved"
+ },
+ {
+ "authority": "accepted",
+ "description": "Queue capacity, waiting/running counts, active job ids, oldest waiting age, and hosted mode.",
+ "owner": "repixelizer.gui",
+ "portable": true,
+ "schema": "repixelizer.queue_snapshot.v0",
+ "storage": "memory-dev-only-with-cc-witness-reserved"
+ },
+ {
+ "authority": "redacted-projection",
+ "description": "Redacted progress event, stage, index, timestamp, and job id.",
+ "owner": "repixelizer.gui",
+ "portable": true,
+ "schema": "repixelizer.job_event.v0",
+ "storage": "memory-dev-only-with-cc-witness-reserved"
+ },
+ {
+ "authority": "accepted",
+ "description": "Hosted flags, queue limits, visible UI flags, solver config hash placeholder, deployment id, and spool path.",
+ "owner": "repixelizer.gui",
+ "portable": true,
+ "schema": "repixelizer.runtime_config.v0",
+ "storage": "memory-dev-only-with-cc-witness-reserved"
+ },
+ {
+ "authority": "external-authority-projection",
+ "description": "Auth mode, provider availability, subject capability summary, and Heimdall claim freshness without raw tokens.",
+ "owner": "repixelizer.access",
+ "portable": true,
+ "schema": "repixelizer.auth_projection.v0",
+ "storage": "heimdall-projection-with-cc-witness-reserved"
+ }
+ ],
+ "serviceId": "repixelizer.service",
+ "styleCapabilities": [
+ {
+ "lossiness": {
+ "read-only-first-cut": "operator surface names state and routes but does not own queue truth",
+ "tui": "no-pixel-canvas-editing; status, queue, artifact refs, and command intents only"
+ },
+ "preferredLowerings": [
+ "css",
+ "android-native",
+ "native-eve",
+ "tui"
+ ],
+ "styleProfile": "repixelizer.product",
+ "tokenGroups": [
+ "repixelizer.retro.pixel",
+ "repixelizer.comparisonCanvas",
+ "repixelizer.cleanupTool",
+ "repixelizer.queuePanel",
+ "repixelizer.operatorStatus"
+ ]
+ }
+ ],
+ "surfaces": [
+ {
+ "audience": "operator",
+ "commands": [
+ "repixelizer.web.open",
+ "repixelizer.job.submit",
+ "repixelizer.job.cancel_own"
+ ],
+ "key": "cultmesh://repixelizer/surfaces/operator",
+ "mode": "read-only-first-cut",
+ "schema": "gamecult.eve.surface.v1",
+ "status": "available",
+ "styleProfile": "repixelizer.retro.pixel",
+ "surfaceId": "repixelizer.operator.surface",
+ "transport": "http-json",
+ "url": "https://repixelizer.gamecult.org/eve/surface"
+ },
+ {
+ "canonical": false,
+ "canonicalSurfaceId": "repixelizer.operator.surface",
+ "schema": "gamecult.eve.surface.v1",
+ "surfaceId": "repixelizer.web.app",
+ "transport": "browser-lowering",
+ "url": "https://repixelizer.gamecult.org/app/"
+ }
+ ],
+ "title": "Repixelizer",
+ "updatedAt": "2026-06-04T00:00:00Z",
+ "verseId": "gamecult.local",
+ "witnesses": [
+ {
+ "freshness": {
+ "state": "planned",
+ "updatedAt": "2026-06-04T00:00:00Z"
+ },
+ "id": "repixelizer.hosted.witness",
+ "kind": "cc-export-path-reserved",
+ "path": "state/repixelizer.witness.cc",
+ "redaction": "bulk-images-raw-tokens-and-private-claims-removed",
+ "schemas": [
+ "repixelizer.job.v0",
+ "repixelizer.queue_snapshot.v0",
+ "repixelizer.job_event.v0",
+ "repixelizer.runtime_config.v0",
+ "repixelizer.auth_projection.v0"
+ ]
+ }
+ ]
+}
diff --git a/web/index.html b/web/index.html
index 2a3a48e..ab5af23 100644
--- a/web/index.html
+++ b/web/index.html
@@ -19,13 +19,11 @@
-
+
+
+
+ loading providers
+
diff --git a/web/styles.css b/web/styles.css
index 37eac32..49d250b 100644
--- a/web/styles.css
+++ b/web/styles.css
@@ -1,14 +1,29 @@
:root {
color-scheme: dark;
--bg: #020909;
+ --bg-top: var(--bg);
+ --bg-mid: var(--bg);
+ --bg-bottom: var(--bg);
+ --shell-top: #010707;
+ --shell-bottom: #010707;
--panel: #071918;
--panel-2: #0a2423;
--line: rgba(103, 240, 228, 0.28);
+ --line-deep: rgba(103, 240, 228, 0.18);
--line-hot: rgba(142, 252, 255, 0.72);
--text: #e7f1f1;
+ --text-bright: var(--text);
--quiet: #8ba5a3;
--accent: #ffb84f;
+ --accent-strong: var(--accent);
+ --corner-accent: var(--accent);
--cyan: #8efcff;
+ --shadow: rgba(0, 0, 0, 0.35);
+ --pixel-grid: rgba(142, 252, 255, 0.04);
+ --border-width: 1px;
+ --border-bottom-width: var(--border-width);
+ --corner-accent-size: 0;
+ --font-mono: "Cascadia Mono", Consolas, monospace;
}
* {
@@ -18,7 +33,7 @@
body {
margin: 0;
min-height: 100vh;
- background: var(--bg);
+ background: linear-gradient(180deg, var(--bg-top), var(--bg-mid), var(--bg-bottom));
color: var(--text);
font-family: var(--font-body, Inter), "Segoe UI", system-ui, sans-serif;
}
@@ -30,7 +45,7 @@ body {
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid var(--line);
- background: #010707;
+ background: linear-gradient(180deg, var(--shell-top), var(--shell-bottom));
}
h1 {
@@ -58,25 +73,37 @@ p {
gap: 2px;
}
-.tabs {
+.provider-bar {
display: flex;
+ align-items: center;
gap: 8px;
- padding: 10px 16px 0;
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--line-deep);
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
}
-.tabs button {
- border: 1px solid var(--line);
- border-radius: 6px 6px 0 0;
- background: var(--panel);
+.provider-bar label {
color: var(--quiet);
- padding: 8px 12px;
- font: 600 13px "Cascadia Mono", Consolas, monospace;
+ font: 700 11px var(--font-mono);
+ text-transform: uppercase;
}
-.tabs button.active {
- color: var(--cyan);
- border-color: var(--line-hot);
- background: var(--panel-2);
+.provider-bar select {
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: var(--panel);
+ color: var(--text);
+ padding: 8px 12px;
+ font: 700 13px var(--font-mono);
+ min-width: min(28rem, 52vw);
+}
+
+.provider-meta {
+ color: var(--quiet);
+ font: 600 11px var(--font-mono);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.surface {
@@ -105,6 +132,7 @@ p {
border-radius: 6px;
background: rgba(7, 25, 24, 0.92);
padding: 12px;
+ box-shadow: 0 12px 26px var(--shadow);
}
.pane h2 {
@@ -120,6 +148,12 @@ p {
border-radius: 6px;
background: rgba(10, 36, 35, 0.96);
padding: 10px;
+ box-shadow: 0 8px 20px var(--shadow);
+}
+
+.cultui-card[data-command-id] {
+ cursor: pointer;
+ border-bottom-width: var(--border-bottom-width);
}
.ctb .card {
@@ -140,17 +174,17 @@ p {
.card-title {
white-space: pre-line;
margin-top: 4px;
- font: 700 11px "Cascadia Mono", Consolas, monospace;
+ font: 700 11px var(--font-mono);
}
.health {
color: var(--accent);
- font: 700 10px "Cascadia Mono", Consolas, monospace;
+ font: 700 10px var(--font-mono);
}
.detail {
white-space: pre-wrap;
- font: 600 12px "Cascadia Mono", Consolas, monospace;
+ font: 600 12px var(--font-mono);
line-height: 1.45;
}
@@ -169,7 +203,7 @@ p {
display: flex;
justify-content: space-between;
color: var(--quiet);
- font: 700 10px "Cascadia Mono", Consolas, monospace;
+ font: 700 10px var(--font-mono);
}
.bar {
@@ -192,8 +226,8 @@ p {
border: 1px solid var(--line);
border-radius: 6px;
background:
- linear-gradient(rgba(142, 252, 255, 0.04) 1px, transparent 1px),
- linear-gradient(90deg, rgba(142, 252, 255, 0.04) 1px, transparent 1px),
+ linear-gradient(var(--pixel-grid) 1px, transparent 1px),
+ linear-gradient(90deg, var(--pixel-grid) 1px, transparent 1px),
rgba(3, 15, 17, 0.94);
background-size: 44px 44px;
}
@@ -242,12 +276,49 @@ p {
align-content: start;
}
+.cultui-surface-root {
+ min-height: calc(100vh - 150px);
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 12px;
+ align-content: start;
+}
+
+.cultui-pane {
+ border-width: var(--border-width);
+ border-bottom-width: var(--border-bottom-width);
+}
+
+body[data-provider="repixelizer"] .surface {
+ position: relative;
+}
+
+body[data-provider="repixelizer"] .surface::after {
+ content: "";
+ pointer-events: none;
+ position: fixed;
+ inset: 0;
+ background: repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.035) 0 1px, transparent 1px 4px);
+ opacity: 0.28;
+ mix-blend-mode: screen;
+}
+
+body[data-pixel-art="true"] img,
+body[data-pixel-art="true"] .cultui-background {
+ image-rendering: pixelated;
+}
+
.cultui-title {
color: var(--accent);
font: 700 16px "Cascadia Mono", Consolas, monospace;
line-height: 1.35;
}
+.mono {
+ color: var(--accent);
+ font-family: var(--font-mono);
+}
+
.cultui-reactive-metric {
margin-top: 8px;
}
diff --git a/web/surface.js b/web/surface.js
index 8de04c0..c31a7a0 100644
--- a/web/surface.js
+++ b/web/surface.js
@@ -4,57 +4,147 @@ const statusEl = document.querySelector("#status");
const app = document.querySelector("#app");
const surfaceId = document.querySelector("#surface-id");
const surfaceVersion = document.querySelector("#surface-version");
-const voidBotTab = document.querySelector("#voidbot-tab");
-const fensalirTab = document.querySelector("#fensalir-tab");
-const saiTab = document.querySelector("#sai-tab");
-const huginnTab = document.querySelector("#huginn-tab");
-const dslTab = document.querySelector("#dsl-tab");
+const providerSelect = document.querySelector("#provider-select");
+const providerMeta = document.querySelector("#provider-meta");
let socket;
+let providers = [];
+let currentProvider;
-voidBotTab.addEventListener("click", () => {
- setActiveTab(voidBotTab);
- openVoidBot();
+const defaultStyleTokens = {
+ "--bg": "#020909",
+ "--bg-top": "#020909",
+ "--bg-mid": "#020909",
+ "--bg-bottom": "#020909",
+ "--shell-top": "#010707",
+ "--shell-bottom": "#010707",
+ "--panel": "#071918",
+ "--panel-2": "#0a2423",
+ "--line": "rgba(103, 240, 228, 0.28)",
+ "--line-deep": "rgba(103, 240, 228, 0.18)",
+ "--text": "#e7f1f1",
+ "--text-bright": "#e7f1f1",
+ "--quiet": "#8ba5a3",
+ "--accent": "#ffb84f",
+ "--accent-strong": "#ffb84f",
+ "--corner-accent": "#ffb84f",
+ "--cyan": "#8efcff",
+ "--shadow": "rgba(0, 0, 0, 0.35)",
+ "--pixel-grid": "rgba(142, 252, 255, 0.04)",
+ "--border-width": "1px",
+ "--border-bottom-width": "1px",
+ "--corner-accent-size": "0",
+ "--font-body": "Inter, Segoe UI, system-ui, sans-serif",
+ "--font-title": "Cascadia Mono, Consolas, monospace",
+ "--font-mono": "Cascadia Mono, Consolas, monospace",
+};
+
+const localProviders = [
+ {
+ providerId: "voidbot.swarm",
+ title: "VoidBot Live",
+ kind: "service.operator",
+ freshness: { state: "live" },
+ surfaces: [{ transport: "mimir-eve-deck", surfaceId: "voidbot.swarm" }],
+ },
+ {
+ providerId: "repixelizer",
+ title: "Repixelizer",
+ kind: "service.product",
+ advertisement: "./fixtures/repixelizer.provider-advertisement.json",
+ surfaces: [{ transport: "local-json", surfaceId: "repixelizer.operator.surface", url: "./fixtures/repixelizer.eve-surface.json" }],
+ },
+ {
+ providerId: "fensalir.direct2d",
+ title: "Fensalir Direct2D",
+ kind: "surface.renderer",
+ freshness: { state: "fixture" },
+ surfaces: [{ transport: "local-json", surfaceId: "fensalir.direct2d.fixture", url: "./fixtures/fensalir-client-surface.json" }],
+ },
+ {
+ providerId: "sai.visual_novel",
+ title: "Sai VN Surface",
+ kind: "content.runtime",
+ freshness: { state: "fixture" },
+ surfaces: [{ transport: "local-json", surfaceId: "sai.visual_novel.surface", url: "./fixtures/sai-vn-surface.json" }],
+ },
+ {
+ providerId: "cultcache.huginn.inspector",
+ title: "Huginn .cc",
+ kind: "inspection.huginn",
+ freshness: { state: "fixture" },
+ surfaces: [{ transport: "local-eve-dsl", surfaceId: "cultcache.huginn.inspector", url: "./fixtures/huginn-cc-surface.eve" }],
+ },
+ {
+ providerId: "eve.reactive.dsl",
+ title: "Reactive DSL",
+ kind: "surface.fixture",
+ freshness: { state: "fixture" },
+ surfaces: [{ transport: "local-eve-dsl", surfaceId: "eve.reactive.dsl", url: "./fixtures/reactive-composition.eve" }],
+ },
+];
+
+providerSelect.addEventListener("change", () => {
+ const provider = providers.find(candidate => candidate.providerId === providerSelect.value);
+ if (provider) void openProvider(provider);
});
-fensalirTab.addEventListener("click", async () => {
- setActiveTab(fensalirTab);
- closeSocket();
- const response = await fetch("./fixtures/fensalir-client-surface.json");
- renderSurface(await response.json(), "fixture");
-});
+void bootProviders();
-saiTab.addEventListener("click", async () => {
- setActiveTab(saiTab);
- closeSocket();
- const response = await fetch("./fixtures/sai-vn-surface.json");
- renderSurface(await response.json(), "fixture");
-});
-
-huginnTab.addEventListener("click", async () => {
- setActiveTab(huginnTab);
- closeSocket();
- const response = await fetch("./fixtures/huginn-cc-surface.eve");
- renderSurface(compileEveDsl(await response.text()), "dsl");
-});
-
-dslTab.addEventListener("click", async () => {
- setActiveTab(dslTab);
- closeSocket();
- const response = await fetch("./fixtures/reactive-composition.eve");
- renderSurface(compileEveDsl(await response.text()), "dsl");
-});
-
-openVoidBot();
-
-function setActiveTab(tab) {
- for (const button of [voidBotTab, fensalirTab, saiTab, huginnTab, dslTab]) {
- button.classList.toggle("active", button === tab);
- }
+async function bootProviders() {
+ providers = await Promise.all(localProviders.map(loadProviderAdvertisement));
+ providerSelect.replaceChildren(...providers.map(provider => {
+ const option = document.createElement("option");
+ option.value = provider.providerId;
+ option.textContent = provider.title || provider.providerId;
+ return option;
+ }));
+ const firstProduct = providers.find(provider => provider.providerId === "repixelizer") || providers[0];
+ providerSelect.value = firstProduct.providerId;
+ await openProvider(firstProduct);
}
-function openVoidBot() {
+async function loadProviderAdvertisement(provider) {
+ if (!provider.advertisement) return provider;
+ const response = await fetch(provider.advertisement);
+ const advertisement = await response.json();
+ return {
+ ...advertisement,
+ ...provider,
+ surfaces: provider.surfaces || advertisement.surfaces || [],
+ localAdvertisement: advertisement,
+ };
+}
+
+async function openProvider(provider) {
closeSocket();
+ currentProvider = provider;
+ providerMeta.textContent = `${provider.kind || "provider"} | ${provider.providerId} | ${provider.freshness?.state || "unknown"}`;
+ surfaceId.textContent = provider.providerId;
+ surfaceVersion.textContent = "opening";
+ const surface = provider.surfaces?.[0];
+ if (!surface) {
+ statusEl.textContent = `${provider.title || provider.providerId} has no advertised surface`;
+ app.replaceChildren(emptyState("No advertised surface"));
+ return;
+ }
+
+ if (surface.transport === "mimir-eve-deck") {
+ openVoidBot(provider);
+ return;
+ }
+
+ if (surface.transport === "local-eve-dsl") {
+ const response = await fetch(surface.url);
+ renderSurface(compileEveDsl(await response.text()), "local dsl");
+ return;
+ }
+
+ const response = await fetch(surface.url);
+ renderSurface(await response.json(), "local fixture");
+}
+
+function openVoidBot(provider) {
const scheme = location.protocol === "https:" ? "wss:" : "ws:";
const host = location.hostname || "127.0.0.1";
const url = `${scheme}//${host}:8795/eve/deck`;
@@ -64,7 +154,7 @@ function openVoidBot() {
socket.addEventListener("open", () => {
if (socket !== activeSocket) return;
statusEl.textContent = "connected to Mimir Eve broker";
- activeSocket.send(JSON.stringify({ type: "open-provider", providerId: "voidbot.swarm" }));
+ activeSocket.send(JSON.stringify({ type: "open-provider", providerId: provider.providerId }));
});
socket.addEventListener("message", event => {
if (socket !== activeSocket) return;
@@ -96,6 +186,7 @@ function renderSurface(state, source) {
surfaceVersion.textContent = `v${state.version ?? "?"}`;
statusEl.textContent = `${state.title || "surface"} (${source})`;
applySurfaceStyles(state.surface?.styles);
+ document.body.dataset.provider = state.providerId || currentProvider?.providerId || "";
if (state.surface?.root) {
app.replaceChildren(renderCultComponent(state.surface.root));
} else if (state.providerId === "voidbot.swarm") {
@@ -108,20 +199,41 @@ function renderSurface(state, source) {
function applySurfaceStyles(styles) {
const tokens = styles?.tokens || {};
const root = document.documentElement.style;
+ for (const [variable, value] of Object.entries(defaultStyleTokens)) {
+ root.setProperty(variable, value);
+ }
const map = {
colorBackground: "--bg",
+ colorBackgroundTop: "--bg-top",
+ colorBackgroundMid: "--bg-mid",
+ colorBackgroundBottom: "--bg-bottom",
+ colorShellSurfaceTop: "--shell-top",
+ colorShellSurfaceBottom: "--shell-bottom",
colorPanel: "--panel",
colorPanelAlt: "--panel-2",
+ colorPanelBorder: "--line",
+ colorPanelBorderDeep: "--line-deep",
colorText: "--text",
+ colorTextBright: "--text-bright",
colorMuted: "--quiet",
colorAccent: "--accent",
+ colorAccentStrong: "--accent-strong",
+ colorCornerAccent: "--corner-accent",
colorLink: "--cyan",
+ colorShadow: "--shadow",
+ colorPixelGrid: "--pixel-grid",
};
for (const [token, variable] of Object.entries(map)) {
if (tokens[token]) root.setProperty(variable, tokens[token]);
}
if (tokens.fontBody) root.setProperty("--font-body", tokens.fontBody);
if (tokens.fontTitle) root.setProperty("--font-title", tokens.fontTitle);
+ if (tokens.fontMono) root.setProperty("--font-mono", tokens.fontMono);
+ if (tokens.borderWidthPx) root.setProperty("--border-width", `${tokens.borderWidthPx}px`);
+ if (tokens.borderBottomWidthPx) root.setProperty("--border-bottom-width", `${tokens.borderBottomWidthPx}px`);
+ if (tokens.cornerAccentPx) root.setProperty("--corner-accent-size", `${tokens.cornerAccentPx}px`);
+ document.body.dataset.pixelArt = tokens.pixelArt || tokens.imageRendering === "pixelated" ? "true" : "false";
+ document.body.dataset.scanlines = tokens.scanlineOverlay ? "true" : "false";
}
function renderCultComponent(node) {
@@ -187,10 +299,34 @@ function renderCultComponent(node) {
return card;
}
+ if (kind === "surface") {
+ const surface = el("section", "cultui-surface-root");
+ for (const child of children) surface.append(renderCultComponent(child));
+ return surface;
+ }
+
+ if (kind === "pane" || kind === "panel") {
+ const pane = el("section", "pane cultui-pane");
+ if (props.title || node.text) pane.append(el("h2", "", props.title || node.text));
+ for (const child of children) pane.append(renderCultComponent(child));
+ return pane;
+ }
+
if (kind === "card") {
const card = el("article", "card cultui-card");
- if (props.title) card.append(el("div", "card-title", props.title));
+ if (props.title || node.text) card.append(el("div", "card-title", props.title || node.text));
for (const child of children) card.append(renderCultComponent(child));
+ if (props.commandId || node.commandId) {
+ card.dataset.commandId = props.commandId || node.commandId;
+ card.tabIndex = 0;
+ card.addEventListener("click", () => publishCommandIntent(card.dataset.commandId, props));
+ card.addEventListener("keydown", event => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ publishCommandIntent(card.dataset.commandId, props);
+ }
+ });
+ }
return card;
}
@@ -212,7 +348,7 @@ function renderCultComponent(node) {
}
if (kind === "text.dialogue" || kind === "text" || kind === "text.title") {
- const text = el("div", kind === "text.title" ? "cultui-title" : "detail", props.text || "");
+ const text = el("div", textClassName(kind, props), props.text || node.text || "");
bindText(text, props);
return text;
}
@@ -327,6 +463,36 @@ function currentMesh() {
return window.__eveCurrentMesh;
}
+function textClassName(kind, props) {
+ if (kind === "text.title" || props.role === "title") return "cultui-title";
+ if (props.role === "mono") return "detail mono";
+ return "detail";
+}
+
+function emptyState(message) {
+ const pane = el("section", "pane");
+ pane.append(el("h2", "", "No Surface"));
+ pane.append(el("div", "detail", message));
+ return pane;
+}
+
+function publishCommandIntent(commandId, props = {}) {
+ const command = {
+ type: "surface-command",
+ schema: "gamecult.eve.command.v1",
+ providerId: currentProvider?.providerId || surfaceId.textContent,
+ surfaceId: currentProvider?.surfaces?.[0]?.surfaceId || surfaceId.textContent,
+ command: commandId || "invoke",
+ payload: {
+ transport: props.transport || null,
+ },
+ issuedAt: new Date().toISOString(),
+ clientId: "browser.reference",
+ };
+ statusEl.textContent = `command ${command.command}`;
+ console.info("Eve command intent", command);
+}
+
function renderCultGraph(props) {
const graph = el("section", "cultui-graph");
graph.dataset.engine = props.engine?.id || "graph";