Save-focus candidate

A subscription a person might not be using, standing next to the one control that ends it. Two targets on one line, and telling them apart is the whole design: the row goes to the subscription, the cut goes to the cancel guide, and confusing the two is what costs the cancellation.

Molecule1 grey pageone form3 states, both themeshosts the subscription row

Anatomy

A list item holding two anchors and a divider. Everything below is drawn by design/system/components/save-focus-candidate.css and by subscription-row.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.

It is an ORGANISM from 2026-08-20, and it was filed a rung low for four stages. The ladder in docs/architecture.md says a thing that hosts a molecule is an organism, and what this hosts is the subscription row. The reason it stayed a molecule was never the ladder - it was that moving it changes the @import order in index.css, which nobody wanted to do mid-round. The founder settled it. Measured after the move: the 460px reflow still fires between 460 and 461, unchanged, which is the one thing the order could have broken.

Behaviour at width

A container query and not a media query since stage 10, at 28.75rem (460px). Under the threshold the candidate wraps, the cut control drops to its own line and pushes itself to the end, and the padding changes. The third rule of the block lives in subscription-row.css for a cascade reason and now carries the same container condition: the two halves have to agree or the reflow happens on one line and not the other.

The threshold is local, is not a token, and is in the register on Width. Zero pixels moved on the conversion.

Variants and sizes

The candidate has one form, so there is no matrix to draw. Both instances in the product render identically, and the census measured .cut, as it then was, among the sixteen families with a single value on every property, at both viewports. What it does have is a width behaviour, which is not a variant: below 460px the cut leaves the row's line and takes the end of its own.

WidthWhat happensWhy
460px and uprow and cut share one line, the row taking the slackThe natural shape: the name is on the left, the decision is on the right
under 460px.cand wraps, the row takes the full width, the cut drops beneath it and pushes itself to the endA merchant name, an amount and a second control do not share 360px without the name wrapping into the amount, and the name is the thing being decided about

That reflow is written in two files, and the reason is the cascade rather than tidiness. Two of its three rules are here; the third, .cand .row { flex: 1 1 100% }, is in subscription-row.css, because both rules about the row are 0-3-0, a media query adds no specificity, and molecules import alphabetically, so this file is read before that one. Written here, the reflow would have lost to the row's own flex: 1 on source order alone and would have silently stopped happening at 460, on the one screen in the product where two controls share a line.

What is NOT here Why the cell is empty Verdict
A destructive tone on the cut Forbidden with a reason rather than missing. DESIGN.md spends clay on OUR failure only, never on a thing a person chose to do, and D3 puts no paywall and no friction at the cancel moment. Cancelling is a small win, which the product's own principles call its most important emotional beat; a red button would tell a person they are doing something wrong at the exact moment they are doing something good prohibited
A disabled cut Forbidden with a reason. A subscription that cannot be cancelled is not listed as a candidate. Where the guide itself is unavailable, the product says so on the next screen with its own state, which is why cancel-guide-blocked exists as a page rather than as a greyed control here prohibited
A merge of .cut into .btn The inventory already recorded a.cut as a control with no family, dimensionally identical to the anchors in what was then nav.secondary and is nav.actions now. After the founder's review of the foundations the gap to the Button itself is down to two things, because 13px and 14px are both --type-body and 14px and 18px of side padding are both --space-16. It is not merged at step 5: renaming happens at step 6 together with the markup of the screens, and a merge here would leave the pixel comparison with nothing to prove a line in the backlog
step 9 decides
The two differences that are left.btn.cut
font-weight600400, inherited
hoverfill, edge and ink all changeedge and ink change, the fill does not
everything else--tap, --space-0 --space-16, --line-control, --radius-sm, --bg-surface, --type-body, --text-body: identical after the fold

Both rows above are read from the rendered page rather than from the first declaration in the CSS: getComputedStyle on the cut in the specimen and on the button in the anti-rule below returns min-height: 44px, padding: 0px 16px and font-size: 14px for each of them, and font-weight 400 against 600. That is the whole remaining gap, measured.

When to use it

This is J3 drawn: cancel what I do not use. It stands on one screen, the save-focus state of Home, under the line "Two you have not opened in a while. No pressure, just a nudge." Everything about the component is built around that sentence. Two candidates and not ten, so it is a nudge and not a purge. No urgency, no countdown, no saving shown as a loss. And the row keeps its ordinary shape, so a person can still just look at the subscription without deciding anything.

The second control is separately named because the cost of confusing the two is asymmetric. A person who meant to open a subscription and lands on a cancel guide has been startled by their money app, which is the one thing this product exists not to do. A person who meant to cancel and lands on the detail screen gives up. So there are two anchors with two accessible names, "Peloton App, $12.99 a month, trial ends in 17 days" and "Cancel Peloton App", rather than one row with an ambiguous target.

The cut is a way in, not the act itself. It opens node 4.9, the cancel guide, where the real commit lives as a filled button ("Open netflix.com and cancel"). Nothing is cancelled from Home. That is why the control here is quiet, outline and 400 weight: it asks a question, and the screen it opens answers it.

The rule, and the anti-rule

Two controls, two names

The row keeps its whole-row target and the cut sits beside it as a sibling. Two anchors, two hrefs, two accessible names, and a keyboard user reaches them in that order: open it, then end it.

Use a plain subscription row instead

Where there is no second action, the Subscription row is the whole component and this one is not needed. Do not reach for the candidate to get a slightly different list: the only thing it adds is the cut, and a candidate with nothing beside the row is a row wearing an extra divider. And never nest the cut inside the row to save a line: a link inside a link is invalid, and the version browsers accept gives a person two overlapping targets on the one decision that costs them money if they hit the wrong one.

