pristine
All examples

Operate · Platform

Mobile web app

A transit ticket app rebuilt for the phone in your hand: live service status and the active ticket up top, zone prices in context, a ride counter that shows when a day pass pays off, a pay bar in thumb reach, and a ticket that proves it isn't a screenshot.

Persona
Mira Okafor-Lindqvist, Daily commuter
Their goal
Buy the right ticket for herself and her son with one thumb on a moving platform, then show it at the gate in seconds.
Lenses applied
adapt, delight, clarify, harden, layout, animate, colorize
/pristine polish src/pages/tickets.astro

Full pages. Scroll either one; they move together.

Before3.0/10

Mobile web app before Pristine

After9.5/10

Mobile web app after Pristine

Verdict

3.0/10 Level 2

A stock teal mobile-app template: pill filters, white cards and an orange badge on everything, so it looks like a phone app but behaves like a demo. Prices only show on hover, 14px inputs zoom the page, and the Buy bar and 52px tab bar sit on top of the ticket's own buttons and under the home indicator. Mira can't see what she's paying until the alert says it worked.

9.5/10 Level 10

Mira opens to everything she needs on one screen: the F1 ferry is running late, her current ticket has 72 minutes left, and zone prices sit on the tiles. She taps how many rides she'll take and a small chart shows singles cost less than a day pass until ride 3. Pay sits above the tab bar, and the ticket screen shows a moving band and a ticking clock that an inspector can trust. On a laptop it's a proper two-column page, not a phone floating in empty space.

Scores

Every Dimension, before and after.

Dimension scores out of 10
DimensionBeforeAfterChange
Persona fit310
Hierarchy & clarity310
Layout & spacing49
Typography49
Color & theming49
Consistency49
Responsiveness310
Motion & interaction39
Accessibility29
Copy310
Robustness & performance39

The critique

What the red pen found.

Vibe-code tells

  • Stock mobile-app template look
  • Teal app bar with hamburger and Login
  • Identical white rounded-2xl cards with shadow-md
  • Marketing 'NEW' promo card above the task
  • Orange badge for NEW, Soon and ACTIVE alike
  • Hover-only price tooltip
  • 'Coming soon' dead pill
  • 14px inputs that zoom on iOS
  • Sub-44px tap targets
  • Fixed bars ignoring safe areas
  • alert() for confirmation
  • outline: none on focus
  • transition: all 0.3s ease-in
  1. P0 Lens: clarify

    Prices are hover-only

    The Single / Day Pass / 7-Day pills hide their price in a tooltip that appears on :hover. On a phone there is no hover, so Mira picks a ticket without seeing what it costs.

  2. P0 Lens: adapt

    Inputs zoom and targets are tiny

    Selects and inputs use `font-size: 14px`, so iOS zooms the page on focus. The pills are 12px text with 5px padding, the tab labels are 9px, and Refund, Share and Delete are 11px pills side by side, all well under 44px.

  3. P1 Lens: adapt

    Stacked fixed bars cover the content

    `.fixed-bar { bottom: 52px }` sits on a `.tabbar { position: fixed; bottom: 0; height: 52px }` with no safe-area padding and no bottom padding on the page, so the ticket's Refund, Share and Delete hide behind the bars and the tab labels sit under the home indicator.

  4. P1 Lens: distill

    Template sameness, nothing leads

    Every block is the same white rounded-2xl card with shadow-md, every choice is the same teal-50 pill, and orange-500 marks NEW, Soon and ACTIVE alike, so a valid ticket looks like a promo. A marketing card for 'Get Started' takes the top of the screen while service status is nowhere.

  5. P1 Lens: clarify

    No ticket to show

    After paying, Mira gets `alert("Payment successful!")` and no clear place to find the ticket or see how long it's valid.

  6. P2 Lens: animate

    Slow, desktop-style feedback

    `transition: all 0.3s ease-in` and focus outlines removed. Taps feel late, the gray tap flash shows, and there's no pressed state.

The changes

Before, after, and why.

The report Pristine hands back after a run, with the real values from the two pages above.

LensBeforeAfterWhy
adapt.fixed-bar { position: fixed; bottom: 52px; padding: 8px 16px } over .tabbar { position: fixed; bottom: 0; height: 52px }.app { height: 100dvh; display: flex } with .scroller { overflow-y: auto; overscroll-behavior: contain }, .paybar { position: sticky; bottom: 0 } and a tab bar padded with env(safe-area-inset-bottom)The total and Pay sit above the tab bar and clear of the home indicator, and nothing slides under them as the URL bar shows and hides.
delightTicket type picked blind; no way to compare"How many rides today?" chart: four tappable bars (singles add up per ride) under a dashed day-pass line at €12.30, overspend shaded red, with "2 rides on singles: €8.40, €3.90 less than a day pass. It pays off from ride 3." and a Switch link when the other ticket is cheaperMira sees the break-even point instead of doing fare maths on a platform.
clarify.price-tip { opacity: 0 } shown on .type:hover over rounded-full pillsThree zone tiles (A / A + B / A + B + C) with ring diagrams and the price on each tile, which switches between single and day-pass faresPrice is visible before the choice, with no hover needed.
clarifyNo service informationStatus line under the title: "Trams and buses on time · F1 ferry about 10 min late", repeated on the matching route in Plan tripThe one thing likely to change Mira's trip is visible before she pays.
hardenalert("Payment successful!")Pay shows a spinner and "Paying" for 900ms, then the Tickets view opens with a moving striped band, an HH:MM:SS clock that ticks every second, a large QR and a validity meterThe ticket is one tap away, and a live band and clock let an inspector tell it from a screenshot.
hardenCard problems surface only when a payment failsAmber dot on the Account tab, "Visa ··4417 · expires 10/26" by the Pay button, and an inline expiry form that rejects 10/26 and 13/29 with specific messagesThe problem shows up while it's still cheap to fix, not at the gate.
adapt.form select, .form input { font-size: 14px; padding: 6px 10px; }; .mini pills at 11px; .tabbar a { font-size: 9px }16px inputs with inputmode="numeric" enterkeyhint="done"; 44×44 steppers and min-height: 44px on every controliOS doesn't zoom on focus, the number pad opens for rider counts, and every target works one-handed on a moving tram.
layout5 icon tabs with 9px labels plus a hamburger and Login in the app bar; desktop stretches the phone app bar and tab bar to 1440px4-item bottom tab bar on phones; from 960px a sticky top nav, a buy form on the left and a sticky 360px order summary with line items and Pay on the rightEach screen size gets a layout built for it, not a phone column floating in empty space.
animatetransition: all 0.3s ease-in; default gray tap flash-webkit-tap-highlight-color: transparent; :active { transform: scale(0.97) } over 120ms cubic-bezier(0.23, 1, 0.32, 1); chart bars grow via scaleY with a 60ms stagger; band and bar growth stop under prefers-reduced-motionTaps feel immediate, motion animates only transform and opacity, and the only constant motion is the band, which has a job.
colorizeNo theme-color meta; hover colors stick after a tapTwo theme-color metas (#F6F7F5 light, #121514 dark); hover styles inside @media (hover: hover); amber --warning only for the ferry delay and the card expiryThe browser chrome matches the app, touch doesn't leave stale hovers, and amber always means something needs attention.