Copying the marketing logo did not fix the colour, because the marketing
artwork does not match --primary either. Measured, the mark held two
orange families: one already near brand at hsl(19,93%,52%) and an amber
cluster at hsl(28,98%,48%). The amber is what read as the old colour.
Regraded only the saturated orange fill onto hsl(21,90%,48%), preserving
shading, and left the tan body and dark figure alone. The rendered mark
now samples at hsl(22,89%,48%) against the text-primary "bee" beside it
at hsl(21,90%,48%).
The bigger problem was not colour. The source was a 500x500 canvas whose
artwork occupied a 387x319 box and only 24.7% of the pixels, drawn at
24px in the header. Roughly three quarters of that box rendered pure
white, so the mark was a faint smudge and whatever colour it carried was
barely visible. Cropped to the content on a square canvas.
The favicon carries the same regrade across all three of its frames.
Also fixes a latent flake in the RelativeTime component tests. They fed
the component dates derived from a hardcoded NOW while the component
reads the real clock, so "7 days ago" only held on 2026-07-18 and broke
the moment the date rolled over.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each key rendered a Card stripped of its border and shadow inside the
outer Card, buying markup and padding for no visual result. It is now a
plain row: the key glyph drops from 24px to 16px and reads as muted
rather than competing with the key name, the tinted code chip hugs the
key instead of stretching into a full-width bar, and the timestamps
share one line.
Removed the per-row "Active" badge. This list only ever contains active
keys, revoked ones live behind their own dialog, so the badge appeared
on every row and distinguished nothing. Say if you would rather keep it.
The loading skeleton now mirrors the loaded row, so the list no longer
resizes as it settles.
The logo and favicon were still the older lighter amber artwork while
the marketing site had been regraded to the deeper brand orange that
--primary already uses here. Same artwork, so both files are copied
across rather than regenerated, which keeps the two properties
byte-identical instead of merely similar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old screen was one 425-line component with correctness bugs, not just a
weak layout.
Fixed defects:
- Empty recipients were sent. handleSendBulkSMS mapped every parsed row into
the payload, so a blank phone cell produced a message addressed to "".
There was no filtering, no phone validation and no dedupe, so the same
number listed twice was texted twice.
- User-facing copy printed raw bytes: "max 1048576 bytes".
- The row limit could be wrong. maxRows fell back to 50 while the
subscription query was still loading, so an unlimited-plan user could be
told their file was too big. The cap is now only enforced once the real
limit is known.
- Steps were gated with opacity-50 + pointer-events-none, which leaves
controls in the tab order and unannounced. Locked steps now use `inert`.
- Data came from raw useQuery(['devices']) + devices?.data instead of the
typed useDevices(), the same shared-cache-key shape hazard that caused the
devices?.filter crash in round 1.
The new flow: upload (with parse summary and a preview table of the first 5
rows), map (auto-detected phone column plus live valid/invalid/duplicate
counts), compose (clickable variable chips, SMS segment counter, preview that
cycles through real recipients) and review (explicit count, an itemised list
of skipped rows and why, nothing sent until confirmed).
bulk-csv.ts holds the rules that decide who receives a message, as pure
functions with 24 unit tests. Those tests caught a real bug during
development: the short "to" hint substring-matched a column named "total",
which would have silently auto-selected the wrong column. Short hints are now
exact-match only.
Sample CSV at /samples/bulk-sms-sample.csv using 555 reserved-for-fiction US
and Canada numbers, so an unedited upload cannot text a real person, with an
order-confirmation template that demonstrates multi-variable interpolation.
E2e asserts the actual request payload contains exactly the two valid
recipients from a file seeded with a blank, an invalid and a duplicate row,
and that the parsed-CSV table does not reintroduce sideways scroll at 375px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>