Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

uitest/src/places.rs

//! **Where things live, and what a person may see** (owner, 2026-10-02) — the
//! rows for the storage information architecture, the Overview moving under
//! Administration, the events a person is shown, plain page titles, and the
//! buy button that goes straight to the vendor.
//!
//! Every row drives the console as a person does and asserts what the page
//! DREW (or where the browser LANDED), against the server's own answer where
//! there is one. `admin` is bob's session, `person` kim's — a viewer in an
//! isolated browser context with an authenticator of their own.

use std::time::{Duration, Instant};

use crate::cdp::Cdp;
use crate::http;
use crate::json::esc;
use crate::matrix::{go, want, Row};
use crate::rig::Rig;

const MAIN_TEXT: &str = "(document.querySelector('main') || document.body).innerText";

/// The SHA-256 fingerprint of the host key the rig's SSH listener presents,
/// read with stock OpenSSH (`ssh-keyscan | ssh-keygen -lf -`) — what a user's
/// ssh client shows on first connect.
fn presented_host_key(r: &Rig) -> Result<String, String> {
    let scan = std::process::Command::new("ssh-keyscan")
        .args(["-t", "ed25519", "-p", &r.ssh.to_string(), "127.0.0.1"])
        .output()
        .map_err(|e| format!("ssh-keyscan: {e}"))?;
    let lines = String::from_utf8_lossy(&scan.stdout).into_owned();
    let mut keygen = std::process::Command::new("ssh-keygen")
        .args(["-lf", "-"])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| format!("ssh-keygen: {e}"))?;
    {
        use std::io::Write as _;
        keygen.stdin.take().ok_or("no stdin")?.write_all(lines.as_bytes()).map_err(|e| e.to_string())?;
    }
    let out = keygen.wait_with_output().map_err(|e| e.to_string())?;
    let said = String::from_utf8_lossy(&out.stdout).into_owned();
    said.split_whitespace()
        .find(|w| w.starts_with("SHA256:"))
        .map(str::to_owned)
        .ok_or_else(|| format!("no fingerprint from ssh-keyscan/ssh-keygen: {said:?} (scan {lines:?})"))
}

/// The top bar's entries, as `(menu, label, href)`: menu is `main`, `You` or
/// `Administration`.
/// The viewport as a PHONE (`mobile: true`): overlay scrollbars, as a phone draws them. A
/// desktop viewport of 390 px with a classic 15 px scrollbar is a 375 px screen, so the row
/// about how a phone's bar fits measures on this one. (Here, not in `cdp.rs`, which monetize
/// keeps a checked copy of.)
fn viewport_phone(c: &mut Cdp, sess: &str, w: u32, h: u32) -> Result<(), String> {
    c.call(
        Some(sess),
        "Emulation.setDeviceMetricsOverride",
        &format!(r#"{{"width":{w},"height":{h},"deviceScaleFactor":1,"mobile":true}}"#),
    )?;
    Ok(())
}

fn menus(c: &mut Cdp, sess: &str) -> Result<Vec<(String, String, String)>, String> {
    c.wait_for(sess, "!!document.querySelector('header.topbar a.top-link')", 30_000)?;
    let j = c.eval(
        sess,
        "[...document.querySelectorAll('header.topbar a.top-link, header.topbar details.menu a.menu-link')].map(a => {
            const d = a.closest('details');
            const menu = !d ? 'main' : (d.classList.contains('admin-menu') ? 'Administration' : 'You');
            return [menu, a.textContent.trim(), a.getAttribute('href')]; })",
    )?;
    Ok(j
        .arr()
        .iter()
        .map(|p| {
            let at = |i: usize| p.arr().get(i).and_then(|x| x.as_str()).unwrap_or("").to_owned();
            (at(0), at(1), at(2))
        })
        .collect())
}

/// Wait until the page has drawn a page header.
fn header(c: &mut Cdp, sess: &str) -> Result<(String, String), String> {
    c.wait_for(sess, "!!document.querySelector('header.page-header h1')", 30_000)?;
    let h1 = c.eval_str(sess, "document.querySelector('header.page-header h1').textContent.trim()")?;
    let line = c.eval_str(sess, "(document.querySelector('header.page-header p') || {textContent: ''}).textContent.trim()")?;
    Ok((h1, line))
}

/// The panels drawn on the page, by heading.
fn panels(c: &mut Cdp, sess: &str) -> Result<Vec<String>, String> {
    let j = c.eval(sess, "[...document.querySelectorAll('section.panel > header h2')].map(h => h.textContent.trim())")?;
    Ok(j.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect())
}

