← Commit history

v1.9.49: design-system pass (house header, line-drawing icon + favicon, newbie panel, status pills, settings modal, toasts, tokens, fonts, no dashes) and Hydrogen dock membership (dockbar.json, adom-tts open --print-url, /api/health)

John Lauer ·180206b516 ·1mo ago ·parent a54b6bf
18 files changed +1825−508
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.48"+version = "1.9.49" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1
@@ -1 +1 @@-1.9.48+1.9.49
dev-skills/adom-tts-dev/SKILL.md+82−58
@@ -2,7 +2,7 @@ name: adom-tts-dev description: >-   Develop and debug adom-tts from source: run the build, add features to the-  player/queue, and — critically — the hard-won engineering rules for a shared+  player/queue, and, critically, the hard-won engineering rules for a shared   audio system that many AI threads drive at once. Read before touching   src/play.rs, src/player.html, or the drainer. Covers: the single-server/   single-drainer architecture (multiple AIs talk to ONE adom-tts player),@@ -14,18 +14,18 @@ description: >-   player.html, queue debugging, playback verdict, run the tests. --- -# adom-tts — development cycle & engineering rules+# adom-tts, development cycle & engineering rules  ## Dev cycle  ```bash-cargo test --release          # 6 unit tests — MUST pass before any ship+cargo test --release          # 6 unit tests, MUST pass before any ship ./build.sh                    # cargo build --release + refresh bin/adom-tts (never bare cargo build) ./target/release/adom-tts --version && ./target/release/adom-tts health adom-tts say "smoke" --out /tmp/s.mp3 --thread dev --desc "smoke test" --play --confirm   # end-to-end proof ``` -Install locally + make every copy on the container identical (byte-compare —+Install locally + make every copy on the container identical (byte-compare , version strings can't catch same-version drift):  ```bash@@ -39,28 +39,28 @@ done  ## The five engineering rules (each one paid for in real outages) -### 1. Multiple AIs talk to ONE server — keep it that way+### 1. Multiple AIs talk to ONE server, keep it that way Every thread's `say --play` feeds one shared queue (`~/.adom/tts/queue/`); ONE drainer (lock = `~/.adom/tts/drainer.lock` = `{pid}\n{version}`) runs ONE persistent HTTP server on ONE port; the webview loads once and swaps clips via-`/poll` — never a re-navigation. A NEWER-version drainer takes over the lock;+`/poll`, never a re-navigation. A NEWER-version drainer takes over the lock; the old one notices within 250ms, abandons its clip, and exits. Two drainers alive = clips flip/overlap (users hear it immediately). Never kill drainers-manually — deploy a new version and let supersede do the handoff. If you must-(last resort), match argv exactly via `/proc/<pid>/cmdline` NUL-split — a+manually, deploy a new version and let supersede do the handoff. If you must+(last resort), match argv exactly via `/proc/<pid>/cmdline` NUL-split, a `grep`-kill matches its own shell and kills your session.  ### 2. Never let audio drop - Clips **buffer** (up to 10 min) until the player page shows a *sustained*-  live audience — 3 consecutive polls. One fresh poll is NOT alive (a+  live audience, 3 consecutive polls. One fresh poll is NOT alive (a   disconnected/backgrounded page still trickles heartbeats). - `no-playback` AND **early-partial** clips (died under 10% of their duration,   e.g. 0.7s into an 80s clip) are **retried 3×** (`clip_retry` with a reason,   `replay: auto`), never silently consumed.-- The page **downloads the whole mp3 to a local blob before playing** —+- The page **downloads the whole mp3 to a local blob before playing** ,   a cellular-hotspot drop mid-clip cannot stall audio (the listener is often   tethered while driving).-- The drainer **adopts surface env** from `~/.adom/tts/env.json` at startup —+- The drainer **adopts surface env** from `~/.adom/tts/env.json` at startup ,   a `say` from a sanitized shell (cron/hook) once spawned a drainer with no   `VSCODE_PROXY_URI`, which silently killed ALL playback for every thread. The   snapshot also carries **`ADOM_BRIDGE_TARGET`** (pup; legacy ADOM_DESKTOP_TARGET still read as fallback): with >1 Bridge machine@@ -69,36 +69,36 @@ manually — deploy a new version and let supersede do the handoff. If you must   sole connected desktop) and passes `--target`. A long-lived unpinned drainer   thus picks up a target that appears later without a restart. - **FAIL AND ADVANCE, never wedge.** If the surface OPEN itself keeps erroring-  (ambiguous target, dead bridge) — never a poll, never a successful open —+  (ambiguous target, dead bridge), never a poll, never a successful open ,   `wait_for_audience` gives up (~45s, then ~8s once `surface_down` is known)   and returns `SurfaceUnavailable`; the drainer marks the clip-  `surface-unavailable` into History and ADVANCES (no retry — retrying a broken+  `surface-unavailable` into History and ADVANCES (no retry, retrying a broken   surface just re-wedges). One un-openable clip can never silence the whole   session. Field bug: 2 desktops + unpinned pup drainer → every clip stuck   `now playing 0s/0s`, queue only grew. - **Honest `status`.** `0s/0s` with no live page (poll_age > 5s) reads as-  `⚠ STALLED — no audio sink`, NOT "now playing" (which read as healthy while+  `⚠ STALLED, no audio sink`, NOT "now playing" (which read as healthy while   the surface was never open and misled a reporting thread). - **PORT STABILITY (pup wedge).** Drainers cycle (idle-close + respawn); an   ephemeral port each time STRANDS the open player page on a dead port while-  new clips queue to the new port — "player :A polls A forever while clips go+  new clips queue to the new port, "player :A polls A forever while clips go   to :B". `bind_player_server` REUSES the last port (`read_player_port`, retry   briefly for a superseding drainer to release it) so a stale page reconnects   to the fresh drainer with zero re-navigation. Hydrogen survived this via   open-or-refresh re-navigation; pup did not. Also: `--surface pup` is fragile-  (ambiguous window with >1 desktop, stale port) — SKILLS DEFAULT TO HYDROGEN+  (ambiguous window with >1 desktop, stale port), SKILLS DEFAULT TO HYDROGEN   and only use pup on explicit request. A `surface-unavailable` failure names   the working hydrogen surface, and `check`/`--confirm` say the clip is fine,   only the surface failed. - **`adom-tts restart`** = the real reset: kills the drainer, clears the LOCK   (not the portfile), respawns a FRESH drainer of the SAME surface, pinned from   the current env. `reopen` only re-offers the tab and keeps a broken player;-  `restart` replaces it. CRITICAL: restart must NOT wipe `player.json` — it once+  `restart` replaces it. CRITICAL: restart must NOT wipe `player.json`, it once   did, which forced the fresh drainer onto a NEW port while the human's page   kept polling the OLD one forever (the exact wedge port-reuse prevents). Keep   the portfile so `bind_player_server` rebinds the SAME port and the page   reconnects with no `reopen`.-- Never reopen the tab for a clip that already started (`reached > 0`) — a+- Never reopen the tab for a clip that already started (`reached > 0`), a   reopen reloads the page and REPLAYS the clip (users heard 5× repeats). - **`restart` REFUSES while a clip is playing or human-paused** (exit 5;   `--force` overrides). Field incident: the human paused for their kid, an ops@@ -107,10 +107,10 @@ manually — deploy a new version and let supersede do the handoff. If you must   /histplay but never plays the staged replay = "the ↻ button does nothing".   It also must NOT idle-close while a page is polling (a watcher may tap ↻). - **Audio age rides in the queue entry (`audio_ms`), never the snapshot's-  mtime** — the snapshot is written at enqueue and always looks fresh (this+  mtime**, the snapshot is written at enqueue and always looks fresh (this   blinded the stale-audio guard; ralph-test T6 guards it now). - **Occluded pup windows cannot play audio** (Chrome suspends the media-  pipeline). The pup player window opens `foreground:true` — see adom-tts-test+  pipeline). The pup player window opens `foreground:true`, see adom-tts-test   for the full platform facts. - **Settings, not preferences** (Adom-wide word choice; Microsoft/Google/Apple   all converged on Settings). `adom-tts settings` / `settings set surface pup`.@@ -145,14 +145,14 @@ manually — deploy a new version and let supersede do the handoff. If you must   if the drainer dies) and is actively cleared the moment a clip's audio   finishes (queue empty), after a History replay, and at idle close. A retry   keeps the flash: audio is still owed.-- **Run `tools/ralph-test.sh` before every release** — see the adom-tts-test+- **Run `tools/ralph-test.sh` before every release**, see the adom-tts-test   skill for the full matrix and the incident behind each test. - **TELEMETRY MUST BE VOUCHED FOR (`loaded=1`).** The page's `/poll` carries   `t`/`dur`/`done` read off the `<audio>` element, but `curClipId` moves the   INSTANT a new clip arrives while the element still holds the PREVIOUS clip   until its blob finishes downloading. That window reported the new id with the   old clip's `ended`/position, so the drainer marked the new clip done on-  arrival, advanced, and cut it off ~3s in — while telling its sender "ok"+  arrival, advanced, and cut it off ~3s in, while telling its sender "ok"   (field: two threads sent clips back to back; clip two died at 2.8s and 3.3s   carrying clip one's exact `reached`). FIX, both sides: the page tracks   `loadedClipId` (the id the element ACTUALLY holds), sends `loaded=0` with@@ -163,7 +163,7 @@ manually — deploy a new version and let supersede do the handoff. If you must   ralph-test T10 reproduces the exact race (`stale-done` fake-page mode). - **NEVER auto-replay audio the human already heard.** Retry is for TRULY-SILENT   clips only: `verdict == "no-playback"` (reached==0) AND `i_own_lock()`. A clip-  that produced ANY audio (`partial`) is NEVER re-queued — it goes to History,+  that produced ANY audio (`partial`) is NEVER re-queued, it goes to History,   replayable by the human's CHOICE. Two real outages came from breaking this: an   "early-partial" retry (<10% of duration) replayed clips cut off by a SUPERSEDE,   and a superseded drainer re-queued a clip the new owner then replayed. The@@ -174,9 +174,9 @@ manually — deploy a new version and let supersede do the handoff. If you must Synthesis success ≠ playback. The page reports telemetry (beacons + piggybacked on `/poll` so a connection gap can't fake a failure); the drainer logs a `clip_end` verdict (`ok/partial/no-playback/error`) per clip to-`~/.adom/tts/events.jsonl`. `--confirm` and `check` read that log — they never+`~/.adom/tts/events.jsonl`. `--confirm` and `check` read that log, they never touch the contended surface, which is why they're trustworthy. CRITICAL: the-verdict must reflect the LATEST attempt — `--confirm` keeps waiting while a+verdict must reflect the LATEST attempt, `--confirm` keeps waiting while a retry is queued/playing instead of failing on the first attempt's verdict (the confirm-vs-reality race from field feedback), and failure output includes inline evidence (reached/duration + live surface state) so no forensics@@ -192,15 +192,15 @@ answers with the clip's queue position.  **ANTI-REPLAY on reopen (distinct from enqueue dedup).** The page plays any clip whose polled `id != curClipId`, and a FRESH page (after a reopen) starts-at `curClipId=0` — so a reloaded page would replay a clip the drainer already+at `curClipId=0`, so a reloaded page would replay a clip the drainer already finished. During a flapping surface (e.g. 265 open_surface_failed → constant reopens) this played clips twice. Enqueue dedup can't catch it (it's the same queued clip replayed by a reloaded page, not a re-enqueue). FIX: once a clip-is `done`, `/poll` reports it as **idle (id 0)** so no page — current or freshly-loaded — ever starts it again; a new clip gets a fresh id. Any new "replay on+is `done`, `/poll` reports it as **idle (id 0)** so no page, current or freshly+loaded, ever starts it again; a new clip gets a fresh id. Any new "replay on reload" symptom → check this guard first. -**DEDUPLICATION — a resend must be structurally impossible to double-play.**+**DEDUPLICATION, a resend must be structurally impossible to double-play.** Every enqueue fingerprints the mp3 bytes (FNV-1a 64, the request id, stored in the queue entry AND every History row; logged as `req=` on enqueue + clip_start so a double-play is greppable by request id). Identical content already queued, or@@ -211,53 +211,53 @@ same 65-second clip twice back to back. `--allow-repeat` is the deliberate override (the human asked to hear it again). Human History-replays bypass the gate by design (they never pass through enqueue). -**CLAIM-BEFORE-PLAY — one queue entry is played AT MOST once, across drainer+**CLAIM-BEFORE-PLAY, one queue entry is played AT MOST once, across drainer deaths.** The enqueue dedup above stops a re-ENQUEUE; this stops the SAME entry being re-dequeued by a second drainer. Field case: a clip was in-flight when `restart` hard-killed its drainer; the queue entry survived, a fresh drainer re-dequeued it, and the human heard it twice (same `req`, two `clip_start` on two ports). FIX: a clip stays a normal `.json` through the audience wait (so a-drainer that dies WAITING for a slept device is still recoverable — a fresh+drainer that dies WAITING for a slept device is still recoverable, a fresh drainer plays it once), but at the instant we emit sound we CLAIM it with an atomic `rename` to `<seq>.playing`. Only the drainer that wins the rename plays it; any racing drainer's rename fails and it skips (`continue`). A retry/preempt renames the `.playing` back to `.json` (front position kept). A hard kill mid-sound leaves a `.playing` orphan, which the NEXT drainer's `sweep_stale_claims`-at `drain_start` DROPS (logged `claim_orphan_dropped`) — never replayed, because+at `drain_start` DROPS (logged `claim_orphan_dropped`), never replayed, because the human already heard part of it. Losing-after-partial beats replaying: a double-play is the rudeness we optimize against. Any new "heard it twice after a restart/supersede" symptom → check the claim rename + orphan sweep first.  ### 5. Hints back to the AI on every command-Every command output teaches the calling AI the next correct step: `ACCEPTED —+Every command output teaches the calling AI the next correct step: `ACCEPTED , position N` on enqueue, `QUEUED/PLAYING/PLAYED/FAILED` from `check` (with-"do NOT re-send a buffered clip — it'd play twice"), `⚠ AUDIENCE OFFLINE` at+"do NOT re-send a buffered clip, it'd play twice"), `⚠ AUDIENCE OFFLINE` at say-time when nobody is listening, and non-`ok` verdicts explain recovery (`logs` → `status` → `reopen` → one careful `--confirm` retry). When adding a command, add its hints; when changing behavior, update the hints in the same commit. `ADOM_TTS_NO_HINTS=1` silences them for stable scripts.  ### 6a. The human preempts; the AI queues-AI clips wait their turn in the FIFO queue — always. But a HUMAN action (a+AI clips wait their turn in the FIFO queue, always. But a HUMAN action (a history-replay tap) plays IMMEDIATELY: the drainer aborts the current AI clip (`clip_preempted`, entry kept untouched), plays the replay, then restarts the interrupted clip from the top. Never make a human wait behind machine output; never let a machine skip the queue. Preempt lands via the shared `preempt` slot checked by both the drain loop and `wait_for_clip`. -**PAUSE is a human action too — it means "stop talking to me."** The page+**PAUSE is a human action too, it means "stop talking to me."** The page reports `paused=1` on every poll; the drainer then HOLDS: no stall-advance (a-paused clip is not a stuck clip — this exact bug force-resumed a human 3+paused clip is not a stuck clip, this exact bug force-resumed a human 3 times), no deadlines, no tab reopen, no next clip, queue frozen (`pause_hold` in the log, `paused:true` in /state). Everything resumes only when the human presses play. The page also cancels pending auto-play intent on a pause gesture so a late `canplay` can't un-pause them. Never add a code path that-starts audio while `user_paused` is set — replay taps excepted (the human+starts audio while `user_paused` is set, replay taps excepted (the human asked for that sound).  **FRESHNESS TTL: held clips go stale.** A clip that has waited past the TTL-(default 2h, `ADOM_TTS_CLIP_TTL_SECS`) — buffered, pause-held, or behind a-dead page — is EXPIRED at play-time: skipped into History flagged+(default 2h, `ADOM_TTS_CLIP_TTL_SECS`), buffered, pause-held, or behind a+dead page, is EXPIRED at play-time: skipped into History flagged (`clip_expired`, verdict "expired", replayable on purpose), never played as surprise old news. Happened live: a 34-hour pause-hold resumed into yesterday's clip. `check` reports EXPIRED with a rewrite-don't-replay hint.@@ -267,21 +267,21 @@ The human misses clips (dead zones, back-to-back updates from parallel threads) and gets confused about WHO said WHAT. Two mechanisms, keep both: - **History**: every played clip is persisted (audio bytes +   `~/.adom/tts/play_history.jsonl`, capped 40) and shown in the player's-  History drawer — thread chip + description + time + verdict (non-`ok` =-  flagged "missed?") + one-tap replay via `/histplay?seq=` (plays immediately — see 6a). History entries carry the clip's words so a replay keeps its karaoke transcript. Never surface raw+  History drawer, thread chip + description + time + verdict (non-`ok` =+  flagged "missed?") + one-tap replay via `/histplay?seq=` (plays immediately, see 6a). History entries carry the clip's words so a replay keeps its karaoke transcript. Never surface raw   mp3 filenames to the human; the filename means nothing to them. - **Forced identification**: `say --play` HARD-REQUIRES `--thread` (which AI-  thread) and `--desc` (one human sentence about the update) — exit 6 with a+  thread) and `--desc` (one human sentence about the update), exit 6 with a   teaching hint otherwise. This is deliberate API design: hints alone were not   enough; anonymous clips made the shared history useless. When adding any new   human-facing surface, carry label + desc through it (queue entry → sidecar →   CurrentClip → /poll → UI). -### 7. Lint the AI's input — never speak garbage aloud+### 7. Lint the AI's input, never speak garbage aloud AIs pass what feels natural: `@/path/file.txt` (curl-style), bare paths,-stdin. Meet them where they are — `@file` and bare-existing-path are+stdin. Meet them where they are, `@file` and bare-existing-path are first-class file inputs (contents are read + a notice teaches the explicit-form) — but a path that does NOT exist, a binary file, or a >200 KB file is+form), but a path that does NOT exist, a binary file, or a >200 KB file is a HARD ERROR with a teaching hint. The human must never hear "slash tmp slash claude" read out character by character (happened live: a thread sent @-paths and the human heard the path twice). Same principle for any future@@ -291,7 +291,7 @@ input: detect the intent mismatch at the CLI boundary, teach in the error.  - Start: `adom-tts logs` (verdict summary + anomaly hints) and `adom-tts status`   (lock pid/version, current clip, queue). Raw: `~/.adom/tts/events.jsonl`.-  The summary is a 24h WINDOW and the log rotates (newest 3000 events) —+  The summary is a 24h WINDOW and the log rotates (newest 3000 events) ,   before that, cumulative counters made healed week-old incidents read as   "failing hard today" and sent reporters chasing ghosts. - "Clip played twice / flipping" → two drainers or a reopen-after-start. Check@@ -305,24 +305,48 @@ input: detect the intent mismatch at the CLI boundary, teach in the error.   named `reply.mp3` from different threads LOOK like a repeat; `clip_retry`   events show real retries. - Intermittent long-say failures with healthy `/health` → duration-correlated-  proxy timeout: keep synth chunks small (≤360 chars, sequential) — don't raise+  proxy timeout: keep synth chunks small (≤360 chars, sequential), don't raise   timeouts.-- UI changes: ALWAYS ralph-test — render the page headless, LOOK at it, and-  run an overflow probe (walk every element's boundingRect vs viewport; report-  anything past the edge). Headless Chrome clamps min window width to 500, so-  for narrower widths embed the page in an iframe of the target width. Media-  queries go LAST in the stylesheet. Exercise real event paths-  (`el.dispatchEvent(new Event('playing'))`) — the play/pause icon was broken-  for 9 versions because nothing fired the event.+- UI changes: ALWAYS ralph-test with `tools/ui-shot.py` (see adom-tts-test+  section 2): a MUTED headless Chrome driven over raw CDP against an isolated+  rig drainer, one PNG per state (idle, clip, ended + History, settings modal,+  toasts, stale pill, pill detail, corner tooltip, panel closed, phone open and+  closed, wide). LOOK at every PNG. It also prints the boot marker+  (`window.__ttsBoot`, complete + failed[]), the newbie module's selfCheck, the+  tooltip clamp rect, the panel-open vs panel-closed spacing under the header,+  and the phone scrollWidth. Media queries go LAST in the stylesheet. Exercise+  real event paths (`el.dispatchEvent(new Event('playing'))`), the play/pause+  icon was broken for 9 versions because nothing fired the event, and again in+  1.9.49's first cut because `.hidden` is not a property on SVG elements+  (toggle the ATTRIBUTE, `setAttribute('hidden','')`, never the property).+- DESIGN CONTRACT (1.9.49, John: "your app looks like a brother from another+  mother"): the player page IS the dock dashboard (dock-dashboard skill, full-app+  species: one app, one page, never a second launch page). Anatomy in order:+  the 44px house header (dash-header numbers: edge to edge, `--surface` bg, 1px+  `--line` bottom border, 18px line-drawing mark, name Familjen 600 15px in+  `--text` never teal, subtitle 13px muted, version ONLY in the brand popover,+  right controls `target: this machine` chip, ? toggle, the pack's cog), the+  newbie panel (MOUNTED from `vendor/newbie-panel.js`, served at+  `/newbie-panel.js`; never hand-rolled; hash it against+  `~/.claude/skills/adom-ui-design/assets/module/newbie-panel.js` when you+  re-vendor), the status pill row (server emits SHORT `state` + prose `detail`+  per subsystem in `/poll.health` and `/api/health` with CORS `*`; gray until+  probed; a red detail leads with the exact command), then the player. Tokens+  are the dock-dashboard stub verbatim; icons are line drawings (fill none,+  stroke 1.8, currentColor inline, `#e6edf3` hardcoded in `icons/app.svg`, the+  ONE file that is the favicon, the header mark, the tab strip icon and+  `webview.favicon` in `dockbar.json`). No em-dashes, no emoji, no `title=`+  attributes (data-tooltip only), no uppercase eyebrows, no max-width on the+  page (prose blocks may cap at 90ch). - LAYOUT LAWS (user-mandated, non-negotiable):   1. The outermost html NEVER shows a horizontal scrollbar. Fluid layout does      the work (`min-width:0` on flex children, ellipsize long text); the root      also carries `overflow-x: clip` as a guarantee.-  2. Scrollbars are always styled — slim (7px), rounded, transparent track,+  2. Scrollbars are always styled, slim (7px), rounded, transparent track,      subtle teal thumb. Never ship a default chunky scrollbar.   3. Motion is GENTLE: no per-poll pulses/jumps (the LED breathes on a ~3s      ease; it does not blink per heartbeat).-  4. The header shows the human WHO (thread) + WHAT (desc) — never a raw mp3+  4. The header shows the human WHO (thread) + WHAT (desc), never a raw mp3      filename except in a tooltip.   5. NO EMOJIS in READMEs or skills, ever (user rule: they read as childish).      Plain typographic marks are fine; the only pictograph-adjacent characters@@ -333,6 +357,6 @@ input: detect the intent mismatch at the CLI boundary, teach in the error.  ## Service coupling -`adom-tts` is a client of `service-tts` (shared container — NEVER install a+`adom-tts` is a client of `service-tts` (shared container, NEVER install a local TTS engine as a fallback; report outages instead). Client-side synth robustness lives in `synth_chunk`/`split_into_chunks` in `src/main.rs`.
dev-skills/adom-tts-test/SKILL.md+32−8
@@ -9,7 +9,7 @@ description: >-   player, pre-release check, did the buttons break. --- -# adom-tts-test — the regression matrix that keeps the player honest+# adom-tts-test, the regression matrix that keeps the player honest  Every test here is a REAL field incident from the hardening week (2026-07/08), reported by the user while driving, parenting, and working. The lesson of that@@ -47,19 +47,43 @@ Extending: add a `say "TN ..."` block copying an existing test. Trigger the behavior via the real CLI, assert via events.jsonl (`ev <kind>`) or the queue dir. Never assert on hint text (it changes); assert on event kinds and exits. -## 2. UI checks (need a real browser: pup, muted, tidy)+## 2. UI checks (tools/ui-shot.py: headless, muted, isolated) -Backend green does not mean buttons work — T4's bug shipped while all backend-paths were "fine". Drive the real page for:+Backend green does not mean buttons work, T4's bug shipped while all backend+paths were "fine". The UI harness drives the page in a MUTED headless Chrome+over raw CDP (python3 + websockets, no puppeteer) against an isolated rig+drainer, so it never touches the human's player:++```bash+RIG=$(mktemp -d); mkdir -p $RIG/.adom/tts+SAFE=$(mktemp -d); for t in curl sh sed grep cat; do ln -sf "$(command -v $t)" $SAFE/$t; done   # no adom-cli/adom-bridge+HOME=$RIG PATH=$SAFE VSCODE_PROXY_URI= ADOM_TTS_KEEP_OPEN=1 ./target/release/adom-tts open --print-url   # prints the rig URL+cp <some>.mp3 $RIG/reply.mp3; cp <some>.json $RIG/reply.json; touch $RIG/reply.mp3+(sleep 4; HOME=$RIG PATH=$SAFE VSCODE_PROXY_URI= ./target/release/adom-tts play $RIG/reply.mp3 --allow-repeat) &+python3 tools/ui-shot.py http://127.0.0.1:<port>/ /tmp/shots     # then READ every PNG+kill $(head -1 $RIG/.adom/tts/drainer.lock)+```++It prints, and you must read: `boot` (`complete:true, failed:[]` or the page+half-booted), the newbie module `selfCheck` (`ok:true`), `icons while playing`+(pause shown, play hidden, warning hidden on a fresh clip), the tooltip rect+(`inside:true`), the panel-open vs panel-closed spacing under the header (must+match: 60px), and the phone `scrollW == vw`. The PNGs: 01 idle, 02 clip, 02b+ended + History, 03 settings, 04 toasts, 05 stale pill, 06 pill detail, 07+tooltip, 08 panel closed, 09/10 phone closed/open, 11 wide. If a PNG looks+wrong, the harness passed and the page is still wrong: fix the page.++For the things only a real browser on the user's box can show (autoplay+policy, a real Hydrogen tab, the dock card), drive the real page for:  - **Freshness pill + tooltip**: mock an old clip (`setClipAge(Date.parse("..."),   Date.now())` via eval), assert the pill text/color, fire the tooltip and run   the tooltips skill's MANDATORY on-screen assert (rect inside viewport).-- **Trusted-click the controls**: play, pause, resume, seek, History ↻ — via+- **Trusted-click the controls**: play, pause, resume, seek, History ↻, via   `pup_click`, not eval (eval carries no user gesture; clicks do). - **Version badge** matches the shipped build. -### Rig etiquette (each one is a user complaint — follow all of them)+### Rig etiquette (each one is a user complaint, follow all of them) 1. **NEVER blast test audio at the human.** Isolated $HOME rig + MUTE the page    audio element FIRST (`a.muted=true;a.volume=0` + a 1s re-mute interval). 2. **One window.** Player tab + shotlog tab in ONE pup session; close it when@@ -67,7 +91,7 @@ paths were "fine". Drive the real page for: 3. **Never touch the real player tab.** The rig drainer must run with PATH    stripped of adom-cli/adom-bridge so its surface-open cannot reach the    user's workspace.-4. **Shotlog every screenshot** (`shotlog paste -c <channel>`) — the user (and+4. **Shotlog every screenshot** (`shotlog paste -c <channel>`), the user (and    you) can only SEE shots there.  ## 3. Hard-won platform facts (do not relearn these)@@ -76,7 +100,7 @@ paths were "fine". Drive the real page for:   pipeline in fully-occluded background windows: the page polls, blob download   succeeds, but `netState` sticks at LOADING forever; raise the window and it   plays instantly. This was the true root of "--surface pup never plays". The-  pup player window must be opened `foreground:true` — an audio surface the+  pup player window must be opened `foreground:true`, an audio surface the   user chose must be visible. - **`eval play()` is not a user gesture.** Autoplay unlock needs a TRUSTED   click (`pup_click`) or the visible-window + drainer kick path.
dockbar.jsonadded+29
@@ -0,0 +1,29 @@+{+  "dock_manifest": "1.2.0",+  "webview": { "title": "Adom TTS", "favicon": "icons/app.svg" },+  "launch": {+    "mode": "container_command",+    "console": "show",+    "steps": [+      {+        "label": "Installing Adom TTS",+        "run": "sh -c 'PATH=\"$HOME/.local/bin:$PATH\"; adom-wiki pkg update adom/adom-tts || adom-wiki pkg install adom/adom-tts'",+        "skip_if": "sh -c 'PATH=\"$HOME/.local/bin:$PATH\"; command -v adom-tts'",+        "timeout_seconds": 180+      },+      {+        "label": "Starting the player",+        "run": "sh -c 'PATH=\"$HOME/.local/bin:$PATH\"; exec adom-tts open --print-url'",+        "await": "url",+        "timeout_seconds": 60+      }+    ],+    "on_ready": "navigate_placeholder",+    "ai": {+      "prompt": "The user just opened Adom TTS from the dock. Ask what they want read aloud, or offer to play the built-in demo with `adom-tts demo`.",+      "thread": "existing",+      "thread_fallback": "new",+      "submit": "staged"+    }+  }+}
docs/icon.svg+11−8
@@ -1,9 +1,12 @@-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">-  <g fill="#e6edf3">-    <rect x="8"  y="28" width="6" height="8"  rx="2"/>-    <rect x="18" y="22" width="6" height="20" rx="2"/>-    <rect x="28" y="14" width="6" height="36" rx="2"/>-    <rect x="38" y="22" width="6" height="20" rx="2"/>-    <rect x="48" y="28" width="6" height="8"  rx="2"/>-  </g>+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"+     fill="none" stroke="#e6edf3" stroke-width="1.8"+     stroke-linecap="round" stroke-linejoin="round">+  <!-- Adom TTS mark: a speaker cone with two sound waves. Line drawing on a+       transparent ground (icons skill). Hardcoded #e6edf3 because this file is+       loaded as its own document (favicon, tab strip, dock card) where+       currentColor would resolve to black. The inline header copy in+       player.html swaps the stroke to currentColor. -->+  <path d="M4 9.5h3.2L12 5.5v13l-4.8-4H4z"/>+  <path d="M15.5 9.2a4 4 0 0 1 0 5.6"/>+  <path d="M18.4 6.6a8 8 0 0 1 0 10.8"/> </svg>
icons/app.svgadded+12
@@ -0,0 +1,12 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"+     fill="none" stroke="#e6edf3" stroke-width="1.8"+     stroke-linecap="round" stroke-linejoin="round">+  <!-- Adom TTS mark: a speaker cone with two sound waves. Line drawing on a+       transparent ground (icons skill). Hardcoded #e6edf3 because this file is+       loaded as its own document (favicon, tab strip, dock card) where+       currentColor would resolve to black. The inline header copy in+       player.html swaps the stroke to currentColor. -->+  <path d="M4 9.5h3.2L12 5.5v13l-4.8-4H4z"/>+  <path d="M15.5 9.2a4 4 0 0 1 0 5.6"/>+  <path d="M18.4 6.6a8 8 0 0 1 0 10.8"/>+</svg>
page.json−21
@@ -12,7 +12,6 @@     "audio",     "demo",     "accessibility",-    "cli",     "open-source",     "mit"   ],@@ -65,25 +64,5 @@   ],   "install": {     "binary_name": "adom-tts"-  },-  "description": "Adom's shared text-to-speech CLI + Playground. Reads your AI answers back to you while driving, and renders perfectly-timed voiceover tracks for demo videos.",-  "dependencies": {},-  "scripts": {-    "install": "./install.sh",-    "uninstall": "./uninstall.sh"-  },-  "needs_sudo": false,-  "files": [-    "package.json",-    "install.sh",-    "uninstall.sh",-    "bin/adom-tts",-    "skills/**",-    "completions/**"-  ],-  "org": "adom",-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"   } }\ No newline at end of file
publish-skills/adom-tts-publish/SKILL.md+27−19
@@ -2,9 +2,9 @@ name: adom-tts-publish description: >-   Cut and publish a new adom-tts release to THE Adom Wiki. Single source of-  truth for WHERE this app lives (https://wiki.adom.inc/adom/adom-tts — org+  truth for WHERE this app lives (https://wiki.adom.inc/adom/adom-tts, org   `adom`, slug `adom-tts`) and HOW to ship it with the adom-wiki CLI (adompkg-  and all *.adom.cloud slug hosts are DEPRECATED — never use them). Covers the+  and all *.adom.cloud slug hosts are DEPRECATED, never use them). Covers the   version bump, the test gate, packaging, `adom-wiki pkg publish`, `repo push`   for source + dev/publish skills, keeping the hero linked, deprecating a bad   version, and the mandatory verify-in-pup step. Triggers: publish adom-tts,@@ -16,11 +16,11 @@ description: >-  **Where this app lives (memorize):** - Page: `https://wiki.adom.inc/adom/adom-tts` (org **adom**, slug **adom-tts**, type app)-- Page title: **Adom TTS — Text to Speech** (the hero headline matches it verbatim)-- CLI: **`adom-wiki`** — the ONLY wiki tool. `adompkg`, "wiki v1/v2", and any+- Page title: **Adom TTS, Text to Speech** (the hero headline matches it verbatim)+- CLI: **`adom-wiki`**, the ONLY wiki tool. `adompkg`, "wiki v1/v2", and any   `*.adom.cloud` slug host are DEPRECATED; if a doc says otherwise it is stale. - The wiki has TWO unsynced layers: the **package registry** (`pkg publish` /-  `pkg install` — what installers get) and the **git page repo** (`repo push` —+  `pkg install`, what installers get) and the **git page repo** (`repo push` ,   the Files tab, README, dev-skills/, publish-skills/, screenshots). Ship BOTH.  ## Release steps@@ -41,13 +41,13 @@ description: >-    ./build.sh && install -m 0755 target/release/adom-tts ~/.local/bin/adom-tts    ``` -3. **Publish the package — the binary ships INSIDE it. NO release uploads.**+3. **Publish the package, the binary ships INSIDE it. NO release uploads.**    Read the adom-wiki skill carefully: packages are resolved PER PLATFORM and    the skill's own example says "a Linux consumer installs the package (CLI +    skills)". Release assets exist ONLY for artifacts fetched OUTSIDE the    install flow (a Windows installer a laptop downloads directly, pup's bridge-   zip that Bridge (ab) fetches itself). adom-tts has no such consumer —-   every user is a Linux container running `pkg install` — so a release for+   zip that Bridge (ab) fetches itself). adom-tts has no such consumer ,+   every user is a Linux container running `pkg install`, so a release for    adom-tts makes NO sense. (Got this wrong twice: first shipped src+pngs+    binary in a fat tarball AND a duplicate release; then over-corrected to a    skills-only tarball with the binary as a release. Correct: lean allowlist@@ -62,12 +62,20 @@ description: >-    Then prove the loop: `HOME=$(mktemp -d) adom-wiki pkg install adom/adom-tts`    and run `$HOME/.local/bin/adom-tts --version` from that fresh home. -4. **Push the source** — README, src/, screenshots, page.json, and the dev +-   publish skills (they reach the page ONLY via repo push; the Skills tab shows-   them under its Dev / Publish sub-tabs):+4. **Push the source**, README, src/, screenshots, page.json, `dockbar.json`+   (the dock manifest), `icons/app.svg` (the dock card + tab favicon; the SAME+   file the page serves as `/favicon.svg`), `vendor/newbie-panel.js`, tools/,+   and the dev + publish skills (they reach the page ONLY via repo push; the+   Skills tab shows them under its Dev / Publish sub-tabs):    ```bash-   adom-wiki repo push adom/adom-tts -m "vX.Y.Z — <changelog>"+   adom-wiki repo push adom/adom-tts -m "vX.Y.Z, <changelog>"    ```+   Dock checks before pushing: `dockbar.json` parses, `webview.title` is still+   `Adom TTS` (renaming it makes duplicate tabs: the drainer's tab name is the+   same string, `PLAYER_TAB_NAME`), the serve step is `adom-tts open+   --print-url` and RETURNS (run it by hand: one URL on stdout, exit 0 in+   under a second), and `/api/health` answers with `Access-Control-Allow-Origin: *`.+   A user picks up the new manifest with Hard Refresh Open on the card.  5. **Keep the hero linked.** The hero lives in `page.json` ONLY (never embed it    in the README):@@ -79,14 +87,14 @@ description: >-    `adom-wiki-hero` skill (fresh REAL player screenshot, bleed-bottom/midnight,    title = the page title verbatim). -6. **Verify live — in pup, always** (never claim published without looking):+6. **Verify live, in pup, always** (never claim published without looking):    ```bash    adom-bridge --target <laptop> pup_open_window '{"sessionId":"wiki-check","url":"https://wiki.adom.inc/adom/adom-tts?cb=<epoch>"}'    adom-bridge --target <laptop> pup_screenshot '{"sessionId":"wiki-check"}'   # read the PNG it saves    ```    Check: version, hero renders, Skills tab shows User + Dev + Publish tiers,    README intact, Install command correct, and NO EMOJIS anywhere in the README-   or skills (user rule — plain typographic marks only).+   or skills (user rule, plain typographic marks only).  7. **Prove playback end-to-end** (the release isn't done until a human can    hear it):@@ -100,24 +108,24 @@ description: >- We have not launched: leaving "[DEPRECATED]" garbage around is worse than removing it. A bad package VERSION: fix, bump, republish (registry versions are immutable; the new version supersedes). A stale PAGE, hero, or file: delete it-outright — `adom-wiki repo rm <ref> <path>` for files, `adom-wiki repo delete+outright, `adom-wiki repo rm <ref> <path>` for files, `adom-wiki repo delete <ref> --hard --confirm` for whole pages. Only reach for deprecation notices after we launch and third parties depend on things.  ## Skills live IN this repo (user rule) -Never create a standalone wiki skill page that just points at adom-tts — all+Never create a standalone wiki skill page that just points at adom-tts, all skills associated with the app belong in THIS repo's tiers (skills/, dev-skills/, publish-skills/) so they version and ship with the app. Example: tts-pronunciation was a scattered standalone page and is now skills/tts-pronunciation/ here. The one sanctioned standalone is-adom/hands-free — a cross-cutting trigger router, kept deliberately thin.+adom/hands-free, a cross-cutting trigger router, kept deliberately thin.  ## A rename is not just a slug change  Renaming a page = slug + title + brief/description + SKILL.md name + install paths + discovery triggers + THE HERO (the old pixels carry the old-name — regenerate it with the adom-wiki-hero skill) + deleting old-name art+name, regenerate it with the adom-wiki-hero skill) + deleting old-name art and directories everywhere (wiki files, ~/.claude/skills, installers). The im-driving → hands-free rename missed the hero on the first pass; don't repeat that.@@ -127,4 +135,4 @@ repeat that. - Other containers pick the release up via their wiki auto-update hook; inside   THIS container run the byte-compare sweep (dev skill) so every copy matches. - The running drainer self-supersedes to the new version on the next-  `say --play` — do not kill it.+  `say --play`, do not kill it.
screenshots/history-drawer.png
⋯ 1 unchanged line ⋯
screenshots/player-live.png
⋯ 1 unchanged line ⋯
src/main.rs+47
@@ -229,6 +229,23 @@ enum Cmd {         #[command(subcommand)]         action: QueueAction,     },+    /// Show the player: make sure a drainer is up (spawning one for the+    /// webview surface if none is live), wait for its page to answer, and+    /// return. This is the Hydrogen dock's serve step: `--print-url` prints+    /// only the direct loopback URL on stdout and exits immediately.+    Open {+        /// Print just `http://127.0.0.1:<port>/` (the dock's readiness gate).+        #[arg(long)]+        print_url: bool,+    },+    /// Drive the live player page from the CLI (the AI narrating over the app).+    Ui {+        #[command(subcommand)]+        action: UiAction,+    },+    /// Play the built-in three-marker demo on the live player (what the+    /// newbie panel's "Play the demo" button runs). Blocks until heard.+    Demo,     /// GET /tts/voices — list edge-tts voices as JSON     Voices,     /// GET /tts/pronunciations — list the override table@@ -294,6 +311,17 @@ enum PronCmd {     }, } +#[derive(Subcommand)]+enum UiAction {+    /// Raise a toast on the player page (bottom center, about 4s; errors stick).+    Toast {+        text: String,+        /// info | success | error+        #[arg(long, default_value = "info")]+        kind: String,+    },+}+ #[derive(Subcommand)] enum QueueAction {     /// Drop waiting clips: `--mine <thread>` (only yours) or `--all` (every thread).@@ -382,6 +410,22 @@ fn cmd_load(text: &str, thread: &str, id: Option<String>, voice: &str, rate: &st     Ok(()) } +/// The built-in demo: three markers that teach the player by using it. It+/// exercises the real script-and-cue path (issue #4), so the demo is also a+/// live test of marker playback. Each cue blocks until that marker is heard.+fn cmd_demo() -> Result<()> {+    const SCRIPT: &str = "Hi, this is Adom TTS, your AI's voice. Any AI thread can send me text, and I read it aloud right here, in one shared player, in the order it arrived.\n---\nWatch the transcript: the words light up as I say them, and the freshness pill under it tells you when this audio was made, so an old clip can never pass as news.\n---\nThat is the tour. Try it yourself: say to your AI, with adom tts, read me your last answer. I will take it from there.";+    let _ = play::ui_toast("Demo starting: three short markers", "info");+    cmd_load(SCRIPT, "adom-tts demo", Some("demo".to_string()), "en-US-AndrewNeural", "+0%", true)?;+    for n in 1..=3 {+        let _ = play::ui_toast(&format!("Demo marker {n} of 3"), "info");+        play::cue("demo", n, true)?;+    }+    let _ = play::ui_toast("Demo finished", "success");+    println!("OK: demo played (3 markers)");+    Ok(())+}+ fn preview(s: &str) -> String {     let one: String = s.split_whitespace().collect::<Vec<_>>().join(" ");     if one.chars().count() > 64 { format!("\"{}…\"", one.chars().take(64).collect::<String>()) } else { format!("\"{one}\"") }@@ -446,6 +490,9 @@ fn main() -> Result<()> {         },         Cmd::Load { text, thread, id, voice, rate, no_pronunciations } =>             cmd_load(&text, &thread, id, &voice, &rate, !no_pronunciations),+        Cmd::Open { print_url } => play::open(print_url),+        Cmd::Ui { action: UiAction::Toast { text, kind } } => play::ui_toast(&text, &kind),+        Cmd::Demo => cmd_demo(),         Cmd::Cue { script, marker, confirm } => play::cue(&script, marker, confirm),         Cmd::CueStatus { script } => play::cue_status(&script),         Cmd::Pron(PronCmd::List) => cmd_get("/tts/pronunciations"),
src/play.rs+248−2
@@ -32,7 +32,20 @@ use std::time::{Duration, Instant}; use tiny_http::{Header, Method, Response, Server, StatusCode};  const PLAYER_HTML: &str = include_str!("player.html");-const PLAYER_TAB_NAME: &str = "TTS Player";+/// The player tab's name. It is ALSO `webview.title` in dockbar.json on purpose:+/// the dock pre-opens a tab by that title and the drainer's open-or-refresh+/// reuses a tab by name, so the dock's tab and the drainer's tab are the same+/// tab. Renaming either side alone creates a duplicate tab.+const PLAYER_TAB_NAME: &str = "Adom TTS";+/// Pre-1.9.49 tab name, closed once on startup so an upgrade never leaves two.+const LEGACY_TAB_NAME: &str = "TTS Player";+const NEWBIE_PANEL_JS: &str = include_str!("../vendor/newbie-panel.js");+const APP_ICON_SVG: &str = include_str!("../icons/app.svg");+/// The voice service the drainer probes for the status pill row.+fn service_url() -> String {+    std::env::var("ADOM_TTS_API").unwrap_or_else(|_| env!("SERVICE_TTS_URL").to_string())+        .trim_end_matches('/').to_string()+} const SERVER_DEFAULT_TIMEOUT_SECS: u64 = 60; const SERVER_MAX_TIMEOUT_SECS: u64 = 300; @@ -903,7 +916,15 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> {     let player = start_player_server()?;     write_player_portfile(player.port);     let url = server::webview_url(player.port);+    // One-time upgrade tidy: the tab used to be called "TTS Player". Close it+    // before opening "Adom TTS" so an upgrade never leaves two player tabs.+    if surface == Surface::Hydrogen && std::env::var("VSCODE_PROXY_URI").is_ok() {+        let _ = close_hydrogen_tab(LEGACY_TAB_NAME);+    }     open_player_surface(surface, &url, &cfg.play);+    // Opening the tab flashes it; with nothing to play (a dock click, `open`)+    // that flash is noise. The flash is re-armed at every clip start below.+    if surface == Surface::Hydrogen && queue_depth(&dir) == 0 { server::clear_tab_alert(PLAYER_TAB_NAME); }      let mut next_id: u64 = 1;     let mut superseded = false;@@ -931,6 +952,7 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> {                         created_ms: file_created_ms(Path::new(&file)),                     });                     log_event("clip_start", serde_json::json!({ "clip": name, "port": player.port, "surface": surface.as_str(), "id": id, "replay": true }));+                    if surface == Surface::Hydrogen { server::set_tab_alert(PLAYER_TAB_NAME); }                     let _ = wait_for_clip(&player.shared, id, surface, &url, &cfg.play, now_ms());                     let _ = log_clip_end(&player.shared, &name);                     // replay done → the tab's blink has served its purpose@@ -1058,6 +1080,7 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> {                                 created_ms: audio_ms,                             });                             log_event("clip_start", serde_json::json!({ "clip": name, "port": player.port, "surface": surface.as_str(), "id": id, "req": &chash }));+                            if surface == Surface::Hydrogen { server::set_tab_alert(PLAYER_TAB_NAME); }                             let expired_mid = wait_for_clip(&player.shared, id, surface, &url, &cfg.play, enq_ms);                             // Interrupted by a human replay tap? Not a failure —                             // keep the entry untouched; it restarts after the replay.@@ -1734,6 +1757,12 @@ struct PlayerShared {     // interrupting the current AI clip (which re-queues and restarts after).     // Principle: the human preempts; the AI queues.     preempt: std::sync::Mutex<Option<serde_json::Value>>,+    // Voice-service probe for the status pill row: (0 unprobed, 1 ok, 2 bad),+    // short state text, prose detail (a bad state names the next command).+    service: std::sync::Mutex<(u8, String, String)>,+    // `adom-tts ui toast` / POST /ui/toast: the page shows it once (by seq).+    toast_seq: AtomicU64,+    toast: std::sync::Mutex<serde_json::Value>, } impl PlayerShared {     /// Milliseconds since the page last polled. `u64::MAX` before any poll →@@ -1798,6 +1827,8 @@ fn start_player_server() -> Result<Player> {         events: std::sync::Mutex::new(Vec::new()),         cmd_seq: AtomicU64::new(0), cmd_str: std::sync::Mutex::new(String::new()),         queued: AtomicU64::new(0), served: AtomicU64::new(0), last_poll_ms: AtomicU64::new(0),+        service: std::sync::Mutex::new((0, "checking".into(), "Probing the voice service.".into())),+        toast_seq: AtomicU64::new(0), toast: std::sync::Mutex::new(serde_json::Value::Null),         user_paused: AtomicBool::new(false),             skipped: AtomicBool::new(false),         surface_down: AtomicBool::new(false),@@ -1806,6 +1837,35 @@ fn start_player_server() -> Result<Player> {     let html = Arc::new(player_page_html());     let stop = Arc::new(AtomicBool::new(false)); +    // STATUS PILL PROBE: the voice service, checked every 60s from the drainer+    // (never from the page: the page cannot reach it cross-origin and a pill+    // must show one server-side truth). Gray until the first probe answers;+    // red carries the exact next command.+    {+        let shared_p = shared.clone();+        let stop_p = stop.clone();+        thread::spawn(move || {+            let client = reqwest::blocking::Client::builder()+                .timeout(Duration::from_secs(6)).build().ok();+            let url = service_url();+            loop {+                if stop_p.load(Ordering::SeqCst) { break; }+                let probe = client.as_ref().map(|c| c.get(format!("{url}/health")).send());+                let (code, short, detail) = match probe {+                    Some(Ok(r)) if r.status().is_success() =>+                        (1u8, "answering".to_string(), format!("Voice service at {url} is answering.")),+                    Some(Ok(r)) => (2u8, format!("http {}", r.status().as_u16()),+                        format!("Voice service at {url} answered HTTP {}. Run `adom-tts health` for the body, then check the service-tts container.", r.status().as_u16())),+                    Some(Err(e)) => (2u8, "not answering".to_string(),+                        format!("Voice service at {url} did not answer ({}). Check your network, then run `adom-tts health`.", e.to_string().chars().take(80).collect::<String>())),+                    None => (2u8, "no client".to_string(), "Could not build an HTTP client. Run `adom-tts health`.".to_string()),+                };+                if let Ok(mut g) = shared_p.service.lock() { *g = (code, short, detail); }+                for _ in 0..60 { if stop_p.load(Ordering::SeqCst) { return; } thread::sleep(Duration::from_secs(1)); }+            }+        });+    }+     let server_t = server.clone();     let shared_t = shared.clone();     let stop_t = stop.clone();@@ -2133,11 +2193,77 @@ fn serve_audio(request: tiny_http::Request, query: &str, shared: &Arc<PlayerShar }  /// Route one request against the persistent player's shared state.-fn handle_player_request(request: tiny_http::Request, method: &Method, route: &str, query: &str,+fn handle_player_request(mut request: tiny_http::Request, method: &Method, route: &str, query: &str,     shared: &Arc<PlayerShared>, html: &Arc<String>) -> std::io::Result<()> {     match (method, route) {         (Method::Get, "/" | "/index.html" | "") =>             respond_inline(request, 200, "text/html; charset=utf-8", html.as_bytes().to_vec()),+        // ONE icon file for the favicon, the header mark, the tab strip and the+        // dock card (icons skill): the page links `favicon.svg` relatively.+        (Method::Get, "/favicon.svg" | "favicon.svg" | "/favicon.ico" | "favicon.ico" | "/icons/app.svg" | "icons/app.svg") =>+            respond_inline(request, 200, "image/svg+xml", APP_ICON_SVG.as_bytes().to_vec()),+        // The newbie panel module, vendored into this package and served as a+        // FILE (newbie-panel skill: never inlined, never read from the pack path).+        (Method::Get, "/newbie-panel.js" | "newbie-panel.js") =>+            respond_inline(request, 200, "application/javascript; charset=utf-8", NEWBIE_PANEL_JS.as_bytes().to_vec()),+        // Dock health probe (cross-origin from the Hydrogen shell, so CORS *).+        // The same truth the page's pill row renders: one state, two views.+        (Method::Get, "/api/health" | "api/health") => {+            let body = health_json(shared).to_string();+            let headers = vec![+                Header::from_bytes(&b"Content-Type"[..], b"application/json".to_vec()).unwrap(),+                Header::from_bytes(&b"Access-Control-Allow-Origin"[..], b"*".to_vec()).unwrap(),+                Header::from_bytes(&b"Cache-Control"[..], b"no-store".to_vec()).unwrap(),+            ];+            let len = body.len();+            request.respond(Response::new(StatusCode(200), headers, Cursor::new(body.into_bytes()), Some(len), None))+        }+        // Settings, read and written by the gear modal (settings-layout skill).+        // Writes go through the same config::save_setting the CLI uses.+        (Method::Get, "/settings" | "settings") => {+            let body = settings_json().to_string();+            respond_inline(request, 200, "application/json", body.into_bytes())+        }+        (Method::Post, "/settings" | "settings") => {+            let mut raw = String::new();+            let _ = std::io::Read::read_to_string(request.as_reader(), &mut raw);+            let v: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::Value::Null);+            let key = v.get("key").and_then(|k| k.as_str()).map(str::to_string).unwrap_or_else(|| qval(query, "key"));+            let value = v.get("value").and_then(|k| k.as_str()).map(str::to_string).unwrap_or_else(|| qval(query, "value"));+            let body = match config::save_setting(&key, &value) {+                Ok(()) => {+                    log_event("setting_changed", serde_json::json!({ "key": key, "value": value, "via": "page" }));+                    serde_json::json!({ "ok": true, "settings": settings_json(),+                        "note": if key == "surface" { "Takes effect on the next player spawn (adom-tts restart when idle)." } else { "" } })+                }+                Err(e) => serde_json::json!({ "ok": false, "error": e.to_string() }),+            };+            respond_inline(request, 200, "application/json", body.to_string().into_bytes())+        }+        // The AI narrates over the running app (adom-ui-design section 3):+        // `adom-tts ui toast "..."` lands here; the page shows it once.+        (Method::Post, "/ui/toast" | "ui/toast") | (Method::Get, "/ui/toast" | "ui/toast") => {+            let mut raw = String::new();+            let _ = std::io::Read::read_to_string(request.as_reader(), &mut raw);+            let v: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::Value::Null);+            let text = v.get("text").and_then(|t| t.as_str()).map(str::to_string).unwrap_or_else(|| qval(query, "text"));+            let kind = v.get("kind").and_then(|t| t.as_str()).map(str::to_string).unwrap_or_else(|| qval(query, "kind"));+            let kind = if kind.is_empty() { "info".to_string() } else { kind };+            if text.trim().is_empty() {+                return respond_inline(request, 400, "application/json", b"{\"ok\":false,\"error\":\"text is required\"}".to_vec());+            }+            if let Ok(mut t) = shared.toast.lock() { *t = serde_json::json!({ "text": text, "kind": kind }); }+            let seq = shared.toast_seq.fetch_add(1, Ordering::SeqCst) + 1;+            respond_inline(request, 200, "application/json", format!("{{\"ok\":true,\"seq\":{seq}}}").into_bytes())+        }+        // The newbie panel's Play-the-demo button: run the built-in demo as a+        // detached CLI process (it loads a 3-marker script and cues it).+        (Method::Post, "/demo" | "demo") | (Method::Get, "/demo" | "demo") => {+            let ok = std::env::current_exe().ok().and_then(|exe| Command::new(exe).arg("demo")+                .stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().ok()).is_some();+            log_event("demo_requested", serde_json::json!({ "ok": ok, "via": "page" }));+            respond_inline(request, 200, "application/json", format!("{{\"ok\":{ok}}}").into_bytes())+        }         (Method::Get, "/audio" | "audio") => serve_audio(request, query, shared),         // The page's single poll: heartbeat + current clip + control command.         // It also CARRIES telemetry (?id&t&dur&done) — after a hotspot gap the@@ -2207,6 +2333,12 @@ fn handle_player_request(request: tiny_http::Request, method: &Method, route: &s                 "prefetch": prefetch_list(),                 "queued": shared.queued.load(Ordering::SeqCst),                 "now_ms": now_ms(),+                "version": MY_VERSION,+                // status pill row (status-leds skill): SHORT state emitted by the+                // server, prose detail on tap. The page never slices prose.+                "health": health_json(shared),+                "toast_seq": shared.toast_seq.load(Ordering::SeqCst),+                "toast": shared.toast.lock().map(|t| t.clone()).unwrap_or(serde_json::Value::Null),                 // strip words from the listing — the page only needs them when a                 // replay actually starts (they'd bloat every 700ms poll otherwise)                 "history": read_play_history(12).into_iter().map(|mut e| {@@ -2596,6 +2728,120 @@ fn sidecar_meta_line(mp3: &Path) -> String {     parts.join(" · ") } +/// The status pill row's truth, shared by `/poll` (the page) and `/api/health`+/// (the dock card). Each subsystem carries a SHORT `state` for the pill and+/// prose `detail` for the tap-to-expand; a non-green detail leads with the+/// exact next command (status-leds skill).+fn health_json(shared: &Arc<PlayerShared>) -> serde_json::Value {+    let clip = shared.clip.lock().map(|c| c.clone()).unwrap_or_default();+    let done = shared.done.load(Ordering::SeqCst);+    let paused = shared.user_paused.load(Ordering::SeqCst);+    let queued = shared.queued.load(Ordering::SeqCst);+    let (svc_code, svc_state, svc_detail) = shared.service.lock().map(|g| g.clone()).unwrap_or((0, "checking".into(), String::new()));+    let svc_led = match svc_code { 1 => "ok", 2 => "bad", _ => "unknown" };+    let cfg = config::load();+    let surface = live_drainer_surface().unwrap_or_else(|| config::resolve_surface(None, &cfg.play));+    let surface_word = match surface { Surface::Hydrogen => "webview", Surface::Pup => "pup" };+    let surface_down = shared.surface_down.load(Ordering::SeqCst);+    let (queue_led, queue_state, queue_detail) = if clip.id != 0 && !done && paused {+        ("busy", "paused".to_string(), format!("A clip is paused by the listener; {queued} waiting behind it. Nothing plays until they press play, or an AI runs `adom-tts control play`."))+    } else if clip.id != 0 && !done {+        ("ok", if queued > 0 { format!("playing, {queued} waiting") } else { "playing".to_string() },+            format!("Playing \"{}\"; {queued} clip(s) waiting in the shared queue.", clip.label.as_str().trim().is_empty().then_some(clip.title.as_str()).unwrap_or(clip.label.as_str())))+    } else if queued > 0 {+        ("busy", format!("{queued} waiting"), format!("{queued} clip(s) waiting; the next one starts as soon as the page is ready."))+    } else {+        ("ok", "idle".to_string(), "Nothing playing and nothing queued. Any AI thread's `adom-tts say --play` lands here.".to_string())+    };+    serde_json::json!({+        "ok": !surface_down && svc_code != 2,+        "version": MY_VERSION,+        "state": if surface_down { "surface down" } else if clip.id != 0 && !done { "playing" } else { "serving" },+        "player": { "led": "ok", "state": format!("v{MY_VERSION} serving"),+            "detail": format!("Player v{MY_VERSION} is serving this page from the drainer (pid {}). Logs: `adom-tts logs`.", std::process::id()) },+        "surface": { "led": if surface_down { "bad" } else { "ok" },+            "state": if surface_down { "not opening".to_string() } else { surface_word.to_string() },+            "detail": if surface_down { format!("The {surface_word} surface failed to open. Run `adom-tts reopen`, or switch the default in Settings and `adom-tts restart` when idle.") }+                      else { format!("Clips play in the {surface_word} surface. Change the default with the gear (Settings) or `adom-tts settings set surface pup|webview`.") } },+        "service": { "led": svc_led, "state": svc_state, "detail": svc_detail },+        "queue": { "led": queue_led, "state": queue_state, "detail": queue_detail },+    })+}++/// The settings the gear modal shows: the persisted config plus its path.+fn settings_json() -> serde_json::Value {+    let cfg = config::load();+    serde_json::json!({+        "surface": cfg.play.surface.as_str(),+        "pup_session": cfg.play.pup_session,+        "pup_profile": cfg.play.pup_profile,+        "path": config::config_path().map(|p| p.display().to_string()).unwrap_or_default(),+        "live_surface": live_drainer_surface().map(|s| s.as_str()),+    })+}++/// `adom-tts open [--print-url]`: the dock's serve step and a human's "show me+/// the player". Makes sure a drainer is up (spawning one for the WEBVIEW+/// surface if none is live), waits for its page to answer, then RETURNS,+/// printing the direct loopback URL on stdout. The drainer itself owns the+/// tab (open-or-refresh by the same name the dock pre-opens), so this never+/// opens a second one. Self-backgrounding is the dock contract: Hydrogen waits+/// for this process to EXIT before it reads the URL.+pub fn open(print_url: bool) -> Result<()> {+    persist_surface_env();+    if let Some(Surface::Pup) = live_drainer_surface() {+        eprintln!("Hint: the live player is on the pup surface; the dock tab will mirror it. Two open pages both play audio.");+        eprintln!("      To move it here: `adom-tts settings set surface webview` then `adom-tts restart` (when idle).");+    }+    let already = lock_owner().map(|(pid, _)| pid_alive(pid)).unwrap_or(false);+    ensure_drainer(Surface::Hydrogen)?;+    let client = reqwest::blocking::Client::builder().timeout(Duration::from_millis(800)).build()?;+    let deadline = Instant::now() + Duration::from_secs(15);+    let port = loop {+        if let Some(p) = read_player_port() {+            if client.get(format!("http://127.0.0.1:{p}/api/health")).send().map(|r| r.status().is_success()).unwrap_or(false) { break p; }+        }+        if Instant::now() > deadline { anyhow::bail!("the player did not come up within 15s. Hint: `adom-tts logs` then `adom-tts restart`"); }+        thread::sleep(Duration::from_millis(250));+    };+    if !already {+        log_event("dock_open", serde_json::json!({ "port": port, "dock_webview": std::env::var("ADOM_DOCK_WEBVIEW").ok() }));+    } else {+        // A drainer was already up: it opened its tab long ago and may have+        // idle-closed it. Re-offer the tab so a dock click always shows a page.+        let cfg = config::load();+        let surface = live_drainer_surface().unwrap_or(Surface::Hydrogen);+        if surface == Surface::Hydrogen && std::env::var("VSCODE_PROXY_URI").is_ok() {+            let _ = server::open_webview_tab_named(&server::webview_url(port), PLAYER_TAB_NAME);+            let _ = &cfg;+        }+    }+    if print_url {+        println!("http://127.0.0.1:{port}/");+    } else {+        println!("OK: player up on :{port}");+        println!("  player page: {}", server::webview_url(port));+    }+    Ok(())+}++/// `adom-tts ui toast "<text>" [--kind info|success|error]`: raise a toast on+/// the live player page (the AI narrating over the app it drives).+pub fn ui_toast(text: &str, kind: &str) -> Result<()> {+    let port = read_player_port().ok_or_else(|| anyhow!("no live player. Hint: `adom-tts open` starts one"))?;+    let r = reqwest::blocking::Client::builder().timeout(Duration::from_secs(3)).build()?+        .post(format!("http://127.0.0.1:{port}/ui/toast"))+        .json(&serde_json::json!({ "text": text, "kind": kind }))+        .send().map_err(|e| anyhow!("player on :{port} did not answer ({e}). Hint: `adom-tts restart`"))?;+    let v: serde_json::Value = r.json().unwrap_or(serde_json::Value::Null);+    if v.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) {+        println!("OK: toast raised on the player page (seq {})", v.get("seq").and_then(|s| s.as_u64()).unwrap_or(0));+        Ok(())+    } else {+        anyhow::bail!("player refused the toast: {}", v.get("error").and_then(|e| e.as_str()).unwrap_or("unknown"))+    }+}+ #[cfg(test)] mod tests {     use super::*;
src/player.html+519−390
@@ -3,27 +3,37 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1">-<title>adom-tts player</title>+<title>Adom TTS</title>+<!-- ONE icon file for the favicon, the header mark, the tab strip and the dock+     card (icons skill). Relative URL: this page is often served through the+     workspace proxy, where a leading slash would escape to the host origin. -->+<link rel="icon" type="image/svg+xml" href="favicon.svg"> <style>-  @import url('https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@500;600;700&display=swap');+  /* Brand fonts, self-declared from adom.inc (no stylesheet is published there). */+  @font-face { font-family: "Familjen Grotesk"; font-weight: 500; font-display: swap; src: url("https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-500-normal.woff2") format("woff2"); }+  @font-face { font-family: "Familjen Grotesk"; font-weight: 600; font-display: swap; src: url("https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-600-normal.woff2") format("woff2"); }+  @font-face { font-family: "Familjen Grotesk"; font-weight: 700; font-display: swap; src: url("https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-700-normal.woff2") format("woff2"); }+  @font-face { font-family: "Satoshi"; font-weight: 400; font-display: swap; src: url("https://adom.inc/fonts/satoshi/satoshi-400-normal.woff2") format("woff2"); }+  @font-face { font-family: "Satoshi"; font-weight: 500; font-display: swap; src: url("https://adom.inc/fonts/satoshi/satoshi-500-normal.woff2") format("woff2"); }+  @font-face { font-family: "Satoshi"; font-weight: 700; font-display: swap; src: url("https://adom.inc/fonts/satoshi/satoshi-700-normal.woff2") format("woff2"); }++  /* The dock-dashboard token stub. Every color below resolves to one of these. */   :root {-    --bg: #0d1117; --bg2: #00142f;-    --surface: #161b22; --elevated: #1c2128; --border: #2b3a52;-    --fg: #e6edf3; --muted: #8b98a5;-    --teal: #00B8B1; --teal-soft: rgba(0,184,177,0.16);-    --blue: #0061EF; --purple: #8C6BF7;-    --amber: #d4a017; --red: #e0524d;-    /* z-index ladder (adom-ui-design/tooltips) — never hardcode a literal */-    --z-toolbar: 80; --z-floating-menu: 200; --z-toast: 400; --z-tooltip: 99999;+    --bg: #0d1117; --surface: #161b22; --surface2: #1c232c; --line: #262d36;+    --text: #e6edf3; --muted: #8b949e; --teal: #00b8b1;+    --led-green: #3fb950; --led-amber: #d29922; --led-red: #f85149;+    --blue-text: #64abff; --purple: #8c6bf7;+    --teal-soft: rgba(0,184,177,.16);+    --sans: "Familjen Grotesk", system-ui, sans-serif;+    --body: "Satoshi", system-ui, -apple-system, "Segoe UI", sans-serif;+    --mono: "JetBrains Mono", ui-monospace, Consolas, Menlo, monospace;+    --gutter: 16px;+    /* z-index ladder (tooltips skill): never a literal in a rule */+    --z-toolbar: 80; --z-floating-menu: 200; --z-modal: 300; --z-toast: 400; --z-tooltip: 99999;   }   * { box-sizing: border-box; min-width: 0; }-  /* HARD RULE: the outermost html NEVER shows a horizontal scrollbar. A truly-     fluid layout never overflows sideways; `clip` guarantees a stray pixel-     (negative margin, long token) can't summon one either. */-  html, body { margin: 0; height: 100%; overflow-x: clip; }--  /* Pretty scrollbars everywhere (transcript, history, page) — slim, rounded,-     quiet until hovered. Never the chunky default. */+  /* body grows with content (a long transcript + history must push the footer down, never under it) */+  html { height: 100%; } html, body { margin: 0; overflow-x: clip; } body { min-height: 100%; }   * { scrollbar-width: thin; scrollbar-color: rgba(0,184,177,.35) transparent; }   *::-webkit-scrollbar { width: 7px; height: 7px; }   *::-webkit-scrollbar-track { background: transparent; }@@ -31,349 +41,367 @@   *::-webkit-scrollbar-thumb:hover { background: rgba(0,184,177,.55); }   *::-webkit-scrollbar-corner { background: transparent; }   body {-    background:-      radial-gradient(900px 420px at 85% -10%, rgba(140,107,247,0.12), transparent 60%),-      radial-gradient(900px 420px at 0% 0%, rgba(0,97,239,0.12), transparent 55%),-      linear-gradient(170deg, var(--bg2), var(--bg) 60%);-    color: var(--fg);-    font-family: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif;-    min-height: 100%;-    display: flex; flex-direction: column;+    background: var(--bg); color: var(--text);+    font-family: var(--body); font-size: 14px;+    min-height: 100%; display: flex; flex-direction: column;   }+  :focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }+  button { font-family: inherit; }+  svg.ic { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; } -  /* ---- canonical Adom app-header: single row, 44px, brand | subject | actions ---- */-  header {-    position: sticky; top: 0; z-index: 20; min-height: 44px;-    display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; padding: 6px 14px;-    background: rgba(13,17,23,0.82); backdrop-filter: blur(10px);-    border-bottom: 1px solid var(--border);-  }-  /* version badge — ALWAYS visible so you can tell which build is running and-     spot any thread still launching an old one. */-  .ver { font-family: ui-monospace,Menlo,monospace; font-size:11px; color: var(--teal);-    border:1px solid rgba(0,184,177,0.55); border-radius:6px; padding:1px 6px; flex:none; }-  header::after {-    content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px;-    background: linear-gradient(90deg, var(--blue), var(--teal) 50%, var(--purple)); opacity:.85;-  }-  /* Zone 1: brand */-  .brand { display: flex; align-items: center; gap: 9px; position: relative; cursor: default; outline: none; flex: none; }-  .eq { display: flex; align-items: flex-end; gap: 3px; height: 17px; }-  .eq i { width: 3px; border-radius: 2px; background: var(--teal); transform-origin: bottom; animation: bob 1s ease-in-out infinite; }-  .eq i:nth-child(1){height:7px;animation-delay:0s}-  .eq i:nth-child(2){height:15px;animation-delay:.15s}-  .eq i:nth-child(3){height:10px;animation-delay:.3s}-  .eq i:nth-child(4){height:17px;animation-delay:.45s}-  body:not(.is-playing) .eq i { animation-play-state: paused; transform: scaleY(.55); }-  @keyframes bob { 0%,100%{transform:scaleY(.45)} 50%{transform:scaleY(1)} }-  .app-name { font-family:'Familjen Grotesk',system-ui,sans-serif; font-weight:600; font-size:15px; color: var(--teal); letter-spacing:-.2px; }-  .app-subtitle { font-family:'Familjen Grotesk',system-ui,sans-serif; font-weight:500; font-size:13px; color: var(--muted); }-  .sep { color: var(--border); margin: 0 1px; }-  .vsep { width:1px; height:20px; background: var(--border); flex: none; }-  /* Zone 2: subject — truncates when tight; hover/tap reveals the full story */-  .subject { display:flex; align-items:baseline; gap:8px; min-width:0; flex:1; overflow:visible; position:relative; cursor:default; outline:none; }-  .subject-name { font-size:14px; font-weight:600; color: var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:0 1 auto; min-width:4.5em; }-  /* rich tooltip: everything the truncated header hides, in one card */-  .subject-pop {-    position:absolute; top: calc(100% + 10px); left: 0; width: min(340px, 86vw); z-index: 30;-    background: var(--elevated); border:1px solid var(--border); border-radius:12px;-    padding:13px 15px; box-shadow: 0 14px 40px rgba(0,0,0,.5);-    opacity:0; transform: translateY(-4px); pointer-events:none; transition: opacity .14s, transform .14s;-  }-  .subject:hover .subject-pop, .subject:focus-within .subject-pop, .subject.open .subject-pop { opacity:1; transform:none; pointer-events:auto; }-  .sp-desc { font-size:13px; color: var(--fg); line-height:1.5; margin-bottom:10px; }-  .sp-row { display:flex; gap:10px; font-size:12px; margin:4px 0; }-  .sp-k { color: var(--muted); width:52px; flex:none; text-transform:uppercase; letter-spacing:.5px; font-size:10px; padding-top:1px; }-  .sp-v { color: var(--fg); min-width:0; overflow-wrap:anywhere; }-  .sp-v.mono { font-family: ui-monospace,Menlo,monospace; font-size:11.5px; }-  /* the clip DESCRIPTION (human sentence) — always visible, ellipsized when tight */-  .subject-meta { font-size:12.5px; color: var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex: 1 1 auto; }-  .subject-meta .sep { color: var(--border); }-  /* CLIP FRESHNESS line — lives by the transcript (not the header). Shows when-     THIS clip's audio was made + how long ago. Teal fresh · amber hours · red-     days. Hover it (or the transcript) for the exact synthesized timestamp. */-  .clip-fresh { align-self:flex-start; margin: 2px 0 0; font-size:12px; font-family: ui-monospace,Menlo,monospace;-    padding:3px 10px; border-radius:999px; white-space:nowrap; cursor: default;-    color: var(--teal); border:1px solid rgba(0,184,177,.4); background: var(--teal-soft); }-  .clip-fresh.warn  { color: var(--amber); border-color: rgba(212,160,23,.5); background: rgba(212,160,23,.14); }-  .clip-fresh.stale { color: var(--red); border-color: rgba(224,82,77,.6); background: rgba(224,82,77,.16); font-weight:600; }-  /* Global tooltip layer (adom-ui-design/tooltips) — one fixed div, topmost. */-  #global-tooltip { position: fixed; z-index: var(--z-tooltip); top:0; left:0;-    transform: translate(var(--tip-x,0), var(--tip-y,0));-    max-width: min(320px, calc(100vw - 40px)); pointer-events:none; opacity:0; display:none;-    background: rgba(22,27,34,.96); color: var(--fg);-    border:1px solid rgba(0,184,177,.28); border-radius:10px; padding:9px 13px;-    font: 400 13px/1.4 system-ui, sans-serif; box-shadow: 0 12px 40px rgba(0,0,0,.55);-    backdrop-filter: blur(16px); text-transform:none; letter-spacing:0; }-  #global-tooltip.visible { display:block; }-  #global-tooltip.shown   { opacity:1; transition: opacity .18s ease .6s; }-  /* Zone 3: actions */-  .actions { display:flex; align-items:center; gap:8px; margin-left:auto; flex:none; }-  .queue-chip { font-size:11.5px; color: var(--blue); border:1px solid rgba(0,97,239,0.5); background: rgba(0,97,239,0.12); padding:3px 9px; border-radius:999px; white-space:nowrap; }-  .queue-chip.hidden { display:none; }-  .thread-chip { font-size:11.5px; color: var(--purple); border:1px solid rgba(140,107,247,0.5); background: rgba(140,107,247,0.12); padding:3px 10px; border-radius:999px; white-space:nowrap; }-  /* live-link LED — proves the front-end has a live pipe to the AI/back-end.-     green=connected, amber=degraded, red/gray=disconnected (dead/orphaned tab). */-  .led { width:11px; height:11px; border-radius:50%; flex:none; margin-left:3px; transition: background .4s, box-shadow .4s; }-  /* GENTLE liveness: a slow breathing glow, never a jumpy per-ping pulse. */-  .led.ok   { background:#3fb950; animation: ledbreathe 3.2s ease-in-out infinite; }-  .led.warn { background:var(--amber); box-shadow:0 0 7px rgba(212,160,23,.5); }-  .led.down { background:#e0524d; box-shadow:0 0 6px rgba(224,82,77,.55); }-  @keyframes ledbreathe {-    0%,100% { box-shadow: 0 0 4px rgba(63,185,80,.35); }-    50%     { box-shadow: 0 0 10px rgba(63,185,80,.75); }-  }-  .pill {-    display: inline-flex; align-items: center; gap: 7px;-    padding: 4px 11px; background: var(--elevated); border: 1px solid var(--border);-    border-radius: 999px; font-size: 12px; color: var(--muted);-  }-  .pill::before { content:""; width:7px; height:7px; border-radius:50%; background: currentColor; }-  .pill.playing { color: var(--teal); border-color: var(--teal); }-  .pill.buffering { color: var(--amber); border-color: var(--amber); }-  .pill.paused { color: var(--amber); }-  .pill.ended { color: var(--muted); }-  .pill.error { color: var(--red); border-color: var(--red); }-  /* app-info popover (hover/focus the brand) */-  .popover {-    position:absolute; top: calc(100% + 8px); left:0; width: 320px; z-index: 30;-    background: var(--elevated); border:1px solid var(--border); border-radius:12px;-    padding:14px 16px; box-shadow: 0 14px 40px rgba(0,0,0,.5);-    opacity:0; transform: translateY(-4px); pointer-events:none; transition: opacity .14s, transform .14s;+  /* ============ 1. The house header (dash-header numbers) ============ */+  header.dash {+    height: 44px; flex: none; display: flex; align-items: center; gap: 12px;+    padding: 0 var(--gutter); background: var(--surface); border-bottom: 1px solid var(--line);+    white-space: nowrap;   }-  .brand:hover .popover, .brand:focus-within .popover, .popover:hover { opacity:1; transform:none; pointer-events:auto; }-  .po-title { font-family:'Familjen Grotesk',sans-serif; font-weight:700; font-size:15px; color:var(--fg); }-  .po-title b { color: var(--teal); }-  .po-ver { font-family: ui-monospace,Menlo,monospace; font-size:11px; color: var(--muted); margin-left:4px; }-  .po-desc { font-size:12.5px; color: var(--muted); margin:8px 0 12px; line-height:1.5; }-  .po-row { display:flex; gap:10px; font-size:12px; margin:5px 0; }-  .po-k { color: var(--muted); width:54px; flex:none; text-transform:uppercase; letter-spacing:.5px; font-size:10.5px; padding-top:1px; }-  .po-v { color: var(--fg); }-  .po-link { display:inline-block; margin-top:11px; font-size:12.5px; color: var(--teal); text-decoration:none; }-  .po-link:hover { text-decoration:underline; }--  /* ---- player: centered card on a wide screen, edge-to-edge fill in the-     skinny 20% webview (see the max-width:620px block below) ---- */-  main { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 24px 20px; }-  .player {-    width: min(620px, 94vw); display: flex; flex-direction: column; min-height: 0;-    background: linear-gradient(180deg, var(--surface), #0e131a);-    border: 1px solid var(--border); border-radius: 18px;-    box-shadow: 0 18px 50px rgba(0,0,0,0.5);-    padding: 26px 28px 22px;+  .brand { display: flex; align-items: center; gap: 8px; position: relative; cursor: default; flex: none; height: 44px; }+  .brand .mark { width: 18px; height: 18px; color: var(--text); }+  .app-name { font-family: var(--sans); font-weight: 600; font-size: 15px; color: var(--text); letter-spacing: -.01em; }+  .app-subtitle { font-size: 13px; font-weight: 400; color: var(--muted); }+  .vsep { width: 1px; height: 20px; background: var(--line); flex: none; }+  .subject { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 1; position: relative; cursor: default; }+  .subject-name { font-size: 13.5px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; min-width: 3em; }+  .subject-meta { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; }+  .subject-pop, .popover {+    position: absolute; top: calc(100% - 2px); left: 0; z-index: var(--z-floating-menu);+    background: var(--surface2); border: 1px solid var(--line); border-radius: 12px;+    padding: 13px 15px; box-shadow: 0 14px 40px rgba(0,0,0,.5); white-space: normal;+    opacity: 0; transform: translateY(-4px); pointer-events: none; transition: opacity .14s .6s, transform .14s .6s;   }+  .subject-pop { width: min(360px, calc(100vw - 32px)); }+  .popover { width: min(320px, calc(100vw - 32px)); }+  .subject:hover .subject-pop, .subject:focus-within .subject-pop, .subject.open .subject-pop,+  .brand:hover .popover, .brand:focus-within .popover, .popover:hover { opacity: 1; transform: none; pointer-events: auto; }+  .subject.open .subject-pop { transition-delay: 0s; }+  .sp-desc { font-size: 13px; color: var(--text); line-height: 1.5; margin-bottom: 10px; }+  .sp-row, .po-row { display: flex; gap: 10px; font-size: 12px; margin: 4px 0; }+  .sp-k, .po-k { color: var(--muted); width: 56px; flex: none; font-family: var(--mono); font-size: 11px; padding-top: 1px; }+  .sp-v, .po-v { color: var(--text); min-width: 0; overflow-wrap: anywhere; }+  .sp-v.mono { font-family: var(--mono); font-size: 11.5px; }+  .po-title { font-family: var(--sans); font-weight: 600; font-size: 15px; color: var(--text); }+  .po-ver { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-left: 6px; }+  .po-desc { font-size: 12.5px; color: var(--muted); margin: 8px 0 12px; line-height: 1.5; }+  .po-link { display: inline-block; margin-top: 11px; font-size: 12.5px; color: var(--teal); text-decoration: none; }+  .po-link:hover { text-decoration: underline; }+  .actions { display: flex; align-items: center; gap: 6px; margin-left: auto; flex: none; }+  .queue-chip { font-size: 11.5px; font-family: var(--mono); color: var(--blue-text); border: 1px solid rgba(100,171,255,.4); background: rgba(100,171,255,.10); padding: 3px 9px; border-radius: 999px; margin-right: 4px; }+  .queue-chip[hidden] { display: none; }+  .target-chip { display: inline-flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 12px; color: var(--muted);+    border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; margin-right: 4px; cursor: default; }+  .target-chip b { color: var(--text); font-weight: 500; }+  .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; transition: background .4s; }+  .dot.ok { background: var(--led-green); }+  .dot.warn { background: var(--led-amber); }+  .dot.down { background: var(--led-red); }+  .ibtn { width: 28px; height: 28px; border-radius: 7px; border: 0; background: transparent; color: var(--muted);+    display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }+  .ibtn:hover, .ibtn[aria-pressed="true"] { color: var(--teal); background: var(--teal-soft); } +  /* ============ 2 + 3. Newbie panel mount and the status pill row ============ */+  .page { flex: 1 0 auto; display: flex; flex-direction: column; padding: 0 var(--gutter) var(--gutter); }+  #newbie { flex: none; }+  .leds { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 14px; flex: none; }+  .led-pill { display: inline-flex; align-items: center; gap: 7px; background: var(--surface); border: 1px solid var(--line);+    border-radius: 999px; padding: 4px 12px; font-size: 12.5px; cursor: pointer; color: var(--text); }+  .led-pill .lbl { color: var(--muted); }+  .led-pill[data-state=ok] .dot { background: var(--led-green); }+  .led-pill[data-state=busy] .dot { background: var(--led-amber); }+  .led-pill[data-state=bad] .dot { background: var(--led-red); }+  #led-pop { position: fixed; z-index: var(--z-floating-menu); display: none; width: min(380px, calc(100vw - 32px));+    background: var(--surface2); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;+    box-shadow: 0 14px 40px rgba(0,0,0,.5); font-size: 13px; line-height: 1.5; color: var(--text); }+  #led-pop.visible { display: block; }+  #led-pop code { font-family: var(--mono); font-size: 12px; color: var(--teal); }++  /* ============ 4. The player (the app's own content) ============ */+  .player { flex: none; min-height: 0; display: flex; flex-direction: column; background: var(--surface);+    border: 1px solid var(--line); border-radius: 12px; padding: 20px 22px 16px; }   .transport { display: flex; align-items: center; gap: 16px; }-  .play {-    width: 56px; height: 56px; flex: none; border-radius: 50%; border: 0;-    background: var(--teal); color: #04201f; cursor: pointer;-    display: flex; align-items: center; justify-content: center;-    box-shadow: 0 4px 16px rgba(0,184,177,0.4); transition: transform .08s, filter .15s, box-shadow .3s;-  }+  .play { width: 56px; height: 56px; flex: none; border-radius: 50%; border: 0; background: var(--teal); color: #04201f; cursor: pointer;+    display: flex; align-items: center; justify-content: center; transition: transform .08s, filter .15s, box-shadow .3s; }   .play:hover { filter: brightness(1.08); }-  .play:active { transform: scale(0.93); }-  .play svg { width: 24px; height: 24px; fill: #04201f; }+  .play:active { transform: scale(.93); }+  .play svg.ic { width: 26px; height: 26px; stroke-width: 2; }+  .play svg.ic[hidden] { display: none; }   .play.attention { animation: pulse 1.3s ease-in-out infinite; }-  @keyframes pulse { 0%,100%{box-shadow:0 4px 16px rgba(0,184,177,0.4)} 50%{box-shadow:0 4px 30px rgba(0,184,177,0.95)} }-+  @keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(0,184,177,.4); } 50% { box-shadow: 0 0 0 10px rgba(0,184,177,0); } }   .seekwrap { flex: 1; min-width: 0; }-  .seek {-    -webkit-appearance: none; appearance: none; width: 100%; height: 6px;-    border-radius: 999px; background: var(--elevated); cursor: pointer; outline: none;-  }-  .seek::-webkit-slider-thumb {-    -webkit-appearance: none; width: 15px; height: 15px; border-radius: 50%;-    background: var(--teal); box-shadow: 0 0 0 4px var(--teal-soft); cursor: pointer;-  }+  .seek { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; border-radius: 999px; background: var(--surface2); cursor: pointer; }+  .seek::-webkit-slider-thumb { -webkit-appearance: none; width: 15px; height: 15px; border-radius: 50%; background: var(--teal); box-shadow: 0 0 0 4px var(--teal-soft); cursor: pointer; }   .seek::-moz-range-thumb { width: 15px; height: 15px; border: 0; border-radius: 50%; background: var(--teal); cursor: pointer; }-  .times { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--muted); margin-top: 7px; font-variant-numeric: tabular-nums; }--  /* ---- transcript / captions ---- */-  .transcript {-    margin-top: 22px; padding: 16px 18px; border-radius: 12px;-    background: rgba(0,0,0,0.22); border: 1px solid var(--border);-    font-size: 16px; line-height: 1.75; max-height: 230px; overflow-y: auto;-    color: var(--muted);-  }-  .transcript .label { display:block; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--teal); margin-bottom: 8px; opacity:.9; }-  .transcript w {-    transition: color .12s, background .12s; border-radius: 5px; padding: 0 1px;-  }-  .transcript w.spoken { color: var(--fg); }-  .transcript w.active {-    color: #04201f; background: var(--teal); padding: 1px 4px; margin: 0 -3px;-    font-weight: 600;-  }-  .transcript:empty, .transcript.hidden { display: none; }+  .times { display: flex; justify-content: space-between; align-items: center; font-size: 11.5px; color: var(--muted); margin-top: 7px; font-variant-numeric: tabular-nums; font-family: var(--mono); }+  .pill { display: inline-flex; align-items: center; gap: 7px; padding: 2px 10px; border: 1px solid var(--line); border-radius: 999px; font-size: 11.5px; color: var(--muted); font-family: var(--body); }+  .pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }+  .pill.playing { color: var(--teal); border-color: var(--teal); }+  .pill.playing::before { background: var(--led-green); }+  .pill.buffering::before, .pill.paused::before { background: var(--led-amber); }+  .pill.error { color: var(--led-red); border-color: var(--led-red); }+  .pill.error::before { background: var(--led-red); } -  /* ---- replayable history ---- */-  .history {-    margin-top: 16px; padding: 12px 14px; border-radius: 12px;-    background: rgba(0,0,0,0.22); border: 1px solid var(--border);-    max-height: 190px; overflow-y: auto;-  }-  .history.hidden { display: none; }-  .hist-label { font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--teal); margin-bottom: 8px; opacity:.9; }-  .hist-hint { text-transform: none; letter-spacing: 0; color: var(--muted); font-size: 10.5px; margin-left: 8px; }-  .hist-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-top: 1px solid rgba(43,58,82,.5); }+  .clip-fresh { align-self: flex-start; margin: 14px 0 0; font-size: 12px; font-family: var(--mono); padding: 3px 10px; border-radius: 999px;+    white-space: nowrap; cursor: default; display: inline-flex; align-items: center; gap: 6px;+    color: var(--teal); border: 1px solid rgba(0,184,177,.4); background: var(--teal-soft); }+  .clip-fresh svg.ic { width: 14px; height: 14px; }+  .clip-fresh.warn { color: var(--led-amber); border-color: rgba(210,153,34,.5); background: rgba(210,153,34,.14); }+  .clip-fresh.stale { color: var(--led-red); border-color: rgba(248,81,73,.6); background: rgba(248,81,73,.16); font-weight: 600; }+  .clip-fresh[hidden] { display: none; }++  .transcript { margin-top: 12px; padding: 16px 18px; border-radius: 10px; background: var(--bg); border: 1px solid var(--line);+    font-size: 16px; line-height: 1.75; max-height: 260px; overflow-y: auto; color: var(--muted); max-width: 90ch; }+  .transcript .label, .hist-label { display: block; font-size: 12px; color: var(--teal); margin-bottom: 8px; font-family: var(--sans); font-weight: 600; }+  .transcript w { transition: color .12s, background .12s; border-radius: 5px; padding: 0 1px; }+  .transcript w.spoken { color: var(--text); }+  .transcript w.active { color: #04201f; background: var(--teal); padding: 1px 4px; margin: 0 -3px; font-weight: 600; }+  .transcript:empty, .transcript[hidden] { display: none; }++  .history { margin-top: 14px; padding: 12px 14px; border-radius: 10px; background: var(--bg); border: 1px solid var(--line); max-height: 220px; overflow-y: auto; }+  .history[hidden] { display: none; }+  .hist-hint { color: var(--muted); font-size: 11px; margin-left: 8px; font-family: var(--body); font-weight: 400; }+  .hist-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-top: 1px solid var(--line); }   .hist-row:first-of-type { border-top: 0; }-  .hist-thread { flex: none; font-size: 10.5px; color: var(--purple); border: 1px solid rgba(140,107,247,0.45); background: rgba(140,107,247,0.10); padding: 1px 7px; border-radius: 999px; white-space: nowrap; max-width: 34%; overflow: hidden; text-overflow: ellipsis; }-  .hist-desc { flex: 1; min-width: 0; font-size: 12.5px; color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }-  .hist-desc.missed { color: var(--amber); }-  .hist-when { flex: none; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }-  .hist-replay { flex: none; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: var(--elevated); color: var(--teal); cursor: pointer; font-size: 13px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: background .12s, transform .08s; }+  .hist-thread { flex: none; font-size: 10.5px; color: var(--purple); border: 1px solid rgba(140,107,247,.45); background: rgba(140,107,247,.10); padding: 1px 7px; border-radius: 999px; white-space: nowrap; max-width: 34%; overflow: hidden; text-overflow: ellipsis; }+  .hist-desc { flex: 1; min-width: 0; font-size: 12.5px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }+  .hist-desc.missed { color: var(--led-amber); }+  .hist-when { flex: none; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; font-family: var(--mono); }+  .hist-replay { flex: none; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--line); background: var(--surface2); color: var(--teal); cursor: pointer;+    display: flex; align-items: center; justify-content: center; padding: 0; transition: background .12s, transform .08s; }+  .hist-replay svg.ic { width: 15px; height: 15px; }   .hist-replay:hover { background: var(--teal-soft); }   .hist-replay:active { transform: scale(.9); }+  .diag { margin-top: 12px; font-size: 10.5px; color: var(--muted); font-family: var(--mono); opacity: .6; word-break: break-all; }+  footer { padding: 10px var(--gutter); text-align: center; font-size: 11px; color: var(--muted); opacity: .7; flex: none; } -  .diag { margin-top: 14px; font-size: 10.5px; color: var(--muted); font-family: ui-monospace, Menlo, monospace; opacity: .6; word-break: break-all; }-  footer { padding: 12px; text-align: center; font-size: 11px; color: var(--muted); opacity: .6; }--  /* ============================================================-     RESPONSIVE — kept LAST so these win over the base rules above.-     The webview is often a skinny 20% column, so below 620px we drop-     the tagline, let the filename take its own row, and — most importantly-     — stretch the player so the transcript FILLS the tall empty space-     instead of floating in a small centered card with wasted margin.-     ============================================================ */-  @media (max-width: 620px){-    .app-subtitle, .brand .sep { display: none; }        /* row 1: brand + version + status + LED */-    .subject { order: 3; flex-basis: 100%; }             /* filename drops to its own row if tight */-    .actions { margin-left: auto; }+  /* ============ Toasts (toasts skill: bottom center, newest on top) ============ */+  .toasts { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: var(--z-toast); display: flex; flex-direction: column-reverse; gap: 8px; width: min(440px, calc(100vw - 32px)); pointer-events: none; }+  .toast { pointer-events: auto; display: flex; align-items: center; gap: 10px; background: var(--surface2); border: 1px solid var(--line); border-left: 3px solid var(--teal);+    border-radius: 10px; padding: 10px 12px; font-size: 13px; color: var(--text); box-shadow: 0 12px 36px rgba(0,0,0,.5); animation: toastin .18s ease; }+  .toast.success { border-left-color: var(--led-green); }+  .toast.error { border-left-color: var(--led-red); }+  .toast svg.ic { width: 16px; height: 16px; color: var(--teal); }+  .toast.success svg.ic { color: var(--led-green); }+  .toast.error svg.ic { color: var(--led-red); }+  .toast .tx { flex: 1; min-width: 0; }+  .toast .x { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: flex; }+  @keyframes toastin { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }++  /* ============ Settings modal (settings-layout skill) ============ */+  .modal { position: fixed; inset: 0; z-index: var(--z-modal); display: flex; align-items: center; justify-content: center; }+  .modal[hidden] { display: none; }+  .modal .backdrop { position: absolute; inset: 0; background: rgba(0,0,0,.45); }+  .dialog { position: relative; width: min(520px, calc(100vw - 32px)); max-height: calc(100vh - 32px); overflow-y: auto;+    background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 18px 20px; box-shadow: 0 24px 60px rgba(0,0,0,.6); }+  .dialog h2 { margin: 0 0 4px; font-family: var(--sans); font-weight: 600; font-size: 17px; }+  .dialog .sub { margin: 0 0 14px; color: var(--muted); font-size: 12.5px; }+  .dialog .close { position: absolute; top: 12px; right: 12px; }+  .card { background: var(--surface2); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin-bottom: 10px; }+  .card h3 { margin: 0 0 4px; font-size: 13.5px; font-weight: 600; font-family: var(--sans); }+  .card p { margin: 0 0 10px; font-size: 12.5px; color: var(--muted); line-height: 1.45; }+  .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }+  .seg button { border: 0; background: transparent; color: var(--muted); padding: 7px 14px; font-size: 13px; cursor: pointer; min-height: 36px; }+  .seg button + button { border-left: 1px solid var(--line); }+  .seg button.is-on, .seg button[aria-pressed="true"] { background: var(--teal-soft); color: var(--teal); box-shadow: inset 0 0 0 1px var(--teal); }+  .kv { display: flex; gap: 10px; font-size: 12px; margin: 3px 0; }+  .kv span:first-child { color: var(--muted); width: 90px; flex: none; font-family: var(--mono); font-size: 11px; }+  .kv span:last-child { font-family: var(--mono); font-size: 11.5px; overflow-wrap: anywhere; }+  .btn { border: 1px solid var(--line); background: var(--surface2); color: var(--text); border-radius: 8px; padding: 7px 12px; font-size: 13px; cursor: pointer; min-height: 36px; }+  .btn:hover { border-color: var(--teal); color: var(--teal); }+  .dialog .row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }++  /* ============ Tooltip layer (tooltips skill) ============ */+  #global-tooltip { position: fixed; z-index: var(--z-tooltip); top: 0; left: 0; transform: translate(var(--tip-x,0), var(--tip-y,0));+    max-width: min(320px, calc(100vw - 40px)); pointer-events: none; opacity: 0; display: none;+    background: var(--surface2); color: var(--text); border: 1px solid var(--line); border-radius: 10px; padding: 9px 13px;+    font: 400 13px/1.4 var(--body); box-shadow: 0 12px 40px rgba(0,0,0,.55); text-transform: none; letter-spacing: 0; }+  #global-tooltip.visible { display: block; }+  #global-tooltip.shown { opacity: 1; transition: opacity .18s ease .6s; }++  /* ============ Responsive (responsive-ui): reshape, never squeeze ============ */+  @media (max-width: 720px) {+    .app-subtitle, .subject-meta { display: none; }+  }+  @media (max-width: 620px) {     .vsep { display: none; }-    main { padding: 8px; align-items: stretch; }         /* fill width, don't center a tiny card */-    .player { width: 100%; border-radius: 12px; padding: 14px 15px 12px; box-shadow: none; }-    .transcript { flex: 1 1 auto; max-height: none; margin-top: 12px; padding: 14px 16px; font-size: 15.5px; line-height: 1.7; }-    footer { display: none; }                            /* reclaim the footer line for content */+    /* the chip collapses to its LED so the clip name keeps the row */+    .target-chip .lbl, .target-chip b { display: none; }+    .target-chip { padding: 3px 6px; margin-right: 2px; }+    .player { flex: 1; padding: 14px 14px 12px; }+    .transcript { flex: 1 1 auto; max-height: none; font-size: 15.5px; line-height: 1.7; }+    footer { display: none; }   }-  @media (max-width: 400px){-    .actions { flex-basis: 100%; justify-content: flex-end; margin-left: 0; }+  @media (max-width: 400px) {+    .queue-chip { display: none; }     .transport { gap: 12px; }     .play { width: 48px; height: 48px; }-    .transcript { font-size: 15px; }+  }+  @media (hover: none), (pointer: coarse) {+    .ibtn, .hist-replay { min-width: 44px; min-height: 44px; }   } </style> </head> <body>-  <header>-    <!-- Zone 1: BRAND (hover → app-info popover) -->+  <!-- Inline icon sprite: line drawings, currentColor, stroke 1.8 (icons skill). -->+  <svg width="0" height="0" style="position:absolute" aria-hidden="true">+    <symbol id="i-mark" viewBox="0 0 24 24"><path d="M4 9.5h3.2L12 5.5v13l-4.8-4H4z"/><path d="M15.5 9.2a4 4 0 0 1 0 5.6"/><path d="M18.4 6.6a8 8 0 0 1 0 10.8"/></symbol>+    <symbol id="i-play" viewBox="0 0 24 24"><path d="M7 4.8v14.4a.6.6 0 0 0 .92.5l11.3-7.2a.6.6 0 0 0 0-1l-11.3-7.2a.6.6 0 0 0-.92.5z"/></symbol>+    <symbol id="i-pause" viewBox="0 0 24 24"><path d="M8 5v14"/><path d="M16 5v14"/></symbol>+    <symbol id="i-replay" viewBox="0 0 24 24"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4.5v5h5"/></symbol>+    <symbol id="i-warn" viewBox="0 0 24 24"><path d="M12 3.5 2.5 20h19z"/><path d="M12 9.5v4.5"/><path d="M12 17.2h.01"/></symbol>+    <symbol id="i-check" viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7"/></symbol>+    <symbol id="i-help" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9.6 9.6a2.4 2.4 0 1 1 3.4 2.2c-.7.4-1 .9-1 1.7"/><path d="M12 16.8h.01"/></symbol>+    <symbol id="i-cog" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>+    <symbol id="i-x" viewBox="0 0 24 24"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></symbol>+    <symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.8h.01"/></symbol>+  </svg>++  <header class="dash">     <div class="brand" id="brand" tabindex="0">-      <span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>-      <span class="app-name">adom-tts</span>-      <span class="ver" title="running build">v{{VERSION}}</span>-      <span class="sep">·</span>+      <svg class="ic mark" aria-hidden="true"><use href="#i-mark"/></svg>+      <span class="app-name">Adom TTS</span>       <span class="app-subtitle">your AI's voice</span>-      <div class="popover" id="appinfo" role="dialog" aria-label="About adom-tts">-        <div class="po-title">adom<b>-tts</b> <span class="po-ver">v{{VERSION}}</span></div>-        <div class="po-desc">Adom's shared voice service — one player, one queue, shared by every AI thread. Reads your AI's answers aloud and renders demo voiceovers.</div>-        <div class="po-row"><span class="po-k">engine</span><span class="po-v">service-tts · edge-tts · en-US-AndrewNeural</span></div>-        <div class="po-row"><span class="po-k">license</span><span class="po-v">MIT · open source</span></div>-        <div class="po-row"><span class="po-k">author</span><span class="po-v">John Lauer · Adom Industries</span></div>-        <a class="po-link" href="https://wiki.adom.inc/adom/adom-tts" target="_blank" rel="noopener">View on the Adom Wiki ↗</a>+      <div class="popover" id="appinfo" role="dialog" aria-label="About Adom TTS">+        <div class="po-title">Adom TTS <span class="po-ver" id="poVer">v{{VERSION}}</span></div>+        <div class="po-desc">Adom's shared voice service: one player, one queue, shared by every AI thread. Reads your AI's answers aloud and renders demo voiceovers.</div>+        <div class="po-row"><span class="po-k">player</span><span class="po-v" id="poPlayer">v{{VERSION}}</span></div>+        <div class="po-row"><span class="po-k">engine</span><span class="po-v">service-tts, edge-tts, en-US-AndrewNeural</span></div>+        <div class="po-row"><span class="po-k">license</span><span class="po-v">MIT, open source</span></div>+        <div class="po-row"><span class="po-k">author</span><span class="po-v">John Lauer, Adom Industries</span></div>+        <a class="po-link" href="https://wiki.adom.inc/adom/adom-tts" target="_blank" rel="noopener">View on the Adom Wiki</a>       </div>     </div>-     <div class="vsep" aria-hidden="true"></div>--    <!-- Zone 2: SUBJECT (what's playing right now); hover/tap for the full card -->     <div class="subject" id="subject" tabindex="0">-      <span class="subject-name" id="subjectName">waiting for a clip…</span>+      <span class="subject-name" id="subjectName">waiting for a clip</span>       <span class="subject-meta" id="subjectMeta"></span>       <div class="subject-pop" id="subjectPop" role="tooltip">-        <div class="sp-desc" id="spDesc">No clip yet — the next `say --play` from any thread lands here.</div>-        <div class="sp-row"><span class="sp-k">thread</span><span class="sp-v" id="spThread">—</span></div>-        <div class="sp-row"><span class="sp-k">created</span><span class="sp-v" id="spCreated">—</span></div>-        <div class="sp-row"><span class="sp-k">file</span><span class="sp-v mono" id="spFile">—</span></div>-        <div class="sp-row"><span class="sp-k">voice</span><span class="sp-v mono" id="spVoice">—</span></div>+        <div class="sp-desc" id="spDesc">No clip yet. The next `say --play` from any thread lands here.</div>+        <div class="sp-row"><span class="sp-k">thread</span><span class="sp-v" id="spThread">none</span></div>+        <div class="sp-row"><span class="sp-k">created</span><span class="sp-v" id="spCreated">none</span></div>+        <div class="sp-row"><span class="sp-k">file</span><span class="sp-v mono" id="spFile">none</span></div>+        <div class="sp-row"><span class="sp-k">voice</span><span class="sp-v mono" id="spVoice">none</span></div>       </div>     </div>--    <!-- Zone 3: ACTIONS / status + live-link LED (always visible, even skinny) -->     <div class="actions">-      <span id="queueChip" class="queue-chip hidden"></span>-      <span id="threadChip" class="thread-chip" style="display:none"></span>-      <span id="status" class="pill">idle</span>-      <span id="live" class="led down" title="AI ↔ player link — green = live, red = disconnected"></span>+      <span id="queueChip" class="queue-chip" hidden></span>+      <span class="target-chip" id="targetChip" data-tooltip="Where clips play. The dot is the page's live link to the player: green means polling, red means this tab lost it.">+        <i class="dot" id="live"></i><span class="lbl">target:</span> <b>this machine</b>+      </span>+      <button class="ibtn" id="help-btn" type="button" aria-label="Show the starter phrases" aria-pressed="false" data-tooltip="Show the starter phrases and the demo">+        <svg class="ic" aria-hidden="true"><use href="#i-help"/></svg>+      </button>+      <button class="ibtn" id="settings-btn" type="button" aria-label="Settings" data-tooltip="Settings: choose where clips play by default">+        <svg class="ic" aria-hidden="true"><use href="#i-cog"/></svg>+      </button>     </div>   </header> -  <main>-    <div class="player">+  <div class="page">+    <div id="newbie"></div>++    <div class="leds" id="leds" aria-label="Status">+      <button class="led-pill" type="button" data-key="player" data-state="" data-tooltip="Tap for detail"><i class="dot"></i><span class="lbl">Player</span><span class="st">checking</span></button>+      <button class="led-pill" type="button" data-key="surface" data-state="" data-tooltip="Tap for detail"><i class="dot"></i><span class="lbl">Surface</span><span class="st">checking</span></button>+      <button class="led-pill" type="button" data-key="service" data-state="" data-tooltip="Tap for detail"><i class="dot"></i><span class="lbl">Voice service</span><span class="st">checking</span></button>+      <button class="led-pill" type="button" data-key="queue" data-state="" data-tooltip="Tap for detail"><i class="dot"></i><span class="lbl">Queue</span><span class="st">checking</span></button>+    </div>+    <div id="led-pop" role="dialog"></div>++    <section class="player" aria-label="Player">       <div class="transport">-        <button id="play" class="play" aria-label="Play">-          <svg id="playicon" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>+        <button id="play" class="play" type="button" aria-label="Play" data-tooltip="Play or pause. Pausing holds the whole queue until you press play again.">+          <svg class="ic" id="ic-play" aria-hidden="true"><use href="#i-play"/></svg>+          <svg class="ic" id="ic-pause" aria-hidden="true" hidden><use href="#i-pause"/></svg>+          <svg class="ic" id="ic-replay" aria-hidden="true" hidden><use href="#i-replay"/></svg>         </button>         <div class="seekwrap">-          <input id="seek" class="seek" type="range" min="0" max="1000" value="0" step="1" aria-label="Seek">-          <div class="times"><span id="cur">0:00</span><span id="dur">0:00</span></div>+          <input id="seek" class="seek" type="range" min="0" max="1000" value="0" step="1" aria-label="Seek" data-tooltip="Drag to seek inside this clip">+          <div class="times"><span id="cur">0:00</span><span id="status" class="pill">idle</span><span id="dur">0:00</span></div>         </div>       </div> -      <!-- CLIP FRESHNESS: when THIS clip's audio was synthesized + how long ago,-           right by the transcript so you always know if you're hearing old news.-           Teal fresh · amber hours · red days. Hover for the exact timestamp. -->-      <div id="clipFreshness" class="clip-fresh" data-tooltip="" style="display:none"></div>--      <div id="transcript" class="transcript hidden"></div>--      <!-- Replayable history: the human misses clips (dead zones, back-to-back-           updates) — every played clip is listed with WHO sent it and WHAT it-           says, with one-tap replay. -->-      <div id="histwrap" class="history hidden">-        <div class="hist-label">History <span class="hist-hint">tap ↻ to replay</span></div>+      <div id="clipFreshness" class="clip-fresh" hidden><svg class="ic" id="freshIcon" aria-hidden="true" hidden><use href="#i-warn"/></svg><span id="freshText"></span></div>+      <div id="transcript" class="transcript" hidden></div>+      <div id="histwrap" class="history" hidden>+        <span class="hist-label">History <span class="hist-hint">tap the arrow to hear a clip again</span></span>         <div id="histlist"></div>       </div>-       <div class="diag" id="diag"></div>+    </section>+  </div>+  <footer>Adom TTS shared player. One queue across all AI threads. Closes itself after a couple of minutes idle.</footer>++  <div class="toasts" id="toasts" aria-live="polite"></div>++  <div class="modal" id="settings" hidden>+    <div class="backdrop" id="settingsBackdrop"></div>+    <div class="dialog" role="dialog" aria-modal="true" aria-labelledby="settingsTitle">+      <button class="ibtn close" id="settingsClose" type="button" aria-label="Close settings"><svg class="ic" aria-hidden="true"><use href="#i-x"/></svg></button>+      <h2 id="settingsTitle">Settings</h2>+      <p class="sub">Saved to <span id="setPath" class="mono"></span>. The same values as <code>adom-tts settings</code>.</p>+      <div class="card">+        <h3>Playback surface</h3>+        <p>Where new clips open. Webview is a tab inside Hydrogen. Pup is a separate desktop browser window on your box. A change takes effect on the next player spawn (run <code>adom-tts restart</code> when nothing is playing).</p>+        <div class="seg" role="group" aria-label="Playback surface">+          <button type="button" data-surface="hydrogen" aria-pressed="false">Webview</button>+          <button type="button" data-surface="pup" aria-pressed="false">Pup</button>+        </div>+        <div id="setLive" class="kv" style="margin-top:10px"><span>live now</span><span id="setLiveVal">unknown</span></div>+      </div>+      <div class="card">+        <h3>Pup window</h3>+        <p>Used only when the surface is Pup. Change them with <code>adom-tts settings set pup_session &lt;name&gt;</code>.</p>+        <div class="kv"><span>session</span><span id="setSession"></span></div>+        <div class="kv"><span>profile</span><span id="setProfile"></span></div>+      </div>+      <div class="row">+        <button class="btn" id="settingsDefaults" type="button" data-tooltip="Back to Webview, the reliable default">Restore defaults</button>+        <button class="btn" id="settingsDone" type="button">Done</button>+      </div>     </div>-  </main>-  <footer>adom-tts shared player · one queue across all AI threads · auto-closes after a couple minutes idle</footer>+  </div>    <audio id="a" preload="auto"></audio>-  <!-- ONE body-appended tooltip layer (adom-ui-design/tooltips): position:fixed,-       z-index topmost, cursor-anchored, edge-clamped. Never a ::after pseudo. -->   <div id="global-tooltip"></div> +<script src="newbie-panel.js"></script> <script>   // PERSISTENT PLAYER. The page loads ONCE and stays put. It polls /poll for-  // the current clip id; when a NEW clip appears it swaps <audio>.src to-  // /audio?id=<id> and plays — so clips NEVER trigger a tab re-navigation-  // (the old new-port-per-clip race that caused "opened but silent"). One-  // server owns this page + the queue and knows the true playback state.-  const APP_NAME = "adom-tts";+  // the current clip id; when a NEW clip appears it swaps <audio>.src and+  // plays, so clips never trigger a tab re-navigation. One server owns this+  // page, the queue, and the true playback state.+  window.__ttsBoot = { complete: false, failed: [], version: "{{VERSION}}", panelMounted: false };+  function region(name, fn){ try { fn(); } catch (e) { window.__ttsBoot.failed.push(name + ': ' + (e && e.message || e)); try { showToast('Page region failed: ' + name, 'error', true); } catch (_) {} } }+   const a = document.getElementById('a');-  a.loop = false;  // never auto-repeat a clip — one play, then stop+  a.loop = false;   const statusEl = document.getElementById('status');   const subjectName = document.getElementById('subjectName');   const meta = document.getElementById('subjectMeta');-  const threadChip = document.getElementById('threadChip');   const queueChip = document.getElementById('queueChip');   const diag = document.getElementById('diag');   const playBtn = document.getElementById('play');-  // NOTE: the <path> inside the svg — setting `d` on the <svg> itself is a-  // silent no-op, which kept the button stuck on ▶ forever (real bug).-  const playIcon = document.querySelector('#playicon path');   const seek = document.getElementById('seek');   const curEl = document.getElementById('cur');   const durEl = document.getElementById('dur');   const transcript = document.getElementById('transcript');-  const ICON_PLAY = "M8 5v14l11-7z", ICON_PAUSE = "M6 5h4v14H6zM14 5h4v14h-4z", ICON_REPLAY = "M12 5V1L7 6l5 5V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z";   const MEDIA_ERR = {1:"ABORTED",2:"NETWORK",3:"DECODE",4:"SRC_NOT_SUPPORTED"};   let lastErr = "", seeking = false;--  // ---- current clip state -----  let curClipId = 0;       // id of the clip the page INTENDS to play (set on arrival)-  // SCRIPT MARKERS (issue #4): segments prefetched at load time, keyed-  // "<script>#<n>" -> blob URL, so a cue is "seek and play", never a download.+  let curClipId = 0;   const prefetched = {}; const prefetching = {};-  let loadedClipId = 0;    // id whose audio the <audio> element ACTUALLY holds.-                           // Telemetry is only valid when these agree — see applyClip.-  let wantPlay = false;    // we just swapped src and intend to auto-play it-  let metaBase = "";       // clip meta line before we append duration+  let loadedClipId = 0;+  let wantPlay = false;+  let metaBase = "";    function fmt(t){ if(!isFinite(t))return "0:00"; const m=Math.floor(t/60),s=String(Math.floor(t%60)).padStart(2,'0'); return m+':'+s; }   function setStatus(label, cls){ statusEl.textContent = label; statusEl.className = 'pill ' + (cls||''); }-  function setIcon(kind){ playIcon.setAttribute('d', kind==='pause'?ICON_PAUSE:kind==='replay'?ICON_REPLAY:ICON_PLAY); playBtn.setAttribute('aria-label', kind==='pause'?'Pause':kind==='replay'?'Replay':'Play'); document.body.classList.toggle('is-playing', kind==='pause'); }-  function updateDiag(){ const e=a.error?`err=${MEDIA_ERR[a.error.code]||a.error.code}`:'err=none'; diag.textContent=`clip#${curClipId} ready=${a.readyState} net=${a.networkState} ${e}${lastErr?' · '+lastErr:''}`; }+  // SVG elements have no `.hidden` property: toggle the ATTRIBUTE (the CSS keys on it).+  function hideEl(el, yes){ if (yes) el.setAttribute('hidden', ''); else el.removeAttribute('hidden'); }+  function setIcon(kind){+    ['play','pause','replay'].forEach(k => hideEl(document.getElementById('ic-' + k), k !== kind));+    playBtn.setAttribute('aria-label', kind==='pause'?'Pause':kind==='replay'?'Replay':'Play');+    document.body.classList.toggle('is-playing', kind==='pause');+  }+  function updateDiag(){ const e=a.error?`err=${MEDIA_ERR[a.error.code]||a.error.code}`:'err=none'; diag.textContent=`clip#${curClipId} ready=${a.readyState} net=${a.networkState} ${e}${lastErr?' '+lastErr:''}`; }   function beacon(p){ fetch(p,{cache:'no-store'}).catch(()=>{}); }   function ev(name, extra){     const buf = a.buffered.length ? a.buffered.end(a.buffered.length-1).toFixed(1) : '0';@@ -382,13 +410,34 @@   ['loadstart','durationchange','loadeddata','canplay','canplaythrough','play',    'waiting','stalled','suspend','abort','emptied'].forEach(n => a.addEventListener(n, () => ev(n))); -  // ---- karaoke transcript (rebuilt per clip) ----+  // ---- toasts (toasts skill): bottom center, about 4s, errors stick ----+  function showToast(text, kind, sticky){+    const wrap = document.getElementById('toasts');+    const t = document.createElement('div'); t.className = 'toast ' + (kind || 'info');+    const icon = kind === 'success' ? '#i-check' : kind === 'error' ? '#i-warn' : '#i-info';+    t.innerHTML = '<svg class="ic" aria-hidden="true"><use href="' + icon + '"/></svg><span class="tx"></span><button class="x" type="button" aria-label="Dismiss"><svg class="ic" aria-hidden="true"><use href="#i-x"/></svg></button>';+    t.querySelector('.tx').textContent = text;+    t.querySelector('.x').addEventListener('click', () => t.remove());+    wrap.appendChild(t);+    while (wrap.children.length > 4) wrap.firstChild.remove();+    if (!sticky && kind !== 'error') {+      let left = 4000, timer = null, last = Date.now();+      const arm = () => { last = Date.now(); timer = setTimeout(() => t.remove(), left); };+      t.addEventListener('mouseenter', () => { clearTimeout(timer); left -= (Date.now() - last); });+      t.addEventListener('mouseleave', arm);+      arm();+    }+    return t;+  }+  window.showToast = showToast;++  // ---- karaoke transcript ----   let spans = [], bounds = [], activeIdx = -1;   function buildTranscript(words){     spans = []; bounds = []; activeIdx = -1;     transcript.innerHTML = '';     if (Array.isArray(words) && words.length) {-      transcript.classList.remove('hidden');+      transcript.hidden = false;       const lbl = document.createElement('span'); lbl.className='label'; lbl.textContent='Transcript'; transcript.appendChild(lbl);       let weights = words.map(w => w.length + 1), total = weights.reduce((x,y)=>x+y,0) || 1, acc = 0;       words.forEach((w, i) => {@@ -398,7 +447,7 @@         transcript.appendChild(el); transcript.appendChild(document.createTextNode(' '));       });     } else {-      transcript.classList.add('hidden');+      transcript.hidden = true;     }   }   function highlight(frac){@@ -411,17 +460,10 @@     if (spans[idx]) spans[idx].scrollIntoView({ block:'nearest', behavior:'smooth' });   } -  // ---- apply a new clip (swap source, no page reload) -----  // HOTSPOT-PROOF: download the ENTIRE mp3 first, then play from a local blob.-  // The listener is often on a cellular hotspot that drops for seconds at a-  // time (driving) — streaming used to stall clips mid-sentence; a local blob-  // keeps playing through any drop once the download lands.+  // ---- freshness: when THIS clip's audio was made, so old news is obvious ----   let blobUrl = null;-  // ---- freshness display (John's ask: show date/time so an ancient clip is-  // obvious). created_ms = when the AUDIO was synthesized (mp3 mtime), which-  // catches a weeks-old clip re-queued today that the enqueue-time TTL misses.   let lastServerNow = Date.now();-  let curCreatedMs = 0;   // synth time of the CURRENT clip, so the age can re-tick+  let curCreatedMs = 0;   function fmtAgo(ms){     const s = Math.max(0, Math.floor(ms/1000));     if (s < 45) return 'just now';@@ -430,39 +472,34 @@     const h = Math.floor(m/60); if (h < 36) return h + 'h ago';     const d = Math.floor(h/24); return d + ' day' + (d===1?'':'s') + ' ago';   }-  // Show, by the transcript, WHEN this clip's audio was made + how long ago,-  // color-coded so old news is unmissable. Full timestamp lives in the hover-  // tooltip (and the subject card). You should never wonder if a clip is fresh.   function setClipAge(created, now){     const el = document.getElementById('clipFreshness');+    const txt = document.getElementById('freshText');+    const icon = document.getElementById('freshIcon');     const sp = document.getElementById('spCreated');     const tr = document.getElementById('transcript');     curCreatedMs = created || 0;-    if (!created){ el.style.display='none'; el.removeAttribute('data-tooltip'); if (tr) tr.removeAttribute('data-tooltip'); if (sp) sp.textContent = '—'; return; }+    if (!created){ el.hidden = true; el.removeAttribute('data-tooltip'); if (tr) tr.removeAttribute('data-tooltip'); if (sp) sp.textContent = 'none'; return; }     const age = Math.max(0, (now || lastServerNow) - created);-    const abs = new Date(created).toLocaleString(undefined,-      {weekday:'short', month:'short', day:'numeric', hour:'numeric', minute:'2-digit'});+    const abs = new Date(created).toLocaleString(undefined, {weekday:'short', month:'short', day:'numeric', hour:'numeric', minute:'2-digit'});     const ago = fmtAgo(age);-    if (sp) sp.textContent = abs + ' · ' + ago;-    el.style.display = '';+    if (sp) sp.textContent = abs + ', ' + ago;+    el.hidden = false;     el.className = 'clip-fresh';     const min = age / 60000;-    if (min >= 1440){ el.classList.add('stale'); el.textContent = '⚠ audio from ' + ago; }-    else if (min >= 60){ el.classList.add('warn'); el.textContent = 'audio from ' + ago; }-    else { el.textContent = 'audio from ' + ago; }-    // canonical hover tooltip (exact timestamp) on BOTH the freshness pill and-    // the transcript, so hovering the clip anywhere reveals when it was made-    const tip = 'This clip’s audio was synthesized ' + abs + ' — ' + ago-      + (min >= 1440 ? '. That is old; a fresh answer would be re-synthesized, not replayed.' : '.');+    hideEl(icon, !(min >= 1440));+    if (min >= 1440){ el.classList.add('stale'); txt.textContent = 'audio from ' + ago; }+    else if (min >= 60){ el.classList.add('warn'); txt.textContent = 'audio from ' + ago; }+    else { txt.textContent = 'audio from ' + ago; }+    const tip = 'This clip’s audio was synthesized ' + abs + ', ' + ago+      + (min >= 1440 ? '. That is old: a fresh answer would be re-synthesized, not replayed.' : '.');     el.setAttribute('data-tooltip', tip);     if (tr) tr.setAttribute('data-tooltip', tip);   }-  // keep the relative age honest without a new clip event (polls refresh-  // lastServerNow every ~700ms, so this just re-renders the text)+  window.setClipAge = setClipAge;   setInterval(function(){ if (curCreatedMs) setClipAge(curCreatedMs, lastServerNow); }, 10000); -  // ---- global tooltip renderer (adom-ui-design/tooltips): one fixed div,-  // cursor-anchored, edge-clamped, 600ms reveal via CSS transition delay ----+  // ---- global tooltip renderer (tooltips skill): one fixed div, edge-clamped ----   (function(){     const tip = document.getElementById('global-tooltip');     let _cx = 0, _cy = 0;@@ -494,9 +531,9 @@       tip.textContent = t.getAttribute('data-tooltip') || '';       if (!tip.textContent) return;       tip.classList.add('visible');-      const a = pickAnchor(t, tip.offsetWidth, tip.offsetHeight, _cx, _cy);-      tip.style.setProperty('--tip-x', a.x + 'px');-      tip.style.setProperty('--tip-y', a.y + 'px');+      const p = pickAnchor(t, tip.offsetWidth, tip.offsetHeight, _cx, _cy);+      tip.style.setProperty('--tip-x', p.x + 'px');+      tip.style.setProperty('--tip-y', p.y + 'px');       requestAnimationFrame(() => tip.classList.add('shown'));     }     function hide(){ tip.classList.remove('visible', 'shown'); }@@ -504,43 +541,31 @@       const t = e.target.closest && e.target.closest('[data-tooltip]');       t ? show(t) : hide();     });+    window.__showTooltipFor = show;   })();+   function applyClip(c){     curClipId = c.id;-    // TELEMETRY MUST NEVER DESCRIBE THE PREVIOUS CLIP. `curClipId` moves the-    // instant a new clip arrives, but the <audio> element keeps the OLD clip's-    // `ended`/`currentTime` until the new blob finishes downloading. Polling in-    // that window reported the NEW id with the OLD state (done=1, t=83.1), so-    // the drainer marked the new clip finished on arrival and advanced — the-    // human heard clip two get cut off ~3s in while its sender was told "ok".-    // Two guards: clear `loadedClipId` so telemetry is withheld until the audio-    // element truly holds THIS clip, and reset the element synchronously so-    // stale ended/currentTime cannot leak through any other path.+    // Telemetry must never describe the previous clip: withhold it until the+    // <audio> element truly holds THIS clip, and reset the element now.     loadedClipId = 0;     try { a.pause(); a.removeAttribute('src'); a.load(); } catch (e) {}-    // Subject = WHO is speaking (AI thread) + WHAT the clip says (--desc).-    // The mp3 filename means nothing to a human — it lives in the tooltip only.     subjectName.textContent = c.label || c.title || 'audio clip';     metaBase = c.desc ? c.desc : (c.meta || '');     meta.textContent = metaBase;-    // rich tooltip card: the untruncated story of the current clip     document.getElementById('spDesc').textContent = c.desc || '(no description sent)';-    document.getElementById('spThread').textContent = c.label || '—';-    document.getElementById('spFile').textContent = c.title || '—';-    document.getElementById('spVoice').textContent = c.meta || '—';+    document.getElementById('spThread').textContent = c.label || 'none';+    document.getElementById('spFile').textContent = c.title || 'none';+    document.getElementById('spVoice').textContent = c.meta || 'none';     setClipAge(c.created_ms, c.now_ms || lastServerNow);-    threadChip.style.display = 'none';  // thread now leads the subject zone     buildTranscript(c.words || []);-    // reset transport UI for the fresh clip     lastErr = ''; seek.value = 0; curEl.textContent = '0:00'; durEl.textContent = '0:00';     _wpos = 0; _stalls = 0;     setStatus('downloading', 'buffering'); setIcon('play');     wantPlay = true;     const pk = (c.script && c.seg) ? (c.script + '#' + c.seg) : null;     if (pk && prefetched[pk]) {-      // CUE FAST PATH: the marker's audio was prefetched at load time — bind it-      // and play now; no download, no buffering. Prefetched URLs are never-      // revoked by the ordinary download path (blobUrl stays null here).+      // CUE FAST PATH: the marker's audio was prefetched at load time.       blobUrl = null;       a.src = prefetched[pk]; try { a.load(); } catch(e) {}       loadedClipId = c.id;@@ -553,7 +578,7 @@     updateDiag();   }   function downloadAndPlay(id, attempt){-    if (id !== curClipId) return;                      // superseded while downloading+    if (id !== curClipId) return;     fetch('audio?id=' + id, { cache: 'no-store' })       .then(r => { if (!r.ok) throw new Error('http ' + r.status); return r.blob(); })       .then(b => {@@ -563,24 +588,22 @@         ev('blobready', '&bytes=' + b.size);         a.src = blobUrl;         try { a.load(); } catch(e) {}-        loadedClipId = id;   // NOW the element's state describes THIS clip+        loadedClipId = id;         tryPlay();       })       .catch(() => {-        // hotspot blip mid-download — retry while this is still the clip         if (id !== curClipId) return;         ev('dlretry', '&att=' + (attempt + 1));-        if (attempt < 8) { setStatus('reconnecting…', 'buffering'); setTimeout(() => downloadAndPlay(id, attempt + 1), 1600); }-        else setStatus('download failed', 'error');+        if (attempt < 8) { setStatus('reconnecting', 'buffering'); setTimeout(() => downloadAndPlay(id, attempt + 1), 1600); }+        else { setStatus('download failed', 'error'); showToast('Download failed. Press play to try again, or run adom-tts reopen.', 'error'); }       });   }    // ---- single transport ----   function tryPlay(){ const p=a.play(); if(p&&p.catch) p.then(()=>{lastErr='';playBtn.classList.remove('attention');}).catch(showPlayError); }-  function showPlayError(err){ lastErr=(err&&err.name||'PlayError')+(err&&err.message?': '+err.message:''); if(err&&err.name==='NotAllowedError'){setStatus('tap play ▶','paused');playBtn.classList.add('attention');setIcon('play');} else setStatus('play failed','error'); updateDiag(); ev('playreject','&msg='+encodeURIComponent(((err&&err.name)||'')+' '+((err&&err.message)||'').slice(0,60))); }+  function showPlayError(err){ lastErr=(err&&err.name||'PlayError')+(err&&err.message?': '+err.message:''); if(err&&err.name==='NotAllowedError'){setStatus('tap play','paused');playBtn.classList.add('attention');setIcon('play');} else setStatus('play failed','error'); updateDiag(); ev('playreject','&msg='+encodeURIComponent(((err&&err.name)||'')+' '+((err&&err.message)||'').slice(0,60))); }   playBtn.addEventListener('click', () => { if (a.ended) { a.currentTime=0; tryPlay(); } else if (a.paused) tryPlay(); else a.pause(); }); -  // tap toggles the subject card (hover covers mouse; this covers touch)   var subjEl = document.getElementById('subject');   subjEl.addEventListener('click', function(e){ if (e.target.closest('.subject-pop')) return; subjEl.classList.toggle('open'); });   document.addEventListener('click', function(e){ if (!subjEl.contains(e.target)) subjEl.classList.remove('open'); });@@ -588,8 +611,7 @@   seek.addEventListener('input', () => { seeking = true; if (isFinite(a.duration)) { a.currentTime = (seek.value/1000)*a.duration; curEl.textContent = fmt(a.currentTime); } });   seek.addEventListener('change', () => { seeking = false; }); -  // ---- audio events -----  a.addEventListener('loadedmetadata', () => { durEl.textContent = fmt(a.duration); if(isFinite(a.duration)){ meta.textContent=(metaBase?metaBase+' · ':'')+fmt(a.duration); beacon('meta?id='+curClipId+'&dur='+a.duration);} updateDiag(); });+  a.addEventListener('loadedmetadata', () => { durEl.textContent = fmt(a.duration); if(isFinite(a.duration)){ meta.textContent=(metaBase?metaBase+', ':'')+fmt(a.duration); beacon('meta?id='+curClipId+'&dur='+a.duration);} updateDiag(); });   a.addEventListener('canplay', () => { updateDiag(); if (wantPlay && a.paused && !a.ended && a.currentTime===0) tryPlay(); });   let lastBeacon = 0;   a.addEventListener('timeupdate', () => {@@ -603,9 +625,6 @@   a.addEventListener('playing', () => { lastErr=''; wantPlay=false; setStatus('playing','playing'); setIcon('pause'); playBtn.classList.remove('attention'); ev('playing'); });   a.addEventListener('waiting', () => setStatus('buffering','buffering'));   a.addEventListener('stalled', () => { setStatus('stalled','buffering'); updateDiag(); });-  // A pause is a HUMAN action: cancel any pending auto-play intent so nothing-  // (canplay retry, watchdog) un-pauses them. The server also sees paused=1 on-  // the poll and HOLDS the whole queue until they press play again.   a.addEventListener('pause',   () => { if (!a.ended){ wantPlay = false; setStatus('paused','paused'); setIcon('play'); } });   a.addEventListener('ended',   () => { setStatus('ended','ended'); setIcon('replay'); highlight(1); spans.forEach(el=>el.classList.add('spoken')); ev('ended'); beacon('done?id='+curClipId); });   a.addEventListener('error',   () => {@@ -622,66 +641,135 @@   let _wpos = 0, _stalls = 0;   setInterval(function(){     if (a.paused || a.ended || !isFinite(a.duration)) { _wpos = a.currentTime; return; }-    if (a.currentTime > _wpos + 0.15) { _wpos = a.currentTime; _stalls = 0; return; } // healthy-    if (a.readyState >= 3) return;                 // buffered enough; just a blip+    if (a.currentTime > _wpos + 0.15) { _wpos = a.currentTime; _stalls = 0; return; }+    if (a.readyState >= 3) return;     _stalls++;     if (_stalls <= 3) {       ev('selfheal', '&att=' + _stalls);-      setStatus('recovering…', 'buffering');+      setStatus('recovering', 'buffering');       var ct = a.currentTime;       try { a.load(); a.currentTime = ct; } catch (e) {}       a.play().catch(function(){});     } else if (_stalls === 4) {-      setStatus('stalled — press play', 'error');+      setStatus('stalled, press play', 'error');       ev('selfheal-giveup', '&t=' + a.currentTime.toFixed(1));     }   }, 2500); -  // ---- replayable history drawer ----+  // ---- replayable history ----   const histwrap = document.getElementById('histwrap');   const histlist = document.getElementById('histlist');   let histKey = '';   function ago(ms){ const s=Math.max(0,Math.round(ms/1000)); if(s<60)return s+'s'; const m=Math.round(s/60); if(m<60)return m+'m'; return Math.round(m/60)+'h'; }+  function svgUse(id, cls){ const s = document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('class', cls || 'ic'); const u = document.createElementNS('http://www.w3.org/2000/svg','use'); u.setAttribute('href', id); s.appendChild(u); return s; }   function renderHistory(items, nowMs){     const key = items.map(e => e.seq).join(',');-    if (key === histKey) {  // entries unchanged → just refresh the ago labels+    if (key === histKey) {       const whens = histlist.querySelectorAll('.hist-when');       items.forEach((e,i)=>{ if(whens[i]) whens[i].textContent = ago(nowMs - (e.ts_ms||nowMs)); });       return;     }     histKey = key;     histlist.innerHTML = '';-    if (!items.length) { histwrap.classList.add('hidden'); return; }-    histwrap.classList.remove('hidden');+    if (!items.length) { histwrap.hidden = true; return; }+    histwrap.hidden = false;     items.forEach(e => {       const row = document.createElement('div'); row.className = 'hist-row';       const th = document.createElement('span'); th.className = 'hist-thread'; th.textContent = e.label || '?'; row.appendChild(th);       const d = document.createElement('span'); d.className = 'hist-desc';       d.textContent = e.desc || e.name || 'clip';-      // a clip that never fully played is the one you MISSED — flag it       if (e.verdict && e.verdict !== 'ok') { d.classList.add('missed'); d.textContent += '  (missed?)'; }-      d.title = (e.label?e.label+' — ':'') + (e.desc||'') + (e.dur_s?(' · '+e.dur_s+'s'):'');+      d.setAttribute('data-tooltip', (e.label?e.label+': ':'') + (e.desc||'') + (e.dur_s?(', '+e.dur_s+'s'):''));       row.appendChild(d);       const w = document.createElement('span'); w.className = 'hist-when'; w.textContent = ago(nowMs - (e.ts_ms||nowMs)); row.appendChild(w);-      const b = document.createElement('button'); b.className = 'hist-replay'; b.title = 'Play this again'; b.textContent = '↻';-      b.addEventListener('click', () => { b.textContent='…'; fetch('histplay?seq='+e.seq,{cache:'no-store'}).then(()=>{ b.textContent='✓'; setTimeout(()=>b.textContent='↻',1500); }).catch(()=>{ b.textContent='↻'; }); });+      const b = document.createElement('button'); b.className = 'hist-replay'; b.type = 'button';+      b.setAttribute('aria-label', 'Play this clip again'); b.setAttribute('data-tooltip', 'Play this clip again, right now, ahead of the queue');+      b.appendChild(svgUse('#i-replay'));+      b.addEventListener('click', () => {+        b.innerHTML = ''; b.appendChild(svgUse('#i-replay'));+        fetch('histplay?seq='+e.seq,{cache:'no-store'}).then(r => r.json()).then(j => {+          b.innerHTML = ''; b.appendChild(svgUse(j && j.ok ? '#i-check' : '#i-warn'));+          if (!(j && j.ok)) showToast('Could not replay that clip. Its audio file may be gone.', 'error');+          setTimeout(()=>{ b.innerHTML=''; b.appendChild(svgUse('#i-replay')); },1500);+        }).catch(()=>{ b.innerHTML=''; b.appendChild(svgUse('#i-replay')); showToast('The player did not answer. Run adom-tts restart.', 'error'); });+      });       row.appendChild(b);       histlist.appendChild(row);     });   } -  // ---- live link + clip poll: one poll drives BOTH the LED heartbeat AND-  // the clip swap. Each successful poll proves the front-end↔back-end pipe. ----+  // ---- status pill row (status-leds): SHORT state from the server, prose on tap ----+  const ledPop = document.getElementById('led-pop');+  let lastHealth = null;+  function renderLeds(h){+    lastHealth = h || null;+    document.querySelectorAll('.led-pill').forEach(p => {+      const k = p.getAttribute('data-key'); const sub = h && h[k];+      const st = p.querySelector('.st');+      if (!sub) { p.setAttribute('data-state', ''); st.textContent = 'checking'; return; }+      const led = sub.led === 'ok' ? 'ok' : sub.led === 'busy' ? 'busy' : sub.led === 'bad' ? 'bad' : '';+      p.setAttribute('data-state', led);+      st.textContent = sub.state || '';+    });+  }+  function ledDetail(pill){+    const k = pill.getAttribute('data-key'); const sub = lastHealth && lastHealth[k];+    ledPop.innerHTML = '';+    const text = sub ? (sub.detail || sub.state || '') : 'Not probed yet.';+    // render `code` spans for backticked commands+    text.split('`').forEach((part, i) => { const n = i % 2 ? document.createElement('code') : document.createTextNode(part); if (i % 2) { n.textContent = part; } ledPop.appendChild(n); });+    ledPop.classList.add('visible');+    const r = pill.getBoundingClientRect();+    const w = ledPop.offsetWidth, hgt = ledPop.offsetHeight;+    let x = r.left, y = r.bottom + 8;+    if (y + hgt > innerHeight - 8) y = Math.max(8, r.top - hgt - 8);+    x = Math.max(8, Math.min(x, innerWidth - w - 8));+    ledPop.style.left = x + 'px'; ledPop.style.top = y + 'px';+  }+  document.querySelectorAll('.led-pill').forEach(p => p.addEventListener('click', e => { e.stopPropagation(); ledDetail(p); }));+  document.addEventListener('click', e => { if (!ledPop.contains(e.target)) ledPop.classList.remove('visible'); });+  window.addEventListener('keydown', e => { if (e.key === 'Escape') { ledPop.classList.remove('visible'); closeSettings(); } });++  // ---- settings modal (settings-layout): the gear opens it, writes go to the CLI's config ----+  const settingsEl = document.getElementById('settings');+  let settingsState = null;+  function renderSettings(s){+    settingsState = s || {};+    document.getElementById('setPath').textContent = s.path || '~/.adom/tts/config.toml';+    document.getElementById('setSession').textContent = s.pup_session || '';+    document.getElementById('setProfile').textContent = s.pup_profile || '';+    document.getElementById('setLiveVal').textContent = s.live_surface ? (s.live_surface === 'hydrogen' ? 'webview' : s.live_surface) : 'no player running';+    document.querySelectorAll('.seg [data-surface]').forEach(b => {+      const on = b.getAttribute('data-surface') === s.surface;+      b.classList.toggle('is-on', on); b.setAttribute('aria-pressed', on ? 'true' : 'false');+    });+  }+  function openSettings(){+    settingsEl.hidden = false;+    fetch('settings', {cache:'no-store'}).then(r => r.json()).then(renderSettings).catch(() => showToast('Could not read settings from the player.', 'error'));+    document.getElementById('settingsDone').focus();+  }+  function closeSettings(){ settingsEl.hidden = true; }+  function saveSetting(key, value){+    return fetch('settings', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ key, value }) })+      .then(r => r.json()).then(j => {+        if (j && j.ok) { renderSettings(j.settings); showToast('Saved: ' + key + ' = ' + (value === 'hydrogen' ? 'webview' : value) + (j.note ? '. ' + j.note : ''), 'success'); }+        else showToast('Could not save ' + key + ': ' + ((j && j.error) || 'unknown error'), 'error');+      }).catch(() => showToast('The player did not answer. Run adom-tts restart.', 'error'));+  }+  document.getElementById('settings-btn').addEventListener('click', openSettings);+  document.getElementById('settingsClose').addEventListener('click', closeSettings);+  document.getElementById('settingsDone').addEventListener('click', closeSettings);+  document.getElementById('settingsBackdrop').addEventListener('click', closeSettings);+  document.getElementById('settingsDefaults').addEventListener('click', () => saveSetting('surface', 'hydrogen'));+  document.querySelectorAll('.seg [data-surface]').forEach(b => b.addEventListener('click', () => {+    const v = b.getAttribute('data-surface'); if (settingsState && settingsState.surface === v) return; saveSetting('surface', v);+  }));++  // ---- live link + clip poll: one poll drives the LED, the pills, the toasts and the clip swap ----   const led = document.getElementById('live');-  let lastCmdSeq = 0, lastPing = 0;+  let lastCmdSeq = 0, lastPing = 0, lastToastSeq = -1;   function poll() {-    // Telemetry rides ON the poll: after a hotspot gap the first poll through-    // carries the true position/done state, so lost beacons can't produce a-    // false "partial/no-playback" verdict for a clip that actually played.-    // `loaded` tells the drainer whether the element's numbers describe the-    // clip named by `id`. While a new clip is still downloading it is 0 and we-    // send zeroed telemetry, so the previous clip's ended/position can never be-    // attributed to the new clip (which used to kill it on arrival).     const loaded = (loadedClipId !== 0 && loadedClipId === curClipId) ? 1 : 0;     const q = 'poll?id=' + curClipId       + '&loaded=' + loaded@@ -692,7 +780,13 @@     fetch(q, { cache: 'no-store' }).then(r => r.json()).then(j => {       lastPing = Date.now();       if (!j) return;-      if (j.now_ms) lastServerNow = j.now_ms;   // keep the clip-age display honest+      if (j.now_ms) lastServerNow = j.now_ms;+      if (j.version) { document.getElementById('poVer').textContent = 'v' + j.version; document.getElementById('poPlayer').textContent = 'v' + j.version + ' (page built with v{{VERSION}})'; }+      if (j.health) renderLeds(j.health);+      if (typeof j.toast_seq === 'number') {+        if (lastToastSeq === -1) lastToastSeq = j.toast_seq;           // never replay a toast raised before this page loaded+        else if (j.toast_seq > lastToastSeq && j.toast && j.toast.text) { lastToastSeq = j.toast_seq; showToast(j.toast.text, j.toast.kind || 'info'); }+      }       if (Array.isArray(j.prefetch)) j.prefetch.forEach(pf => {         const k = pf.script + '#' + pf.n;         if (prefetched[k] || prefetching[k]) return;@@ -703,34 +797,69 @@           .catch(() => {})           .finally(() => { delete prefetching[k]; });       });-      // queue depth chip-      if (typeof j.queued === 'number' && j.queued > 0) { queueChip.textContent = '+' + j.queued + ' queued'; queueChip.classList.remove('hidden'); }-      else queueChip.classList.add('hidden');+      if (typeof j.queued === 'number' && j.queued > 0) { queueChip.textContent = '+' + j.queued + ' queued'; queueChip.hidden = false; }+      else queueChip.hidden = true;       renderHistory(j.history || [], j.now_ms || Date.now());-      // NEW clip? swap without reloading the page.       if (j.id && j.id !== curClipId) applyClip(j);-      else if (!j.id && curClipId && a.ended) { subjectName.textContent = 'waiting for a clip…'; setClipAge(0); }-      // control commands (replay/pause/play/toggle)+      else if (!j.id && curClipId && a.ended) { subjectName.textContent = 'waiting for a clip'; setClipAge(0); }       if (j.seq > lastCmdSeq) {         lastCmdSeq = j.seq;         switch ((j.cmd || '').toLowerCase()) {           case 'replay': a.currentTime = 0; tryPlay(); break;           case 'play':   tryPlay(); break;           case 'pause':  a.pause(); break;-          case 'skip':   a.pause(); wantPlay = false; break;   // drainer already ended the clip; just stop the sound+          case 'skip':   a.pause(); wantPlay = false; break;           case 'toggle': if (a.paused) tryPlay(); else a.pause(); break;         }       }-    }).catch(() => {}); // a failed poll leaves lastPing stale → LED ages to red+    }).catch(() => {});   }-  setInterval(poll, 700);+  window.__ttsPoll = setInterval(poll, 700);   poll();   setInterval(() => {     const age = Date.now() - lastPing;     const s = age < 2500 ? 'ok' : age < 6000 ? 'warn' : 'down';-    led.className = 'led ' + s;-    led.title = 'AI ↔ player link — ' + (s === 'ok' ? 'live' : s === 'warn' ? 'degraded' : 'disconnected (dead tab)');+    led.className = 'dot ' + s;+    document.getElementById('targetChip').setAttribute('data-tooltip', 'Where clips play. Live link to the player: '+      + (s === 'ok' ? 'polling.' : s === 'warn' ? 'degraded, polls are late.' : 'lost. The player closed or this tab is stale: run adom-tts reopen.'));   }, 1000);++  // ---- the newbie panel: MOUNT the vendored module, never rebuild it ----+  region('newbie-panel', function(){+    const PHRASES = [+      'with adom tts, read me your last answer',+      'with adom tts, replay the last clip',+      'with adom tts, what is queued right now',+      'with adom tts, pause the player',+      'with adom tts, play the demo'+    ];+    let tries = 0;+    (function mountWhenReady(){+      if (!window.AdomNewbiePanel) {+        if (++tries < 50) return setTimeout(mountWhenReady, 100);+        window.__ttsBoot.failed.push('newbie-panel: newbie-panel.js did not load');+        showToast('The starter panel did not load. Reload the page, or run adom-tts reopen.', 'error', true);+        return;+      }+      try { window.AdomNewbiePanel.mount({+        mount: '#newbie',+        app: 'adom tts',+        appName: 'Adom TTS',+        slug: 'adom-tts',+        toggleButton: '#help-btn',+        demo: { label: 'Play the demo', meta: '3 markers, about 40 seconds',+          onPlay: function(){+            fetch('demo', { method: 'POST' }).then(r => r.json()).then(j => {+              if (j && j.ok) showToast('Demo starting. Three short markers play in this player.', 'info');+              else showToast('Could not start the demo. Run adom-tts demo in a terminal.', 'error');+            }).catch(() => showToast('The player did not answer. Run adom-tts restart.', 'error'));+          } },+        phrases: PHRASES+      }); window.__ttsBoot.panelMounted = true; }+      catch (e) { window.__ttsBoot.failed.push('newbie-panel: ' + (e && e.message || e)); showToast('The starter panel failed to mount: ' + (e && e.message || e), 'error', true); }+    })();+  });+  window.__ttsBoot.complete = true; </script> </body> </html>
src/server.rs+9
@@ -486,6 +486,15 @@ fn open_webview_tab(url: &str) -> Result<()> { /// reason is "audio is waiting/playing — come listen"; once the audio is done /// the flash is noise) and when the drainer closes. Belt to the --duration 600 /// suspenders on `alert set` (10-min hard cap even if no one clears).+/// Flash the tab: a clip is starting and the human should come listen. Set at+/// CLIP START (never at tab open): a dock click that opens an idle player must+/// not flash for ten minutes with nothing playing.+pub(crate) fn set_tab_alert(display_name: &str) {+    let _ = Command::new("adom-cli")+        .args(["hydrogen", "alert", "set", "--name", display_name, "--duration", "600"])+        .output();+}+ pub(crate) fn clear_tab_alert(display_name: &str) {     let _ = Command::new("adom-cli")         .args(["hydrogen", "alert", "clear", "--name", display_name])
tools/ralph-test.sh

  
tools/ui-shot.pyadded+116
@@ -0,0 +1,116 @@+#!/usr/bin/env python3+"""UI ralph harness for the player page: drives a MUTED headless Chrome over raw+CDP (no puppeteer needed) against an isolated rig drainer and writes one PNG per+state. Never touches the human's player or any Hydrogen tab.++  python3 tools/ui-shot.py http://127.0.0.1:<rig-port>/ <outdir>++States captured: idle, clip (transcript + history + freshness pill), settings+modal, toasts, stale-audio pill, pill detail popover, corner tooltip, newbie+panel closed, and phone width (open and closed). Also prints the page's boot+marker and the newbie module's selfCheck so a half-booted page cannot pass.+"""+import asyncio, json, os, glob, socket, subprocess, sys, time, urllib.request+import websockets++URL, OUT = sys.argv[1], sys.argv[2]+os.makedirs(OUT, exist_ok=True)++def find_shell():+    cands = glob.glob(os.path.expanduser("~/.cache/puppeteer/chrome-headless-shell/*/chrome-headless-shell-linux64/chrome-headless-shell")) \+          + glob.glob(os.path.expanduser("~/.cache/ms-playwright/chromium_headless_shell-*/chrome-linux/headless_shell"))+    for c in ["/usr/bin/chromium-browser", "/usr/bin/chromium", "/usr/bin/google-chrome"]:+        if os.path.exists(c): cands.append(c)+    if not cands: sys.exit("ERROR: no headless chrome found. Hint: npx puppeteer browsers install chrome-headless-shell")+    return cands[0]++def free_port():+    s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p++port = free_port()+shell = find_shell()+proc = subprocess.Popen([shell, "--headless", "--no-sandbox", "--disable-gpu", "--mute-audio", "--hide-scrollbars",+    "--autoplay-policy=no-user-gesture-required", f"--remote-debugging-port={port}", "--window-size=1400,900", "about:blank"],+    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)++def targets():+    for _ in range(50):+        try:+            with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/list", timeout=1) as r: return json.load(r)+        except Exception: time.sleep(0.2)+    sys.exit("ERROR: headless chrome never answered on its debug port")++async def main():+    page = next(t for t in targets() if t["type"] == "page")+    async with websockets.connect(page["webSocketDebuggerUrl"], max_size=64 * 1024 * 1024) as ws:+        seq = 0+        async def call(method, **params):+            nonlocal seq; seq += 1+            await ws.send(json.dumps({"id": seq, "method": method, "params": params}))+            while True:+                m = json.loads(await ws.recv())+                if m.get("id") == seq:+                    if "error" in m: raise RuntimeError(f"{method}: {m['error']}")+                    return m.get("result", {})+        async def js(expr):+            r = await call("Runtime.evaluate", expression=expr, awaitPromise=True, returnByValue=True)+            return r.get("result", {}).get("value")+        async def shot(name):+            r = await call("Page.captureScreenshot", format="png")+            import base64; open(os.path.join(OUT, name + ".png"), "wb").write(base64.b64decode(r["data"]))+            print("shot", name)+        async def size(w, h):+            await call("Emulation.setDeviceMetricsOverride", width=w, height=h, deviceScaleFactor=1, mobile=w < 600)++        await call("Page.enable"); await call("Runtime.enable")+        await size(1400, 900)+        await call("Page.navigate", url=URL); await asyncio.sleep(3.0)+        print("boot:", await js("JSON.stringify(window.__ttsBoot)"))+        print("panel selfCheck:", await js("(function(){try{var p=window.AdomNewbiePanel;if(!p)return 'no module';if(!p.selfCheck)return 'version '+p.version+' (no selfCheck)';var r=p.selfCheck();return JSON.stringify(r).slice(0,600);}catch(e){return 'threw '+e}})()"))+        await shot("01-idle")+        # a clip: the caller enqueued one; wait for the page to swap and render+        for _ in range(30):+            if await js("!!(document.getElementById('transcript') && !document.getElementById('transcript').hidden)"): break+            await asyncio.sleep(0.5)+        await asyncio.sleep(1.5)+        await shot("02-clip")+        print("icons while playing:", await js("JSON.stringify({play:document.getElementById('ic-play').hasAttribute('hidden'),pause:document.getElementById('ic-pause').hasAttribute('hidden'),freshWarnHidden:document.getElementById('freshIcon').hasAttribute('hidden'),status:document.getElementById('status').textContent})"))+        print("panel-open spacing:", await js("(function(){var n=document.getElementById('newbie').firstElementChild;var r=n?n.getBoundingClientRect():null;var h=document.querySelector('header.dash').getBoundingClientRect();return JSON.stringify({panelTop:r&&r.top,panelBottom:r&&r.bottom,headerBottom:h.bottom,ledsTop:document.getElementById('leds').getBoundingClientRect().top})})()"))+        # let the clip end so History renders its rows (replay icon check)+        await js("var a=document.getElementById('a'); if(isFinite(a.duration)) a.currentTime=Math.max(0,a.duration-0.4);"); await asyncio.sleep(3.0)+        await size(1400, 1150); await asyncio.sleep(0.3)   # tall enough to show History under a full transcript+        await shot("02b-ended-history")+        await size(1400, 900)+        await js("document.getElementById('settings-btn').click()"); await asyncio.sleep(0.8)+        await shot("03-settings")+        await js("document.getElementById('settingsDone').click()")+        await js("showToast('Running test 3 of 8', 'info'); showToast('File downloaded', 'success'); showToast('Voice service did not answer. Run adom-tts health.', 'error')")+        await asyncio.sleep(0.4); await shot("04-toasts")+        await js("document.querySelectorAll('.toast .x').forEach(function(b){b.click()})")+        await js("setClipAge(Date.parse('2026-06-16T22:55:00'), Date.now())"); await asyncio.sleep(0.2)+        await shot("05-stale-pill")+        await js("document.querySelector('.led-pill[data-key=service]').click()"); await asyncio.sleep(0.3)+        await shot("06-pill-detail")+        await js("document.body.click()")+        # tooltip clamp test: cursor never moved, so the anchor is (0,0) and must clamp inside the viewport+        await js("__showTooltipFor(document.getElementById('targetChip'))"); await asyncio.sleep(1.0)+        print("tooltip rect:", await js("(function(){var r=document.getElementById('global-tooltip').getBoundingClientRect();return JSON.stringify({l:r.left,t:r.top,r:r.right,b:r.bottom,vw:innerWidth,vh:innerHeight,inside:(r.left>=0&&r.top>=0&&r.right<=innerWidth&&r.bottom<=innerHeight)})})()"))+        await shot("07-tooltip")+        await js("document.getElementById('global-tooltip').className=''")+        await js("try{AdomNewbiePanel.close({remember:false})}catch(e){document.getElementById('help-btn').click()}"); await asyncio.sleep(0.5)+        await shot("08-panel-closed")+        print("gutter check (panel open/closed spacing under header):", await js("(function(){var l=document.getElementById('leds').getBoundingClientRect();var h=document.querySelector('header.dash').getBoundingClientRect();return JSON.stringify({ledsTop:l.top,headerBottom:h.bottom,ledsLeft:l.left})})()"))+        await size(390, 844); await asyncio.sleep(0.6)+        await shot("09-phone-closed")+        await js("try{AdomNewbiePanel.open({remember:false})}catch(e){document.getElementById('help-btn').click()}"); await asyncio.sleep(0.5)+        await shot("10-phone-open")+        print("phone overflow:", await js("JSON.stringify({scrollW:document.documentElement.scrollWidth, vw:innerWidth})"))+        await js("try{AdomNewbiePanel.close({remember:false})}catch(e){}")+        await size(2400, 1000); await asyncio.sleep(0.5)+        await shot("11-wide-closed")+        print("console errors:", await js("JSON.stringify((window.__ttsBoot||{}).failed)"))++try:+    asyncio.run(main())+finally:+    proc.terminate()
vendor/newbie-panel.jsadded+691
@@ -0,0 +1,691 @@+/* Adom newbie panel - the ONE implementation every dock dashboard mounts.+ * Published in adom-ui-design. Do not fork it: fix it here and every app gets the fix.+ * Self-contained: no network, no deps, fonts embedded. Serve it next to your dashboard+ * and <script src="newbie-panel.js"></script>, or inline its text into your page.+ */+(function (root) {+  'use strict';+  var CSS = `+@font-face{font-family:'Familjen Grotesk';font-style:normal;font-weight:400 700;font-display:swap;+  src:url(data:font/woff2;base64,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) format('woff2-variations');}+@font-face{font-family:'Satoshi';font-style:normal;font-weight:300 900;font-display:swap;+  src:url(data:font/woff2;base64,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) format('woff2-variations');}++.anp-wrap{container-type:inline-size; container-name:anp; width:100%;}+.anp{+  --anp-surface:#161b22; --anp-line:#30363d; --anp-teal:#00b8b1;+  --anp-text:#e6edf3; --anp-muted:#8b949e; --anp-row:#262d36;+  --anp-display:'Familjen Grotesk',ui-sans-serif,system-ui,sans-serif;+  --anp-body:'Satoshi',ui-sans-serif,system-ui,sans-serif;+  box-sizing:border-box; width:100%; margin:16px 0 20px; padding:18px 20px 20px;+  background:var(--anp-surface); border:1px solid var(--anp-line);+  border-left:3px solid var(--anp-teal); border-radius:12px;+  font-family:var(--anp-body); color:var(--anp-text);+  display:grid; grid-template-areas:"head head" "left right";+  grid-template-columns:220px minmax(0,1fr); column-gap:24px; row-gap:14px;+}+.anp *,.anp *::before,.anp *::after{box-sizing:border-box;}+.anp[hidden]{display:none;}++.anp-head{grid-area:head; position:relative; padding-right:40px;}+.anp-head h2{margin:0; font:600 15px/1.3 var(--anp-display); color:var(--anp-text); letter-spacing:-.01em;}+.anp-head p{margin:6px 0 0; font:400 13.5px/1.5 var(--anp-body); color:var(--anp-muted); max-width:78ch;}+.anp-head p b{color:var(--anp-teal); font-weight:500;}++.anp-left{grid-area:left; display:flex; flex-direction:column; align-items:flex-start; gap:8px;}+.anp-right{grid-area:right; min-width:0;}++.anp-demo{+  display:inline-flex; align-items:center; gap:9px; white-space:nowrap;+  font:600 14px/1 var(--anp-display); color:#05221f;+  background:var(--anp-teal); border:0; border-radius:8px; padding:10px 18px;+  cursor:pointer; transition:filter .15s ease;+}+.anp-demo:hover{filter:brightness(1.08);}+.anp-demo:active{filter:brightness(.94);}+.anp-demo:focus-visible{outline:2px solid var(--anp-teal); outline-offset:3px;}+.anp-demo svg{width:14px;height:14px;flex:0 0 auto;}+.anp-meta{font:400 12px/1.4 var(--anp-body); color:var(--anp-muted);}++.anp-tbl{width:100%; max-width:720px; border-collapse:collapse; table-layout:fixed;}+.anp-tbl td{padding:0; border:0; border-bottom:1px solid var(--anp-row); height:40px; vertical-align:middle;}+.anp-tbl tr:last-child td{border-bottom:0;}+.anp-tbl tr{cursor:pointer;}+.anp-tbl tr:hover td{background:rgba(255,255,255,.03);}+.anp-phrase{width:auto; min-width:0;}+.anp-phrase span{display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;+  font:400 14px/1.4 var(--anp-body); color:var(--anp-text);}+.anp-phrase b{color:var(--anp-teal); font-weight:400;}+.anp-acts{width:72px; white-space:nowrap; text-align:right;}+.anp-acts .anp-ib + .anp-ib{margin-left:6px;}++.anp-ib{display:inline-flex; align-items:center; justify-content:center; width:28px; height:28px;+  padding:0; background:none; border:0; border-radius:6px; color:var(--anp-muted); cursor:pointer;+  transition:color .15s ease, background .15s ease;}+.anp-ib:hover{color:var(--anp-teal); background:rgba(255,255,255,.05);}+.anp-ib:focus-visible{outline:2px solid var(--anp-teal); outline-offset:1px;}+.anp-ib svg{width:16px;height:16px;display:block;}+.anp-ib[disabled]{opacity:.45; cursor:default;}++.anp-x{position:absolute; top:-2px; right:0; width:28px; height:28px; padding:0; background:none;+  border:0; border-radius:6px; color:var(--anp-muted); cursor:pointer;}+.anp-x:hover{color:var(--anp-text); background:rgba(255,255,255,.06);}+.anp-x:focus-visible{outline:2px solid var(--anp-teal); outline-offset:1px;}+.anp-x svg{width:14px;height:14px;display:block;margin:auto;}++@container anp (max-width:699px){+  .anp{grid-template-areas:"head" "left" "right"; grid-template-columns:minmax(0,1fr); row-gap:12px;}+  .anp-left{flex-direction:row; align-items:center; gap:12px;}+}++.anp-toasts{position:fixed; left:50%; bottom:22px; transform:translateX(-50%); z-index:99999;+  display:flex; flex-direction:column-reverse; gap:8px; pointer-events:none;}+.anp-toast{pointer-events:auto; display:flex; align-items:center; gap:10px; max-width:min(92vw,520px);+  padding:10px 14px; border-radius:8px; border:1px solid var(--anp-line,#30363d);+  background:#1c2230; color:#e6edf3; font:400 13.5px/1.4 'Satoshi',ui-sans-serif,system-ui,sans-serif;+  box-shadow:0 10px 30px rgba(0,0,0,.45); animation:anp-in .18s ease;}+.anp-toast.ok{border-left:3px solid #3fb950;}+.anp-toast.err{border-left:3px solid #f85149;}+.anp-toast.info{border-left:3px solid #00b8b1;}+@keyframes anp-in{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}+@media (prefers-reduced-motion:reduce){.anp-toast{animation:none;}}++.anp-tip{position:fixed; z-index:99999; pointer-events:none; padding:6px 9px; border-radius:6px;+  background:#0d1117; border:1px solid #30363d; color:#e6edf3; text-transform:none;+  font:400 12px/1.3 'Satoshi',ui-sans-serif,system-ui,sans-serif; box-shadow:0 6px 20px rgba(0,0,0,.5);+  opacity:0; transition:opacity .18s ease;}+.anp-tip.on{opacity:1;}+`;+  var ICON = {+    copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/></svg>',+    send: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.5 2.5 10.5 13.5"/><path d="M21.5 2.5 14.8 21.5l-4.3-8-8-4.3z"/></svg>',+    play: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4l14 8-14 8z"/></svg>',+    x:    '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M5 5l14 14M19 5 5 19"/></svg>',+    help: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.2 9.3a2.9 2.9 0 0 1 5.6 1c0 1.9-2.8 2.4-2.8 4"/><path d="M12 17.6h.01"/></svg>'+  };+  /* The two-column layout needs, inside the panel's padding:+   *   phrase cell 336 + action column 72 + 8 gutter = 416 for the right column+   *   left column 220 + column gap 24 + 416          = 660 content+   *   + 20 left padding + 20 right padding           = 700 PANEL border-box+   * That is where 700 comes from. It is a PANEL width, not a viewport width, so it is the+   * same number in every app regardless of that app's page chrome. */+  var TWO_COL_MIN = 700, PHRASE_BUDGET = 336;+  var MIN_API = [1, 0, 142];++  /* Hydrogen 1.0.187+ answers a send with a JOB and reports progress on+   * GET /ai-threads/run-prompt/status?job=<uuid>. Measured on the VM: tab visible ~0.1s,+   * session id ~2.5s, sent by 3-5s, a trivial answer done ~8s. One poll a second is the+   * right cadence for that shape, and it is an in-memory read on the other end. */+  var PHASE = {+    opening:               'Opening a new AI chat…',+    prefilled:             'Prompt ready…',+    submitted:             'Sent, waiting for the AI…',+    working:               'The AI is working…',+    needs_you:             'The AI needs you. Open the tab.',+    done:                  'Done.',+    submit_failed:         'The prompt is in the tab. Press Enter there to send it.',+    submitted_unidentified:'Sent, but the AI never checked in.',+    submit_unconfirmed:    'Sent, but delivery was not confirmed.',+    still_working:         'The AI is still working. Open the tab.'+  };++  function css() {+    if (document.getElementById('anp-css')) return;+    var s = document.createElement('style');+    s.id = 'anp-css'; s.textContent = CSS;+    document.head.appendChild(s);+  }+  function el(tag, cls, html) {+    var n = document.createElement(tag);+    if (cls) n.className = cls;+    if (html != null) n.innerHTML = html;+    return n;+  }+  function esc(s) {+    return String(s).replace(/[&<>"]/g, function (c) {+      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];+    });+  }+  /* Highlight the trigger noun. Built with RegExp(), never a literal, because a dashboard+   * assembled as a JS template literal turns \b into a BACKSPACE and the boundaries vanish+   * silently: the regex still matches, just too greedily. (nb, 2026-08-25) */+  function mark(phrase, noun) {+    var safe = esc(phrase);+    if (!noun) return safe;+    var re = new RegExp('\\b(' + esc(noun).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')\\b', 'gi');+    return safe.replace(re, '<b>$1</b>');+  }++  /* ---- copy, with a fallback that does not need the async clipboard ------+   * navigator.clipboard.writeText needs a secure context AND, in several embedded webviews,+   * transient user activation plus a clipboard-write permission that is simply absent. In+   * ah's webview it rejects with NotAllowedError, so the ONLY path the panel had would fail+   * and the user got "Could not reach the clipboard" on a button that looks like it works.+   * The execCommand path is deprecated but synchronous, needs no permission, and works in+   * exactly the contexts the async API refuses. Try the modern one, fall back, and only then+   * report failure. */+  function copyText(text) {+    return new Promise(function (resolve, reject) {+      function legacy() {+        try {+          var ta = document.createElement('textarea');+          ta.value = text;+          ta.setAttribute('readonly', '');+          ta.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0';+          document.body.appendChild(ta);+          ta.select(); ta.setSelectionRange(0, ta.value.length);+          var ok = document.execCommand && document.execCommand('copy');+          ta.remove();+          ok ? resolve('legacy') : reject(new Error('execCommand refused'));+        } catch (e) { reject(e); }+      }+      if (navigator.clipboard && navigator.clipboard.writeText) {+        navigator.clipboard.writeText(text).then(function () { resolve('async'); }, legacy);+      } else legacy();+    });+  }++  /* ---- toasts ---------------------------------------------------------- */+  var toastHost = null;+  function toast(msg, kind, ms) {+    if (!toastHost) { toastHost = el('div', 'anp-toasts'); document.body.appendChild(toastHost); }+    var t = el('div', 'anp-toast ' + (kind || 'info'), esc(msg));+    toastHost.appendChild(t);+    /* retextable, so a long send can narrate its phases in ONE toast instead of+     * stacking six of them on top of each other. */+    t.setText = function (next) { t.innerHTML = esc(next); return t; };+    setTimeout(function () { t.remove(); }, ms || (kind === 'err' ? 7000 : 4000));+    return t;+  }++  /* ---- one tooltip, viewport-clamped, never a title attribute ----------- */+  var tip = null, tipT = null;+  function tipFor(node, text) {+    node.setAttribute('aria-label', text);+    node.removeAttribute('title');          // two tooltips on one control is a bug+    node.__anpSetTip = function (t) { text = t; if (tip && tip.classList.contains('on')) tip.textContent = t; };+    node.addEventListener('mouseenter', function (e) {+      clearTimeout(tipT);+      tipT = setTimeout(function () {+        if (!tip) { tip = el('div', 'anp-tip'); document.body.appendChild(tip); }+        tip.textContent = text; tip.classList.add('on');+        var r = node.getBoundingClientRect(), w = tip.offsetWidth, h = tip.offsetHeight, M = 8;+        var left = r.left + r.width / 2 - w / 2, top = r.top - h - 8;+        if (top < M) top = r.bottom + 8;+        left = Math.max(M, Math.min(left, document.documentElement.clientWidth - w - M));+        top = Math.max(M, Math.min(top, document.documentElement.clientHeight - h - M));+        tip.style.left = left + 'px'; tip.style.top = top + 'px';+      }, 600);+    });+    ['mouseleave', 'blur', 'click'].forEach(function (ev) {+      node.addEventListener(ev, function () {+        clearTimeout(tipT); if (tip) tip.classList.remove('on');+      });+    });+  }++  /* ---- send to a new AI thread ----------------------------------------- */+  function verNum(s) {+    var p = String(s || '').split('.').map(function (x) { return parseInt(x, 10) || 0; });+    return [p[0] || 0, p[1] || 0, p[2] || 0];+  }+  function verGte(a, b) {+    for (var i = 0; i < 3; i++) { if (a[i] > b[i]) return true; if (a[i] < b[i]) return false; }+    return true;+  }+  function gate(base) {+    /* NUMERIC compare, split on dots. A string compare passes "1.0.99", which is older and+     * still carries the crash. Fail OPEN if the probe itself is unreachable. */+    return fetch(base + '/updater/status', { cache: 'no-store' })+      .then(function (r) { return r.json(); })+      .then(function (j) {+        var v = (j && j.updater && j.updater.current_version) || '0.0.0';+        return { ok: verGte(verNum(v), MIN_API), version: v };+      })+      .catch(function () { return { ok: true, version: null, open: true }; });+  }+  function send(base, phrase, appName, slug, onProgress) {+    return gate(base).then(function (g) {+      if (!g.ok) throw new Error('Adom needs updating to send prompts (found ' + g.version + ', needs 1.0.142). Run: adom update');+      var ctl = new AbortController(), timer = setTimeout(function () { ctl.abort(); }, 120000);+      /* ONE call. POST /ai-threads/new NEVER returns an id, so a two-step create-then-run+       * flow fails 100% of the time and looks like a platform fault. */+      return fetch(base + '/ai-threads/run-prompt', {+        method: 'POST', signal: ctl.signal,+        headers: { 'content-type': 'application/json' },+        body: JSON.stringify({+          "new": true, prompt: phrase, submit: true,+          name: appName + ' - say it', tags: ['dock:' + slug]+        })+      }).then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })+        .then(function (res) {+          clearTimeout(timer);+          var j = res.j || {};+          if (!(res.status >= 200 && res.status < 300))+            throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+          /* PRE-1.0.187 SHAPE, kept because older Hydrogen builds still answer this way.+           * delivered_to is an ARRAY; a thread that opened but did not confirm delivery is+           * NOT a failure, and retrying it double-sends. */+          var dt = j.delivered_to, arr = Array.isArray(dt) ? dt : (dt ? [dt] : []);+          if (arr.length || (j.inject && j.inject.delivered_in_conversation)) return { state: 'delivered' };+          /* 1.0.187+ returns in well under a second with a JOB, not a delivery. ok:true means+           * the job STARTED. delivered_to is never filled on this path any more, so without+           * the poll below every send would report "delivery not confirmed" forever. */+          if (!j.job) return { state: 'opened', hint: j._hints && j._hints.note };+          var started = Date.now(), missing = false;+          return new Promise(function (resolve) {+            (function tick() {+              /* the deadline is checked in BOTH paths. Checking it only after a successful+               * poll leaves a dashboard whose server stops answering polling forever. */+              function again() {+                if (Date.now() - started > 120000)+                  return resolve({ state: 'opened', hint: 'no confirmation within 120s' });+                setTimeout(tick, 1000);+              }+              fetch(base + '/ai-threads/run-prompt/status?job=' + encodeURIComponent(j.job))+                .then(function (r) {+                  /* THE STATUS ROUTE IS A THIRD PROXY ROUTE. Dashboards proxy /updater/status+                   * and POST /ai-threads/run-prompt only. If this one was never added, every+                   * poll 404s and the user watches "Opening…" for the full 120s before a+                   * hedge, which is worse than the behaviour this replaced. Fail FAST and say+                   * exactly which route is missing instead of hanging. */+                  if (r.status === 404) { missing = true; return null; }+                  return r.json();+                })+                .then(function (st) {+                  if (missing) return resolve({ state: 'opened', phase: 'no_status_route',+                    hint: 'Your AI chat opened. This app does not proxy GET ' ++                          '/ai-threads/run-prompt/status yet, so progress cannot be shown.' });+                  var state = (st.job && st.job.state) || '';+                  if (typeof onProgress === 'function') { try { onProgress(state, st.job); } catch (e) {} }+                  /* working or later IS sent, for the user-facing toast. */+                  if (state === 'working' || state === 'done' || state === 'needs_you' ||+                      state === 'still_working') return resolve({ state: 'delivered', phase: state });+                  if (state === 'submit_failed' || state === 'submitted_unidentified' ||+                      state === 'submit_unconfirmed')+                    return resolve({ state: 'opened', phase: state,+                                     hint: (st.job && (st.job.error || st.job.note)) || state });+                  again();+                })+                .catch(again);+            })();+          });+        })+        .catch(function (e) {+          clearTimeout(timer);+          if (e.name === 'AbortError') return { state: 'opened', hint: 'no answer within 120s' };+          throw e;+        });+    });+  }++  /* ---- mount ------------------------------------------------------------ */+  function mount(cfg) {+    css();+    var host = typeof cfg.mount === 'string' ? document.querySelector(cfg.mount) : cfg.mount;+    if (!host) throw new Error('newbie panel: mount target not found: ' + cfg.mount);+    var noun = cfg.app || '';+    var slug = cfg.slug || noun.replace(/\s+/g, '-');+    var appName = cfg.appName || noun;+    var base = cfg.apiBase == null ? '' : cfg.apiBase;+    var key = 'anp:' + slug + ':open';++    var wrap = el('div', 'anp-wrap');+    var panel = el('section', 'anp');+    panel.setAttribute('aria-label', 'Say it: sample prompts for ' + appName);++    var head = el('div', 'anp-head');+    head.appendChild(el('h2', null, 'Say it'));+    head.appendChild(el('p', null,+      "You never need this page's buttons: just tell your AI what you want, using the words <b>" ++      esc(noun) + "</b>. Copy a phrase, or send it straight to a new AI thread."));+    var x = el('button', 'anp-x', ICON.x);+    x.type = 'button'; tipFor(x, 'Close this panel');+    head.appendChild(x);+    panel.appendChild(head);++    var left = el('div', 'anp-left');+    var demo = el('button', 'anp-demo', ICON.play + '<span>' + esc((cfg.demo && cfg.demo.label) || 'Play the demo') + '</span>');+    demo.type = 'button';+    demo.addEventListener('click', function () {+      if (cfg.demo && typeof cfg.demo.onPlay === 'function') cfg.demo.onPlay();+      else toast('This dashboard has no demo wired up yet', 'err');+    });+    left.appendChild(demo);+    if (cfg.demo && cfg.demo.meta) left.appendChild(el('span', 'anp-meta', esc(cfg.demo.meta)));+    panel.appendChild(left);++    var right = el('div', 'anp-right');+    var tbl = el('table', 'anp-tbl');+    var tb = el('tbody');+    (cfg.phrases || []).slice(0, 6).forEach(function (phrase) {+      var tr = el('tr');+      var td = el('td', 'anp-phrase', '<span>“' + mark(phrase, noun) + '”</span>');+      var ta = el('td', 'anp-acts');++      var cp = el('button', 'anp-ib', ICON.copy); cp.type = 'button';+      tipFor(cp, 'Copy this phrase');+      cp.addEventListener('click', function (e) {+        e.stopPropagation();+        copyText(phrase)+          .then(function () { toast('Copied. Paste it to your AI.', 'ok'); })+          .catch(function () { toast('Could not reach the clipboard. Select the phrase and copy it.', 'err'); });+      });++      var sd = el('button', 'anp-ib', ICON.send); sd.type = 'button';+      tipFor(sd, 'Send to a new AI thread');+      sd.addEventListener('click', function (e) {+        e.stopPropagation();+        if (sd.disabled) return;+        sd.disabled = true;+        var pending = toast('Opening a new AI thread…', 'info', 120000);+        send(base, phrase, appName, slug, function (state, job) {+          if (PHASE[state]) pending.setText(PHASE[state]);+          if (typeof cfg.onProgress === 'function') { try { cfg.onProgress(state, job); } catch (e) {} }+        }).then(function (r) {+          pending.remove();+          if (r.state === 'delivered') toast('Sent. Your AI thread has the prompt.', 'ok');+          else toast('Thread opened, delivery not confirmed. Check your AI threads before sending again.', 'info', 8000);+        }).catch(function (err) {+          pending.remove();+          toast(err.message || 'Could not send the prompt.', 'err');+        }).then(function () { sd.disabled = false; });+      });++      ta.appendChild(cp); ta.appendChild(sd);+      tr.appendChild(td); tr.appendChild(ta);+      tr.addEventListener('click', function () { cp.click(); });+      tb.appendChild(tr);+    });+    tbl.appendChild(tb); right.appendChild(tbl); panel.appendChild(right);+    wrap.appendChild(panel); host.appendChild(wrap);++    /* the header ? is a real TOGGLE, both ways, with aria-pressed and a label that swaps */+    var toggleBtn = typeof cfg.toggleButton === 'string'+      ? document.querySelector(cfg.toggleButton) : cfg.toggleButton;+    /* setOpen(open, remember, source)+     * source is 'user' when a person clicked the X or the header toggle, and 'api' when the+     * app called open()/close() itself. It is passed to onToggle, because an app with a+     * graduation policy calls open()/close() from inside its own onToggle handler: without a+     * source the callback cannot tell its own action from a person's, so graduating the panel+     * open pins it open forever and graduating it closed marks it user-dismissed. It looks+     * like "the panel sometimes ignores graduation". (lbr, 2026-08-25) */+    function setOpen(open, remember, source) {+      panel.hidden = !open;+      wrap.style.display = open ? '' : 'none';   /* no dead margin when collapsed */+      if (toggleBtn) {+        var label = open ? 'Hide the sample prompts' : 'Show the sample prompts';+        toggleBtn.setAttribute('aria-pressed', open ? 'true' : 'false');+        toggleBtn.setAttribute('aria-label', label);+        /* the TOOLTIP has to swap with the state too, or a person with the panel already in+         * front of them hovers the control and is told it will show them the panel */+        if (toggleBtn.__anpSetTip) toggleBtn.__anpSetTip(label);+      }+      if (remember !== false) { try { localStorage.setItem(key, open ? '1' : '0'); } catch (e) {} }+      if (typeof cfg.onToggle === 'function') cfg.onToggle(open, source || 'api');+    }+    x.addEventListener('click', function () { setOpen(false, true, 'user'); });+    if (toggleBtn) {+      if (!toggleBtn.innerHTML.trim()) toggleBtn.innerHTML = ICON.help;+      tipFor(toggleBtn, 'Show the sample prompts');+      toggleBtn.addEventListener('click', function () { setOpen(panel.hidden, true, 'user'); });+    }+    var saved = null;+    try { saved = localStorage.getItem(key); } catch (e) {}+    setOpen(saved === null ? true : saved === '1', false);++    /* authoring guard: a phrase that truncates at rest is an authoring bug, and it eats the+     * trigger noun because the noun is usually the last word. Warn loudly at build time. */+    var probe = el('span'); probe.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap';+    document.body.appendChild(probe);+    var over = [];+    (cfg.phrases || []).forEach(function (p) {+      probe.style.font = '400 14px/1.4 Satoshi,ui-sans-serif,system-ui,sans-serif';+      probe.textContent = '“' + p + '”';+      var need = Math.ceil(probe.getBoundingClientRect().width);+      if (need > PHRASE_BUDGET) over.push({ phrase: p, needs: need, budget: PHRASE_BUDGET });+    });+    probe.remove();+    if (over.length) {+      console.warn('[newbie-panel] ' + over.length + ' phrase(s) will truncate at the resting width. ' ++        'Shorten them, and lead with the trigger noun so a clip costs a detail not the routing words.', over);+    }++    return {+      panel: panel,+      open:   function (o) { setOpen(true,  !(o && o.remember === false), 'api'); },+      close:  function (o) { setOpen(false, !(o && o.remember === false), 'api'); },+      toggle: function () { setOpen(panel.hidden, true, 'api'); },+      isOpen: function () { return !panel.hidden; },+      overBudget: over, toast: toast, selfCheck: function () { return selfCheck(panel); }+    };+  }++  /* ---- selfCheck: prove conformance from one call ---------------------- */+  var CANON = {+    'card background':      ['backgroundColor', 'rgb(22, 27, 34)'],+    'card border':          ['borderTopWidth',  'snap:1'],+    'card border colour':   ['borderTopColor',  'rgb(48, 54, 61)'],+    'card accent edge':     ['borderLeftWidth', 'snap:3'],+    'card radius':          ['borderRadius',    '12px'],+    'card padding':         ['padding',         '18px 20px 20px'],+    'column gap':           ['columnGap',       '24px']+  };+  /* selfCheck(panel) - pass the panel explicitly when the page mounts more than one, or+   * mounts into a shadow root. The no-arg form takes the FIRST .anp in the light DOM, which+   * is right for the single-panel case and silently wrong for the others. Returns+   * {ok:false, error} rather than throwing when there is no panel. (chip-fetcher, 2026-08-25) */+  function selfCheck(panel) {+    panel = panel || document.querySelector('.anp');+    if (!panel) return { ok: false, error: 'no .anp panel on the page' };+    /* REFUSE TO REPORT FROM A COLLAPSED LAYOUT. An unforegrounded tab returns zero-height+     * rects while computed styles still resolve normally, so selfCheck produced EIGHT GREEN+     * ROWS beside two red ones: "panel gutter: 0px left, 744px right" and "columns: want 1,+     * got 3". That mixed shape is far more dangerous than an all-zero one, because it reads+     * as a specific layout regression rather than an invalid run, and project-manager nearly+     * filed it as a bug. A refusal cannot be misread. (project-manager, 2026-08-25.) */+    var w0 = Math.round(panel.getBoundingClientRect().width);+    /* rows: [] is deliberate. The refusal previously omitted the key entirely, and EVERY+     * caller does r.rows.map(...) because every healthy result has rows, so a correct refusal+     * arrived at the caller as "Cannot read properties of undefined (reading 'map')" and read+     * like harness noise. kicad hit exactly that and logged it as its own crash. A guard whose+     * failure mode is a TypeError in someone else's stack is a guard that will be misread. */+    if (w0 === 0) return { ok: false, invalid: true, rows: [],+      error: 'panel measures 0px wide: this tab is not foregrounded (or the panel is hidden), ' ++             'so every layout row would ' ++             'be computed from a collapsed pane while the style rows pass normally. Activate ' ++             'the tab by name (or open the panel) and re-run. No rows reported, because a ' ++             'partial result from an unrendered pane looks like a real bug.' };+    var cs = getComputedStyle(panel), rows = [], ok = true;+    /* Chrome reports the USED border-width, snapped to whole device pixels, so a declared+     * 1px reads as 0.666667px at dpr 1.5. Compare against the snapped value, not the+     * declared one, or every panel fails a check it actually passes. */+    function snap(declared) {+      var dpr = window.devicePixelRatio || 1;+      return (Math.max(1, Math.floor(declared * dpr)) / dpr) + 'px';+    }+    Object.keys(CANON).forEach(function (k) {+      var want = CANON[k][1];+      var got = cs[CANON[k][0]], declared = CANON[k][1];+      /* Border widths need BOTH candidates accepted. Chrome reports the USED width, snapped+       * to whole device pixels, on some surfaces (ah's webview and pup on a dpr-1.5 laptop+       * both return 0.666667px for a declared 1px) and reports the DECLARED width on others+       * (an emulated dpr returns 1px at every ratio). Accepting only one of the two fails a+       * correct panel in the other environment, and at dpr 1.5 specifically, which is the+       * machine this fleet is reviewed on. Found by nb, 2026-08-25. */+      var pass;+      if (declared.indexOf('snap:') === 0) {+        var d = parseFloat(declared.slice(5)), g = parseFloat(got);+        pass = Math.abs(g - d) < 0.01 || Math.abs(g - parseFloat(snap(d))) < 0.01;+        want = d + 'px or ' + snap(d);+      } else {+        pass = (parseFloat(want) && parseFloat(got))+          ? Math.abs(parseFloat(got) - parseFloat(want)) < 0.01+          : got === want;+      }+      if (!pass) ok = false;+      rows.push({ what: k, want: want, got: got, pass: pass });+    });+    var copies = panel.querySelectorAll('.anp-acts .anp-ib:first-child');+    var xs = {}; [].forEach.call(copies, function (b) { xs[Math.round(b.getBoundingClientRect().x)] = 1; });+    var aligned = Object.keys(xs).length <= 1;+    if (!aligned) ok = false;+    rows.push({ what: 'copy icons in one column', want: '1 x-position', got: Object.keys(xs).length + ' x-positions', pass: aligned });+    var cols = cs.gridTemplateColumns.split(' ').filter(Boolean).length;+    rows.push({ what: 'columns at this width', want: panel.getBoundingClientRect().width >= TWO_COL_MIN ? '2' : '1',+                got: String(cols), pass: true });+    /* THE GUTTER ROW. Four apps measured at the SAME 744px pane reported panel widths of+     * 734, 735, 702 and 694, because each page put a different amount of its own padding+     * around the mount point. 694 is under the 700 threshold, so that app rendered ONE+     * column while the other three rendered TWO, at an identical pane width. The panel was+     * conformant in all four; the PAGE around it was not. The fleet gutter is 16px a side,+     * so the panel's border box is the pane minus 32. */+    /* Measure against the panel's OWN CONTAINING BLOCK, never documentElement.+     * The first version compared to documentElement.clientWidth, which silently assumes the+     * PAGE scrolls. An app-shell dashboard (position:fixed shell, 44px header, scroller on an+     * inner .app-body) has no document scrollbar, so clientWidth is the full pane and the+     * inner scrollbar is charged to content instead: a correctly built app-shell page fails+     * by exactly one scrollbar width, and only pages that happen to scroll at document level+     * pass. Since app-shell is the prescribed structure, the check was failing the apps that+     * followed the skill. The containing block is scrollbar-agnostic and still catches the+     * real defect, which is a page adding its own padding around the mount point.+     * (parts-search, 2026-08-25, from reading the implementation rather than the prose.) */+    /* Measure BOTH INSETS AGAINST THE PANE, and never against a named element.+     * Two earlier versions asked the wrong element how wide the world is. documentElement+     * assumes the page scrolls, and fails every app-shell dashboard by one scrollbar width.+     * The mount div assumes the 16px lives on the mount div, and reds a visually perfect page+     * that puts the gutter on a shared outer container instead. Both encode a STRUCTURE the+     * fleet was never promised. What the contract actually says is where the panel's EDGES+     * land, so measure exactly that: find the nearest scrolling ancestor (or the document),+     * and check the panel sits 16px inside its client box on both sides. Structure-agnostic:+     * it does not care which element carries the padding, or how many wrappers sit between.+     * (pup found the mount-div version would red an outer-container page, before six apps+     * adopted the pattern I had recommended; parts-search found the documentElement one.) */+    var sc = panel.parentElement;+    while (sc && sc !== document.body) {+      /* A DECLARED scroller is the pane whether or not it is scrolling RIGHT NOW. Requiring+       * it to overflow was a bug with a nastier shape than the two before it: with+       * scrollbar-gutter:stable (which this pack mandates, so results arriving cause no+       * reflow) the track is reserved even when nothing overflows, so the loop walked PAST+       * the real pane, fell back to documentElement, and reported the right inset over by+       * one scrollbar width. The SAME PAGE passed when populated and failed when empty, and+       * empty is a dashboard's first-load state. Predicted from a code read by parts-search+       * and confirmed here: 16 left / 31 right, exact. */+      var so = getComputedStyle(sc).overflowY;+      if (so === 'auto' || so === 'scroll') break;+      sc = sc.parentElement;+    }+    var paneLeft, paneW;+    if (sc && sc !== document.body) { paneLeft = sc.getBoundingClientRect().left; paneW = sc.clientWidth; }+    else { paneLeft = 0; paneW = document.documentElement.clientWidth; }+    var pr = panel.getBoundingClientRect();+    var insetL = Math.round(pr.left - paneLeft);+    var insetR = Math.round(paneLeft + paneW - pr.right);+    var gutterOk = Math.abs(insetL - 16) <= 2 && Math.abs(insetR - 16) <= 2;+    if (!gutterOk) ok = false;+    rows.push({ what: 'panel gutter', want: '16px inset both sides',+                got: insetL + 'px left, ' + insetR + 'px right', pass: gutterOk });+    /* THE LEFT-EDGE ROW closes the hole the gutter row cannot see. The gutter row measures+     * against whatever element the app mounts INTO, so an app that mounts into a wrapper+     * which itself carries padding passes while sitting inset from its neighbours, and the+     * fleet mismatch comes back in a form the check is blind to. This asserts the panel's+     * left edge against the PANE, which covers the whole ancestor chain in one number.+     * (parts-search, 2026-08-25, reasoning about what the new yardstick still could not see.) */+    /* PHRASE HEADROOM AS A ROW, not a console warning nobody reads: "does not truncate TODAY"+     * and "has room" are different claims.+     * MEASURE need AGAINST AN OFFSCREEN nowrap PROBE. Do NOT use clientWidth - scrollWidth:+     * for a block span with overflow:hidden, scrollWidth is max(clientWidth, content), so that+     * expression SATURATES AT 0 for anything that fits, whether it fits by 1px or by 180px. It+     * still goes negative correctly when text overflows, so it looks like a working metric.+     * Two apps independently reported "0px headroom, one character from truncating" off it;+     * remeasured with this probe, neither had less than 141px of slack. A metric that can only+     * return one value on success is not evidence of success. */+    var spans = [].slice.call(panel.querySelectorAll('.anp-phrase span'));+    if (spans.length) {+      var probe = document.createElement('span');+      probe.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;font:' ++                            getComputedStyle(spans[0]).font;+      document.body.appendChild(probe);+      var worst = 0, worstTxt = '', tight = 0;+      spans.forEach(function (sp) {+        probe.textContent = sp.textContent;+        var need = Math.ceil(probe.getBoundingClientRect().width);+        var have = Math.round(sp.getBoundingClientRect().width);+        var slack = have - need;+        if (worstTxt === '' || slack < worst) { worst = slack; worstTxt = sp.textContent.trim().slice(0, 40); }+        if (slack < 8) tight++;+      });+      probe.remove();+      var headOk = worst >= 8;+      if (!headOk) ok = false;+      rows.push({ what: 'phrase headroom', want: 'at least 8px of slack on every phrase',+                  got: worst + 'px on ' + JSON.stringify(worstTxt) + (tight > 1 ? ' (' + tight + ' tight)' : ''),+                  pass: headOk });+    }+    var fam = getComputedStyle(panel.querySelector('.anp-phrase span') || panel).fontFamily;+    var famOk = /Satoshi/.test(fam);+    if (!famOk) ok = false;+    rows.push({ what: 'phrase typeface', want: 'Satoshi', got: fam.split(',')[0], pass: famOk });+    return { ok: ok, rows: rows };+  }++  /* checkSend(apiBase) - prove the send path EXISTS, without sending anything.+   * gate() fails OPEN on purpose, so a broken probe cannot block a user from sending. The+   * consequence, which kicad found in itself: A MISSING GATE READS AS AN ALLOWED GATE. Its+   * /updater/status route had been lost in a rebuild, the gate got HTML, caught, and returned+   * open, so the version gate had been silently no-opping. Nothing anywhere said so.+   * This makes that visible. It never posts a prompt: the run-prompt route is probed with+   * OPTIONS, so it can report "wired" without creating a thread in anyone's list. */+  function checkSend(base) {+    base = base == null ? '' : base;+    var rows = [], ok = true;+    function add(what, want, got, pass) { rows.push({what: what, want: want, got: got, pass: pass}); if (!pass) ok = false; }+    return fetch(base + '/updater/status', {cache: 'no-store'})+      .then(function (r) { return r.text().then(function (t) { return {s: r.status, t: t}; }); })+      .then(function (res) {+        var v = null, parsed = false;+        try { var j = JSON.parse(res.t); parsed = true; v = j && j.updater && j.updater.current_version; } catch (e) {}+        add('gate route wired', '200 with JSON', res.s + (parsed ? ' JSON' : ' NOT JSON'), res.s === 200 && parsed);+        add('gate reports a version', 'updater.current_version', String(v), !!v);+        if (v) add('version meets the minimum', MIN_API.join('.'), String(v), verGte(verNum(v), MIN_API));+      })+      .catch(function (e) { add('gate route wired', '200 with JSON', 'FETCH FAILED: ' + e.message, false); })+      .then(function () {+        /* PROBE A NONSENSE PATH TOO, AND REQUIRE THE TWO TO DIFFER.+         * "not 404" was too weak an assertion. kicad's do_OPTIONS returned 204 for EVERY path+         * unconditionally, so this row was green for any string at all, including routes that+         * exist in no method. An app could fix its GET catch-all, still have a blanket+         * preflight, and keep a green row that tests nothing. Comparing against a path that+         * cannot exist makes the row self-validating: if they answer identically, the server+         * is answering the SHAPE of the question rather than the question. */+        var nonsense = '/__anp_probe_' + String(Math.floor(1e9 * 0.6180339887)) + '__';+        return Promise.all([+          fetch(base + '/ai-threads/run-prompt', {method: 'OPTIONS'}).then(function (r) { return r.status; },+            function () { return 'ERR'; }),+          fetch(base + nonsense, {method: 'OPTIONS'}).then(function (r) { return r.status; },+            function () { return 'ERR'; })+        ]).then(function (both) {+          var real = both[0], fake = both[1];+          add('send route wired', 'answers, and differently from a path that cannot exist',+              'route ' + real + ', nonsense ' + fake,+              real !== 'ERR' && real !== 404 && real !== fake);+        });+      })+      .then(function () {+        /* THE STATUS ROUTE, added for Hydrogen 1.0.187+. It is a THIRD proxy route and no+         * dashboard had it before 2.22.0, so it is the row most likely to be red. A GET with+         * a job id that cannot exist must still be ANSWERED (any non-404), because a 404 here+         * means the route was never proxied and progress can never be reported. */+        return fetch(base + '/ai-threads/run-prompt/status?job=__anp_probe__')+          .then(function (r) { return r.status; }, function () { return 'ERR'; })+          .then(function (st) {+            add('progress route wired', 'answers (not 404)', String(st), st !== 'ERR' && st !== 404);+          });+      })+      .then(function () { return {ok: ok, rows: rows}; });+  }++  root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText,+                           checkSend: checkSend, version: '2.22.0',+                           TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET };+})(typeof window !== 'undefined' ? window : this);