Liubov Tsebekova

03 · Então? Comer!

An independent Lisbon guide, and the bugs no screenshot could show

Forty minutes of scrolling, then a mediocre dinner. Not because good places are rare — because every available tool answers a different question than the one being asked.

Product
Então? Comer! — an independent Lisbon restaurant and bar guide. Working name during development: LUSO
Catalogue
80 hand-picked places, each with neighbourhood, price band, editorial note and tags. Grown from 37 deliberately, with sources
Stack
React 19 · TypeScript · Vite 6 · Tailwind 4 · Motion · Cloudflare Pages
Design system
21 colour tokens named by role, replacing 378 hardcoded values. Light and dark from one set
Verified
288 quiz paths run programmatically · 160 copy fields diffed against source · contrast measured per pair, worst 4.58:1
Built with
Claude Code for scaffolding and TypeScript plumbing. Positioning, product decisions, editorial voice and every audit mine

What this case doesn’t claim. No usability study, no satisfaction score, no conversion data. There is no product analytics on this yet, and the north star — did a person get from a vague want to one place they were willing to open on a map — is a direction, not a measured metric. What is measured is listed above: paths, fields, contrast ratios, and search results counted against the database. Photos are stock, and labelled as stock on the site.

The reason every tool fails at this

On every major platform, the restaurant is the customer. It pays for the listing, the commission, the analytics, the visibility. The diner is the inventory being sold.

Every downstream behaviour follows from that one fact — sponsored results, opaque ranking, the same twenty venues everywhere. None of it is broken. It is the business model working exactly as designed, which is why no amount of better filtering fixes it.

I wrote the competitive analysis as a table rather than prose, because prose lets you hedge and a table forces each column to commit. Ranking signal: rating × volume, or popularity, or editorial judgement. Who pays: the restaurant, the advertiser, or nobody. Intent served: book a table, see what is trending, or decide where to go tonight.

The right-hand column of that table is the product spec. Everything below is downstream of it.

Three doors into eighty places

Eighty places is small enough to defend by hand and large enough to answer most Friday nights. The catalogue caps there on purpose: curation that cannot scale is the point, not a limitation to be engineered away.

There are three ways in, because there are three moods of arrival. Plain-language search — "quiet natural wine in Arroios" gets parsed into neighbourhood, concept and mood, and the interpretation is shown back as chips so you can see what was understood instead of guessing. A four-question quiz: area, format, mood, dietary. Four taps, no typing, no account, designed for someone standing on a street deciding now. And an editorial layer of field notes — how to read a tasca before sitting down, how to order wine without being tested — each linking to the places it argues for.

One design rule came out of this and then governed everything else: options are cards, not dropdowns. The answer being asked for is a mood, and moods are much easier to recognise than to recall.

Whatever the door, the answer is three places with a reason attached to each. Not a ranked list. A list of forty is a way of declining to answer.

v1, the critique I wrote as a table, then v2

The first direction was "printed, not digital" — zine and guidebook references, hard offset shadows with no blur, near-square corners, navy and cream lifted from azulejo tiles. The intent was handmade and opinionated, the opposite of a booking platform.

After living with it I audited it as if someone else had made it, and wrote the result as a table so that every complaint had to name a cause. This matters more than it sounds: "feels flat" is not actionable, and "twenty-nine identical shadows" is. Nothing looked more important than anything else because the same hard shadow was on 29 elements — a shadow used everywhere stops meaning "raised" and becomes decoration. The brand read as generic because the azulejo pattern only appeared on the loading screen. The icons were Material Symbols, the same set as thousands of other apps. The corner radii were four ad-hoc values with no scale.

v2 kept the palette and the voice and rebuilt the physics. Shadows became diffuse and scaled with importance, so depth means hierarchy again. Radii went onto a three-step scale. The tile motif moved off the loading screen into the structure of the page. The mood section became a bento grid where cards differ in surface as well as size — photo, flat colour, texture — so scanning it is a visual decision rather than a reading task.

The single most load-bearing decision was in the tokens: name colours for what they do, not what they look like. 21 tokens replaced 378 hardcoded values. A dark theme is impossible to express if the token is called "navy" — in dark mode ink has to become light while brand stays dark. One name, two opposite jobs. Every hard problem in the redesign traced back to a token named after its appearance.

