diff --git a/nextjs-app/app/apple-icon.png b/nextjs-app/app/apple-icon.png
new file mode 100644
index 0000000..fa11243
Binary files /dev/null and b/nextjs-app/app/apple-icon.png differ
diff --git a/nextjs-app/app/apple-icon.tsx b/nextjs-app/app/apple-icon.tsx
deleted file mode 100644
index 8e03a8f..0000000
--- a/nextjs-app/app/apple-icon.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { ImageResponse } from 'next/og';
-import { markDataUri } from '@/components/Logo';
-
-// iOS ignores SVG touch icons — it needs a real raster, on an opaque ground,
-// with no transparency (it composites its own rounded mask). Previously this
-// pointed at favicon.svg, so add-to-home-screen produced a blank tile.
-//
-// Teal ground with the mark knocked out in white, matching the app icon in the
-// guideline's "brand in action" row.
-export const size = { width: 180, height: 180 };
-export const contentType = 'image/png';
-
-const TEAL = '#0F766E';
-
-export default function AppleIcon() {
- return new ImageResponse(
- (
-
- ),
- size
- );
-}
diff --git a/nextjs-app/app/icon.png b/nextjs-app/app/icon.png
new file mode 100644
index 0000000..6658290
Binary files /dev/null and b/nextjs-app/app/icon.png differ
diff --git a/nextjs-app/app/icon.svg b/nextjs-app/app/icon.svg
deleted file mode 100644
index 2a77b41..0000000
--- a/nextjs-app/app/icon.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/nextjs-app/app/opengraph-image.tsx b/nextjs-app/app/opengraph-image.tsx
index 1883f82..e3af9a6 100644
--- a/nextjs-app/app/opengraph-image.tsx
+++ b/nextjs-app/app/opengraph-image.tsx
@@ -1,7 +1,6 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { ImageResponse } from 'next/og';
-import { markDataUri } from '@/components/Logo';
/**
* The site had no og:image at all, so every link pasted into a class WhatsApp
@@ -28,10 +27,21 @@ async function font(file: string) {
return readFile(join(process.cwd(), 'assets', file));
}
+/**
+ * The mark, as a data URI. Satori has no access to the public/ URL space, so
+ * the artwork is read off disk and inlined. This is the same file the header
+ * serves — public/brand/mark.png — so the card can never drift from the site.
+ */
+async function markDataUri() {
+ const png = await readFile(join(process.cwd(), 'public', 'brand', 'mark.png'));
+ return `data:image/png;base64,${png.toString('base64')}`;
+}
+
export default async function OpengraphImage() {
- const [regular, bold] = await Promise.all([
+ const [regular, bold, mark] = await Promise.all([
font('Manrope-Regular.ttf'),
font('Manrope-Bold.ttf'),
+ markDataUri(),
]);
return new ImageResponse(
@@ -62,7 +72,7 @@ export default async function OpengraphImage() {
{/* Lockup */}
schoolcompare
diff --git a/nextjs-app/components/Footer.module.css b/nextjs-app/components/Footer.module.css
index 2f51b3b..1b271e3 100644
--- a/nextjs-app/components/Footer.module.css
+++ b/nextjs-app/components/Footer.module.css
@@ -47,13 +47,14 @@
.lockupMark {
display: inline-flex;
flex: 0 0 auto;
- width: 34px;
+ /* Height drives; the artwork is taller than it is wide. */
height: 34px;
}
-.lockupMark svg {
- width: 100%;
+.lockupMark img {
+ display: block;
height: 100%;
+ width: auto;
}
/* Same wordmark as the header, inverted: "school" in the band's foreground,
diff --git a/nextjs-app/components/Footer.tsx b/nextjs-app/components/Footer.tsx
index 55ff700..68f0ef1 100644
--- a/nextjs-app/components/Footer.tsx
+++ b/nextjs-app/components/Footer.tsx
@@ -25,7 +25,7 @@ export function Footer() {
*/}
-
+
schoolcompare
diff --git a/nextjs-app/components/Logo.tsx b/nextjs-app/components/Logo.tsx
index 7809df0..3301f13 100644
--- a/nextjs-app/components/Logo.tsx
+++ b/nextjs-app/components/Logo.tsx
@@ -1,107 +1,84 @@
/**
* The schoolcompare mark.
*
- * A location pin with a leaf growing inside it: where a school is, and a child
- * growing there. The pin's tail sweeps left as the guideline's "signature path
- * shape" — the journey a parent takes to find the right school — so the mark
- * and the illustration style share one gesture.
+ * This is the supplied brand artwork, not a reconstruction of it. Two
+ * colourways were extracted from the logo sheet and live in public/brand:
*
- * This is the single source for the mark. app/icon.svg, app/apple-icon.tsx and
- * app/opengraph-image.tsx all derive from the same geometry (see MARK_PATHS),
- * because the header and the favicon had previously drifted into two different
- * logos.
+ * mark.png the primary lockup's mark — teal pin, white window and
+ * path, green leaves. Reads correctly on Warm White,
+ * white cards and Sand, and on both dark-theme grounds.
+ * mark-on-dark.png the on-dark colourway — white pin with the counter
+ * knocked through to the ground. Required wherever the
+ * ground is teal, because there the teal pin's silhouette
+ * disappears and only the white window survives.
*
- * Drawn on a 48×48 grid. The pin takes the brand hue and the leaf is knocked
- * out of it, so the mark needs exactly two colours and works on any ground.
+ * `variant="auto"` serves the on-dark artwork to dark-theme viewers via a
+ * source, so this needs no JavaScript and no client boundary.
+ *
+ * The wordmark beside it is live text in Manrope rather than the sheet's
+ * raster: the written style guide specifies Manrope, and live text stays
+ * selectable, scales cleanly and recolours with the theme.
+ *
+ * RESOLUTION CAVEAT: the largest instance on the supplied sheet is 153×189,
+ * which is ample for the header (36px), the favicon and the share card, but
+ * short of a 512px PWA icon — that one is upscaled and is slightly soft. Drop
+ * a vector (SVG/AI/EPS) into public/brand and regenerate to fix it; every
+ * consumer goes through this component or public/brand, so it is one swap.
*/
-/** Geometry shared by every rendering of the mark, on a 0 0 48 48 viewBox. */
-export const MARK_PATHS = {
- /** The path tail, stroked — 8.5 wide with a round cap. */
- tail: 'M17 30 C15 35.6 11.6 40.2 7.8 42.8',
- /** The pin head. */
- head: { cx: 26.5, cy: 18.5, r: 16.5 },
- /** Leaf stem, stroked — 2.6 wide with a round cap. */
- stem: 'M26.5 29.5 C26.5 25 26.6 21 27 18',
- /** Upper leaf, filled. */
- leafUpper: 'M27 19.6 C28.2 13.8 32.4 9.8 37.6 9.4 C38 15.6 33.8 20.8 27 19.6 Z',
- /** Lower leaf, filled. */
- leafLower: 'M26.4 24.6 C25.2 20 21.2 17 16.4 17.1 C16.2 21.8 19.8 25.8 26.4 24.6 Z',
-} as const;
+const MARK_LIGHT = '/brand/mark.png';
+const MARK_ON_DARK = '/brand/mark-on-dark.png';
-/**
- * The mark as a standalone SVG string.
- *
- * Satori (next/og) renders `` with a data URI far more reliably than it
- * renders inline SVG children, so the generated icon and share card both draw
- * the mark this way — from the same geometry as the React component above,
- * which is the whole point of MARK_PATHS.
- */
-export function markSvg(pin: string, leaf: string): string {
- return [
- '',
- ].join('');
-}
-
-/** The same string as a data URI, ready for ``. */
-export function markDataUri(pin: string, leaf: string): string {
- return `data:image/svg+xml;base64,${Buffer.from(markSvg(pin, leaf)).toString('base64')}`;
-}
+/** Intrinsic aspect of the artwork, used to derive width from height. */
+const ASPECT = 153 / 189;
interface LogoMarkProps {
className?: string;
- /** Rendered size in px. Defaults to inheriting via CSS. */
+ /** Rendered height in px. Width follows the artwork's aspect ratio. */
size?: number;
- /** The pin. Defaults to the brand token so it inverts with the theme. */
- pin?: string;
- /** The leaf knocked out of the pin. */
- leaf?: string;
+ /**
+ * Which colourway to serve.
+ * - `auto` teal pin, swapping to the on-dark artwork in the dark theme
+ * - `onDark` always the white pin — for teal grounds, which are teal in
+ * both themes (the footer band)
+ */
+ variant?: 'auto' | 'onDark';
title?: string;
}
-export function LogoMark({
- className,
- size,
- pin = 'var(--brand)',
- leaf = 'var(--bg-card)',
- title,
-}: LogoMarkProps) {
+export function LogoMark({ className, size = 36, variant = 'auto', title }: LogoMarkProps) {
+ const height = size;
+ const width = Math.round(size * ASPECT);
+ const alt = title ?? '';
+
+ if (variant === 'onDark') {
+ return (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ );
+ }
+
return (
-
+
);
}
diff --git a/nextjs-app/components/Navigation.module.css b/nextjs-app/components/Navigation.module.css
index 3bea92b..b372b01 100644
--- a/nextjs-app/components/Navigation.module.css
+++ b/nextjs-app/components/Navigation.module.css
@@ -57,13 +57,15 @@
.logoIcon {
display: inline-flex;
flex: 0 0 auto;
- width: 36px;
- height: 36px;
+ /* The artwork is taller than it is wide (153:189), so height drives the box
+ and width follows. Constraining both would squash the pin. */
+ height: 38px;
}
-.logoIcon svg {
- width: 100%;
+.logoIcon img {
+ display: block;
height: 100%;
+ width: auto;
}
/*
diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx
index 62107a9..d579238 100644
--- a/nextjs-app/components/Navigation.tsx
+++ b/nextjs-app/components/Navigation.tsx
@@ -102,14 +102,13 @@ export function Navigation() {
{/*
- LogoMark's defaults are already correct for this ground: the pin
- takes var(--brand) and the leaf is knocked out in var(--bg-card),
- which is exactly the header's own background in both themes. No
- explicit pin/leaf needed here — the footer, which sits on the
- sunken teal band, does have to pass them.
+ variant="auto": the teal pin on the white header, swapping to the
+ on-dark artwork for dark-theme viewers. The footer has to force
+ onDark instead, because its band is teal in both themes and the
+ teal pin has no silhouette against it.
*/}
-
+
schoolcompare
diff --git a/nextjs-app/next.config.js b/nextjs-app/next.config.js
index 0139401..54474c0 100644
--- a/nextjs-app/next.config.js
+++ b/nextjs-app/next.config.js
@@ -81,9 +81,10 @@ const nextConfig = {
],
},
{
- // 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',
+ // The mark is now the supplied raster artwork, so the favicon is
+ // app/icon.png rather than an SVG. Pointing this at the old path was
+ // caching a 404.
+ source: '/icon.png',
headers: [
{
key: 'Cache-Control',
diff --git a/nextjs-app/public/brand/mark-on-dark.png b/nextjs-app/public/brand/mark-on-dark.png
new file mode 100644
index 0000000..d76e82b
Binary files /dev/null and b/nextjs-app/public/brand/mark-on-dark.png differ
diff --git a/nextjs-app/public/brand/mark.png b/nextjs-app/public/brand/mark.png
new file mode 100644
index 0000000..e02914f
Binary files /dev/null and b/nextjs-app/public/brand/mark.png differ
diff --git a/nextjs-app/public/icon-192.png b/nextjs-app/public/icon-192.png
index db38a01..cccb489 100644
Binary files a/nextjs-app/public/icon-192.png and b/nextjs-app/public/icon-192.png differ
diff --git a/nextjs-app/public/icon-512.png b/nextjs-app/public/icon-512.png
index 5fd5ab3..b5192da 100644
Binary files a/nextjs-app/public/icon-512.png and b/nextjs-app/public/icon-512.png differ
diff --git a/nextjs-app/public/icon-maskable-512.png b/nextjs-app/public/icon-maskable-512.png
index 82a913f..b3a22a6 100644
Binary files a/nextjs-app/public/icon-maskable-512.png and b/nextjs-app/public/icon-maskable-512.png differ
diff --git a/nextjs-app/public/manifest.json b/nextjs-app/public/manifest.json
index 7bfef57..639c5bf 100644
--- a/nextjs-app/public/manifest.json
+++ b/nextjs-app/public/manifest.json
@@ -8,9 +8,10 @@
"theme_color": "#0F766E",
"icons": [
{
- "src": "/icon.svg",
- "sizes": "any",
- "type": "image/svg+xml"
+ "src": "/icon.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "purpose": "any"
},
{
"src": "/icon-192.png",