feat: tighten the wordmark's letter-spacing for the panel

The logotype is drawn with wide spacing — handsome on a website, and a
third of the panel's header width spent on nothing. The spacing is
artwork, not typesetting, so no property adjusts it; but every pixel
column between two letters is blank, and a blank column carries no
shape. Dropping most of them moves the letters together without altering
a single letterform.

At the same 260px the capitals are now a third taller, which the
hairlines needed more than the spacing did: the A of ITA reads at a
glance instead of being rescued by a threshold.

Done at full resolution, before the downscale. Compressing columns that
are already an average of ink and paper would smear exactly the strokes
this logo can least afford to lose.

The rendered size is now read from the PNG's own header rather than kept
as a constant beside it. The spacing is adjustable and every value
changes the height; a number copied into the renderer would have drifted
and squashed the logo with nothing failing to say so.

`npm run brand:variants` renders the spacings side by side, magnified,
since this is a decision to be looked at rather than reasoned about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
2026-09-21 23:32:19 +02:00
co-authored by Claude Opus 5
parent 61074d02f9
commit 49f103c0a0
9 changed files with 266 additions and 42 deletions
+24 -12
View File
@@ -4,29 +4,41 @@ import path from 'node:path';
/**
* The 1-bit wordmark, inlined as a data URI.
*
* It is read from the committed `public/brand/logo-eink.png` and embedded in
* the SVG rather than linked: the renderer must not depend on the network, and
* an image the rasteriser silently failed to fetch would leave a hole in the
* shop window. Regenerate the file with `npm run brand`.
* Read from the committed `public/brand/logo-eink.png` and embedded in the
* SVG rather than linked: the renderer must not depend on the network, and an
* image the rasteriser silently failed to fetch would leave a hole in the shop
* window. Regenerate the file with `npm run brand`.
*
* Its dimensions are read from the file rather than kept as constants beside
* it. The letter-spacing of the wordmark is adjustable, and every adjustment
* changes the height — a number copied here would drift and squash the logo
* without anything failing.
*/
/** Matches the committed asset; see scripts/brand.ts. */
export const LOGO_WIDTH = 260;
export const LOGO_HEIGHT = 36;
export type Logo = { dataUri: string; width: number; height: number };
let cached: string | null | undefined;
let cached: Logo | null | undefined;
export async function loadLogoDataUri(): Promise<string | null> {
export async function loadLogo(): Promise<Logo | null> {
if (cached !== undefined) {
return cached;
}
try {
const file = path.join(process.cwd(), 'public', 'brand', 'logo-eink.png');
cached = `data:image/png;base64,${(await readFile(file)).toString('base64')}`;
const bytes = await readFile(file);
cached = {
dataUri: `data:image/png;base64,${bytes.toString('base64')}`,
...readPngSize(bytes),
};
} catch {
// Not fatal: the header falls back to the shop name set in the settings,
// which is worse-looking but still a working screen.
// Not fatal: the header falls back to the shop name from the settings,
// which is plainer but still a working screen.
cached = null;
}
return cached;
}
/** A PNG states its size in the IHDR chunk, at a fixed offset. */
export function readPngSize(bytes: Buffer): { width: number; height: number } {
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
}