From 1bd69e693a3922f48303f5754f4487afd0777d59 Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 2 Jul 2026 14:28:09 +0100 Subject: [PATCH] docs(audit): cross-cutting cohesion pass notes Co-Authored-By: Claude Fable 5 --- .../cohesion-pass.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-02-ux-audit-notes/cohesion-pass.md diff --git a/docs/superpowers/specs/2026-07-02-ux-audit-notes/cohesion-pass.md b/docs/superpowers/specs/2026-07-02-ux-audit-notes/cohesion-pass.md new file mode 100644 index 0000000..6afd52f --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-ux-audit-notes/cohesion-pass.md @@ -0,0 +1,148 @@ +# Cross-cutting cohesion pass — audit notes (Task 7) + +**Pages compared:** `/`, `/compare`, `/rankings`, `/admissions`, `/school/136916-the-castle-school` (secondary), `/school/146678-welland-primary-school` (primary) +**Viewport:** 1440×900 default; mobile 390×844 for the /compare 3-school check. +**Method:** Ran a `browser_evaluate` typography+colour collector on each of the six live pages (headings, body, buttons, links, chips — computed `fontSize/fontWeight/fontFamily`, `color/backgroundColor/borderRadius/padding`). Diffed the results into the tables below. For every ambiguous drift, grepped `nextjs-app/` (`app/globals.css` + `components/*.module.css`) to decide **token exists & bypassed (→ enforce)** vs **no token (→ create)**. +**Criterion used throughout:** Nielsen #4 "Consistency and standards" unless a WCAG SC is named. No taste-only findings — every claim below carries computed-style and/or source evidence. + +--- + +## Task attempt log + +Collected computed styles on all six pages in sequence, then verified against source. The dominant story is **two axes of drift**: (a) the two school-detail pages are *parallel component implementations* (`SchoolDetailView` vs `SecondarySchoolDetailView`) whose tokens have diverged; (b) a rich design-token system exists in `globals.css` (`--radius-*`, `--accent-*`) but is pervasively **bypassed with hardcoded px/hex** in the module CSS. + +### OPEN item — /compare with THREE same-phase schools at 390×844 +Loaded `/compare?urns=142161,113105,124613&metric=rwm_expected_pct` at 390×844 — three **primary** schools (St Mary & St Thomas CofE, Ottery St Mary, Trimley St Mary), same phase, confirmed via the "Primary (3) / Secondary (0)" phase tabs. Handling is sound: the three school cards **stack vertically** (each showing name, LA, type, and the selected metric value in its series colour — 77.0% teal, 51.0% coral, 44.0% blue), so no card is squeezed. The "Performance Over Time" chart canvas renders all three series (verified: canvas has drawn content — 9,720 non-white pixel samples in a 324×300 canvas; a full-page screenshot showed it momentarily blank, which is a Chart.js/`fullPage` capture artifact, not a real defect). The "Detailed Comparison" table becomes 4 columns (metric label + 3 schools) at 869px inside a 324px container: it scrolls **horizontally within its own `overflow-x:auto` wrapper**, and the page body itself does **not** overflow (`document.scrollWidth` = 390 = `window.innerWidth`). So a third same-phase school is legible and contained — no layout break. Only nit (already a general finding, not compare-specific): at 390px only the first school's column is visible without scrolling, so a 3-way visual scan of the table requires swiping. Screenshot: `cohesion-compare-3school-mobile.jpeg`. + +--- + +## Typography comparison (per page, desktop 1440) + +| Page | Page-title H1 | H1 family | Section H2/H3 | Notable body sizes | Off-scale / cross-page flags | +|---|---|---|---|---|---| +| `/` | **48px** /700 Playfair | Playfair | H2 28px & 21.6px; H3 16px & 20px Playfair | 16.8, 14.72, 14, 13.76, 13.12px | Search CTA button **20px** /600 (see F1); many near-dup body sizes | +| `/rankings` | **36px** /700 Playfair | Playfair | — | table TH 12px, TD 15px/16px | H1 smaller than home; table type 12/15 | +| `/compare` | **36px** /700 Playfair | Playfair | H2 **18px & 24px** Playfair (mixed) | table TH 12px, TD 15px | 3× **BUTTON 20px Arial** (off-family, F7); mixed H2 | +| `/admissions` | **44px** /700 Playfair | Playfair | H2 24px & 21.6px Playfair; **H3 15.2px /700 DM Sans** | 14.08, 14.4, 14, 13.12px | H3 rendered in **body font** not Playfair (F7); 3rd distinct H1 size | +| `/school/…primary` (Welland) | **52px** /700 Playfair | Playfair | H2 18px Playfair ×5; H3 14px DM Sans | table TH **11px**, TD **13px** | largest H1; table type 11/13 (F8) | +| `/school/…secondary` (Castle) | **52px** /700 Playfair | Playfair | H2 18px Playfair ×4; H3 14px DM Sans | table TH **11px**, TD **13px** | matches Welland (good); table 11/13 (F8) | + +**Diff summary:** Four distinct page-title sizes across five pages — **36 / 44 / 48 / 52px**. Tool pages (rankings, compare) agree on 36; the three "hero" pages (home 48, admissions 44, school 52) each pick a different size. Source: each is a **hardcoded `clamp()`** in its own module, not a shared token — home `.hero-title` `clamp(2rem,5vw,3.5rem)`; both school `.schoolName` `clamp(2rem,5vw,3.25rem)`; admissions title `2.75rem`. → **no page-title/hero token exists → CREATE.** Body copy shows a cloud of near-duplicate sizes (0.875/0.88/0.9/0.92rem → 14/14.08/14.4/14.72px) that no single scale explains. + +Fonts are otherwise disciplined: **DM Sans** body + **Playfair Display** display everywhere. Exceptions: (a) admissions/school **H3 elements render in DM Sans** while H2 stays Playfair (F7); (b) `Arial`/`Helvetica Neue` buttons+links on chart and map pages are **third-party** (Chart.js legend, Leaflet zoom/attribution) — noted, not a first-party defect. + +--- + +## Colour comparison (buttons / links / chips) + +| Role | Colour(s) observed | Token | Verdict | +|---|---|---|---| +| Primary CTA bg | `rgb(224,114,86)` `#e07256` everywhere (search, +Add School, active phase tab, btn-primary) | `--accent-coral` | Consistent ✓ | +| Primary CTA **hover** | `#c45a3f` (`btn-primary`) **vs `#d4654a`** (`.btn-compare:hover`, globals.css:754) | `--accent-coral-dark` = #c45a3f | **Near-dup / same-role different colour (F6)** — #d4654a hardcoded, bypasses token | +| Secondary / supporting | `rgb(45,125,125)` `#2d7d7d` (teal outline btn, teal chips, "Near me", info) | `--accent-teal` | Consistent ✓ | +| Nav active | coral text on `rgba(224,114,86,0.12)` tint | `--accent-coral-bg` | Consistent ✓ | +| Gold accent (admissions cross-link badge on school page) | `rgb(184,146,14)` `#b8920e` | tokens are `--accent-gold #c9a227` / `--accent-gold-text #7a6800` | **Third gold** — matches neither token (F6 family) | +| Chip series colours (countdown / SATs) | coral `#e07256`, teal `#2d7d7d`, blue chart-5 | `--chart-*` | Consistent ✓ (data encoding) | +| Map furniture links | `rgb(0,120,168)` `#0078a8` blue | none (Leaflet/OSM) | Third-party — off-palette blue leaks into school hero (see hero verdict) | + +**Cluster flags:** coral resolves to **three** values doing hover/pressed work — `#e07256`, `#c45a3f`, `#d4654a`; the middle two are the same semantic role (pressed coral) at different hex. Gold has a third off-token value `#b8920e`. Teal is clean. No different-role/same-colour collisions found (coral=primary, teal=secondary is held consistently). + +--- + +## Component variant table + +| Component / role | `/` | `/rankings` | `/compare` | `/admissions` | school (primary) | school (secondary) | Drift | +|---|---|---|---|---|---|---|---| +| Segmented / phase switcher | search-mode toggle: **rounded** (container radius-lg, btn radius-md), coral active | phaseTab: **radius 0**, coral fill, bordered | phaseTab: **radius 0** (matches rankings ✓) | sub-nav: **radius 0**, underline, teal active | section-nav pills radius 4/999 | tab btns radius 4 | **F5** — 3+ different treatments for "switch view/phase" | +| Primary "+Compare/+Add" button | btn-primary radius 8, pad 20×40 (F1) | btn radius 8, pad 12×24 | btn radius 8, pad 12×24 | — | btnAdd **radius 8, pad 12×20** | btnAdd **radius 6, pad 8×16** | **F1 / F2** | +| Back link | — | — | — | — | topBack coral, radius 0 | topBack coral, radius 0 | consistent ✓ | +| Data table type | — | TH 12 / TD 15 | TH 12 / TD 15 | — | TH **11** / TD **13** | TH **11** / TD **13** | **F8** | +| Small badge/pill | ofsted badge radius 4 | rank badge 50% | — | deadline chip radius 12 | SATs natPill radius **4**; nav pill 999 | badge radius **3**; att8 badge radius **3** | **F4** | +| Nav header / footer | identical | identical | identical | identical | identical | identical | **consistent ✓** | + +Radius scale audit across all `*.module.css`: **15 distinct raw-px radius values** in use — 4px(30×), 8px(28×), 999px(21×), 12px(19×), 3px(16×), 6px(14×), 10px(13×), 2px(8×), 16px(4×), 14px(3×), 9999px(2×), 1px(2×), 9px, 5px, 25px — against a token scale of only `--radius-sm/md/lg/xl` = 4/8/16/24. Tokens exist and are widely **bypassed**; there is **no pill token** for the 999/9999 values. + +--- + +## Friction points + +- **F1. Duplicate `.btn` rule set in `globals.css` — small-button padding is dead code, sizes drift** + - Evidence: `globals.css` defines `.btn` **twice** — line 151 (radius 6px, pad `0.5rem 1rem`, font 0.875rem, 1px border) and again line 1558 (radius `--radius-md`, pad `0.75rem 1.5rem`, font 0.9rem, `border:none`). `.btn-sm` (line 218, pad `0.3rem 0.625rem`) is declared *between* them, so the later `.btn` (equal specificity, source order wins) **overrides** it. Live proof: rankings `.btn.btn-sm` computes to padding **12px 24px**, not the intended 4.8×10px. Meanwhile the home search `.btn.btn-primary` computes to pad **20px 40px** / font 20px (a FilterBar override on top). + - Criterion violated: Nielsen #4; touch-target predictability. + - Argument: "small" buttons aren't small, and the base button geometry depends on which of two conflicting blocks wins — any future button edit has a 50/50 chance of hitting the dead rule. Silent, repo-wide. + - Source verdict: **token/rule conflict → ENFORCE** (dedupe to one `.btn` definition; restore `.btn-sm`). + - Severity: **P2**. + +- **F2. Two parallel school-detail components have drifted on the same controls** + - Evidence: `SchoolDetailView.module.css` vs `SecondarySchoolDetailView.module.css` implement the same UI with divergent hardcoded values: `.btnAdd` **radius 8px / pad 0.75rem 1.25rem** (primary) vs **radius 6px / pad 0.5rem 1rem** (secondary); national-average marker `.natPill` radius **4px** (primary) vs `.badge`/`.att8` radius **3px** (secondary); section-tab padding `4.8px 10px` vs `4.8px 12px`. Both files hardcode px rather than referencing `--radius-md`. + - Criterion violated: Nielsen #4. + - Argument: a parent moving from a primary school page to a secondary one (the compare flow explicitly mixes phases) meets the "+ Compare" button and nav tabs rendered at subtly different sizes/corners — the classic "two things that should be one" tax, and double the maintenance surface. + - Source verdict: token EXISTS (`--radius-md:8px`) but **bypassed → ENFORCE** (both should use the token; ideally share one component). + - Severity: **P2**. + +- **F3. Page-title (H1) sizing is unsystematic across page types** + - Evidence: H1 computes to **36px** (rankings, compare), **44px** (admissions), **48px** (home), **52px** (school). Source: each is a separate hardcoded `clamp()`/rem in its own module (home `clamp(2rem,5vw,3.5rem)`; school `clamp(2rem,5vw,3.25rem)`; admissions `2.75rem`); tool pages fall back to smaller local values. + - Criterion violated: Nielsen #4 (visual hierarchy consistency). + - Argument: page-to-page the "you are here" title jumps size with no rule a user could infer; hero pages don't even agree with each other. + - Source verdict: **no shared page-title/hero token → CREATE** (`--title-hero`, `--title-section`) and apply. + - Severity: **P2** (hierarchy), leaning P3 in isolation. + +- **F4. Radius scale is bypassed system-wide (15 raw values vs 4 tokens; no pill token)** + - Evidence: radius audit above — 3/5/6/9/10/12/14/25px and 999/9999px all appear hardcoded despite `--radius-sm/md/lg/xl`. Same-role badges differ (natPill 4 vs secondary badge 3; ofsted badge 4 vs att8 badge 3). + - Criterion violated: Nielsen #4. + - Argument: corner rounding is a primary "family resemblance" cue; with 15 values it reads as many hands, not one system. + - Source verdict: **mixed** — tokens exist for 4/8/16 (**ENFORCE**); pill radius has **no token → CREATE** `--radius-pill: 999px`. + - Severity: **P2**. + +- **F5. "Switch view / phase" control has 3+ different treatments** + - Evidence: home search-mode toggle is a **rounded** segmented control (container `--radius-lg`, coral active); rankings & compare phase tabs are **square** (radius 0) bordered coral-fill tabs; admissions sub-nav is an **underline** tab strip (radius 0, teal active). Same job, three shapes and two accent colours. + - Criterion violated: Nielsen #4. + - Argument: the segmented switch is a recurring interaction; users re-learn it on each page. (Rankings↔compare agreeing is the one bright spot.) + - Source verdict: **no shared segmented-control component → CREATE/CONSOLIDATE**. + - Severity: **P2**. + +- **F6. Near-duplicate accent colours for the same role** + - Evidence: pressed/hover coral is `--accent-coral-dark #c45a3f` on `.btn-primary` but a **hardcoded `#d4654a`** on `.btn-compare:hover` (globals.css:754); gold appears as `#b8920e` on the school-page admissions cross-link badge, matching neither `--accent-gold #c9a227` nor `--accent-gold-text #7a6800`. + - Criterion violated: Nielsen #4. + - Argument: two hovers for the same "coral button being pressed" is exactly the "two blues doing the same job" consistency defect. + - Source verdict: token EXISTS → **ENFORCE** (`--accent-coral-dark`); the off-token gold → ENFORCE `--accent-gold-text`. + - Severity: **P3**. + +- **F7. Heading font-family and off-family buttons break the type system locally** + - Evidence: on `/admissions` and both school pages, `H3` elements render in **DM Sans /700** while `H2` stays Playfair — an inconsistent semantic-heading treatment. Separately, `/compare` shows 3× `BUTTON` in **Arial 20px** and school pages show Arial/Helvetica-Neue controls. + - Criterion violated: Nielsen #4 (the H3 case). The Arial/Helvetica cases are **third-party** (Chart.js legend toggles, Leaflet zoom/attribution) — recorded as environmental, not a first-party fix. + - Argument: an H3 in body font reads as a bold paragraph, weakening the Playfair hierarchy the rest of the site sells. + - Source verdict: H3 font-family is set locally, no heading-family token discipline → **ENFORCE** Playfair for display headings (or intentionally reclass those H3s as labels). + - Severity: **P3**. + +- **F8. School-detail data tables use a smaller type scale than the shared data tables** + - Evidence: rankings & compare tables compute **TH 12px / TD 15px**; both school-detail tables compute **TH 11px / TD 13px**. + - Criterion violated: Nielsen #4; borderline WCAG 1.4.4 (13px data is small but resizable). + - Argument: the same kind of KS2 figures appear one size on compare and a size smaller on the school page — inconsistent, and the smaller variant is the one a parent studies most. + - Source verdict: table type is set per-component, **no shared table-type token → CREATE** and apply. + - Severity: **P3**. + +--- + +## Recent-additions verdicts (native vs bolted-on) + +- **Map-blended hero (`SchoolHeroMap` on school pages): mostly native, with a third-party seam.** The framing is on-brand — coral back-link, coral "+ Compare", cream surround, Playfair title over the map. But the embedded Leaflet layer imports **off-palette blue `#0078a8` attribution links and Arial zoom controls** straight into the hero (F7), the one place they're most visible. Verdict: **native design, bolted-on furniture** — worth restyling the Leaflet attribution/controls to the palette. +- **Characteristic chips (school rows + detail badges): native.** Tints use `--accent-teal-bg`/gold tints that belong to the palette, tone is quiet per the recent commit. Only blemish is radius drift (badge 3px vs natPill 4px, F4) — a token issue, not a stylistic mismatch. +- **Admissions cross-links: native in colour, inconsistent in treatment.** The teal `stepTool`/`navLink` links match the accent system, but the *same* "go to a tool" intent is a plain underlined **text link** on `/admissions` yet a **gold badge (`#b8920e`)** on the school page (F6) — and journey-5 already logged one such cross-link at a 39px tap target. Verdict: **native palette, slightly bolted-on** because the cross-link component isn't unified. + +--- + +## Works well — keep +- **Header + footer are pixel-identical on all six pages** (logo Playfair, coral nav-active tint, dark footer with faded links) — the strongest cohesion anchor on the site. +- **Coral = primary / teal = secondary** is held consistently for button and link roles (no role/colour collisions). +- **Rankings and compare phase tabs are genuinely shared** (radius 0, pad 10×24, coral active) — the model for what F5 should become everywhere. +- **Both school-detail H1s agree at 52px**, and the deadline countdown chip is byte-for-byte identical between the homepage widget and `/admissions` (radius 12px, pad 16px 17.6px 14.4px) — a correctly reused component. + +## Self-review +- No taste-only findings: every F cites either a computed-style diff (F1/F2/F3/F4/F6/F7/F8 all carry live px/hex) or a shared-component absence (F5), plus a criterion. +- Token-vs-no-token recorded for every source-checked finding: **ENFORCE** — F1, F2, F4(4/8/16), F6, F7; **CREATE** — F3 (hero title token), F4 (pill radius token), F5 (segmented-control component), F8 (table-type token). +- Third-party styling (Chart.js Arial, Leaflet blue/Arial) is explicitly separated from first-party defects rather than filed as findings. +- Severity spread: **P2 ×5** (F1, F2, F3, F4, F5), **P3 ×3** (F6, F7, F8). No P0/P1 — nothing blocks a task; the journey-5 contrast WCAG failure is already logged there and not re-filed here. + +## Screenshots (referenced, not committed) +- `cohesion-compare-3school-mobile.jpeg`: `/compare` at 390×844 with three same-phase primary schools — cards stack, chart renders, table scrolls within its container.