| name | PurifyMail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| description | A local mail sorter that spends its credibility on legibility and reversibility instead of invention. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| colors |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typography |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rounded |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spacing |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| components |
|
Creative North Star: "The Clean Desk"
PurifyMail is a desktop utility whose entire claim is that nothing leaves the machine. An interface making that claim cannot also be the interface that throws confetti at you. So the system spends its credibility on legibility and reversibility instead of invention: warm paper ground, a white working surface, one green that means this went well, and hierarchy carried by space and weight rather than by boxes. The visual register is the desk after it has been cleared: flat, warm, undramatic, with everything in a place you can name.
Density is calm but real. This is a tool for a session where several hundred senders get dispatched, so rows are tight (9px vertical padding on a 34px avatar) while gutters stay generous (32px page inset, 20-32px between sections). The tension between those two is the whole layout problem, and the system resolves it by refusing to draw containers: a sender row is a hover background and a radius, nothing more; a settings group is a hairline and 32px of clearance, not a card. Nothing in the app is a box inside a box.
Both themes are first-class and were balanced independently. Dark is not a tinted inversion of light, and the accent shifts hue and lightness between them rather than being reused. Colour is rationed hard: exactly one chromatic accent carries the product's good outcome, red and amber are reserved for genuine states, and the only other hues on screen are the avatar tints that give four hundred senders distinguishable identity, frozen in lightness within each theme and rebalanced between them. The confirmed anti-reference is the category's dopamine treatment, Cleanfox's confetti-and-reward register. The session summary in this build is three rings expanding once. That is the ceiling for celebration.
Key Characteristics:
- Warm-neutral ground (
#f6f6f4) with a true white working surface; never grey-on-grey - Exactly one chromatic accent; danger and warning are states, not a palette
- Sections, not cards: separation is space plus one hairline
- Hairlines on lifted surfaces are drawn as inset shadows, so nothing resizes on hover
- Weight is the primary hierarchy device and moves in half-steps (520 / 560 / 620 / 640)
- Tabular figures on every number that can change
- Phosphor icons at a single
regularweight, globally enforced - Two motion curves only: one for arriving, one for leaving; leaving is always faster
A warm neutral field with one deep green that means the good outcome, plus two state hues that never behave like accents.
- Rinse Green (
#0d8f61): The single accent. It marks the good outcome and nothing else: the primary scan button, a successful unsubscribe, the progress fill, the volume bar, the checked checkbox, the focus ring, the app mark, the summary rings. It is deliberately absent from the Keep action, because keeping a sender is the neutral outcome, not the rewarded one. - Rinse Green Pressed (
#0a7a52): The hover state of a filled accent surface. Darker rather than lighter, because a filled control gets heavier under the cursor, not brighter. - Accent Ink (
#ffffff): Type and icon colour on filled accent surfaces, and the stroke colour inside the app mark. - Green Wash (
#e7f4ee): The accent at ambient strength. It carries the selected sender row, the sidebar pill, the success badge, the welcome bullet icons, the indeterminate checkbox, and the 3px focus halo around inputs. The progress ring's track is the same idea computed rather than authored: the accent at 14% in oklab, so the unfilled arc belongs to the fill instead of borrowing the neutral border. This is how the accent covers area without shouting. - Green Edge (
#b9ddcd): The hairline that belongs to Green Wash. Used as an inset ring on a tinted surface so the wash has a defined boundary in its own hue rather than borrowing the neutral border.
There is no second accent. Colours below are states.
- Terracotta Alarm (
#b8402f): Destructive and error only: the delete button, field errors, a failed sender's badge, failed connection results. Never decorative, never a category colour. - Terracotta Wash (
#fbeae7) / Terracotta Edge (#eec7bf): The resting state of a destructive button, which sits tinted-and-outlined until hover fills it solid. Delete announces itself before it is pressed, not after. - Amber Caution (
#9a6b12): Partial success and degraded state: the mailbox that failed to scan, the sender whose unsubscribe did not complete, the warning dot in the sidebar. The distinction from Terracotta is load-bearing: amber means incomplete, red means destructive or broken. - Amber Wash (
#fbf2df): Ambient amber for the warning badge and for a mailbox that failed mid-scan.
- Warm Paper (
#f6f6f4): The application ground, visible behind the sidebar rail. Warm rather than blue-grey; it is what makes the white surface read as paper rather than as a dialog. - Working White (
#ffffff): The main content surface, and the fill of every lifted object (buttons at rest, the work bar, the bulk bar, dialogs, the current nav item, the active segment). - Sunken Track (
#efefec): The recessed counterpart. Used for tracks and grooves that something sits in: the segmented control's trough, the volume bar's unfilled remainder, the quiet badge, inline code, and the resting Keep action. - Hover Warmth (
#f4f4f1): The universal hover background. One value for every hoverable neutral thing: nav items, rows, ghost buttons, icon buttons. - Quiet Hairline (
#e3e3de): Structural division: the sidebar/main seam, toolbar underline, section separators, account list rules, input borders at rest. - Stated Hairline (
#d2d2cb): The hairline that has been noticed. Input border on hover, the toggle's off track, scrollbar thumbs, checkbox outlines, underline colour on quiet link buttons. - Near-Black Ink (
#17191a): Primary text. Not pure black; it carries a trace of the same cool cast as the borders. - Muted Ink (
#5b615e): Secondary text: sender addresses, hints, subtitles, prose, the resting state of nav and icon buttons. - Faint Ink (
#8b918d): Tertiary text: placeholders, sidebar group labels, empty-state icons, separator dots, the "no unsubscribe link" note.
The One Accent Rule. Exactly one chromatic accent exists, and it means this is the good outcome. Red and amber are states with a fixed vocabulary, not additions to the palette. A second brand colour is out of the system, including one invented to give a third action its own identity; the neutral action takes a neutral surface (surface-sunken) instead. Audit test: if a new colour cannot be described as "the good outcome", "destructive", or "incomplete", it does not ship.
The Wash-and-Edge Rule. A tinted surface never takes a border-color. It takes its own hue's -wash as background and its own hue's -edge as an inset 0 0 0 1px ring. A tinted panel bounded by the neutral grey border reads as a foreign object dropped onto the page.
The Frozen Tint Rule. Sender avatars are the one place a hue other than green appears. The hue is hashed from the sender's domain so a sender always looks the same, while lightness and chroma are pinned (oklch(0.93 0.045 H) on the disc, oklch(0.42 0.09 H) on the initials). The hue wheel is free; the other two axes never move. That is what keeps four hundred rows from becoming a fruit salad, and it is why these tints do not violate The One Accent Rule: they are identity, not meaning.
Display Font: the platform system stack, -apple-system / BlinkMacSystemFont / Segoe UI Variable Text / Segoe UI / Ubuntu / Cantarell / Noto Sans
Body Font: the same stack. There is no second family and no webfont.
Label/Mono Font: none. Numerals are handled by a feature setting, not a family.
This is a recorded decision, not an unexamined default. PurifyMail is a native desktop binary running in the system webview, sitting next to the OS title bar and the OS scrollbars. A bundled face would render as a foreign object inside native chrome, and would add weight to a download whose selling point is that it is about 10 MB. The stack is deliberately headed by the variable Windows face (Segoe UI Variable Text) because the whole hierarchy depends on intermediate weights being real.
Character: Neutral, native, and completely deferential. All expression comes from weight and negative tracking rather than from letterforms. Headings tighten to -0.021em at 27px and above, which is the only place the type calls attention to itself.
- Display (640, 36px, 1.2,
-0.021em): One use: the welcome headline on the first-run screen, the only moment the app gets a full-size voice. - Headline (640, 27px, 1.2,
-0.021em): Theh1of every view, always paired with a live tabular count directly beneath it. Also the figure size in the session summary (660,-0.02em). - Title (620, 21px, 1.25,
-0.016em): Dialog headings and empty-state headings. - Subhead (600, 17px, 1.35,
-0.009em): Settings and account section headings, and the sidebar brand line (640,-0.014em). - Body (400, 14.5px, 1.5): The document default. Input text, sender names (570), nav labels (500-600).
- Body Secondary (520-620, 13.5px, 1.5): The workhorse of the interface: button labels (560), field labels (560), row counts (620), band titles (620), the work bar line (560), prose. More of the app is set at this size than at Body.
- Caption (400, 12.5px, 1.45): Row sub-lines, hints, account metadata, wait lines, scan detail. Prose measure is capped at 68ch, empty-state copy at 46ch, the summary note at 42ch.
- Label (620-640, 11.5px,
0.05em, uppercase): Sidebar group headers only. Pills and badges use the same 11.5px size at 620-640 weight but stay sentence case.
The Half-Step Weight Rule. Hierarchy is carried by weight, and weight moves in half-steps the variable system face can actually render: 500, 520, 560, 570, 600, 620, 640, 660. The size ramp is correspondingly shallow: four of its eight steps live between 11.5px and 14.5px. When two things need to be distinguished, change the weight by 40-60 before you change the size.
The Tabular Count Rule. Every number that can change carries .tnum (font-variant-numeric: tabular-nums). Sender counts, header totals, the sidebar pill, lifetime stats, summary figures. A count that reflows its own digits as it animates is a bug, not a style choice.
The Native Face Rule. No webfont ships. If a surface needs a voice the system stack cannot give, the answer is weight, tracking, or space, never a new family.
The shell is a two-column CSS grid: a fixed 244px sidebar and a minmax(0, 1fr) main column, full viewport height, with overflow: hidden at the root. Nothing in the app scrolls the document; only .view-body scrolls, and it scrolls vertically alone. The main column carries the white surface and a single left hairline against the warm ground. That seam is the only structural border in the shell.
Every view is a vertical stack of three fixed bands: a header (32px 32px 16px), a toolbar with its bottom hairline, and the scrolling body. Page inset is 32px (--s-7) on every one of them, so the header, the toolbar and the list share one left edge. Progress is not a fourth band: it either takes the whole screen or lives in a 2px rail at the top edge.
The spacing scale is ten steps: 4, 8, 12, 16, 20, 24, 32, 40, 56, 72. Steps 1-4 handle intra-component gaps, 5-7 handle section rhythm, 8-10 handle empty states and first-run breathing room. Content measure is capped rather than fluid: settings and accounts hold 780px, prose 68ch, the welcome column 52ch.
Two width breaks, both about the window rather than about a phone, because this is a desktop binary and there is no mobile layout:
- 1040px: the sidebar collapses to a
62pxicon-only rail (labels and the whole account section hide, nav items centre), page inset drops from 32px to 20px, and the volume column narrows from 116px to 82px. - 900px: the view header wraps, and the "no unsubscribe link" marker leaves the row sub-line.
The Shared Column Rule. The sender row's checkbox sits on exactly the same optical column as the toolbar's select-all above it: the list carries 20px side padding and the row carries 12px, summing to the toolbar's 32px inset. Any control that pairs vertically with a control in another band must resolve to the same total inset, not merely look close.
The One Scroller Rule. Exactly one element scrolls per view, and it is the list body. overscroll-behavior: none on the body kills rubber-banding, because a desktop app is not a document. Floating furniture reserves its own clearance instead of overlaying content: the list body carries 96px of bottom padding so the bulk bar never sits on the last row.
Mostly flat, with a strictly rationed three-step shadow set and a warm/white tonal pairing doing most of the work. Depth means one of exactly two things: this floats above the content or this has been lifted out of a groove. Sections, panels, rows and inputs get no shadow at all: they are separated by space, a hover tint, or a single hairline.
Every shadow in the set is a two-layer soft shadow with a real blur and a vertical offset. There are no hard offset shadows and no zero-blur shadows anywhere in the build. Dark theme re-derives all three from black at much higher opacity rather than reusing the light values.
- Lift (
--shadow-sm:0 1px 2px rgb(23 25 26 / 0.06), 0 1px 1px rgb(23 25 26 / 0.04)): The barely-there lift that separates a resting control from its surface: quiet buttons, the primary button, the current nav item, the active segment, the toggle knob. - Float (
--shadow-md:0 4px 12px rgb(23 25 26 / 0.08), 0 1px 3px rgb(23 25 26 / 0.05)): Transient furniture that appears over content: the work bar and the shoulders behind the one-by-one card. - Overlay (
--shadow-lg:0 18px 44px rgb(23 25 26 / 0.14), 0 4px 12px rgb(23 25 26 / 0.07)): Things that command the screen. The bulk action bar and dialogs.
Modal scrims are color-mix(in oklab, var(--text) 26%, transparent) with a 2px backdrop blur. The scrim is mixed from the text colour, so it darkens correctly in both themes without a dedicated token.
The Inset Hairline Rule. A lifted surface never uses border for its outline; it appends inset 0 0 0 1px var(--border) to its box-shadow. Buttons and the bulk bar both do this, which is why hovering a quiet button can swap its outline from border to border-strong without the control changing size by a pixel. Structural dividers use a real border (the shell seam, toolbar underline, section rules, input strokes), because they divide rather than lift.
The No-Card Rule. A group of related controls is a section, not a card: padding-top: 32px and a single top hairline against its predecessor. Guidance text is set as prose with an icon beside it, and a result message is carried by the icon and heading colour alone. A tinted or bordered box inside a form is a container inside a container, and the settings screen becomes a wall of frames.
The Flat-Row Rule. List rows never lift. A sender row expresses hover with surface-hover, selection with accent-wash, and a decided state with opacity: 0.62. No shadow, no border, no outline.
Corners are soft but not round, and the ramp is hand-set rather than doubled: 7px → 11px → 16px → 22px, growing by 4, then 5, then 6. Small controls (buttons, inputs, nav items, icon buttons, the search field) take 7px; rows, the scan screen's error notes and the segmented control take 11px; dialogs take 16px; the floating bulk bar takes 22px, which is what makes it read as a dock rather than a panel. A fifth step, 5px, exists only for a control nested inside a 7px container: the checkbox box and the segmented control's inner buttons.
Full pills (99px) are reserved, not decorative. Everything shaped as a pill either expresses a quantity or a binary: the sidebar count pill, badges, the mailbox filter chip, the progress track and fill, the volume bar and its fill, the toggle, scrollbar thumbs. Circles (50%) mark identity and status: the sender avatar, the account status dot, the toggle knob, the summary rings.
Icons are Phosphor at the regular weight, set once through IconContext at the application root so no call site can drift. Sizes range from 11px (inside a badge) to 30px (empty state), but the weight never changes. When a checkmark needed more presence, it was sized up rather than weighted up. The application mark is authored SVG rather than a library glyph: an envelope whose lower-right corner has been carried away, with three motes rising from the gap, drawn with var(--accent) and var(--accent-ink) so it re-tints itself in both themes.
The Pill-for-Quantity Rule. A 99px radius means "this is a count or a state". Content lives in rectangles with a radius from the ramp. If a new element holds a sentence, it is not a pill.
The One Icon Weight Rule. One Phosphor weight for the whole application, declared once at the root. A viewport that mixes bold row actions with light nav glyphs reads as two icon sets. Vary size, never weight.
- Shape: Softly squared (7px), 32px tall, 13px of horizontal padding, with a 40px / 20px / 11px large variant used for first-run and empty-state calls to action.
- Primary: Filled Rinse Green with white ink and the Lift shadow. Reserved for the one action that moves the session forward, and most views show at most one. The scan button demotes itself from
primarytoquietthe moment a scan has produced results, because it is no longer the thing to do next. - Quiet (default): White surface, near-black ink, an inset neutral hairline plus Lift. This is the default tone in the API, so a button written without a tone is quiet.
- Ghost: No surface at all, muted ink, background appears only on hover.
- Danger: Terracotta wash with terracotta ink and a terracotta inset edge, filling solid on hover and dropping its shadow. It is the only control in the system that inverts on hover.
- Hover / Focus: Background, border-colour and shadow transition at
130mson the settle curve. Every button also has a physical press:translateY(0.5px) scale(0.985)over 90ms. Disabled isopacity: 0.42withnot-allowed. - Loading: A loading button is disabled by construction and swaps its icon for a spinning
SpinnerGap, because a loading button that stays clickable fires the action twice.
The primary controls on every sender row, so they hold real contrast at rest (muted ink on no background) rather than waiting for a hover to become legible. 30×30, 7px radius, 17px glyph, scale(0.92) on press. Four tones, and their hover colours are the entire state vocabulary: neutral and keep go to a neutral surface, accent goes to Green Wash, danger goes to Terracotta Wash. A label prop is mandatory and supplies both aria-label and title, so no icon can ship unnamed.
- Style: White surface, 1px Quiet Hairline, 7px radius, 34px tall, 14.5px text. Inputs are the one place the system opts back into
user-select: text, since the document body disables selection everywhere else. - Hover: Border steps up to Stated Hairline, but only when not focused.
- Focus: Border becomes Rinse Green and a
0 0 0 3pxGreen Wash halo appears. The same treatment applies to the search field via:focus-within. - Field: Label (13.5px / 560) above the control, with a hint below in 12.5px muted, replaced in place by a 12.5px terracotta error carrying
role="alert". Fields lay out in an auto-fit grid with a230pxminimum. - Search: A composite field with a leading magnifier and a trailing clear button, floored at
232pxwide, enough for the longest of the two languages' placeholders so neither truncates.
- Checkbox: 17px, 5px radius, 1.5px Stated Hairline stroke. Checked fills Rinse Green with a white 14px check; indeterminate fills Green Wash and keeps the accent border.
- Toggle: 40×23 pill on Stated Hairline, filling Rinse Green when on, with a 19px white knob that translates 17px over 220ms on the settle curve.
- Segmented: A Sunken Track trough with 2px padding; the selected segment lifts out of it as a white surface with Lift and 600 weight. This is the tonal-layering half of the elevation model in its clearest form.
The sidebar is a 244px rail on the warm ground: brand mark and wordmark, three nav items, an optional mailbox list, then lifetime stats pinned to the bottom by a flex spacer. Nav items are 14.5px muted text with a 17px icon and 12px gap, going to Hover Warmth on hover and lifting to a white surface with Lift and 600 weight when current. Mailbox entries are smaller (13.5px, 5px vertical padding), take Green Wash when they are filtering the list, and carry a 6px status dot that turns amber on a connection error. Group headers are the only uppercase type in the application. Below 1040px the whole rail collapses to icons and the mailbox section hides entirely.
The component the product exists to render, and the one place density and legibility are both non-negotiable. A flex row at 9px × 12px padding and 11px radius, holding, in order: a checkbox that stays at opacity: 0 until the row is hovered, focused within, or any selection is active; a 34px circular avatar tinted from the sender's domain; the identity block (name at 14.5px / 570, then a 12.5px muted sub-line of address · last received · mailbox · unsubscribe availability, separated by middot glyphs); a fixed 116px volume column; and three icon actions.
The volume column is the system's one piece of data visualisation: a tabular count over a 3px bar whose fill is scaled by messageCount / peak, floored at 0.035 so the smallest sender still shows a mark, and anchored transform-origin: right so all bars grow toward the count above them. The fill sits at opacity: 0.55 and rises to 0.9 on hover or selection. The bar is the count, drawn. It makes 412-versus-8 legible before the eye reaches the digits, which is what makes a four-hundred-row list scannable.
- Result: The outcome of a connection test, animating open and closed by height so the form below it moves once, not twice. Deliberately not a panel: it is an icon plus a coloured heading over muted prose, because a filled box here would be a third nested surface inside a form that is already inside a section. A failure folds the server's own words away behind a Details disclosure, wrapping anywhere so a single unbroken TLS error cannot widen its container.
- Bulk bar: A 22px-radius white dock floating 24px from the bottom, centred, with Overlay shadow and an inset hairline. Its container spans the view and is
pointer-events: noneso it cannot swallow clicks on the rows it floats above. - Dialog: 440px max, 16px radius, 24px padding, Overlay shadow, on a text-mixed scrim with a 2px blur. Destructive confirmations use
role="alertdialog"and autofocus the confirm button.
There is no toast. Confirming every decision in a corner of the screen meant forty notifications for a batch of forty, stacked on top of work the user had just watched happen. The row leaving the list is the confirmation; a failure brings it back wearing its badge.
The one loading shape in the application, at every size. An SVG ring drawn as an accent-at-14% track under a full-strength accent fill: 132px by default, 6px stroke above 90px and 4px below, round cap, the fill set by stroke-dashoffset so nothing about it is a layout property. When no total is known it holds a fixed 22% arc and rotates, and it carries role="progressbar" with aria-valuenow omitted in exactly that case.
A ring rather than a bar because it has a middle, and the middle is where the number goes. A bar spends its whole width saying how far along something is; here the count is the more interesting fact.
The scan takes the whole screen, because nothing can be decided while it runs and every control in the app is disabled, so a list nobody can touch, with the real event squeezed into a band above it, wastes the one moment the user has to wait through. A single centred column: the ring with the message count easing inside it, the phase and the folder underneath, the running tally of what has been found, a rotating line, a hint, and Stop.
Three details carry it. The count eases rather than jumps, over 420ms on an exponential curve, and resets its origin when the scan moves to another folder, so a burst of four hundred still reads as counting. The tally only ever goes up, accumulating from the scan's own events rather than from the sender list, which is not rebuilt until the scan ends. One line at a time changes every seven seconds, drawn from twelve written in each language and shuffled per scan: what the app is and is not doing with the mailbox, and what will be possible when it finishes. A first scan of a large mailbox runs for minutes, and a screen with nothing to say for minutes is a screen that feels stuck.
The deliberate opposite of the scan screen, for work that runs while the user keeps sorting: a 2px accent rail pinned to the top edge, one truncating line of text, and Stop. Determinate when the batch size is known, sweeping when it is not. Its Stop cancels the queued decisions and nothing else: a scan running behind it is not what the user is pointing at.
Two registers, and the difference between them is the rule: a wait that blocks every decision takes the screen; a wait the user can work through takes 2px.
The list is for surveying, the card is for deciding, and the card is the only
place the system spends real scale. A 540-560px column holds a progress bar, a
card stack, three 54px buttons and a shortcut line; nothing else is on screen.
The card carries a 52px avatar, the sender at --t-xl, the volume set at
--t-3xl, the largest type in the app, because the count is the reason the
sender is in front of you, and the three most recent subject lines under a
small-caps label.
Two shoulders sit behind the active card, offset 13px and scaled down 3.5% each with falling opacity. They are the reassurance that the pile ends. All three live in one CSS grid cell, so the shoulders take the card's exact height with nothing measured.
A decision throws the card out in the direction of its meaning: unsubscribe exits upward, delete leaves left with a -13° roll, keep leaves right with +13°. That is the whole reason the mode exists: the gesture carries the decision.
The row's three actions are about a whole sender, which is right until it is not: the shop that sent three hundred order updates also sent four invoices. The picker is the way out of all-or-nothing. A 720px dialog, wider than the link finder because a truncated subject is a message nobody can recognise, lists the sender's messages newest first, each one a checkbox, a subject and a right-aligned date and size.
Ticked means kept. The destructive reading would be the other way round, and it is rejected on purpose: the user is choosing what to save, the confirm button counts what is left over (Delete 37 messages), and it disables itself when nothing is left over. The kept rows take Green Wash: they are the exception in a list where everything else is on its way out.
Its entry point is the sender's name, styled as a title that grows an underline on row hover. Only the name is a control, so the address underneath stays selectable text.
A dialog listing every link found in a sender's most recent message, likeliest
first, with a badge-ok on the ones PurifyMail would have followed itself. It
exists because detection is never perfect and a sender you cannot leave is the
one failure this product cannot afford. Rows are hairline-separated with the
link text over its URL in --t-sm faint ink, and a quiet Open button per row.
The screen the product is most likely to lose someone on, so every question with a knowable answer is answered for them: the address picks the provider, the pasted app password keeps working with the spaces the provider printed it with, and a button opens the exact page that creates one. What cannot be removed, the app password itself, is explained in one sentence at the point where it is asked for, rather than in documentation nobody opens.
For a server PurifyMail has no preset for, the host field carries a Find it
button that works the hostname out from the user's own domain. The result of a
test is a result band: an icon plus a coloured heading over muted prose,
never a filled panel, with the server's own words folded away behind a
disclosure.
The product's whole claim, parked permanently at the foot of the sidebar rather
than inside a settings page nobody opens: a shield-toned HardDrives glyph in
Rinse Green, a --t-sm heading and a --t-xs line under it. It collapses to
the icon alone on the narrow rail.
Two curves and three durations, and they are tokens rather than per-component choices: --ease-out cubic-bezier(0.16, 1, 0.3, 1) for everything that arrives or settles, --ease-in cubic-bezier(0.7, 0, 0.84, 0) for everything that leaves, at 130ms (state), 220ms (movement) and 420ms (the volume bar). The same two curves are restated as literal arrays in the React layer.
Entrances are small and vertical: a row enters at y: 6 over 240ms, the work bar at y: -6 over 240ms, a dialog at y: 12, scale: 0.98 over 220ms, a wait line in at y: 6 over 400ms and out at y: -6. Exits are faster than entrances and horizontal: a dispatched row leaves at x: 28 in 180ms on the leave curve, which reads as being swept off rather than fading out.
The Two Curve Rule. One curve for everything that settles, one for everything that leaves, and leaving is always faster than arriving (180ms out against 220-280ms in). A third easing does not get invented for a single component.
The Earned Celebration Rule. The end-of-session summary animates once: three rings expanding outward over 1.5s at 190ms stagger, plus counters that ease to their real values. Not a particle burst, not confetti. The reward reflects work that actually completed, and its ceiling is a single expanding ring.
The Two Waiting Registers Rule. A wait that blocks every decision takes the screen: centred ring, live count, something to read. A wait the user can keep working through gets a 2px rail and one line. There is no third treatment, and neither one is ever a toast.
The Transform-Only Loop Rule. Anything that animates continuously animates transform or opacity and nothing else. The indeterminate ring rotates and the work rail translates, because they run for the length of a whole operation and animating a layout property would relayout every frame. Each also names itself in an explicit animation: none under prefers-reduced-motion, because the global duration collapse turns an infinite loop into a strobe rather than stopping it.
- Do keep Rinse Green (
#0d8f61) to the good outcome. If a new element cannot be described as "the good outcome", "destructive", or "incomplete", it takes neutral ink. - Do give tinted surfaces their own
-washbackground and-edgeinset ring, never the neutral border. - Do separate groups with
32pxof clearance plus one top hairline. A section is not a card. - Do draw outlines on lifted surfaces as
inset 0 0 0 1pxinside thebox-shadow, so hover can change the outline without changing the control's size. - Do put
.tnumon every number that can change, including counts inside pills and badges. - Do reach for weight before size: half-steps between 500 and 660 are the primary hierarchy device.
- Do set every icon at Phosphor
regularand vary size instead of weight; give every icon-only control a mandatorylabel. - Do align controls that pair vertically across bands to the same total inset: the row checkbox and the toolbar select-all both resolve to 32px.
- Do reserve the 99px pill radius for counts, states and tracks; content takes a radius from the 7/11/16/22 ramp.
- Do animate
transformandopacityfor anything continuous, and give any looping animation an explicitprefers-reduced-motionstop rather than relying on the global duration collapse. - Do treat light and dark as independent balances. A new token needs a value authored for each, not a computed inversion.
- Don't introduce a second chromatic accent, including one invented to give an action its own identity. The neutral action takes
surface-sunken. (The domain-hashed avatar tints are exempt: they carry identity, not meaning. Only the hue varies, and it comes from JS as a bare number in--tint; lightness and chroma are frozen per theme in CSS, so the disc holds the same distance from its own surface in light and in dark.) - Don't nest a filled or bordered box inside another filled or bordered box. Guidance is prose with an icon; a result is an icon plus a coloured heading.
- Don't put a shadow on a section, a row, an input, or anything that is not floating above content or lifting out of a groove.
- Don't use a hard offset or zero-blur shadow. Every shadow in this system has a real blur and two layers.
- Don't add a webfont or a display family. The platform system stack is the deliberate choice for a native binary in a system webview.
- Don't set type in uppercase anywhere except sidebar group headers. Badges, pills and buttons stay sentence case.
- Don't animate a layout-triggering property in anything that loops or runs for the length of an operation.
- Don't celebrate with confetti, particle bursts or reward animation. The confirmed anti-reference is the category's dopamine treatment; the ceiling is a ring that expands once.
- Don't let a control's size change on hover or focus. Outlines shift colour and weight; geometry stays still.
- Don't ship an icon-only control without an accessible name, or a loading button that remains clickable.