🩹 QuizWizz Design Remediation — Merge-Ready Patch Spec
Do not use as a patch source
This 2026-07-24 bench-proven development handoff no longer applies to live source and carries no planning authority: its seven-column Appearance-workspace target is superseded by the nine-section single-scroll Settings document and its Fine-tune source anchors are obsolete, although the intended dispatch is current through unified Print→draft→editor and the enriched unavailable state also remains current. Acceptance statements below are retained as 2026-07-24 browser evidence unless a later dated audit explicitly re-exercised them, and current work is governed by Program Masterplan. Current handoff: 2026-08-27 canonical handoff.
Provenance
Authored + verified live on the wrdp bench (no QuizWizz source repo). Per
BUILDING.md(locked, signed, SLSA-provenance pipeline) these are development artifacts only — the release owner MUST re-apply against canonical source and rebuild through the signed pipeline. Every change was smoke-verified in-browser (computed styles + screenshots). Base version on bench:QUIZWIZZ_VERSION1.10.0(unchanged — drives the?ver=cache-bust).
Stale-audit reconciliation
An intervening corrective pass had already fixed several items the design audit flagged. Verified live and therefore NOT re-touched: Studio review rail is already single-column (
qwizz-builder.css:406-409.qwizz-builder__layout{display:block;max-width:700px}); the primary CTA is alreadyfont-weight:800(:667) and.qwizz-step__continue800;qwizz-admin-settings-visual.cssis already fully tokenized with a canonical--qsv-*scope (:8-18, violet#6d28d9/#5b21b6). Re-applying the audit’s original prescriptions (a:has()2-col revival, a 700 CTA down-weight, a parallel--qw-*settings scope) would have REGRESSED the current build.
Summary
| Change | Area | Files | Nature |
|---|---|---|---|
| Brand token unification (fork farm → one violet) | tokens | 7 CSS files | value swaps |
| Public neutral convergence | tokens | qwizz-builder.css, qwizz-embeds.css | value swaps |
| Product-wide density toggle (comfortable/compact) | feature | new assets/js/qwizz-density.js + 6 CSS + Plugin.php + Shortcode.php | additive |
| Studio step-title weight | hierarchy | qwizz-builder.css | +1 rule |
| Dashboard metric weight + soft tiles | hierarchy | qwizz-admin-dashboard.css | +2 rules |
| Admin subject-tile juice (hover-lift) | juice | qwizz-admin-dashboard.css | +2 rules (reduced-motion-guarded) |
| Settings IA: new “Appearance” workspace | IA | settings-page.php, qwizz-admin-settings-visual.css | markup moves + grid 6→7 |
| Player/library empty state enrichment | empty-state | includes/Frontend/QuizEmbed.php, qwizz-embeds.css | +icon/heading/copy + filtered CTA |
1 — Brand token unification (D3 / A3 “fork farm”)
Five accidental brand-violet forks across five namespaces collapsed to the canonical pair base #6d28d9 / deep #5b21b6. Value swaps only — token NAMES retained (no renames; safest on the locked pipeline). Convergence map:
- base violet:
#6546d7,#5b35d5,#6d4aff→#6d28d9 - deep/dark/hover:
#5034bc,#4f35b4,#4e35b0,#4322b4→#5b21b6
| File:line | Token(s) before → after |
|---|---|
qwizz-finetune.css:2 | --qe-violet:#6546d7 → #6d28d9; --qe-violet-hover:#5034bc → #5b21b6 |
qwizz-admin-shell.css:2-3 | --qw-admin-brand:#6546d7 → #6d28d9; --qw-admin-brand-dark:#4f35b4 → #5b21b6 |
qwizz-admin-dashboard.css:1 | --qw-brand:#6546d7 → #6d28d9; --qw-brand-dark:#4e35b0 → #5b21b6 |
qwizz-admin-editor.css:1 | --qw-brand:#6546d7 → #6d28d9 (subjects var(--qw-brand-dark,#4f35b4) fallback → #5b21b6) |
qwizz-admin-starter.css:5-6 | --qw-starter-violet:#5b35d5 → #6d28d9; --qw-starter-violet-dark:#4322b4 → #5b21b6 |
qwizz-admin-subjects.css:6,258 | --qw-brand:#6d4aff → #6d28d9; fallback ,#4f35b4 → ,#5b21b6 |
qwizz-admin-import.css:1 | --qi-violet:#6546d7 → #6d28d9; --qi-violet-dark:#4f35b4 → #5b21b6 |
Already canonical, untouched: qwizz-builder.css (--qw-v/--qw-v2), qwizz-player.css (base + all 6 [data-theme] moods kept verbatim), qwizz-embeds.css (--qwizz-violet), qwizz-admin-settings-visual.css (--qsv-violet).
Acceptance (verified): grep -riE '#6546d7|#5b35d5|#6d4aff|#5034bc|#4f35b4|#4e35b0|#4322b4' assets/css → 0 hits. Computed primary-CTA color = rgb(109,40,217) on Studio, Player (default), Dashboard, Settings, Starter, Editor; player mood themes still render their own hues.
Residual (deferred, cosmetic): a few derived shadow tints still use the old brand as
rgb(101 70 215 / …)(≈#6546d7) at ≤20% alpha (e.g.qwizz-admin-import.css:4,8). Visually indistinguishable fromrgb(109 40 217 / …); convert opportunistically.
2 — Public neutral convergence
Accidental 1-digit forks aligned so builder == player:
qwizz-builder.css:1:--qw-ink:#201b31→#211d31,--qw-muted:#706a83→#6e6981,--qw-line:#e8e4f0→#e8e4f1,--qw-soft:#f4efff→#f5f1ffqwizz-embeds.css:4-5:--qwizz-ink:#241c30→#211d31,--qwizz-muted:#706778→#6e6981
Note: qwizz-builder.css:26 retains an intentional darker --qw-muted:#625b70 override from the prior corrective pass (contrast); left as-is (it wins by cascade and is a deliberate a11y choice).
3 — Product-wide density toggle (comfortable / compact)
New capability: a per-browser interface-density preference applied via a single data-qwizz-density="compact" attribute on <html> (absent = comfortable default). Compact retunes vertical rhythm only — never tap targets or body type (Fitts/a11y preserved). Delivered as additive compact overrides (no refactor of comfortable-mode CSS → zero regression risk).
New file assets/js/qwizz-density.js — reads localStorage['qwizzDensity'], toggles the html attribute, injects one segmented control (role=group, [data-qwizz-density-toggle] buttons, aria-pressed reflects active mode) at the first present anchor (.qwizz-builder__brand · .qwizz-dashboard__actions · .qwizz-settings__hero-actions · .wrap > h1.wp-heading-inline), labels from window.qwizzDensityI18n. Full source lives on the bench at that path.
Server wiring:
includes/Frontend/Shortcode.php(after the builder script enqueue, ~L142):wp_enqueue_script('qwizz-density', QUIZWIZZ_URL.'assets/js/qwizz-density.js', array(), QUIZWIZZ_VERSION, true);+wp_localize_script('qwizz-density','qwizzDensityI18n', array('label'=>__('Interface density','quizwizz'),'comfortable'=>__('Comfortable','quizwizz'),'compact'=>__('Compact','quizwizz')));includes/Plugin.phpenqueue_admin_shell()(~L159): same enqueue + localize (covers all admin pages).includes/Plugin.phpinit/boot(~L53):add_action('wp_head', array(self::class,'print_density_boot'), 1);+add_action('admin_head', array(self::class,'print_density_boot'), 1);includes/Plugin.phpnew method (no-FOUC boot):public static function print_density_boot(): void { echo '<script>try{var d=localStorage.getItem("qwizzDensity");if(d==="compact"){document.documentElement.setAttribute("data-qwizz-density","compact");}}catch(e){}</script>'; }
Control styling (appended): qwizz-builder.css (frontend, var(--qw-*) tokens) + qwizz-admin-shell.css (admin, literal #6d28d9/#dfe4ec/#f6f7f9) —
.qwizz-density{display:inline-flex;gap:2px;padding:2px;border:1px solid …;border-radius:10px;background:…}
.qwizz-density button{min-height:34-36px;padding:4-5px 11px;border:0;border-radius:8px;background:transparent;font-weight:700;cursor:pointer}
.qwizz-density button[aria-pressed="true"]{background:#fff;color:#6d28d9;box-shadow:0 1px 3px rgba(16,24,40,.12)}Compact overrides (appended per surface):
qwizz-builder.css:[data-qwizz-density="compact"] .qwizz-builder{padding-top:16px}·.qwizz-step__content{padding:12px}·.qwizz-step.is-active,.is-done{margin-bottom:10px}·.qwizz-actions{margin-top:10px}qwizz-player.css:.qwizz-player__top{padding:12px 18px}·.qwizz-player__form{padding:15px 15px}qwizz-finetune.css:.qwizz-editor__questions{padding:8px 12px 40px}qwizz-admin-dashboard.css:.qwizz-metric{min-height:54px;padding:7px 11px}·.qwizz-subject-grid>a{min-height:60px}qwizz-admin-settings-visual.css:.qwizz-settings .qwizz-subcard{padding:9px}·.qwizz-panel__body{gap:9px}·.qwizz-panel>header{padding:10px 12px}
Acceptance (verified): control shows on Studio/Dashboard/Settings with comfortable pre-selected (aria-pressed); clicking Compact sets data-qwizz-density=compact + localStorage, computed .qwizz-step__content padding 17px→12px, settings subcard padding tightens; reload persists the attribute at DCL (boot, no comfortable flash); toggle-back clears it.
4 — Studio step-title weight (D1/S1 remainder)
qwizz-builder.css (append): .qwizz-step__heading h3{font-weight:700} — the active step’s question was 16px/400 (indistinct from body). Now 16px/700 while the CTA stays 800; verified computed 700.
5 — Dashboard metric weight + soft tiles (A1/D7)
qwizz-admin-dashboard.css (append): .qwizz-metric{background:var(--qw-soft)} + .qwizz-metric b{font-size:28px;font-weight:700;line-height:1.1} — numeral was 22px/600 on a transparent cell. Now 28px/700 on #f6f7f9 tiles (metric markup DashboardPage.php:169 <span><b>{n}</b>…). Verified computed 28px/700, bg rgb(246,247,249).
6 — Admin subject-tile juice (D8)
qwizz-admin-dashboard.css (append): .qwizz-subject-grid>a{transition:transform .14s ease,box-shadow .14s ease} + :hover{transform:translateY(-1px);box-shadow:0 6px 18px rgba(16,24,40,.10)}. Guarded by the existing @media(prefers-reduced-motion:reduce){.qwizz-dashboard *{transition:none!important}} (:14). (Metric hover-lift intentionally skipped — clipped by .qwizz-dashboard__status{overflow:hidden}.)
7 — Settings IA: “Appearance” workspace (D6 / T1 / T3)
The “print” tab overloaded 3 panels (density cliff). New 7th workspace splits the shared/visual policy out of print.
settings-page.php:91— new nav buttondata-workspace-tab="appearance"(iconpalette, label “Appearance”, badge2, summary “Shared look: presentation policy and selectable themes.”) inserted between Player and Print.settings-page.php:116#qw-presentationand:162#qw-theme:data-workspace="print"→"appearance".#qw-pdf(:136) staysprint.settings-page.php:92Print nav button: badge3→1, summary → “Paper, banner and the exact pre-print flow.” (label “PDF & Brand” kept).qwizz-admin-settings-visual.css:131nav gridrepeat(6,minmax(0,1fr))→repeat(7,minmax(0,1fr))(responsive:1110repeat(3) /:1147repeat(2) unchanged).
Attribute-driven tabs = no settings-JS change. Result: general 1 · builder 1 · player 1 · appearance 2 · print 1 · ai 1 · advanced 2. Verified: 7-col nav fits with zero overflow (scrollWidth == clientWidth), Appearance shows Shared presentation + PDF theme, Print shows only PDF & Print.
8 — Player / library empty state (P3 / D9)
includes/Frontend/QuizEmbed.php unavailable() — from prose-only to icon + heading + copy + optional CTA:
$icon = Icons::icon_html( 'book', 30 );
$cta_url = (string) apply_filters( 'qwizz_library_cta_url', '' );
$cta = '' !== $cta_url ? '<a class="qwizz-library__empty-cta" href="' . esc_url( $cta_url ) . '">' . esc_html__( 'Browse quizzes', 'quizwizz' ) . '</a>' : '';
return '<div class="qwizz-library__empty" role="status"><span class="qwizz-library__empty-icon" aria-hidden="true">' . $icon . '</span><strong>' . esc_html__( 'Quiz unavailable', 'quizwizz' ) . '</strong><span>' . esc_html__( 'This quiz is not available right now. Choose another quiz, or ask the site owner to update this page.', 'quizwizz' ) . '</span>' . $cta . '</div>';CTA renders only when a host filters qwizz_library_cta_url (no broken default link). qwizz-embeds.css (append): .qwizz-library__empty becomes a centered card (icon chip, --qwizz-violet-soft bg fallback #f3eefe, ink heading, muted copy, gradient CTA). Verified live via a temp page: centered lavender card, book icon, bold “Quiz nicht verfügbar”, body copy, no CTA.
i18n: the reworded body string is new — translation-ready (
esc_html__) but has nodeentry yet; release owner adds it to the.po/.mo.
Deliberately not changed (assessed, no action)
- S4 step-1 scent:
stepSummary('content')(qwizz-builder.js:184-185) already returns a non-empty value (“Alle Kategorien” / joined category names); the continue button is gated on a category selection so the collapsed summary is never empty. The audit’s “1 Change” was stale. - D2 fine-tune hint: entry is already signposted — the PDF button relabels to “Review & print” when the editor feature is on (
qwizz-builder.js:785) and the dispatch is gated onconfig.editorAvailable(:1327), so there is no silent no-op path to caption. - A2 admin H1 weight: dashboard/settings H1 are 26px/400 = WordPress-native admin H1 weight; bumping would break consistency with core. Starter hero (marketing) stays distinct.
- KI/AI panel scent: the master toggle already carries gated-state copy (“Provider traffic and spending remain off until this is enabled…”).
- Extra micro-interactions (submit pending-state, player answer-select feedback, step-complete pop): optional future polish; core “juice” (unified brand, density, hierarchy, hover-lift) delivered.
Still open for the release owner (P2, unchanged from prior spec)
mPDF vendor prune (BE-1); PHP 8.1 floor + syntax gate (BE-2); GenerateController DI (BE-4/5); AI settle/release dedup (BE-6); WP-compat CI + “Tested up to” (BE-8).