skill / app-creator
!

Not installable via adompkg

This skill has no published release. adompkg install kyle/app-creator will not work until a maintainer publishes a tarball with install.sh and uninstall.sh.

See the publishing docs for the package.json schema and tarball layout required to ship this skill.


name: app-creator
description: >
Use when the user wants to build a new Adom "app" โ€” a mini web server that
renders its UI in a Hydrogen webview tab. Covers architecture (server in
any language: Rust+tiny_http, Node, Python, Go), required conventions
(Adom brand, custom SVG favicon, proxy URL, webview safety), and publish
flow (GitHub repo + wiki app page for discovery and install). Trigger
words: make an app, create an app, build an app, new app, adom app, build
an adom app, mini web app, mini web server, webserver app, build a
webview app, webview app, hydrogen app, build a widget, create mini app,
tiny app, tool with a UI, cli with a UI, app needs an icon, favicon for
an app, app branding, app icon, app standards, how to make an adom app,
app conventions, app template, webview-hosted app, first-class app, app
page on wiki.

Adom App Creator

๐Ÿ›‘ STEP 0 โ€” MANDATORY before writing one line of UI code

Before authoring any HTML, CSS, JS, SVG, or webview content:

  1. Read gallia/skills/brand/SKILL.md end-to-end.
    Palette tokens (#0d1117 page bg, #161b22 panels, #00b8b1 accent),
    Familjen Grotesk + Satoshi + JetBrains Mono fonts, monochrome icons
    only
    (#e6edf3 or currentColor โ€” never emoji, never coloured
    favicons, never coloured tab-strip icons), 0.15 s transitions, frosted-
    glass card pattern, dark theme always.
  2. Read gallia/skills/human-ui-patterns/SKILL.md end-to-end.
    Tooltips (rule 1d โ€” body-appended position:fixed div, z 99999, NEVER
    title="" or CSS ::after), HUDs (draggable + collapsible + dismissible),
    NEVER ALL CAPS, click previews, hero images, provenance captions,
    AI-drivability, the full pre-publish checklist.

Both skills are <500 lines each. Reading them takes ~30 s and prevents the
#1 user complaint on every UI build: "have you even bothered to look at
the brand guide skill and ui design skill?"
(verbatim, 2026-04-26 on the
aci library dashboard).

Rule of thumb: if you've just typed <div> or :root { without having
the open contents of those two files in your context window in the last
two minutes, stop and Read them. The rules are easy to half-remember and
half-violate at the same time โ€” emoji icons, the wrong shade of background
grey, a teal favicon, title="" tooltips. Every one of those has shipped
from a Claude Code session that "knew the rules" but didn't re-read.

No exceptions for "quick prototypes" or "internal tools." This skill,
aci library, every chip-fit HUD, every wiki page hero โ€” they all started as
"I'll just throw something together and polish later." The polish phase
has never come; the violations ship.

Adom "apps" are mini web servers that render their UI inside a Hydrogen
webview panel tab (not the Adom Viewer). They look and feel like native
Hydrogen panels to the user, but under the hood they're just HTTP servers
the AI drives. Any future Adom user can discover them via wiki triggers and
install them via the standard app install prompt.

This skill covers:

  1. What counts as an "app" (vs a CLI-only tool or an AV widget)
  2. The required conventions every app must follow (brand, favicon, proxy URL)
  3. The mini web server pattern (any language / any HTTP library)
  4. How to ship: GitHub repo, wiki app page, install prompt
  5. Where apps fit in the broader skill ecosystem

What is an Adom app?

An Adom app is:

  • A mini web server that runs on the Adom Docker container (or the user's
    own machine, for desktop integrations). The server listens on a TCP port.
  • Its UI is HTML + CSS + JavaScript served by that server, rendered in a
    Hydrogen webview panel tab. Not the Adom Viewer. Not a Tauri window.
    Not a browser tab the user opens manually, a first-class Hydrogen panel.
  • The server can be a single page or a multi-page site, the app's
    author decides based on what fits the feature.
  • The implementation language is the AI's choice. Rust with embedded
    tiny_http works great for tools that also expose a CLI (e.g.,
    video-post). Node/Python/Go/anything else is fine if it better matches
    the workflow.
  • The app is discoverable via wiki triggers. A user says something like
    "speed up my demo recording" and Claude Code suggests the matching app
    from the wiki, with a paste-into-Claude install prompt.

An app is NOT:

  • A CLI-only tool (those belong in adom-cli-design / tool-publisher
    without the web UI)
  • An Adom Viewer widget (those use av-creator and push HTML to the AV
    server, which is a different render surface)
  • A Hydrogen built-in panel like 3D Editor or Schematic Editor (those are
    Tauri native + ship with Hydrogen itself)

Read adom-app-model first for the bigger picture (repo layout, wiki
publishing, Tier A vs Tier B distribution, service containers, version
discipline). This skill only covers the client-side UI conventions; the
model skill covers "where does this app live and how does it ship?"

Also read adom-cli-design. Every app ships a CLI surface (at minimum
<app> install, <app> serve, and whatever subcommands the AI drives the
server with). That CLI must follow the Adom CLI conventions โ€” Rust +
clap, OK: / ERROR: / Hint: output lines, isatty-aware colors, the
skill-file pattern. Apps with a half-finished CLI surface annoy everyone
downstream; don't skip this read.

Does your app need a service container?

If the app has a backend that holds API keys, caches third-party responses,
or otherwise wants to be shared across users, it needs a private service
container
on the default-light image. See standalone-service skill for
the full setup, but the short version:

  • Add service/{deploy.sh, watchdog.sh, service.json} to your repo root
  • Deploy once with adom-cli carbon containers create --image-id <default-light> --repo-id <REPO_ID> --ssh
    followed by ssh ... bash service/deploy.sh (the CLI requires --repo-id)
  • The watchdog cron auto-pulls origin/main every 2 min and rebuilds +
    restarts โ€” you don't redeploy after releases, just git push
  • The user's container points <APP>_API at the service container's public URL
    for shared-cache benefits

If the app is a pure client (no backend state worth sharing), skip the
service/ directory and let it run locally per-user.

Each app ships a BUILD-SKILL.md

Every app's release process is the same shape (bump VERSION โ†’ cargo build โ†’
git tag โ†’ gh release โ†’ adom-wiki asset upload โ†’ adom-wiki page update
โ†’ verify). Bundle this as <repo>/BUILD-SKILL.md and deploy it from the
binary's install subcommand to ~/.claude/skills/<slug>-build/SKILL.md.
See adom-app-model for the template. Trigger words like "release X",
"bump X", "publish X" route Claude to it.

