diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css
index 779695a..4456fb4 100644
--- a/nextjs-app/components/SchoolDetailView.module.css
+++ b/nextjs-app/components/SchoolDetailView.module.css
@@ -756,38 +756,26 @@
margin-top: 0.15rem;
}
-/* Tone variants — keep consistent with .ofstedGrade{N} / .rcGrade{N} */
-.heroChip.ofstedGrade1,
-.heroChip.rcGrade1,
-.heroChip.rcGrade2,
-.heroChipGood {
- background: var(--accent-teal-bg, rgba(45, 125, 125, 0.08));
- border-left-color: var(--accent-teal, #2d7d7d);
+/* Hero tone scheme — independent of the .ofstedGrade{N} / .rcGrade{N} badges
+ so the same tone class can be applied to a chip (background tint + border)
+ or a serif number (colour only) without one bleeding into the other. */
+.tone-teal { --hero-tone: var(--accent-teal, #2d7d7d); }
+.tone-green { --hero-tone: #3c8c3c; }
+.tone-gold { --hero-tone: var(--accent-gold, #c9a227); }
+.tone-coral { --hero-tone: var(--accent-coral, #e07256); }
+.tone-neutral { --hero-tone: var(--text-muted, #8a847a); }
+
+.heroChip.tone-teal,
+.heroChip.tone-green,
+.heroChip.tone-gold,
+.heroChip.tone-coral,
+.heroChip.tone-neutral {
+ border-left-color: var(--hero-tone);
+ background: color-mix(in srgb, var(--hero-tone) 10%, var(--bg-card, white));
}
-.heroChip.ofstedGrade2 {
- background: rgba(60, 140, 60, 0.08);
- border-left-color: #3c8c3c;
-}
-
-.heroChip.ofstedGrade3,
-.heroChip.rcGrade3,
-.heroChipWarn {
- background: var(--accent-gold-bg, rgba(201, 162, 39, 0.10));
- border-left-color: var(--accent-gold, #c9a227);
-}
-
-.heroChip.ofstedGrade4,
-.heroChip.rcGrade4,
-.heroChip.rcGrade5 {
- background: var(--accent-coral-bg, rgba(224, 114, 86, 0.10));
- border-left-color: var(--accent-coral, #e07256);
-}
-
-.heroChip.heroChipNeutral {
+.heroChip.tone-neutral {
background: var(--bg-secondary, #f3ede4);
- border-left-color: var(--border-color, #e5dfd5);
- color: var(--text-muted, #8a847a);
}
/* ── Hero at-a-glance stats (A3) ─────────────────────────────────────── */
@@ -824,15 +812,13 @@
color: var(--text-primary, #1a1612);
}
-.heroStatNumberSerif.ofstedGrade1 { color: var(--accent-teal, #2d7d7d); }
-.heroStatNumberSerif.ofstedGrade2 { color: #3c8c3c; }
-.heroStatNumberSerif.ofstedGrade3 { color: #b8920e; }
-.heroStatNumberSerif.ofstedGrade4 { color: var(--accent-coral, #e07256); }
-.heroStatNumberSerif.rcGrade1 { color: var(--accent-teal, #2d7d7d); }
-.heroStatNumberSerif.rcGrade2 { color: #3c8c3c; }
-.heroStatNumberSerif.rcGrade3 { color: #b8920e; }
-.heroStatNumberSerif.rcGrade4 { color: #c2410c; }
-.heroStatNumberSerif.rcGrade5 { color: var(--accent-coral, #e07256); }
+.heroStatNumberSerif.tone-teal,
+.heroStatNumberSerif.tone-green,
+.heroStatNumberSerif.tone-gold,
+.heroStatNumberSerif.tone-coral,
+.heroStatNumberSerif.tone-neutral {
+ color: var(--hero-tone);
+}
.heroStatLabel {
font-size: 0.6875rem;
diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx
index 596305c..5d9b910 100644
--- a/nextjs-app/components/SchoolDetailView.tsx
+++ b/nextjs-app/components/SchoolDetailView.tsx
@@ -213,7 +213,7 @@ export function SchoolDetailView({
{/* Hero signal chip strip */}
-
+
{ofstedHeroChip.title}
{ofstedHeroChip.subtitle}
{ofstedHeroChip.detail && (
@@ -222,7 +222,7 @@ export function SchoolDetailView({
{admissions?.oversubscribed && (
-
+
Oversubscribed
{admissions.first_preference_offer_pct != null
@@ -233,7 +233,7 @@ export function SchoolDetailView({
)}
{isPrimary && heroRwm != null && heroRwmNat != null && heroRwm > heroRwmNat && (
-
+
Above national average
Reading, Writing & Maths · {Math.round(heroRwm)} vs {Math.round(heroRwmNat)}
@@ -242,7 +242,7 @@ export function SchoolDetailView({
)}
{isSecondary && heroAtt8 != null && heroAtt8Nat != null && heroAtt8 > heroAtt8Nat && (
-
+
Above national average
Attainment 8 · {heroAtt8.toFixed(1)} vs {heroAtt8Nat.toFixed(1)}
@@ -276,7 +276,7 @@ export function SchoolDetailView({
{ofsted && (
-
+
{ofstedHeroChip.state === 'oeif'
? ofstedHeroChip.title.replace(/^Ofsted\s+/, '')
: ofstedHeroChip.state === 'reportCard'
diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts
index 9c64ba8..2291399 100644
--- a/nextjs-app/lib/utils.ts
+++ b/nextjs-app/lib/utils.ts
@@ -423,59 +423,73 @@ function formatOfstedMonth(date: string | null | undefined): string {
return d.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
}
+export type HeroTone = 'teal' | 'green' | 'gold' | 'coral' | 'neutral';
+
export interface OfstedHeroChip {
state: 'oeif' | 'reportCard' | 'none';
title: string; // Main label (e.g. "Ofsted Outstanding", "Ofsted Report Card")
subtitle: string; // Context line (e.g. "Inspected November 2023")
detail?: string; // Optional extra line (e.g. "Safeguarding: Met")
- toneClass: string; // CSS class key — matches .ofstedGrade{N} / .rcGrade{N} / neutral
+ tone: HeroTone; // Maps to dedicated hero tone classes (not badge classes)
}
/**
* Build the hero-strip Ofsted chip, branching on the inspection framework.
* Never synthesises a single overall grade for ReportCard schools.
+ *
+ * Note: the API may return ``framework`` as a literal string ``"NULL"`` for
+ * older inspections, so we explicitly only branch into the ReportCard layout
+ * when the value is exactly ``"ReportCard"``. Anything else with an
+ * ``overall_effectiveness`` score is treated as OEIF.
*/
export function buildOfstedHeroChip(ofsted: OfstedInspection | null | undefined): OfstedHeroChip {
- if (!ofsted || !ofsted.framework) {
+ if (!ofsted) {
return {
state: 'none',
title: 'Ofsted pending',
subtitle: 'No inspection on record',
- toneClass: 'heroChipNeutral',
+ tone: 'neutral',
};
}
const when = formatOfstedMonth(ofsted.inspection_date);
- if (ofsted.framework === 'OEIF') {
- const grade = ofsted.overall_effectiveness;
- if (grade && OFSTED_OEIF_WORDS[grade]) {
- return {
- state: 'oeif',
- title: `Ofsted ${OFSTED_OEIF_WORDS[grade]}`,
- subtitle: when ? `Inspected ${when}` : 'Inspected',
- toneClass: `ofstedGrade${grade}`,
- };
- }
+ // ReportCard branch — only if the API explicitly says so
+ if (ofsted.framework === 'ReportCard') {
+ const safeguarding = ofsted.rc_safeguarding_met;
return {
- state: 'oeif',
- title: 'Ofsted inspected',
- subtitle: when ? `Inspected ${when}` : 'Inspection on record',
- toneClass: 'heroChipNeutral',
+ state: 'reportCard',
+ title: 'Ofsted Report Card',
+ subtitle: when ? `Inspected ${when}` : 'New framework inspection',
+ detail:
+ safeguarding == null
+ ? undefined
+ : safeguarding ? 'Safeguarding: Met' : 'Safeguarding: Not met',
+ tone: safeguarding === false ? 'coral' : 'green',
+ };
+ }
+
+ // Otherwise treat as OEIF (covers framework === 'OEIF', null, "NULL", etc.)
+ const grade = ofsted.overall_effectiveness;
+ if (grade && OFSTED_OEIF_WORDS[grade]) {
+ const oeifTone: HeroTone =
+ grade === 1 ? 'teal' :
+ grade === 2 ? 'green' :
+ grade === 3 ? 'gold' :
+ 'coral';
+ return {
+ state: 'oeif',
+ title: `Ofsted ${OFSTED_OEIF_WORDS[grade]}`,
+ subtitle: when ? `Inspected ${when}` : 'Inspected',
+ tone: oeifTone,
};
}
- // ReportCard — never fabricate an overall grade
- const safeguarding = ofsted.rc_safeguarding_met;
return {
- state: 'reportCard',
- title: 'Ofsted Report Card',
- subtitle: when ? `Inspected ${when}` : 'New framework inspection',
- detail:
- safeguarding == null
- ? undefined
- : safeguarding ? 'Safeguarding: Met' : 'Safeguarding: Not met',
- toneClass: safeguarding === false ? 'rcGrade5' : 'rcGrade2',
+ state: 'oeif',
+ title: 'Ofsted inspected',
+ subtitle: when ? `Inspected ${when}` : 'Inspection on record',
+ tone: 'neutral',
};
}