From 8c3a5cc4e9f551f0190d85357ce7741ad87f3a4c Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 6 Aug 2026 12:29:53 +0100 Subject: [PATCH] fix(design): keep below/attention off the brand hue, harden the share-card fonts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- nextjs-app/Dockerfile | 6 ++++++ nextjs-app/app/globals.css | 8 ++++++-- nextjs-app/components/AdmissionsView.module.css | 6 +++--- nextjs-app/components/DeltaChip.module.css | 4 ++-- nextjs-app/components/HomeView.module.css | 6 +++--- nextjs-app/components/HomeView.tsx | 2 +- nextjs-app/components/LeafletMapInner.tsx | 2 +- nextjs-app/components/SchoolCard.module.css | 4 ++-- nextjs-app/components/SchoolRow.module.css | 4 ++-- .../components/SecondarySchoolRow.module.css | 4 ++-- .../components/compare/compareSections.module.css | 8 ++++---- .../components/school/schoolSections.module.css | 14 +++++++------- nextjs-app/next.config.js | 13 ++++++++++++- 13 files changed, 51 insertions(+), 30 deletions(-) diff --git a/nextjs-app/Dockerfile b/nextjs-app/Dockerfile index 93d19e7..2f4665f 100644 --- a/nextjs-app/Dockerfile +++ b/nextjs-app/Dockerfile @@ -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 diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index 67ae427..7d4e77f 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -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. */ diff --git a/nextjs-app/components/AdmissionsView.module.css b/nextjs-app/components/AdmissionsView.module.css index f80a0c5..c4dc43d 100644 --- a/nextjs-app/components/AdmissionsView.module.css +++ b/nextjs-app/components/AdmissionsView.module.css @@ -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 { diff --git a/nextjs-app/components/DeltaChip.module.css b/nextjs-app/components/DeltaChip.module.css index 974270e..849679c 100644 --- a/nextjs-app/components/DeltaChip.module.css +++ b/nextjs-app/components/DeltaChip.module.css @@ -28,8 +28,8 @@ } .bad { - background: var(--brand-bg); - color: var(--brand-strong); + background: var(--status-below-bg); + color: var(--status-below); } .neutral { diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css index fffce2b..aec98f9 100644 --- a/nextjs-app/components/HomeView.module.css +++ b/nextjs-app/components/HomeView.module.css @@ -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 { diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index ae9c459..22a56c7 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -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)' }), diff --git a/nextjs-app/components/LeafletMapInner.tsx b/nextjs-app/components/LeafletMapInner.tsx index 0f0c488..5bfe7db 100644 --- a/nextjs-app/components/LeafletMapInner.tsx +++ b/nextjs-app/components/LeafletMapInner.tsx @@ -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] }; } diff --git a/nextjs-app/components/SchoolCard.module.css b/nextjs-app/components/SchoolCard.module.css index 8fb2897..c25dcf1 100644 --- a/nextjs-app/components/SchoolCard.module.css +++ b/nextjs-app/components/SchoolCard.module.css @@ -132,8 +132,8 @@ } .trendDown { - color: var(--brand); - background: var(--brand-bg); + color: var(--status-below); + background: var(--status-below-bg); } .trendStable { diff --git a/nextjs-app/components/SchoolRow.module.css b/nextjs-app/components/SchoolRow.module.css index fd4bed7..9a1ceb8 100644 --- a/nextjs-app/components/SchoolRow.module.css +++ b/nextjs-app/components/SchoolRow.module.css @@ -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 ──────────────────────────────────────────────── */ /* ofsted1–4 already defined above; these cover the two new framework states */ diff --git a/nextjs-app/components/SecondarySchoolRow.module.css b/nextjs-app/components/SecondarySchoolRow.module.css index 8ecccf5..3f0a7d0 100644 --- a/nextjs-app/components/SecondarySchoolRow.module.css +++ b/nextjs-app/components/SecondarySchoolRow.module.css @@ -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); } diff --git a/nextjs-app/components/compare/compareSections.module.css b/nextjs-app/components/compare/compareSections.module.css index fc5df84..f78afaf 100644 --- a/nextjs-app/components/compare/compareSections.module.css +++ b/nextjs-app/components/compare/compareSections.module.css @@ -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 { diff --git a/nextjs-app/components/school/schoolSections.module.css b/nextjs-app/components/school/schoolSections.module.css index fc781f8..d863e09 100644 --- a/nextjs-app/components/school/schoolSections.module.css +++ b/nextjs-app/components/school/schoolSections.module.css @@ -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) */ diff --git a/nextjs-app/next.config.js b/nextjs-app/next.config.js index c334147..0139401 100644 --- a/nextjs-app/next.config.js +++ b/nextjs-app/next.config.js @@ -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',