It caught me anyway: five classes were using the foreground token as a background, and .glass had white hardcoded, which left the quiz bar and the mobile nav glowing on a dark page. Same mistake twice, both found by looking rather than by testing. Contrast is measured with the relative-luminance formula rather than eyeballed — worst pair in either theme is 4.58:1 against a 4.5 requirement, and the same pass caught a failure inherited from v1 at 3.53:1, which is why the clay accent is now #9d5f3e. The reason is written in the stylesheet next to the value, not in my memory.

174 pixels below the fold

On a 375×667 phone, the button that submits the quiz sat 174 pixels below the fold. The intro text was eating about 530 pixels on every step, and the cancel control had a touch area of roughly 16 pixels. So a person would answer the last question and simply not see that there was anything left to do.

None of this was visible in the mockups. It appears when you measure the built thing at the sizes people actually hold.

The fix was a sticky action panel with safe-area padding, a compacted mobile intro, option cards at a minimum of 64px, actions at 48px, edit controls at 44px. Focus transfers when the step changes; aria-pressed, aria-live and aria-current carry the state to screen readers; MotionConfig reducedMotion="user" hands the animation decision back to the system. Then the CTA was verified visible without scrolling at 375, 768 and 1280.

The second iteration added the thing the first one was missing: a review screen. Four answers listed, each editable in one tap, before anything is submitted. A quiz that cannot be corrected is an interrogation.

The bug no screenshot could show

This is the section I would want a lead designer to read.

The interface promises, in my own words, that it will not pad the list. I ran the quiz combinations against the database rather than looking at them, and found that 44 of 288 paths ended in zero results. Others returned fewer than the three the product had just promised. The type fallback could quietly hand you a restaurant when you had asked for a bar, some venues were under-tagged, and a few neighbourhoods were unreachable from the quiz at all.

Search was worse. A query for craft beer returned twelve places when only six carried the tag. Quality bonuses for "local gem", "no tourists" and curator picks were being applied before anything was checked for relevance, so unrelated venues scored above zero and filled the page. The interface was promising not to pad the list, and the code was padding the list.

The fix in search.ts is three lines of ordering: compute relevance first, apply the quality bonuses only to things already relevant, filter on both. Craft beer now returns six — and the honest "no reliable match yet" state became reachable for the first time since launch.

The lesson is not about search. The most damaging flaw in the product was invisible in every single screenshot, and would have stayed invisible through any amount of visual review. Audit the behaviour, not the pixels.

The same principle produced the rule I now apply to writing. The catalogue copy had drifted into superlatives and invented enemies — "no ratings maze, paid listings, or tourist-trap filler" is marketing wearing an anti-marketing costume. Rewriting 160 fields meant making claims about real businesses, so I wrote a verification script before the rewrite: it diffs every field and flags any word that was not in the original. It caught me three times, including a detail about a menu I had invented while making a sentence sound better. Reviewing your own edits by reading them does not work.

The name, and what is still open

The working name was a placeholder that never meant anything. The product is a question asked between two people at the end of a working day, so the name became the question and its answer: Então? Comer! — "So then? Let's eat!"

The mark is the eight-point star from the centre of every azulejo tile. The hero ornament looked subtly wrong for a week before I read the path data and found one of three stacked diamonds centred 24 units off. I rebuilt it so that every element is defined once and repeated by rotation around the centre — which made symmetry a property of the construction rather than something to verify by eye. If correctness has to be checked visually, it will eventually be wrong.

The icons were drawn rather than installed, in one vocabulary: a sardine for the local rooms, a wine table for slow evenings, a Portuguese guitar for loud nights, tiled speech bubbles for conversation. The favicon is the star reduced until it still reads at 16 pixels.

Open, and worth saying: four case-study screenshots still show the pre-rebrand interface and are the weakest evidence on the page. The results, detail and editorial screens are still English-only even though the dictionary exists. The 160 place fields are not yet in European Portuguese, and that needs a native reviewer — machine-checked key parity is not the same as correct register, and on a product whose whole value is voice, register is the product.