Wash block
A tinted rectangle holding one or two sentences and, sometimes, one link. It is how this product says something out of band without raising its voice: no icon, no border, no left bar, no red. Three classes in the old stylesheet, one component here, and tone is the axis that produces all three.
Anatomy
Two content slots, which is what makes it a molecule rather than an atom: a sentence, and an optional standalone link. Everything below is drawn by design/system/components/wash-block.css through the same entry point a product screen loads, so this page cannot drift from the screens: there is no second copy of the CSS here to drift from.
Netflix went up by $2.50, now $17.99 a month.
See what changed.wash.attentionthe molecule itself, here on the attention tone. Also.wash,.wash.errorand.wash.code: four tones, one rule. It was.attention,.notice,.notice.is-errorand.decoderuntil 2026-08-12, when the renaming map indocs/inventory.mdlanded in the CSS and in the markup of all 28 coloured screens in one steppthe sentence. No class of its own: the wash has exactly one paragraph slot, so a class here would name a thing that cannot vary. Primary ink on the neutral, attention and error tones> athe standalone link, and it is a direct child rather than part of the sentence. That structural fact is what gives it the 44px target floor, with no class and no markup change:> aagainstp aa::afterthe arrow, on the attention tone only. It is content with an empty alternative text, so a screen reader is not handed the words "right arrow" mid sentence.rawthe code tone's second slot: the statement string, shown as the string. Not present on the other three tones--radius-wash12px, the corner all four tones now share. It exists for exactly this object and it is named after it
The wash carries no atom. It hosts a paragraph and an anchor, both of them bare elements, which is unusual in this level: it is a molecule by the second half of the definition, two or more content slots in one unit, rather than by hosting something from the kit. On the code tone the loading form hosts the skeleton bar, and that is the only kit component that ever stands inside one.
Behaviour at width
A reading measure and no query at all: --container-text, 52ch, added at stage 10. The sentence is running prose (the failed-payment explanation is 173 characters, the unrecognized one 130) and it had no measure of its own. The width sweep found it at 594px, 69 characters to a line, at a 1600 window, inside a column that is allowed to be that wide. The block itself still takes the width of its column: what is capped is the line, not the fill, so the tone still reads as a band across the content. At 360 nothing moves, because the column is narrower than the measure. See Width.
And one point, 47.5rem, on the one band that carries an action row. Added 2026-08-19: below it the three parts stack in the order a person reads them - what this is, the action, the honest footnote - and past it the band is over a thousand pixels wide while a paragraph in it stops at 52ch, so the right half was empty air. The action moves there and spans both text rows, which is what makes the band read as ONE object rather than as a button that happens to have a sentence near it. :has(> .actions) and not a modifier class: whether a band carries an action is a fact about what is inside it, and the three washes that carry only prose are not asked to declare that they do not.
Variants and sizes
The consolidation, in one sentence. .attention, .notice and .decoder were three components in kit.css and are one here. Read side by side they were the same object: the same padding, the same margin, the same fill family, differing in the fill token, in one corner, and in nothing else. What follows is the axis matrix that replaces those three declarations, not a list of the places they occurred. Not one pixel moved at the rename, and the reason is that sentence: all four tones already shared one padding, one margin and, since the corner was consolidated at step 3, one radius, so there was nothing left for a fold to collide over.
| Tone Chosen by whose news it is, never by how loud it feels. Neutral is the default and covers anything ordinary. Attention is a change the person should know about. Error is our failure and nobody else's. Code is a machine string quoted verbatim |
Tokens | Where it stands |
|---|---|---|
Neutral .wash |
--bg-callout --text-primary |
3 pages. The unrecognized charge, the failed merchant payment, and the reconnect notice in the grey. All ordinary facts |
It repeats like a subscription, but we could not match it to a service. | ||
Attention .wash.attention |
--bg-attention --text-attention |
3 pages. A price change, on Home and on the subscription's own screen. Amber and never red: the persona closes any app that shows her red |
Netflix went up by $2.50, now $17.99 a month. See what changed | ||
Error .wash.error |
--bg-failure --text-failure |
1 page, and it is opt in. Clay is spent on our failure only: the sync error and the could-not-load screen |
We could not refresh just now. Showing your last update from today, 9:14 AM. Try again | ||
Code .wash.code |
--bg-callout --text-body + --text-primary |
5 pages, every state of the subscription detail. Two inks rather than one: the label is quiet, the string is not |
The error tone is opt in, and the inversion was a real repair. Clay used to be the default on .notice, and three of the five pages that carried it are not failures at all. Where the clay is spent is now settled and narrow. A payment that did not go through at the merchant is not our failure and not the person's: it is a fact about the world, its own copy says so in as many words ("Nothing is wrong with your money"), and it keeps the neutral wash.
| Content The arrow says the link leads somewhere else. No arrow says it re-runs something here. It is not decoration and it is not an emphasis step |
Measured | Where it stands |
|---|---|---|
| With an arrow | ::after, 8px left | The attention tone only. "See what changed" goes to another screen |
Netflix went up by $2.50, now $17.99 a month. See what changed | ||
| Without | - | Neutral and error. "Try again" re-runs the thing that failed and goes nowhere |
We could not refresh just now. Try again | ||
| Code, with a link | - | Forbidden with a reason rather than missing. The code tone quotes a machine string; there is nothing at the other end of it to link to, and a link there would promise a destination the product does not have. If a raw string ever needs an action, it belongs beside the wash and not inside it |
The two axes are not independent today, and pretending otherwise would be a fiction. The arrow rides on the attention tone alone. That is a coupling rather than a rule, and it is written here so the next person meets it as a fact rather than as a discovery: a neutral wash whose link genuinely leads somewhere is allowed the arrow, and nothing in the CSS stops it.
There is no size axis. All four tones take one padding step and one type step, and the wash is always the full width of its column: 100% against auto is layout, exactly as it is for the button. The only measurement that varies between the four is the height, and that is the sentence being longer.
When to use it
A wash is where the product says something about the screen rather than something on it: a price went up, we could not refresh, we could not name this charge, here is what the bank actually wrote. It sits above the content it is talking about, and the content underneath still works. That is the whole test. If the message is the screen, it is a text block and not a wash, and no wash is drawn around it.
On the path a person meets it twice, and both are moments the product exists for. On Home, the attention wash carrying "Netflix went up by $2.50, now $17.99 a month" is one of the three elements wireframes/docs/screens.md names as carrying the strategic dimension. On the subscription's own screen, the code tone carrying SPOTIFYAB STOCKHOLM is the product admitting what the bank actually said, which is the transparency principle drawn rather than claimed: show the source of every figure.
Copy comes from voice/docs/microcopy.md and states the fact before the feeling: "A payment to Amazon Prime did not go through on Jul 20. Amazon usually tries again within a few days, and Tendd will tell you when it does. Nothing is wrong with your money." Active voice, the actor named, no exclamation mark, and never the word "failed" about the person.
The rule, and the anti-rule
It repeats like a subscription, but we could not match it to a service.
One mark for one message: a fill, a corner, and the sentence. No border, no icon, no coloured bar down the left. The grey original drew a border and a 3px bar and both were dropped when colour arrived, because fill plus border plus bar is three marks saying the same thing. This also obeys the Flat Paper Rule in DESIGN.md: a new surface earns a fill and at most a hairline, never a shadow.
We could not load your subscriptions
Nothing is lost. Try again in a moment.
When the message is the entire screen, the right component is the text block, written .textblock.status, with no wash at all. A wash exists to tell a message apart from the content beside it; a full screen message has nothing beside it, the words and the Try again button already say what happened, and a tinted rectangle around the whole viewport is decoration. This was decided rather than overlooked, and it is why the five error screens are untoned.
We could not refresh just now. Showing your last update from today, 9:14 AM.
Try againInside a calm sentence that is explaining what happened, "Try again" belongs to the wash and its own inline link. The figures on the screen are still there and still true; the wash is the footnote saying how old they are.
When the same words stand alone in an action row with nothing to explain, the component is the button, .btn.primary. A filled petrol button inside an explanation shouts over the explanation; a bare link in an action row is not findable as the way on. The census found "Try again" playing three different ways across four error screens, and this pair is the rule that decides which.
States
Three, and they belong to the link rather than to the wash. The rectangle itself is not a target: it is not an anchor, not a button and not pressed on any of the 8 grey pages or the 7 coloured ones, so it has no hover of its own and none is declared. What is interactive is the standalone link inside it, and it has rest, hover and focus visible. There is no disabled state: a message cannot be turned off.
Rest, live
A payment to Amazon Prime did not go through on Jul 20.
Try againWe could not refresh just now. Showing your last update from today, 9:14 AM.
Try againNetflix went up by $2.50, now $17.99 a month.
See what changedHover them and tab through them: these are the real components on the real entry point, and the three links measure 44px tall because each one is a direct child of its wash.
The code tone's loading form, live
The one state that belongs to the wash rather than to its link, and this page did not show it until 2026-08-12. On the loading state of the subscription detail the code tone holds two skeleton bars where the label and the string will land. The host sets the gap between them; the atom draws the bars. It is the only kit component that ever stands inside a wash.
A wash that ended lower than its own padding, and the fix is the host's. .skel.line carries margin: var(--space-4) 0, because a run of bars needs air between them. The last bar in the wash spent that air on nothing: it stacked under the wash's own bottom padding, so the block ended 4px lower than the same block ends when it holds a sentence. .wash.code > .skel:last-child now zeroes it, written in wash-block.css and not in skeleton-bar.css, and the reason is the ladder: the atom cannot know it is last inside anything, and a :last-child rule in the atom's file would be the atom guessing at its hosts. The host knows exactly one thing the atom cannot, which is where its own padding starts.
The link, in both themes
The states are described, not photographed. Founder's decision, 2026-08-11: the snapshot strip went from every page in this kit, 120 images across 18 pages. They were framed as measurements rather than illustrations, and that framing was honest, but the bill came due all session: a value moves, and every picture of it is quietly stale until somebody re-shoots it. A stale picture is worse than no picture, because it looks checked. What is left is what a picture never carried anyway: the state, and the token it reads. The resting component is live and interactive above, so hover it, tab to it, and read the value off the real thing rather than off a photograph of it.
--text-body, --text-failure, --text-attention, underlined in --line-container--bg-callout is the one role that is NOT mirrored: on dark it lifts off the surface while the page ground stays put--text-action on the ink and the underline, on all four tones. The ink used to be a tie and lose it; the tie went on 2026-08-12 and the finding below is now a record rather than a defect--color-focus, drawn once for the whole system by base.css at specificity 0. It read --line-selected until 2026-08-12, when two roles claiming one job were separatedRetake, same recipe as the etalon, because there is no node Playwright in this repo:
serve the repo, then for theme in (none, data-theme="dark" on <html>):
goto /design/kit/wash-block.html
move the pointer away -> screenshot #live-rest -> screens/wash-block-rest-<theme>.png
hover the link in #shot-error -> screenshot #live-rest -> wash-block-hover-<theme>.png
Tab to the link in #shot-attention, assert :focus-visible
-> screenshot #live-rest -> wash-block-focus-<theme>.png
The hover was dead on two of the three tones, and on 2026-08-12 it came back. It was measured rather than deduced, before and after, on this page and on the product. Before: hovering the neutral and error links returned rgb(154, 88, 66) for the ink, which is the resting clay, and rgb(28, 106, 118) for the underline. Only the underline moved. After: both return rgb(28, 106, 118) for the ink and rgb(28, 106, 118) for the underline, which is what kit.css declared all along and could not deliver. The attention tone is unchanged, because its hover was the one that already worked.
The arithmetic of the tie, because it is what makes this a repair and not a redesign. .notice a:hover is 0-2-1: one class, one pseudo-class, one type. .app .notice a is also 0-2-1 and was written 130 lines later, so it won, and it sets a colour and no decoration colour: the ink was pinned and the underline was free. On .notice.is-error a, 0-3-1, even the underline was lost. Meanwhile .attention a:hover happened to be written scoped, 0-3-1, and worked. Two washes, two different hovers, and nothing in either file said so. Nobody wrote that behaviour; it is what five rules for one link produce when two of them weigh the same.
What replaced it is arithmetic and not order. The link's shared properties are one rule at 0-2-1, each tone's ink is one rule of its own, and every hover out-specifies its own rest by a whole class: 0-3-1 against 0-2-1 for the base, 0-4-1 against 0-3-1 for the error. Nothing written later in this file or any other can take the response away by being written afterwards, which is what "one owner" buys. Every value is one kit.css already wrote; nothing new was invented.
The selectors carried both names for one step, and the old half is now gone. The repair above had to be written under .notice and .wash at once, because deleting the old half then would have stripped the link style off four wash specimens on pages that round did not own, and writing only the old half would have put the tie back the day the markup landed. The markup landed on 2026-08-12. Every list now names the new class only, at the specificity the pair was written to hold, and the repair reaches the product screens in the same breath: they load ../system/index.css now, not kit.css. Measured on the real screens rather than on this page: hovering the error link on Home, could not refresh and the attention link on Home returns rgb(28, 106, 118) for the ink and the underline on both.
The technical half
| Reads | Which token | Growing from |
|---|---|---|
| the neutral and code fill | --bg-callout | --canvas, and --panel-dark in the dark theme: the one role pair that is deliberately not mirrored |
| the attention fill | --bg-attention | --amber-wash |
| the error fill | --bg-failure | --clay-wash |
| the sentence | --text-primary | --ink |
| the code tone's label | --text-body | --slate |
| the link, per tone | --text-body, --text-failure, --text-attention | --slate, --clay, --amber. The link never introduces a colour the wash has not already established |
| the link under the pointer | --text-action | --petrol |
| the underline at rest | --line-container | --hairline. A full strength rule under every link in a calm paragraph reads as emphasis |
| corner and target floor | --radius-wash, --tap | primitive, straight: geometry has nothing to override |
One declared change of appearance, and it is why this is a consolidation rather than three files stapled together. The code tone's corner was --radius-sm, 10px, measured on the real subscription detail screen today. It becomes --radius-wash, 12px, on 5 pages. Same fill, same padding, same margin as its siblings and a different corner for no named reason: two washes cannot have two corners, and the wash radius exists for exactly this. The line is in docs/tokens-audit.md under "Consolidated drift", and the pixel comparison at step 8 checks it against that list.
One class had two opposite meanings depending on its tag, and that is why it is not in this file. kit.css carried .app p.notice { background: none; border-radius: 0; padding: 0; color: var(--soft) }, which cancelled every declaration this component makes. Written on a section, .notice was a wash; written on a p, it was a plain muted sentence. Step 2 split them, the paragraph went to the Muted line atom where it now lives, and it stands in exactly one place, Upgrade. Specificity, not order, is what keeps that working across the split: 0-2-1 against this component's 0-2-0, so the atom wins from the atoms layer even though this file is imported below it. The rename map took p.notice to .muted on 2026-08-12, and with the wash declaration gone from that paragraph there is no longer anything for the three cancelling lines to undo: they are deleted from muted-line.css too. A class that meant two things now has two names, and neither has to unsay the other.
Lives in design/system/components/wash-block.css. Stands on 10 grey pages and 9 coloured ones, 13 washes, the newest being the export band on History and Trends, 2026-08-19. The inventory's "13" is the sum of the three old classes' page counts: it double counts home-error, which carries two washes, and it counts the one p.notice that is not a wash at all. Coloured screens carrying it: Home, Home, could not refresh, Subscription Detail, the price change, the failed payment, the unrecognized charge, the loading state.
<section class="wash" role="status" aria-label="What happened">
<p>A payment to Amazon Prime did not go through on Jul 20.</p>
</section>
<section class="wash attention" role="status" aria-label="Needs your attention">
<p>Netflix went up by $2.50, now $17.99 a month.</p>
<a href="subscription-detail-price-change.html">See what changed</a>
</section>
<section class="wash error" role="status">
<p>We could not refresh just now.</p>
<a href="home-loading.html">Try again</a>
</section>
<section class="wash code" aria-label="How this appears on your statement">
Appears on your statement as
<span class="raw">SPOTIFYAB STOCKHOLM</span>
</section>