---
title: "One band, one owner: design rules from a one-thumb ride screen"
slug: one-band-one-owner-ride-dock
summary: "A screenshot of my MTB route editor's ride screen showed five controls buried under the search box while every test was green. Fixing it produced a handful of rules I now want to carry to every app: named layout bands with a CI gate, a primary button that never moves, transparent surfaces that own no hits, and a screen that never says \"saved\" before the write returns. Each rule maps to an older principle, from Fitts's law to write-ahead logging, and a read-only review by a second model found what 111 passing tests had not."
tags: ux, design, mobile, maps, reitti-2000, ai, agents, cycling
ai_model: claude-fable-5-1, gpt-6-astra, grok-4.7
published_at: 2026-10-24T05:38:20.158+00:00
canonical_url: https://jarkkolietolahti.com/articles/one-band-one-owner-ride-dock
markdown_url: https://cemecdwfdvdbvpvowuln.supabase.co/functions/v1/content-md?slug=one-band-one-owner-ride-dock
---

I ride mountain bike loops in Nuuksio and record them with my own route editor, Reitti 2000. The recording screen has to work with one thumb, with the phone on the stem or in one hand, while the other hand is on the bar. This week I took a screenshot of that screen on my OnePlus and looked at it properly for the first time in a while. The search box sat on top of five controls in the top row: undo, redo, fit-to-route, Help and the AI launcher. At the bottom, the collapsed workspace handle covered the status chip from the first letter of "Waiting for checkpoints…". Every automated test was green.

