diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs
index 00ae0dc..3b7c3e9 100644
--- a/crates/cb-render-html/src/doc.rs
+++ b/crates/cb-render-html/src/doc.rs
@@ -220,6 +220,15 @@ h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf}
.pick{cursor:grab;user-select:none;box-shadow:0 2px 0 #0006,0 0 0 1px #5a7a inset;
transition:transform .08s,box-shadow .08s}
.pick:hover{box-shadow:0 3px 8px #000a,0 0 0 1px #7ca inset;transform:translateY(-1px)}
+/* CB-WP-0017's principle, one step further: a thing you CLICK and a thing
+ you DRAG must not look identical either. `.pick` promises carry-me-
+ somewhere; these promise press-me. They wore `.pick` and therefore a
+ grab cursor, which invited a drag -- and a drag released over nothing
+ posts nothing, so the button appeared dead. Reported both as the button
+ showing a hand it should not, AND as being unable to start a new game. */
+.tap{cursor:pointer;user-select:none;box-shadow:0 2px 0 #0006,0 0 0 1px #a7d6 inset;
+ transition:transform .08s,box-shadow .08s}
+.tap:hover{box-shadow:0 3px 8px #000a,0 0 0 1px #a7d inset;transform:translateY(-1px)}
/* A legal destination for the thing currently held -- and ONLY for that
thing. The set is written by Rust into data-targets; the script matches
it and never derives it (ADR-0010 D1).
@@ -1062,7 +1071,7 @@ fn move_section(
if !spatial {
let _ = write!(
s,
- "
{}
",
+ "{}
",
esc(&format!("{c:?}"))
);
}
@@ -1075,7 +1084,7 @@ fn move_section(
}
if may_pass {
s.push_str(
- "pass \u{2014} decline to act
",
+ "pass \u{2014} decline to act
",
);
}
}
@@ -1336,8 +1345,8 @@ pub fn ending(
let _ = write!(
s,
"\
-
play again
\
-
end session \u{2014} stops the game server
\
+
play again
\
+
end session \u{2014} stops the game server
\
",
);
log_section(&mut s, log);
diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs
index a58d230..76db89f 100644
--- a/crates/cb-render-html/src/lib.rs
+++ b/crates/cb-render-html/src/lib.rs
@@ -1275,6 +1275,8 @@ mod played_cards {
/// table and a `play again` pointing at a closed port.
#[cfg(test)]
mod ending_page {
+ use cb_kernel::PlayerId;
+
use crate::{doc, jsrun};
fn page() -> String {
@@ -1326,6 +1328,53 @@ mod ending_page {
);
}
+ /// **A thing you click must not look like a thing you drag.**
+ ///
+ /// Reported two ways at once: *"the button shows a hand to pick up
+ /// that it probably shouldn't"* and *"I can't start another game"*.
+ /// They are one defect — a grab cursor invites a drag, and a drag
+ /// released over nothing posts nothing, so the button looks dead.
+ #[test]
+ fn click_targets_do_not_wear_the_drag_affordance() {
+ let pages = [
+ doc::ending(None, "m", "/command?t=x", &[], &[]),
+ doc::document(
+ &crate::testfix::view(Some(PlayerId(0))),
+ &[games_ground::GroundCommand::SelectAction {
+ action: games_ground::Action::Investigate,
+ target: None,
+ problem: Some(2),
+ }],
+ "/command?t=x",
+ Some(PlayerId(0)),
+ true,
+ ),
+ ];
+ for html in &pages {
+ for key in ["again", "done", "pass"] {
+ let Some(i) = html.find(&format!("data-drop=\"{key}\"")) else {
+ continue;
+ };
+ let tag = &html[html[..i].rfind('<').expect("an opening tag")..i];
+ assert!(
+ !tag.contains("pick"),
+ "`{key}` is a click target wearing `.pick`, which is cursor:grab — \
+ it invites a drag, and a drag onto nothing posts nothing"
+ );
+ assert!(
+ tag.contains("tap"),
+ "`{key}` must still look pressable: {tag}"
+ );
+ }
+ }
+ // The inverse, or this passes for a page with no affordances at
+ // all: a real draggable still carries `.pick`.
+ assert!(
+ pages[1].contains("class=\"card act pick\""),
+ "action cards must still be draggable"
+ );
+ }
+
/// **CB-WP-0028 T06, asserted both ways.** A test that only checked
/// the win case would pass for a page that always says "solved".
#[test]
diff --git a/specs/FindingRegister.md b/specs/FindingRegister.md
index 6c96ef4..be96283 100644
--- a/specs/FindingRegister.md
+++ b/specs/FindingRegister.md
@@ -47,6 +47,8 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
| F17 | degenerate | note | — | — | 2026-08-06 | ground-game |
| F18 | inert | raised | — | — | 2026-08-06 | clay-borg |
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
+| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
+| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
@@ -76,6 +78,28 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
since GROUND-WP-0005 is blocked on exactly this number. The withdrawal
was reported (ADR-0012 D5). Its reproduction is `difficulty.rs`, whose
policy panel is plural *because of this finding*.
+- **F21 — dragging did not work until after the first note was saved.**
+ Reported 2026-08-06: *"I could not drag and drop at the beginning but
+ after I added the first comment it worked."* **`note`, and I could not
+ reproduce it.** The gesture logic is correct against the served page
+ (the JS harness posts properly), the ghost carries `pointer-events:none`
+ so it cannot intercept the drop, and the markup is identical before and
+ after the note — the 303 re-renders the same page from the same state.
+ **Candidates, none confirmed:** a `` toggle inside an action
+ card shifting the layout mid-drag; a first-load timing difference; or a
+ browser-level pointer capture. **Reproducing it needs a browser, which
+ no test here has** — the same gap F19 named. Recorded rather than
+ guessed at.
+- **F20 — a click target wearing a drag affordance made the ending
+ controls look dead.** `play again`, `end session`, `pass` and the move
+ buttons carried `.pick`, which is `cursor:grab`. A grab cursor invites a
+ **drag**, and a drag released over nothing posts nothing — so the button
+ did nothing and appeared broken. Reported as two separate observations
+ (*"the button shows a hand to pick up that it probably shouldn't"* and
+ *"I can't start another game or stop the server"*) which are one defect.
+ **`applied`**: click targets now carry `.tap`. **This extends CB-WP-0017's
+ rule** — interactive and inert must not look identical — to *click and
+ drag must not look identical either*.
- **F19 — the engine shipped a table nobody could play on, and every gate
was green.** CB-WP-0028's overhead view was 620px tall, so the action
cards sat a screen below the Problems; dragging between them was
diff --git a/trials/2026-08-06-2213.md b/trials/2026-08-06-2213.md
new file mode 100644
index 0000000..ecba877
--- /dev/null
+++ b/trials/2026-08-06-2213.md
@@ -0,0 +1,17 @@
+# Trial log
+
+What the player said while playing, bound to the position they said it at
+(CB-WP-0027, ADR-0014). The recording beside this file is the position;
+`state_hash` is what reaches it.
+
+**These are raw notes and they stay here.** Nothing in this file travels to
+`ground-game` (ADR-0014 D4) — a note reaches them only by being promoted to a
+register finding, by a human, with the wording chosen then.
+
+
+
+| n | round | step | state_hash | comment |
+|---|---|---|---|---|
+| 1 | 1 | Select | 47369fdf8d30 | Lets check the notes. |
+
+
diff --git a/trials/2026-08-06-2213.yaml b/trials/2026-08-06-2213.yaml
new file mode 100644
index 0000000..4a291e8
--- /dev/null
+++ b/trials/2026-08-06-2213.yaml
@@ -0,0 +1,149 @@
+scenario: ground/cb-play-session
+description: recorded by cb-play (CB-WP-0008 T02)
+covers: []
+provisional: false
+provisional_owner: ''
+provisional_raised: ''
+ruled: ''
+ruled_by: ''
+ruled_note: ''
+encodes_u_item: ''
+seed: 1
+setup:
+ players: 3
+ preset: standard-3p
+ patch: {}
+commands:
+- actor: P1
+ cmd: select_action
+ args:
+ action: INVESTIGATE
+ problem: 2
+- actor: P2
+ cmd: select_action
+ args:
+ action: SOLVE
+ problem: 1
+- actor: P3
+ cmd: select_action
+ args:
+ action: SOLVE
+ problem: 1
+- actor: SYSTEM
+ cmd: reveal
+ args: {}
+- actor: SYSTEM
+ cmd: resolve
+ args: {}
+- actor: SYSTEM
+ cmd: end_round
+ args: {}
+- actor: P1
+ cmd: select_action
+ args:
+ action: SUPPORT
+ target: P2
+- actor: P2
+ cmd: select_action
+ args:
+ action: INVESTIGATE
+ problem: 3
+- actor: P3
+ cmd: select_action
+ args:
+ action: INVESTIGATE
+ problem: 3
+- actor: SYSTEM
+ cmd: reveal
+ args: {}
+- actor: P2
+ cmd: respond_to_support
+ args:
+ response: accept_bond
+- actor: SYSTEM
+ cmd: resolve
+ args: {}
+- actor: SYSTEM
+ cmd: end_round
+ args: {}
+- actor: P1
+ cmd: select_action
+ args:
+ action: SOLVE
+ problem: 3
+- actor: P2
+ cmd: select_action
+ args:
+ action: INVESTIGATE
+ problem: 4
+- actor: P3
+ cmd: select_action
+ args:
+ action: SOLVE
+ problem: 2
+- actor: SYSTEM
+ cmd: reveal
+ args: {}
+- actor: SYSTEM
+ cmd: resolve
+ args: {}
+- actor: SYSTEM
+ cmd: end_round
+ args: {}
+- actor: P1
+ cmd: select_action
+ args:
+ action: SUPPORT
+ target: P3
+- actor: P2
+ cmd: select_action
+ args:
+ action: SUPPORT
+ target: P1
+- actor: P3
+ cmd: select_action
+ args:
+ action: SOLVE
+ problem: 4
+- actor: SYSTEM
+ cmd: reveal
+ args: {}
+- actor: P3
+ cmd: respond_to_support
+ args:
+ response: accept_bond
+- actor: SYSTEM
+ cmd: resolve
+ args: {}
+- actor: SYSTEM
+ cmd: end_round
+ args: {}
+- actor: P1
+ cmd: select_action
+ args:
+ action: SUPPORT
+ target: P2
+- actor: P2
+ cmd: select_action
+ args:
+ action: SUPPORT
+ target: P1
+- actor: P3
+ cmd: select_action
+ args:
+ action: SUPPORT
+ target: P1
+- actor: SYSTEM
+ cmd: reveal
+ args: {}
+- actor: SYSTEM
+ cmd: resolve
+ args: {}
+- actor: SYSTEM
+ cmd: end_round
+ args: {}
+expect:
+ events: []
+ state: {}
+ rejects: []
+ state_hash: e2ee858a1b907a4595c230747b65eede6c56574308a7ece7dec8ebe30e56fc10