Brindle Savings
Nothing in this app can send, transfer or spend
Rill is a cash-forecast app for one self-employed person. It connects to a single account, reads the balance and the payments already scheduled on it, and prints one figure: what is safe to spend between today and the end of a fourteen-day horizon. It is not a budgeting app, not an invoice book, not a tax calculator, and it contains no chart of any kind — no arc, no ring, no bar, no sparkline. The timeline is the visualisation.
Every name, figure, date, counterparty and institution in this project is invented. Rosa Kilbane, Brindle Savings, Alder & Frame Studio and INV-2026-014 do not exist; no real institution, person, rate, jurisdiction or product is named anywhere in the build. Rill states no tax rate, no threshold, no deadline and no obligation, and the words owe, liability and return appear in no copy slot. Nothing on this page or in the product is financial advice.
The first screen is a refusal, and the refusal is the product
Screen 1 states what the app will not do before it asks for anything. The support line under the title is the sentence the whole build is arranged around, and the first of four rationale rows opens onto a third line that closes the loophole in it.
Rill reads your account so it can show you what is already committed. It never moves money.
Nothing in this app can send, transfer or spend. The only figure Rill changes is the one you set aside yourself, and that stays inside your own account.
src/screens/Connection.tsx:96–98 · :37Two of the four rows are constraints rather than features — Read-only access and Disconnect any time — and the retention row states its window as prose rather than as a setting: a reading is a balance and a date. Rill keeps the last 90 days of them so it can show you what changed, and drops the rest. That 90 is a single constant, RETENTION_DAYS, and it is a constant because the static scan described in ⑤b caught it written out twice on its first run.
The wait that follows names its three stages — Reading your balances, Reading your scheduled payments, Building your timeline — instead of spinning. The counter that would tell a user they are on step 1 of 3 exists only as an accessible name; no step counter is painted anywhere in Rill.
in any state
Fri 20 Mar 09:41
One figure, and the subtraction beneath it
Home carries £1,240.00 at 48 px in IBM Plex Mono, and directly under it — not on a second screen, not behind a disclosure — the three numbers it came from: balance £4,182.60, committed −£2,192.60, reserve held £750.00. The subtraction closes exactly, and a reader can check it without leaving the screen.
Safe to spend is defined, not asserted. It is the lowest projected available balance across the horizon, where available is balance minus reserve minus everything committed to that date. The low point is £1,240.00 on Friday 3 April, which is the figure Home shows and the reason it needs no disclaimer: it is not a prediction of behaviour, it is the floor of an arithmetic printed on screen 3.
The row beneath the figure — 8 committed payments to 3 April — is itself the control that opens that arithmetic. The header row above it, Last reading 09:12 today · Brindle Savings, is the control that opens the record of where the numbers came from. Both are whole 358 × 44 rows; neither is a chevron a thumb has to find.
The one panel that exists to say a number is missing
Under the composition sits a block headed NOT COUNTED: £1,850.00 from Alder & Frame Studio is 14 days overdue. It is not in any figure above. An invoice fourteen days late is exactly the money a forecast is tempted to count, and counting it would make the forecast better than the evidence. Rill names the exclusion in words on the screen the exclusion affects.
to Friday 3 April
14 days
Nine rows, eight payments, and one word between them
The timeline is a single stream bucketed by week, with a right-aligned total on every bucket header — −£62.60, −£1,052.00, −£1,078.00 — and each of those totals is the sum of the rows drawn under it. The three add to £2,192.60, which is the committed figure on Home. Every row carries the balance it leaves behind, so the last projected balance, £1,990.00, is safe to spend plus the reserve.
The header counts 9 payments while Home says 8 committed payments, and that is not a contradiction: nine rows are painted and one of them, the inbound INV-2026-014, is marked not counted. Two different quantities, distinguished by the word committed — which is also the name of the segment the reading is on. Both are derived, and the agreement suite asserts each against its own population rather than against the other.
The rill, which is what the product is named after
A 3 px rule runs down the 44 px gutter beside every row, in lime for as long as the projection stays above the reserve. It is not a chart and it encodes one fact only. On this page the same rule runs down the left of every entry, and it changes colour at the same point the product's does.
430 is a reflow, not a scale
The sanity viewport widens the gutter 16 → 20 and the content 358 → 390; no type size changes and no region is added. Measured in Chromium on the running build: the timeline's scroll region is 618 px at 390 and 706 px at 430, so 4 rows sit wholly inside the fold at 390 and 6 at 430 — and Card repayment, cut by the fold at 390, is whole at 430. The segmented control's two labels measure 177.5 and 178.5 at 390 against 193.5 and 194.5 at 430.
- Scroll region height, measured in Chromium
- 618 px · 706 px
- Payment rows wholly inside the fold
- 4 · 6
- Segmented control labels
- 177.5 / 178.5 · 193.5 / 194.5
- Type sizes changed between the two
- 0
across three buckets
INV-2026-014
The money that is deliberately not counted
One invoice, £1,850.00, fourteen days overdue. The screen states its status, the obligation, both dates, the evidence and then the actions — in that order — and the expected-payment panel derives its date from the client's own history rather than asserting one: Both invoices Alder & Frame Studio has settled arrived 21 days after the due date. Under it, in risk-text: It is not counted in your safe-to-spend figure until it clears.
A destructive action that admits it changes nothing
The overflow menu offers two actions and names the destructive one in words rather than in an icon. Its confirmation is the sentence most products would not write, because it removes the reason to feel anything about pressing the button:
The invoice and its expected payment are removed. Your safe-to-spend figure does not change, because this invoice was never counted in it.
src/screens/InvoiceRisk.tsx:228–230The cancel is labelled Keep the invoice, not Cancel, and it takes initial focus. The destructive item is set in risk-text and is not a filled button — there is no filled destructive control anywhere in Rill, and a Playwright test asserts that across the invoice and timeline surfaces.
One thing the flow refuses to be
The Invoices tab opens on a single invoice rather than a list. The critical slice is recovering one overdue invoice, so a list view is out of scope by decision: STATE_INVENTORY.md enumerates no list state, and the case study is not going to claim one.
in no figure above
3. Cash Timeline
The day the projection crosses its own line
A ninth committed payment arrives in a later reading — equipment insurance, £1,610.00 on 30 March — and the projection falls below the £750.00 reserve on Thursday 2 April at £476.00, ending the horizon £370.00 under it. Three things change and one thing deliberately does not.
The gutter rail gains a crossing marker at 2 April and the lime stops there. An advisory band sits under the summary carrying one sentence and no figure of its own. Home's hero reads £0.00 and its derivation row becomes 9 committed payments take you £370.00 below your reserve.
The figure is not recoloured and no error icon appears. A correct number in an alarm colour teaches a user that the product is broken; the hero stays in ink, the summary panel stays unrecoloured, and the sentence carries the alarm. £0.00 is also a floor, not a negative: the arithmetic is £4,182.60 − £3,802.60 = £380.00, which is £370.00 short of the reserve, and the hero prints £0.00 rather than a negative number the user cannot spend.
The decline leaves the marker, because the marker is data
Show the payments after 2 April filters the stream to the rows from the crossing onward and states in words how many were hidden. Not now closes the band for the session — and the crossing marker on the rail stays, because the band is advice and the marker is a fact. Reopening the screen brings the band back.
at the horizon
the horizon
The consequence is printed before the button
Both affected balances are on screen before the amount is asked for. Choosing £400.00 writes two before-and-after rows — £1,240.00 → £840.00 and £750.00 → £1,150.00 — and then a sentence that stops the user believing the wrong thing about what they just did:
Your lowest projected balance is still Friday 3 April. Setting money aside moves the line, not the forecast.
src/screens/TaxBuffer.tsx:218The confirmation replaces the selector in place: a stamp, 20 Mar 2026 09:43, the two figures that changed, and the sentence that makes this screen consistent with screen 1's promise.
It stays in your Brindle Savings account. Rill has not moved anything; it has changed what it counts as spendable.
src/screens/TaxBuffer.tsx:111–113There is no confetti, no success animation and no tick that fills. Back on Home both figures carry the move and the balance and committed rows are unchanged, because only one of the three inputs moved.
A spec that could not be satisfied, resolved in favour of the presses
SCREEN_SPEC.md §5 asks for a £25 minimum, a £25 step and a maximum of "the whole of safe to spend", £1,240.00. Those three cannot all hold: 1,240 ÷ 25 = 49.6, so the highest amount the stepper can reach is £1,225.00. The grid wins, because it is what a user's presses actually produce, and the disabled-step reason names £1,225.00 rather than a figure no press can reach. A test drives the field to 99999 and asserts it settles at £1,225.00 with impact rows of £15.00 and £1,975.00.
reserve £1,150.00
6. Sync Recovery
Two days old, and the record says so three times
When the newest reading is 18 March and the clock reads 20 March, a 44 px band docks above the tab bar naming the source and the age. Every figure keeps its value and its position. Nothing is blanked, nothing is greyed, no dialog opens — and the band has no dismiss control, which makes it the one non-dismissible surface in the product. A forecast may not let its user hide the age of the data it rests on.
The data screen states the consequence in the user's terms rather than the network's:
These figures are 2 days old. Anything you have spent since 18 March is not in them.
src/data/derive.ts:411Underneath, the sync record has three rows, because three things happened: a session ended by the source at 07:12 with No reading, the 18 March reading, and the original connection on 2 March. A fourth row describing a 09:12 reading was in the delivered source and was removed — in this world that reading never arrived, and a record listing it above a panel saying it did not is the ⑤b defect with a list instead of a badge.
The third edge is this one with the failure surfaced. Try again succeeds at 09:44, the failed attempt stays in the record, and every figure updates. Enter a balance by hand opens a numeric field pre-filled with the last good figure, and a figure entered that way is marked entered by hand beside every place it appears.
3 rows in the record
the empty range
An empty range that routes out instead of apologising
The one empty state in the slice does three things in order: it says what is true, it says what Rill does have, and it offers the one control that leads somewhere.
Nothing scheduled in this range.
Rill has readings from 2 March. Switch to Committed to see the eight payments already scheduled.
src/screens/CashTimeline.tsx:298 · src/data/derive.ts:380Above it sits the only raster in the product's own UI: a closed cloth-bound ledger, unbranded, carrying no readable text, no lettering, no numerals, no hand and no coin. It is a photograph of the object the product is a version of, not an illustration of a shrug. The count in that sentence is written in words by a derived helper, so eight cannot drift away from 8.
Why no text run in this build is unmeasurable
axe cannot compute a contrast ratio for a run whose ancestor carries a background image, and reports it indeterminate — a report of zero violations that leaves runs unmeasured has not measured them. Rill sets no text over a raster in any state: the ledger photograph carries none, and the paper grain is a 3 % pseudo-element under the whole frame rather than an ancestor of anything. The audit's contrastIndeterminate count is 0 across all 55 renders, and that is a measurement rather than a waiver.
one segment away
Playwright
The same morning again, walked once, without a cut
Everything above is a frame held still. This is the morning at its own pace: 27.12 seconds, one take, no cuts and no speed change, recorded straight from the running Vite build at the primary viewport. Every step goes through a control the product exposes, resolved by role and accessible name, using the same drivers the acceptance suite uses. There is one navigation in the whole take — the first goto('/') — and no seed.
The path is T1, T3, T4, T5, T6, with one detour into the write-off dialog, because that dialog is the product's argument in a single sentence. The pauses exist so the screens can be read — the only thing directed here is the pace.
What the take does not contain, and why
No edge state appears. All three live in a seeded world, and there is no forward path to any of them from the healthy one: the copy deck stamps every one of them before the session opens, so no sequence of presses reproduces them. Reaching one would mean a second navigation, which is a cut. T2 / R1 — Home to the data record and back — is left out for length; it is covered by the acceptance suite and by EX-06. Saying so here is cheaper than a take that pretends otherwise.
The frame this file reports is the frame it contains
The two SaaS takes in this portfolio declare a video size equal to their viewport. The two earlier mobile takes declare 780 × 1688 against a 390 × 844 viewport — and Playwright only ever scales a page down to fit a requested size, never up. Measured on the shipped bytes, both files carry the page in a corner and pad the rest: content bounding box 394 × 848 and 394 × 846 inside a 780 × 1688 frame, with 74.9 % and 74.7 % of every sampled frame a flat #7E7E7E. This take declares 390 × 844, which is the same picture at the same fidelity with none of the padding.
- Length, measured with ffprobe — the §17 window is 20–30 s
- 27.12 s
- Frame, VP8 in WebM, no audio track
- 390 × 844
- Frame rate, and frames counted in the file
- 25 fps · 678
- Bytes on disk
- 1,434,986
- Padding, as a share of every sampled frame
- 0 %
- Caption cues, each read against its own frame
- 14 / 14
- Cuts · speed changes · seeded states · stubs · navigations after the first
- 0
- CoversConnection, one rationale row opened, a provider chosen → Connect Brindle Savings → the three named stages → Home with its one-visit band → Home standing → the derivation row → the committed stream, scrolled → the inbound row → the invoice → the overflow menu → the write-off dialog → Keep the invoice → Set money aside instead → Reserve → Move £400.00 to reserve → the confirmation → Back to today at £840.00. It ends on Home, holding.
- Captions14 cues in media/rill-flow.vtt, written by the run that recorded the take, off the same clock as the beats — so the two cannot drift apart. The recording is silent; the cues describe the action. A cue may name only what is drawn or written inside the frame it is timed to — not a value that lives in an accessible name or below the fold. Each of the 14 was read against a frame extracted from the shipped file at an instant inside its own window.
- Driven byAccessible names only, through tests/states.ts — the same drivers the acceptance suite uses. The build has no QA route and no debug hook, so every frame is a surface a user can reach.
- Recorded bytests-video/critical-flow.spec.ts via playwright.video.config.ts, both under apps/rill/. Its testDir is ./tests-video against the acceptance suite's ./tests, so the take can never join or alter the acceptance count — which is 154 before and after.
There is no poster image: the element carries none, so the frame a reader sees before pressing play is the take's own first frame, which is the Connection screen. No QA control appears in any frame. A throwaway Playwright pass replayed the take's choreography and read every control's accessible name, the painted body text and the page HTML at each of the 14 dwell states the take holds on — 133 controls in all: 0 States / Help / Reduced motion / Debug / Inspector control, 0 role="switch", 0 leaked state IDs, 0 painted occurrences of seed, fixture or world, and 0 occurrences of the concept disclosure. The one data-testid the sweep found in any frame is underlay, an inert 390 × 844 transparent element react-aria-components renders under its own popover; it carries no text and it is not Rill's.
14 cues · 0 audio streams