2026-09-21 22:18:38 +01:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Similar Schools Nearby</ title >
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Manrope:wght@500;600;700&display=swap" rel = "stylesheet" >
< style >
/* Tokens copied verbatim from nextjs-app/app/(frontend)/globals.css so this
mockup cannot drift from the shipped palette. Light values first, dark
under prefers-color-scheme, both overridable by the theme switch. */
: root {
color-scheme : light ;
--bg-primary : #FAFAF8 ; --bg-secondary : #F5EFE6 ; --bg-card : #FFFFFF ;
--text-primary : #1C2731 ; --text-secondary : #4A5560 ; --text-muted : #5F6A75 ;
--border : #E5E7EB ; --border-strong : #D3D7DD ;
--brand : #0F766E ; --brand-strong : #0C5F58 ; --brand-bg : rgba ( 15 , 118 , 110 , .10 ); --brand-on : #FFFFFF ;
--action : #BE3C27 ; --action-strong : #A33320 ; --action-on : #FFFFFF ;
--sand : #F5EFE6 ; --mustard : #F2C94C ; --mustard-ink : #806200 ;
--font-display : Manrope , - apple-system , BlinkMacSystemFont , sans-serif ;
--font-ui : Inter , - apple-system , BlinkMacSystemFont , sans-serif ;
--radius-md : 8 px ; --radius-lg : 16 px ;
--shadow : 0 1 px 2 px rgba ( 28 , 39 , 49 , .06 ), 0 1 px 3 px rgba ( 28 , 39 , 49 , .05 );
}
@ media ( prefers-color-scheme : dark ) {
: root : not ([ data-theme = "light" ]) {
color-scheme : dark ;
--bg-primary : #111A20 ; --bg-secondary : #16222A ; --bg-card : #18242C ;
--text-primary : #E9EEF0 ; --text-secondary : #B4C2C7 ; --text-muted : #8B9AA1 ;
--border : #26343D ; --border-strong : #35454F ;
--brand : #5FC7BB ; --brand-strong : #7BD6CC ; --brand-bg : rgba ( 95 , 199 , 187 , .14 ); --brand-on : #0A1418 ;
--action : #F08A72 ; --action-strong : #F5A492 ; --action-on : #241009 ;
--sand : #1B2730 ; --mustard : #F2C94C ; --mustard-ink : #EFC658 ;
--shadow : 0 1 px 2 px rgba ( 0 , 0 , 0 , .3 ), 0 1 px 3 px rgba ( 0 , 0 , 0 , .25 );
}
}
: root [ data-theme = "dark" ] {
color-scheme : dark ;
--bg-primary : #111A20 ; --bg-secondary : #16222A ; --bg-card : #18242C ;
--text-primary : #E9EEF0 ; --text-secondary : #B4C2C7 ; --text-muted : #8B9AA1 ;
--border : #26343D ; --border-strong : #35454F ;
--brand : #5FC7BB ; --brand-strong : #7BD6CC ; --brand-bg : rgba ( 95 , 199 , 187 , .14 ); --brand-on : #0A1418 ;
--action : #F08A72 ; --action-strong : #F5A492 ; --action-on : #241009 ;
--sand : #1B2730 ; --mustard : #F2C94C ; --mustard-ink : #EFC658 ;
--shadow : 0 1 px 2 px rgba ( 0 , 0 , 0 , .3 ), 0 1 px 3 px rgba ( 0 , 0 , 0 , .25 );
}
* { box-sizing : border-box ; }
body {
margin : 0 ; padding : 32 px 16 px 80 px ; background : var ( -- bg - primary );
color : var ( -- text - primary ); font : 15 px / 1.55 var ( -- font - ui );
-webkit- font-smoothing : antialiased ;
}
. page { max-width : 960 px ; margin : 0 auto ; }
. page > header { margin-bottom : 28 px ; display : flex ; flex-wrap : wrap ; gap : 16 px ; align-items : flex-start ; justify-content : space-between ; }
. page > header h1 { font : 700 25 px / 1.25 var ( -- font - display ); letter-spacing : -.6 px ; margin : 0 0 6 px ; }
. page > header p { margin : 0 ; color : var ( -- text - muted ); font-size : 14 px ; max-width : 60 ch ; }
. theme-switch { border : 1 px solid var ( -- border - strong ); background : var ( -- bg - card ); color : var ( -- text - secondary ); border-radius : 999 px ; padding : 8 px 14 px ; font : 500 13 px var ( -- font - ui ); cursor : pointer ; min-height : 40 px ; }
. theme-switch : hover { border-color : var ( -- brand ); color : var ( -- brand ); }
/* ── The page context each variant is shown inside ─────────────────── */
. variant { margin-bottom : 40 px ; }
. variant > . context { padding : 0 4 px 12 px ; }
. variant . eyebrow { margin : 0 0 4 px ; font-size : 12 px ; letter-spacing : .04 em ; text-transform : uppercase ; color : var ( -- text - muted ); }
. variant . context h2 { font : 600 18 px / 1.35 var ( -- font - display ); margin : 0 ; color : var ( -- text - secondary ); }
. variant . note { margin : 10 px 4 px 0 ; font-size : 12.5 px ; color : var ( -- text - muted ); }
. variant . note b { color : var ( -- text - secondary ); font-weight : 600 ; }
/* ── The section itself — mirrors components/school/Section ────────── */
. card {
background : var ( -- bg - card ); border : 1 px solid var ( -- border );
border-radius : var ( -- radius - lg ); padding : 28 px ; box-shadow : var ( -- shadow );
}
. card > h2 { font : 700 22 px / 1.25 var ( -- font - display ); letter-spacing : -.4 px ; margin : 0 ; }
2026-09-21 22:29:07 +01:00
. lede { margin : 8 px 0 20 px ; color : var ( -- text - secondary ); font-size : 14.5 px ; max-width : 64 ch ; }
2026-09-21 22:18:38 +01:00
. grid { display : grid ; grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 14 px ; }
@ media ( max-width : 820px ) { . grid { grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr )); } }
@ media ( max-width : 560px ) { . grid { grid-template-columns : 1 fr ; } . card { padding : 20 px ; } body { padding-left : 16 px ; padding-right : 16 px ; } }
. school {
position : relative ; display : flex ; flex-direction : column ;
border : 1 px solid var ( -- border ); border-radius : var ( -- radius - md );
padding : 16 px ; background : var ( -- bg - card );
}
. school : has ( . add [ aria-pressed = "true" ]) { border-color : var ( -- brand ); background : var ( -- brand - bg ); }
. distance { display : flex ; align-items : center ; gap : 5 px ; font-size : 12 px ; color : var ( -- text - muted ); margin-bottom : 10 px ; }
. distance svg { width : 13 px ; height : 13 px ; flex : none ; }
. school h3 { font : 600 16 px / 1.35 var ( -- font - display ); margin : 0 0 6 px ; }
/* The whole card is the link target; the button sits above it on z-index so
it stays independently clickable. Same trick the search result cards use. */
. school h3 a { color : var ( -- text - primary ); text-decoration : none ; }
. school h3 a :: after { content : "" ; position : absolute ; inset : 0 ; border-radius : var ( -- radius - md ); }
. school : hover { border-color : var ( -- border - strong ); }
. school h3 a : hover { color : var ( -- brand ); text-decoration : underline ; }
. school h3 a : focus-visible { outline : none ; }
. school : has ( h3 a : focus-visible ) { outline : 2 px solid var ( -- brand ); outline-offset : 2 px ; }
. meta { margin : 0 0 12 px ; font-size : 12.5 px ; color : var ( -- text - muted ); }
. shared { display : flex ; flex-wrap : wrap ; gap : 6 px ; margin-bottom : 14 px ; }
. shared span {
font-size : 11.5 px ; line-height : 1.4 ; padding : 4 px 8 px ; border-radius : 999 px ;
background : var ( -- brand - bg ); color : var ( -- brand ); border : 1 px solid transparent ;
}
. shared span . loose { background : transparent ; color : var ( -- text - muted ); border-color : var ( -- border ); }
. metric { margin-top : auto ; padding-top : 13 px ; border-top : 1 px solid var ( -- border ); }
. value { font : 700 26 px / 1.1 var ( -- font - display ); letter-spacing : -.6 px ; }
. value . absent { font-size : 15 px ; font-weight : 600 ; color : var ( -- text - muted ); letter-spacing : 0 ; }
. metric . label { margin : 4 px 0 0 ; font-size : 12 px ; color : var ( -- text - secondary ); }
. metric . ref { margin : 2 px 0 0 ; font-size : 12 px ; color : var ( -- text - muted ); }
. add {
position : relative ; z-index : 1 ; margin-top : 14 px ; width : 100 % ; min-height : 40 px ;
font : 500 13 px var ( -- font - ui ); cursor : pointer ; border-radius : var ( -- radius - md );
border : 1 px solid var ( -- border - strong ); background : var ( -- bg - card ); color : var ( -- brand );
}
. add : hover { border-color : var ( -- brand ); background : var ( -- brand - bg ); }
. add [ aria-pressed = "true" ] { border-color : var ( -- brand ); background : var ( -- brand - bg ); font-weight : 600 ; }
. add : focus-visible { outline : 2 px solid var ( -- brand ); outline-offset : 2 px ; }
. footer {
display : flex ; flex-wrap : wrap ; align-items : center ; justify-content : space-between ;
gap : 14 px ; margin-top : 20 px ; padding-top : 18 px ; border-top : 1 px solid var ( -- border );
}
. footer p { margin : 0 ; font-size : 12.5 px ; color : var ( -- text - muted ); }
. footer strong { display : block ; font : 600 14 px var ( -- font - ui ); color : var ( -- text - primary ); }
/* Coral: the one decisive action in this section, and there is only one. */
. compare {
min-height : 44 px ; padding : 0 20 px ; border-radius : var ( -- radius - md ); cursor : pointer ;
font : 600 14 px var ( -- font - ui ); background : var ( -- action ); color : var ( -- action - on );
border : 1 px solid var ( -- action ); text-decoration : none ; display : inline-flex ; align-items : center ; gap : 8 px ;
}
. compare : hover { background : var ( -- action - strong ); border-color : var ( -- action - strong ); }
. compare [ aria-disabled = "true" ] { opacity : .45 ; pointer-events : none ; }
. compare : focus-visible { outline : 2 px solid var ( -- action ); outline-offset : 3 px ; }
. caption { margin : 18 px 0 0 ; font-size : 11.5 px ; color : var ( -- text - muted ); }
. sr { position : absolute ; width : 1 px ; height : 1 px ; padding : 0 ; margin : -1 px ; overflow : hidden ; clip : rect ( 0 0 0 0 ); white-space : nowrap ; border : 0 ; }
</ style >
</ head >
< body >
< div class = "page" >
< header >
< div >
< h1 > Similar schools nearby</ h1 >
< p > A new section on the school detail page. Fictional schools and figures; shipped
colour, type and section shell taken from < code > globals.css</ code > .</ p >
</ div >
< button class = "theme-switch" type = "button" id = "theme" > Dark theme</ button >
</ header >
< div id = "variants" ></ div >
</ div >
< script >
const PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>' ;
const variants = [
{
id : 'primary' ,
eyebrow : 'Variant 1 · Primary page, full intake match' ,
context : 'Meadowbrook Primary School — Ages 4– 11 · Mixed · No religious character · Community school' ,
lede : 'Other primary schools near Meadowbrook Primary School, with a similar intake.' ,
metric : 'Reading, writing & maths' ,
caption : 'Meeting the expected standard at key stage 2, 2025.' ,
thisValue : '72%' ,
note : 'Every card qualified at <b>tier 1</b>: same phase, same gender intake, same religious character, within 3 miles. The chips state exactly what is shared.' ,
schools : [
{ name : 'Willow Lane Primary School' , distance : '0.6' , meta : 'Community school · Ages 4– 11' , shared : [ 'Mixed' , 'No religious character' ], value : '74%' },
{ name : 'Oakfield Primary School' , distance : '1.1' , meta : 'Academy converter · Ages 3– 11' , shared : [ 'Mixed' , 'No religious character' ], value : '69%' },
{ name : 'Brookside Primary School' , distance : '1.8' , meta : 'Community school · Ages 4– 11' , shared : [ 'Mixed' , 'No religious character' ], value : 'Not published' },
],
},
{
id : 'secondary' ,
eyebrow : 'Variant 2 · Secondary page, mixed tiers' ,
context : 'Meadowbrook High School — Ages 11– 18 · Mixed · Non-selective · Academy' ,
lede : 'Other secondary schools near Meadowbrook High School, with a similar intake.' ,
metric : 'Attainment 8' ,
caption : 'Average GCSE attainment score across eight qualifications, 2025.' ,
thisValue : '51.2' ,
note : 'The third card only qualified once the religious-character preference was relaxed at <b>tier 2</b>. Its chip set says so by omission rather than claiming a match it does not have — and selectivity is never relaxed, so no grammar school can appear here.' ,
schools : [
{ name : 'Rivermead High School' , distance : '0.9' , meta : 'Academy converter · Ages 11– 18' , shared : [ 'Mixed' , 'Non-selective' , 'No religious character' ], value : '52.8' },
{ name : 'Oakfield Academy' , distance : '1.7' , meta : 'Academy sponsor led · Ages 11– 16' , shared : [ 'Mixed' , 'Non-selective' , 'No religious character' ], value : '49.6' },
{ name : 'St Aidan’ s Catholic High School' , distance : '2.4' , meta : 'Voluntary aided · Ages 11– 18' , shared : [ 'Mixed' , 'Non-selective' ], value : '53.4' },
],
},
{
id : 'sparse' ,
eyebrow : 'Variant 3 · Rural page, tier 3 fallback' ,
context : 'Little Ashby Church of England Primary School — Ages 4– 11 · Mixed · Church of England · Voluntary controlled' ,
lede : 'Other primary schools near Little Ashby Church of England Primary School.' ,
metric : 'Reading, writing & maths' ,
caption : 'Meeting the expected standard at key stage 2, 2025.' ,
thisValue : '66%' ,
note : 'Nothing matched on religious character within range. At <b>tier 3</b> the lede drops the phrase “with a similar intake”, the chips fall back to the plain phase, and only two schools are offered rather than padding to three. One school fewer than this and the section would not render at all.' ,
schools : [
{ name : 'Great Marden Primary School' , distance : '4.2' , meta : 'Community school · Ages 4– 11' , shared : [ 'Primary school' ], looseFrom : 0 , value : '71%' },
{ name : 'Ashby Vale Academy' , distance : '7.8' , meta : 'Academy converter · Ages 4– 11' , shared : [ 'Primary school' ], looseFrom : 0 , value : '58%' },
],
},
];
const selected = Object . fromEntries ( variants . map (( v ) => [ v . id , new Set ()]));
function schoolCard ( v , s , i ) {
const on = selected [ v . id ]. has ( i );
const chips = s . shared . map (( label , n ) => {
const loose = s . looseFrom !== undefined && n >= s . looseFrom ;
return `<span class=" ${ loose ? 'loose' : '' } "> ${ label } </span>` ;
}). join ( '' );
const absent = s . value === 'Not published' ;
return `
<article class="school">
<p class="distance"> ${ PIN }${ s . distance } miles away</p>
<h3><a href="#"> ${ s . name } </a></h3>
<p class="meta"> ${ s . meta } </p>
<div class="shared"> ${ chips } </div>
<div class="metric">
<p class="value ${ absent ? 'absent' : '' } "> ${ s . value } </p>
<p class="label"> ${ v . metric } </p>
<p class="ref"> ${ v . thisValue } at this school</p>
</div>
<button class="add" type="button" data-variant=" ${ v . id } " data-index=" ${ i } " aria-pressed=" ${ on } ">
${ on ? '✓ Added to compare' : '+ Add to compare' }
<span class="sr"> ${ s . name } </span>
</button>
</article>` ;
}
function render () {
document . getElementById ( 'variants' ). innerHTML = variants . map (( v ) => {
const count = selected [ v . id ]. size ;
return `
<section class="variant">
<div class="context">
<p class="eyebrow"> ${ v . eyebrow } </p>
<h2> ${ v . context } </h2>
</div>
<div class="card">
<h2>Similar schools nearby</h2>
<p class="lede"> ${ v . lede } </p>
<div class="grid"> ${ v . schools . map (( s , i ) => schoolCard ( v , s , i )). join ( '' ) } </div>
<div class="footer">
<p aria-live="polite">
<strong> ${ count ? ` ${ count } school ${ count === 1 ? '' : 's' } selected` : 'Compare side by side' } </strong>
${ count ? 'This school is included automatically.' : 'Add a school to compare it with this one.' }
</p>
<a class="compare" href="#" aria-disabled=" ${ count ? 'false' : 'true' } ">
${ count ? `Compare ${ count + 1 } schools` : 'Compare' } →
</a>
</div>
2026-09-21 22:29:07 +01:00
<p class="caption">Distances are straight-line from this school, not road distance.
${ v . caption } Fictional schools and figures for this mockup.</p>
2026-09-21 22:18:38 +01:00
</div>
<p class="note"> ${ v . note } </p>
</section>` ;
}). join ( '' );
}
document . getElementById ( 'variants' ). addEventListener ( 'click' , ( event ) => {
const button = event . target . closest ( '.add' );
if ( ! button ) return ;
const { variant , index } = button . dataset ;
const set = selected [ variant ];
const i = Number ( index );
set . has ( i ) ? set . delete ( i ) : set . add ( i );
render ();
document . querySelector ( `.add[data-variant=" ${ variant } "][data-index=" ${ index } "]` ). focus ();
});
const themeButton = document . getElementById ( 'theme' );
themeButton . addEventListener ( 'click' , () => {
const dark = document . documentElement . dataset . theme === 'dark' ;
document . documentElement . dataset . theme = dark ? 'light' : 'dark' ;
themeButton . textContent = dark ? 'Dark theme' : 'Light theme' ;
});
render ();
</ script >
</ body >
</ html >