Gallery Web design notes

Stepo Web — Design Authority

Scope: everything served by stepo-web — the apex landing (stepo.app), the public share shells (/@username, journey, step), legal pages, and the admin dashboard (admin.stepo.app). This doc rules web-specific design. On shared identity questions (palette, type voice, accent semantics, meter rule, copy honesty) the app ledger wins: ../app/DESIGN.md. Mocks live in ./mocks/.

Stack rulings

Tokens (BRIGHT system → Tailwind theme)

Mirrors the app's 2026-07-12 temperature ruling ("neutral app, warm human moments"). If the app tokens change, this table changes in the same commit.

Tailwind nameHexUse
paper#FAFAF9Canvas everywhere
paper2#F2F2F0Chips, secondary fills
card#FFFFFFCards — lift via border-line + soft gray shadow
ink#191817Primary text; solid buttons; carbon surfaces
ink2#403E3BSecondary text
mutedink#666360Tertiary text
faintink#A6A4A1Whisper text, placeholders
line / line2#E7E6E3 / #DBDAD7Borders (solid; they carry card hierarchy)
live / livetext#FF6B3C / #C4441C24h window; fills/rings vs text/glyphs
starter / startertext#EF9400 / #9A6100First steps, Starter
celeb / celebtext#F0457E / #C22860Finales, Celebrator
danger#B3322EDestructive only — never rose

Rules carried over verbatim: accents never carry text (use the -text twins); one accent per card; a count is a memory, a ratio is a meter (no meters); Fraunces italic = authored voice only — journey titles, human quotes, brand taglines. Never UI labels, never admin chrome.

About (stepo.app/about) — the one first-person page (ruled 2026-07-30)

Mock: about.html (en + vi side by side). Everywhere else Stepo is chrome around other people's content; this is the one page where the maker speaks. Read once, by someone curious enough to look, so it is a page, not a pitch: no hero, no install CTA, no metrics, no team grid.

BUILT 2026-07-30. stepo.app/about, linked from the landing footer, and hosted rather than rebuilt in Flutter: the app's Settings hub opens the same page in a webview, first in the About group above Terms. One copy of a person's words is easier to keep true than two. The page reads nothing from the API, so it renders on a day the backend is down; the only moving part is which journey "the build itself" links to, which is the same editorial config the landing already reads. The app asks for the page in the language it is currently speaking, which is the one way this link differs from the legal ones. Offline it falls back to a bundled copy per language, regenerated from the live page by stepo-mobile/scripts/refresh-about-asset.sh.

Public share shells — "a quiet window into the app"

One shell for profile / journey / step (mock: share-step.html, share-profile.html):

Store links (ruled 2026-07-28, both stores approved)

Stepo is live on the App Store (6790456635) and Google Play (com.chauapps.stepo). Three rules govern every place the web offers the app:

Site localization (ruled 2026-07-13, settled 2026-07-29 when it shipped)

Every public page answers in the reader's language: the landing, the share shells, the legal documents, About. Copy comes from per-locale dictionary files generated from the app's own Dialect source, so the site says what the app says; the one dynamic slot (the latest/live step from the public API) renders as authored on both locales — real content never gets translated, only chrome (captions, explainers, kickers, CTA) localizes.

Language rides the request, not the path. An explicit ?lang= → a remembered cookie → Accept-Language → English, resolved once per request. The parameter wins so a shared ?lang=vi link reads Vietnamese to someone whose cookie says otherwise: the link is the more specific intent. A corollary for anything linking in — the app's Settings row, an email, a QR — a clean URL guesses, so a link that means one language names it. There is no /vi/ prefix: the share grammar is locked, so one shared thing stays one indexed URL in every language. The canonical address of any page is always its clean URL, and the ?lang= addresses exist as real hreflang alternates that render directly (an alternate that redirects is a lie). A page that answers in two languages carries Vary: Accept-Language, Cookie. The language toggle in the footer is plain links.

Legal documents are the exception to the dictionary: a policy is a document, not a string catalog, so each ships as a whole authored body per language.

Landing (stepo.app) — the journey is the pitch (ruled 2026-07-13)

Mock: landing-v4-vision.html — the ruled direction (PO vision, codex structure pass, design-authority ratification). The page teaches Stepo entirely through real content; no slogan/claims sections (the "Never streaks…" band and pitch paragraphs are retired from the landing — that voice lives in the store kit and the explanatory captions).

Sections, in order: 1. The journey (split hero) — left: eyebrow, "Making Stepo" serif title, author row, journey description (per-locale; see Site localization), quiet stat line, supporters as ONE memory sentence (not a stats card); one Get Stepo CTA. Right: the latest step as a full card with media, marked live (marking the latest step "live" is acceptable even when the window has lapsed). 2. How a journey works — a numbered 01→02→03 rail; three step cards, media-forward: the real first step (Starter chip), the real latest step (live), and an example finale of Making Stepo. The connectors between the numbers are labelled "many more steps", because the rail is teaching that a journey is a run of steps and these are three of them. Short sans captions under each teach Starter / live / Achievement+Celebrator. This is the one section that illustrates rather than records — see "The rail illustrates" below. 3. Recognition — founder profile card (bio, journey chip, earned badge chips) beside a "People who showed up" list (role lines as memories). 4. Final CTA strip (Get Stepo + both store badges, live per Store links) and footer (legal links, the language toggle, About, wordmark).

Rules: media honesty — every photo ships real. The mock's Unsplash/randomuser faces are placeholders standing in for people who do not exist; no fake testimonials, no feature carousel bloat. The one invented number on the page is the thousand in the rail's example finale, and it is inside a card tagged Example — anywhere the page speaks in its own voice, a number is a real one or it is absent.

BUILT 2026-07-28. The landing is now the mock's structure rendered from the live public API at request time, so it tells the truth by construction — there is no copy of the journey to go stale, and the photos are whatever the founder actually posted. The journey is resolved from config (FOUNDER_HANDLE / FOUNDER_JOURNEY_CODE, defaulting to the identifiers in ../../StepoBackend/docs/stepo/founder_journey_playbook.md → "Live identity"), never hard-coded in a component — the pair already changed once.

Honesty falls out of the data rather than out of care, which is the point:

The rail illustrates (ruled 2026-08-07)

Section 2 has a second job the rest of the page does not: it teaches Starter, live and Celebrator. Making Stepo has nobody on its first step and no finale at all, so holding the rail to "records only" taught nothing — the finale sat as an empty dashed frame reading "not written yet", and card 01 said "where this journey begins" where a presence row belongs. The rail is therefore allowed to illustrate, and every illustrated part is dressed as one. The dressing is the ruling; the illustration is just what it protects.

The exploration mocks landing-v1-journey/-v2-split/-v3-showcase.html are kept as the decision record in ../archive/explorations/landing-concepts/; landing.html is the pre-launch page this replaced.

Admin dashboard (admin.stepo.app)

Mocks: admin-reports.html, admin-config.html. Utilitarian, on-token, zero serif — the dashboard is chrome, and the interface is speaking. Desktop-first, fully usable at 390px (the founder acts from a phone via report-alert email links).

States & accessibility

Empty states follow the app's quiet doctrine (an empty queue says "Nothing waiting." — no illustration, no cheer). Focus rings visible (ink 2px offset), all actions keyboard-reachable, text ≥14px on public pages, WCAG AA against paper/card (the -text twins exist for exactly this).