This is the third post in a series about interface rules I keep arriving at. [Broowser](https://jarkkolietolahti.com/articles/broowser-ux-kohokohdat) was about a glass tab bar where nothing moves by itself. [Teerminal](https://jarkkolietolahti.com/articles/projects-on-the-left-sessions-on-top) was about putting the stable thing on one axis and the changing thing on the other. This one is about a map screen that has to stay usable when I cannot look at it for more than a second. The rules below are the ones that survived building it. Where a rule matches something better known, I say so.

The work itself was done by coding agents from a plan and then reviewed by another one, read-only, in nine passes. I come back to what that review found at the end, because it is the part that changed how I think about tests.

![Plan on the left, Ride on the right: the planning tools leave the map in Ride and the dock arrives with its primary button already in place.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-plan-vs-ride.png)

## One band, one owner

The buried controls had a simple cause. The map surface had no layout budget. The search box positioned itself at the top with its own numbers. The undo buttons, the fit button, help and the AI launcher each positioned themselves too. The status chip and the workspace handle both chose the bottom edge. None of them knew about the others, and the architecture guarantees that: features in this app are isolated and cannot import each other. Each one looked fine in its own test.

The fix was to name the bands. On a phone every floating element now anchors to one of a handful of CSS custom properties: two rows in the top band, a map-controls row, the Ride dock, and the fixed mode row at the bottom. Nothing on the map surface may hard-code its own bottom or top offset any more. The rule is written into the project's agent guide as "one band, one owner", and a Playwright gate enforces it in CI before deploy. The gate walks the phone shell at 360, 390 and 412 pixel widths, plus a short 390 by 420 viewport and the OnePlus viewport with the keyboard open, through Plan, Library and Ride with the sheet open and closed and while recording. It fails on pairwise overlap, on a control that is not the element at its own centre point, on anything off-screen or within 8 pixels of the bottom edge, on a button or input under 44 pixels, and on a clickable chip under 36 pixels tall. It does not audit dialogs or popovers; those own their own layout. A deliberate overlap has to be allow-listed with a reason and a ticket id.

The closest well-known idea is a design token. A band is a shared name for a position, the same way a colour token is a shared name for a colour. Once the position has a name, two features can agree on it without ever seeing each other. Nielsen's [consistency and standards](https://www.nngroup.com/articles/ten-usability-heuristics/) is the user-facing half of the same rule.

## The primary action does not move

In Ride mode the dock has one large filled button and one outlined one. The filled button reads Start, Pause or Resume depending on the phase. In the current build it is 64 pixels tall and it never changes position. The status text above it grows upward when the elapsed time and distance appear, so the button stays where my thumb learned it was. The More button opens its sheet upward, and the sheet is not allowed to cover the primary.

![Recording: phase, elapsed time and distance above a fixed Pause button; More beside it.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-recording.png)

Two older ideas carry this, and they carry different halves. [Fitts's law](https://www.nngroup.com/articles/fitts-law/) supports the size: a large target close to where the thumb already rests is fast to hit. [Spatial memory](https://www.nngroup.com/articles/spatial-memory/), the same NN/g piece I leaned on for Broowser, supports keeping it still: the rider learns one position, and every phase change that moves the button throws that away. The low placement is my own grip, not a proven rule. Steven Hoober's [How Do Users Really Hold Mobile Devices?](https://www.uxmatters.com/mt/archives/2013/02/how-do-users-really-hold-mobile-devices.php) is a census of grips, and its lesson is that people shift between them; a phone on a stem mount is a different case again, and I have not measured it yet. The 44 pixel floor is Apple's point size and WCAG's [enhanced target size](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html), the AAA criterion. The AA minimum, [2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html), is only 24. On a bike I treat 44 as the floor for secondary controls, and the plan calls 56 to 64 for the primary a test goal rather than proof with gloves.

There is a small timing rule attached. After a Pause or Resume the primary is disabled for 500 milliseconds and repeated taps are dropped. A second tap inside that window on a bumpy trail should not turn a pause into an unintended resume. Nielsen calls this error prevention. In the handler it is a cooldown on a non-idempotent action.

## Transparent surfaces own no hits

The dock and the mode row have no card behind them. The map shows through between the buttons. I wanted this because the map is the content and a white panel over the bottom quarter of it was the single ugliest thing in the old screenshot.

Transparency has a technical consequence that is easy to get wrong. A transparent wrapper is still a DOM element, and if it accepts pointer events it swallows map gestures across its whole width. So the wrappers are `pointer-events: none` and only the real buttons take hits. The gate's centre-point check catches a button that something else has covered; the wrapper CSS is what lets gestures through.

The other consequence is legibility. White text over a busy map tile is unreadable, and the OnePlus screen is bright. The phase and distance sit on a small dark pill with a text shadow rather than on a panel, and after I looked at the first build on the phone the More button got the same dark translucent fill, because a two-pixel white outline disappeared over a forest tile. This is the same lesson as the glass tab bar in Broowser: a see-through surface only looks right against the thing behind it, so it has to be judged against real tiles, not a mock-up.

![More opens upward as a stack of full-width actions and never covers Pause.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-more-open.png)

## Chrome steps aside while the map is touched

The rule I like most came last. While I hold, drag or pinch the map, or drag a checkpoint, every floating control fades to 15 percent: the top row, the status chips, locate and zoom, the dock, the mode row. When the gesture settles, including the inertia after a flick, it all comes back. A quick tap does not trigger it, and neither does a programmatic recentre or the zoom button. An open More sheet, the destination picker, dialogs and the checkpoint context menu never fade, because the map does not pass under them.

![Fingers off the map on the left, holding the map on the right: the chrome has faded to 15 percent and the trail is fully visible.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-fade.png)

The reason is the trail. On a phone the bands cover a third of the map, and the part of the recorded track I most want to see is usually the part under my thumb. Fading the chrome while the map is the thing being manipulated gives the content back exactly when it matters, which is the oldest argument for [direct manipulation](https://www.nngroup.com/articles/direct-manipulation/): the object you are working on should be what you see. Video players have done the same with their controls for twenty years.

Two implementation rules made it safe. It is opacity only: hit-testing, focus and layout are untouched, so a tap that lands on a faded Pause still pauses, and the overlap gate's geometry still holds. And it has a floor: a hold has to last about a quarter of a second or move more than six pixels before anything fades, a missed end event is cleared by a safety timer, and the fade is skipped entirely under the system's reduced-motion setting. The blur behind the panels is also suspended while fading, because the Android WebView dropped frames when it had to blur and pan at once. The plan for this was reviewed by three models before anyone wrote code, and the end-to-end spec covers the hold, the pan, the wheel, the marker drag, the context menu and a window blur mid-gesture.

## The screen says what is true, and nothing is saved until it is

A recording is the one thing on this screen that must not be lost. The old Stop button stopped the ride and then wrote to storage. If the write failed, the screen had already said the ride was done.

The order is now reversed and the wording follows the state. Finish is a request: it opens one confirmation, a second request while that dialog is open is ignored, and Cancel leaves the phase, the trace and the navigation untouched. On confirm, the trace is written to the device's pending list first. Only when that write succeeds does the recorder clear its active flag, announce completion, or say "Saved on this device". If the write fails, the ride stays active, the dock reads "Not saved — still recording", and Retry and Export appear. A finish with zero fixes says "No points were recorded" instead of claiming a save.

![Finish asks once. Nothing is cleared until the write to the device returns.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-finish-confirm.png)

Nielsen's first heuristic, visibility of system status, is usually quoted about spinners. The stronger reading is that the interface may not describe a state that has not happened yet. The engineering version is the rule a database commit follows: do not report success until the write you mean is durable. A ride recorder on a phone whose local storage quota is full is a small instance of the same problem.

The reload path got the same treatment. The per-fix write of the live trace is now allowed to fail without freezing the map, and a failed write marks the ride as not saved rather than silently stopping recovery. When the app restarts, a pending row and an active flag with the same ride id are reconciled as one ride, and the longer copy of the trace wins. That last rule exists because the review found a case where a reload replaced the full retained ride with a stale shorter copy.

## Two lifecycles, no coupling

Recording a track and navigating to a destination are separate things that happen to share a screen. Stopping navigation does not touch the recorder. Finishing a recording releases the map's recenter lease but leaves active guidance alone, and the navigation feature keeps its own GPS subscription. Switching to Plan while recording hides the dock, keeps the recording running, and the fixed mode row brings me back to Pause in one tap.

![Plan mode while a recording continues: the dock steps aside, the mode row stays, nothing stops.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-plan-while-recording.png)

This is separation of concerns, which is not a UX principle, but it is why the UX rule can be kept. If the two features shared state, a Stop navigation button would eventually stop the wrong thing. Nielsen's user control and freedom covers the visible half: the rider can leave either activity without being asked about the other.

## The arrow points through the air, and the card says so

Destination guidance during a ride is the feature I use most after recording itself. Pick a target, and a card at the top shows an arrow and two numbers: the direct distance and bearing through the air, and the routed distance over trails when the router has returned one. On the map they are two different lines: a dashed one straight from my position to the target, and a solid purple one along the way the router found. The card never shows one number pretending to be the other. The routed distance appears only when a real route came back; offline, or when the router fails, the card says direct only and the dashed line is all you get. Within 20 metres on an accurate fix it says Arrived.

![Recording and navigating at once: the card reads 1.2 km direct, 2.4 km route; a dashed bearing line and a solid routed line leave the GPS position.](https://cemecdwfdvdbvpvowuln.supabase.co/storage/v1/object/public/article-images/one-band-one-owner-frame-navigating.png)

The rule underneath is that a straight line is never labelled a trail. In a forest the difference between 1.2 km through the air and 2.4 km around the bog is the difference between a good evening and a wet one. The same honesty applies to the fix itself: a position older than 15 seconds or worse than 30 metres hides the guidance instead of guessing, and the card never substitutes the map centre for the rider. The arrow is a target bearing, not a heading, and it subtracts the map's rotation so it still points the right way after I pan. Routing is throttled to a request every 25 metres or ten seconds, a late answer can never overwrite a newer one, and a hung request is abandoned after 20 seconds, so a stale route cannot draw itself over a fresh position.

This is Tufte's graphical integrity applied to a two-line HUD: the drawing must mean what the label says. Nielsen's match between system and real world is the softer version. The recording in the same screenshot is unaffected by any of it; Stop navigation leaves the trail running, as the previous section promised. The ride recorder has its own, older version of the same idea: drift more than 40 metres off a planned route and it draws an air-line back to the nearest point and asks the router for a way home, labelled as two separate things.

## Plan is for planning

In Ride mode the top of the screen holds two things: Layers and a compact search trigger. Undo, redo, fit-to-route, help and the AI launcher are gone from the map and live in Plan or in the Ride details sheet. Choosing a destination opens a picker at the bottom of the screen that moves up with the keyboard, so the thumb never has to reach the top row during a ride.

Each feature hides its own floating control when the shell announces a mode change; the core never reaches into a feature's DOM. I first wrote this down as progressive disclosure, but it is not: nothing is revealed deeper inside the task. It is plain mode-based organisation, and it is the part of the design that made the top band affordable in the first place.

## Green tests are where the review starts

Everything above was implemented by coding agents. A Codex session running GPT-6 Astra coordinated, planned the row allocation, and integrated. Separate worktrees built the shell (Claude), the Ride dock (Grok) and the navigation HUD. Then I had Claude Fable 5.1 review the integration branch read-only, with no permission to edit, build or touch the phone, and asked it for at most five source-backed blockers per pass.

By the middle of the review the branch's own suites were green: 30 of 30 ride lifecycle tests, 81 of 81 end-to-end. The review still found things over nine passes, and most of them were design rules that held in code but broke in a state nobody had tested:

- Opening More and then tapping Plan on the fixed mode row left a body class behind, and that class hid the locate, zoom and status controls in every mode until the rider came back and tapped Close. The dock disappeared correctly; its side effects did not.
- Hiding the mode row while Settings was open fixed a buried submit button and created a dead end: open Settings, tap "Full map", and the screen had no row, no sheet and no Settings button. The fix was one CSS selector narrower, hiding the row only while the sheet is actually open.
- Two popovers had a 120 pixel minimum height. On the short 390 by 420 viewport in Ride mode, that floor pushed the legend under the top band and the layer list under the status chips, the exact overlap the gate was built to prevent. The gate walks that viewport but does not audit popovers, so it could not see it.
- Start, Pause and Resume changed memory before their storage write. A failed Pause could mark the ride paused in memory, ignore new fixes and show Resume, while the device still said the ride was running. The finish path already waited for the write; the other three did not.
- The reviewer was also wrong once, and the mistake is worth keeping. It claimed the post-ride dialog's `autofocus` attribute could never fire on a device, because the document-level autofocus algorithm discards candidates once anything has focus. The dialog is a native `<dialog>` opened with `showModal()`, which has its own focusing steps that honour `autofocus`. The fix that followed, an explicit focus call after `showModal()`, is harmless, and the test now taps a button first, but the finding was not a bug. The peer review of this draft, by GPT-6 Astra and Grok 4.7, caught it.

Each real finding became a test. That is the point I want to make, and it is the same point as the wedge bug in Broowser that only a 60 fps screen recording caught. Dijkstra's line that testing shows the presence of bugs and never their absence is usually quoted as a shrug. I read it as a budget: a passing suite tells you what you thought to check. A second pair of eyes, human or model, told to find the state you did not think of, is cheap compared to finding it on a trail. And the second pair of eyes needs a third one, because it will be confidently wrong about something too.

What I would reuse from this screen is the fixed primary action, shared named positions with a machine checking them, and a save message that is tied to an actual successful write. The app is private and built for one rider, so I cannot offer a demo. I have not yet ridden with this build, although I have recorded 203 km with the app on earlier ones, which is more than ten laps of Lake Sääksi. The next test is the real one: a loop around Lake Sääksi with gloves on, recorded with this build, and the screenshot I take afterwards. If you ride there too, the [Sääksi shared adventure](https://saaksi.bike/seikkailu) is running: you ride, the audience helps you find the spots, and you collect points with task photos in the [Sääksi Bike app](https://apps.apple.com/fi/app/s%C3%A4%C3%A4ksi-bike/id6806614306). The Android version is in beta and looking for testers; the site is the place to ask.

*Written with Claude Fable 5.1, which also did the read-only review; the screen was built by GPT-6 Astra (Codex), Claude and Grok, and this draft was peer-reviewed by GPT-6 Astra and Grok 4.7.*

