fix(design): keep below/attention off the brand hue, harden the share-card fonts
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 1m8s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 1m8s
Review follow-up on #86. The blind coral -> brand rename recreated the exact collision this PR set out to remove: coral had been both the primary CTA and the "below average" signal, so every negative indicator followed --primary onto iris. Sixteen rules moved back onto the status ramp — delta chips, trend-down arrows, progress-negative values, statusBad, chipBad/badgeBad, and the urgent deadline chips. The Ofsted scale had also lost its worst step, with grade 4 landing on brand while 1-2 were teal and 3 was amber. It now escalates by weight rather than by reaching for another hue: a tinted amber chip for "requires improvement", a solid amber one for "inadequate" (5.1:1 light, 7.7:1 dark). Report-card grade 5 follows the same rule. globals.css now describes status as valence — teal above/good, amber below/needs-attention — which is what it has to mean for an urgent deadline, rather than the narrower "comparison point only" the first draft claimed. On the share-card fonts: /opengraph-image is prerendered, so the font read happens in the builder stage where assets/ exists, and the baked PNG ships inside .next/standalone/.next/server/app/. File tracing independently places the fonts at .next/standalone/assets, which the existing standalone COPY carries to /app/assets. So the reported ENOENT doesn't occur — but it depends on the tracer resolving a runtime join(), and a miss would be a silent 500 rather than a build failure. Declared outputFileTracingIncludes for the route and made the Dockerfile COPY explicit so neither is left to inference. Also repointed the immutable Cache-Control rule from the deleted favicon.svg to app/icon.svg, where it was caching a 404. Verified: tsc clean, 159/159 tests, clean rebuild prerenders all three image routes with the fonts present in standalone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,12 @@ COPY --from=builder /app/public ./public
|
|||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
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
|
# Set correct permissions
|
||||||
RUN chown -R nextjs:nodejs /app
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,14 @@
|
|||||||
* five-bar spread that appears inside every school row.
|
* five-bar spread that appears inside every school row.
|
||||||
*
|
*
|
||||||
* Colour has exactly three jobs and they never borrow each other's hues:
|
* Colour has exactly three jobs and they never borrow each other's hues:
|
||||||
* brand (iris) interactive + identity. Never means "good".
|
* brand (iris) interactive + identity. Never carries valence.
|
||||||
* status (teal / amber) above / below the comparison point. Never a CTA.
|
* status (teal / amber) valence — teal is above/good, amber is
|
||||||
|
* below/needs-attention. Never a CTA.
|
||||||
* phase (desaturated) category chips, deliberately subordinate.
|
* 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
|
* Teal/amber rather than green/red so the above/below signal survives every
|
||||||
* form of colour blindness — the audience includes parents reading closely.
|
* form of colour blindness — the audience includes parents reading closely.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -181,8 +181,8 @@
|
|||||||
.chipOffer::before { background: var(--status-above); }
|
.chipOffer::before { background: var(--status-above); }
|
||||||
|
|
||||||
.chipUrgent {
|
.chipUrgent {
|
||||||
border-color: rgba(var(--brand-rgb), 0.4);
|
border-color: rgba(var(--status-below-rgb), 0.4);
|
||||||
background: rgba(var(--brand-rgb), 0.04);
|
background: rgba(var(--status-below-rgb), 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chipTrack {
|
.chipTrack {
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
|
|
||||||
.chipDeadline .chipDays,
|
.chipDeadline .chipDays,
|
||||||
.chipUrgent .chipDays {
|
.chipUrgent .chipDays {
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chipOffer .chipDays {
|
.chipOffer .chipDays {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bad {
|
.bad {
|
||||||
background: var(--brand-bg);
|
background: var(--status-below-bg);
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
|
|
||||||
.neutral {
|
.neutral {
|
||||||
|
|||||||
@@ -1308,8 +1308,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.countdownChipUrgent {
|
.countdownChipUrgent {
|
||||||
border-color: rgba(var(--brand-rgb), 0.4);
|
border-color: rgba(var(--status-below-rgb), 0.4);
|
||||||
background: rgba(var(--brand-rgb), 0.04);
|
background: rgba(var(--status-below-rgb), 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chipTrack {
|
.chipTrack {
|
||||||
@@ -1350,7 +1350,7 @@
|
|||||||
|
|
||||||
.countdownChipDeadline .chipDays,
|
.countdownChipDeadline .chipDays,
|
||||||
.countdownChipUrgent .chipDays {
|
.countdownChipUrgent .chipDays {
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
|
|
||||||
.countdownChipOffer .chipDays {
|
.countdownChipOffer .chipDays {
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm
|
|||||||
...(ofstedBadge.cssClass === 'ofsted1' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } :
|
...(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 === 'ofsted2' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } :
|
||||||
ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--status-below-bg)', color: 'var(--status-below)' } :
|
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 === 'ofstedRc' ? { background: 'var(--phase-secondary-text)', color: 'var(--text-inverse)' } :
|
||||||
ofstedBadge.cssClass === 'ofstedPending' ? { background: 'var(--bg-secondary)', color: 'var(--text-muted)' } :
|
ofstedBadge.cssClass === 'ofstedPending' ? { background: 'var(--bg-secondary)', color: 'var(--text-muted)' } :
|
||||||
{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }),
|
{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }),
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function buildPopupBadge(school: School): PopupBadge {
|
|||||||
1: 'background:var(--status-above-bg);color:var(--status-above)',
|
1: 'background:var(--status-above-bg);color:var(--status-above)',
|
||||||
2: '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)',
|
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] };
|
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,8 +132,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trendDown {
|
.trendDown {
|
||||||
color: var(--brand);
|
color: var(--status-below);
|
||||||
background: var(--brand-bg);
|
background: var(--status-below-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trendStable {
|
.trendStable {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trendUp { color: var(--status-above); }
|
.trendUp { color: var(--status-above); }
|
||||||
.trendDown { color: var(--brand); }
|
.trendDown { color: var(--status-below); }
|
||||||
.trendStable { color: var(--text-muted); }
|
.trendStable { color: var(--text-muted); }
|
||||||
|
|
||||||
/* Line 4: location */
|
/* Line 4: location */
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
.ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
|
.ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
|
||||||
.ofsted2 { background: rgba(var(--status-above-rgb), 0.12); 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); }
|
.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 ──────────────────────────────────────────────── */
|
/* ── Ofsted badge variants ──────────────────────────────────────────────── */
|
||||||
/* ofsted1–4 already defined above; these cover the two new framework states */
|
/* ofsted1–4 already defined above; these cover the two new framework states */
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.deltaPositive { color: var(--status-above); }
|
.deltaPositive { color: var(--status-above); }
|
||||||
.deltaNegative { color: var(--brand); }
|
.deltaNegative { color: var(--status-below); }
|
||||||
|
|
||||||
/* Line 4: location + distance */
|
/* Line 4: location + distance */
|
||||||
.line4 {
|
.line4 {
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
.ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
|
.ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
|
||||||
.ofsted2 { background: rgba(var(--status-above-rgb), 0.12); 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); }
|
.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 ──────────────────────────────────────────────── */
|
/* ── Ofsted badge variants ──────────────────────────────────────────────── */
|
||||||
.ofstedRc { background: var(--phase-secondary-text); color: var(--text-inverse); }
|
.ofstedRc { background: var(--phase-secondary-text); color: var(--text-inverse); }
|
||||||
|
|||||||
@@ -123,8 +123,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chipBad {
|
.chipBad {
|
||||||
background: var(--brand-bg);
|
background: var(--status-below-bg);
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chipNeutral {
|
.chipNeutral {
|
||||||
@@ -151,8 +151,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badgeBad {
|
.badgeBad {
|
||||||
background: var(--brand-bg);
|
background: var(--status-below-bg);
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rcList {
|
.rcList {
|
||||||
|
|||||||
@@ -860,7 +860,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progressNegative {
|
.progressNegative {
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,8 +876,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.statusBad {
|
.statusBad {
|
||||||
background: var(--brand-bg);
|
background: var(--status-below-bg);
|
||||||
color: var(--brand-strong);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Charts Section */
|
/* Charts Section */
|
||||||
@@ -1025,8 +1025,8 @@
|
|||||||
color: var(--status-below);
|
color: var(--status-below);
|
||||||
}
|
}
|
||||||
.ofstedGrade4 {
|
.ofstedGrade4 {
|
||||||
background: var(--brand-bg);
|
background: var(--status-below);
|
||||||
color: var(--brand-strong);
|
color: var(--text-inverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Report Card grade colours (5-level scale, lower = better) */
|
/* Report Card grade colours (5-level scale, lower = better) */
|
||||||
@@ -1047,8 +1047,8 @@
|
|||||||
color: var(--status-below);
|
color: var(--status-below);
|
||||||
} /* Needs attention */
|
} /* Needs attention */
|
||||||
.rcGrade5 {
|
.rcGrade5 {
|
||||||
background: var(--brand-bg);
|
background: var(--status-below);
|
||||||
color: var(--brand-strong);
|
color: var(--text-inverse);
|
||||||
} /* Urgent improvement */
|
} /* Urgent improvement */
|
||||||
|
|
||||||
/* Safeguarding value (used inside a standard metricCard) */
|
/* Safeguarding value (used inside a standard metricCard) */
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ const nextConfig = {
|
|||||||
// Enable standalone output for Docker
|
// Enable standalone output for Docker
|
||||||
output: 'standalone',
|
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
|
// The /api/* and /sitemap.xml proxies to the FastAPI backend are route
|
||||||
// handlers (app/api/[...path]/route.ts, app/sitemap.xml/route.ts) rather
|
// 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
|
// 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: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: 'Cache-Control',
|
key: 'Cache-Control',
|
||||||
|
|||||||
Reference in New Issue
Block a user