feat(design): adopt the Cohort identity — palette, type, mark and dark theme #86

Merged
tudor merged 2 commits from design/cohort-identity into main 2026-08-06 11:36:02 +00:00
13 changed files with 51 additions and 30 deletions
Showing only changes of commit 8c3a5cc4e9 - Show all commits
+6
View File
@@ -47,6 +47,12 @@ COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Fonts for the generated share card. File tracing already places these in
# .next/standalone/assets, so the COPY above carries them — this makes the
# dependency explicit rather than implicit in the tracer's behaviour, because
# a miss here is a silent 500 on /opengraph-image, not a build failure.
COPY --from=builder /app/assets ./assets
# Set correct permissions
RUN chown -R nextjs:nodejs /app
+6 -2
View File
@@ -6,10 +6,14 @@
* five-bar spread that appears inside every school row.
*
* Colour has exactly three jobs and they never borrow each other's hues:
* brand (iris) interactive + identity. Never means "good".
* status (teal / amber) above / below the comparison point. Never a CTA.
* brand (iris) interactive + identity. Never carries valence.
* status (teal / amber) valence — teal is above/good, amber is
* below/needs-attention. Never a CTA.
* phase (desaturated) category chips, deliberately subordinate.
*
* Escalate within status by weight, not by reaching for a new hue: a tinted
* amber chip for "requires improvement", a solid amber one for "inadequate".
*
* Teal/amber rather than green/red so the above/below signal survives every
* form of colour blindness — the audience includes parents reading closely.
*/
@@ -181,8 +181,8 @@
.chipOffer::before { background: var(--status-above); }
.chipUrgent {
border-color: rgba(var(--brand-rgb), 0.4);
background: rgba(var(--brand-rgb), 0.04);
border-color: rgba(var(--status-below-rgb), 0.4);
background: rgba(var(--status-below-rgb), 0.04);
}
.chipTrack {
@@ -218,7 +218,7 @@
.chipDeadline .chipDays,
.chipUrgent .chipDays {
color: var(--brand-strong);
color: var(--status-below);
}
.chipOffer .chipDays {
+2 -2
View File
@@ -28,8 +28,8 @@
}
.bad {
background: var(--brand-bg);
color: var(--brand-strong);
background: var(--status-below-bg);
color: var(--status-below);
}
.neutral {
+3 -3
View File
@@ -1308,8 +1308,8 @@
}
.countdownChipUrgent {
border-color: rgba(var(--brand-rgb), 0.4);
background: rgba(var(--brand-rgb), 0.04);
border-color: rgba(var(--status-below-rgb), 0.4);
background: rgba(var(--status-below-rgb), 0.04);
}
.chipTrack {
@@ -1350,7 +1350,7 @@
.countdownChipDeadline .chipDays,
.countdownChipUrgent .chipDays {
color: var(--brand-strong);
color: var(--status-below);
}
.countdownChipOffer .chipDays {
+1 -1
View File
@@ -604,7 +604,7 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm
...(ofstedBadge.cssClass === 'ofsted1' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } :
ofstedBadge.cssClass === 'ofsted2' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } :
ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--status-below-bg)', color: 'var(--status-below)' } :
ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--brand-bg)', color: 'var(--brand)' } :
ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--status-below)', color: 'var(--text-inverse)' } :
ofstedBadge.cssClass === 'ofstedRc' ? { background: 'var(--phase-secondary-text)', color: 'var(--text-inverse)' } :
ofstedBadge.cssClass === 'ofstedPending' ? { background: 'var(--bg-secondary)', color: 'var(--text-muted)' } :
{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }),
+1 -1
View File
@@ -58,7 +58,7 @@ function buildPopupBadge(school: School): PopupBadge {
1: 'background:var(--status-above-bg);color:var(--status-above)',
2: 'background:var(--status-above-bg);color:var(--status-above)',
3: 'background:var(--status-below-bg);color:var(--status-below)',
4: 'background:var(--brand-bg);color:var(--brand)',
4: 'background:var(--status-below);color:var(--text-inverse)',
};
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
}
+2 -2
View File
@@ -132,8 +132,8 @@
}
.trendDown {
color: var(--brand);
background: var(--brand-bg);
color: var(--status-below);
background: var(--status-below-bg);
}
.trendStable {
+2 -2
View File
@@ -139,7 +139,7 @@
}
.trendUp { color: var(--status-above); }
.trendDown { color: var(--brand); }
.trendDown { color: var(--status-below); }
.trendStable { color: var(--text-muted); }
/* Line 4: location */
@@ -204,7 +204,7 @@
.ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
.ofsted2 { background: rgba(var(--status-above-rgb), 0.12); color: var(--status-above); }
.ofsted3 { background: var(--status-below-bg); color: var(--status-below); }
.ofsted4 { background: var(--brand-bg); color: var(--brand); }
.ofsted4 { background: var(--status-below); color: var(--text-inverse); }
/* ── Ofsted badge variants ──────────────────────────────────────────────── */
/* ofsted14 already defined above; these cover the two new framework states */
@@ -128,7 +128,7 @@
}
.deltaPositive { color: var(--status-above); }
.deltaNegative { color: var(--brand); }
.deltaNegative { color: var(--status-below); }
/* Line 4: location + distance */
.line4 {
@@ -207,7 +207,7 @@
.ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
.ofsted2 { background: rgba(var(--status-above-rgb), 0.12); color: var(--status-above); }
.ofsted3 { background: var(--status-below-bg); color: var(--status-below); }
.ofsted4 { background: var(--brand-bg); color: var(--brand); }
.ofsted4 { background: var(--status-below); color: var(--text-inverse); }
/* ── Ofsted badge variants ──────────────────────────────────────────────── */
.ofstedRc { background: var(--phase-secondary-text); color: var(--text-inverse); }
@@ -123,8 +123,8 @@
}
.chipBad {
background: var(--brand-bg);
color: var(--brand-strong);
background: var(--status-below-bg);
color: var(--status-below);
}
.chipNeutral {
@@ -151,8 +151,8 @@
}
.badgeBad {
background: var(--brand-bg);
color: var(--brand-strong);
background: var(--status-below-bg);
color: var(--status-below);
}
.rcList {
@@ -860,7 +860,7 @@
}
.progressNegative {
color: var(--brand-strong);
color: var(--status-below);
font-weight: 700;
}
@@ -876,8 +876,8 @@
}
.statusBad {
background: var(--brand-bg);
color: var(--brand-strong);
background: var(--status-below-bg);
color: var(--status-below);
}
/* Charts Section */
@@ -1025,8 +1025,8 @@
color: var(--status-below);
}
.ofstedGrade4 {
background: var(--brand-bg);
color: var(--brand-strong);
background: var(--status-below);
color: var(--text-inverse);
}
/* Report Card grade colours (5-level scale, lower = better) */
@@ -1047,8 +1047,8 @@
color: var(--status-below);
} /* Needs attention */
.rcGrade5 {
background: var(--brand-bg);
color: var(--brand-strong);
background: var(--status-below);
color: var(--text-inverse);
} /* Urgent improvement */
/* Safeguarding value (used inside a standard metricCard) */
+12 -1
View File
@@ -3,6 +3,15 @@ const nextConfig = {
// Enable standalone output for Docker
output: 'standalone',
// app/opengraph-image.tsx reads the Schibsted Grotesk files off disk at
// request time (Satori needs a font buffer; it has no system fallback).
// File tracing currently picks these up on its own, but that relies on the
// tracer resolving a runtime join() — declare them so a Next upgrade can't
// silently drop them and turn every link preview into a 500.
outputFileTracingIncludes: {
'/opengraph-image': ['./assets/**'],
},
// The /api/* and /sitemap.xml proxies to the FastAPI backend are route
// handlers (app/api/[...path]/route.ts, app/sitemap.xml/route.ts) rather
// than rewrites, so the backend host is read from FASTAPI_URL at runtime
@@ -72,7 +81,9 @@ const nextConfig = {
],
},
{
source: '/favicon.svg',
// favicon.svg was replaced by app/icon.svg (the mark now has one
// source in components/Logo.tsx); this rule was caching a 404.
source: '/icon.svg',
headers: [
{
key: 'Cache-Control',