Halo's look and the decisions behind it. Built with the Hallmark design skill (MIT), installed in this project only (.claude/skills/hallmark, via npx skills add nutlope/hallmark --agent claude-code -y). Every colour is in one file, src/styles/palette.css, imported by the app, the landing page, the consent page and the helper page, so a recolour is a single swap. Other tokens are in src/styles/tokens.css (app) and landing/tokens.css (landing page); Hallmark's record of the run is in .hallmark/picks.md and .hallmark/log.json.
The architecture and flow diagrams are in the diagrams; the build pipeline, including the Hallmark pass, is here.
- Who: adults with face blindness, often in a social moment, often one-handed, sometimes anxious. Many have low vision or read on a small screen in poor light.
- Feel: calm and kind, like a friend leaning in to remind you. An assistive tool people trust, never surveillance, and never a generic tech look.
- One memorable thing: the light halo that settles on a face you know. Everything else stays quiet so it can carry the product.
The source. Ajrakh is a block-printed cotton made in Kutch, in Gujarat, and across the border in Sindh. A length of cloth goes through many rounds of printing with carved wooden blocks, washing and dyeing. The dyes are few and come from plants and earth: indigo from the indigo plant, a deep red from madder root, and a black made by leaving scrap iron to rust with jaggery. Where the printer stamps a paste of clay and lime, the dye can't reach, so the cotton's own off-white survives as the pattern. The cloth reads as a few honest colours, worn soft, with the light parts held back on purpose.
How Halo uses it.
| Pigment | Light mode | Dark mode and the night | Use |
|---|---|---|---|
| Undyed cotton | paper oklch(96% 0.014 85), steps to oklch(89% 0.026 82) |
ink oklch(93.5% 0.016 85) |
The page by day; the text by night |
| Iron black | ink oklch(21% 0.014 50), muted oklch(44% 0.017 55) |
paper oklch(17% 0.012 50), the night oklch(15.5% 0.012 50) |
Text by day; the ground for dark mode, Whisper mode, pocket mode and the landing hero. A warm brown-black, not blue |
| Indigo | oklch(37% 0.105 266) filled, oklch(41% 0.12 266) as text and lines |
washed pale, oklch(78% 0.06 262) |
What you act on: the primary button, the Whisper lamp, links, focus rings, fader fills |
| Madder | oklch(47% 0.14 29) |
oklch(70% 0.12 33) |
The mark, like a maker's seal; serious things (errors, delete); "silent until you check your earbud" |
| Resist white | oklch(94% 0.03 85) |
The halo and the name tag in Whisper mode: a known face is the part of the cloth left light |
A small amount of moss green (oklch(46% 0.08 135)) marks success. There is no gradient wash, no neon and no blue-purple glow; depth still comes from light on machined parts, now lit in warm whites.
Why this and not the old palette. The first version used Hallmark's Ribbon colours: a warm white day, a blue-black night and an amber accent. Harsh found it generic. Asked again for a craft palette, Hallmark's theme.mjs deal --genre editorial with a prompt about a textile palette dealt Loom first ("fits: textile, craft, indigo"): ecru paper, warm near-black ink and one deep indigo. Halo takes Loom's colours as the base and adds the other two Ajrakh dyes, madder and iron black, from the source above. Type and parts stay Ribbon's (Instrument Sans over Atkinson Hyperlegible Next, machined controls), because this was a recolour, not a redesign. theme.mjs check --axes light/grotesk-sans/cool --hue 266 noted that Cobalt and Grid sit on the same axes; neither has a warm cotton paper, a brown-black night or a second pigment, so they don't read alike.
Before and after. Contact sheets of every screen in the old and new palettes: docs/shots/recolour/ (before-*.png, after-*.png).
Contrast. Checked with the WCAG formula for every pair the screens use: ink on cotton 15.8:1, muted on the deepest cotton step 5.6:1, cotton on indigo 9.6:1, indigo text on cotton 8.0:1, madder on cotton 6.5:1; in dark mode ink on iron 15.8:1, muted 6.8:1 or more, iron text on pale indigo 9.4:1, madder 6.8:1; on the night, the name tag's iron text on resist white 15.2:1. Axe finds no contrast problems on any page or screen (tests/e2e/a11y.spec.ts).
Not by colour alone. The three recognition states differ by line, label and sound, not only by colour: Confident is a solid ring (a resist-white core with a dark edge and a soft glow) with the name tag and the caption; Maybe is a dotted ring with "Could be Raj?" and no sound; No match is a faint dashed outline with "No match". Buzz codes differ by rhythm.
Whisper mode, pocket mode and the landing hero always sit on the night, whatever the mode. Higher contrast steps muted text and rules toward the ink.
- Instrument Sans 400 and 500 for headings and names (display weight 400, tracking -0.03em on display sizes).
- Atkinson Hyperlegible Next 400 and 700 for reading. It was designed by the Braille Institute for low-vision readers; letters like I, l and 1 are distinct.
- Root size 18px in the app (21px with Bigger text), a 1.2 ratio, body never under 16px. Both families are self-hosted.
- A 4px-based scale (
--space-2xs4px to--space-4xl144px). - Controls 52px tall, icon buttons 48px, tab bar 76px: thumb-sized for one hand.
- Radius by nesting level: cards 20px, inputs 14px, small parts 6px, pills for controls and round faces.
- Depth from light, not from blur: a hairline top highlight (
--edge-light), a short cast shadow on raised parts, a sunk bed for tracks and segmented controls.
- Buttons: one filled primary per screen (indigo with cotton text; on the night, resist white with iron text); secondary buttons are paper with a hairline; quiet buttons are text. Every control has hover, focus-visible, active, disabled and busy states.
- Segmented control: a sunk bed with a raised, lit segment (whisper style, output mode, theme).
- Faders: volume and speed as a machined fader: a sunk track, an indigo fill to the level, a tick scale either side, a gripped cap with a lit index line.
- Switches for on/off settings; sheets for confirmations (delete, Enhanced mode consent) that say exactly what happens.
- People list: name tags (round face, name, the line, a play button), not cards.
- Tab bar: People, Today, a lit Whisper lamp in the middle, Practice, Settings.
- Whisper mode: the night, a "Recognised on this phone only" badge, the camera, the ring states (a solid resist-white halo with a dark edge for confident, dotted for maybe, a faint dashed outline for no match), a caption card with the line, cues and where you met, five round controls (mute, pause, switch camera, who was that, pocket mode).
- Notes under the status line: small night pills for "silent until you check your earbud", quiet hours and event mode, so the state that changes what you hear is always on screen.
- Pocket mode: the night at its darkest (
--pocket, near black so an OLED screen is nearly off), one line of status in large type, a plain Wake the screen button and double-tap to wake. Buzz codes stand in for sound: two short, three short, one long. - The 60-second coach: a compact card on the night with the step, a timer, three progress marks that turn resist white, and an explicit "Play whispers aloud" toggle; it ends in a summary sheet.
- Lock screen: cotton paper, the mark, one field or one button, plain errors, and an honest way out (wipe and start again). Nothing else of the app is rendered while locked.
- Your data: the stored items as ruled rows with counts, people with a remove button each, export and delete everything, then Wipe now alone in a madder-red panel (the only red fill in the app).
- Documents (privacy, terms, safety): a reading page in the same palette, ruled rows for "what is kept", a 54-character measure, typographic quotes.
- Helper page: the consent page's system (cotton paper, one form, the line set large and written live), so a helper sees the same product.
Only transform and opacity, 120 to 420ms with an ease-out, plus one signature: when a face becomes confident, two soft ripples leave it and the halo settles (about 0.9s, once per person on a track). prefers-reduced-motion turns the bloom and smooth scrolling off.
Checked by tests/e2e/a11y.spec.ts, which runs axe-core with the WCAG 2.0, 2.1 and 2.2 A and AA rules on the landing, helper, consent, privacy, terms and safety pages and on every app screen in light and dark (tour, people, today, practice, settings, privacy, a person, add someone, Your data, Help, the lock screen, Whisper mode, pocket mode). Result on the final build: no serious or critical findings on any of them, and no minor ones either. Axe can't judge everything; the items below were checked by hand or by the same test file.
- Screen readers. Every screen has one
h1; on each route change, each tour step, entering pocket mode and the end of the 60-second try, focus moves to the new heading or the main control, so the change is announced. A visually hidden live region says who was recognised; the 60-second coach's step text is a live region too. Icon-only buttons have labels ("Pocket mode: screen dark, phone at chest height", "Who was that? The last five whispers"); toggles usearia-pressedorrole="switch"witharia-checked. The camera video and canvas arearia-hidden: everything they show is also in text. - Keyboard. Every control is a real button, link or input, reachable with Tab in reading order, with a 3px focus ring (the test checks the ring on each tour control). Sheets are native
<dialog>elements, so Escape closes them and focus stays inside. Pocket mode returns focus to its own button when you wake the screen. - Reduced motion.
prefers-reduced-motionturns off screen transitions, the halo bloom, smooth scrolling and the loader; the test checks the step animation isnone. - Not only sound. Speech is never the only output: captions always show, and buzz, chime and silent modes exist. Nothing is said aloud before the earbud check, in quiet hours, or for anyone marked "never whisper".
- Reading. Contrast passes in both modes; Higher contrast steps muted text and rules toward the ink; Bigger text scales the whole interface; body text is never under 16px; Atkinson Hyperlegible Next for reading.
- Touch. Controls are at least 48px; the main ones 52px; the Whisper mode buttons 56px.
Hallmark shape: Narrative Workflow (swapped from the drawn Marquee Hero, because the product needs explaining and has a real sequence). Counter hero: the headline and two actions beside a real capture of Whisper mode. The page whispers: Hear a whisper speaks a real line in the browser's voice, and the note box turns a typed note into a whisper with the app's own rules. Sections: their words, four stages, before you walk in, the promise on the night under drawn stars, honest limits, fair questions, close. Every picture is a screenshot of the real app taken by its own Playwright run.
- Pre-flight and before record:
sloplint --beforeon the first version recorded 44 FAIL, 56 REFLEX and 20 WARN (tracked-caps labels, colour literals, animated widths, glass panels, raw spacing). The record itself stays local (.hallmark/before.txt, ignored by Hallmark's own.gitignore). - Theme deal:
theme.mjs dealwith the editorial and playful genres; Ribbon was dealt (3/3 and 1/3).theme.mjs draw --material product --job try --theme ribbondrew the landing shape (drawaaf455). - Scaffold:
theme.mjs scaffold ribbon --redesign full --scope app --dir src/stylesandtheme.mjs scaffold ribbon --dir landingwrote the tokens and base styles; the app's original body face (Atkinson Hyperlegible Next) and the ring signature were kept. - Design before code: the picks (theme, shape, chrome, hero, sections, material) were written to
.hallmark/picks.mdbefore building. - Sweeps:
sloplintstatic and--render(Chromium throughpuppeteer-core) on the app (--scope app) and the landing page, repeated until clean. Gates fixed along the way include tracked caps, off-scale spacing, colour literals, width animations, glass, middle-dot bullets, a filled pressed state, two filled hero buttons, content hidden without JavaScript, the hero over the fold and word counts. - Reviews in fresh context: a hero review (verdict fix, 5 findings, 4 resolved, 1 partial: the night drawn as light) and a finish review (verdict fix, 8 findings, all resolved: an old capture, the horizon's edges and fold, a cropped consent capture, the close line against its capture, a faded label, the faders, amber numbers, an empty gap).
- Second round (pocket mode, the coach, the notes, the helper page, quiet hours, event mode): swept again with the helper page added to the app scope. It found one FAIL (gate 34: the tour footer ran 50px past a 375px screen once "See it in 60 seconds" joined it) and two REFLEX (gates 7b and 22: pocket mode's pure black). All fixed: the footer wraps, and the black is now the night at its darkest.
- Third round (the lock screen, Your data, Help, the privacy, terms and safety pages): swept with the documents added. It found one FAIL 19 twice ("Unlock" read as a buzzword; the buttons now say "Open Halo" and "Open with this phone's lock"), FAIL 63 (middle-dot link rows; now commas and lists), F1 (straight quotes in the documents; now typographic), REFLEX 25 (a 93-character measure on the privacy page; now 54ch) and 26b (no pressed state on document links). All fixed; F4 for the red Wipe now fill is dismissed with its reason in the stamp.
- Final state: app, helper page and the three documents 0 FAIL, 0 REFLEX, 0 WARN, 2 waived (28b: the live camera video has no poster), 22 dismissed with reasons in the stamps; the documents alone 0 FAIL, 0 WARN, 2 notes (word count: they must say everything); landing 0 FAIL, 0 WARN, 4 dismissed (verbatim quotes, one-sentence ledes), 2 notes (word count: the quotes and the six limits are fixed wording).
Every screen in light and dark, phone and desktop: docs/shots/ and the contact sheets docs/shots/contact-sheet-*.png.