Fix 02: mind.html, standard.html, server.py

Round 2 fixer report for the mind-standard group. 2026-10-13. Files owned: prototype/mind.html, prototype/standard.html, prototype/server.py. Also touched, additively: prototype/data/ets.sample.json (two visitor-copy keys under venue). Not touched: shared.css, index.html, research/, brief/.

Verification: headless Chrome for Testing via Playwright, 1440x900 at DPR 2 and 390x844 at DPR 3 with mobile emulation. An offline pass (demo mode through ?demo=1, plus mocked /api/mind routes for the error, waiting, stop and reset states) and a live pass through server.py on 127.0.0.1:8777 (5 Claude exchanges: mind x3, standard dock x2, within the 8-exchange allowance). Every shot listed below was read as pixels before judging. Console: zero page errors and zero console errors in every real context; the only console lines are the browser's own "Failed to load resource" entries in the two mocked contexts where the route was deliberately aborted or answered 400. uv run --script server.py --selftest is green (scanner fuzz 400/400, sanitizer exact counts, history echo, budget, history trust, digest leak check). The server was stopped by its exact PID (44101); lsof shows 8777 free and the pid file was removed by the server's own finally.


1. Decisions applied

Decision State in these files
2. Dates Hero mono line // may 3–6, 2027 · austin, tx; mind opener names Austin, May 3–6, 2027.
3. One accent No #e8a06a, rgba(232,160,106,…), --ets-ember or --ets-signal left in either page. Hover and pressed fills use var(--accent-ink) (the opening fixer added it to shared.css, #8f0c43); the mind's realm sun halo is rgba(255,243,232,.35); the standard hero gradient is #fafaf9 to #ffffff with no peach radial.
4. Persona Unnamed "the mind" in the top bar and dock head; sentence-case captions; no exclamation marks; error copy owned by the client, mapped by code.
5. Head The CSS egg is gone. Both pages carry an inline SVG mannequin cranium from the nose bridge up (smooth matte dome, gentle brow ridge, two shallow soft hollows, a soft bridge, dissolving into white through a gradient mask; no dark streaks or drips). The 24 px glyph (symbol #gHead) carries the same silhouette in the mind's top bar, the dock trigger and the dock head. The slot keeps the CSS a baked shader still would need (absolute right 6%, bottom 0, width:min(38vw,520px), bottom mask, 7 s breathe) so a still can replace the SVG without layout change; see Skipped.
6. Presence #head fades to opacity 0 over 800 ms the moment data-state leaves empty and never returns; the 24 px glyph fades into the top bar beside the brand and pulses while thinking; the top bar is an opaque var(--bg) band with an 18 px fade below it. Measured #head opacity after answers: 0 at both widths.
7. Escape hatch and sample marker Every page-backed component ends with [ open the map: lineup / agenda / venue / tickets / partners ] (text_block: [ open the map ]); session lists deep-link to standard.html#panel-dN and standard.html selects that day on load and on hashchange. Sample markers: speaker grids, session lists (· sample when the data is placeholder), partner walls, every standard section eyebrow, the hero facts strip, the merge bar.

Decision 1 belongs to index.html; mind.html supports its hand-off: a draft in sessionStorage['ets27.draft'] (or a ?q= deep link, stripped from the URL with replaceState) lands in the composer, focused, caret at the end, and is never auto-sent.


2. Before and after, by finding

Blockers

# Before After Shots
mind phone overflow (three reports) Document 672–741 px wide at 390; chips row never scrolled; answers clipped. .stage, .turn, .ui use grid-template-columns:minmax(0,1fr); .thread, .turn, .ui, .comp, .composer-wrap, .opener have min-width:0; .chips has min-width:0;max-width:100%; .ask and .say use overflow-wrap:anywhere. Measured: scrollWidth = innerWidth = 390 in the empty state, after one answer and after two; 1440 stays 1440 after a 300-character unbroken question. r2-mind-empty-390x844.png, r2-mind-after1-390x844.png (live), r2-mind-after2-390x844.png (offline)

Majors, mind.html

Finding Before After Shots
Head shows through as a grey disc Fixed egg at opacity .92 behind the thread. Fades to 0 once live; glyph in an opaque top bar; thinking indicator is a breathing accent dot after the composer prompt plus the pulsing glyph. r2-mind-after1-1440x900.png, r2-mind-topbar-scrolled-1440x900.png, r2-mind-midstream-1440x900.png
No per-component link to the map Zero links in the thread. .esc anchor after the source line in every data-bearing component (see decision 7). r2-mind-after1-*.png
New answer lands behind the sticky composer First component at y 787 under the composer. The turn scrolls into view (scroll-margin-top:96px) on the first caption text, on each component and after done, unless the visitor scrolled during the stream; .thread has 8 rem bottom padding and html 8 rem scroll-padding-bottom. Measured first component top 196–236 px against a composer at 790 px. r2-mind-after2-1440x900.png
Live regions Whole thread aria-live. No aria-live on the thread; one visually hidden role=status written three times per turn ("Thinking", then the final caption plus "Showing 12 speakers, sample data, 4 follow-up suggestions.", or the error line); aria-busy on the streaming turn only. metrics in the run log
No timeout, no stop UI frozen on a hung request. AbortController per turn; // one moment after 2 s; the unavailable state after 20 s; [ stop ] beside [ send ] while busy; Escape in the composer aborts; pagehide aborts. r2-mind-waiting-1440x900.png
Chips as list items role=list / role=listitem on buttons. Plain buttons in a role=group labelled "Suggested questions"; // and > are CSS alt-text / "" so they stay visual only. r2-mind-empty-*.png
Composer has no focus indicator Outline removed, nothing else. .composer:focus-within turns the hairline accent and adds a 1 px accent shadow (measured rgb(193,17,90)); same in the dock. r2-mind-composer-focus-*.png
Page loses its heading Opener h1 display:none when live. The h1 stays as a visually hidden heading for the session; each question is an h2 (.ask); kit heads are h3; speaker names, session titles and the venue name are h4. metrics
Failed request switches to demo forever Any non-OK response set demo mode permanently. Demo mode only with ?demo=1 or a failed boot probe. A runtime failure renders the 4.7 state: // the mind is quiet right now — the map still works. plus a door-style Open the map card, the composer disabled with that placeholder, and /api/health re-probed with backoff (3 s doubling to 30 s); the composer re-enables without a reload (verified: re-enabled after the route was restored). 400 resets the conversation (new session id, // starting fresh …); session_full likewise; 429 is busy. The unanswered user turn is popped so the server's exact history stays matched; a retry chip ask that again is offered. r2-mind-unavailable-*.png, r2-mind-reset-*.png
Provisional components never reconcile Streamed components stayed as rendered. On done the final answer.ui is diffed by position against the provisional nodes (same tree kept, others replaced without the entry animation); on error they are removed, or dimmed (retired) for truncated and stopped turns. metrics

Majors, standard.html

Finding Before After Shots
Nav wordmark boxed by .logo Partner tiles and the wordmark shared .logo. Partner tiles are .partner; computed wordmark: Unbounded 800, no border. r2-standard-top-1440x900.png
The mind absent from the hero Peach radial and bluish wash. The SVG head at the right third with its cut plane on the hero's lower edge, breathing; on phones behind the lockup at 60 % with a white gradient under the copy; hero gradient #fafaf9 to #fff. r2-standard-top-1440x900.png, r2-standard-top-390x844.png
Mobile menu: Register overprints Partners Absolutely positioned .reg. Register is the last list row; rows are 44 px; the menu closes on link activation and on Escape (focus returns to [ menu ]); the button has aria-controls. Measured rows About 68–112 … Partners 244–288, Register 304–348. r2-standard-menu-390x844.png
Hero numerals at 16 px Invalid clamp(2rem,1.4rem+2vw,3.4rem). clamp(3rem, 2rem + 4vw, 6rem): computed 89.6 px at 1440, 48 px at 390. r2-standard-top-*.png
Dock is a dialog in name only Off-screen but focusable; aria-modal. Closed: inert and visibility:hidden after the slide (Tab from the trigger leaves the page; nothing inside is reachable). Open: a non-modal complementary region (aside with aria-label, no role=dialog); on desktop main gets 440 px right padding so nothing sits under the sheet; focus returns to whichever control opened it (trigger, hero link or /). r2-standard-dock-open-*.png, metrics
Phone merge hidden behind the sheet 76 vh sheet over the merged grid. After a merge the sheet collapses to a 57 px peek bar (caption plus [ expand ]); [ collapse ] / [ expand ] are the keyboard equivalents; the merged section scrolls under the nav (top 73 px). r2-standard-dock-merge-390x844.png, r2-standard-dock-peek-390x844.png
Desktop dock covers the merge and its clear control Clear button at x 1254 under the dock. [ clear ] leads the merge bar (x 48–117, dock starts at 1000). r2-standard-dock-merge-1440x900.png
Landmarks and lists No main; divs everywhere. main#main wraps the hero (a section labelled by its h1) and the sections; skip link "Skip to content" to #main; lineup is ul/li with h3 names; sessions are ol/li with h3 titles; venue card titles h3; one h1. metrics
Sample markers missing One hero note. Every section eyebrow carries · sample: ets26 … in --muted-strong; the facts strip labels say sessions · sample etc.; the merge bar says · sample. r2-standard-agenda-1440x900.png
Second page-backed component wipes the first; merged grid duplicates the lineup clearMerges per component; an 8-card grid inserted above 77 cards. Merges clear once at the start of ask. Design-direction 5.6 order: filter (speaker grids hide non-matching lineup cards; session lists select the day and hide non-matching rows; a tiered partner wall hides other tiers) with the bar // showing 12 speakers · sample and [ clear ]; reveal (venue, tickets, full partner wall) scrolls and draws a 1 px accent outline for 2 s; materialize in the sheet only when nothing on the page matches, with [ open page: … ]. Escape clears a merge first (focus to the dock input, or the section heading when the sheet is collapsed), then closes the dock. Live: 12 of 77 cards shown, 65 hidden, zero duplicate grids. r2-standard-dock-merge-1440x900.png

Majors, server.py

Finding After
Host, Origin, Content-Type, budget Host must be 127.0.0.1:8777 or localhost:8777 (421) on every method; a present Origin outside those two is 403; non-JSON bodies are 415; missing or non-numeric Content-Length is 411 and anything outside 1..262144 bytes is 413. Process-wide Budget: 30 turns per 10 minutes and 80,000 output tokens per day summed from usage (429 with Retry-After and a rate / budget code). A non-loopback --host is refused unless --allow-remote --token T is given, after which POST needs Authorization: Bearer T. Probed live: 200 / 404 (server.py) / 421 / 403 / 415 / 411 / 400 / 400 / HEAD 200 / NUL path 404.
Client-supplied assistant turns build_messages never reads assistant content. When the stored user texts do not match, the turn starts from the new message alone and the stream opens with {"type":"notice","code":"reset"}; both clients then drop their stale local history and show // picking up fresh — earlier answers are not in context. The cap now answers with session_full (SSE error, or a 400 with that code for an oversized body). The selftest asserts a forged assistant turn never reaches the request.
Provisional components At most LIMITS["components"] (4) ui events are emitted, numbered by emitted count.
Fallback storage to_history_content keeps every block after the last fallback block and every earlier block that is not thinking, redacted_thinking or tool_use (selftest).
Body cap, socket timeout, crash paths Handler.timeout = 20; strict length parsing; the body must be a JSON object with a string mode; static resolution wraps ValueError/OSError into 404; a bare --pid-file exits with a usage line.
Heartbeat A : hb comment line goes out at most once a second on any upstream event (pings included), so a closed tab surfaces within a second and the with block closes the upstream stream.
Cache breakpoint Top-level cache_control={"type":"ephemeral"} on beta.messages.stream (SDK 1.13.0 accepts it). Live turn 2 read 19,300 cached tokens and wrote 288.
Measured prefix The first cold turn logs prefix_measured=19327; the startup line marks the chars/4 figure as approximate.
say cap and error copy say capped at 320 characters in sanitize_answer and in the streamed deltas; error events carry a code only; reasons are logged server-side.
PID file Bind first, then O_CREAT|O_EXCL; a stale file naming a live process is refused; the file is unlinked in a finally that also covers start-up failures.
request_id, logging, sanitizer, static stream.request_id on every turn line with refusal and reset; missing timings print -; BadRequestError logs the status and error.type only; drops are counted once (selftest asserts exact counts, and the component-cap overflow); only the MIME allowlist is served (no .py), .mjs .woff .wasm .avif .gif .mp4 .webm added, HEAD handled, Server: ets27-proto/0.2 without the Python version.
Digest The venue block uses the visitor copy; the research note and the unconfirmed street pin never reach the model (selftest asserts).

Minors applied


3. Live run record

Turn Surface Outcome Cache read / write First caption Total
1 mind 1440, "who's speaking" ok, 12 speakers + choices 0 / 19,327 (cold) 3.1 s 4.8 s
2 mind 1440, "what is on day 2" ok, 10 sessions + choices 19,300 / 288 1.5 s 4.0 s
3 mind 390, "who's speaking" ok, 12 speakers + choices 19,327 / 0 1.2 s 3.0 s
4 standard 1440, "who is speaking" ok, filtered 12 of 77 19,293 / 61 1.2 s 3.0 s
5 standard 390, "who is speaking" ok, filtered 12 of 77, peek 19,354 / 0 1.2 s 3.0 s

No refusals, fallbacks or dropped ids. Request ids are in the server log line for each turn.


4. Captures (prototype/shots/, prefix r2-)

Live: r2-mind-empty-1440x900.png, r2-mind-midstream-1440x900.png, r2-mind-after1-1440x900.png, r2-mind-after2-1440x900.png, r2-mind-topbar-scrolled-1440x900.png, r2-mind-empty-390x844.png, r2-mind-midstream-390x844.png, r2-mind-after1-390x844.png, r2-mind-topbar-scrolled-390x844.png, r2-standard-top-1440x900.png, r2-standard-agenda-1440x900.png, r2-standard-dock-open-1440x900.png, r2-standard-dock-midstream-1440x900.png, r2-standard-dock-merge-1440x900.png, r2-standard-tickets-1440x900.png, r2-standard-footer-1440x900.png, r2-standard-hover-register-1440.png, r2-standard-top-390x844.png, r2-standard-agenda-390x844.png, r2-standard-menu-390x844.png, r2-standard-dock-open-390x844.png, r2-standard-dock-merge-390x844.png, r2-standard-dock-peek-390x844.png.

Offline (demo or mocked): r2-mind-after2-390x844.png (demo), r2-mind-composer-focus-*.png, r2-mind-unavailable-*.png, r2-mind-waiting-*.png, r2-mind-reset-*.png, r2-mind-nojs-*.png, r2-standard-agenda-d2-1440x900.png (deep link #panel-d2), r2-standard-nojs-1440x900.png.

Round-1 shots were not overwritten.


5. Contracts the other group should know


6. Skipped, and why