🩹 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_VERSION 1.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 already font-weight:800 (:667) and .qwizz-step__continue 800; qwizz-admin-settings-visual.css is 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

ChangeAreaFilesNature
Brand token unification (fork farm → one violet)tokens7 CSS filesvalue swaps
Public neutral convergencetokensqwizz-builder.css, qwizz-embeds.cssvalue swaps
Product-wide density toggle (comfortable/compact)featurenew assets/js/qwizz-density.js + 6 CSS + Plugin.php + Shortcode.phpadditive
Studio step-title weighthierarchyqwizz-builder.css+1 rule
Dashboard metric weight + soft tileshierarchyqwizz-admin-dashboard.css+2 rules
Admin subject-tile juice (hover-lift)juiceqwizz-admin-dashboard.css+2 rules (reduced-motion-guarded)
Settings IA: new “Appearance” workspaceIAsettings-page.php, qwizz-admin-settings-visual.cssmarkup moves + grid 6→7
Player/library empty state enrichmentempty-stateincludes/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:lineToken(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 from rgb(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#f5f1ff
  • qwizz-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.php enqueue_admin_shell() (~L159): same enqueue + localize (covers all admin pages).
  • includes/Plugin.php init/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.php new 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 button data-workspace-tab="appearance" (icon palette, label “Appearance”, badge 2, summary “Shared look: presentation policy and selectable themes.”) inserted between Player and Print.
  • settings-page.php:116 #qw-presentation and :162 #qw-theme: data-workspace="print""appearance". #qw-pdf (:136) stays print.
  • settings-page.php:92 Print nav button: badge 31, summary → “Paper, banner and the exact pre-print flow.” (label “PDF & Brand” kept).
  • qwizz-admin-settings-visual.css:131 nav grid repeat(6,minmax(0,1fr))repeat(7,minmax(0,1fr)) (responsive :1110 repeat(3) / :1147 repeat(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 no de entry 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 on config.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).