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
With the week down the right-hand side the main block is narrower, and
putting the French and English on one row set the two languages
competing for the width. In production that read as "Ouvre demain à" /
"11:00" on one side and the separator stranded on a second line with
"am" — visibly broken.
They are stacked now, French above English, which is already the pattern
in the banner, and each line is fitted to the width available rather
than allowed to wrap.
Found by rendering the real production payload rather than the sample
one: the sample had a short enough sentence to hide it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
A bottom strip gave each day a 114px column, and that width was what
capped the type size. A side band gives 142px of usable width and a 68px
row, so the hours go from 19pt to 22pt, the day labels from 20 to 23,
and each day fits on one line — "Mar 11:00–18:00" reads at a glance
where a column had to be read downwards.
Right rather than left, for two reasons the renders make plain: on the
left the logo is pushed into the middle and loses its place as a
signature, and the eye meets the week before the day's status. OUVERT is
what has to be read first from the pavement.
The times stay whole. Only the spaces around the dash go, which is
enough to hold 22pt without abbreviating anything: a shop's opening hour
is not a detail to shorten.
The idea came from the shop owner, and testing it surfaced a defect that
had been in production all along. Rendering the worst case the data
model allows — three periods in a day, none on a round hour — showed the
day's hours wrapping, pushing the layout down, and the English line of
the banner falling off the bottom edge. A panel does not scroll and does
not reflow: what does not fit is lost.
Type size is now fitted to the space available, with a floor below which
it will not go, and nothing may leave its box. The width estimate is
deliberately crude: measuring glyphs would mean loading the font in that
module and would still be an estimate, since the renderer applies its
own kerning. Erring small costs a point of type, erring large costs a
broken screen.
Three periods a day will reportedly never happen. The protection stays
anyway — it costs nothing and removes the need to remember.
`npm run screen:fonts` renders every arrangement and both alternative
typefaces, including the worst case, because choosing type for a 1-bit
panel is done by looking at it thresholded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
The application is published at horaires.ita-ito.com. One origin serves
both the panel and the browser, so the OIDC callback, the image URL the
device is handed and the documentation all follow from this single name.
Port 3000 is already taken by the facture_ocr stack on the development
machine, so the app listens on 3010 there and the reason is written next
to the setting rather than left to be rediscovered.
The fixtures and the frozen payload snapshot move to the real domain too:
a contract snapshot carrying a hostname that never existed is a small
puzzle left for whoever reads it next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
With the move to BYOS there is no TRMNL cloud to interpret a Liquid
template, so the application draws the screen itself.
The pipeline is satori (flexbox to SVG) then resvg (SVG to pixels) then
a hand-written 1-bit encoder. Chromium was the alternative and was
rejected: half a gigabyte and a real memory appetite in the runtime
image, for a screen of eight blocks. Playwright still handles the E2E
tests, in its own pinned image, never in the application one.
The payoff is testability. The layout is asserted on the element tree
and the geometry on the SVG text, so a rendering regression shows up as
a readable diff instead of a pixel comparison. The BMP and PNG encoders
are verified field by field against their specifications, including an
independent CRC-32 for the PNG: a device rejecting a malformed image is
expensive to debug from a shop window.
Two properties are pinned because the battery depends on them: the same
payload must produce byte-identical output, and changed hours must
produce different output. The filename handed to the device is a hash of
these bytes, and the firmware skips the redraw when it is unchanged.
The fonts are vendored into public/fonts and the logo into public/brand,
both committed. Rendering must not depend on an install tree, a CDN or
the network, or the bytes drift and the panel wakes for nothing.
`npm run screen:preview` writes a real 1-bit image plus a magnified view,
which is where clipping and thin strokes give themselves up. That is how
the week strip was caught clipping and condensed.
`npm run brand` rebuilds the assets: it locates the wordmark band in the
shop logo rather than hard-coding offsets a future revision would break,
and thresholds it with the panel's own encoder. sharp is a devDependency
used only there; nothing at runtime needs it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd