Verdict
Method demo · working prototype
Working prototypes, not screenshots. Try A, D and E on a case, then Z to undo.
A fraud reviewer decides in 45 seconds with a model whispering a number. The interface has to make that number arguable.
Client
Fintech · fraud review console
Brief from
Myself, with a two-week clock
Team
Solo
Users
None. Domain researched, not observed.
Role
Product design, design system, front-end
Surface
Internal ops console, desktop, dark-first
Duration
2026 · Two weeks
In short
- Self-initiated concept. Researched from published card-network and risk-ops material, not lived. No users, no deployment, no outcome claims.
- Built as a working prototype: three screens, two flows, sixteen components, all keyboard operable.
- 287 design tokens across three enforced tiers, with 68 contrast pairs measured by the build and zero below their WCAG gate.
- Exists to answer one question a hiring manager cannot otherwise check: is this person's work considered, or is it generated?
What it moved
The numbers, in plain sight.
287
Design tokens, three tiers
68
Contrast pairs measured, 0 below gate
16
Components, five categories
3.09:1
Contrast floor, both themes
The story · 11 chapters
How Verdict got built.
rev 00brief
What this is, and what it is not
❝Card processors flag a fraction of transactions for human review. A model scores each one, and a person approves or declines. Verdict is the console that person works in.
It is self-initiated. There is no client, nobody was interviewed, and nothing is deployed to production. I researched the domain from published card-network documentation and risk-operations writing. I have not worked in it, and the case would be worth less if I pretended otherwise.
So it claims no outcomes. What it claims instead is that every number below is a property of the repository, reproducible by anyone who clones it and runs the build.
A concept has no users. It can still have receipts.
rev 01investigation
A shift on the queue
❝Two people use this. A reviewer works 200 to 400 flagged transactions a shift, measured on both speed and accuracy, which pull against each other. A risk manager sets the threshold that decides how much lands in the queue at all.
Their problems are different in kind. The reviewer is drowning in context switches. The manager is flying blind.
Reassembly, not decision
Evidence sits across four tabs. Rebuilding context costs more than the judgement does, on every single case.
A number with no argument
The score arrives bare. Analysts either rubber-stamp it or ignore it, and both of those are failures.
Hesitation
No undo, so people slow down to be sure. Caution looks like diligence and costs throughput.
Silence after the fact
A wrong decline loses a good customer quietly. Reviewers rarely learn whether they were right.
Moving a line blind
The pain that earned its own screen
Nothing says what a five-point change costs in false positives, queue volume, or analyst hours.
No stated reason
Cases arrive with no explanation of why. Not every case is here because of the score, and the tool never says which.
rev 02insight
Risk is ordinal, so the traffic light is a bug
❝Almost every tool in this space renders risk as red, amber and green. That is a categorical encoding of an ordered quantity. It collapses a continuous score into three buckets, it fails colour-blind reviewers, and it dies on the projector in a shared operations room.
Verdict encodes risk as one hue climbing in lightness, plus an explicit rank, plus the printed numeric range. Three redundant carriers, so the ordering reads with no colour perception at all.
This is the decision the rest of the system hangs off. It produced the token architecture, the accessibility argument and the central instrument in one move.
Why the traffic light is a bug
Traffic light
what almost every tool ships
desaturated
Four ordered levels forced into three categories, and two of them are the same colour. Desaturated, the order is gone entirely.
Ordinal ramp, rank, range
what shipped
desaturated
One hue climbing in lightness, so the order survives desaturation. The rank and the range survive losing colour altogether.
rev 03decision
How the work actually ran
❝Six screens generated in an afternoon, five of them killed by the end of it. That is not waste, it is the method.
Slop is what happens when the generated artifact becomes the deliverable. Here the artifact is disposable by design: it exists to make an argument visible fast enough to reject. Nothing generated survives contact with the system layer, because the system layer is built after the exploration and rebuilt from tokens up.
The order matters more than the tools. Tokens before components, components before screens, screens before flows. Reverse any two of those and the result is a demo that cannot be extended.
How the work actually ran
01
Artifacts
Claude artifacts
Six whole screens in an afternoon
Five of them, on purpose
02
Tokens
TypeScript source, generated CSS
Three enforced tiers, a contrast gate
Every colour the artifacts invented
03
Components
Radix, CVA, Storybook
Sixteen components in five categories
The artifact markup entirely
04
Screens and flows
Next, real state, real keyboard
Three screens, two flows, one system
Nothing. This is what ships.
rev 04decision
Where the model gets a vote
❝Speed is only worth having if judgement stays where it belongs. So the boundary is explicit rather than intuitive.
The clearest way to show that boundary is not to describe it. It is to show what the model proposed and what shipped instead.
Where the model gets a vote
The model decides
I take the output as-is
- Layout permutations to look at
- First-draft copy for every string
- Boilerplate variants and prop plumbing
- Mock data volume and edge-case shapes
The model proposes, I decide
Its answer is an option, not a result
- Information hierarchy on a screen
- Which states a component must carry
- Naming, in the user's vocabulary
- Where a pattern is reused or forked
I decide alone
Never delegated, not once
- Token architecture and the tier rules
- The accessibility contract
- What the flow is, and where friction goes
- What the interface refuses to do
| The model proposed | What shipped, and why | |
|---|---|---|
| Risk display | Red, amber and green severity chips | One ordinal ramp with rank and range. Colour alone fails 1.4.1 and dies on a projector. |
| Destructive action | A confirm dialog on every decline | Optimistic commit with a six-second undo. A confirm seen 300 times a shift trains dismissal. |
| The score | A bare 87 in large type | 87 with its confidence span and its contributing signals. A bare number is obeyed or ignored. |
| Evidence | Only the signals that raised the score | Signals both ways, same weight and size. Showing one side turns a decision aid into a prosecution. |
| Queue order | Sorted by score, riskiest first | Sorted by time left. A case nobody decided is the one outcome you cannot recover. |
| Bulk actions | One confirm dialog for every bulk action | Approve commits with undo, decline asks first. The consequences are not symmetrical. |
| Why am I here | Nothing. The score was assumed to be the reason | Every case states why it is in the queue. Three of nine are held by a rule, not the model. |
rev 05decision
Tokens before components, and lint that enforces it
❝Three tiers. Primitives are raw scales that no component may name. Semantic roles are the only tier a component reads, and the only tier that differs between light and dark. Component tokens alias semantics and never reach past them.
The rule is worth nothing unless something checks it. The build exits non-zero on a tier violation, on a role defined in one theme and missing from the other, and on any contrast pair below its gate. A custom lint rule bans raw hex outside the token source.
The gate caught eleven failures on its first run. One of them was not a bad colour value but a wrong idea: a sequential ramp can never carry 3:1 between adjacent steps, because that is what makes it sequential. The band boundaries moved onto tick marks and printed ranges, which is where the contrast requirement actually belonged.
Every gated pair, measured · dark theme
- Disabled control label (WCAG exempts inactive components; measured anyway)text-disabled on surface-raised2.2n/a
- Escalate button boundarydecision-escalate-border on surface-raised3.13
- Band boundary ticksinstrument-tick on instrument-well-bg3.73
- Confidence span against the wellinstrument-confidence on instrument-well-bg3.73
- Decline-weighted mark in a chartdecision-decline-vivid on surface-raised4.33
- Label on the primary buttontext-on-accent on surface-accent4.84.5
- Ramp range, lowest band against highestinstrument-band-1 on instrument-band-44.93
- Decline button labeltext-on-accent on decision-decline-solid5.14.5
- Risk band R4 labelrisk-4-fg on risk-4-bg5.34.5
- Metadata and column headerstext-tertiary on surface-raised5.34.5
- Input and outline-button boundary in a panelborder-control on surface-raised5.33
- Metadata on the app backgroundtext-tertiary on surface-base5.94.5
- Input boundary on the app backgroundborder-control on surface-base5.93
- Approve button labeltext-on-accent on decision-approve-solid6.24.5
- Links and the threshold readouttext-accent on surface-raised6.94.5
- Focus ring inside a panelfocus-ring on surface-raised6.93
- Approve-weighted mark in a chartdecision-approve-vivid on surface-raised7.63
- Focus ring against the shellfocus-ring on surface-base7.73
- Row metadata under the cursortext-secondary on surface-hover7.74.5
- Risk band R1 labelrisk-1-fg on risk-1-bg7.74.5
- Threshold line against the wellinstrument-threshold on instrument-well-bg8.03
- Supporting text in a paneltext-secondary on surface-raised8.54.5
- Informational toaststatus-info-fg on status-info-bg9.24.5
- Declined chipdecision-decline-fg on decision-decline-bg9.64.5
- Risk band R3 labelrisk-3-fg on risk-3-bg9.94.5
- Cautionary toaststatus-warn-fg on status-warn-bg9.94.5
- Risk band R2 labelrisk-2-fg on risk-2-bg10.44.5
- Escalated chipdecision-escalate-fg on decision-escalate-bg10.74.5
- Approved chipdecision-approve-fg on decision-approve-bg10.84.5
- Selected row texttext-primary on surface-selected14.54.5
- Tooltip texttext-inverse on surface-inverse14.84.5
- Body text in a paneltext-primary on surface-raised15.14.5
- Body text on the app backgroundtext-primary on surface-base16.94.5
- Score marker against the wellinstrument-marker on instrument-well-bg17.73
123
primitives
73
semantic roles, × 2 themes
91
component tokens
0
tier violations
rev 06build
The category decides the API
❝Sixteen components in five categories, and the category is not a filing convenience. It determines what a component is allowed to know.
A primitive takes visual props and nothing else. The moment a Button understands what a decline is, every future consumer inherits fraud vocabulary it did not ask for. Domain components are allowed to be un-reusable, and saying so out loud prevents the usual failure, where everything is flattened into a generic library that fits nothing.
The base is shadcn on Radix rather than a hand-rolled headless layer. Radix supplies the parts that are cheap to get subtly wrong and expensive to find in an audit: indeterminate checkboxes, roving focus, dismissal on Escape.
The category decides the API
Primitives
7 components
Visual props only. Knows nothing about data or domain.
Compounds
5 components
Accepts data shapes, carries no business meaning.
Patterns
5 components
Encodes a decision. Reusable across domains.
Domain
5 components
Speaks the vocabulary of risk. Deliberately not reusable.
Layout
3 components
Owns page structure and focus regions.
rev 07build
Every state has a story, including the ugly ones
❝A component that only exists in its default state is a mockup. The states that break a handoff are the ones nobody drew: empty, loading, failed, one item, four hundred items, a customer name too long for the column.
So every component carries a story per applicable state, and the matrix below is the coverage, not an aspiration. Grey cells are combinations that genuinely do not apply, marked rather than quietly skipped, because an unmarked gap and a deliberate exclusion look identical in a screenshot.
Every state has a story
| default | hover | focus | active | disabled | loading | error | empty | long | dark | |
|---|---|---|---|---|---|---|---|---|---|---|
| Button | story exists | story exists | story exists | story exists | story exists | story exists | not applicable | not applicable | story exists | story exists |
| Input | story exists | story exists | story exists | not applicable | story exists | not applicable | story exists | not applicable | story exists | story exists |
| Checkbox | story exists | story exists | story exists | story exists | story exists | not applicable | not applicable | not applicable | not applicable | story exists |
| Panel | story exists | not applicable | not applicable | not applicable | not applicable | story exists | story exists | story exists | story exists | story exists |
| DataTable | story exists | story exists | story exists | not applicable | not applicable | story exists | story exists | story exists | story exists | story exists |
| RiskBadge | story exists | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | story exists | story exists |
| ScoreInstrument | story exists | not applicable | story exists | not applicable | not applicable | story exists | not applicable | not applicable | story exists | story exists |
| SignalList | story exists | not applicable | not applicable | not applicable | not applicable | story exists | not applicable | story exists | story exists | story exists |
| DecisionBar | story exists | story exists | story exists | story exists | story exists | not applicable | not applicable | not applicable | not applicable | story exists |
| UndoToast | story exists | not applicable | story exists | not applicable | not applicable | not applicable | not applicable | not applicable | story exists | story exists |
rev 08build
Two flows, two opposite philosophies
❝Working a case is optimised for speed. Moving the threshold is optimised for consequence. They sit in one system and behave nothing alike, and that is the point.
Consistency in a design system means the same components and the same vocabulary. It does not mean the same ceremony regardless of what is at stake. A tool that treated these two moments identically would be wrong twice.
| Working a case | Moving the threshold |
|---|---|
| Commits immediately | Staged, then confirmed |
| Six-second undo | No undo, so it asks first |
| Single-key shortcuts, A, D, E | No shortcuts at all |
| No reason required | A written reason, or it will not apply |
| Affects one transaction | Affects every transaction next week |
| Reversible | The customers you turn away do not come back |
The threshold screen shows the full distribution of a week of scored transactions, with the overlap visible. There is no setting that catches all the fraud and troubles none of the customers. Making the manager look at that trade, rather than letting them type a number, is the whole design.
rev 09decision
What I hand over, and what I do not
❝The riskiest claim a designer can make is that they ship production code. So this is stated as a scope with an explicit list of things I did not do.
What I hand a team is design-complete, typed, accessible and running, with mock data. What I do not hand them is a data layer, an auth model, error semantics, a performance budget or a test suite. Those are engineering decisions, and a designer who blurs the line there is asking to be trusted on ground they have not earned.
Treat the repository as a spec that happens to compile. If your stack differs, throw the code away and keep the contract: the tokens, the states, and the accessibility behaviour.
My lane, the shared lane, your lane
I ship this
in the repo, running, testable
- Typed components on Radix, variants via CVA
- Tokens as the only source of colour, enforced by lint
- Keyboard paths, focus states, roles and labels
- Every state as a story, including the ugly ones
- Motion tokens, reduced-motion honoured
- Mock data covering empty, single, excessive, failed
We settle this together
a conversation, not a handoff
- Which states the real API can actually produce
- What an error means to a user, versus what it means to a server
- Where my component boundaries meet your data boundaries
- Naming, once your domain vocabulary meets mine
Your engineers own this
I do not touch it, and I say so
- Data fetching, caching, state architecture
- Auth, permission enforcement, sessions
- API contracts, error taxonomy, retry and backoff
- Rendering strategy, performance budgets, bundle splitting
- Security, sanitisation, rate limiting
- Test coverage beyond interaction stories
- i18n extraction and full RTL
- Analytics, consent, the release pipeline
rev 10reflection
What building alone changed
❝For most of my career a Figma file was the deliverable. Someone else turned it into software, and the gap between what I drew and what shipped was somebody else's problem to explain.
Working without an engineering team on the other side of a handoff removed that comfort. A file stopped being a deliverable and became an argument I still had to prove. Verdict is what that looks like when nobody is going to catch the parts I got wrong: a contrast gate because I will not remember to check, a lint rule because I will reach for a raw hex at midnight, a matrix of states because I know which ones I skip.
The tooling changed too. Generating six screens in an afternoon is not the interesting part. The interesting part is that when artifacts become nearly free, the only thing left that is scarce is knowing which one to keep.
02. A risk review console built end to end, from token architecture to working keyboard flows.
Want this kind of work on your team?