🃏 2026-08-30 choice-card prepend fix
Historical record — 2026-08-30
Preserved as observed. This is the closure evidence for open-work row 3.2.7; a later measurement creates a new dated record naming this one rather than editing this page. Current work state: Program masterplan; the executable contract is unchanged on the probe harness.
Disposition
Issue loca/quizwizz#6, titled [3.2.7] Render prepend-style choice cards, selected from open issues #1–#8 as the narrowest live code defect with a deterministic browser seam and an existing applicable probe wave.
| Field | Value |
|---|---|
| acceptance refinement, posted before implementation | issue comment 1 |
| verification receipt | issue comment 2 — POST HTTP 201, comment id 2 |
| issue closure | PATCH HTTP 201; final state closed; no other issue commented, edited or closed |
| mirror baseline | 7ec58f4 |
| pushed commit | cf0753043e196c52d3a9df807a3a84ce895d9330 |
| push range | 7ec58f4..cf07530 main -> main, through scripts/qwgit.sh |
| changed paths | exactly one — assets/css/qwizz-admin-settings-layout.css; 1 file changed, 80 insertions(+) |
| classification | releaseEligible: false |
Root cause
fieldset.qwizz-choice-cards is emitted twice by structurally identical Settings controls: the Prefix fieldset at includes/Admin/views/settings-page.php:462, writing presentation.prepend_style, and the Print columns fieldset at :516, writing presentation.layout_cols.
The class had no base CSS rule anywhere in the plugin. Re-reading the pristine file out of the mirror at 7ec58f4 confirms its only two stylesheet references were co-tenant selector slots in shared restraint-pass rules — assets/css/qwizz-admin-settings-layout.css:219 inside the border and box-shadow rule at :217-229, and :233 inside the checked-state inset rule at :231-241. Neither rule supplies layout, radio hiding, card geometry, selected fill, hover, focus or forced-colors behaviour, so WordPress painted native radios beside plain inline labels.
Four competing hypotheses were falsified rather than assumed away. No other stylesheet defined the class, so nothing could have reset it. The singular .qwizz-choice-card is a different, front-end Craft builder class at assets/css/qwizz-builder.css:12, not the wp-admin Settings surface. Values, labels, checked state and data-path attributes were all present and correct, so it was not a markup or data-path defect. No JavaScript references .qwizz-choice-cards, so the missing behaviour was CSS-only.
Measured before the fix in cache-disabled Chromium at 1600×1000 and 390×844: the fieldset computed display: block; native radios painted at roughly 24×24 px with position: static, opacity: 1 and clip-path: none; option spans computed padding: 0px with transparent backgrounds, border-radius: 0px, 17 px height and cursor: default; checked and unchecked spans shared identical background and border colours. The formal browser set returned 11 passed / 32 failed / 43 total at each viewport, and the minimised reproducer repeated deterministically at 3/3 failures.
The change
One stylesheet, appended only. The live file now defines wrapping flex fieldsets and compact legends at assets/css/qwizz-admin-settings-layout.css:243-264, positioned labels at :265-269, absolute transparent radios at :271-276, card geometry with a 44 px minimum hit target at :278-290, and distinct selected, hover and :focus-visible states at :292-305. The forced-colors block adds CanvasText, Highlight and HighlightText states plus an explicit focus outline colour at :971-985.
No second convention was introduced: radio hiding reuses the established .qwizz-visual-checks input absolute-and-transparent pattern at assets/css/qwizz-admin-settings-core.css:859-863.
The refinement fixed both columns deliberately, because they share the undefined class and identical DOM shape, and it declined two changes before implementation. No shell-probe paint assertion was added, because computed paint is browser-authoritative and the standing full probe plan had to remain 1..76; a mutation-proved browser tracer carries that burden instead. No disabled-state rule was added, because neither fieldset nor input can enter a disabled state and Settings JS only disables [data-save].
Stage-and-install discipline held for every mutation and restoration; the live file was never edited in place. The complete file was staged at /tmp/qwfix/assets/css/qwizz-admin-settings-layout.css and installed atomically with sudo -n install -o www-data -g www-data -m 0644. Final staged and live parity: SHA-256 06f1cd91bfd14af4d6168e06c10424e3c31f0f8f54ce9cf8d596f329fbfca111 on both paths, owner and mode www-data:www-data 644. PHP lint was not applicable because no PHP file changed; plugin reactivation was not applicable because bootstrap and service wiring did not change; schema, QUIZWIZZ_VERSION, vendor/, build/, release ZIPs, provenance/ and db_data/ were untouched. ./scripts/qwgit.sh diff --check produced no output, mirror status before commit contained exactly one path, and status after push was empty.
One review correction is on the record. The first forced-colors implementation distinguished selected cards and computed a 3 px focus outline, but screenshot review showed that outline resolving to white on a white Canvas, and the standards gate rejected it as not separately discernible. The final file adds outline-color: CanvasText inside @media (forced-colors: active) at :982-984. Forced-colors computed focus afterwards: :focus-visible = true, outline = rgb(0, 0, 0) solid 3px, outline-offset = 2px, Canvas = rgb(255, 255, 255), and a 4× nearest-neighbour crop confirmed a separate black contour with a white gap outside the selected blue card.
Verification
| Gate | Command or method | Result |
|---|---|---|
| narrow probe wave | ./scripts/quizwizz-probe.sh --wave 6 | 1..14, assertions=14 failures=0 |
| full probe plan | ./scripts/quizwizz-probe.sh --wave all | 1..76, assertions=76 failures=0, wall time 90.44 seconds |
| repository browser smoke | scripts/quizwizz-browser-smoke.js against https://wrdp.loca.zone | overallPass: true, assertions: 30, failed: [], consoleErrors: [], pageErrors: [], locale: en-US |
| debug-log window | pinned pre-install offset 7042962 | start_offset and end_offset both 7042962, window_bytes 0, window_lines 0, severity_hits 0, matches [] |
The after-fix computed state kills the original reproducer: the fieldset computes display: flex; radios compute position: absolute with opacity: 0 and do not paint; cards compute padding: 8px 12px with a >=44 px minimum height; the checked card is rgb(246, 247, 247) on rgb(56, 88, 233), the unchecked card rgb(255, 255, 255) on rgb(220, 220, 222); the normal focus outline is rgb(56, 88, 233) solid 3px; and the tracer passes at both 1600×1000 and 390×844 with no horizontal overflow.
Matched mutation pair
The fixed staged file was backed up, the exact pristine issue state (SHA-256 fe3d819d94d5826bea9b90f451507a811601a1852466ce0db6473e1e9916731f) was installed through /tmp/qwfix, both tracers were run, and the fixed file was atomically restored to SHA-256 06f1cd91....
| Tracer | Pristine issue state | Restored final file |
|---|---|---|
| normal Chromium — prepend-style cards at both viewports | not ok 1, assertions=1 failures=1; detail native-radio-visible,card-geometry,selected-paint,hover-paint,focus-ring at 1600x1000 and again at 390x844 | ok 1, assertions=1 failures=0 |
| forced-colors Chromium — selected and focus states | not ok 1, assertions=1 failures=1; detail active=true selected=false focus=false, checked and unchecked both bg=rgba(255, 255, 255, 0) color=rgb(0, 0, 0) border=rgb(0, 0, 0), canvas=rgb(255, 255, 255) | ok 1, assertions=1 failures=0 |
The forced-colors run used a genuinely separate Chromium process launched with --force-high-contrast, which reported matchMedia('(forced-colors: active)').matches === true; it was stopped after the proof.
Interaction, accessible names and persistence
The keyboard sequence used real ArrowRight events with autosave and reload after every transition — none -> alphabetical, alphabetical -> numerical, numerical -> none. For each transition exactly one radio was checked, data-path remained presentation.prepend_style, the values remained none, alphabetical and numerical, :focus-visible was true, the card outline was rgb(56, 88, 233) solid 3px, save state returned to saved, and the value survived reload. The pointer sequence clicked the visible Alphabetical label card and confirmed one checked radio at alphabetical after reload, then clicked the None card and confirmed none. There were zero console errors and zero page errors throughout.
Accessible names survived the restyle, observed under the administrator’s inherited German locale:
group "Präfix"
radio "Keine" [checked]
radio "A. B. C."
radio "1. 2. 3."Residue
Post-smoke browser readback returned lang: de, prepend { value: none, checked: 1 }, layoutCols { value: 1, checked: 1 }, saveState: saved, and the smoke harness reported settingsRestored: true with settingsOriginal: "10" restored to "10" and editorRestored: true. The server-side check agreed:
sudo -n docker compose --profile cli run --rm wpcli eval '$s=\QuizWizz\ExposureSettings::get(); echo wp_json_encode(["prepend_style"=>$s["presentation"]["prepend_style"],"layout_cols"=>$s["presentation"]["layout_cols"]]);'{"prepend_style":"none","layout_cols":1}Recorded limits
Three limits are recorded rather than smoothed over.
The browser smoke harness carries one English-only /unsaved/i assertion. Under the administrator’s normal inherited German locale it observed the correct screen-reader copy nicht gespeicherte Änderungen, leaving every actual behaviour green and that text regex red. User 1 had no explicit locale meta, so the precondition was met by temporarily adding en_US, running the harness, then deleting the meta key; the original inherited German locale was confirmed afterwards.
A no-JS pointer click is not a valid scenario here: with JavaScript disabled the existing Settings tab shell keeps the panel itself at zero size. The fieldset, legend, labels and radios remained server-rendered, and no no-script usability claim is made beyond the fact that this CSS-only patch leaves the baseline markup and JS behaviour unchanged.
The before, after and forced-colors screenshots and the magnified focus crop were bench-local /tmp artifacts used during the run. They are not retained in this vault, so the paint claims above rest on the computed-style tracers and the mutation pair rather than on a stored image.
Fix-loop provenance
This was the first live pass of .omp/skills/qwizz-fix-loop/SKILL.md — 180 lines, five gated phases at :21 diagnose, :47 specify, :80 implement, :109 verify and :133 review and handoff. The reason that skill is bench-local rather than an upstream install is recorded in the 2026-08-30 skill distillation, and the vocabulary it enforces is on the QuizWizz shared language. The loop behaved as written: the refinement was posted before implementation, two requested changes were declined with reasons instead of implemented as dead CSS, and the standards gate rejected a first forced-colors attempt that a green tracer alone would have passed.
Release boundary
releaseEligible: false. The live WRDP plugin and the Gitea mirror hold a verified development fix, and this page authorises nothing further. Canonical release eligibility stays false until the release owner carries the source change through the locked source and release pipeline; that step is the release owner’s, not the bench’s. The executable contract did not change, so the probe harness is untouched and 1..76 remains the standing gate.