diff --git a/nextjs-app/app/admissions/page.tsx b/nextjs-app/app/admissions/page.tsx
new file mode 100644
index 0000000..c4cad97
--- /dev/null
+++ b/nextjs-app/app/admissions/page.tsx
@@ -0,0 +1,14 @@
+import type { Metadata } from 'next';
+import { AdmissionsView } from '@/components/AdmissionsView';
+
+export const dynamic = 'force-static';
+
+export const metadata: Metadata = {
+ title: 'School Admissions Guide',
+ description:
+ 'Understand the Primary and Secondary school admissions process in England, with live countdowns to every key deadline and National Offer Day.',
+};
+
+export default function AdmissionsPage() {
+ return ;
+}
diff --git a/nextjs-app/components/AdmissionsView.module.css b/nextjs-app/components/AdmissionsView.module.css
new file mode 100644
index 0000000..6460ee6
--- /dev/null
+++ b/nextjs-app/components/AdmissionsView.module.css
@@ -0,0 +1,426 @@
+.page {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 0 1.25rem 4rem;
+}
+
+/* ─── Hero ───────────────────────────────────────────── */
+
+.hero {
+ text-align: center;
+ padding: 3rem 0 2rem;
+}
+
+.eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--accent-teal, #2d7d7d);
+ background: rgba(45, 125, 125, 0.1);
+ padding: 0.3rem 0.7rem;
+ border-radius: 999px;
+ margin-bottom: 1rem;
+}
+
+.eyebrowDot {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--accent-teal, #2d7d7d);
+}
+
+.heroTitle {
+ font-family: var(--font-playfair), 'Playfair Display', serif;
+ font-size: 2.75rem;
+ font-weight: 700;
+ line-height: 1.1;
+ letter-spacing: -0.015em;
+ color: var(--text-primary, #1a1612);
+ margin-bottom: 0.85rem;
+}
+
+.heroSub {
+ font-size: 1.05rem;
+ color: var(--text-secondary, #5c564d);
+ max-width: 640px;
+ margin: 0 auto;
+ line-height: 1.55;
+}
+
+/* ─── Countdown strip ────────────────────────────────── */
+
+.countdownSection {
+ padding: 0 0 2.5rem;
+ border-bottom: 1px solid var(--border-color, #e5dfd5);
+ margin-bottom: 2.5rem;
+}
+
+.stripHeader {
+ margin-bottom: 0.85rem;
+}
+
+.stripLabel {
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted, #6d685f);
+}
+
+.countdownRail {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.75rem;
+}
+
+.chip {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e5dfd5);
+ border-radius: 12px;
+ padding: 1rem 1.1rem 0.9rem;
+ box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06);
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+ position: relative;
+ overflow: hidden;
+}
+
+.chip::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ border-radius: 12px 12px 0 0;
+}
+
+.chipDeadline::before { background: var(--accent-coral, #e07256); }
+.chipOffer::before { background: var(--accent-teal, #2d7d7d); }
+
+.chipUrgent {
+ border-color: rgba(224, 114, 86, 0.4);
+ background: rgba(224, 114, 86, 0.04);
+}
+
+.chipTrack {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.6rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.15rem;
+}
+
+.chipTrackDeadline { color: var(--accent-coral, #e07256); }
+.chipTrackOffer { color: var(--accent-teal, #2d7d7d); }
+
+.chipTrackDot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: currentColor;
+ flex-shrink: 0;
+}
+
+.chipDays {
+ font-family: var(--font-playfair), 'Playfair Display', serif;
+ font-size: 2.6rem;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: -0.02em;
+}
+
+.chipDeadline .chipDays,
+.chipUrgent .chipDays {
+ color: var(--accent-coral-dark, #c45a3f);
+}
+
+.chipOffer .chipDays {
+ color: var(--accent-teal, #2d7d7d);
+}
+
+.chipDaysUnit {
+ font-size: 0.78rem;
+ font-weight: 500;
+ color: var(--text-muted, #6d685f);
+ margin-left: 0.2rem;
+ vertical-align: bottom;
+ line-height: 2;
+}
+
+.chipMilestone {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1612);
+ line-height: 1.25;
+ margin-top: 0.1rem;
+}
+
+.chipDate {
+ font-size: 0.75rem;
+ color: var(--text-muted, #6d685f);
+ margin-top: 0.05rem;
+}
+
+/* ─── Track (Secondary / Primary) ────────────────────── */
+
+.track {
+ margin-bottom: 3rem;
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e5dfd5);
+ border-radius: 14px;
+ overflow: hidden;
+}
+
+.trackHeader {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1.5rem;
+ padding: 1.75rem 2rem 1.5rem;
+ border-bottom: 1px solid var(--border-color, #e5dfd5);
+ flex-wrap: wrap;
+}
+
+.trackHeaderLeft {
+ flex: 1;
+ min-width: 0;
+}
+
+.trackKicker {
+ display: block;
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--accent-coral, #e07256);
+ margin-bottom: 0.35rem;
+}
+
+.trackTitle {
+ font-family: var(--font-playfair), 'Playfair Display', serif;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary, #1a1612);
+ margin-bottom: 0.4rem;
+ line-height: 1.2;
+}
+
+.trackSub {
+ font-size: 0.9rem;
+ color: var(--text-secondary, #5c564d);
+ line-height: 1.5;
+ margin: 0;
+}
+
+.trackDates {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ flex-shrink: 0;
+ text-align: right;
+}
+
+.trackDateRow {
+ display: flex;
+ flex-direction: column;
+ gap: 0.05rem;
+}
+
+.trackDateLabel {
+ font-size: 0.62rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted, #6d685f);
+}
+
+.trackDateVal {
+ font-size: 0.88rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1612);
+}
+
+/* ─── Timeline ───────────────────────────────────────── */
+
+.timeline {
+ list-style: none;
+ padding: 1.5rem 2rem 1.75rem;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.step {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.stepDotCol {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex-shrink: 0;
+ width: 20px;
+ padding-top: 0.15rem;
+}
+
+.stepDot {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--bg-card, #fff);
+ border: 2px solid var(--border-color, #e5dfd5);
+ flex-shrink: 0;
+ transition: border-color 0.2s ease, background 0.2s ease;
+}
+
+.stepDeadline .stepDot {
+ background: rgba(224, 114, 86, 0.15);
+ border-color: var(--accent-coral, #e07256);
+}
+
+.stepOffer .stepDot {
+ background: rgba(45, 125, 125, 0.15);
+ border-color: var(--accent-teal, #2d7d7d);
+}
+
+.stepLine {
+ width: 2px;
+ flex: 1;
+ min-height: 24px;
+ background: var(--border-color, #e5dfd5);
+ margin: 4px 0;
+}
+
+.stepContent {
+ padding-bottom: 1.5rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.step:last-child .stepContent {
+ padding-bottom: 0;
+}
+
+.stepDate {
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--text-muted, #6d685f);
+ margin-bottom: 0.2rem;
+}
+
+.stepDeadline .stepDate { color: var(--accent-coral, #e07256); }
+.stepOffer .stepDate { color: var(--accent-teal, #2d7d7d); }
+
+.stepTitle {
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: var(--text-primary, #1a1612);
+ margin-bottom: 0.3rem;
+ line-height: 1.3;
+}
+
+.stepDeadline .stepTitle { color: var(--accent-coral-dark, #c45a3f); }
+.stepOffer .stepTitle { color: var(--accent-teal, #2d7d7d); }
+
+.stepBody {
+ font-size: 0.88rem;
+ color: var(--text-secondary, #5c564d);
+ line-height: 1.55;
+ margin: 0;
+}
+
+/* ─── Tips ───────────────────────────────────────────── */
+
+.tips {
+ margin-top: 1rem;
+}
+
+.tipsHeading {
+ font-family: var(--font-playfair), 'Playfair Display', serif;
+ font-size: 1.35rem;
+ font-weight: 700;
+ color: var(--text-primary, #1a1612);
+ margin-bottom: 1.25rem;
+}
+
+.tipsGrid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+}
+
+.tipCard {
+ background: var(--bg-secondary, #f3ede4);
+ border-radius: 12px;
+ padding: 1.25rem;
+}
+
+.tipNumber {
+ font-family: var(--font-playfair), 'Playfair Display', serif;
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: var(--border-color, #e5dfd5);
+ line-height: 1;
+ margin-bottom: 0.6rem;
+}
+
+.tipHeading {
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: var(--text-primary, #1a1612);
+ margin-bottom: 0.4rem;
+ line-height: 1.3;
+}
+
+.tipBody {
+ font-size: 0.875rem;
+ color: var(--text-secondary, #5c564d);
+ line-height: 1.55;
+ margin: 0;
+}
+
+/* ─── Responsive ─────────────────────────────────────── */
+
+@media (max-width: 768px) {
+ .heroTitle {
+ font-size: 2rem;
+ }
+
+ .countdownRail {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .trackHeader {
+ flex-direction: column;
+ padding: 1.25rem 1.25rem 1rem;
+ }
+
+ .trackDates {
+ text-align: left;
+ flex-direction: row;
+ gap: 1.5rem;
+ }
+
+ .timeline {
+ padding: 1.25rem;
+ }
+
+ .tipsGrid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/nextjs-app/components/AdmissionsView.tsx b/nextjs-app/components/AdmissionsView.tsx
new file mode 100644
index 0000000..d8aaa03
--- /dev/null
+++ b/nextjs-app/components/AdmissionsView.tsx
@@ -0,0 +1,291 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import styles from './AdmissionsView.module.css';
+
+/* ─── Date helpers ─────────────────────────────────────── */
+
+function daysUntil(month: number, day: number): number {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const y = today.getFullYear();
+ let target = new Date(y, month - 1, day);
+ if (target <= today) target = new Date(y + 1, month - 1, day);
+ return Math.round((target.getTime() - today.getTime()) / 86_400_000);
+}
+
+function nextDate(month: number, day: number): Date {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const y = today.getFullYear();
+ const target = new Date(y, month - 1, day);
+ if (target <= today) return new Date(y + 1, month - 1, day);
+ return target;
+}
+
+function fmtDate(month: number, day: number): string {
+ return nextDate(month, day).toLocaleDateString('en-GB', {
+ weekday: 'short',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ });
+}
+
+/* ─── Data ─────────────────────────────────────────────── */
+
+interface Chip {
+ type: 'deadline' | 'offer';
+ track: string;
+ milestone: string;
+ month: number;
+ day: number;
+}
+
+const CHIPS: Chip[] = [
+ { type: 'offer', track: 'Primary · Offer Day', milestone: 'Primary National Offer Day', month: 4, day: 16 },
+ { type: 'deadline', track: 'Secondary · Deadline', milestone: 'Secondary applications close', month: 10, day: 31 },
+ { type: 'deadline', track: 'Primary · Deadline', milestone: 'Primary applications close', month: 1, day: 15 },
+ { type: 'offer', track: 'Secondary · Offer Day', milestone: 'Secondary National Offer Day', month: 3, day: 1 },
+];
+
+interface Step {
+ date?: string;
+ title: string;
+ body: string;
+ highlight?: 'deadline' | 'offer';
+}
+
+const SECONDARY_STEPS: Step[] = [
+ {
+ title: 'Check entry criteria',
+ body: 'Look at each school\'s admissions policy — catchment areas, faith criteria, sibling priority, and aptitude tests vary widely. Use school detail pages on SchoolCompare for admissions history.',
+ },
+ {
+ date: 'September',
+ title: 'Portal opens',
+ body: 'Your local council opens its online admissions portal. Register early to avoid last-minute technical issues. You apply through your home council even if you prefer schools in neighbouring boroughs.',
+ },
+ {
+ date: '31 October',
+ title: 'Application deadline',
+ body: 'Submit your ranked list of up to six schools. Councils treat all preferences equally — list schools in the genuine order you want them, not strategically.',
+ highlight: 'deadline',
+ },
+ {
+ date: '1 March',
+ title: 'National Offer Day',
+ body: 'Results are published online, usually from 12:01 am. You\'ll receive an email or letter with your allocated school.',
+ highlight: 'offer',
+ },
+ {
+ date: '~15 March',
+ title: 'Accept or decline',
+ body: 'Respond by the deadline your council gives — typically around 15 March. Accepting does not prevent you from keeping a place on a waiting list for a preferred school.',
+ },
+ {
+ title: 'Appeals',
+ body: 'If unsuccessful, you can appeal within 20 school days of the refusal letter. Secondary appeals consider whether prejudice to the school outweighs your case — success rates vary.',
+ },
+];
+
+const PRIMARY_STEPS: Step[] = [
+ {
+ title: 'Research entry criteria',
+ body: 'Faith schools, language units, and distance-based catchments differ by school. Start by reading each school\'s admissions policy on their website or the council\'s website.',
+ },
+ {
+ date: 'September',
+ title: 'Portal opens',
+ body: 'Apply through your home council\'s portal, even if your preferred school is in another borough. Most councils accept applications from September.',
+ },
+ {
+ date: '15 January',
+ title: 'Application deadline',
+ body: 'List up to 3–6 schools (the number varies by council) in genuine preference order. The equal preference rule means all preferences are considered before any offers are made.',
+ highlight: 'deadline',
+ },
+ {
+ date: '16 April',
+ title: 'National Offer Day',
+ body: 'Results are published online. Reception offers are sent on 16 April (or the next working day if that falls on a weekend or bank holiday).',
+ highlight: 'offer',
+ },
+ {
+ date: '~1 May',
+ title: 'Accept or decline',
+ body: 'Respond by your council\'s deadline, typically around 1 May. Accepting secures the place while you wait to see if a preferred school\'s waiting list moves.',
+ },
+ {
+ title: 'Appeals',
+ body: 'Infant class-size appeals (Reception to Year 2) have a very narrow legal test and a low success rate. For Year 3+, appeals follow the same process as secondary.',
+ },
+];
+
+interface Tip {
+ heading: string;
+ body: string;
+}
+
+const TIPS: Tip[] = [
+ {
+ heading: 'Equal preference rule',
+ body: 'Councils rank offers by your eligibility for each school, not by the order you listed them. You cannot game the system — put schools in the order you actually want them.',
+ },
+ {
+ heading: 'Late applications go to the back',
+ body: 'Submit before the deadline even if your child does not turn the required age until later in the year. Late applicants are only considered after all on-time applications.',
+ },
+ {
+ heading: 'Waiting lists',
+ body: 'You can go on waiting lists for multiple schools simultaneously. Lists are ordered by admissions criteria, not when you joined. They can move significantly over the summer.',
+ },
+];
+
+/* ─── Component ────────────────────────────────────────── */
+
+export function AdmissionsView() {
+ const [chipDays, setChipDays] = useState<(number | null)[]>(CHIPS.map(() => null));
+
+ useEffect(() => {
+ setChipDays(CHIPS.map(c => daysUntil(c.month, c.day)));
+ }, []);
+
+ return (
+
+
+ {/* Hero */}
+
+
+
+ England · Primary & Secondary
+
+ School Admissions Guide
+
+ Everything parents need to know about applying for a school place in England — from opening dates to National Offer Day, with live countdowns to every key milestone.
+
+
+
+ {/* Countdown strip */}
+
+
+ Days until next milestone
+
+
+ {CHIPS.map((chip, i) => {
+ const days = chipDays[i];
+ const isUrgent = days !== null && days <= 14;
+ const chipClass = [
+ styles.chip,
+ chip.type === 'deadline' ? styles.chipDeadline : styles.chipOffer,
+ isUrgent ? styles.chipUrgent : '',
+ ].filter(Boolean).join(' ');
+ return (
+
+
+
+ {chip.track}
+
+
+ {days ?? '—'}
+ {days !== null && days }
+
+
{chip.milestone}
+
{days !== null ? fmtDate(chip.month, chip.day) : ''}
+
+ );
+ })}
+
+
+
+ {/* Secondary track */}
+
+
+
+
Year 7 entry
+
Secondary school admissions
+
For children starting secondary school (Year 7) in September. Applications are submitted in the autumn of Year 6.
+
+
+
+ Deadline
+ {fmtDate(10, 31)}
+
+
+ Offer Day
+ {fmtDate(3, 1)}
+
+
+
+
+
+ {SECONDARY_STEPS.map((step, i) => (
+
+
+
+ {i < SECONDARY_STEPS.length - 1 &&
}
+
+
+ {step.date &&
{step.date}
}
+
{step.title}
+
{step.body}
+
+
+ ))}
+
+
+
+ {/* Primary track */}
+
+
+
+
Reception entry
+
Primary school admissions
+
For children starting Reception (Year R) in September. Applications are submitted in the autumn of the year before entry.
+
+
+
+ Deadline
+ {fmtDate(1, 15)}
+
+
+ Offer Day
+ {fmtDate(4, 16)}
+
+
+
+
+
+ {PRIMARY_STEPS.map((step, i) => (
+
+
+
+ {i < PRIMARY_STEPS.length - 1 &&
}
+
+
+ {step.date &&
{step.date}
}
+
{step.title}
+
{step.body}
+
+
+ ))}
+
+
+
+ {/* Tips */}
+
+ Three things most parents get wrong
+
+ {TIPS.map((tip, i) => (
+
+
{String(i + 1).padStart(2, '0')}
+
{tip.heading}
+
{tip.body}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/nextjs-app/components/Footer.tsx b/nextjs-app/components/Footer.tsx
index c327bf2..be11755 100644
--- a/nextjs-app/components/Footer.tsx
+++ b/nextjs-app/components/Footer.tsx
@@ -25,6 +25,7 @@ export function Footer() {
Search schools
Rankings
Compare shortlist
+ Admissions guide
diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css
index 05503a5..6627b8a 100644
--- a/nextjs-app/components/HomeView.module.css
+++ b/nextjs-app/components/HomeView.module.css
@@ -1173,3 +1173,156 @@
.loadMoreButton {
min-width: 160px;
}
+
+/* =========================================================
+ Admissions Countdown Strip
+ ========================================================= */
+
+.admissionsStrip {
+ padding: 1.5rem 0 2rem;
+ border-top: 1px solid var(--border-color, #e5dfd5);
+}
+
+.stripHeader {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.stripLabel {
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted, #6d685f);
+}
+
+.stripCta {
+ font-size: 0.82rem;
+ color: var(--accent-teal, #2d7d7d);
+ font-weight: 600;
+ text-decoration: none;
+}
+
+.stripCta:hover {
+ text-decoration: underline;
+}
+
+.countdownRail {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.75rem;
+}
+
+.countdownChip {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e5dfd5);
+ border-radius: 12px;
+ padding: 1rem 1.1rem 0.9rem;
+ box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06);
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+ position: relative;
+ overflow: hidden;
+}
+
+.countdownChip::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ border-radius: 12px 12px 0 0;
+}
+
+.countdownChipDeadline::before {
+ background: var(--accent-coral, #e07256);
+}
+
+.countdownChipOffer::before {
+ background: var(--accent-teal, #2d7d7d);
+}
+
+.countdownChipUrgent {
+ border-color: rgba(224, 114, 86, 0.4);
+ background: rgba(224, 114, 86, 0.04);
+}
+
+.chipTrack {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.6rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.15rem;
+}
+
+.chipTrackDeadline {
+ color: var(--accent-coral, #e07256);
+}
+
+.chipTrackOffer {
+ color: var(--accent-teal, #2d7d7d);
+}
+
+.chipTrackDot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: currentColor;
+ flex-shrink: 0;
+}
+
+.chipDays {
+ font-family: var(--font-playfair), 'Playfair Display', serif;
+ font-size: 2.6rem;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: -0.02em;
+}
+
+.countdownChipDeadline .chipDays,
+.countdownChipUrgent .chipDays {
+ color: var(--accent-coral-dark, #c45a3f);
+}
+
+.countdownChipOffer .chipDays {
+ color: var(--accent-teal, #2d7d7d);
+}
+
+.chipDaysUnit {
+ font-family: 'DM Sans', sans-serif;
+ font-size: 0.78rem;
+ font-weight: 500;
+ color: var(--text-muted, #6d685f);
+ margin-left: 0.2rem;
+ vertical-align: bottom;
+ line-height: 2;
+}
+
+.chipMilestone {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1612);
+ line-height: 1.25;
+ margin-top: 0.1rem;
+}
+
+.chipDate {
+ font-size: 0.75rem;
+ color: var(--text-muted, #6d685f);
+ margin-top: 0.05rem;
+}
+
+@media (max-width: 768px) {
+ .countdownRail {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx
index aedb624..3a017f7 100644
--- a/nextjs-app/components/HomeView.tsx
+++ b/nextjs-app/components/HomeView.tsx
@@ -24,6 +24,39 @@ interface HomeViewProps {
totalSchools?: number | null;
}
+function daysUntil(month: number, day: number): number {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const y = today.getFullYear();
+ let target = new Date(y, month - 1, day);
+ if (target <= today) target = new Date(y + 1, month - 1, day);
+ return Math.round((target.getTime() - today.getTime()) / 86_400_000);
+}
+
+function formatCountdownDate(month: number, day: number): string {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const y = today.getFullYear();
+ let target = new Date(y, month - 1, day);
+ if (target <= today) target = new Date(y + 1, month - 1, day);
+ return target.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric' });
+}
+
+interface CountdownChipData {
+ type: 'deadline' | 'offer';
+ track: string;
+ milestone: string;
+ month: number;
+ day: number;
+}
+
+const ADMISSIONS_CHIPS: CountdownChipData[] = [
+ { type: 'offer', track: 'Primary · Offer Day', milestone: 'Primary National Offer Day', month: 4, day: 16 },
+ { type: 'deadline', track: 'Secondary · Deadline', milestone: 'Secondary applications close', month: 10, day: 31 },
+ { type: 'deadline', track: 'Primary · Deadline', milestone: 'Primary applications close', month: 1, day: 15 },
+ { type: 'offer', track: 'Secondary · Offer Day', milestone: 'Secondary National Offer Day', month: 3, day: 1 },
+];
+
export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProps) {
const searchParams = useSearchParams();
const router = useRouter();
@@ -43,6 +76,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
const prevSearchParamsRef = useRef(searchParams.toString());
const [geoState, setGeoState] = useState<'idle' | 'requesting' | 'error'>('idle');
const [geoError, setGeoError] = useState(null);
+ const [chipDays, setChipDays] = useState<(number | null)[]>(ADMISSIONS_CHIPS.map(() => null));
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
const isLocationSearch = !!searchParams.get('postcode');
@@ -100,6 +134,11 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
.catch(() => {});
}, []);
+ // Compute admissions countdown days client-side to avoid SSR mismatch
+ useEffect(() => {
+ setChipDays(ADMISSIONS_CHIPS.map(c => daysUntil(c.month, c.day)));
+ }, []);
+
const handleLoadMore = async () => {
if (isLoadingMore || !hasMore) return;
setIsLoadingMore(true);
@@ -240,6 +279,47 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
)}
+ {/* Admissions countdown strip — only on landing page */}
+ {!isSearchActive && (
+
+
+
+ {ADMISSIONS_CHIPS.map((chip, i) => {
+ const days = chipDays[i];
+ const isUrgent = days !== null && days <= 14;
+ const chipClass = [
+ styles.countdownChip,
+ chip.type === 'deadline' ? styles.countdownChipDeadline : styles.countdownChipOffer,
+ isUrgent ? styles.countdownChipUrgent : '',
+ ].filter(Boolean).join(' ');
+ const trackClass = [
+ styles.chipTrack,
+ chip.type === 'deadline' ? styles.chipTrackDeadline : styles.chipTrackOffer,
+ ].join(' ');
+ return (
+
+
+
+ {chip.track}
+
+
+ {days ?? '—'}
+ {days !== null && days }
+
+
{chip.milestone}
+
+ {days !== null ? formatCountdownDate(chip.month, chip.day) : ''}
+
+
+ );
+ })}
+
+
+ )}
+
{/* How it works — only on landing page */}
{!isSearchActive && (
diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx
index 4c33a8c..0c4a18d 100644
--- a/nextjs-app/components/Navigation.tsx
+++ b/nextjs-app/components/Navigation.tsx
@@ -59,6 +59,12 @@ export function Navigation() {
>
Rankings
+
+ Admissions
+