Required conventions

Every Adom app MUST follow these rules. They exist so apps feel like
first-class Hydrogen panels, not random HTML pages someone served on a port.

1. Use the Adom brand guide

Source: ~/project/gallia/skills/brand/SKILL.md, read it first.

Minimum requirements for every app's UI:

  • Fonts: Familjen Grotesk (headlines) + Satoshi (body) + JetBrains Mono
    (code). Load from https://adom.inc/fonts/ via @import url(...) in
    your CSS. Never fall back to Inter, Arial, Helvetica, or system fonts
    as primary.
  • Color palette: full :root CSS token block at tab-wiring-reference.md ยง Brand CSS tokens. Use --accent (#00b8b0 teal) for primary actions, --green for success, --red for destructive.
  • Dark theme by default. The Hydrogen workspace is dark, so any white
    panel looks jarring. Start from --bg: #0d1117 and only go lighter when
    you need contrast.
  • Adom teal accent (#00b8b0) for primary actions, focus rings, and
    highlights. Green (#3fb950) only for success states, red (#f85149)
    only for destructive actions.
  • 8px / 12px / 16px spacing grid. No random 13px margins.
  • Border radius 8px for cards and buttons, 12px for larger
    containers. No sharp corners unless you have a specific reason.

See the video-post voiceover UI (src/voiceover/ui.html in the
adom-inc/video-post repo) for a working example that follows every
rule above.

1b. ๐Ÿ›‘ EVERY icon is monochrome white โ€” no exceptions, no favicon carve-out

Per brand/SKILL.md: every icon in an Adom app is monochrome white
(#e6edf3) or currentColor inheriting from a white-text context
.
This includes:

  1. In-app icons โ€” header logo, button glyphs, tooltip icons,
    sidebar icons, empty-state art.
  2. The Hydrogen tab-strip icon โ€” docs/icon.svg, both as the HTML
    <link rel="icon"> favicon AND as the base64-encoded data URL
    passed to adom-cli hydrogen workspace add-tab --display-icon.
    The tab strip is Adom UI chrome; every app dropping a colored icon
    in there turns the strip into a clown car.
  3. Every other rendering of the app icon inside the Adom UI โ€”
    panel catalog, discovery list, start-menu equivalents, etc.

The only place brand color is allowed is wiki marketing art โ€”
the hero image and thumbnail on the app's wiki page. Those are
screenshots-of-branded-products, not Adom UI elements.

Past Claudes (including the author of this app) have missed this
rule three times.
If you are auditing an app and your instinct is
"the skill says favicons CAN be teal, so my teal docs/icon.svg is
fine"
โ€” you are reading a stale cache of this skill. There is no
favicon carve-out. Delete that belief and re-read this section.

Pre-publish audit โ€” gate every release on this returning empty: see tab-wiring-reference.md ยง Icon audit grep commands for the two grep commands and the wrong/right SVG HTML examples.

Both must return zero. The docs/icon.svg hit is the most common one because authors assume favicons are exempt. They are not. Prefer MDI icons (already monochrome, work with currentColor) or custom 24x24 SVGs with fill="currentColor". Never use colored icon sets. If using currentColor, verify the parent's color is var(--text) or #e6edf3 โ€” a teal parent silently turns the icon teal.

No favicon carve-out. (Earlier revisions of this skill said docs/icon.svg could be colored. That was wrong โ€” the favicon renders in the Hydrogen tab strip, which is UI chrome, not a marketing surface.)

2. Ship a custom SVG favicon

Code samples for serving /favicon.svg and referencing it in HTML, icon design tips, and icon generation guidance: see tab-wiring-reference.md ยง SVG favicon implementation.

Requirements:

  • SVG format, square viewBox (0 0 64 64 or 0 0 24 24), transparent background.
  • Monochrome white only โ€” single fill="#e6edf3" (or currentColor). NO teal, no gradients. See ยง1b.
  • Saved at docs/icon.svg in the repo; serve it at /favicon.svg with Content-Type: image/svg+xml.
  • Reference with a relative href="favicon.svg" (no leading slash โ€” see ยง7a).

3. Use the Adom proxy URL, not 127.0.0.1

The Hydrogen webview iframe cannot load http://127.0.0.1:PORT/ directly โ€” the browser blocks it as cross-origin. Always point the tab at the Coder proxy URL: https://<slug>.adom.cloud/proxy/<PORT>/.

Read VSCODE_PROXY_URI from the environment and substitute {{port}}. Rust code sample and 127.0.0.1 fallback for terminal testing: see tab-wiring-reference.md ยง Proxy URL substitution.

4. Open the webview tab via adom-cli

Full adom-cli hydrogen workspace add-tab command, --display-icon forms (MDI string vs base64 data URL), Rust embed pattern, and tab-cleanup command: see tab-wiring-reference.md ยง Opening and closing the webview tab.

Key points:

  • Panel type UUID for Web View: adom/a1b2c3d4-0031-4000-a000-000000000031.
  • Get leaf-id with adom-cli hydrogen workspace get | jq -r '.focusedPanelId'.
  • Pass docs/icon.svg as a data:image/svg+xml;base64,... URL via --display-icon (Hydrogen does not auto-pick up the <link rel="icon"> favicon).
  • Always call adom-cli hydrogen workspace remove-tab on exit.

5. Handle browser requirements

The webview is a real Chromium-based iframe. Everything browser-native
works (MediaRecorder, WebAudio, Canvas, WebGL, fetch, etc.) but remember:

  • Permissions (mic, camera, geolocation) go through Hydrogen, not the
    iframe. If your app needs the mic, use adom-cli hydrogen audio enable
    from the CLI and let Hydrogen capture, don't try getUserMedia inside
    the iframe, it'll fail on permissions.
  • File downloads trigger Hydrogen's download handler, which saves to
    ~/project/ paths on the container. Use those paths in subsequent CLI
    commands, not browser Downloads/.
  • Range requests, support them on any endpoint that streams large
    files (video, audio, big JSON blobs). Hydrogen's video player uses Range
    by default; if your server 404s on Range it breaks seeking.
  • No cookies / no localStorage expectations, webviews are fresh every
    session. Persist state to disk via your CLI, not the browser.

6. Graceful shutdown

Apps should have a /shutdown endpoint (or equivalent) that the UI can
call when the user is done. On receiving it, the server:

  1. Finishes any pending work (file writes, ffmpeg mux, etc.)
  2. Removes the Hydrogen webview tab
  3. Exits the CLI process cleanly

Never leave the server running after the user is done, it holds a port,
shows a zombie tab, and confuses future invocations.

7a. Always use relative URLs inside the webview (CRITICAL)

Apps hosted through the Coder /proxy/<port>/ prefix MUST use relative
URLs for every fetch, href, src, and link. A leading slash resolves to
the origin (hydrogen.adom.inc) and bypasses the proxy entirely, so your
fetches hit the wrong server and fail silently.

Wrong/right HTML examples: see tab-wiring-reference.md ยง Relative URL wrong vs right examples.

Every fetch(), <a href>, <link rel="icon">, <img src>, <script src>, every CSS url(). If it's in the HTML or JS your app serves, no leading slash. Ever.

Symptom when you get this wrong: the UI loads (since the top-level HTML
comes from the proxy), but every button click silently does nothing, the
video player stays blank, and your /console log stays empty because the
JS console forwarding is also hitting the wrong URL. You can spend hours
chasing "why isn't this working" before realizing every network request
is going to the wrong host. This is the single most common bug when
wiring up a new Adom app.

7. 2-way HTTP communication (CRITICAL)

Every action the UI can trigger MUST be independently triggerable via
plain HTTP from outside the UI.
This is the single most important rule
for Adom apps and is what makes them AI-drivable.

Why: the AI building/testing/operating the app has to be able to drive
it without a human clicking buttons. If the Record button is wired to a
JS function that only the UI calls, the AI is stuck. If the Record button
POSTs to /start-recording (the same endpoint the AI can hit with curl),
the AI can test, debug, and automate the app by itself.

Rules:

  • Every user-visible action has a server endpoint. Not just a
    JS function. The JS onClick handler is a thin wrapper that POSTs to the
    endpoint; it does NOT contain the logic.

  • State changes go through the server, not through in-memory JS state.
    If the UI needs to know something, it reads it from the server via
    GET /state (or SSE / websocket for live updates).

  • The AI can drive the full app lifecycle via curl. Before shipping, you MUST be able to script a full user session from bash. If any step requires clicking a DOM element with no HTTP equivalent, that's a bug. See webview-conventions-detail.md ยง 7 for the curl-driven test sequence.

  • Always expose GET /state (or similar) returning the app's current
    state as JSON so the AI can check progress without scraping HTML.

  • Return structured JSON on all POST endpoints, not just HTML
    redirects. The AI parses the response to decide what to do next.

This is NOT a nice-to-have, it's the defining property of an Adom app.
A web server that can only be driven by clicking a human's mouse is just
a website. An Adom app is an AI-drivable surface with a human-friendly UI
layered on top.

Server state is the single source of truth. The UI must not keep its own copy of state like "am I recording?". Instead: poll GET /state at 300-500ms, compare phase against the last seen value, and drive UI transitions on every change. Buttons fire POST commands to the server only โ€” they do NOT update local state. Full state-poller JS pattern and curl-driven happy-path test sequence: see webview-conventions-detail.md ยง 7. 2-way comms โ€” state poller pattern.

The test: if the AI curl-driven flow and the human-click flow look identical in the UI, the wiring is correct. If they diverge, the UI is keeping local state that should live on the server.

7c. Every button needs a hover-delayed tooltip

Full tooltip implementation (CSS pattern, clipping rules, edge-anchored positioning, eval channel, console forwarding), plus mini web server patterns (Rust/Node/Python): see webview-conventions-detail.md.

Rule: every button (and any label whose meaning is not obvious to a
first-time user) must have a tooltip. The tooltip must be gentle: it
appears only after the pointer has been still on the target for about
500 ms, and it fades in smoothly, not instantly. A moved pointer that
passes over many buttons must not cause any tooltip to show.

Why: buttons in an AI-built app have to be self-explaining, because
the user may not have read any docs and the AI cannot be standing next
to them. At the same time, tooltips that pop in on every movement are
aggressively noisy and make the UI feel twitchy. The 500 ms hover
delay is the standard tradeoff: intentional hovers get the explanation,
casual passes do not.

Required content of a button tooltip:

  1. What the button does, in plain English.
  2. What will change in the app after you click it.
  3. For destructive or irreversible-seeming actions, what is and is
    not preserved (files on disk, state in memory, etc.).
  4. For actions that use technical jargon in the button label (for
    example, "EBU R128", "Auto-level", "Mux"), an explanation of the
    jargon, written for a non-expert. Never assume the user has the
    vocabulary; always define the terms.

Required content of a non-button label tooltip (for example, a
waveform meta line that says "EBU R128 -16 LUFS / -1.5 dBTP"):

  1. What the label means, in human terms, no jargon.
  2. A breakdown of any technical parameters, one by one, each with
    a plain-English explanation and what it means for the user's
    outcome ("quieter moments got slightly louder", "no audio will
    clip on playback", etc.).
  3. Whatever the practical effect is on the thing the user was
    looking at. Do not just define terms; explain the consequence.

Implementation pattern, clipping rules, and edge-anchored positioning CSS: see webview-conventions-detail.md ยง 7c.

Key: use data-tooltip attribute with 500ms CSS transition delay. Do NOT use HTML title= attributes. See reference file for full CSS and edge-anchoring pattern.

7e. Diagnostic views NEVER auto-correct โ€” show the truth

Diagnostic views must show source data, never an auto-corrected prettier version. Compute corrections, display them with a clear signal, but never silently apply them. Full rule, forbidden examples, and the wrapper.position.z = -minZ trap: see webview-conventions-detail.md ยง 7e.

7d. Provide a live "now playing" / "current state" indicator for any media or document being viewed

When an artifact exists in multiple versions (raw/normalized/preview/committed), the UI must always show which version is currently visible or playing. Labeled pill overlay pattern and color-keyed dot details: see webview-conventions-detail.md ยง 7d.

7b. Always expose a frontend eval channel so the AI can hot-patch the UI

Full implementation (server ring-buffer, UI setInterval poller, AI curl usage, and caveats): see webview-conventions-detail.md ยง 7b.

Key points:

  • Ship POST /eval + GET /eval/:id endpoints so the AI can push JS snippets into the running UI.
  • Gate behind --dev / DEV_MODE=1 โ€” this is a full remote-code-execution primitive.
  • Results route through /console or a dedicated /eval-result endpoint; never just "ok": true.
  • Catch and return exceptions as {error, stack} so the AI can diagnose snippet failures.

7a. AI drives the frontend via state mutations

The flip side of 2-way comms is that the AI is an equal citizen to the
human user. When the AI wants to drive the UI, it:

  1. POSTs to a command endpoint (/start-recording, /play, /seek)
  2. The server updates its state
  3. The UI's state poller sees the change and reflects it

There is no separate "AI remote control" channel. The same POST /start-recording that your UI's Record button fires is what the AI
fires. The UI's state poller is what turns the server state change into
visual feedback the human sees.

This means your server's command endpoints double as AI automation
hooks. Test that during development: before shipping, curl -X POST .../start-recording from the terminal and watch the UI animate without
touching the browser. If it doesn't animate, the state poller is broken
or the UI is keeping local state.

8. JavaScript console forwarding

Full <script> snippet, server ring-buffer implementation, and AI debug workflow: see webview-conventions-detail.md ยง 8.

Key points:

  • Override console.log/warn/error to POST /console as JSON; keep a bounded ring buffer server-side (500 entries).
  • Catch window.onerror and unhandledrejection and forward them too.
  • Expose GET /console so the AI can read all UI-side log output without DevTools access.
  • Never ship an app without this โ€” it is the only way to debug UI-side bugs in the webview.

9. Use Hydrogen's built-in recording countdown

Hydrogen's recording start --countdown 3 shows a native 3-2-1 countdown
overlay before starting capture. Use this instead of writing your own
JS countdown in the app's UI.
The native overlay is:

  • Visually consistent with the rest of Hydrogen
  • Reliable (runs in the parent window, can't be broken by iframe issues)
  • Works for mic-only sessions too (disable screen, enable audio, still get countdown)

If your app needs a countdown before capturing audio, trigger
adom-cli hydrogen recording start --countdown 3 --audio-only (or whatever
the current flag spelling is, check adom-cli hydrogen recording start --help)
from your server's /start-recording endpoint. Don't roll your own JS
timer; it'll drift, miss the first click, and look different from every
other countdown in the system.

Mini web server patterns

Full code templates for Rust (tiny_http), Node.js, and Python: see webview-conventions-detail.md ยง Mini web server patterns.

Pick whichever language the CLI is already using. Embed UI assets at compile time (Rust include_str!) or load them from disk (Node.js / Python). Always serve /favicon.svg with Content-Type: image/svg+xml and implement /shutdown for clean exit.

Before publishing: show the user the working app

Never publish an app to GitHub or the wiki before the user has seen it
working.
The user has to click through the UI (or watch the AI click
through the 2-way HTTP endpoints) and confirm it behaves correctly before
any git push, gh release create, or adom-wiki page create runs.

This is a hard rule because:

  • The AI cannot judge UI correctness by reading code. Fonts load wrong,
    layouts break, animations stutter, and the code still compiles.
  • Publishing means other users will find it via wiki discovery. Shipping a
    broken app wastes every future user's time.
  • Git history and wiki publish dates are public. A bad v0.1.0 is embarrassing.

Correct flow:

  1. Build the app locally
  2. Start it (my-app run or equivalent)
  3. Show the user a screenshot of the UI or ask them to click through it
  4. Drive the 2-way HTTP endpoints yourself to verify the happy path works
  5. Fix whatever the user or the verification surfaces
  6. Only after the user gives explicit approval, publish to GitHub + wiki

If the user says "build an app that does X", the default end state is
a running app with a screenshot shown to the user, NOT a published
GitHub repo. The publish step comes after a separate "ship it" from the user.

CLI surface

Follow adom-cli-design for the app's CLI โ€” output conventions
(OK: / ERROR: / Hint:), isatty-aware colors, the skill-file pattern,
and the Rust + clap defaults. All of those apply in full to apps; this
skill doesn't restate them.

Publishing an app to the wiki

Follow the tool-publisher skill for the full lifecycle. The short version:

  1. Create the GitHub repo (adom-inc/<app-name>, private by default).
  2. Push v0.1.0 with Cargo.toml / package.json / setup.py + SKILL.md +
    README.md + docs/icon.svg.
  3. Create a GitHub release with the binary (or install script) attached.
  4. Publish a wiki page of type app at apps/<app-name> with type, slug, title, brief, and metadata fields including repo, version, releases.adom_docker, discovery_triggers, and discovery_pitch. Full JSON template: see tab-wiring-reference.md ยง Wiki page metadata template.
  5. Verify the wiki page renders the install prompt correctly (adom-wiki page get apps/my-app-name).

The wiki's /discover aggregator picks up discovery_triggers on next
regeneration and adds them to adom-wiki-discover, so any Claude Code
session that hears a matching trigger will suggest your app automatically.

Relationship to other skills

  • tool-publisher, how to publish any CLI tool (including apps) to
    the wiki. Apps are a specific kind of tool: CLI + embedded HTTP server +
    webview UI.
  • adom-cli-design, the output conventions (OK: / ERROR: /
    Hint:) that apply to the app's CLI surface, before and after the
    server runs.
  • brand, the visual identity (colors, fonts, spacing) that every
    app's HTML must follow.
  • adom-panels / adom-panels-webview, the Hydrogen webview
    panel API, including navigate, refresh, and proxy mode.
  • adom-workspace-control, adding/removing tabs, finding panel leaf
    IDs, activating tabs.
  • adom-wiki, the CLI for publishing wiki pages and uploading
    assets (including the app's wiki page + icon + install prompt).
  • av-creator โ€” DEPRECATED. The Adom Viewer is no longer the
    canonical display surface. Build new visual content (3D models,
    diagrams, widgets, charts) as apps via this skill, not as AV widgets.
    Only consult av-creator if you're maintaining an existing
    un-ported AV view.

Reference apps

  • video-post (adom-inc/video-post), Rust CLI + tiny_http
    server, voiceover recording UI, ffmpeg mux pipeline. Demonstrates the
    full pattern: brand-compliant UI, SVG favicon, proxy URL, Hydrogen
    audio integration, graceful shutdown.
  • adom-desktop demo server (adom-desktop/skills/demo/server.cjs) -
    Node.js HTTP server that drives the step-by-step demo webview. Uses the
    same webview tab + proxy URL pattern.

Checklist before publishing

Full checklist (UI + brand, server wiring, 2-way comms, CLI output, verification, repo + wiki): see publish-checklist.md.

The two blocking categories are 2-way comms (every action must be curl-scriptable, GET /state must exist, GET /console must be wired) and verification (user must have seen the running UI and given explicit approval before any git push, gh release create, or wiki publish runs).