fix(home): stop implying we are official, and fix the mobile hero and search #96

Merged
tudor merged 1 commits from fix/hero-mobile-and-wording into main 2026-08-14 22:28:33 +00:00
6 changed files with 108 additions and 2 deletions

No files matched your search

+27
View File
@@ -121,6 +121,33 @@ test('the home page has a real title', async ({ page }) => {
expect(await page.title()).toMatch(/schoolcompare/i); expect(await page.title()).toMatch(/schoolcompare/i);
}); });
test('the site states its independence and never claims to be official', async ({ page }) => {
await page.goto('/');
// Carried in the footer, so it appears on every page rather than only here.
await expect(
page.getByText(/an independent site\.\s*not affiliated with the department for education/i)
).toBeVisible();
/*
* Describing the DATA as official is accurate and appears throughout
* ("official DfE figures", "the official figure isn't in"). Describing the
* SITE as official is not: schoolcompare republishes government data, it is
* not a government service. The difference is the grammatical subject, so
* this looks for text that opens with "Official" as a standalone label —
* the shape a value-prop title or heading takes, and the shape the old
* "Official & trusted" took.
*/
const claims = await page.evaluate(() =>
[...document.querySelectorAll('body *')]
.flatMap(el => [...el.childNodes]
.filter(n => n.nodeType === 3)
.map(n => (n.textContent ?? '').trim()))
.filter(t => /^Official\b/i.test(t) && !/^Official\s+(DfE|data|figures)/i.test(t))
);
expect(claims, `text presenting the site itself as official: ${claims.join(' | ')}`).toEqual([]);
});
test('the landing bands share one vertical rhythm', async ({ page }) => { test('the landing bands share one vertical rhythm', async ({ page }) => {
await page.goto('/'); await page.goto('/');
const gaps = await page.evaluate(() => { const gaps = await page.evaluate(() => {
@@ -460,6 +460,30 @@
} }
} }
/*
* Narrow phones: the button drops below the input instead of sharing the row.
*
* "Search schools" is a fixed 134px with `white-space: nowrap`, so on a 390px
* screen it took 48% of the row and left the input 102px of text space for a
* 194px placeholder — the field showed "School name or" and stopped. At 320px
* the input was down to 32px, which is too narrow to read what you are typing.
*
* Wrapping rather than restructuring: the pill stays one element, so it keeps
* its border, shadow and :focus-within ring, and the button gets a full-width
* tap target on the way. Giving the button a 100% flex-basis is what forces
* the wrap; the input keeps `flex: 1` and so takes the whole first row.
*/
@media (max-width: 480px) {
.heroMode .omniBoxContainer {
flex-wrap: wrap;
gap: 0.5rem;
}
.heroMode .searchButton {
flex: 1 1 100%;
width: 100%;
}
}
@media (max-width: 420px) { @media (max-width: 420px) {
/* Below this the pin costs more room than it earns. */ /* Below this the pin costs more room than it earns. */
.heroMode .omniIcon { .heroMode .omniIcon {
+16
View File
@@ -92,6 +92,22 @@
max-width: 34ch; max-width: 34ch;
} }
/*
* Quieter than .description, but by size only — not by contrast.
*
* --on-sunken-faint measured 4.71:1 here, which clears AA by 0.21. That is a
* fine margin for decorative text and the wrong one for a disclaimer, whose
* whole job is to be legible to someone checking whether this is a government
* site. --on-sunken-muted is 5.66:1 on the same band.
*/
.independence {
margin: 0;
font-size: var(--step--2);
line-height: 1.6;
color: var(--on-sunken-muted);
max-width: 38ch;
}
.sectionTitle { .sectionTitle {
margin: 0; margin: 0;
font-family: var(--font-display); font-family: var(--font-display);
+8
View File
@@ -35,6 +35,14 @@ export function Footer() {
<p className={styles.description}> <p className={styles.description}>
Compare primary and secondary schools across England. Compare primary and secondary schools across England.
</p> </p>
{/*
Says once, plainly, what the landing page's value props only
imply: we publish official data, we are not an official body.
Cheap to state and expensive to be wrong about.
*/}
<p className={styles.independence}>
An independent site. Not affiliated with the Department for Education or Ofsted.
</p>
<a <a
href="mailto:contact@schoolcompare.co.uk" href="mailto:contact@schoolcompare.co.uk"
className={styles.link} className={styles.link}
+19 -1
View File
@@ -265,12 +265,30 @@
* the same decision. * the same decision.
*/ */
@media (max-width: 860px) { @media (max-width: 860px) {
/*
* Artwork above the copy, not below it.
*
* The DOM keeps .heroContent first so the desktop overlay does not depend on
* source order, which left the band stranded under the search on phones —
* reading as a strip stuck on the end rather than as a hero image. `order`
* moves it visually only; it is decorative and aria-hidden, so there is no
* reading order to disturb.
*
* It costs about 136px above the search. Measured on a 667px viewport (the
* shortest phone still in use) the search still lands near 400px, well
* inside the fold.
*/
.heroPanel {
display: flex;
flex-direction: column;
}
.heroContent { .heroContent {
padding: 2rem 1.5rem 1.75rem; padding: 1.75rem 1.5rem 1.75rem;
max-width: none; max-width: none;
} }
.heroArt { .heroArt {
position: static; position: static;
order: -1;
height: 11rem; height: 11rem;
} }
/* The band crop centres the schoolhouse at about 65% across, so squeezing /* The band crop centres the schoolhouse at about 65% across, so squeezing
+14 -1
View File
@@ -157,7 +157,20 @@ const VALUE_PROPS: ValueProp[] = [
{ {
icon: <ShieldCheckIcon />, icon: <ShieldCheckIcon />,
tintClass: styles.propIconTrust, tintClass: styles.propIconTrust,
title: 'Official & trusted', /*
* "Built on official data", never "Official".
*
* The previous title was "Official & trusted", whose grammatical subject
* is this site — it reads as a claim that schoolcompare is itself an
* official service. It is not: it is an independent site that republishes
* official figures. The distinction is the difference between describing
* the data and describing ourselves, and only the first is true.
*
* Everywhere else the word appears ("official DfE figures", "the official
* figure isn't in") it already qualifies the data, which is correct and
* should stay.
*/
title: 'Built on official data',
body: 'Every figure comes from DfE performance tables and Ofsted.', body: 'Every figure comes from DfE performance tables and Ofsted.',
}, },
{ {