fn b64(bytes: &[u8]) -> String {
    const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut out = String::new();
    for chunk in bytes.chunks(3) {
        let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
        let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
        out.push(T[(n >> 18) as usize & 63] as char);
        out.push(T[(n >> 12) as usize & 63] as char);
        out.push(if chunk.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' });
        out.push(if chunk.len() > 2 { T[n as usize & 63] as char } else { '=' });
    }
    out
}

pub fn rows(r: &Rig, rows: &mut Vec<Row>, c: &mut Cdp, admin: &str, person: &str) {
    // ── a person lands on Repositories, headed by the server key ───────────
    go(rows, "a-person-lands-on-repositories-headed-by-the-server-key", || {
        let want_fp = presented_host_key(r)?;
        c.navigate(person, &r.origin())?;
        c.wait_for(person, "location.pathname === '/repos'", 30_000)
            .map_err(|e| format!("a person opening the console is not taken to Repositories: {e}"))?;
        let (h1, _) = header(c, person)?;
        want(h1 == "Repositories", || format!("the landing page is headed {h1:?}"))?;
        c.wait_for(person, "(document.querySelector('header.page-header p') || {textContent:''}).textContent.includes('SHA256:')", 30_000)?;
        let (_, line) = header(c, person)?;
        want(line == format!("Server key {want_fp}"), || {
            format!("the header's second line is {line:?}; the SSH listener presents {want_fp}")
        })?;
        Ok(format!("landed on /repos, line 2 {line:?} = what ssh-keyscan reads"))
    });

    // ── the top bar: what a person and an administrator are offered ───────
    go(rows, "the-top-bar-offers-a-person-no-administration", || {
        let mine = menus(c, person)?;
        let main: Vec<&str> = mine.iter().filter(|(m, ..)| m == "main").map(|(_, l, _)| l.as_str()).collect();
        want(main == ["Repositories"], || format!("the top bar's main entries are {main:?}"))?;
        want(!mine.iter().any(|(m, ..)| m == "Administration"), || format!("a person is offered Administration: {mine:?}"))?;
        let you: Vec<(&str, &str)> = mine.iter().filter(|(m, ..)| m == "You").map(|(_, l, h)| (l.as_str(), h.as_str())).collect();
        want(!you.iter().any(|(l, _)| *l == "Storage") && you.contains(&("Activity", "/account/activity")) && you.contains(&("Account", "/account")), || format!("You ▾ is {you:?}"))?;
        let logout = c.eval_str(person, "(document.querySelector('header.topbar details.menu .menu-logout') || {textContent:''}).textContent.trim()")?;
        want(logout == "Log out", || format!("You ▾ has no Log out: {logout:?}"))?;
        for gone in ["/storage", "/usage", "/events", "/overview", "/"] {
            want(!mine.iter().any(|(_, _, h)| h == gone), || format!("{gone} is still on a person's top bar: {mine:?}"))?;
        }
        let theirs = menus(c, admin)?;
        let admin_menu: Vec<&str> = theirs.iter().filter(|(m, ..)| m == "Administration").map(|(_, l, _)| l.as_str()).collect();
        // Owner, 2026-10-04: Administration ▾ Overview · Storage · Identities ·
        // Mirrors · Instruments — the Machine page is a card on the Overview;
        // company SSO (2026-10-05) adds the Identity provider after Identities,
        // and the owner's enterprise list (2026-10-05) Accounts (the totals)
        // after Overview and Audit log before Instruments.
        want(admin_menu == ["Overview", "Accounts", "Storage", "Identities", "Identity provider", "Mirrors", "Audit log", "Instruments"], || format!("Administration ▾ is {admin_menu:?}"))?;
        Ok(format!("person: main {main:?}, You {you:?}; admin: Administration {admin_menu:?}"))
    });

    // ── inside a repository: the tabs ──────────────────────────────────────
    go(rows, "a-repository-has-one-tab-row-readme-first", || {
        c.navigate(admin, &format!("{}/repo/alice/uitest", r.origin()))?;
        c.wait_for(admin, "document.querySelectorAll('nav.repo-tabs a').length >= 5", 30_000)?;
        let tabs = c.eval(admin, "[...document.querySelectorAll('nav.repo-tabs a')].map(a => a.textContent.trim())")?;
        let tabs: Vec<String> = tabs.arr().iter().filter_map(|x| x.as_str().map(str::to_owned)).collect();
        want(tabs == ["Readme", "Code", "History", "Compare", "Search", "Blame", "Pulls", "Issues", "Activity", "Settings"], || format!("the admin's tabs are {tabs:?}"))?;
        let on = c.eval_str(admin, "(document.querySelector('nav.repo-tabs a.on') || {textContent:''}).textContent.trim()")?;
        want(on == "Readme", || format!("the repository's home marks {on:?}"))?;
        c.eval(admin, "[...document.querySelectorAll('nav.repo-tabs a')].find(a => a.textContent.trim() === 'Activity').click()")?;
        // GUN-GUEST (2026-10-30): a repository is `/{ns}/{name}`, its tabs below it.
        c.wait_for(admin, "location.pathname === '/alice/uitest/activity'", 30_000)?;
        c.wait_for(admin, "(document.querySelector('nav.repo-tabs a.on') || {textContent:''}).textContent.trim() === 'Activity'", 30_000)?;
        c.wait_for(admin, &format!("{MAIN_TEXT}.includes('Stream')"), 30_000)?;
        // A person's tabs: no Settings.
        c.navigate(person, &format!("{}/repos", r.origin()))?;
        Ok(format!("tabs {tabs:?}; Activity opens the repository's own stream"))
    });

    // ── the old addresses lead a person to their own ───────────────────────
    go(rows, "the-old-storage-and-usage-addresses-lead-a-person-to-their-own", || {
        let mut trail = Vec::new();
        for (from, to) in [
            ("/storage", "/account"),
            ("/usage", "/account"),
            ("/usage/alice", "/account"),
            ("/admin/overview", "/repos"),
            ("/admin/storage", "/account"),
            ("/overview", "/repos"),
            ("/events", "/account/activity"),
            ("/access", "/account"),
            ("/machine", "/repos"),
        ] {
            c.navigate(person, &format!("{}{from}", r.origin()))?;
            c.wait_for(person, &format!("location.pathname === {}", esc(to)), 30_000)
                .map_err(|e| format!("{from} did not lead a person to {to}: {e}"))?;
            trail.push(format!("{from} → {to}"));
        }
        Ok(trail.join(", "))
    });

    // ── /account/storage is no page of its own (owner 2026-10-17) ─────────
    // It goes on to the ONE sign-up page in the account's signed-in mode.
    go(rows, "account-storage-goes-on-to-the-signup-page", || {
        want(r.bench.is_some(), || "NOT RUN: no --bench-monetize (a community console sells nothing: the sign-up page, the Plan panel and the shop are a metered console's)".into())?;
        c.navigate(person, &format!("{}/account/storage", r.origin()))?;
        c.wait_for(person, "location.pathname === '/signup' && ['Buy a plan', 'Grow your account'].includes(document.getElementById('heading')?.textContent)", 30_000)
            .map_err(|e| format!("/account/storage did not go on to the sign-up page's Buy a plan / Grow your account: {e}"))?;
        Ok(c.eval_str(person, "location.pathname + location.search")?)
    });

    // ── Administration → Storage ───────────────────────────────────────────
    go(rows, "the-admin-sees-the-machine-under-administration-storage", || {
        c.navigate(admin, &format!("{}/admin/storage", r.origin()))?;
        let deadline = Instant::now() + Duration::from_secs(30);
        let mut seen = Vec::new();
        while Instant::now() < deadline {
            seen = panels(c, admin)?;
            if seen.iter().any(|p| p == "Engine") && seen.iter().any(|p| p == "Disks") {
                break;
            }
            std::thread::sleep(Duration::from_millis(200));
        }
        want(seen.iter().any(|p| p == "Engine") && seen.iter().any(|p| p == "Disks"), || format!("panels {seen:?}"))?;
        let st = c.eval(admin, "fetch('/api/storage').then(r => r.json())")?;
        let (name, desc, url) = (st.text("engine.name"), st.text("engine.description"), st.text("engine.repo_url"));
        want(!name.is_empty(), || format!("/api/storage names no engine in parts: {}", http::cut(&st.dump(), 200)))?;
        let link = c.eval(
            admin,
            "(() => { const dt = [...document.querySelectorAll('dt')].find(e => e.textContent.trim() === 'Engine');
                      const a = dt && dt.nextElementSibling && dt.nextElementSibling.querySelector('a');
                      return a ? [a.textContent.trim(), a.getAttribute('href'), a.getAttribute('title')] : null; })()",
        )?;
        let parts: Vec<String> = link.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect();
        want(parts == [name.clone(), url.clone(), desc.clone()], || {
            format!("the Engine row is {parts:?}; gunnar says name {name:?}, source {url:?}, description {desc:?}")
        })?;
        let text = c.eval_str(admin, MAIN_TEXT)?;
        for na in ["N/A", "not reported", "Write amplification", "Priority transfer"] {
            want(!text.contains(na), || format!("a row gunnar did not report: {na:?} in {}", http::cut(&text, 300)))?;
        }
        let data_sets = seen.iter().filter(|p| *p == "Data set").count();
        want(data_sets <= 1, || format!("the Data set is drawn {data_sets} times"))?;
        Ok(format!("panels {seen:?}; Engine = <a href={url:?} title={desc:?}>{name}</a>"))
    });

    // ── Administration → Overview, and the console's own identity ──────────
    go(rows, "the-overview-is-the-admins-and-carries-no-console-identity", || {
        let fp = presented_host_key(r)?;
        c.navigate(admin, &format!("{}/admin/overview", r.origin()))?;
        let (h1, _) = header(c, admin)?;
        want(h1 == "Overview", || format!("headed {h1:?}"))?;
        c.wait_for(admin, "(document.querySelector('header.page-header p') || {textContent:''}).textContent.includes('SHA256:')", 30_000)?;
        let (_, line) = header(c, admin)?;
        want(line == format!("Server key {fp}"), || format!("line 2 {line:?}, ssh presents {fp}"))?;
        let seen = panels(c, admin)?;
        want(!seen.iter().any(|p| p.contains("own identity")), || format!("the Overview still shows the console's identity: {seen:?}"))?;
        c.navigate(admin, &format!("{}/admin", r.origin()))?;
        c.wait_for(admin, &format!("{MAIN_TEXT}.includes(\"The console's own identity\")"), 30_000)
            .map_err(|e| format!("the console's identity is not on Administration either: {e}"))?;
        Ok(format!("Overview: {line:?}, panels {seen:?}; the identity is under Instruments"))
    });

    // ── the Machine card, and no Machine page (owner, 2026-10-04) ──────────
    go(rows, "the-machine-page-is-gone-and-its-address-leads-to-the-overview", || {
        c.navigate(admin, &format!("{}/machine", r.origin()))?;
        c.wait_for(admin, "location.pathname === '/admin/overview'", 30_000)
            .map_err(|e| format!("/machine did not lead the administrator to the Overview: {e}"))?;
        Ok("/machine → /admin/overview".into())
    });

    go(rows, "the-overview-draws-the-machine-card-as-gunnar-said", || {
        c.navigate(admin, &format!("{}/admin/overview", r.origin()))?;
        c.wait_for(admin, "!!document.querySelector('.machine-card .estate-picture svg') && document.querySelectorAll('.machine-card ul.glance li').length > 0", 30_000)
            .map_err(|e| format!("no Machine card with an inline picture and its lines: {e}"))?;
        let seen = panels(c, admin)?;
        want(seen.first().map(String::as_str) == Some("Machine"), || format!("the Machine card is not the Overview's first panel: {seen:?}"))?;
        // The picture: the shared renderer's live board of Admin.Topology,
        // INLINE (swapped in place, never an <img> that blanks), one named
        // group per box gunnar reported.
        let topo = c.eval(admin, "fetch('/api/machine/topology').then(r => r.json())")?;
        let boxes = topo.path("boxes").map(|b| b.arr().len()).unwrap_or(0);
        let drawn = c.eval_str(admin, "String(document.querySelectorAll('.machine-card .estate-picture svg g[data-box]').length)")?;
        want(drawn == boxes.to_string(), || format!("the card draws {drawn} box(es); gunnar has {boxes}"))?;
        let imgs = c.eval_str(admin, "String(document.querySelectorAll('.machine-card img').length)")?;
        want(imgs == "0", || "the card still draws the picture as an <img>".into())?;
        let alt = c.eval_str(admin, "document.querySelector('.machine-card .estate-picture').getAttribute('aria-label')")?;
        want(alt.contains(&format!("{boxes} box")), || format!("the picture's text says {alt:?}; gunnar has {boxes} box(es)"))?;
        // The lines: gunnar's, in gunnar's order, each with gunnar's verdict.
        let api = c.eval(admin, "fetch('/api/machine').then(r => r.json())")?;
        let want_states: Vec<String> = api.path("glance").map(|g| g.arr().iter().map(|x| format!("{}={}", x.text("key"), x.text("state"))).collect()).unwrap_or_default();
        let drawn = c.eval(admin, "[...document.querySelectorAll('.machine-card ul.glance li')].map(li => li.dataset.key + '=' + li.dataset.state)")?;
        let drawn: Vec<String> = drawn.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect();
        want(drawn == want_states, || format!("the card draws {drawn:?}; gunnar says {want_states:?}"))?;
        let tones = c.eval(admin, "[...document.querySelectorAll('.machine-card ul.glance li')].map(li => li.dataset.state + ':' + li.querySelector('.badge').className)")?;
        for t in tones.arr().iter().filter_map(|x| x.as_str()) {
            let (state, class) = t.split_once(':').unwrap_or(("", ""));
            let tone = match state { "ok" => "pass", "warn" => "warn", "bad" => "bad", _ => "plain" };
            want(class.split_whitespace().any(|c| c == tone), || format!("gunnar said {state}, the badge is {class:?}"))?;
        }
        Ok(format!("picture of {boxes} box(es); lines {drawn:?}"))
    });

    // ★ **The picture follows the console's theme, live** (owner, 2026-10-05:
    // "the SVG is nice BUT make it follow the chosen theme of the web site; we
    // have 2 themes"). The drawing names its colours as page tokens
    // (`var(--estate-…, skade's)`) and console.css maps them onto the theme's
    // own; so the colours a browser PAINTS the picture with — read off it
    // with getComputedStyle, not off the markup — must be the console's
    // tokens in the light theme, the dark theme's after the header's ☾ switch
    // is pressed, the surprise theme's own board after ✦ (owner, same day: a
    // third theme), and the light's again after ☀, on the SAME picture: no
    // fetch, no redraw. And the boxes read in order (owner, same day): this
    // gunnar's main box on the top row, the console that calls it below.
    go(rows, "the-estate-picture-follows-the-theme-switch-live", || {
        c.navigate(admin, &format!("{}/admin/overview", r.origin()))?;
        c.wait_for(admin, "!!document.querySelector('.machine-card .estate-picture svg g[data-box] rect') && !!document.querySelector('button.theme-toggle')", 30_000)
            .map_err(|e| format!("no inline picture or no theme switch: {e}"))?;
        let topo = c.eval(admin, "fetch('/api/machine/topology').then(r => r.json())")?;
        let boxes = topo.path("boxes").map(|b| b.arr().to_vec()).unwrap_or_default();
        let me = boxes.iter().find(|b| b.path("this_box").and_then(|v| v.as_bool()) == Some(true)).map(|b| b.text("name")).ok_or("gunnar names no box as this box")?;
        let console = boxes.iter().find(|b| b.text("role") == "console").map(|b| b.text("name")).ok_or("no console box on the rig")?;
        // Start light whatever the last row left: the switch walks ☀ → ☾ → ✦
        // and round, so light is at most two presses away.
        let light = "!document.documentElement.hasAttribute('data-theme')";
        for _ in 0..2 {
            if c.eval_str(admin, &format!("String({light})"))? == "true" {
                break;
            }
            c.eval(admin, "document.querySelector('button.theme-toggle').click()")?;
            std::thread::sleep(Duration::from_millis(200));
        }
        c.wait_for(admin, light, 5_000)?;
        c.eval(admin, "document.querySelector('.machine-card .estate-picture svg').__uitest_same = 'yes'")?;
        // What the picture is painted with, and what the console's tokens
        // are, in the theme that is on now.
        let read = format!(
            "(() => {{ const svg = document.querySelector('.machine-card .estate-picture svg'); \
               const me = svg.querySelector('g[data-box=' + JSON.stringify({me}) + ']'); \
               const cs = e => getComputedStyle(e); \
               const probe = document.createElement('i'); document.body.appendChild(probe); \
               const tok = v => {{ probe.style.color = 'var(' + v + ')'; return cs(probe).color; }}; \
               const out = {{ theme: document.documentElement.getAttribute('data-theme') || 'light', \
                 same: svg.__uitest_same || '', \
                 board: cs(svg.querySelector('#board stop')).stopColor, \
                 edge: cs(me.querySelector('rect')).stroke, \
                 name: cs(me.querySelector('text')).fill, \
                 lamp: cs(me.querySelector('circle')).fill, \
                 surface: tok('--surface'), ink_dim: tok('--ink-dim'), ink: tok('--ink'), pass: tok('--pass'), \
                 accent: tok('--accent') }}; \
               probe.remove(); return out; }})()",
            me = esc(&me),
        );
        let paint = |c: &mut Cdp| -> Result<Vec<(String, String)>, String> {
            let j = c.eval(admin, &read)?;
            Ok(["theme", "same", "board", "edge", "name", "lamp", "surface", "ink_dim", "ink", "pass", "accent"].iter().map(|k| (k.to_string(), j.text(k))).collect())
        };
        let get = |p: &[(String, String)], k: &str| p.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone()).unwrap_or_default();
        // Painted with the console's tokens: board on --surface, main's edge
        // --ink-dim (the surprise theme's own board: --accent, its purple),
        // its name --ink, its `up` lamp --pass.
        let painted_as_the_console = |p: &[(String, String)]| -> Result<(), String> {
            let edge = if get(p, "theme") == "surprise" { "accent" } else { "ink_dim" };
            for (drawn, token) in [("board", "surface"), ("edge", edge), ("name", "ink"), ("lamp", "pass")] {
                want(!get(p, drawn).is_empty() && get(p, drawn) == get(p, token), || {
                    format!("{} theme: the picture's {drawn} is painted {:?}, the console's --{} is {:?}: {p:?}", get(p, "theme"), get(p, drawn), token.replace('_', "-"), get(p, token))
                })?;
            }
            want(get(p, "same") == "yes", || format!("{} theme: the picture was replaced, not repainted: {p:?}", get(p, "theme")))
        };
        let light_paint = paint(c)?;
        painted_as_the_console(&light_paint)?;
        // ☾: the header's own switch, pressed.
        c.eval(admin, "document.querySelector('button.theme-toggle').click()")?;
        c.wait_for(admin, "document.documentElement.getAttribute('data-theme') === 'dark'", 5_000)
            .map_err(|e| format!("the switch did not turn the console dark: {e}"))?;
        let dark_paint = paint(c)?;
        painted_as_the_console(&dark_paint)?;
        for k in ["board", "edge", "name", "lamp"] {
            want(get(&light_paint, k) != get(&dark_paint, k), || format!("the picture's {k} is {:?} in both themes", get(&dark_paint, k)))?;
        }
        // ✦: the third theme, its own board.
        c.eval(admin, "document.querySelector('button.theme-toggle').click()")?;
        c.wait_for(admin, "document.documentElement.getAttribute('data-theme') === 'surprise'", 5_000)
            .map_err(|e| format!("the switch did not move the console from dark to surprise: {e}"))?;
        let surprise_paint = paint(c)?;
        painted_as_the_console(&surprise_paint)?;
        for k in ["board", "edge"] {
            want(get(&surprise_paint, k) != get(&dark_paint, k) && get(&surprise_paint, k) != get(&light_paint, k), || {
                format!("the picture's {k} is {:?} in surprise, as in another theme", get(&surprise_paint, k))
            })?;
        }
        // ☀: and back, still the same picture.
        c.eval(admin, "document.querySelector('button.theme-toggle').click()")?;
        c.wait_for(admin, light, 5_000).map_err(|e| format!("the switch did not turn the console light again: {e}"))?;
        let again = paint(c)?;
        painted_as_the_console(&again)?;
        want(get(&again, "board") == get(&light_paint, "board") && get(&again, "edge") == get(&light_paint, "edge"), || format!("light again is not light: {again:?} vs {light_paint:?}"))?;
        // Reading order: the main box on top, the console that calls it below.
        let tops = c.eval_str(
            admin,
            &format!(
                "(() => {{ const t = n => document.querySelector('.machine-card .estate-picture svg g[data-box=' + JSON.stringify(n) + '] rect').getBoundingClientRect().top; \
                   return t({}) + ' ' + t({}); }})()",
                esc(&me),
                esc(&console)
            ),
        )?;
        let (main_top, console_top) = tops.split_once(' ').map(|(a, b)| (a.parse::<f64>().unwrap_or(0.0), b.parse::<f64>().unwrap_or(0.0))).unwrap_or((0.0, 0.0));
        want(main_top < console_top, || format!("the console is not on the row below the main box: main top {main_top}, console top {console_top}"))?;
        Ok(format!(
            "light: board {} edge {} lamp {}; dark: board {} edge {} lamp {}; surprise: board {} edge {} lamp {}; light again; same picture; {me} on top, {console} below",
            get(&light_paint, "board"),
            get(&light_paint, "edge"),
            get(&light_paint, "lamp"),
            get(&dark_paint, "board"),
            get(&dark_paint, "edge"),
            get(&dark_paint, "lamp"),
            get(&surprise_paint, "board"),
            get(&surprise_paint, "edge"),
            get(&surprise_paint, "lamp")
        ))
    });

    // ★ **Three themes, one switch** (owner, 2026-10-05: a third theme,
    // "surprise"). The header's switch walks ☀ light → ☾ dark → ✦ surprise →
    // ☀ light, its label always naming the NEXT one; each press must put the
    // theme's attribute on <html>, remember the choice where the pre-paint
    // script reads it, and repaint — read off the page as a browser painted it:
    // a token (--bg) and the body's own ground, the estate picture's board, the
    // face the text is set in. The surprise face (Manrope, bundled) must be
    // fetched and loaded in surprise and NEVER asked for in light or dark —
    // no face of it requested by the page, no byte of it over the wire —
    // which is half of "light and dark look exactly as before". A resource
    // entry alone is not a download: a new page in the same renderer can list
    // a woff2 a page before it left in the memory cache, with nothing
    // transferred and its face never requested (measured 2026-10-05, one
    // light page in three). And a reload in surprise comes back in surprise.
    go(rows, "the-theme-switch-walks-light-dark-surprise-and-each-one-applies", || {
        let url = format!("{}/admin/overview", r.origin());
        let out = (|| -> Result<String, String> {
            c.navigate_fresh(admin, &url)?;
            c.wait_for(admin, "!!document.querySelector('.machine-card .estate-picture svg #board stop') && !!document.querySelector('button.theme-toggle')", 30_000)
                .map_err(|e| format!("no inline picture or no theme switch: {e}"))?;
            let attr = "(document.documentElement.getAttribute('data-theme') || 'light')";
            // Start light: at most two presses away.
            for _ in 0..2 {
                if c.eval_str(admin, attr)? == "light" {
                    break;
                }
                c.eval(admin, "document.querySelector('button.theme-toggle').click()")?;
                std::thread::sleep(Duration::from_millis(200));
            }
            // A fresh document in light, so no face of the surprise theme has
            // been asked for in it yet.
            c.navigate_fresh(admin, &url)?;
            c.wait_for(admin, "!!document.querySelector('.machine-card .estate-picture svg #board stop') && !!document.querySelector('button.theme-toggle')", 30_000)?;
            let read = "(() => { const r = document.documentElement; \
                const probe = document.createElement('i'); document.body.appendChild(probe); \
                const tok = v => { probe.style.color = 'var(' + v + ')'; return getComputedStyle(probe).color; }; \
                let store = ''; try { store = localStorage.getItem('gunnar-theme') || ''; } catch (e) { store = 'storage throws'; } \
                const svg = document.querySelector('.machine-card .estate-picture svg'); \
                const out = { theme: r.getAttribute('data-theme') || 'light', store, \
                  label: (document.querySelector('button.theme-toggle') || {}).textContent || '', \
                  bg: tok('--bg'), body: getComputedStyle(document.body).backgroundColor, \
                  surface: tok('--surface'), accent: tok('--accent'), \
                  board: svg ? getComputedStyle(svg.querySelector('#board stop')).stopColor : 'no picture', \
                  font: getComputedStyle(document.body).fontFamily, \
                  hairline: getComputedStyle(document.querySelector('header.topbar')).borderImageSource.slice(0, 15), \
                  wire: performance.getEntriesByType('resource').filter(e => /manrope-[0-9]+[.]woff2/.test(e.name) && e.transferSize > 0).map(e => e.name.split('/').pop()).join(' '), \
                  entries: performance.getEntriesByType('resource').filter(e => /manrope-[0-9]+[.]woff2/.test(e.name)).map(e => e.name.split('/').pop() + ' transfer ' + e.transferSize + ' B, ' + Math.round(e.duration) + ' ms, delivery ' + JSON.stringify(e.deliveryType)).join('; '), \
                  asked: [...document.fonts].filter(f => f.family.replace(/[\"']/g, '') === 'Manrope' && f.status !== 'unloaded').map(f => f.weight + ':' + f.status).join(' '), \
                  loaded: [...document.fonts].filter(f => f.family.replace(/[\"']/g, '') === 'Manrope' && f.status === 'loaded').map(f => f.weight).join(' '), \
                  age: String(Math.round(performance.now())) + ' ms' }; \
                probe.remove(); return out; })()";
            const KEYS: [&str; 15] = ["theme", "store", "label", "bg", "body", "surface", "accent", "board", "font", "hairline", "wire", "entries", "asked", "loaded", "age"];
            let snap = |c: &mut Cdp| -> Result<Vec<(String, String)>, String> {
                let j = c.eval(admin, read)?;
                Ok(KEYS.iter().map(|k| (k.to_string(), j.text(k))).collect())
            };
            let get = |p: &[(String, String)], k: &str| p.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone()).unwrap_or_default();
            let mut said = Vec::new();
            let first = snap(c)?;
            want(get(&first, "theme") == "light" && get(&first, "label") == "☾ Dark", || format!("not light, or the switch does not offer ☾ Dark: {first:?}"))?;
            want(get(&first, "bg") == "rgb(244, 244, 244)" && get(&first, "body") == get(&first, "bg"), || format!("light's ground is not #f4f4f4: {first:?}"))?;
            want(get(&first, "wire").is_empty() && get(&first, "asked").is_empty() && !get(&first, "font").contains("Manrope"), || format!("light names, asks for or downloads the surprise face: {first:?}"))?;
            said.push(format!("light: bg {} board {} font {}; surprise face asked for: none; its entries: [{}]", get(&first, "bg"), get(&first, "board"), get(&first, "font"), get(&first, "entries")));
            let mut prev = first.clone();
            let mut seen = vec![first.clone()];
            // (the theme a press lands on, the label it must then show, its --bg
            // when it is one this row can name exactly, its board's first stop)
            for (theme, label, bg, board) in [
                ("dark", "✦ Surprise", "", ""),
                ("surprise", "☀ Light", "rgb(8, 11, 24)", "rgb(17, 21, 42)"),
                ("light", "☾ Dark", "rgb(244, 244, 244)", ""),
            ] {
                c.eval(admin, "document.querySelector('button.theme-toggle').click()")?;
                c.wait_for(admin, &format!("{attr} === {}", esc(theme)), 5_000)
                    .map_err(|e| format!("a press from {} did not land on {theme}: {e}", get(&prev, "theme")))?;
                if theme == "surprise" {
                    // A face is fetched when text is first laid out in it;
                    // wait for the body's (400), not merely the first one in —
                    // the memory cache can hand back 600 and 800 while 400 is
                    // still on its way.
                    c.wait_for(admin, "document.body.offsetHeight > 0 && document.fonts.status === 'loaded' && [...document.fonts].some(f => f.family.replace(/[\"']/g, '') === 'Manrope' && f.weight === '1 499' && f.status === 'loaded')", 10_000)
                        .map_err(|e| format!("surprise: the bundled body face never loaded: {e}"))?;
                }
                let now = snap(c)?;
                let what = || format!("{now:?}");
                want(get(&now, "label") == label, || format!("{theme}: the switch reads {:?}, not {label:?} (the theme a press goes to): {}", get(&now, "label"), what()))?;
                want(get(&now, "store") == theme, || format!("{theme}: the choice is stored as {:?}: {}", get(&now, "store"), what()))?;
                want(get(&now, "body") == get(&now, "bg"), || format!("{theme}: the body's ground is not the theme's --bg: {}", what()))?;
                want(get(&now, "bg") != get(&prev, "bg") && get(&now, "board") != get(&prev, "board"), || {
                    format!("{theme}: the token or the picture did not change from {}: {} vs {prev:?}", get(&prev, "theme"), what())
                })?;
                want(get(&now, "board") == get(&now, "surface"), || format!("{theme}: the picture's board is not the theme's --surface: {}", what()))?;
                if !bg.is_empty() {
                    want(get(&now, "bg") == bg, || format!("{theme}: --bg is {:?}, not {bg}: {}", get(&now, "bg"), what()))?;
                }
                if !board.is_empty() {
                    want(get(&now, "board") == board, || format!("{theme}: the board is {:?}, not {board}: {}", get(&now, "board"), what()))?;
                }
                if theme == "surprise" {
                    want(get(&now, "font").starts_with("Manrope"), || format!("surprise: the text is set in {:?}: {}", get(&now, "font"), what()))?;
                    want(get(&now, "accent") == "rgb(139, 92, 246)", || format!("surprise: --accent is {:?}: {}", get(&now, "accent"), what()))?;
                    want(get(&now, "hairline").starts_with("linear-gradient"), || format!("surprise: the header carries no gradient: {}", what()))?;
                    // Body text, labels and the headline each on their cut:
                    // all three faces loaded, from the only source they name,
                    // the bundle's own /manrope-*.woff2.
                    want(get(&now, "loaded") == "1 499 500 749 750 1000", || format!("surprise: not every cut of the face loaded: {}", what()))?;
                } else {
                    want(!get(&now, "font").contains("Manrope") && get(&now, "hairline") == "none", || {
                        format!("{theme}: the surprise face or hairline leaks into it: {}", what())
                    })?;
                }
                if theme == "dark" {
                    want(get(&now, "wire").is_empty() && get(&now, "asked").is_empty(), || format!("dark asked for or downloaded the surprise face: {}", what()))?;
                }
                said.push(format!("{theme}: bg {} board {} font {}{}", get(&now, "bg"), get(&now, "board"), get(&now, "font").split(',').next().unwrap_or(""),
                    if theme == "surprise" { format!(" (loaded weights {}; entries [{}])", get(&now, "loaded"), get(&now, "entries")) } else { String::new() }));
                prev = now.clone();
                seen.push(now);
                if theme == "surprise" {
                    // Remembered: a reload comes back in surprise, the switch
                    // offering ☀ Light.
                    c.navigate_fresh(admin, &url)?;
                    c.wait_for(admin, "!!document.querySelector('button.theme-toggle') && !!document.querySelector('.machine-card .estate-picture svg #board stop')", 30_000)?;
                    let back = snap(c)?;
                    want(get(&back, "theme") == "surprise" && get(&back, "label") == "☀ Light" && get(&back, "bg") == "rgb(8, 11, 24)", || {
                        format!("a reload in surprise did not come back in surprise: {back:?}")
                    })?;
                    said.push("reload: still surprise".into());
                    prev = back;
                }
            }
            let bgs: std::collections::BTreeSet<String> = seen.iter().take(3).map(|p| get(p, "bg")).collect();
            want(bgs.len() == 3, || format!("three themes, but {} grounds: {bgs:?}", bgs.len()))?;
            Ok(said.join(" ; "))
        })();
        // Whatever happened, bob is left in light, as the rows after this
        // one (and their pictures) expect him.
        let _ = c.eval(admin, "(() => { try { localStorage.setItem('gunnar-theme', 'light'); } catch (e) {} \
            document.documentElement.removeAttribute('data-theme'); return 'ok'; })()");
        out
    });

    go(rows, "instruments-does-not-repeat-the-menu", || {
        c.navigate(admin, &format!("{}/admin", r.origin()))?;
        c.wait_for(admin, &format!("{MAIN_TEXT}.includes(\"The console's own identity\")"), 30_000)?;
        let seen = panels(c, admin)?;
        want(!seen.iter().any(|p| p == "Pages"), || format!("Instruments still draws a Pages panel: {seen:?}"))?;
        let links = c.eval_str(admin, "String(document.querySelectorAll('main .admin-links, main a.btn[href=\"/machine\"]').length)")?;
        want(links == "0", || format!("{links} menu link block(s) on Instruments"))?;
        Ok(format!("panels {seen:?}"))
    });

    go(rows, "storage-says-each-disk-once-and-names-whose-it-is", || {
        c.navigate(admin, &format!("{}/admin/storage", r.origin()))?;
        c.wait_for(admin, "[...document.querySelectorAll('section.panel > header h2')].some(h => h.textContent.trim() === 'Layout')", 30_000)
            .map_err(|e| format!("no Layout panel on Storage: {e}"))?;
        let labels = c.eval_str(admin, "[...document.querySelectorAll('dt')].map(e => e.textContent.trim()).join(' | ')")?;
        want(labels.split(" | ").any(|l| l == "/serialdat on") && labels.split(" | ").any(|l| l == "/data is"), || format!("the disks are not each named: {labels}"))?;
        for gone in ["Backing device", "Backing class"] {
            want(!labels.split(" | ").any(|l| l == gone), || format!("`{gone}` is back: {labels}"))?;
        }
        let seen = panels(c, admin)?;
        // Layout always; Data set and /serialdat at most once (a rig with no
        // installed disks has no /serialdat to draw).
        for (panel, most) in [("Layout", 1), ("Data set", 1), ("/serialdat", 1)] {
            let n = seen.iter().filter(|p| *p == panel).count();
            want(n <= most && (panel != "Layout" || n == 1), || format!("{panel} drawn {n} times: {seen:?}"))?;
        }
        // The installer's record is there, folded: asked for, not shown.
        let folded = c.eval_str(admin, "String(!!document.querySelector('details.installer-record') && !document.querySelector('details.installer-record').open)")?;
        want(folded == "true", || "the installer's record is not a closed fold".into())?;
        Ok(format!("panels {seen:?}"))
    });

    go(rows, "the-readme-tab-is-the-readme-and-the-clone-line", || {
        c.navigate(admin, &format!("{}/repo/alice/uitest", r.origin()))?;
        c.wait_for(admin, "!!document.querySelector('nav.repo-tabs a.on')", 30_000)?;
        // The clone line is drawn when the console knows its git base
        // (`/auth/status`'s git_url); a rig with none draws none, and says so.
        let status = c.eval(admin, "fetch('/auth/status').then(r => r.json())")?;
        let has_base = !status.text("git_url").is_empty();
        if has_base {
            c.wait_for(admin, "!!document.querySelector('p.clone-line code')", 30_000)
                .map_err(|e| format!("no clone line on the Readme tab although the console has a git base: {e}"))?;
        }
        std::thread::sleep(Duration::from_millis(1500));
        let seen = panels(c, admin)?;
        want(!seen.iter().any(|p| p == "References" || p == "Mirrored from"), || format!("the Readme tab draws more than the README: {seen:?}"))?;
        c.navigate(admin, &format!("{}/repo/alice/uitest/graph", r.origin()))?;
        c.wait_for(admin, "[...document.querySelectorAll('section.panel > header h2')].some(h => h.textContent.trim() === 'References')", 30_000)
            .map_err(|e| format!("the references are not on History: {e}"))?;
        // The panel's header is drawn before its list has answered: wait for
        // the list (or its failure), not for the header beside it — this row
        // read the panel before its list under load on 2026-10-04 and said
        // main was missing.
        // A wait that runs out is not the verdict; the read below is.
        let _ = c.wait_for(
            admin,
            "(() => { const s=[...document.querySelectorAll('section.panel')].find(s => s.querySelector('h2') && s.querySelector('h2').textContent.trim() === 'References');
               return !!s && (s.innerText.includes('main') || !!s.querySelector('.failure')); })()",
            30_000,
        );
        let refs = c.eval_str(admin, "String([...document.querySelectorAll('section.panel')].find(s => s.querySelector('h2').textContent.trim() === 'References').innerText.includes('main'))")?;
        want(refs == "true", || "History's References do not list main".into())?;
        Ok(format!("Readme: {seen:?} (clone line {}); History carries References", if has_base { "drawn" } else { "none: no git base" }))
    });

    go(rows, "a-page-says-one-line-and-a-question-mark", || {
        let mut walked = Vec::new();
        for path in ["/admin/overview", "/admin/storage", "/admin", "/access", "/mirrors"] {
            c.navigate(admin, &format!("{}{path}", r.origin()))?;
            header(c, admin)?;
            std::thread::sleep(Duration::from_millis(1500));
            // Every hint's OWN text (not what waits behind its "?") is one line.
            let long = c.eval(
                admin,
                "[...document.querySelectorAll('main .hint, main .empty p')].filter(e => !e.closest('details.why')).map(e => {
                    const c = e.cloneNode(true); c.querySelectorAll('details').forEach(d => d.remove());
                    return c.textContent.trim(); }).filter(t => t.length > 200)",
            )?;
            let long: Vec<String> = long.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect();
            want(long.is_empty(), || format!("{path} shows {} long text(s) unasked: {:?}", long.len(), long.iter().map(|t| http::cut(t, 80)).collect::<Vec<_>>()))?;
            let open = c.eval_str(admin, "String(document.querySelectorAll('main details.why[open]').length)")?;
            want(open == "0", || format!("{path} opens {open} \"?\" by itself"))?;
            let asks = c.eval_str(admin, "String(document.querySelectorAll('main details.why').length)")?;
            walked.push(format!("{path}: {asks} ?"));
        }
        // A "?" opens on a click and says the rest.
        c.navigate(admin, &format!("{}/admin", r.origin()))?;
        c.wait_for(admin, "!!document.querySelector('.admin-banner details.why summary')", 30_000)?;
        c.eval(admin, "document.querySelector('.admin-banner details.why summary').click()")?;
        let said = c.eval_str(admin, "document.querySelector('.admin-banner details.why').open ? document.querySelector('.admin-banner details.why p').textContent.trim() : ''")?;
        want(!said.is_empty(), || "the banner's \"?\" does not open".into())?;
        Ok(walked.join(", "))
    });

    // ── the live card: a twin dies, the open card goes red ─────────────────
    go(rows, "a-twin-that-stops-goes-red-in-the-open-card", || {
        // A real follower polls this gunnar as a member of group twin (the
        // dual heartbeat), then stops. gunnar decides `silent` after its bound
        // (the rig's 5 s); the OPEN card — no navigation, no reload — must show
        // gunnar's verdict on its own: the twin's chip, its link and its words.
        let dir = r.work.join("live-twin");
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(dir.join("data")).map_err(|e| e.to_string())?;
        let key = dir.join("twin_ed25519");
        let made = std::process::Command::new("ssh-keygen")
            .args(["-q", "-t", "ed25519", "-N", "", "-C", "uitest-twin", "-f"])
            .arg(&key)
            .status()
            .map_err(|e| format!("ssh-keygen: {e}"))?;
        want(made.success(), || "ssh-keygen failed".into())?;
        let pubkey = std::fs::read_to_string(key.with_extension("pub")).map_err(|e| e.to_string())?;
        r.control(&["user", "add", "uitest-twin", "--kind", "machine", "--key", pubkey.trim()])?;
        let _ = r.control(&["group", "add", "twin"]);
        r.control(&["group", "member", "add", "twin", "uitest-twin"])?;
        // The twin's poll: `twin follow --once` every second — each run is one
        // Mirror.LogHead carrying the twin's report (the dual heartbeat), which
        // is all gunnar's verdict reads. Stopping the loop is the twin dying.
        let beat = |r: &Rig| {
            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
            let (cli, at, data, key, log) = (r.cli.clone(), r.at(), dir.join("data"), key.clone(), dir.join("follow.log"));
            let flag = stop.clone();
            let handle = std::thread::spawn(move || {
                while !flag.load(std::sync::atomic::Ordering::Relaxed) {
                    let _ = std::process::Command::new(&cli)
                        .args(["control", "twin", "follow", "--once", "--at", &at, "--data-root"])
                        .arg(&data)
                        .arg("-i")
                        .arg(&key)
                        .stdout(std::process::Stdio::null())
                        .stderr(std::fs::OpenOptions::new().create(true).append(true).open(&log).map(std::process::Stdio::from).unwrap_or(std::process::Stdio::null()))
                        .status();
                    std::thread::sleep(Duration::from_millis(1000));
                }
            });
            (stop, handle)
        };
        let end = |(stop, handle): (std::sync::Arc<std::sync::atomic::AtomicBool>, std::thread::JoinHandle<()>)| {
            stop.store(true, std::sync::atomic::Ordering::Relaxed);
            let _ = handle.join();
        };
        let state = "(document.querySelector('.machine-card .estate-picture svg g[data-box=\"uitest-twin\"]') || {dataset:{}}).dataset.state || ''";
        let link = "(document.querySelector('.machine-card .estate-picture svg g[data-link=\"uitest-twin\"]') || {dataset:{}}).dataset.state || ''";
        let mut twin = Some(beat(r));
        let outcome = (|| -> Result<String, String> {
            c.navigate(admin, &format!("{}/admin/overview", r.origin()))?;
            // Heard: `up`, or `behind` (this twin applies nothing) — both lit.
            let heard = format!("['up','behind'].includes({state})");
            c.wait_for(admin, &heard, 40_000)
                .map_err(|e| format!("the polling twin never showed up on the card: {e}"))?;
            let up_link = c.eval_str(admin, link)?;
            let pulses = c.eval_str(admin, "String(!!document.querySelector('.machine-card .estate-picture svg g[data-link=\"uitest-twin\"] animate'))")?;
            want(["up", "behind"].contains(&up_link.as_str()) && pulses == "true", || format!("the link into a twin that polls is {up_link:?}, pulsing {pulses}"))?;
            // The open page, marked: a reload or a navigation would lose this.
            c.eval(admin, "window.__still_open = 'yes'")?;
            let t0 = Instant::now();
            if let Some(t) = twin.take() {
                end(t);
            }
            c.wait_for(admin, &format!("({state}) === 'silent'"), 25_000)
                .map_err(|e| format!("the twin stopped and the open card did not turn it silent within 25 s: {e}"))?;
            let red_after = t0.elapsed().as_secs();
            let still = c.eval_str(admin, "String(window.__still_open || '')")?;
            want(still == "yes", || "the card went red by a reload or a navigation, not in place".into())?;
            let words = c.eval_str(admin, "(document.querySelector('.machine-card .estate-picture svg g[data-box=\"uitest-twin\"]') || {textContent:''}).textContent")?;
            want(words.contains("silent for") || words.contains("never heard"), || format!("gunnar's words are not on the silent chip: {words:?}"))?;
            let dead_link = c.eval_str(admin, link)?;
            let still_line = c.eval_str(admin, "String(!document.querySelector('.machine-card .estate-picture svg g[data-link=\"uitest-twin\"] animate'))")?;
            want(dead_link == "silent" && still_line == "true", || format!("the link into the dead twin is {dead_link:?}, still {still_line}"))?;
            // gunnar's own answer agrees, word for word.
            let topo = c.eval(admin, "fetch('/api/machine/topology').then(r => r.json())")?;
            let row = topo.path("boxes").and_then(|b| b.arr().iter().find(|x| x.text("name") == "uitest-twin").cloned()).ok_or("gunnar's answer has no uitest-twin")?;
            want(row.text("state") == "silent" && words.contains(&row.text("headline")), || format!("the card says {words:?}; gunnar says {} / {}", row.text("state"), row.text("headline")))?;
            // And back: the twin polls again, the card turns it up again.
            twin = Some(beat(r));
            c.wait_for(admin, &heard, 25_000)
                .map_err(|e| format!("the twin came back and the card did not light it within 25 s: {e}"))?;
            Ok(format!("lit → silent in the open card {red_after} s after the twin stopped (\"{}\"), link still red; lit again when it polled again", row.text("headline")))
        })();
        if let Some(t) = twin.take() {
            end(t);
        }
        // Leave the rig as it was: no twin member for the rows after this one.
        let _ = r.control(&["group", "member", "remove", "twin", "uitest-twin"]);
        outcome
    });

    // ── plain page titles ──────────────────────────────────────────────────
    go(rows, "no-page-header-carries-an-explanatory-subtitle", || {
        let mut walked = Vec::new();
        for (sess, path) in [(admin, "/admin/overview"), (admin, "/admin/storage"), (admin, "/admin"), (admin, "/access"), (admin, "/mirrors"), (admin, "/account"), (person, "/repos"), (person, "/account"), (person, "/account/activity")] {
            c.navigate(sess, &format!("{}{path}", r.origin()))?;
            let (h1, line) = header(c, sess)?;
            let data = line.is_empty() || line.starts_with("Server key SHA256:");
            want(data, || format!("{path} ({h1}) carries a subtitle: {line:?}"))?;
            walked.push(format!("{path}={h1}"));
        }
        Ok(walked.join(" "))
    });

    // ── the events a person is shown ───────────────────────────────────────
    go(rows, "a-person-is-shown-only-events-of-repositories-they-may-read", || {
        // kim listens FIRST (non-blocking), then bob makes a change to a
        // PRIVATE repository and waits for it on his own feed — the control.
        c.eval(
            person,
            "(() => { window.__heard = []; const es = new EventSource('/api/events');
                     es.addEventListener('feed', e => window.__heard.push(String(e.data)));
                     window.__es = es; return 'listening'; })()",
        )?;
        std::thread::sleep(Duration::from_millis(1500));
        let got = c.eval(
            admin,
            "new Promise(resolve => {
                const tag = 'private-' + Math.random().toString(36).slice(2, 10);
                const es = new EventSource('/api/events');
                const done = (ok, why) => { clearTimeout(t); es.close(); resolve({ok, why, tag}); };
                const t = setTimeout(() => done(false, 'nothing arrived in 20 s'), 20000);
                es.addEventListener('feed', e => { if (String(e.data).includes(tag)) done(true, 'arrived'); });
                es.onopen = () => fetch('/api/repos', {method:'POST', headers:{'Content-Type':'application/json'},
                    body: JSON.stringify({namespace:'alice', name: tag})});
             })",
        )?;
        let tag = got.text("tag");
        want(got.path("ok").and_then(|v| v.as_bool()) == Some(true), || format!("the admin's own feed did not carry the change: {}", got.text("why")))?;
        std::thread::sleep(Duration::from_secs(3));
        let heard = c.eval(person, "(() => { const h = window.__heard || []; if (window.__es) window.__es.close(); return h; })()")?;
        let heard: Vec<String> = heard.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect();
        want(!heard.iter().any(|l| l.contains(&tag)), || format!("kim (a viewer) was told about alice/{tag}, a private repository: {heard:?}"))?;
        // And kim's Activity page (You → Activity) does not draw it either.
        c.navigate(person, &format!("{}/account/activity", r.origin()))?;
        c.wait_for(person, &format!("{MAIN_TEXT}.length > 20"), 30_000)?;
        std::thread::sleep(Duration::from_secs(2));
        let page = c.eval_str(person, MAIN_TEXT)?;
        want(!page.contains(&tag), || format!("kim's Activity page shows alice/{tag}: {}", http::cut(&page, 300)))?;
        Ok(format!("bob heard alice/{tag}; kim heard {} lines, none about it", heard.len()))
    });

    // ── the system principals ──────────────────────────────────────────────
    go(rows, "the-identities-list-hides-the-boxs-own-principals-until-asked", || {
        c.navigate(admin, &format!("{}/access", r.origin()))?;
        c.wait_for(admin, "!!document.querySelector('table caption')", 30_000)?;
        let api = c.eval(admin, "fetch('/api/principals').then(r => r.json())")?;
        let system: Vec<String> = api.path("items").map(|i| i.arr().iter().filter(|p| p.text("kind") != "user").map(|p| p.text("id")).collect()).unwrap_or_default();
        want(!system.is_empty(), || format!("the rig has no machine principal to hide: {}", http::cut(&api.dump(), 200)))?;
        let names = |c: &mut Cdp| -> Result<Vec<String>, String> {
            let ids = c.eval(admin, "[...([...document.querySelectorAll('table')].find(t => t.querySelector('caption')?.textContent === 'Principals')?.querySelectorAll('tbody td.oid') ?? [])].map(td => td.textContent.trim())")?;
            Ok(ids.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect())
        };
        let hidden = names(c)?;
        want(!hidden.iter().any(|id| system.contains(id)), || format!("system principals listed by default: {hidden:?} (system {system:?})"))?;
        c.eval(admin, "(() => { const t = document.querySelector('label.toggle input'); t.click(); return 'toggled'; })()")?;
        std::thread::sleep(Duration::from_millis(500));
        let shown = names(c)?;
        want(system.iter().all(|id| shown.contains(id)), || format!("the toggle does not show them: {shown:?} (system {system:?})"))?;
        Ok(format!("{} system principals hidden, shown on the toggle", system.len()))
    });

    // ── pictures of the new navigation, for the owner ──────────────────────
    go(rows, "the-new-navigation-is-photographed", || {
        let dir = r.work.join("shots");
        let mut saved = Vec::new();
        let _ = c.viewport(person, 1280, 900);
        let _ = c.viewport(admin, 1280, 900);
        for (n, sess, path, open) in [
            ("01-person-repositories", person, "/repos", ""),
            ("02-person-you-menu", person, "/repos", "header.topbar details.menu"),
            ("03-person-account", person, "/account", ""),
            ("04-person-activity", person, "/account/activity", ""),
            ("05-admin-administration-menu", admin, "/repos", "header.topbar details.admin-menu"),
            ("06-admin-overview", admin, "/admin/overview", ""),
            ("07-admin-storage", admin, "/admin/storage", ""),
            ("08-repo-code-tab", admin, "/repo/alice/uitest", ""),
            ("09-repo-activity-tab", admin, "/repo/alice/uitest/activity", ""),
            ("10-repo-settings-tab", admin, "/repo/alice/uitest/settings", ""),
        ] {
            c.navigate(sess, &format!("{}{path}", r.origin()))?;
            let _ = c.wait_for(sess, "!!document.querySelector('header.page-header h1')", 20_000);
            std::thread::sleep(Duration::from_millis(1200));
            if !open.is_empty() {
                c.eval(sess, &format!("(() => {{ const d = document.querySelector({}); if (d) d.open = true; return 'ok'; }})()", esc(open)))?;
                std::thread::sleep(Duration::from_millis(300));
            }
            let to = dir.join(format!("{n}.png"));
            c.screenshot(sess, &to)?;
            c.eval(sess, "(() => { document.querySelectorAll('details.menu').forEach(d => d.open = false); return 'closed'; })()")?;
            saved.push(n);
        }
        // A phone: the top bar wraps.
        let _ = c.viewport(person, 390, 844);
        c.navigate(person, &format!("{}/repos", r.origin()))?;
        std::thread::sleep(Duration::from_millis(1200));
        c.screenshot(person, &dir.join("11-person-phone.png"))?;
        c.eval(person, "(() => { const d = document.querySelector('header.topbar details.menu'); if (d) d.open = true; return 'ok'; })()")?;
        std::thread::sleep(Duration::from_millis(300));
        c.screenshot(person, &dir.join("12-person-phone-you-menu.png"))?;
        // The TOP BAR must collapse cleanly (owner): nothing of it past the
        // phone's edge. What a wide table does to the page is reported.
        let wraps = c.eval(person, "(() => { const b = document.querySelector('header.topbar'); return [...b.querySelectorAll('*')].every(e => e.getBoundingClientRect().right <= window.innerWidth + 1) && b.getBoundingClientRect().right <= window.innerWidth + 1; })()")?;
        let page_width = c.eval_str(person, "String(document.documentElement.scrollWidth)")?;
        let wide = c.eval_str(
            person,
            "[...document.querySelectorAll('body *')].filter(e => { const r = e.getBoundingClientRect(); return r.right > window.innerWidth + 1 && !e.closest('table'); })
                .slice(0, 12).map(e => e.tagName + '.' + e.className + ' ' + Math.round(e.getBoundingClientRect().right)).join(' | ') + ' ; scrollWidth ' + document.documentElement.scrollWidth",
        )?;
        let _ = c.viewport(person, 1280, 900);
        want(wraps.as_bool() == Some(true), || format!("on a 390 px phone the top bar runs past the edge: {wide}"))?;
        Ok(format!("{} pictures in {}; on a 390 px phone the top bar wraps inside the screen (the page is {page_width} px wide: {wide})", saved.len() + 2, dir.display()))
    });

    // ── the header's mark is big enough to see (owner, 2026-10-05) ────────
    // "the new gunnar logo — it's so small I can't see it": 34 x 19 px. Now
    // 64 x 35 on a desktop, and on a 390 px phone the largest that keeps the
    // bar's first line whole (42 x 23), in every theme — and the bar pays
    // nothing for it: 52 px tall on a desktop, and on a phone the mark is no
    // taller than the other things on its line (so it cannot be what makes
    // the bar taller), with the brand, Repositories and the theme switch
    // still on one line and nothing past the screen's edge.
    go(rows, "the-header-mark-is-big-enough-and-the-bar-keeps-its-height", || {
        let measure = "(() => { const b = e => e.getBoundingClientRect(); \
            const bar = b(document.querySelector('header.topbar')), mark = b(document.querySelector('.brand-mark-small')), \
                  link = b(document.querySelector('header.topbar .top-link')), sw = b(document.querySelector('button.theme-toggle')); \
            const mid = r => Math.round(r.top + r.height / 2); \
            return [bar.height, mark.width, mark.height, mid(mark), mid(link), mid(sw), Math.max(link.height, sw.height), sw.right, document.documentElement.clientWidth] \
              .map(v => Math.round(v)).join(' '); })()";
        let mut said = Vec::new();
        let out = (|| -> Result<(), String> {
            for theme in ["light", "dark", "surprise"] {
                c.eval(person, &format!("(() => {{ try {{ localStorage.setItem('gunnar-theme', {}); }} catch (e) {{}} return 'ok'; }})()", esc(theme)))?;
                for (w, h) in [(1280u32, 900u32), (480, 900), (390, 844)] {
                    // A phone's width is measured as a phone draws it: overlay scrollbars, so a
                    // long page (kim's, late in the full matrix) does not take 15 px of the 390.
                    if w > 720 {
                        c.viewport(person, w, h)?;
                    } else {
                        viewport_phone(c, person, w, h)?;
                    }
                    c.navigate_fresh(person, &format!("{}/repos", r.origin()))?;
                    c.wait_for(person, "!!document.querySelector('.brand-mark-small') && !!document.querySelector('button.theme-toggle') && !!document.querySelector('header.topbar .top-link')", 20_000)?;
                    let _ = c.eval(person, "document.fonts.ready.then(() => true)");
                    std::thread::sleep(Duration::from_millis(400));
                    // Load-tolerant (r5.3 batch matrix 2026-10-08: "dark 390 px: bar 136, mark
                    // 49x27 … switch right 120" in the full matrix, green alone): measure until two
                    // reads 300 ms apart agree, at most ~10 s, so a layout still settling under load
                    // (the viewport, the stylesheet, the badge) is not judged. A bar that is really
                    // wrong stays wrong and is still refused below.
                    let mut m = c.eval_str(person, measure)?;
                    for _ in 0..33 {
                        std::thread::sleep(Duration::from_millis(300));
                        let again = c.eval_str(person, measure)?;
                        if again == m {
                            break;
                        }
                        m = again;
                    }
                    let n: Vec<i64> = m.split(' ').filter_map(|t| t.parse().ok()).collect();
                    want(n.len() == 9, || format!("{theme} {w} px: nothing to measure: {m}"))?;
                    let (bar, mw, mh, mark_mid, link_mid, sw_mid, line, sw_right, iw) = (n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7], n[8]);
                    let bar_text = c.eval_str(person, "[...document.querySelector('header.topbar').children].map(e => { const b = e.getBoundingClientRect(); return (e.className || e.tagName) + '@' + Math.round(b.left) + ',' + Math.round(b.top) + '+' + Math.round(b.width) + ':' + (e.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 24); }).join(' | ')").unwrap_or_default();
                    let what = || format!("{theme} {w} px: bar {bar}, mark {mw}x{mh}, line {line}, middles mark {mark_mid} link {link_mid} switch {sw_mid}, switch right {sw_right} of {iw} (the bar reads {bar_text:?})");
                    let (min_w, min_h) = if w > 440 { (60, 33) } else { (40, 22) };
                    want(mw >= min_w && mh >= min_h, || format!("the mark is too small to see (want {min_w}x{min_h} or more) — {}", what()))?;
                    want(((mw as f64) / (mh as f64) - 1392.0 / 768.0).abs() < 0.1, || format!("the mark is not 1392:768 — {}", what()))?;
                    if w == 1280 {
                        want(bar == 52 && mh + 8 <= bar, || format!("the bar is not the 52 px it was — {}", what()))?;
                    } else {
                        want(mh <= line, || format!("the mark is the tallest thing on its line, so it makes the bar taller — {}", what()))?;
                        want((mark_mid - link_mid).abs() <= 4 && (mark_mid - sw_mid).abs() <= 4 && sw_right <= iw, || {
                            format!("the brand, Repositories and the switch are not one line inside the screen — {}", what())
                        })?;
                    }
                    said.push(format!("{theme} {w}px: mark {mw}x{mh} bar {bar}"));
                }
            }
            Ok(())
        })();
        let _ = c.viewport(person, 1280, 900);
        let _ = c.eval(person, "(() => { try { localStorage.setItem('gunnar-theme', 'light'); } catch (e) {} document.documentElement.removeAttribute('data-theme'); return 'ok'; })()");
        out?;
        Ok(said.join(" ; "))
    });

    // ── the three themes, photographed for the owner (2026-10-05) ─────────
    // The main pages in ☀ light, ☾ dark and ✦ surprise, each chosen the way a
    // person's choice is remembered (the stored word, applied before the
    // first paint), into shots/themes/<theme>/. Pictures, not proof: the row
    // above measures; these are for eyes, and for the light and dark pictures
    // to be compared against the console before the third theme.
    go(rows, "the-main-pages-are-photographed-in-all-three-themes", || {
        let dir = r.work.join("shots").join("themes");
        let mut saved = 0;
        let _ = c.viewport(person, 1280, 900);
        let _ = c.viewport(admin, 1280, 900);
        let set = |c: &mut Cdp, sess: &str, theme: &str| -> Result<(), String> {
            c.eval(sess, &format!("(() => {{ try {{ localStorage.setItem('gunnar-theme', {}); }} catch (e) {{}} return 'ok'; }})()", esc(theme)))?;
            Ok(())
        };
        let settle = |c: &mut Cdp, sess: &str| {
            let _ = c.eval(sess, "document.fonts.ready.then(() => 'ok')");
            std::thread::sleep(Duration::from_millis(1200));
        };
        let out = (|| -> Result<(), String> {
            for theme in ["light", "dark", "surprise"] {
                set(c, person, theme)?;
                set(c, admin, theme)?;
                for (n, sess, path, open) in [
                    ("01-person-repositories", person, "/repos", ""),
                    ("02-person-you-menu", person, "/repos", "header.topbar details.menu"),
                    ("03-person-account", person, "/account", ""),
                    ("04-person-activity", person, "/account/activity", ""),
                    ("05-admin-administration-menu", admin, "/repos", "header.topbar details.admin-menu"),
                    ("06-admin-overview", admin, "/admin/overview", ""),
                    ("07-admin-storage", admin, "/admin/storage", ""),
                    ("08-repo-code-tab", admin, "/repo/alice/uitest", ""),
                    ("09-repo-activity-tab", admin, "/repo/alice/uitest/activity", ""),
                    ("10-repo-settings-tab", admin, "/repo/alice/uitest/settings", ""),
                ] {
                    c.navigate_fresh(sess, &format!("{}{path}", r.origin()))?;
                    let _ = c.wait_for(sess, "!!document.querySelector('header.page-header h1')", 20_000);
                    let want_attr = if theme == "light" { "null".to_owned() } else { esc(theme) };
                    c.wait_for(sess, &format!("document.documentElement.getAttribute('data-theme') === {want_attr}"), 5_000)
                        .map_err(|e| format!("{theme}: {path} did not open in the stored theme: {e}"))?;
                    settle(c, sess);
                    if !open.is_empty() {
                        c.eval(sess, &format!("(() => {{ const d = document.querySelector({}); if (d) d.open = true; return 'ok'; }})()", esc(open)))?;
                        std::thread::sleep(Duration::from_millis(300));
                    }
                    c.screenshot(sess, &dir.join(theme).join(format!("{n}.png")))?;
                    c.eval(sess, "(() => { document.querySelectorAll('details.menu').forEach(d => d.open = false); return 'closed'; })()")?;
                    saved += 1;
                }
                let _ = c.viewport(person, 390, 844);
                c.navigate_fresh(person, &format!("{}/repos", r.origin()))?;
                settle(c, person);
                c.screenshot(person, &dir.join(theme).join("11-person-phone.png"))?;
                c.eval(person, "(() => { const d = document.querySelector('header.topbar details.menu'); if (d) d.open = true; return 'ok'; })()")?;
                std::thread::sleep(Duration::from_millis(300));
                c.screenshot(person, &dir.join(theme).join("12-person-phone-you-menu.png"))?;
                c.eval(person, "(() => { document.querySelectorAll('details.menu').forEach(d => d.open = false); return 'closed'; })()")?;
                let _ = c.viewport(person, 1280, 900);
                saved += 2;
            }
            Ok(())
        })();
        // Whatever happened, both people are left in light, as they came.
        let _ = c.viewport(person, 1280, 900);
        for sess in [person, admin] {
            let _ = set(c, sess, "light");
            let _ = c.eval(sess, "document.documentElement.removeAttribute('data-theme'); 'ok'");
        }
        out?;
        Ok(format!("{saved} pictures in {} (light, dark, surprise)", dir.display()))
    });

    // ── the typeset README follows the theme (owner, 2026-10-06) ───────────
    // "The rendered README is locked to a white background with black text;
    // the HTML view follows the theme." The typeset page is an SVG the server
    // drew with docbook's paper colours baked in (`fill="#ffffff"` under ink
    // `#16191d`, links `#0a58ca`, code `#f5f6f8`, rules `#cfd4da`); the
    // stylesheet maps each onto the theme's own token. Asserted on the
    // COMPUTED paint of the SVG's own elements against a probe painted with
    // the token itself, in all three themes, so a token that changes is
    // followed and a rule that stops matching is caught. One picture per
    // theme into shots/readme/, taken before the verdict so a red run still
    // shows what it measured.
    go(rows, "the-typeset-readme-follows-the-theme-in-all-three", || {
        let wt = r.work.join("wt-readme-theme");
        std::fs::create_dir_all(&wt).map_err(|e| e.to_string())?;
        r.git(&wt, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&wt, &["config", "user.name", "uitest"])?;
        r.git(&wt, &["config", "user.email", "info@vetra.se"])?;
        std::fs::write(
            wt.join("README.md"),
            "# Gunnar, typeset\n\nA paragraph with a [link](https://example.com), `inline code`, **bold** and *italic*.\n\n\
             > A block quote, quietly.\n\n| Column | Value |\n| ------ | ----- |\n| one    | 1     |\n| two    | 2     |\n\n\
             ```rust\nfn main() {\n    println!(\"the code block\");\n}\n```\n\n- one\n- two\n\n---\n\nThe end.\n",
        )
        .map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "README.md"])?;
        r.git(&wt, &["commit", "-qm", "a readme with every element the page draws"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/readme-theme"), "main"])?;
        let dir = r.work.join("shots").join("readme");
        let _ = c.viewport(admin, 1280, 900);
        // `[paint, token]` pairs: what the SVG's element paints, and what a
        // probe painted with the theme's token paints, read the same way.
        let read = "(() => { const svg = document.querySelector('.readme-page svg'); \
            const probe = document.createElement('div'); document.body.appendChild(probe); \
            const tok = v => { probe.style.color = 'var(' + v + ')'; return getComputedStyle(probe).color; }; \
            const fillOf = sel => { const e = svg.querySelector(sel); return e ? getComputedStyle(e).fill : 'none drawn'; }; \
            const strokeOf = sel => { const e = svg.querySelector(sel); return e ? getComputedStyle(e).stroke : 'none drawn'; }; \
            const out = { \
              paper: [fillOf('path.typst-shape[fill=\"#ffffff\"]'), tok('--surface')], \
              box: [getComputedStyle(document.querySelector('.readme-page')).backgroundColor, tok('--surface')], \
              ink: [fillOf('[fill=\"#16191d\"]'), tok('--ink')], \
              link: [fillOf('[fill=\"#0a58ca\"]'), tok('--readme-link')], \
              code: [fillOf('[fill=\"#f5f6f8\"]'), tok('--surface-raised')], \
              rule: [strokeOf('[stroke=\"#cfd4da\"]'), tok('--line-strong')] }; \
            probe.remove(); return JSON.stringify(out); })()";
        // Every theme is walked and photographed whatever the first one says:
        // a red run must still show all three.
        let mut said = Vec::new();
        let mut wrong: Vec<String> = Vec::new();
        let out = (|| -> Result<(), String> {
            for theme in ["light", "dark", "surprise"] {
                c.eval(admin, &format!("(() => {{ try {{ localStorage.setItem('gunnar-theme', {}); }} catch (e) {{}} return 'ok'; }})()", esc(theme)))?;
                c.navigate_fresh(admin, &format!("{}/repo/alice/readme-theme", r.origin()))?;
                c.wait_for(admin, "!!document.querySelector('.readme-page svg path.typst-shape')", 60_000)
                    .map_err(|e| format!("{theme}: the typeset README was not drawn: {e}"))?;
                let want_attr = if theme == "light" { "null".to_owned() } else { esc(theme) };
                c.wait_for(admin, &format!("document.documentElement.getAttribute('data-theme') === {want_attr}"), 5_000)
                    .map_err(|e| format!("{theme}: the page did not open in the stored theme: {e}"))?;
                let _ = c.eval(admin, "document.fonts.ready.then(() => 'ok')");
                std::thread::sleep(Duration::from_millis(800));
                c.eval(admin, "(() => { const p = document.querySelector('.readme-page'); if (p) p.scrollIntoView({block: 'start'}); return 'ok'; })()")?;
                std::thread::sleep(Duration::from_millis(300));
                c.screenshot(admin, &dir.join(format!("{theme}.png")))?;
                let j = crate::json::parse(&c.eval_str(admin, read)?).map_err(|e| format!("{theme}: {e}"))?;
                let mut line = Vec::new();
                for part in ["paper", "box", "ink", "link", "code", "rule"] {
                    let pair = j.get(part).map(|p| p.arr().iter().map(|x| x.as_str().unwrap_or("").to_owned()).collect::<Vec<_>>()).unwrap_or_default();
                    let (paint, token) = (pair.first().cloned().unwrap_or_default(), pair.get(1).cloned().unwrap_or_default());
                    if token.is_empty() || paint != token {
                        wrong.push(format!("{theme}: the README's {part} paints {paint:?}, the theme's token is {token:?}"));
                    }
                    line.push(format!("{part} {paint}"));
                }
                // And the complaint itself, said plainly: no theme but light
                // draws a white sheet.
                if theme != "light" {
                    let paper = j.get("paper").map(|p| p.arr().first().and_then(|x| x.as_str()).unwrap_or("").to_owned()).unwrap_or_default();
                    if paper == "rgb(255, 255, 255)" || paper == "rgb(244, 244, 244)" {
                        wrong.push(format!("{theme}: the README is still a white sheet ({paper})"));
                    }
                }
                said.push(format!("{theme}: {}", line.join(", ")));
            }
            want(wrong.is_empty(), || format!("the typeset page does not follow the theme — {}", wrong.join(" ; ")))
        })();
        let _ = c.eval(admin, "(() => { try { localStorage.setItem('gunnar-theme', 'light'); } catch (e) {} document.documentElement.removeAttribute('data-theme'); return 'ok'; })()");
        out?;
        Ok(format!("{} ; pictures in {}", said.join(" ; "), dir.display()))
    });

    // ── an account that has bought nothing (owner 2026-10-17) ─────────────
    // ONE panel, "Plan": monetize's `none` reads "Free" and the button "Buy a
    // plan" — never "None yet", never "Subscription" — and the button lands
    // on the ONE sign-up page. Checked against monetize's own answer.
    go(rows, "an-account-that-has-bought-nothing-reads-free-and-buy-a-plan", || {
        want(r.bench.is_some(), || "NOT RUN: no --bench-monetize (a community console sells nothing: the sign-up page, the Plan panel and the shop are a metered console's)".into())?;
        let api = c.eval(admin, "fetch('/api/account/subscription/bundles').then(r => r.json()).then(w => ({ state: String((w.plan || {}).state ?? '') }))")?;
        // No gauge (a shop selling no plans yet) or monetize's `none`: no plan.
        want(api.text("state") == "none" || api.text("state").is_empty(), || format!("monetize does not say bob holds no plan at this point: {}", http::cut(&api.dump(), 300)))?;
        c.navigate_fresh(admin, &format!("{}/account", r.origin()))?;
        c.wait_for(admin, "!!document.getElementById('account-buy-grow')", 30_000).map_err(|e| {
            let body = c.eval_str(admin, MAIN_TEXT).unwrap_or_default();
            format!("You → Account drew no button ({e}): {}", http::cut(&body, 400))
        })?;
        let plan = c.eval_str(admin, "document.getElementById('account-plan').innerText.replace(/\\s+/g, ' ').trim()")?;
        let main = c.eval_str(admin, MAIN_TEXT)?;
        want(plan.contains("Free") && plan.contains("Buy a plan"), || format!("the Plan panel: {plan}"))?;
        for never in ["None yet", "Subscription", "unknown", "contact us", "failed"] {
            want(!main.contains(never), || format!("the page says {never:?}: {}", http::cut(&main, 300)))?;
        }
        c.eval(admin, "document.getElementById('account-buy-grow').click()")?;
        c.wait_for(admin, "location.pathname === '/signup' && document.getElementById('heading').textContent === 'Buy a plan'", 20_000)
            .map_err(|e| format!("Buy a plan did not land on the sign-up page's Buy a plan: {e}"))?;
        Ok(plan)
    });

    // ── the buy button goes STRAIGHT to the vendor ─────────────────────────
    let Some(_) = r.bench.as_ref() else {
        go(rows, "the-offer-buttons-go-straight-to-the-vendors-hosted-checkout", || {
            Err("NOT RUN: no --bench-monetize (a metered rig with the fake Stripe is what this row needs)".into())
        });
        return;
    };
    // ── Buying is the ONE sign-up page (owner 2026-10-17) ──────────────────
    //
    // The console's own More room panel is gone: a signed-in person buys and
    // grows on `/signup?plan=10gb` (Account's ONE button opens it). These rows
    // hold that page to what the panel's rows held: the window's slow reading
    // is said while it lasts, a refused window says the server's sentence,
    // the steps carry monetize's prices, and Upgrade goes straight to the
    // vendor's checkout.
    const SIGNUP_PROBE: &str = "(() => ({step: document.getElementById('step-name')?.textContent.trim() ?? '',
                                         price: document.getElementById('price-month')?.textContent.trim() ?? '',
                                         msg: document.getElementById('msg')?.textContent.trim() ?? '',
                                         err: (document.getElementById('msg')?.className ?? '').includes('err'),
                                         go: !!document.getElementById('go') && !document.getElementById('go').disabled,
                                         heading: document.getElementById('heading')?.textContent.trim() ?? ''}))()";
    let signup = |c: &mut Cdp| -> Result<(), String> {
        c.navigate(person, &format!("{}/signup?plan=10gb", r.origin()))?;
        c.wait_for(person, "location.pathname === '/signup' && !!document.getElementById('heading')", 30_000)
    };
    // ① The window read slowly: the server's "Awaiting the cloud provider…"
    // while it lasts, then the steps — never an empty page.
    go(rows, "the-first-visit-while-the-cloud-reading-is-slow-says-awaiting-then-the-steps", || {
        let (code, said) = r.bench_post("/reading/slow/700")?;
        want(code == 200, || format!("the bench would not slow its reading: {code} {said}"))?;
        let watched = (|| -> Result<String, String> {
            signup(c)?;
            let began = Instant::now();
            let mut awaited_ms = None::<u128>;
            loop {
                let p = c.eval(person, SIGNUP_PROBE)?;
                let last = p.dump();
                if p.text("msg").contains("Awaiting the cloud provider") && awaited_ms.is_none() {
                    awaited_ms = Some(began.elapsed().as_millis());
                }
                if !p.text("price").is_empty() && p.text("step") != "…" && !p.text("step").is_empty() {
                    break;
                }
                if began.elapsed() > Duration::from_secs(60) {
                    return Err(format!("no steps within 60 s: {}", http::cut(&last, 300)));
                }
                std::thread::sleep(Duration::from_millis(100));
            }
            let took = began.elapsed().as_millis();
            let awaited = awaited_ms.ok_or_else(|| format!("the page never said \"Awaiting the cloud provider…\" before the steps at {took} ms"))?;
            want(awaited < 5_000, || format!("\"Awaiting the cloud provider…\" first said only at {awaited} ms"))?;
            Ok(format!("\"Awaiting the cloud provider…\" from {awaited} ms, the steps at {took} ms"))
        })();
        let (code, said) = r.bench_post("/reading/normal")?;
        want(code == 200, || format!("the bench would not restore its reading: {code} {said}"))?;
        watched
    });

    // ② A REFUSED window: the server's sentence, nothing to press; served
    // again, the page (reloaded, as a person does) draws the steps.
    go(rows, "a-refused-window-says-the-servers-sentence-and-nothing-can-be-pressed-then-served-draws-the-steps", || {
        let (code, said) = r.bench_post("/window/refuse")?;
        want(code == 200, || format!("the bench would not refuse the window: {code} {said}"))?;
        let watched = (|| -> Result<String, String> {
            signup(c)?;
            c.wait_for(person, "(document.getElementById('msg')?.className ?? '').includes('err')", 30_000)
                .map_err(|e| format!("a refused window said nothing ({e}): {}", c.eval(person, SIGNUP_PROBE).map(|j| j.dump()).unwrap_or_default()))?;
            let p = c.eval(person, SIGNUP_PROBE)?;
            want(!p.path("go").and_then(|g| g.as_bool()).unwrap_or(true), || format!("Upgrade can be pressed on a refused window: {}", p.dump()))?;
            let server = c.eval_str(person, "fetch('/api/account/subscription/bundles').then(r => r.json()).then(j => j.message ?? j.error ?? '')")?;
            let line = p.text("msg");
            want(!server.is_empty() && line == server, || format!("the page's line {line:?} is not the server's sentence {server:?}"))?;
            let (code, said) = r.bench_post("/window/serve")?;
            want(code == 200, || format!("the bench would not serve the window: {code} {said}"))?;
            signup(c)?;
            c.wait_for(person, "(document.getElementById('price-month')?.textContent.trim() ?? '') !== ''", 30_000)
                .map_err(|e| format!("served again, no steps ({e})"))?;
            Ok(format!("refused: {:?}; served: the steps", http::cut(&line, 120)))
        })();
        let (code, said) = r.bench_post("/window/serve")?;
        want(code == 200, || format!("the bench would not serve the window again: {code} {said}"))?;
        watched
    });

    // ③ The steps carry monetize's price and period; − / + step through them.
    go(rows, "the-signup-steps-carry-monetizes-price", || {
        signup(c)?;
        c.wait_for(person, "(document.getElementById('price-month')?.textContent.trim() ?? '') !== ''", 30_000)?;
        let mut seen = Vec::new();
        for _ in 0..8 {
            let p = c.eval(person, SIGNUP_PROBE)?;
            let (step, price) = (p.text("step"), p.text("price"));
            want(!step.is_empty() && price.chars().any(|c| c.is_ascii_digit()), || format!("a step without monetize's price: {}", p.dump()))?;
            seen.push(format!("{step} {price}"));
            if c.eval(person, "document.getElementById('plus').disabled")?.as_bool() == Some(true) {
                break;
            }
            c.eval(person, "(() => { document.getElementById('plus').click(); return 'plus'; })()")?;
        }
        Ok(seen.join(" · "))
    });

    // ④ Upgrade goes STRAIGHT to the vendor's hosted checkout, same tab.
    go(rows, "the-offer-buttons-go-straight-to-the-vendors-hosted-checkout", || {
        signup(c)?;
        c.wait_for(person, "(document.getElementById('price-month')?.textContent.trim() ?? '') !== ''", 30_000)?;
        // The account's own size is the first step (not buyable): step to one that is.
        for _ in 0..8 {
            if c.eval(person, "!document.getElementById('go').disabled")?.as_bool() == Some(true) {
                break;
            }
            c.eval(person, "(() => { document.getElementById('plus').click(); return 'plus'; })()")?;
        }
        c.call(
            Some(person),
            "Fetch.enable",
            r#"{"patterns":[{"urlPattern":"https://checkout.fake-stripe.test/*","requestStage":"Request"}]}"#,
        )?;
        c.events.clear();
        let before = c.eval_str(person, "location.href")?;
        c.eval(person, "(() => { document.getElementById('go').click(); return 'pressed'; })()")?;
        let paused = c.paused_request(person, 30_000);
        let navigated: Vec<String> = c
            .events
            .iter()
            .filter(|e| e.text("method") == "Page.frameNavigated" && e.text("params.frame.parentId").is_empty())
            .map(|e| e.text("params.frame.url"))
            .collect();
        let Some((id, url)) = paused else {
            let _ = c.call(Some(person), "Fetch.disable", "{}");
            let now = c.eval_str(person, "location.href").unwrap_or_default();
            let said = c.eval(person, SIGNUP_PROBE).map(|j| j.dump()).unwrap_or_default();
            return Err(format!("Upgrade did not go to the vendor within 30 s: still at {now}; the page says {}", http::cut(&said, 600)));
        };
        let page = format!("<!doctype html><title>Checkout (fake Stripe)</title><h1>Fake Stripe checkout</h1><p>{url}</p>");
        c.call(
            Some(person),
            "Fetch.fulfillRequest",
            &format!(
                r#"{{"requestId":{},"responseCode":200,"responseHeaders":[{{"name":"Content-Type","value":"text/html"}}],"body":{}}}"#,
                esc(&id),
                esc(&b64(page.as_bytes()))
            ),
        )?;
        c.wait_for(person, "location.host === 'checkout.fake-stripe.test'", 30_000)?;
        let landed = c.eval_str(person, "location.href")?;
        let _ = c.call(Some(person), "Fetch.disable", "{}");
        want(landed.starts_with("https://checkout.fake-stripe.test/c/pay/cs_test_"), || format!("landed on {landed}"))?;
        let between: Vec<&String> = navigated.iter().filter(|u| !u.starts_with("https://checkout.fake-stripe.test/")).collect();
        want(between.is_empty(), || format!("an intermediate page between {before} and the checkout: {between:?}"))?;
        c.navigate(person, &format!("{}/account", r.origin()))?;
        Ok(format!("pressed once on {before} → {landed}, same tab, no page between"))
    });
}