A quiet way in

One word, outline, 400 weight, no tone. It opens the guide and commits to nothing, which is exactly what it should look like on a screen a person came to in order to look rather than to act.

Use a button instead

The act itself belongs to the Button, on the screen that explains what will happen. That is the filled action on node 4.9, and it says the result rather than the URL. Making the cut louder would move the commitment onto Home, where a person has not yet been told what cancelling does to their billing period, and D3 already forbids putting the paywall at that moment for the same underlying reason: nothing about cancelling is allowed to be a trap.

Limits

U13, context. PRESS. The cut control takes the press and the row around it does not: the candidate is read, and only its control is used. Everything in this product that moves travels exactly one --nudge, which is 2px, and nothing anywhere travels two. Hover and release run at --dur-state (150ms) on --ease-arrive; a press runs at --dur-press (90ms), because it answers a finger that is already down. The full language and the live specimens are on Motion; the table with sources is Usage rules.

States

The candidate itself has none. It is an li holding two anchors: no href, no hover, no ring. Its states are the two controls inside it, and one of them is not this component's: the row's hover and focus are in subscription-row.css and are documented on that page.

The cut has three, and they exist because it is a link. Three rather than four: there is no disabled cut, for the reason in the matrix above. Each state is a token override rather than a style of its own.

Its hover changes the edge and the ink and not the fill, which is the one place this control still differs from the button besides its weight. It is carried exactly as kit.css wrote it rather than aligned to the button, because aligning it would be a visual change with no line in any of the three lists allowed to produce one. The edge strengthens under the pointer, 3.46:1 at rest on the light surface against 5.78:1 hovered, which is what a hover is for.

Rest, live

Tab through it: the row takes focus first and the cut second, which is the order a person thinks in. Both are real anchors with a real href, which is the only way the states below can be reached at all.

The three, 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.

Restlight
--bg-surface fill, --line-control edge, --text-body label, --line-divider under the pair
Restdark
the same four roles, the other half of each pair. The control edge is 3.46:1 on the light surface and 5.15:1 on the dark one, both over the 3:1 that WCAG 1.4.11 asks of a boundary that identifies a control
Hoverlight
--line-control-hover and --text-primary. The fill does not change, which is the remaining difference from the button
Hoverdark
the edge strengthens in the dark theme too: 5.15:1 at rest against 6.40:1 hovered. The value it replaced was lighter than the resting edge, so the boundary used to weaken under the pointer
Focus visiblelight
--color-focus, 6.23:1 as a line, at 2px offset
Focus visibledark
--color-focus, 6.82:1. It earns its place here more than on most controls: the cut sits immediately after a full-width row in the tab order, and a keyboard user has to see that focus moved from "open it" to "end it"
Retake, and this is the whole recipe, because there is no node Playwright in this repo and a command that does not run is worse than no command: serve the repo, then for theme in (none, data-theme="dark" on <html>): goto /design/kit/save-focus-candidate.html move the pointer away -> screenshot #live-rest -> screens/save-focus-candidate-rest-<theme>.png hover #shot-cut -> screenshot #live-rest -> screens/save-focus-candidate-hover-<theme>.png focus #shot-cut by Tab (twice: the row takes focus first), assert :focus-visible -> screenshot #live-rest -> screens/save-focus-candidate-focus-<theme>.png Also shoot the frame at 400px wide once, to record the reflow: two rules of it live here and the third lives in subscription-row.css, so it is the one behaviour on this page that no single file can be checked against on its own. Reached the way a person reaches them: a real pointer and a real Tab. NO FILE COMES OUT OF THIS TODAY, and the recipe is kept for the day one is wanted rather than as a promise. The strip was removed by the founder on 2026-08-11 and the states above are described instead, so there is nothing here for step 9 to diff. The earlier wording said step 9 checks these files for a byte shift; it was written before the decision and outlived it, which is the same freeze it was warning about, in prose.

The technical half

ReadsWhich tokenGrowing from
the cut's fill--bg-surface--paper
the cut's edge--line-control--edge. The boundary that IDENTIFIES a control, 3:1 required, and .cand .cut is named in the six rules of the founder's contrast decision of 2026-08-11
the cut's label--text-body--slate
hover edge and ink--line-control-hover, --text-primary--muted, --ink
the focus ring--color-focus--petrol
the divider under the pair--line-divider--hairline-soft. Decorative, so no threshold applies
the cut's floor and corner--tap, --radius-smprimitive, straight. --tap and not --row: it is a button-shaped control standing inside a 56px line
label size, gaps, padding--type-body, --space-0, --space-4, --space-8, --space-16primitive, straight
the row inside itnonesubscription-row.css, both at rest and in the 460 reflow

Lives in design/system/components/save-focus-candidate.css. Stands on 1 grey page and 2 candidates, and that page is coloured, so the whole component renders in Petrol and Paper today: Home, save focus.

<li class="cand"> <a class="row" href="subscription-detail.html" aria-label="Peloton App, $12.99 a month, trial ends in 17 days, not opened in 3 weeks"> <span class="logo"><img src="visuals/logos/peloton.svg" alt=""></span> <span class="body"><span class="name">Peloton App</span><span class="when">trial ends in 17 days &middot; not opened in 3 weeks</span></span> <span class="amt">$12.99</span> </a> <a class="cut" href="cancel-guide.html" aria-label="Cancel Peloton App">Cancel</a> </li>

The two aria-labels are the component, more than the CSS is. Each control says what it does to which subscription, so a screen reader announces "Cancel Peloton App" rather than "Cancel", which on a list of two is the difference between a decision and a guess. The specimens on this page are written as div rather than li because the Save-focus list organism is what supplies the ul in the product; the class does the styling either way.