The Stepo design
The ruling ledger. What each screen is for, the system behind it, what ships in V1, and the answers to every question raised in review. This doc is the plan the Flutter build follows, and it is the authority: when it and anything else disagree, this doc wins. The screens sit beside it inapp/, and the gallery at the repo root (python3 scripts/build-gallery.py→index.html) renders every one of them with its dominant job.chatandchat-threadare V2 mocks.
What it is, in one paragraph
Stepo's UI is built around one mechanic: a step is live for 24 hours, and stepping with it while it's live is how support becomes recognition (Starter · Companion · Celebrator). The design makes that mechanic the loudest read everywhere — live cards glow and count down, past cards stay vivid but go quiet, and every badge is presented as a person you stepped with, never a number. The mechanic runs both directions: showing up gets remembered, and looking back — scrolling a journey to day one and seeing who started with you, who walked beside you, who was at the finish — is the payoff the whole system builds toward (the PO's day-one thesis; the store description's four beats are the canonical pitch: journey → 24h window → remembered → look back). The skin is neutral-premium: near-white canvas, neutral ink, and a sunset taxonomy inside the brand's orange family carrying every warm moment (the 2026-07-12 temperature ruling: neutral app, warm human moments).
Lineage (what was merged, from where)
| Element | Source concept |
|---|---|
| On air / Earlier feed structure, countdown rings, badge-named-at-action copy | broadcast-orange |
| Serif-italic journey titles, "Needs you now", people carried on journey cards | alongside |
| Inverted carbon final-step card (re-warmed with rose ripple rings) | pulse-orange |
| Windows-open avatar rail, tiered step-with bursts (puff / rise / settle) | cadence |
| Rule-teaching prompt lines ("Heart, comment or share in the next 9h…") | signal |
| "Who you showed up for / Who shows up for you" framing | waymark-2 / broadcast |
The system
Palette — the sunset taxonomy. All recognition colors are siblings of the brand orange:
| Token | Hex | Means |
|---|---|---|
--live | #FF6B3C | The 24h window, interactivity, Companion — fills, rings, borders ONLY, never text |
--live-text | #C4441C | Text/glyph twin of --live — ALL live-hued text and icons, any size (5.0:1 on white) |
--starter-text | #9A6100 | Text/glyph twin of --starter — in every screen's :root since 2026-07-11 |
--celeb-text | #C22860 | Text/glyph twin of --celeb — in every screen's :root since 2026-07-11 |
--danger | #B3322E | Destructive actions only (delete account). NEVER rose — rose means Celebrator, and the celebration color must never mean "destroy" |
--starter | #EF9400 (marigold) | First steps, Starter recognition |
--celeb | #F0457E (rose) | Final steps, Celebrator recognition, achievement |
--carbon | #191817 | Ink; also the inverted "moment" card (neutralized 2026-07-12, was warm #1A1512) |
--paper | #FAFAF9 | Canvas — neutral near-white (2026-07-12 temperature ruling; was warm paper #F6F4F1) |
Canvas temperature (ruled 2026-07-12): "neutral app, warm human moments." The canvas, ink, borders, and shadows are neutral: --paper #FAFAF9, --paper2 #F2F2F0, --ink #191817, --ink2 #403E3B, --muted #666360, --faint #A6A4A1, solid borders --line #E7E6E3 / --line2 #DBDAD7, gray shadows rgba(23,23,23,…). Warmth lives ONLY in elements that earned it — the accent taxonomy, the authored serif, photos, and deliberate moment surfaces — never in ambient chrome, and never screen-scoped (one canvas everywhere; a warm screen would read as a bug). The adjudication line: a first step is yellow because it is a first step, not because the app is already yellow. Decision record: theme-lab/ (four-temperature board; PO + designer + codex chose BRIGHT over value-matched neutral and pure white).
Rule: one accent per card. A card is marigold OR tangerine OR rose, set by its step type; never mixed.
Rule: accents never carry text. Raw --live/--starter/--celeb fail WCAG as text (2.8/2.4/3.6 : 1 on white); every character and glyph in an accent hue uses the -text twin — chips, tags, door numerals, match highlights, serif accent words, all of it. Raw accents are reserved for fills, countdown rings, borders, and white-on-accent photo pills. (White-on-accent pill contrast is a punch-list item for on-device review.)
Type. Archivo (display, numerals, buttons) · Inter (body) · Fraunces italic for the authored voice: journey titles, the verbatim recognition copy (tier names like "Die-Hard Fan"), the stepped with verb in prose lines, and brand taglines (the signup headline's one word). Never UI labels, never chrome. The test: if a human could have said it, it may be serif; if the interface is saying it, it may not.
Craft rules. Cards lift (dual-layer neutral-gray shadow + solid --line border — on the near-white canvas the border carries hierarchy with the shadow assisting), chrome stays flat. Live cards add an accent halo on top of the base shadow. Real photos always; SVG status bars; numbers agree across screens.
The signature moment. While live, Heart · Comment · Share light up in the card's accent and each shows a count (three equal peers). Tapping one fires a tiered particle burst: regular = radial puff, first step = rises up, final step = falls & settles (confetti logic). After 24h all three go flat monochrome — counts stay, color leaves.
Past content — "dim the mechanics, not the memories." Past steps remain full cards: vivid photo, full note, faces, all counts. Only the interaction chrome goes quiet (flat buttons, "From earlier" tag, no glow, no countdown). Photos are never desaturated — a grayed photo reads as broken, and the step is still someone's proud moment.
The screens — one dominant job each
Feed — "Who needs me now?"
On air zone first: live cards with countdown rings beside the actions and prompt lines that sell the badge at the moment of action ("Step with her first step and you become a Starter"). The final-step card is the celebration: inverted carbon with rose ripple rings. Then Earlier: full-but-quiet past cards, infinitely scrollable — the feed must feel alive even when zero windows are open (pre-launch reality). The "While you were away" digest is the only place steps collapse into rows — it's a catch-up affordance, not an archive. Two honesty contracts live here: the avatar rail is the On-air zone's index — the same open windows, jumpable; its count and the zone count are one number — and injected cards wear an ink "Discover" chip on the journey line (the label keeps the feed honest, the injection keeps it alive; Lena's be-the-first card is the demo).
Feed — day zero — "Alive before anyone arrives" (feed-empty.html)
The zero-follow feed (submission pass 2026-07-11; founder auto-follow overturned same day, round 5) is a working feed of Discover-labeled cards, never a blank "follow people first" gate — the backend already serves it (0-follow cadence, hot-set backfill, founder + marketing seeding the early pool; feed_overview.md). The founder is present through a dismissible editorial welcome module: "From the founder" eyebrow, Chau's authored words in Fraunces italic, and two explicit actions — Follow Chau (carbon) and Find people (quiet outline). Nothing enters Following until the user chooses it: an automatic follow would make the graph tell a false story, and the module converts a manufactured count into real, chosen follows — keep-the-mechanism-add-the-honesty, applied to our own ruling. X dismisses forever; Follow becomes Following in place. Every card wears the Discover chip; the rail indexes the injected on-air windows exactly as it indexes followed ones. Push safety: new-step pushes go to journey followers only, so even a widely-followed founder account never touches lock screens. Persona: Minh Trần (@minhtran, men/40), who also carries the empty-states board.
Profile — "Who did I support / who supports me?"
The two door tiles are the heroes: Badges Earned ("Who you showed up for") and Support Received ("Who shows up for you") — faces land before numbers. Below, journey cards carry a media filmstrip of recent steps (steps are photos; show them — the live step gets the tangerine ring + hours pill, the final step gets rose) plus the people line ("Maya and 39 others stepped with you here"). Filmstrip thumbs are 4:5 portrait — phone photos are portrait, and landscape letterboxing chops the moment; 4:5 (not 9:16) so the strip stays below the serif title and people line in the card's hierarchy. Thumbs never overlap: the live/final rings and pills need their full perimeter, and overlap shrinks tap targets. Considered and ranked behind 4:5: 1:1 squares (consistent with the Steps-grid unit and 22px shorter per card — the fallback if profile scroll depth ever becomes a real problem) and a hero-thumb-for-the-live-step variant (rejected: the ring + pill already carries live semantics at this scale, and the layout goes unstable when nothing is live). The "your step is on air" strip closes the creator's own loop. Follower counts are one quiet muted line under the bio (2026-07-11 ruling, from the second external review — the one product point it won): the profile is a progress record, not a creator dashboard, and a prominent follower bar is the single convergent status metric on the page. It also punishes exactly the user the doors ruling protects — a small-circle, quality-over-quantity user reads a big "3 followers" as deficiency, while their doors keep compounding. The doors and the on-air moment carry the hierarchy; the counts stay one tap from Followers.
Journey — "Who walked this with me?"
The screen recognition was built for. Hero (authored serif title, owner, meta), then — if the finish is live — one banner that carries everything about now: countdown ring, Celebrator prompt, a personal presence line, and the lit action buttons as the only CTA. The presence line obeys the meter rule (2026-07-11): a count is a memory, a ratio is a meter — "You've been part of 9 steps on this journey," never "9 of 14," never a progress bar, never "don't miss." The banner's prompt line already invites; the presence line only remembers. Then The people of this journey: Companions (with tier names), Starters (facepile — "You" tagged if you're one), Celebrators ("31 so far" while the window is open). Then the step trail: first step (marigold node) → steps → final step (rose node, elevated).
Badges — "Who did I show up for?"
Opens on a person, mirroring Supporters: "You've stepped with Priya 14 times — more than anyone," with the arithmetic (12 = 3 Companion + 5 Starter + 4 Celebrator) kept quiet below it. Then the people list under the density rule, closed by a whisper line ("Badges arrive on their own — all you ever do is show up"). Everything on this page is past tense (2026-07-11 ruling): the earlier "Needs you now" conversion card and companion-progress bar were removed — they were progress-toward-tier UI, the exact line the 2026-07-10 recognition verdict forbids, surviving here only because this mock predated it. A badges page with a deadline turns friendship into an assignment; the live moment belongs to Feed/Activity, and this room only holds memories.
Badge reveal — "A badge just arrived" (badge-reveal.html)
The discovery moment itself — mechanic #3 ("recognition is discovered, never operated") finally has its mock. A full-screen moment, shown once, the next time the user opens the app or the journey after a tier lands (Starter/Celebrator at their moment; Companion on next open after chapter close): journey cover photo, the tier pill as the one saturated element (raw rose for the ring/glow, --celeb-text for every word), the verbatim memory line ("You were there at the finish."), the journey title in Fraunces italic (authored voice — the title is the owner's words), and a permanence whisper ("Yours forever. It lives with your badges."). No share button — a reveal with a share CTA turns recognition into a performance surface; the moment is for the person who earned it. No next-tier hint, no progress framing: arrival only. Demo: Celebrator on Marco Ferri's guitar journey.
Supporters — "Who shows up for me?"
Opens on the podium: the one person who shows up most, with a crown and a human sentence. Then the list, ranked by step-with count — the one number that means showing up (the old lexicographic chip ordering is superseded; see Q&A). Each row leads with the person's highest recognition chip, then the four "by kind" buckets as facepiles. The beta tripwire is an evidence contract (2026-07-11): (a) supporters never see their own ordinal rank anywhere — this page is owner-only by design and that is load-bearing; (b) rank is never pushed and never appears in prompts or copy addressed to the supporter; (c) the instrument is pre-registered: beta exit interviews with owners as the primary signal (supporters never see ranks, so the real-world risk is owner behavior — do they describe the page in gratitude words or comparison words, and do they screenshot/share rankings at their supporters?), supporters as the secondary; (d) the threshold is pre-registered too: if more than a quarter of interviewed owners reach for pressure/comparison language unprompted, or any rank-sharing-at-supporters behavior surfaces, ordinal ranks collapse to recent/longtime buckets — the pre-agreed fallback, executed without re-litigation.
Step — "The conversation"
The step at top (keeping its live glow and countdown if the window is open), comments below, sticky composer above the tab bar. Comment rules render the spec: top-level comments inside the 24h window carry the "Stepped with" chip; replies indent one level (two max) and never carry it; a reply to a level-2 comment stays level-2 with an @mention. The composer is a conversion surface: "Commenting now counts as stepping with Maya · 9h left."
Profile / Steps tab — "Everything I've posted"
Same profile header; the Steps tab is an Instagram-style 3-column media grid carrying the ultimate's semantics — the live step ringed tangerine with an hours pill, first steps ringed marigold, final steps ringed rose, occasional quiet heart-count overlays. This is the media grid that answers the old calendar view (see Q&A below).
Profile — someone else's — "Can I step with this person?" (profile-other.html)
The visitor's version of the profile, reached from any people tap (feed author avatars, search rows, follower lists, Activity people rows). Same skeleton, four deltas: Follow (carbon-filled) replaces Edit profile; the app bar is a drill-in (back + kebab → report/block — the safety pair is reachable from every profile); the filmstrips have no "+" tile (composing is the owner's affordance); and the doors are tappable — numbers + faces computed from the viewer's visible set, opening the unranked visitor lists (see "Visitor doors open", 2026-07-15, which superseded the round-4 display-only ruling). The ranked Badges/Supporters pages remain owner-only: a visitor who could open Maya's ranked supporters page could see her own ordinal rank, which breaks the owner-facing premise the podium's survival depends on (see the tripwire contract under Supporters). A door whose visible set is empty for this viewer stays flat and does not tap. The on-air strip works as a visit-time conversion moment ("Be the first to step with her"). Demo state: Lena Ortiz, the feed's Discover-card author — day-one sourdough (22h, zero step-withs, one .solo filmstrip thumb) plus an achieved Grow a balcony garden for history. No tab is lit.
Profile — private — "A closed door, held kindly" (profile-locked.html)
What a non-follower sees of a private account, shown in the Requested state (the quiet outline sibling of Follow; tapping cancels). Public metadata stays — avatar, name, handle, bio, follower counts — and nothing below the head leaks: no doors, no journeys, no on-air, no step counts. One ink lock card carries the whole message ("Mai shares her journeys with people she approves… If she accepts, you'll see the journeys she shares with her followers" — consent-accurate: no presumed yes, no promise of everything, round-4 amendment), a whisper points at Activity ("You'll get a quiet note in Activity if she accepts" — the accept/decline row already lives there; declines are silent, never notified), and the rest of the page is intentionally empty paper. Zero accent on this page — quiet is the care. It matters more than it looks: under-18 accounts default to private, so this page is many young users' public face; the copy never scolds and never hints at gates. Report/block stays reachable via the kebab. No tab is lit.
Search landing — "Find your first people"
The Search tab's opening state, and Stepo's only discovery surface — the feed is following-only, so a new user's first follows happen here (ruling in Q&A). Three zones, spec-mirrored from search_overview.md Trending & Discovery: Recent (client-side history — it lives here, never under results), People you might know (suggested users; the reason is the row — "Followed by Maya and 2 others you follow", "Follows you" — never follower counts), Journeys getting steps (trending by 7-day step-with engagement; count rides the owner line, the right slot holds at most one accent — an Achievement chip or an On-air chip). Field idle = ink chrome; the tangerine focus ring belongs to the typed state. Cold-start users see the two discovery zones only. No content grid, no hashtags — discovery stays people-and-journeys shaped.
Search — "Find people and journeys" (typed state)
Opened by tapping the landing's field. People results lead with the relationship, not popularity ("You stepped with her 8 times", "Follows you"); journey results are authored serif titles with owner + step count. Private accounts show Requested state; private journeys don't appear (footer says so). The ✕ clears back to the landing. No hashtags.
Notifications — "What happened while I was away"
New (tinted, dotted) vs Earlier (flat), per the SeenAt/ReadAt model. Rows are step-withs, comments, follows, follow-requests (Accept/Decline inline), journey first-steps and new-steps, and the emotional peaks — recognition rows ("You became a Celebrator on…", "Priya became a Die-Hard Fan on your…") with their taxonomy chip. Batched actors get a face-stack row (V2). Two documentary states: deleted content survives as a struck-through row ("This content is no longer available"), and the one non-person row is Stepo itself (ink mini-mark avatar, announcements only). Tap destinations are contract — interaction and step rows open step detail, people rows open profile-other.html (other people, never your own tab root), achievement opens the journey; the full matrix lives on notifications-board.html.
Notification spec board (notifications-board.html — a spec board, not a screen)
The system behind the Activity screen, icon-board style: push anatomy on the lock screen (iOS collapsed thumbnail / long-press full photo via a Notification Service Extension; Android actor-face largeIcon + BigPicture; per-step thread grouping), the full trigger matrix (15 live enum types + 2 proposed recognition types + the V2 batch pair — trigger, recipient, verbatim push copy, media, tap destination), the one-push-per-step priority ladder (Achievement > First step > New step, publish-gated), the SeenAt/ReadAt badge model (server-COUNT absolute badge, never +1), the quiet rules, the never-send list, and the settings-category mapping.
Empty-states board (states-empty.html — a spec board, not a screen)
The App Review reviewer's first five minutes, as three annotated frames on the new-user persona (Minh Trần, men/40, zero everything): Activity empty — one quiet centered block ("Quiet for now. When someone steps with you — a heart, a comment, a share — it lands here."), no CTA, because Activity never nags; Profile empty — doors present at honest muted 0 / 0 with one seat-line each, the dashed Start a journey tile as the page's only CTA (no progress framing, no ghost content, no "2 of 5"); Search no-results — "No one by that name yet", and no trending backfill under a failed people search (honest empty ≠ filler; discovery lives on the landing, one ✕-tap away). The one loud exception to quiet emptiness is the feed — feed-empty.html — where liveness is the product claim.
Errors & dead ends board (states-error.html — a spec board, not a screen; 2026-07-13)
The empty-states board's sibling: what the app does when something breaks — the states a reviewer on bad hotel Wi-Fi actually hits. Seven panels: Offline (fact, not failure — stated once, quiet ink-outline Retry, auto-retry on reconnect), Couldn't load more (inline row; pagination failures never take over the screen, loaded content stays), Your step didn't post (overlay sheet; everything stays local until the server confirms — retry re-sends note + media as-is; "Discard step" is the only place the word appears, muted, never red), This step is no longer here (the deep-link dead-end), Media that won't load (paper2 field, tap to reload; the card keeps its reserved media height — every step has media, layout never collapses), That didn't work (last-resort catch-all; codes go to logs, never the screen), and the bright lines. The rulings: errors are ink and muted — never an accent, never red (sunset accents stay semantic; alarm color makes a broken moment feel like a broken promise); voice is plain and blame-free (no "Oops!", no exclamation marks, no user-facing error codes); retry is the single action; nothing already loaded is ever thrown away; and the privacy bright line — deleted, author-blocked-you, and you-blocked-author all render the identical dead-end, client and server (a closed door, not a mystery). Governing line: when something breaks, the app gets quieter, not louder.
Media viewer board (media-viewer.html — a spec board, not a screen; 2026-07-13)
Every step has media, so every reviewer will tap one — this board is the canon for the full-screen viewer. Core stance: the viewer is a lightbox, not a surface. Today a step carries one media; multi-media steps are a known possible future (PO, 2026-07-13) — if they come, the viewer becomes a horizontal pager with faint page dots and nothing else changes; every rule here is per-media. Don't hard-code single-media shapes where plural is cheap. Entry point REVISED (2026-07-13, PO device pass): only the step detail's media tap opens the viewer. Cards (feed, chapter, profile grid) are navigation — their taps, media included, go to step detail; the ladder is glance-crop (card) → whole media (detail) → zoom (viewer). The shared-element enter/exit runs from the detail's media, not from cards. Five panels: photo with chrome (one X, and nothing else — top-right and the bottom edge both carry nothing; enter/exit is a shared-element fade+scale from the tapped card), chrome hidden (single tap toggles; zoom also hides it), video (thin scrub line + tabular times; sound ON in the viewer, muted in the feed — intent draws the line; tap = play/pause; no fullscreen button, it already is), the swipe-down dismiss (media follows the finger, the room fades back in; the X exists for the tap-minded, the gesture is the real door), and the bright lines: zero social actions in the viewer (hearts/comments/share live on the step, one dismiss away); no download, no save-to-library (other people's moments are theirs — the affordance isn't built, V1 ruling); no caption (PO, 2026-07-16 — see below); chrome is paper-white on carbon, zero accent; media shown whole — natural aspect, letterboxed, never cropped or filtered. Governing line: the viewer's job is to disappear.
Caption retired (PO, 2026-07-16). The board used to carry one caption line — the step's note in Fraunces italic, plus the date. Two things killed it. The note is free text, so "one line" was a mock convenience: real notes run to paragraphs (the founder's are bilingual), and the caption landed as a wall of text across the photo. And the caption was already redundant by the time it shipped — it was designed when a feed card could open the viewer and the note might be unread, but the same-day entry-point revision above made step detail the only door in, so the note and date are always on the screen you came from. The viewer is for zoom; the caption was the last thing in it that wasn't. The bottom scrim stays for video only — it exists to keep the transport legible, and a photo now has nothing down there to keep legible.
Sign-up — "The first impression"
The product sells itself: miniature feed-card fragments (a live card, a stepped-with face-pile, a Journey-achieved chip) floating on the paper gradient, then the promise ("Every step deserves someone cheering") and the auth block — Apple / Google / Facebook / email OTP, no passwords. Matches the shipped auth stack.
Onboarding — "Become someone people can step with."
One screen, right after auth — the pitch already happened on sign-up, so this screen has one job: identity confirmation. Avatar (prefilled from the social provider, edit badge), name, and the emotional beat — the username row, where the public identity is born (the only live-accented element: "@chaucao is yours"). ToS/Privacy as the standard implicit line above the CTA ("Start stepping") — deliberate: implicit clickwrap adjacent to the CTA is the Instagram/Threads/TikTok pattern and is enforceable; a checkbox adds friction on the most drop-off-sensitive screen for no legal gain. Age gating is a separate question a checkbox can't answer anyway ("I'm at least 13 ☑" fails COPPA's neutral-age-screen principle by telegraphing the answer): the mock now carries the neutral birthdate field (2026-07-10 verdict, propagated 2026-07-11) — same fbox grammar, calendar glyph, and one privacy caption ("Never shown on your profile"); no threshold revealed, no age-gate language. Under-13 rejected and under-18-defaults-private happen server-side (auth_overview.md). No carousels, no permission asks; the follow-suggestions beat is V1 (promoted by the 2026-07-10 review verdict) — it's the next screen after this one; it earned its own mock on 2026-07-12 when the founder distribution ruling turned it into a selection surface (next section).
Onboarding — people — "Start with your people" (onboarding-people.html)
The follow-suggestions beat as a selection screen (2026-07-12 — this is where founder distribution lives; the feed welcome module is retired). Rows are selectable (tap toggles a check; the button does the following) with activity/relationship reasons as captions, never follower counts. The founder is pinned first and pre-selected as an ordinary row — same avatar, selection control, and caption slot as everyone else; no banner, no special styling (caption: "Made Stepo · sharing the journey from day one."). The primary button is dynamic and explicit — "Follow N and continue" (zero selected → plain "Continue", creates nothing); "Continue without following" is visible and low-emphasis. Bright lines: the founder is always deselectable, nothing is followed before the button tap, exactly one account is ever pre-selected — force or silence turns strong onboarding into manufactured relationships. Pinning is driven by the EditorialSeedAccountIds config and switched off when growth escapes the founder's network; while on, the beat always has a row; with it off and zero suggestions, the beat skips silently. Flow: onboarding → people → notification priming → feed.
Onboarding — notifications — "The one shot at the prompt" (onboarding-notifications.html)
The last onboarding beat (after follow-suggestions, before the native iOS prompt). iOS grants exactly one native permission prompt, and Stepo's loop travels through notifications — mute-rate economics makes this the highest-leverage single screen in the app. The priming leads with the quietness promise, which the never-send list makes literally true: "Only the moments that matter." → the real triggers (someone steps with you; a journey you follow goes on air) → "Never streaks. Never 'come back'. Never guilt. Only people." — plus one real push preview (Priya, verbatim anatomy from the spec board) as proof. Carbon CTA fires the native prompt; "Not now" defers, re-asked only from a natural moment (first follow, first step), never as a nag. Zero accent: onboarding is chrome, and orange never enters the chrome.
The natural moment is the publish moment (ruled 2026-07-16; no mock — this ruling is the spec). The deferral above owes exactly one re-ask, and it is raised as a sheet once a step has gone on air, in the canon sheet anatomy (grabber, Archivo w800 headline, muted body, ink CTA, muted "Not now" — publish_failed_sheet.dart). It leads with the window rather than the never-send list, because that is what makes the ask honest here: the step is live for 24 hours, support only counts inside that window, and an author who pockets their phone cannot know their own window is running without a push. The ask is functional, not promotional, and it self-selects — only people who have published are asked. "Your step is on air." / "For the next 24 hours, people can step with you. We can tell you when they do: a heart, a comment, someone sharing your step." / "That's the whole list. Only people." Zero accent, same as the beat it repeats.
The budget is the ruling, not the screens: one primer per surface, and nothing at all once the native prompt has fired — whatever the user answered, the shot is gone. The app never calls the OS from an app-open path; a primer the user taps through is the only thing allowed to reach it. Out of asks, the settings banner is the only door, and it stays passive. Onboarding can never be the only surface: anyone with onboardingCompleted never sees that beat at all, which is what the publish surface exists to reach. First-follow is named above as a natural moment and is not wired; it is a third surface when it earns one.
Create journey — "What are you starting?" (1 of 2)
Journey creation is a two-screen flow per the spec (details → first step, one backend transaction — a journey can never exist empty). Screen 1 is nearly all typography: the title field IS the serif preview — your words render in large Fraunces italic as you type, the moment the authored voice is born. Description optional; no privacy toggle (journey visibility follows account privacy — a quiet caption says so). CTA: "Next: your first step".
Create first step — "Day one, documented." (2 of 2)
The marigold composer. Journey context is a locked chip (no picker — the journey was just named); the photo lands already wearing the first-step marigold ring it will carry everywhere else. One rule-teaching chip ("People who step with this in its first 24 hours become your Starters"), then the marigold CTA with a full 24h ring glyph — "On air for the next 24 hours."
Create step — "Add to the story."
What the tab bar's [Step] FAB opens. Journey picker row on top (defaults to the most recently stepped ongoing journey, "this will be step 15"), with "+ Start a new journey instead" linking into the journey flow. Media + caption, then the screen's single rose element: the "This is the final step" toggle — enabled (the journey is past the 5-step minimum), off, with the Achievement/Celebrator teaching sub-copy. Tangerine CTA with the full 24h ring. Marking a journey achieved IS posting a step — the toggle renders the spec's two-part action as one gesture.
Publishing — "The step goes on air." (ruled 2026-07-13; no mock — this ruling is the spec)
The screen after the composer's CTA, while media uploads and the step goes live — the author's biggest beat in the loop, and it was never designed (the app shipped a pre-ultimate mechanical upload page: black scrim, giant percentage, square progress frame, red error type, shimmer "Uploading...", a disabled Done — and an instant pop on success that reads as a flash when the upload is quick). The ruling: one continuous moment, two phases, on the real card.
- The card is the feed card — the step renders in the exact grammar it is about
to wear in the feed (reuse the step-card widget; one grammar, never a bespoke preview layout). Watching your own card become real IS the preview.
- Phase 1 — uploading: the media sits under a thinning carbon scrim that maps
upload progress (the old page's one good idea, kept), a centered "Publishing" status beside a live spinner, and a progress bar with a visible track. No percentage type, no square frame, no app-bar shimmer. The rest of the card — countdown ring, 24h window, the three actions — renders in full below the media the whole time, so the author sees the real card immediately; the card is inert (nothing tappable) because publishing is not a place to navigate away from. The status word and spinner exist because the earlier lone thin line read as frozen to testers while the first bytes were still going up (added 2026-07-16).
- Phase 2 — ignition: on success the media goes live on screen — the
scrim lifts and the on-air chip ignites (the feed's own live treatment, arriving for the first time) over the photo, while the chrome that was already present simply holds — then one readable beat, then auto-dismiss to wherever the flow already lands. No button, no tap; like the acknowledgment toast, the moment plays and leaves.
- Minimum dwell: the moment has a floor so it can never flash — if the upload
beats the animation, phase 1 stretches to the floor before ignition (~1.8s total feel, ignition beat constant ~1.2s). A slow upload runs phase 1 as long as it needs; ignition never varies. The floor is theater and it is honest theater — the step IS going on air; the screen just refuses to mumble it.
- Failure: the ruled "Your step didn't post." sheet (states-error panel 03) is
the only error dressing — the in-page red headline dies; retry re-dims the same card. Ink and muted, never red.
Step sheet board (step-sheet.html — a spec board, not a screen; 2026-07-13)
The step's overflow sheet ("···"), ruled after the shipped legacy sheet surfaced two dead taps and a doctrine violation. Core stance: a sheet earns its rows. Your regular step: Share · Mark/Remove milestone (with the ruled footnote "Marking is quiet — nobody is notified.") · Delete step in danger ink. Someone else's step: Share · Report step (ink, not danger — protection isn't danger; and never "Report post", Stepo has steps). CUT from the legacy sheet: "Edit step" (a dead tap — no designed edit flow exists; parked for its own future ruling before anyone re-adds it) and "Turn on notifications for this step" (dead tap AND spec violation — the notification doctrine has no per-step subscriptions and never will; notifications announce people, not mechanics). Delete confirm: Archivo headline, one honest consequence sentence ("It goes right away, for everyone — along with its hearts, comments, and step-withs."), styled --danger CTA "Delete step" + muted "Keep it" — never the legacy inversion where the primary button said "Cancel". Deleting a final step returns the journey to ongoing; badges already earned stay (badges are permanent, ruled). Danger ink --danger #B3322E only, never rose. Governing line: the sheet is a tool tray, not a feature list.
Step people board (step-people.html: a spec board, not a screen; 2026-07-15)
Partially superseded the same day by "The step guest book opens" (below): the list is now PUBLIC (viewer-filtered, window-only, equal grid) and the presence line is tappable for every eligible viewer and counts window step-withs only. The owner's detailed sheet specced here (sections, glyphs, timing, late records) is unchanged and stays owner-only — it is the third layer.
Who interacted with a step, and who gets to see it. Ruled from the PO's question ("Threads shows every likers list, X shows reposters; what do we show?") plus the follow-up that made it better: fold the step-with list and the hearts list into one surface. Original stance (see supersession above): the owner keeps the guest book. Hearts, comments and shares stay public as counts; the per-step detailed record is owner-only. It is one list of PEOPLE, not three lists of acts: a person appears once, their heart/comment/share as small ink glyphs on the row, and the 24h window does the organizing it does everywhere: "While it was on air" (arrival order, tiny time captions like "1h in": the book reads as the day unfolded) then "After it aired" (factual, never guilt; late support is valued). Entry point is a presence line under the step detail's interaction bar: three overlapping faces + "Maya, Linh and 5 others have been part of this step." (the ruled presence-line vocabulary: a count is a memory). Owner gets a chevron and the tap into the sheet; a visitor sees the identical line flat, display-only: faces and numbers for everyone, browsable lists owner-only, the exact Badges/Supporters grammar. On a live step the line runs present tense ("Maya and 2 others are stepping with you.") and the book fills top-down while the window is open; the "After" section is born when it closes. No empty state exists: the line renders only once someone has interacted. CUT: a public browsable likers list (turns hearting into performance and exposes a third party's activity to strangers; the visitor's answer is the journey People page, which answers with roles, not audits) · tier chips inside the sheet (recognition lives at the journey level; this sheet remembers one day) · per-person tallies, ranks, or any N-of-M (a memory, not a leaderboard) · a separate "step-with list" (any heart/comment/share in the window IS the same fact) · the owner's own rows (the book remembers who came, not the host). Supersedes the hearts-list-only sketch from the same table session. Build shape when queued (fast-follow, not submission scope): one owner-gated GET /api/v1/steps/{stepId}/people returning window-grouped people with act flags + timestamps; today no list endpoint exists at all (GET /steps/{id}/interactions returns only the viewer's own status). Governing line: the owner keeps the guest book; guests never audit each other.
Visitor doors open — unranked, viewer-filtered (PO ruling, 2026-07-15)
Refined the same day by "One interface — ownership is a module" (below): the separate visitor screens became the visitor STATE of the one Badges / Supporters page (shared list + owner-only module), headlines pronoun-swap. The visibility rule, no-toggle, and permanent ranking privacy all stand.
Overturns the round-4 deferral: both profile doors are now tappable for visitors and open unranked, viewer-filtered relationship lists by default. Rankings stay permanently owner-only. No hide toggle, ever. The PO's bright line, now canon: "Linh supported Maya" is ordinary social context; "Linh supports Maya more than everyone else" turns affection into competition. The flat list is what likers and reposter lists are on other platforms; the podium is what no platform shows, and ours stays private gratitude. Why the deferral fell: a door displaying counts and faces but refusing the tap is a dead door: it says the relationships matter, then withholds the relationships. It also cut discovery and suppressed Stepo's most distinctive profile dimension, identity through kindness. Sequencing must not shape the product model; this is the intended final design, promoted from deferred.
| Surface | Visitor sees | Owner sees |
|---|---|---|
| Badges Earned door | unranked people/journeys this user showed up for | full personal detail (badges.html) |
| Support Received door | unranked people who showed up for this user | private ranked gratitude (supporters.html) |
| Podium, crown, ordinal, "most", "top" | never | yes |
The visibility rule (viewer-aware, one rule everywhere): a visitor sees only relationships whose underlying journey AND participants are visible to that visitor. Public relationship on a visible public journey: shown. Followers-only: shown only to eligible followers. Only-me or inaccessible journey: completely absent. Blocked either direction: absent (indistinguishable, per the block canon). Private or under-18 supporters: never aggregated into a stranger-facing list; eligible followers may see them. The filter's two questions are distinct (build articulation, 2026-07-15, ratified): access — a minor's public step is ordinary content anyone with the link may read — versus aggregation — being readable is not being gathered up and shown to strangers. Door counts and facepiles are computed from the same visible set, so numbers never leak hidden relationships; two viewers may see different door numbers on the same profile, by design (the same honesty property as private-indistinguishable-from-deleted). A door whose visible set is empty for this viewer stays flat and does not tap (no empty state, same grammar as the step presence line). An interaction stays visible in its permitted journey context even when excluded from a stranger's aggregated list: contextual privacy, not erasure.
Why no hide toggle (affirmed): account visibility and journey audience already express who may see a relationship; the profile owner cannot consent on behalf of supporters; default-visible plus optional hiding means visible for almost everyone; and hiding creates a social signal that punishes exactly the people it meant to protect. Less exposure comes from the real privacy layers, consistently everywhere.
Design-authority fill-ins (mine, contestable): the visitor pages are headlined in human language, not door labels: "People {name} showed up for" (Badges Earned) and "People who showed up for {name}" (Support Received). Row grammar: person-first, face + name + earned role chips + visible journey context in relationship language; a person spanning several visible journeys collapses to one row ("and 2 more journeys"); no step-with tallies, no per-row counts, no engagement ordering. Ordering is recency of the most recent visible step-with, newest first: time is a fact, not a measure of affection. Facepiles show the first faces of the same visible set in the same order. Owner pages are unchanged; the supporters tripwire contract stands (supporters still never see a rank, anywhere).
Build shape: the filter framework must be viewer-aware from day one; at launch it evaluates account-level visibility and inherits per-journey audiences when they ship (the effective-audience rule, min(account, journey), already spec'd in journeys_overview). Mocks: badges-visitor.html + supporters-visitor.html, plus profile-other.html doors gaining the tap affordance. Governing line: counts and faces are everyone's; the list is browsable; the ranking is the owner's alone.
The step guest book opens (PO ruling, 2026-07-15, same day)
Refined the same day by "One interface — ownership is a module" (below): the public book gained the After section (no longer window-only), and the equal grid was replaced by one row-based sheet for everyone (owner ink: glyphs + times). The layers, the visibility rule, and the never-rank lines all stand.
Overturns the owner-only list ruled hours earlier on the step-people board. The PO's motivating image: being able to see who stepped with the first step of what later became something enormous — the Tesla-first-step lookback. "Showing up gets remembered, but only the author can see the memory" contradicts the core pitch's look-back beat. The design authority's own reason for conceding, recorded for calibration: window presence was already public for the steps that matter most — Starter and Celebrator are public roles on the journey People page, so the owner-only book protected a secret the product had already published for first and final steps, hiding it only for the middle ones. Opening it makes the model consistent. New governing line, replacing the old one: everyone can remember who was there; only the owner can inspect how they showed up.
Three layers, three questions:
| Layer | Says | Who sees it |
|---|---|---|
| Profile visitor lists | "Linh supported Maya." (aggregate relationship) | any eligible viewer |
| Step guest book | "Linh was there for this step." (shared moment) | any eligible viewer |
| Owner's detailed book | "Linh commented three hours in and hearted later." (behavioral record) | owner only |
The public guest book: anyone eligible to see the step may open its viewer-filtered step-with guest book. While on air, entry language is "People stepping with this" and the book grows as people step with; after the window closes it becomes "People who were here" and stays permanently browsable as part of the step's history. Contents: window step-withs only. It shows face, name, and a tap to the person's permitted profile state. It NEVER shows: which act the step-with came from (heart/comment/share), arrival times or "1h in" captions, chronological position, ordinals or ranking, per-person interaction counts, recognition roles or Companion tiers, or late interactions presented as step-withs. Layout is a face-forward equal grid, visibly non-ranked.
Design-authority refinements (stood ground on two codex suggestions): (1) NO relationship sentences in the public grid — equal faces mean equal rows, and a sentence re-differentiates what the grid exists to equalize (codex's "optionally one quiet sentence" declined; sentences belong to the profile visitor lists, where the durable relationship is the content). (2) Pagination order is a stable per-step shuffle — deterministic so pages don't jump, unreadable as arrival order or contribution (alphabetical declined: an alphabet is a register, a shuffle is a crowd).
The presence line, revised: it now counts window step-withs from the viewer's visible set only (same-set integrity with the book behind it — late hearts no longer inflate the line) and is tappable for every eligible viewer, not just the owner. Live: "Maya and 2 others are stepping with you." (owner) / "Maya and 2 others are stepping with this." (visitor). Closed: "Maya, Linh and 3 others were here." The line still renders only when the viewer's visible set is non-empty. The owner's tap opens the detailed book; a visitor's tap opens the public grid.
The owner's detailed book is unchanged (the step-people board's sheet): While it was on air / After it aired sections, act glyphs, arrival timing, late-support records — the audit-capable surface, owner-only, subject to deletion/block rules. Late support stays valued: it counts in the interaction counts, lands in Activity, and appears in the owner's After section — it never enters the historical guest book, because the 24h distinction stays consequential.
Visibility and count integrity mirror the visitor doors: step and journey must be visible to the viewer; blocked either direction, private-to-strangers, and under-18 participants are excluded from stranger-facing lists (eligible followers may see them); line number, facepile, and grid all compute from the same visible set; two viewers may see different counts.
Build shape when queued: public GET /api/v1/steps/{stepId}/people (viewer-filtered window step-withs, stable-shuffle order, paginated) + an owner-gated detail variant carrying act flags, timestamps, and the late section. Mock: step-people.html reworked to this model (public grid panels added, presence line and counts revised, governing line replaced). Steps carry no ordinal anywhere user-facing (the board's original "Step 12" was mock flavor; the sheet's context line is the journey title alone), and time captions in the owner's book use timezone-honest phrasing ("the next day", never "the next morning": a daypart needs the owner's timezone to be true).
One interface — ownership is a module (PO + both advisors, 2026-07-15)
The PO challenged the day's own output: owner and visitor were getting two different rooms for one feature (public grid vs owner ledger; visitor list pages vs ranked owner pages), and the accumulated hiding was drifting toward "tasteful but lifeless." Both advisors converged independently on the same law, now canon:
One feature, one interface. Privacy removes ink; it never replaces the room. Ownership adds a private module, never a different screen.
Owner and visitor see the same hierarchy, language, sections, and row design. Applied:
- Guest book, one sheet for everyone: context, "While it was on air",
"After it aired", the same person rows (face + name, tap to profile) in both states. The After section is public now (supersedes the window-only public book ruled earlier the same day): late comments were already publicly attributed, so hiding late hearters protected little while making the visitor's book disagree with the public heart count; a public After section reconciles the numbers visibly, remembers late kindness for everyone, and makes the window's consequence publicly legible on every step. Late support sits in its own section, never presented as step-with. Owner-only ink on the same rows: act glyphs, time captions (and future moderation affordances). Visitor rows are shuffled within each section (arrival order stays owner-only). The grid dies; rows for everyone.
- Badges Earned / Support Received, one page each: headline pronoun-swaps
("People you showed up for" / "People Lena showed up for"); below it ONE shared list component for owner and visitor alike — unranked person rows, role chips, journey context, relationship sentences, recency order. The owner's ranked detail collapses into a private module above the list: Supporters gets the podium (crown, most-present, deeper counts — the gratitude moment and its tripwire survive whole); Badges gets a quiet given-summary (raw tallies remain caption language). Even the owner's list below the module is unranked — the podium was always the payoff, not the ranked ledger. The separate visitor screens (badges-visitor.html, supporters-visitor.html) become the visitor STATE of the one page, not sibling designs.
- The narrow restriction list, complete: no public ordinal affection ranking
anywhere; no acts or timing for visitors; no progress meters; no leaking through totals (same-set integrity); no recognition chips inside the guest book. Everything else is open, browsable, permanent.
- Build shape: the endpoint splits collapse into one viewer-aware endpoint
per surface — owner-only fields (act flags, timestamps, module data) are null for visitors. One contract, one widget, density by viewer. /people/record retires.
The openness correction, recorded: the app's public set is now live windows, counts, doors, browsable support graphs, permanent guest books, and the crawl loop they form (book → face → profile → doors → journeys → books). What stays private protects engines, not feelings: public affection rank makes support strategic; behavioral audit makes watching creepy; meters make recognition grindable. Governing line: one expressive surface; hide only the private elements, never the experience.
Peaks public, ladders private (final ruling, 2026-07-15)
The PO's original vision (archive explorations: stat bands, per-badge counts, top supporters, ranked drill-downs) returned to the table with the KOL/startup-journey case; codex argued for a fully public ranked Support Received (complete ordered list, exact counts, "You're #18"). Final ruling, standing between both: peaks are public, ladders are private, and your own place is yours to see. Conceded on merits: a private Top Supporter card cannot make anyone want to become Top Supporter (motivation requires visibility), and unique-step counting is an honest metric of exactly what Stepo celebrates. Held: the complete public ladder adds no motivation beyond summit + own distance; its only marginal product is the shame floor, which lands hardest at friend scale (no crowd to hide in at N=9), where Stepo's entire launch graph lives.
Support Received (IA: headline → stat band → podium → your standing → filters → rows): the podium goes public: top three, faces, exact unique-step counts (overturns "rankings permanently owner-only"; a summit is celebration-shaped, a credits roll). Each supporter sees their own standing privately ("You've stepped with 9 of her steps · #7 of 23"): rendered only to that viewer, so the climb is legible while the ladder between is never published. The shared list stays unranked (recency); the owner keeps the complete ranked list behind their module (the private stats room: the original vision, intact). Stat band for everyone: people · step-withs · tappable Starter / Companion / Celebrator counts that filter the list. The supporters list includes step-with-only people (neutral Step-with chip, no badge): the door counts badges, the list counts people, and the two are different honest facts.
Badges Earned: public totals (badges by type, step-withs given as caption), tappable category filters, and "Shows up most for" restored as a public identity section (a peak in the giving direction: it flatters the strongest relationship without ranking the rest). No outward ranking of the full list, ever: numbering whom you love is publishing a hierarchy of friendships. Owner's full outward record stays in the private drill-down.
The private stats room is mocked: stats-room.html (Supporters variant; the Badges variant, "Your full record", is the same composition with giving-direction data). Owner-only drill-in behind both pages' "Only you see this" rows: the complete ladder with rank ordinals and exact unique-step counts, ties sharing a rank (5 · 5 · 7 demoed, tied ranks in ink, never accent: a tie is a fact, not a moment), step-with-only people included at the bottom, filters that never renumber, and the pull-only rule as the footer whisper ("Standing is yours to look up. Stepo never sends it to you.").
Guardrails (canon, all of them): standing counts unique steps only (one step-with per step; comments cannot farm); no act weighting; ties tie; no "2 more to overtake Linh"; no rank-loss or overtake notifications; no progress bars, multipliers, or prizes; everything viewer-filtered; no hide toggle; and standing is pull-only: you look it up, it never notifies, never pushes, never lands in Activity. The moment Stepo prompts the grind, it has built the thing the doctrine forbids.
Doctrine amendment, admitted openly: identity mechanic #3 narrows to "Badges are discovered. Standing is a visible consequence of showing up: peaks are public, ladders are private, and Stepo never prompts the grind." The supporters tripwire stays live with a pre-registered retreat: comparison language, rank-defense behavior, or mute-rate degradation at friend scale in beta collapses the podium back to owner-only. Calibration note: codex reversed its own round-8 position (public rank "changes optimal behavior") within a day of the PO stating the vision; the concessions above were made on merits, not on the reversal.
The live prompt is visitor copy (2026-07-15)
The on-air ring prompt ("Heart, comment or share in the next 22 hours and you've stepped with Chau") is conversion copy aimed at the step's audience — and the spec has always excluded your own steps from step-with. Shown to the owner it invites an act the system will not count, on their own step, by name. Ruling: the prompt never renders on your own step. The owner's live step detail carries the on-air chip, the countdown, and the presence line; the invitation belongs to everyone else. (Surfaced by the guest-book build: the new presence line sits directly under the prompt and made the false invitation obvious.)
Recognition row sentences — one template per tier (2026-07-16)
The Round-3 build surfaced that the recognition-page rows had ad-hoc sentences: the same tier phrased three ways across mocks, and several sentences claiming moments the tier definitionally excludes (a Die-Hard Fan row saying "there when it began and celebrated the finish" — a Die-Hard Fan by definition missed both). Ruling, two parts:
A sentence is the tier's definition in memory language. It states exactly the moments the tier includes — never more, never fewer. Collapsed chips make this load-bearing: the chip absorbs its constituents, so the sentence is where the absorbed Starter/Celebrator facts remain visible.
One template per tier, both directions and both page states (J = journey title; badges pages prepend the subject, "You" for the owner / first name for a visitor, with verb agreement; supporters pages render subjectless because the subject is the row's person; the caller's own row uses "You"). Sentences name the journey only, never the viewer — so a row's public ink is byte-identical for every eligible viewer, which is the one-interface invariant made testable:
| Tier | Template (subjectless form) |
|---|---|
| Step-with (no badge) | Stepped with J. |
| Starter | There when J began. |
| Celebrator | Showed up to celebrate the finish of J. |
| Starter + Celebrator | There when J began, and back for the finish. |
| Early Believer | There when J began, and kept showing up. |
| Consistent Supporter | Kept turning up through J, and was there at the finish. |
| Die-Hard Fan | Kept turning up through J. |
| Dedicated Follower | There when J began, stayed the whole way, and celebrated the finish. |
The Die-Hard Fan line deliberately names no beginning and no finish: relentless middle is the whole badge. Repetition between adjacent rows is the truth of a templated system and is fine. All six recognition mocks now carry these lines.
Vietnamese voice (ruled 2026-07-16; first locale wave shipped same day)
Stepo's vocabulary is meaning-bearing common words carrying the journey metaphor, not coined proper nouns, so it translates. The keep-English test (all three must hold): genuinely a proper noun or deliberate global surface name; translating loses more identity than it gains; sounds natural inside Vietnamese speech. Only Stepo passes. On air = "Lên sóng" (chip) / "Đang lên sóng" (sentences).
The framework, proven across 751 keys:
- Translation is sentence-level, never substring substitution.
- The mechanic verb never varies: wherever English names the step-with act,
Vietnamese says "bước cùng".
- Presence language varies where English varies: có mặt (a moment), ở bên
(emotional memory), đồng hành (relationship over time). "Bạn đồng hành" is reserved as the Companion title.
- Tier titles are one family of short earned titles, no "Người…" prefix:
Có mặt từ đầu · Chung vui về đích · Bạn đồng hành · Tin từ đầu · Bền bỉ · Fan cứng · Trọn hành trình. Titles appear in mechanic-teach sentences (as English canon does) and never in memory-row sentences.
- Register: the user is bạn; warm, plain, casual-adult. All copy doctrine
binds vi: show-don't-claim, recognition voice earned-only, and zero em-dashes — the six quarantined English canon lines recast with commas in Vietnamese.
The living artifact is stepo-mobile/dialect/glossary.yaml (18 terms); the translations await the PO + codex native-ear review in dialect serve, where reviewed lines get locked.
Chapter-card header earns its place (2026-07-16)
Real content exposed a board weakness: journey.html drew every chapter card with a top row of tag-left/date-right, and on the many untagged steps that row held only an 11px faint date in the corner — the mock's rich three-line demo notes hid the blank band that a real one-line note leaves. Ruling, extending the step-sheet principle (a row earns its place): the chapter card's header row renders only when it has a left occupant (First step / Final step tag or the Milestone kicker). An untagged card with a note opens with the author's words, and the date rides the note's first line, right-aligned — the timeline anchor stays in the card's top-right corner in both card types, so scanning the spine for dates is unchanged. An untagged media-only card keeps the date-only row (it sits directly against the media's visual weight). REJECTED — a permanent left occupant such as a "STEP 4" ordinal: redundant with the spine's sequence, adds ledger flavor to a memory surface, and the count already lives in the zone label. journey.html reworked to match (.hnote.dated).
The trail runs newest-first (2026-07-16)
Early users asked for the newest step on top; the design authority defended story order (oldest-first, finale as the last-card payoff) and proposed a jump-to-latest patch; the PO and codex rejected both, and the concession is on merits: Stepo's own pitch says "look back" and "scroll back to day one" — that is motion from now toward the beginning, not opening on day one. The oldest-first clause in the chapter-card ruling was never independently justified by doctrine. The journey page's identity lives in the hero, the spine, the tags, and the remembered people — not in chronological direction. The rule:
- Hero and People of this journey stay at the top. The header is the
journey's face; landing mid-trail (chat-style anchoring) was rejected in the same discussion because it skips it.
- The trail always starts with the newest step. An on-air step naturally
occupies first position, directly under the header — so the hero jump chip is retired ("pointer, never a second representation" stands; there is nothing left to point at).
- An achieved journey opens with the Final Step card; scrolling reveals
what it took. The header's "Journey achieved" eyebrow already announced the ending, so the finale first spoils nothing. The First Step is the emotional destination at the bottom: where it began.
- One order for every journey state. No toggle, no duplicate card, no
ongoing-vs-finished reversal (silently flipping the page at achievement creates two mental models).
- Watch item (codex): if testers still call the newest step "buried," the
next question is the People panel's position between hero and trail — not the order.
Build shape: the flip belongs to the backend (journey-steps endpoint returns newest-first with the cursor paging older), never a client-side reverse of loaded pages — this also makes the endless founder journey page naturally (newest page first, older loads as you scroll). The date scrubber is direction-agnostic (a position tool). Mocks: journey.html + journey-milestones.html trails reversed, jump chips removed; milestones-board.html prose updated.
The unfinished step is a status, not a broken card (2026-07-16)
The backend intentionally shows owners their own unpublished steps (WhereMediaVisibleTo: PublishDate null is owner-only) so they can watch an upload land and retry a failure. The card rendered that state as a silent gray field inside full live dressing — a countdown ring for a window that hasn't started, action pills nobody can press, on a step nobody else can see. Early users read it as a broken step. Ruling, extending "The live prompt is visitor copy" (chrome that invites or counts acts nobody can perform is false):
- No live dressing. The 24h clock starts at PublishDate, so an unpublished
step is not on air: no On-air pill, no ring, no prompt, no action pills, no counts, no presence line. What remains is the author's words, the date, and the truth about the upload.
- One status voice. The media field carries a small quiet state mark (ink
and muted gray only — tangerine belongs to on-air); the footer row, where the interaction bar would sit, carries one sentence naming state + audience. Publishing: mark = blinking muted dot + PUBLISHING kicker; footer "Publishing this step. Only you can see it until it's up." Failed: mark = retry glyph + TRY AGAIN kicker (the field is the button); footer "This step didn't finish publishing. Only you can see it." + one ink Try again button.
- Quiet failure. Muted ink and a door, never red panic — the danger
palette stays reserved for destructive confirms. Retry re-runs the cached upload or opens the step's media picker (the edit-on-failed-media flow the visibility exemption was built for). Delete stays in the overflow sheet; the failed card gets one primary act, not a fork.
- Stale truth is the backend's job. A sweep fails media stuck in Pending
after an order-of-hours threshold; the client renders state and never infers failure from age.
- Card-level rule: applies wherever the owner meets their own unpublished
step — journey trail, feed, step detail. First/Final/Milestone header tags are authorial facts and survive. No progress bars on the trail; the publish moment (composer) owns the live watch, the trail owns the fact.
Board: step-unpublished.html (both states, bright-lines table).
Delete journey — the owner's last door (2026-07-17)
Journey deletion never existed: the backend service was fully written (cascading step deletes, moderation tracking) but no endpoint exposed it and the Manage sheet had no row — unbuilt plumbing, not a ruled absence. Not an App Store violation (5.1.1(v) requires account deletion, which Stepo has, immediate; no guideline mandates per-content deletion), but three reasons to build it pre-submission: user expectation (a regretted journey was the only permanent object in the app), reviewer dead-ends, and the moderation path — the founder's report-email flow had no lever to remove a reported journey, which is a real 1.2 gap once reports arrive. Ruling:
- Row: last in the Manage journey sheet, danger ink (danger = delete
only, never rose — step-sheet canon). Label "Delete journey", caption "The whole journey, gone for everyone." Close (pause, ink) sits above it; the captions carry the difference.
- Confirm: step-sheet panel-03 grammar — asks once, honest body, styled
danger primary labeled with the real verb ("Delete journey"), quiet "Keep it". Body names the real count: "Its {n} steps go right away, for everyone, along with their hearts, comments, and step-withs. Badges people earned stay theirs and no longer name this journey." (No "This can't be undone" line: the step confirm's voice carries finality as "right away, for everyone"; one family.)
- Recognition permanence extends ("deletion never claws back
recognition", ruled at step level): badges earned on the journey stay on people's walls — tier, earned date, owner attribution, and their contribution to recognition totals. This closes a grief vector: deleting a journey can never strip supporters of what they earned.
- Deleted content never survives as badge metadata (codex challenge,
conceded same day; governing line adopted: content is deletable; earned recognition is permanent; deleted content cannot survive as badge metadata). The journey title is the owner's authored content and is exactly where sensitive words live, so it goes with the deletion — the original "title as memory text" stance is dead. Owner attribution stays: "you supported Marco" derives from the step-with fact, not from journey content. The badge row renders as a non-interactive tombstone: tier + earned date + owner, sentence slot "From a journey that was deleted.", no chevron, no tap (a tap that dead-ends reads as broken — same defect class as the unfinished-step card).
- No state gating: ongoing, closed, achieved, on-air — the owner can
always delete. Share links to a deleted journey dead-end (existing canon).
- Build shape: expose the existing
DeleteJourneyAsyncat
DELETE /api/v1/journeys/{journeyId} (owner-gated; the service's isModeratorAction variant stays ready for the admin-dashboard lever, a separate follow-up). Verify recognition/badge queries tolerate a soft-deleted journey rather than joining its rows away, with tests.
Mock: journey-manage.html (danger row added; confirm copy in its header comment, grammar boarded on step-sheet.html panel 03).
Close is a verb for unfinished journeys (2026-07-17)
Live-prod finding: an achieved journey that was then closed showed Celebrators beside a Closed chip and "the finale hasn't been written yet" — because close silently demoted the achievement (nulled FinalStepId, kept the set-once WasFinalStep), leaving current-state and permanent-state contradicting each other. The reconciling copy fix (people panel speaks in past fact, "finale written", hasFinale from the backend) is correct and ships — but the producible contradiction dies at the source. Ruling:
- Close does not exist on an achieved journey. Its own caption defines
it ("Pause or park it. No final step needed.") — a verb for unfinished journeys; a finished story has nothing to pause. The Manage sheet drops the row on achieved journeys; the backend rejects close on an achieved journey; close never touches FinalStepId (on the journeys it still applies to, there is no finale to touch).
- The un-achieve doors are the existing ruled ones: delete the final
step (journey returns to ongoing, badges stay) or add a new step (the story continues past its peak; the finale becomes history). No third demote path.
- Repair the stranded data: achieved-then-closed journeys (Closed +
null FinalStepId + a live WasFinalStep step) are restored to Achieved with the finale re-pointed. Recognition was never wrong — the Celebrators and Companions on these journeys were legitimately earned; it was the journey state that lied.
- The achieved journey's sheet names the doors (codex amendment,
adopted): Edit / Continue journey / Share / Delete. Continue is the Add-step row relabeled with its consequence ("The story reopens; the finale stays a chapter.") — reopening stops being an unlabeled side effect of posting. No Close, no Achieve.
- Closed-with-history is a valid state (codex amendment, adopted):
achieved → continued → stepped → closed without a second finale is a legitimate Closed journey carrying historical Celebrators. The hero says Closed, plainly; the historical finale keeps its peak chapter card in the trail with its celebration attached ("Achievement reached · N people celebrated this finish").
- Celebrators of a past peak stay visible in past-fact language
(permanence canon): on any journey whose finale became history (finale deleted, or story continued), the people panel counts them against "finale written", never against "the finale hasn't been written yet". REJECTED — removing the Celebrator row from the People panel on such journeys: the panel is the one people surface and its rows are the doors to journey-people; role rows never come and go with journey state. Past-fact language is what prevents the "celebrating the quiet close" misread, not relocation.
Media discloses more the closer you get (2026-07-18)
User feedback surfaced the card-vs-detail ratio difference as a suspected inconsistency (composer previews the square crop; the step page shows more). Adjudicated with codex; both converged on the same frame: this is not one aspect ratio to unify — it is the ladder of disclosure (2026-07-13 ruling), now named as the system-wide contract. Originals are never destructively cropped; navigation reveals more, never less. Four tiers, each with its own duty:
- Tile (profile journey filmstrip 4:5, selection grids and compact
thumbnails 1:1): a pointer to the step, not a representation of it — the same doctrine that keeps the profile grid a navigation index. Tiles crop freely to the shape their index needs and owe no fidelity to the card band. Filmstrip stays 4:5 (thumbnails, not cards); grids stay square.
- Card (feed, trail chapter, composer preview): the step in a stream.
One shared clamp band [1:1 tall … 1.91:1 wide], center cover-crop, display-time only, one source of truth (feedMediaAspect). The square tall cap stands — the deciding criterion is unchanged ("who needs me now?" dies at one-post-per-screen), and it holds for the trail too, now a newest-first skimming surface. Acceptance check: a card with a three-line note, media, presence line, and actions fits about one phone viewport.
- Page (step detail): the step's home — media whole, natural aspect,
fit-width, uncropped, any ratio, photo and video alike. The one-post-per-screen objection belongs to streams, not to a page the reader deliberately opened; if the page cropped, the viewer would become required to learn what is actually published, inverting its role.
- Viewer: the original, zoomable — reached only from the page.
The composer caption (codex finding, adopted; its two-state proposal, rejected): the square preview alone is ambiguous — the author cannot tell whether the crop is destructive, and content near the photo's edge stays publicly visible on the step page after they watched it vanish from the preview. That is a consent-grade surprise, not a visual one. Fix: when (and only when) the picked media falls outside the card band, one quiet muted line under the composer's media card — "The full photo stays on your step. Cards show this crop." (video variant swaps the noun). The preview itself stays exactly the feed card — the publish moment's whole design is that the preview IS the real card, so it never becomes a mode-switcher, and no expand affordance: the author saw the original in the picker a second ago; the ambiguity is about what persists, and the sentence answers exactly that.
REJECTED — author-positioned focal points (stored per-media, reused by every cover crop): a backend field, crop math on every card surface, and a repositioning UI added to a post-in-the-moment flow, all to fix a harm no user has reported. Center cover-crop is the default the whole industry ships. Revisit only on evidence of real subjects lost to the crop.
The composer is one page (2026-07-18)
User feedback: the everyday composer "feels like one long form." The proposed fix was a two-page wizard (media + journey, then content). Adjudicated with codex; both converged on rejecting the split. A step is one atomic thought ("I'm continuing this journey with this moment and this note"); the composer is the app's most-repeated surface and its job is speed-to-post — "Step 1 of 2" makes a daily action feel like onboarding, splits the glanceable whole, and would demand a third review stage when the publish moment already IS the review (the preview is the real card). The long-form feeling is a hierarchy defect: five equal-weight permanent cards (the board ruled four; the Milestone card arrived in round 3 and was never absorbed), with the final-step toggle below the fold once media is selected. Ruling — one page, restructured (create-step.html redrawn as a four-panel state board: 01 before media · 02 media selected · 03 final marked · 04 video selected — video shares the grammar; its two deltas are the stilled thumbnail + duration badge (never autoplay in the composer) and the caption's noun swap; grid video tiles carry the same badge):
- Media selection is a state transition, not a stage (codex, adopted):
before selection the media card holds the picker — camera tile, recents grid, and an "All photos" door; once picked, the grid collapses into the exact card-crop preview with a small Change pill — never grid and preview together. The disclosure caption rides under it only when clamped (per the media-disclosure ruling above).
- Media is mandatory, so the empty state is the picker leading (state
amendment, 2026-07-18): every step has media (PO invariant), so before a pick the Post button sits disabled and the footnote says what's missing ("Add a photo or video to post."); the note shows its placeholder ("Say what happened."). Change means "pick a different one", never "remove": it returns the card to the grid with the current photo ring-marked; the selection persists while browsing, so Post never disarms and there is no way back to the empty state.
- The picker is hybrid — native behind the "All photos" door (ruled
2026-07-18): the inline recents grid is ours (photo_manager; it works under iOS limited access, and when photo permission is denied the grid simply doesn't render — camera + All photos remain). "All photos" opens the NATIVE system picker (iOS PHPicker / Android Photo Picker): both run out-of-process and need no library permission at all, which deletes the permission-nag states and carries the strongest privacy story. Never build a custom full-library browser — canon chrome isn't worth the full-access permission and its state matrix.
- Journey identity is a slim hairline row, not a card: dot + "Adding
to" + journey name + step count + chevron. Most days it confirms the default (most recently stepped ongoing journey); tapping opens the journey sheet, and the "start a new journey" door moves inside that sheet — a rare fork stops permanently occupying the composer.
- The two rare marks share one card, in sheet-row grammar (amended
2026-07-18 — reveal-only hid too much; "what is a final step?" must be answerable before any toggle is touched): milestone (amber) and final step (rose) as compact rows, each keeping its accent and gating (final at >=2 existing steps), each carrying a permanent one-line caption (milestone: "A bigger moment. It stands out on the trail." · final: "Completes the journey as an Achievement."). Enabling a toggle upgrades its caption to the full consequence line (the Celebrators mechanics). A finale cannot also be a milestone (ruled): while one mark is on, the other row dims and takes no taps — no silent auto-swap; turn one off to reach the other.
- Acceptance criterion (codex, adopted): with media selected, a normal
phone shows the journey row, the card preview, the start of the note field, and the sticky Post button without scrolling.
- Create-journey stays its own flow: declaring a journey and posting its
first step are genuinely two decisions; a regular step is not.
The filmstrip shows the trail, not a button (2026-07-18)
The journey card's media strip (profile, profile.html) drops its trailing add tile. One dashed "+" is an invitation; repeated down a page of ongoing journeys it becomes the app tugging your sleeve on every unfinished thing — streak pressure in disguise, on the screen whose doctrine is "progress record, not creator dashboard." The FAB on the same screen is the add door, and the journey page has its own continuation affordance; the strip's one job is photos. Cutting it also heals a grammar split: other people's profiles and achievement cards never had the tile.
- Four fixed slots. Thumbs take the 4-across width at every count and
never widen to fill; the strip shows the most recent steps, left-aligned. Strip fullness is the progress signal: 4+ steps reads full, 2 reads young, 1 reads just-started. The trailing space is an honest short trail, never a placeholder ghost (a ghost is the same nag readmitted).
- People row is omitted, not faked, while nobody has stepped with you
on that journey yet — the card ends after the strip.
- Zero steps (deletion can empty a journey): no strip at all.
- Accents unchanged: live step ring + hours pill, final step rose + Final
pill, one accent per card max.
Chat list — "Pick up the conversation." (V2)
Opened by the Feed's top-right icon. Deliberately plain: avatar, name, preview, time, one tangerine unread dot — no relationship chips (the preview text does the talking). The Stepo-native touch: a "Shared a step" preview row with a tiny step thumbnail — Share is one of the three step-with actions, so steps flow into chat the way posts flow into Instagram DMs.
Messenger — "Talk to one person." (V2)
The thread with Priya. Carbon bubbles for you, card-white for them; the header carries one quiet relationship caption ("You've stepped with her 14 times"). The centerpiece is the shared-step card inside a bubble — mini photo, serif journey title, "On air · 6h left" pill — chat as another door into the 24h window. Sticky composer with a tangerine send button.
Chat carries no tab bar. Chat is a side-space you step into and back out of, not a destination — the same reasoning that rejected a chat tab (it would compete with Feed as "where conversations happen"). Both chat screens present full-screen with a back arrow, like the composers.
The quiet surfaces (settings, forms, sheets)
The doctrine: settings is chrome, and chrome is ink. No sunset color appears on any settings screen — switches have ink tracks, buttons are bordered neutral — so that when orange appears anywhere in the app it still means exactly one thing: live. The single exception class is --danger (see palette) for destructive rows.
- **Settings hub — *"Control your account."*** Identity row → grouped rows: Account,
Privacy (Private account "Off" · Blocked "2"), Notifications, Support (Send feedback — a real backend feature — · Rate Stepo), About/legal, Sign out, and the one danger row (Delete account — "Permanent", per the account-lifecycle spec).
- **Privacy — *"Who can see me?"*** The private-account switch with the spec's real
consequences spelled out (approval for new followers; journeys/steps gated; existing followers keep access), and blocked accounts inline with Unblock. State-catalog note: blocked users' faces appear ONLY here — the spec removes them from search, notifications and comments, so reusing a known face would mock an impossible state.
- **Notifications — *"What wakes me up?"*** Toggle groups mirroring the notification
spec's actual type table (one toggle deliberately OFF — both states documented); batching excluded because the spec itself excludes it from MVP.
- **Edit profile — *"This is how you show up."*** Onboarding's sibling form (same field
grammar); the username caption teaches the share URL ("Your profile lives at stepo.app/@chaucao"). No privacy toggle — privacy lives in Settings.
- **Followers — *"My people."*** Profile drill-in, Profile tab lit; segmented
Followers · 214 / Following · 180 (the profile bar's numbers); relationship-first captions reusing the cast's established facts; real follow states (Following / Follow back / Requested-with-lock) plus one kebab documenting quiet Remove.
- **Report — *"Something's wrong here."*** Bottom sheet over the dimmed step; reason
list is the moderation doc's taxonomy VERBATIM (Spam or Scam · Harassment or Hate Speech · Nudity or Sexual Content · Violence or Graphic Content · Self-harm or Suicide Risk · IP Infringement · Other); anonymous-report subline; all-ink — the scrim dims the app's warmth.
- **Report — crisis response — *"When it's about a person, not a policy."***
(report-crisis.html, 2026-07-11) Submitting under Self-harm or Suicide Risk swaps the confirmation for a warm response: quiet ink check, "Thank you for looking out for them.", the priority promise (these reports are reviewed first), the anonymity promise (the reported person never learns who worried — bolded), and an ink resources card (findahelpline.com for local crisis lines; emergency services for immediate danger — generic and international, no hardcoded national number). Resources come after submit, never obstructing the report. Zero accent; non-clinical voice.
- **Delete account — *"Delete means gone."*** (
settings-delete.html, 2026-07-11)
Overlay sheet over dimmed Settings, per Apple 5.1.1(v) and the account-lifecycle spec's immediate-permanent deletion: plain consent copy (journeys, steps, photos, comments, badges deleted right now; your name disappears from every step you cheered on; no recovery, no waiting period), an intent separator ("Just switching phones? You only need to sign out."), then the one --danger button ("Delete everything" — never rose; rose means celebration) followed in-app by a final native confirm.
- **Journey manage — *"This journey, managed."*** Owner's sheet over their own journey:
Edit · Add a step · Mark as achievement (→ the final-step toggle in create-step, the sheet's only accent) · Share (real URL grammar) · Close — rendered in ink, not red, with the spec's semantics: closing needs no final step and the journey reopens automatically on the next step. No delete row: the spec defines no journey deletion.
Presentation rules these add to the nav contract: settings screens and followers are profile drill-ins (back arrow, Profile tab lit); edit-profile is a form sheet (X + bottom CTA, no tab bar); report, report-crisis, journey-manage and settings-delete are overlay sheets (grabber, scrim tap dismisses, no tab bar); badge-reveal is a full-screen moment (no chrome at all, shown once, X or the single button exits).
The self-harm category and its crisis response shipped into the taxonomy 2026-07-11 (moderation_overview.md → Self-harm reports; mock report-crisis.html) — the backend enum gains the value in the next agreed-changes round.
Force-update interstitial — "Time to update Stepo." (2026-07-13)
The doctrine's counterweight: "break shamelessly" only works after launch if old clients can be told to update. So the app ships a store-driven force-update gate — a full-screen ink-and-paper panel: the Stepo wordmark, an Archivo headline ("Time to update Stepo"), one plain sentence, and a single ink "Update Stepo" CTA that opens the store. It is chrome, so it is ink — zero accent, matching the quiet surfaces. When the client is below the store's minimum supported version the panel cannot be dismissed (no "Later", no back gesture); for an ordinary newer version it softens to a dismissible nudge with a muted "Later". Presentation: a full-screen moment like badge-reveal (no tab bar, no chrome), but system-triggered and shown whenever the gate trips, not once.
The store is the single source of truth (no new backend surface): the App Store / Play listing carries a minimum-version tag ([:mav: x.y.z] iOS · [Minimum supported app version: x.y.z] Android); raising it is the "break" lever. Settings' single version row doubles as the self-serve path — when the store is ahead it grows a quiet underlined ink "Update available" link. Mechanism + runbook: stepo-mobile lib/core/upgrade/README.md (built on the upgrader package, mirroring the EtonHouse pattern). No mock was drawn for this panel; the ruling above is its spec.
The icon (icon.html — a brand board, not a screen)
The zigzag line in the mocks' top bar was a placeholder, drawn fresh in the system's icon idiom — original, but generic (rising lines are everywhere in fitness/finance) and weakly ownable. The shipped Stepo icon already has the right mark: four pills climbing like stepping stones — literally the product, and the same pill/chip language the UI speaks. Decided: B — flat tangerine, crisp pills — is the store icon. C (carbon field, newest pill lit tangerine) serves as the iOS dark appearance of the same icon, and the mono glyph covers iOS tinted mode and Android themed icons — one mark, every system appearance. The board (icon.html) shows the picked mark in its real settings: home screens, the appearance trio, notification banner, Spotlight, in-app chrome, splash. The in-app swap is done: the top-bar wordmark and the Feed tab's glyph both carry the pill mark now (ink square + paper pills in the bar; currentColor pills as the tab icon — the brand mark as "home", the X pattern). Run a real trademark/app-store clearance before committing any mark to the stores.
Assets. Final SVGs live in brand/: stepo-icon-store.svg (B, the store icon — full-bleed square, stores mask their own corners), stepo-icon-store-carbon.svg (C alternate), stepo-mark-ink.svg (the in-app top-bar square), stepo-pills-ink.svg / stepo-pills-paper.svg (mono glyphs for light/dark surfaces), and stepo-icon-android-foreground.svg (adaptive-icon foreground, glyph inset to the safe zone).
Pill weight rule. The shipped icon's pills go "dot dot dot" at small sizes: at 29px its shortest pill renders ~5.5×3.5px, and a fully-rounded pill under ~1.9:1 reads as an oval, not a dash. The re-cut fixes both axes: height 12→15 (of 100) and a minimum pill width of 28 — thicker everywhere, longer only where it was too short. Keep the long-short stagger (that's the stepping rhythm) but never equalize the four lengths — four uniform bars read as a hamburger menu.
The in-app mark stays ink-and-paper, whatever the store icon does. Orange never enters the chrome: inside the app tangerine means exactly one thing — live — and the feed's balance (quiet ink top bar, vivid cards, flat tab bar) depends on the mark staying black-and-white. The store icon is the one surface where the brand competes with other apps instead of its own content, so it alone may carry the loud orange.
Why Badges and Supporters diverge on purpose: they mirror the data but not the emotion. Received support is a gift → the page celebrates (podium, gratitude). Given support is agency → the page activates ("Needs you now"). Both open on a face; they part ways at what you're supposed to feel.
The density rule (Badges / Supporters / Journey lists): dense in people, light in encoding. Every row = face + name + context + ONE chip + ONE number + chevron. Starter /celebrator breakdowns, per-journey detail, and progress bars live one tap deep — that detail view is ~80% of the journey screen's supporter panel, built once.
Questions answered during review
Why a trail on the journey screen instead of the current calendar / grid? A journey is a story, not a schedule. The trail keeps the two recognition anchors visible (first step = where Starters were made, final step = where Celebrators are made) and shows who joined where — a calendar can't carry any of that. The deeper reason: a date-grid visually rewards daily regularity, which is streak-thinking — and Stepo explicitly has no streaks. A month view full of empty days punishes exactly the user Stepo exists to encourage. A media grid view mode (Instagram-style, for 30+-step journeys) is compatible and scoped V2; a calendar is not recommended in any version — if a time-based surface is ever wanted, reframe it as a "monthly recap" (what happened), never a grid of days (what's missing). (2026-07-13: the trail's contents were upsized from compact rows to chapter cards — see the Q&A entry "Chapter cards — the trail ruling, finished". The calendar/grid rejection stands untouched.)
The mirror-door formula (one number per door, same math both ways). Both doors are badge sums, per the spec: Badges Earned = badges you earned on others' journeys (12 = 3 Companion + 5 Starter + 4 Celebrator); Support Received = badges others earned on yours (19 = 3 + 9 + 7). Raw step-with tallies ("87 given", "29 received") are captions, never door numbers — step-with is the currency, badges are what it buys, and the doors count what was bought.
Where do journey/step counts live on the profile? On the segmented tabs ("Journeys · 3" / "Steps · 41") — inventory counts belong on the inventory. The big numerals stay reserved for the two doors (people-stats outrank content-stats in the hierarchy).
Navigation contract. Five-slot bottom bar, Threads-style: Feed · Search · [Step] · Activity · Profile. Search and Activity are destinations, not drill-ins — no back arrows. The tab destinations carry zero icons in V1 on the top bar (wordmark only on Feed; gear on own Profile). True drill-ins get a back affordance; the content drill-ins (journey, step) additionally carry a quiet breadcrumb/title (e.g. "Step · {journey}") and a single overflow ··· that opens the step/journey sheet — per journey.html / step.html. Share is never in the app bar: the card's Share action and the overflow sheet's Share row already own it, so a third app-bar share is redundant (ruled 2026-07-18). The unread badge lives as a dot on the Activity tab's bell.
Tab-active rule: each of the four tabs lights on its own screen; Profile stays lit on its drill-ins (badges, supporters); content drill-ins (journey, step) light no tab — they're reachable from anywhere, so claiming a tab would lie about where you are.
Composer rule: the [Step] FAB opens create-step as a full-screen modal sheet — X to dismiss (back to where you were), no tab bar, no tab lit. The journey flow (create-journey → create-first-step) presents the same way, with a real back arrow between its two screens and a step indicator ("1 of 2" / "2 of 2") top-right. Onboarding and sign-up also carry no tab bar — there is no app behind them yet.
Where does chat go when it exists? The single top-right icon on Feed only — the Instagram DM pattern, landing in space that's deliberately empty in V1 (so adding it is purely additive: no reflow, no re-learning). Not a tab: a chat tab would compete with Feed as "where conversations happen" and break one-job-per-screen. Chat is V2; no dead icon ships until then — the feed mock now shows the V2 state (icon + unread dot), and chat.html / chat-thread.html mock the feature itself.
Why is journey creation two screens instead of one long form? The spec's rule — every journey must have at least one step — means creating a journey IS posting its first step. One combined form would bury that: the title deserves its own typographic moment (it's the story's name, rendered in the authored serif as you type), and the first step deserves its marigold framing (it's where Starters will be made). Two screens, one transaction, each with one job. The reverse door exists too: create-step's "+ Start a new journey instead" jumps into the flow.
Why is onboarding a single screen? Sign-up already did the pitch — the collage and the promise. After auth there is exactly one thing between the user and the feed: confirming who they are (avatar, name, username, ToS). Every extra beat (interest picking, permissions priming, follow suggestions) is a tax on the first session; follow-suggestions is the only one worth building and it's V2, after there are people to suggest.
Why doesn't the first step get the carbon blackout, if Starter is the scarcer badge? Because scarcity calls for urgency, not celebration. The blackout celebrates an achievement that exists (the payoff of a 5+ step story); a first step is a promise that doesn't exist yet — give day one the fireworks and the design rewards declaring goals instead of pursuing them, and pressures the tentative starter with a tuxedo they didn't ask for. Rarity is also the mechanism: first steps vastly outnumber finales, and two blackouts equal no blackout. The first step's scarcity is priced where it converts — the marigold card's prompt line ("step with her first step and you become a Starter") — in the dawn register of the sunset taxonomy. If the backend allows, live first steps may additionally sort to the top of On air (the finale gets the spotlight; the first step gets the front row) — an optional ranking rule, not a design dependency.
What ratio does step media display at in the feed? (2026-07-09 ruling) Fixed width, natural ratio clamped to [1.91:1 wide … 4:5 tall], center cover-crop beyond the bounds, applied at display time only (originals are never destroyed). Native 3:4 camera portraits render at 4:5 — they feel vertical while losing only a ~3% sliver top and bottom; 9:16 screenshots crop to 4:5; panoramas floor at 1.91:1; square and mild ratios display natural. Deciding criterion: the feed must keep ≥2 live cards reachable per swipe ("who needs me now?" dies at one-post-per-screen) while a progress app must never mutilate the proof photo — the clamp band is the only option protecting both, and it matches the profile filmstrip's 4:5 so all media speaks one portrait grammar. Rejected: fixed landscape (decapitates 3:4 shots, ~44% loss), free natural ratio (9:16 posts exceed a viewport), letterbox blur-bars (un-premium filler), zone-dependent clamps (cards would reflow when the window closes), stored upload crops (detail views couldn't show more). Runner-up: natural-up-to-3:4 — wins if Stepo ever goes media-first with lighter card chrome. Corollaries: the compose preview shows the exact feed crop (no surprises); carousels lock their viewport to the first image's clamped ratio; video uses the same band; step detail may show up to 3:4 natural with tap-to-fullscreen for the original.
REVISED on device (2026-07-13): the tall clamp tightens to square — [1.91:1 wide … 1:1 tall]. The 2026-07-09 band was ruled on mocks; the PO's device pass showed 4:5 still fails this ruling's own deciding criterion — a 4:5 card plus its chrome overruns one screen, and "who needs me now?" dies at one-post-per-screen. Square media + chrome always fits with headroom. The crop cost that made 4:5 attractive (~3% off a 3:4 portrait vs ~25% at square) is repriced by the ladder of disclosure ruled the same day: the feed crop is a glance, not the photograph — card tap (media included) → step detail, which shows the media whole (natural aspect, fit-width, uncropped, any ratio) → tapping media on the detail opens the viewer (zoom). Cards never open the viewer directly — a card is navigation into the step and its conversation; the lightbox is for when you're already there (the same logic that kept the profile grid a navigation index). Chapter cards share the card grammar and take the same clamp. Display-time-only, compose-preview-shows- the-feed-crop, carousel-lock, and video corollaries all carry over unchanged; profile filmstrip thumbs stay 4:5 (thumbnails, not cards). (Named as the system-wide contract and extended with the composer caption on 2026-07-18 — see "Media discloses more the closer you get".)
Why full past cards instead of the collapsed timeline broadcast-orange had? Cold start: for months most sessions will have 0–3 live windows; if past steps collapse, the feed is empty paper. And per the spec, late interactions are still valuable — a step is still tappable, heartable, commentable after its window. Collapsed rows survive only as the digest.
Does Search need a landing state before the user types? (2026-07-10 ruling) Yes — three zones: Recent, then suggested people, then trending journeys. Deciding criterion: Stepo has no explore tab and the feed is following-only, so the Search tab is the only place a new user with zero follows can bring the app to life; a blank field at that moment is a churn point, not restraint ("dim the mechanics" protects memories — a landing has none to protect). The spec already mandates it (search_overview.md: the tab "opens with recent searches and trending content"; suggested users ranked by mutual followers + recent activity; trending journeys by 7-day StepWith engagement — the endpoints exist and the shipped app already has an unstyled explore mode). Rejected: recents-only minimalism (starves the sparse-graph cold start), IG-style media-grid explore (doomscroll surface, off-spec, rewards lurking over stepping-with). Runner-up: an end-of-feed "find more people" module — complementary, not a substitute (passive vs. the active intent of a Search tap); V2 candidate once empty-feed drop-off data exists. Corollaries: recents move OUT of the typed state (recents under results imply the search failed); suggestion rows use relationship reasons, never follower counts; the only accents on the landing are semantic chips (Achievement / On air).
Notifications: revamp or keep? (2026-07-10 ruling) Keep the Activity screen's architecture — SeenAt/ReadAt zones, on-air creator strip, one-accent rows — it already encodes the system correctly; a visual revamp would be churn without a user outcome. What changed instead: tap destinations became contract (the old mock sent every row to journey.html; interaction rows must land on the step, people rows on the profile — a notification that lands one screen away from its promise teaches users to stop tapping), the missing types got rows (NewStepAlert, deleted-content strikethrough, system announcement), and the whole system now has a spec board (notifications-board.html). Push doctrine decided there: announce people and openings, never mechanics or nags — we push that a window opened (an event someone caused), never that one is closing (a countdown nag; urgency lives in the feed where the user chose to look). No streak/come-back/milestone pushes, no re-pings, no digest pushes. One voice from lock screen to row: title = actor's name, body = action + ≤150-char preview, no emoji; step/journey pushes carry the step photo (rich push both platforms), comment pushes stay text-only, people pushes carry the face. Rejected: IG-style filter tabs on Activity (volume doesn't justify chrome yet — V2 if mixed-type volume demands it); window-close recap push for the owner ("12 stepped with you" — good V2 candidate, in-app first). Deltas flagged as backlog, not silently changed: backend copy pass ("Step with you by replied" → the board's voice, on-air suffix on first-step/achievement pushes), RecognitionReceived/RecognitionEarned missing from spec table + enum, iOS Notification Service Extension + Android largeIcon avatar in payload, per-step thread-id.
Is the never-send list principle, or founder taste? (2026-07-10, challenged same day) Challenged with "Threads sends some of these." Re-run against the end-user criterion using a sharper test — the consent counterfactual (would the recipient, knowing everything, have chosen to receive this push?) plus channel economics: Stepo's core loop (step-with inside 24h) physically travels through notifications, so mute rate is an existential KPI — Threads monetizes the attention its pushes harvest and can afford a mediocre notification reputation; a muted Stepo is a dead Stepo. Under that test the list split. Bans that survive on economics, not taste: streak/come-back guilt (the app cheering for itself), milestone flattery (taps through to nothing but your own numbers), suggested-content pushes (strangers you never chose — added explicitly during this challenge), re-pings. Two items were overshoot, promoted to earned exceptions (V2): the finale last-call (once, closing hours, companion-tier recipients who haven't stepped yet — missing a finale you supported for months is permanent, unrecoverable regret; the "no countdowns" ban now applies to ordinary steps only) and the lapsed-week digest (7+ days away, real events from your own graph, never generic, self-silencing after two ignored sends). Runner-up: keep the absolute bans — wins again if the guardrails erode in practice (last-call creeping to ordinary steps, digest going generic → revert to the ban).
Notification prefs: 4 spec categories or the screen mock's 7 toggles? (2026-07-10) The 7+master of settings-notifications.html wins, and the board + notification_overview.md were updated to match it (the board had claimed to mirror the screen while summarizing the spec's coarser five — that inconsistency was the bug). Deciding criteria: the spec's categories were inherited-generic groupings ("Step Engagement" lumped post-window hearts with comment replies; "Social Activity" lumped follows with mentions) while the screen groups by relationship direction — Your steps / Following / People — matching the user's mental model; and the splits users will actually want are volume splits the coarse four can't express (muting "Journeys you follow", the loudest type, without silencing a friend's achievement; muting "Later engagement" while keeping the window sacred). Type mapping is in the spec's User Controls tables. Edge rulings: toggles govern push only (Activity always shows every row); FollowRequestAccepted rides New followers; SystemAnnouncement rides the master only; future types add toggles when they ship — no placeholders. Runner-up: 4+master — wins if usage data shows granular toggles go untouched, but the cost of granularity is three rows on a screen visited once.
External product-model review — what we kept, what we changed (2026-07-10) A full outside review (codex) proposed a product-model overhaul. Settled with the PO; deciding criterion throughout: end-user outcome, with distinctiveness treated as load-bearing (the retention hypothesis lives in the unique mechanics, not despite them).
Kept — the soul: Implicit step-with derivation (an explicit "Step with" button creates the two-tier support economy the review itself feared — hearts become the lesser gift; adopted only its runner-up: a one-time "You just stepped with Maya" confirmation after the first qualifying action). Window consequences (late support is valued — post-window interactions work, notify, persist — it just isn't presence; presence has a time dimension and making it legible is the product). Companion subtiers as discovery-layer surprises (never operable: no progress-toward-tier UI in V1). Supporters podium stays as owner-facing gratitude, instrumented in beta — if it produces competition instead of warmth, ordinal ranks collapse to recent/longtime buckets. Doors stay badge-sum moments (PO ruling: unique-people counts would freeze a small-circle user at "3" forever, punishing exactly the depth-over-breadth user Stepo serves; moments compound with the relationship. Legibility handled by caption copy; people are one tap deep). Share stays a full step-with kind (PO ruling, reversing the review and my own first concession: with per-journey audiences, sharing is structurally consent-based — share pages exist only for public content — and share is the costliest, most caring action plus the app's only organic growth loop; guardrails: share affordance only where a public share page exists, binary per person per step).
Changed — the review's genuine catches: Window clock starts at PublishedAt, not creation (a slow encode must not eat the window). Home stays honest, not empty (PO amendment): popularity injection stays interleaved per the feed spec, but every injected card carries a visible "Discover" label — discovery is never disguised as a followed post; follow-suggestions onboarding beat promoted to V1 (an empty feed and a lying feed are both bad apps). Per-journey audience (Only me / Followers / Public, remembered default) — no per-step override (a journey's audience is the story's audience); Selected-people circles are V2. Achievement floor drops 5 → 3 steps (beginning, middle, end — a narrative floor, not a magic number; small goals deserve finales). Achieved journeys: continuing is an explicit warned choice via a sheet whose primary action is "Start a new journey" (the IELTS 5.0 → 7.0 pattern is the paved road); continuing returns the journey to ongoing but badges are permanent, always (resolves the recognition spec's permanent-vs-recalculated contradiction: permanent). Close is quiet by design — zero notifications, zero feed events, no "abandoned" labeling; an optional closing note renders small at the trail's end for people who come looking (for sad endings, the care IS the quiet). Rejected: Paused/Archived states (silence is pause; closed reopens by stepping). Compliance simple-set: neutral birthdate screen + under-18 private defaults; deletion stays immediate (PO ruling — the 14-day grace was self-imposed, no regulation requires it, and the legal doc was aligned to the built behavior; grace is a post-launch revisit); regular-step pushes to journey followers only; reduced-motion + system text scaling honored; the report/block/moderation pipeline is the UGC filter at this scale. Demo bugs fixed (be-the-first card had counts; supporters header reused 29). Supporter ranking = step-with count (spec's lexicographic-by-badge ordering is superseded).
Second external review — the mocks catch up to the ledger (2026-07-11) A second codex review rendered all screens and measured them. Its calibration matched the first round exactly: near-perfect on audits, convergent on product taste. The headline ("redesign the recognition spine") was wrong — the recognition rules already forbade everything it found; the mocks were stale. Verdicts:
Confirmed and fixed (all verified in-file before ruling):
- badges.html violated our own 2026-07-10 ruling — "Needs you now" CTA, companion
progress bar, "Don't miss the final step!" were progress-toward-tier UI in a mock that predated the verdict. Removed; the page is now all past tense (see the Badges section above). The same disease existed on journey.html's live-finish banner (codex missed it): a 90% progress bar + "9 of 14" + "don't miss." Fixed under a new codified line — a count is a memory, a ratio is a meter: presence lines may count ("You've been part of 9 steps"), never divide, never bar, never guilt.
- Contrast: raw accents fail WCAG as text (live 2.83:1, starter 2.36:1, celeb
3.59:1 on white — computed, not estimated). The system already owned the answer (--live-text); the twins are now in every screen's :root and the rule is global: accents never carry text (see The system). Even the door numerals re-tinted.
- 19 vs 48: profile-steps.html's Support Received door said 48; the catalog says 19.
Fixed to 19.
- Rail vs zone count: the rail showed 6 open windows, the zone said 3. Contract
codified: the rail is the On-air zone's index — same windows, same count (now 6); cards below the fold are still in the zone.
- Stale-ruling propagation: the mocks now carry the settled 2026-07-10 verdicts
they were missing — Discover chip on the injected feed card, neutral birthdate on onboarding, follow-suggestions noted V1. Still deliberately absent pending PO go: per-journey audience picker (own session), self-harm report category.
Accepted as a new ruling:
- Profile hierarchy — the one product point codex won, because it wins by our own
doctrine: the ink follower bar was the single convergent status metric on a page that claims to be a progress record, and it punishes the small-circle user the doors ruling protects. Follower counts are now one quiet line under the bio.
Held (re-litigated without new evidence — the standing rulings apply):
- Supporters podium stays owner-facing gratitude with the beta tripwire (if it
breeds competition, ordinal ranks collapse to recent/longtime buckets). The discovered-not-operated line binds the actor, not the recipient: Linh never sees her rank; the owner sees who showed up.
- The feed rail stays — it is identity mechanic #1 made ambient (hours-left on
every open window) plus the jump affordance, not duplication (see the index contract above). Action rows below media are universal feed grammar; no fold-chasing.
- Responsive/touch-target audits of the HTML prototypes measure the wrong artifact:
the shipping surface is Flutter (Material hit-target minimums, constraint layout); reduced-motion, text scaling, and the hit-target audit already sit on the launch backlog. The one real catch — contrast — is fixed above because it lives in the tokens, which are the spec.
Flagged open, then resolved (third review round, 2026-07-11): the voice ladder in recognition_overview.md turned out to be a Progress Messages table keyed to percentage bands — copy indexed to a progress formula is a meter regardless of which surface renders it, so no trace was needed. The pre-qualification ladder is retired (codex's ruling, accepted — it argued from our own meter rule and won): recognition copy is earned-only (reveals + memory language, spec'd in recognition_overview.md → Recognition Voice); "What a friend!" survives as the earned Companion reveal; "One day or day one" as brand editorial only. The spec's Companion Progress Percentage section was deleted and the tier DTO no longer specs the step-with ratio (a ratio in a DTO is meter fuel).
State debts (codex's "only golden paths" list, triaged): deliberate states the catalog now owes — offline/retry, media processing/failure, upload cancel/recover, permission denial, private/no-access content, crisis-report response, account-deletion confirm, long-text/large-type. These are catalog entries and Flutter states, not new mocks; deleted/moderated content already renders (notifications' struck-through row), empty-feed already ruled (Discover injection + "be the first" card).
Own profile vs someone else's — and the private door (2026-07-11) Asked: is the visitor's profile different enough from your own to need a mock, do we need a private-account mock, and where do follow requests get accepted?
Own vs other: the chrome deltas (Follow replaces Edit, drill-in app bar with report/block, no "+" compose tile) could have been a docs paragraph — but one thing couldn't: what happens when a visitor taps the doors. If that tap opens the ranked Supporters page, any supporter can see her own ordinal rank on someone else's profile, and "owner-facing gratitude" — the exact premise the podium tripwire contract stands on — is broken. Ruling: doors render on every profile as identity (numbers + faces); the ranked Badges/Supporters pages are owner-only. The display-only clause and the deferral of the unranked visitor list were superseded 2026-07-15 ("Visitor doors open"): the runner-up recorded here — a visitor tap opening an unranked list, real relationship-first discovery — was promoted by PO ruling to the intended product model, viewer-filtered. The ranking half of this ruling stands permanently. Because this ruling must be visible to builders, it earned a mock: profile-other.html.
Private account: profile-locked.html. Public metadata visible, nothing else — private leaks nothing below the head, not even door numbers (a support graph is relationship data). The mock shows the Requested state because that's the state builders get wrong (it's a quiet outline, not a disabled button, and it cancels on tap). Zero accent; the emotional register is a door held kindly, not a wall — most private accounts will be minors (private-by-default under 18).
Where requests are accepted: already designed and shipped in the V1 list — Activity's inline Accept/Decline row plus the "accepted your follow request" confirmation row. The locked page's whisper points there. No new surface.
Wiring: every people tap now resolves to profile-other.html (feed author avatars, search + search-landing people rows, followers list, Activity people rows); one search-landing suggestion (Astrid) goes to profile-locked.html to demo private-from-discovery. Neither new screen lights a tab (drill-in rule, same as journey/step). New cast recorded in the state catalog below.
Fourth external review — three stands, six amendments (2026-07-11) Codex reviewed the two new profile screens and the round-3 propagation. Verdicts:
Held — moment-of-action prompts keep the tier name. Third re-litigation of recognition copy, now codified so it stays settled: single-act tiers (Starter/Celebrator) may be named at the moment of action — the moment IS the qualification, there is nothing to grind. One first step, one finale; no campaign, no accumulation, no proximity. What earned-only forbids is prospective advertising of the multi-act tier (Companion — the grindable one), and no surface does that. Stripping "…and you become a Starter" from the feed prompt would trade the moment's legibility (the reward named at the invitation — why the moment feels special, and the feed's conversion engine) for no reduction in operability. Post-action reveals already use the earned grammar ("You were there at the beginning — Starter").
Held — the unranked visitor door list stays deferred, for a reason the review missed. A browsable support-graph surface exposes third parties: the people on Lena's doors may be private accounts or minors, and "who supports whom" is relationship data about them, not just her. That surface cannot ship before the audience session sets third-party visibility rules — this is right-layer sequencing, not scope-dodging. What WAS wrong is the affordance: raised cards promised a tap. Visitor doors are now flat — a stat band, not a button. The unranked list stays V2, scheduled with the audience session.
Accepted — Companion is a chapter award (their "still needs PO adjudication" item, resolved as a consequence of the PO-signed item 7): the old spec's open-journey section granted provisional live Companions, which under permanence would freeze step-5 tiers into 100-step stories. Chapter model to its conclusion: open journeys show presence (counts, faces); Companion arrives when the chapter closes — discovered alongside the Celebrators, which is more of a surprise, not less (recognition_overview.md → Open Journeys). Backend must verify its derivation matches (round-2 work list).
Accepted — the locked page's copy presumed consent. "When she accepts" → "If she accepts"; the promise of "her journeys and steps" → "the journeys she shares with her followers" (per-journey audience means acceptance shows what she shares, not everything); the day-one promise dropped (the window may close before she answers). Declines are silent — never notified.
Accepted — minors leave broad recommendations (standing default, ruled now rather than waiting for the session): under-18 accounts never enter suggested-people or Discover injection, regardless of visibility setting; exact search + reciprocal graph only (auth_overview.md). The session may refine, not weaken.
Accepted, half — link reactivation: re-widening restores the same link (stable PublicId; predictability beats a forced modal at a simple toggle) — but a V2 per-journey "reset share link" is the escape hatch, and public-share UI gains the honesty caveat that external caches may hold previews Stepo cannot retract (journeys_overview.md).
Fixed — flow and propagation: whole author block taps in the feed (not just the avatar); back = invoking context and kebab = profile action sheet (Report profile · Block), both annotated in profile-other.html; Runa Vik (the locked search result) now opens the locked page and Astrid the public one — people links are identity-consistent by state; the supporters-ordering contradiction in this doc is reconciled (step-with count ranks; the chip leads the row); the tripwire gained a pre-registered instrument and threshold with owners as the primary signal; handoff/review-doc counts and the stale "flagged open" line corrected.
Submission pass — the five missing moments (2026-07-11) Preparing for store submission, we ranked what the mocks still didn't cover and built the top five in one pass (five screens + one board, all fork-built, specs propagated same-day). The rulings, in priority order:
1. First-run empty states (feed-empty.html + states-empty.html). The App Review reviewer creates a fresh account and lands exactly where we'd never designed. The feed ruling: a zero-follow feed is Discover-fed, never a blank "follow people first" gate — a dead first screen is the worst first-run UX (liveness doctrine), and the backend already serves it (0-follow cadence, hot-set backfill). Honesty holds: every injected card wears the chip. (A same-day PO amendment added founder auto-follow as a primary layer; the fifth review overturned it — see the next Q&A entry. The surviving design: an editorial founder-welcome module with an explicit Follow action.) The other empties stay deliberately quiet — Activity never nags, profile doors show honest zeros with the Start-a-journey tile as the only CTA, and a failed people search never backfills with filler. Empty is only a problem on the surface whose job is liveness. 2. Notification permission priming (onboarding-notifications.html). iOS grants one native prompt and the core loop travels through notifications — the single highest-leverage screen we didn't have. The priming leads with the quietness promise because the never-send list makes it literally true; one real push preview is the proof. "Not now" defers to a natural moment, never a nag loop. 3. Delete-account consent sheet (settings-delete.html). Apple 5.1.1(v) + our immediate-permanent deletion means the consent copy must be exact and plain; the sheet also separates intents (sign-out escape hatch) so deletion is never the accidental path. Danger ink, never rose. 4. Self-harm category + crisis response (report.html + report-crisis.html). Apple 1.2 and duty-of-care: a person-at-risk report is not a policy report — it jumps the queue, thanks the reporter warmly, promises anonymity, and offers resources after submit so the report itself is never obstructed. Copy this sensitive is designed here, not improvised by implementers. 5. The badge reveal (badge-reveal.html). Mechanic #3 says recognition is discovered — and the discovery had no mock, which meant the implementers would have defaulted to a toast. Full-screen, once, one saturated pill, verbatim memory line, no share button (a reveal with a share CTA becomes a performance surface), no next-tier hint. Arrival only.
Runner-up (deliberately not built): offline/retry/media-failure states — that's the external reviewer's state-debt enumeration ask; one states board gets commissioned from its output rather than guessed at now. Under-13 signup refusal needs no mock (a plain blocking line; the neutral birthdate field already never telegraphs the threshold). Also fixed in this pass: the V2 table's stale follow-suggestions row (it was promoted to V1 on 2026-07-10 and built), and the gallery's stale badges blurb (still advertising the removed "Needs you now" card).
Fifth external review — the overturn and the honesty pass (2026-07-11) The strongest round yet: nearly every audit receipt verified real, and the one product overturn was argued from our own doctrine — so it won.
Overturned: founder auto-follow. The same-day PO amendment (every new account auto-follows the founder) fell to a doctrine-internal argument: Discover already delivers the day-one liveness (0-follow cadence, backfill), so the auto-follow's only unique products were an unauthored graph edge and a "1 following" the user never chose — and Stepo's identity says relationships are real (step-with is noticed, never performed). The replacement is our own template, keep-the-mechanism-add-the-honesty: a dismissible editorial founder-welcome module with an explicit Follow Chau action. Founder presence on day one survives intact; the follow becomes user-authored; the manufactured count becomes real, chosen follows. The PO's underlying goal (seeded content, never-empty first feed) is fully preserved. Codex also caught the mock undermining its own rationale (ordering put the "primary" founder card last).
Copy honesty — three fixes in the new screens, all real: the delete sheet no longer overpromises physical erasure ("disappear from Stepo right now… your identity is permanently deleted" — matching the anonymize-and-hide contract); the crisis response no longer implies a 24/7 human desk ("We've marked this urgent" + "Stepo isn't an emergency service" + tappable resources); the priming promise dropped the falsifiable "never 'come back'" (the V2 lapsed digest would break it) — the promise only says what is true forever. The primer's "only people" is kept true the other way: SystemAnnouncement is Activity-only (push only for critical service/legal notices).
New rules ruled: recognition is never pushed (a lock-screen line would front-run the reveal; the previously proposed recognition push types are withdrawn; owner-side record is a quiet Activity row); reveal queue — one reveal per session, oldest first; simultaneous chapter-close awards present as one combined arrival, Companion primary; time-sensitive social acts never silently replay offline (a queued heart landing after the window closes manufactures presence the user never chose — drafts and uploads persist, interactions fail fast and quiet). Propagation defects fixed: Companion DTO arithmetic (tier enum only), the 3-step floor in steps_overview + two mock comments, journey-follow gating keyed to effective audience, Discover exclusions rewritten (effective-Public, adult authors, blocked/muted/reported, minor viewers get the seeded editorial pool only), error_states_overview replaced (quiet empties, the screen-state matrix, the offline rule), "or invite them" cut (promised a feature that doesn't exist).
Held against codex: Circle audience stays V2 — the new-follower-inherits-history hole is real but mitigated by follower approval on private accounts, no store guideline demands it, and it's a real scope hit on the road to submission; the audience grammar reserves its slot (Only me < Selected < Followers < Public), revisit at launch. The lapsed-week digest stays — codex wanted it retired to make the primer copy true; that's fixing copy by deleting a retention mechanism, backwards — we fixed the copy. Prototype navigation conventions stand (back targets, representative profiles, kebab→representative sheet): static HTML can't know its invoking context; the conventions are annotated in-file and the real behavior is spec'd and built in Flutter. "Just switching phones" stays — warmer, and it's the actual user intent. Web findings parked by PO (out of scope now; the list is kept for a pre-launch stepo-web pass). The audience-transition matrix and Discover contract were accepted as session inputs, not V1 scope.
Founder distribution — the onboarding beat (2026-07-12) The PO rejected the round-5 welcome module on sight ("don't like it") and proposed the right surface instead: the follow-suggestions onboarding beat, with the founder on it — plus a hard gate ("force user must follow 1–2 people"). Three-way convergence followed: PO picked the surface, design authority supplied the mechanism, the external reviewer added the legibility polish. Final ruling (built as onboarding-people.html):
- Surface: the existing "People you might know" beat becomes a selection screen.
It beats the feed module on every axis — it's the surface whose job is choosing who to follow, the follow is explicit, and it runs before the feed, so day one opens already-followed for most of the early network.
- Founder as ordinary row, pinned first, pre-selected — no banner, no special
styling; the caption slot carries the honest reason. Position + default + a real early-network relationship produce the ~90% founder-follow rate without a single manufactured edge.
- The button says what it does (reviewer's point, conceded): "Follow N and
continue," dynamic — a generic "Continue" that creates follows is visible consent but not legible consent. Zero selected → plain "Continue," creates nothing.
- The hard gate was rejected (both advisors, PO accepted): force only acts on
the ~10% who deliberately deselected everyone, and those users would tap a random person to escape — recreating the hollow-graph problem force was meant to solve. Discover already protects the skipper's feed. The maximum-defensible fallback (must pick someone, founder replaceable) is on record but not chosen.
- Bright lines (never violate): founder always deselectable; nothing followed
before the button tap; exactly one pre-selected account, ever. Silent follows, un-deselectable rows, or bulk pre-selection cross from strong onboarding into manufactured relationships.
- Kill-switch: pinning reads from
EditorialSeedAccountIds(already shipped for
the minor-viewer seed pool) — cleared when growth escapes the founder's network.
- No interests screen: Stepo has no topic taxonomy that would consume the
answers — pure friction plus fake-personalization smell; revisit with the Discover relevance session. The feed welcome module is retired before it was ever built; feed-empty.html returns to the clean Discover-fed state with the quiet Find-people strip.
Canvas temperature — neutral app, warm human moments (2026-07-12) The PO's designer challenged the warm-paper canvas: cream + warm brown ink + marigold + rounded cards + Fraunces italic has become the recognizable "AI-generated warm lifestyle app" aesthetic, and reads low-effort to exactly our early-adopter audience. The challenge was upheld — with the fix landing on calibration, not on white.
- Diagnosis: base paper
#F6F4F1was only ~2% warm; the cream read was the aggregate
of paper2, the beige desk gradient, ambient marigold, and warm-tinted shadows.
- Candidates, judged on a four-temperature board (
theme-lab/, feed + profile +
journey each): CURRENT warm · NEUTRAL #F5F5F4 value-matched · BRIGHT #FAFAF9 (codex's calibration, solid #E7E6E3 borders) · WHITE #FFFFFF reference.
- Ruling: BRIGHT (PO + designer + codex; design authority argued NEUTRAL and
conceded on the on-glass judgment — under warm ambient light the value-matched canvas can still read beige, and solid borders carry card hierarchy the way the category's crafted apps do). WHITE rejected: cards flatten to hairlines. Screen-scoped warmth rejected: warmth is element-scoped (accents, serif, photos, moment surfaces), never per-screen canvases.
- What deliberately did NOT change: the sunset accent taxonomy, Fraunces italic (it
does product work — authored voice — which the canvas's warmth never did), radii, card grammar. Neutralizing the canvas makes the accents more semantic: a first step is yellow because it is a first step, not because the app is already yellow.
- Follow-ups: ambient-marigold audit (accents only where earned) — separate pass;
mobile AppColors swap rides the next coding-agents round; store shots re-rendered from the retinted mocks automatically.
Milestones — a landmark, not a reward (2026-07-13, V2) Mid-journey milestone markers, adjudicated from a codex proposal + a PO instinct (codex: pure narrative marker, no reward economy; PO: milestones feel kin to first/final steps — maybe a badge or extra Companion credit).
- Adopted (codex core): a Milestone is a special step inside an ongoing journey
marking something that already happened; the journey stays ongoing — only a Final Step mints an Achievement; First Step / Milestone / Final Step are mutually exclusive; while on air it behaves like any step; its normal notification may say "reached a milestone" but creates no additional push; retro-markable, silently; unlimited, framed "Mark as milestone — A small win that changed the journey."; no milestone totals, progress percentages, checklists, deadlines, or meters.
- REJECTED — Companion multiplier: Companion must stay legible ("walked enough of
the road"). Weighted step-with credit makes which steps you attend strategic (farm the peaks), turns presence-memory into hidden arithmetic, and makes the role unexplainable.
- REJECTED — a new role (e.g. "Milestone Witness"): Starter and Celebrator are
minted by a journey's two natural singularities — exactly one beginning, at most one ending, neither author-mintable (a finale even costs the journey its ongoing life). Milestones are unbounded and author-minted at zero cost; a role minted from an unbounded author-controlled event is a reward economy, and it corrupts the marking act (authors would mark milestones to mint badges). Marking must stay consequence-free for others so it stays honest for the author.
- ADOPTED — witnesses elevated in place (the piece codex's "nothing" missed): the
milestone's memory line is a permanent, prominent part of the landmark ("Reached with Minh, Linh and 9 others" — faces first), and milestones anchor look-back, so presence at the peaks is specially remembered on the journey, not as profile inventory. Roles attach to relationships (beginning / road / end); memories attach to moments — milestone presence is deliberately a memory, not a role. Companion display language may later cite milestone co-presence descriptively ("was there for the big ones") — words, never arithmetic.
- Why it's near-core, not bloat: on a journey whose finale is far away (Making
Stepo), milestones are the only mid-road peaks — this gives long journeys their celebration beats without ending anything. Taxonomy stays: First Step → Starters · Milestone → landmark + elevated witnesses · Regular Step · Final Step → Achievement + Celebrators · Companion → the road.
- Timing: V2, post-launch; not a submission item. Needs mock passes (timeline
chapter-marker treatment, create/edit flows) before any build.
- *(Superseded on presentation, 2026-07-13: the trail the milestones sit in was
upsized to chapter cards — see "Chapter cards — the trail ruling, finished". The milestone MODEL above is untouched.)*
- Round 2 — codex "milestone seals" (same day): core REAFFIRMED, payload ADOPTED.
Codex revised toward the PO instinct: a permanent named seal per witnessed milestone ("You were there when Maya ran her first 5K"), +1 Companion credit, a pre-Companion keepsake, guarded by declare-before-publish / no-downgrade / no-visible-points. Adjudication: the +1 credit is the multiplier renamed — invisible arithmetic is worse for Companion legibility, peak-farming returns, and marking becomes a gift lever (the author knows who will step with what they post). The seal-as-object is an author-minted, recurring, permanently-missable collectible — every milestone becomes a scarcity event (miss the 24h window, permanently miss the keepsake), FOMO machinery on unbounded author-controlled supply; Starter is safe with the same missable-moment property only because a journey has exactly one beginning. The tell: every proposed guardrail exists to contain the economy the seal introduces, and one (declare-before-publish) would kill retrospective marking — the economy taxing the story. ADOPTED — the witness-side emotional payload as memory display in three places, never an object: (1) special step-with acknowledgment on a live milestone ("You were there. This one was a milestone." — a moment, not inventory); (2) the permanent in-place memory line (already ruled); (3) Companion reveal/detail cites witnessed milestones by name — codex's example copy ("You walked 7 steps with Maya. You were there for First 5K without stopping.") ships verbatim as display language. No credit, no object, no pre-Companion inventory surface. Dividend of no-economy, now explicit: retro-marking stays (silent) and unmarking is allowed — the story remains the author's to edit. Governing line: roles attach to relationships, memories attach to moments, and nothing the author marks may disburse value — milestone presence is remembered everywhere it emotionally matters and counted nowhere.
- Round 3 — RULING FINAL (codex adopted the model, same day). Codex withdrew
both the +1 credit and the seal, and contributed one additional bright line, recorded: a milestone receives no extra push, no ranking boost, and no urgency treatment — the underlying step follows normal notification and feed rules; author-minted scarcity must not return through distribution instead of badges. Restated final model: Milestone is an editable narrative designation on a regular step (addable retrospectively, removable, always silent); it never changes Companion arithmetic, feed ranking, or step-with value; it grants no badge or collectible; presence is remembered in the moment ("You were there. This one was a milestone."), in place ("Reached with …", permanent), and in the Companion reveal (milestones cited by name). Milestones make Companion more meaningful, never easier or more strategic to earn.
- Display rulings for the screens (2026-07-13): milestone treatment is
ink-weight, not accent — the sunset accents belong to recognition moments (live/Starter/Celebrator); authorial narrative structure is ink. Timeline: filled ink trail node (vs hairline nodes), small uppercase "Milestone" kicker, heavier title, permanent memory line "Reached with …" (faces first). While on air, live rules override — the 24h window stays the loudest read. Feed card while on air carries one quiet ink-outline "Milestone" chip (information, never urgency; ink is not an accent, so one-accent-per-card holds). Mocks: journey-milestones.html (landmark treatment in a long ongoing journey) + milestones-board.html (spec board: mark/unmark flows, feed-card chip, live acknowledgment moment, Companion reveal citation, bright lines).
Chapter cards — the trail ruling, finished (2026-07-13, V1.1)
Amended 2026-07-16 by "The trail runs newest-first" (above): trail direction reversed to newest-first and the hero jump chip retired. The card grammar, spine + nodes, people card, scrubber, and never-elided rulings all stand.
The PO's designer challenged the journey trail's compact rows (46px thumbs, one-line truncation, counts instead of faces, "Show N more" eliding the middle); codex concurred with a hybrid; the PO felt the same; the design authority CONCEDED — honestly: the original trail ruling adjudicated trail-vs-calendar/grid and never ruled on row size. The compact rows were an unexamined default, and the milestone work exposed them (landmarks read as cards, ordinary steps read as filler). The journey screen is the look-back surface — it must be the best reading surface in the app.
- Chapter cards = feed step card MINUS a documented delta — one grammar, one
delta, never a second card spec. Removed: the journey title line (the page is the journey), the author row (the hero is the author), Follow, any Discover labels. Kept: full untruncated authored note (the note is the product), large media on the established aspect clamp — every step has media (PO product invariant 2026-07-13: the composer requires it; a "text-only step" state does not exist in Stepo, so every chapter card carries a media block), the memory line with faces first ("Priya and 10 others stepped with him" — the compact rows' bare counts violated people-before-numbers; this redesign repays it), normal past-card quietness ("dim the mechanics, not the memories"), live treatment only while on air.
- Timeline spine + nodes survive at the cards' left edge — marigold first-step
node, ink milestone nodes + MILESTONE kicker + "Reached with" verb (the milestone differential was never about card size), tangerine live node, rose finale node.
- Hierarchy restructure: journey hero (serif title, description, owner, meta) →
compact "people of this journey" summary (faces + roles, one row, supporters one tap away — the dense ledger no longer blocks the story) → the chapter-card timeline, story order, oldest first, never elided ("Show N more" is dead).
- Pointer, not duplicate: the live-finish banner and the tiny final row rendered
the same moment twice. Now the big live/final card is the ONLY rendering, and the hero meta carries a one-line jump chip ("On air now · 6h left →") that scrolls to it. A pointer to the card, never a second representation.
- Date scrubber (long journeys only; hidden short): appears while dragging,
handle shows month + step number, stronger ticks at First/Milestone/Final (marigold/ink/rose, mirroring node semantics) with light snap + haptic. Bright lines: the scrubber is a position tool, never a meter — no percentages, no progress framing, no fill-toward-the-end visual. No calendar, no Story/Grid toggle yet (media grid stays parked V2 for 30+-step journeys).
- Timing: V1.1, post-launch, built together with Milestones (they were designed
against each other). The submission build ships the compact trail unchanged — at launch-era journey lengths the difference is invisible, and the screen is verified end-to-end; density is cheap to change later, wrong models are not. Mocks: journey.html + journey-milestones.html reworked to chapter cards; scrubber documented on milestones-board.html.
People of this journey — the recognition card, rebuilt (2026-07-13) Codex's review of the chapter-card rework: timeline approved; the one-row people summary judged underpowered for a defining surface (PO concurred), plus three timeline refinements. Adjudication:
- ADOPTED — the destination bug (codex's best catch): the people row linked to
supporters.html, which is the owner-only, cross-journey, RANKED surface. A journey's people belong to a journey-scoped page: journey-people.html — header (journey title, owner, total unique people, the viewer's own place when applicable), filter chips (Everyone / Companions / Starters / Celebrators — a person may appear under several), person rows = face + name + earned role chips (canonical tier names) + relationship language, never arithmetic + witnessed milestones cited by name. No numbered ranking, no "13 of 14". Supporters stays the owner's private cross-journey surface; this page is the memory of ONE journey, and it ships with the card (V1.1).
- ADOPTED — honest copy: "walked this whole way with Marco" is only true of
Companions → "You, Sofia, Kofi and 39 others became part of Marco's journey."
- ADOPTED — the card itself, rebuilt (the one-row summary was the design
authority's over-correction): kicker "People of this journey" + quiet total; the viewer's own relationship as the emotional lead ("You were there at the beginning — and for 9 steps along the way." / owner sees "42 people became part of this journey."); then three compact tappable role rows (small facepile, role name + role-color dot, one relationship sentence, quiet count, chevron): Companions "kept showing up" · Starters "were there at the beginning" · Celebrators "are here for the finish" ("N so far" while the finale window is open; on a journey with no finale yet, the row reads "the finale hasn't been written yet" — no count). Substance without the former dense ledger.
- ADOPTED — chapter cards tap to step detail (they are the card's destination),
but no chevrons/affordance chrome on cards — feed cards carry none; tappability is the platform convention.
- Scrubber threshold lowered: drag-only means zero cost at rest, so it is
available from ~10 steps (the finale-reach complaint itself was already the jump chip's job). Static mocks stay unchanged — a drag-only control is invisible at rest.
- REJECTED — heart/comment summaries on past chapter cards: a tappable card is
not inert; interaction detail lives one tap deep. A second number on every card of the reading surface is counts-creep against the density rule.
- REJECTED (parked) — "Since you last looked, Priya and 3 others…": the
Activity tab owns "what happened while I was away"; a per-journey recency line is a second competing what's-new surface and stateful re-engagement machinery. Revisit post-launch only with evidence.
- Canon addition: Marco's "Played for my parents in their living room" (Mar 8)
is marked a milestone — the flagship journey now demos the landmark treatment, and journey-people.html cites a real milestone instead of a hypothetical.
- **Round 2 corrections (codex canon audit, 2026-07-13 — verified against
recognition_overview.md and adopted, with one fix to codex's own fix): Companion tiers are PATH-BASED and the mock's chip combinations were impossible (Dedicated Follower = Starter AND Celebrator ≥50% · Early Believer = Starter only ≥65% · Consistent Supporter = Celebrator only ≥75% · Die-Hard Fan = neither ≥85%). Canonical mapping for Marco's journey — chosen over codex's proposal because theirs made Sofia a Die-Hard Fan while the first step's memory line ("Sofia and 8 others") makes her a Starter: Sofia = Companion · Early Believer (+ Starter), Kofi = Companion · Dedicated Follower (+ Starter · Celebrator), Ren = Companion · Consistent Supporter (+ Celebrator), Priya Nair (canonical surname per badges.html — not "Sharma") = Starter · Celebrator. Facepiles on the journey card must match the destination page person-for-person — tapping through preserves identity**.
- Milestone citations have no title field (ruled): a citation is an **authored
excerpt** — the step note's first non-empty sentence, clipped at a word boundary; if the note is empty, fall back to "the [Month D] milestone". No milestone-title input ever — a required field would tax retro-marking, and the note is already the authored voice.
- Navigation contract: the card's role rows deep-link to the people page with
that filter selected (header/total → Everyone); person rows open that person's profile ("You" → own profile), with no chevrons on person rows. "and N others" is mock shorthand only — the page must browse ALL people (lazy-loaded); a people page that can't show everyone fails its own question.
- Filter strip: next chip peeks at the edge (or edge fade) so overflow is
discoverable; the strip goes sticky once the header scrolls away.
V1 — ships
- Feed: On air / Earlier zones, live cards (3 step types incl. carbon finale), countdown
rings, prompt lines, tiered bursts, full-quiet past cards
- Profile: doors, quiet follower line, on-air strip, journey cards (filmstrip + people line)
- Journey: hero, live-finish banner w/ personal presence line, people panel, step trail
- Badges: person-first summary, people list, whisper — all past tense
- Supporters: podium, ranked list, by-kind buckets
- Step detail: comments with Stepped-with chips, two-level replies, sticky composer
- Profile Steps tab: media grid with semantic rings
- Other profile: Follow states, visit-time on-air strip, tappable viewer-filtered
doors (unranked lists behind them), report/block kebab
- Private profile: locked state w/ Requested button (accept lives in Activity)
- Search: landing (recents + suggested people + trending journeys) → typed results
(people + journeys), privacy-gated
- Notifications: New/Earlier, follow-request actions, journey rows, deleted-content
state, system row; push with media thumbnails per the spec board (recognition rows ship when the backend type lands)
- Sign-up: Apple / Google / Facebook / email OTP
- Onboarding: single identity-confirmation screen (avatar, name, username, ToS)
- Settings (hub, privacy, notifications), edit profile, followers/following list
- Report sheet + journey manage sheet (block/report = store-required safety pair)
- Create journey: two-screen flow (serif title moment → marigold first-step composer)
- Create step: journey picker, media + caption, final-step toggle, 24h on-air CTA
- Earned-only recognition voice (reveals + memory language; pre-qualification ladder retired 2026-07-11)
- Badge reveal moment: full-screen, shown once, no share / no next-tier (
badge-reveal.html) - First-run states: Discover-fed zero-follow feed with the quiet Find-people strip
(feed-empty.html); empty Activity / profile / search-no-results per the states-empty.html board
- Onboarding people beat as a selection screen with the pinned, pre-selected founder
and the dynamic "Follow N and continue" (onboarding-people.html)
- Onboarding follow-suggestions beat (promoted 2026-07-10; reuses search-landing grammar)
+ notification priming before the single native prompt (onboarding-notifications.html)
- Delete-account consent sheet (
settings-delete.html; Apple 5.1.1(v), immediate-permanent copy) - Self-harm report category + crisis response (
report-crisis.html; Apple 1.2, priority queue)
V2 — visible in mocks, deliberately deferred
| Item | Where | Why deferred |
|---|---|---|
| Windows-open avatar rail | Feed top | Redundant with On air cards until users follow enough people that live cards outnumber the viewport; earns its place at scale |
| "While you were away" digest | Feed | Needs session-gap detection + volume to matter |
| Weekly pulse strip ("Maya, Ray and 2 others drove 21 step-withs this week") | Supporters | Needs aggregation infra; nice-to-have on top of the podium |
| Media grid view toggle | Journey | Valuable for long journeys; trail is enough at launch sizes |
| Discover feed tab | Feed | Follows the 70/30 spec but needs content volume |
| Chat / DMs | Feed's top-right (empty in V1) | Whole feature is V2; the slot is reserved and additive — mocked in chat + chat-thread |
| Step-content search | Search | Spec supports it; people + journeys carry V1, step results need density thinking |
| Batched notification rows (face-stack) | Activity | NotificationBatch schema designed in the backend README; needs interaction volume |
| Owner window-recap ("12 stepped with you") | Activity, in-app first | Good retention moment; not worth lock-screen space until volume proves it |
| Finale last-call push | Lock screen | Once, closing hours, companion tiers who haven't stepped — needs recognition tiers live first |
| Lapsed-week digest push | Lock screen | 7+ days away, own-graph events only, self-silencing after two ignored sends |
| Activity filter tabs (All / People / Replies) | Activity | IG-style chrome; earns its place only when mixed-type volume hurts scanning |
| Expanded journey-picker sheet, drafts | Create step | Collapsed default row carries V1; a picker sheet and step drafts earn their place with volume |
Explicitly out (any version)
Streaks, day-counts, "N more to unlock" counters, global ranks/leaderboards, calendar day-grids — and progress-toward-tier UI of any kind: meters, "needs you now" CTAs, proximity copy, ratios in presence lines. Recognition is per-journey and its voice is earned-only (2026-07-11; recognition_overview.md → Recognition Voice): tier reveals and memory language after the fact ("You were there at the beginning"), never copy keyed to how close anyone is. "One day or day one" survives only as brand editorial, detached from recognition status.
Demo data is a state catalog
The mocks deliberately show many states at once so one screen documents them all: the feed holds all three live step types plus past cards; the journey is an Achievement whose final window is still open; profile shows both an ongoing live journey and a finished one. Numbers are consistent across screens (12/19 doors, Marco's 341/58/19, 5h/6h/9h windows) — if a number disagrees between screens it's a bug (profile-steps' 48 was one; 19 is canonical), but coexisting states are intentional. Don't "fix" them, and don't treat any single screen as the only state.
The composers extend the catalog: they show filled states (a form full of demo text documents more than an empty one), and they add one in-flight journey — Chau is starting "Run my first 10K" in the create-journey flow, while create-step targets Learn to run trails ("this will be step 15", consistent with its 14 steps on profile). The chat mocks reuse the same threads of truth: the messenger is the Priya relationship from badges ("stepped with her 14 times"), and the shared-step card is Chau's live trails step ("On air · 6h left", matching profile).
Two profiles extend the cast (2026-07-11). Lena Ortiz (@lenabakes, women/68, "Bread, patience, and second chances.") gained canon on profile-other.html: 89 followers · 132 following, doors 7 given / 4 received (tappable; the numbers are the viewer's visible set — see "Visitor doors open", 2026-07-15), Journeys · 2 / Steps · 13 — day-one sourdough (22h live, zero step-withs, same photo as the feed card, single .solo filmstrip thumb) + Grow a balcony garden (Achievement, 12 steps, finished October, 23 celebrated). Her door/celebrator faces are hers alone, and the visitor-list screens named them (2026-07-15): given — Noor Haddad (women/22), Elias Okafor (men/55), Freya Lund (women/85), Camille Roy (women/50, the +1 behind the facepile); received — Emil Novak (men/9), Saoirse Doyle (women/8), Tomas Vega (men/60), Ingrid Solberg (women/90). Their journeys (Walk the Camino, Bake rye bread at home, Grow a herb windowsill, Learn to throw pottery) are Lena's-circle canon, distinct from Chau's cast. Mai Phương (@maiphuong, women/81, "Learning to swim at 29.", 41 followers · 52 following) exists ONLY on profile-locked.html — a locked-page-only face, same logic as the blocked-only faces. Search-landing's Lena row reads "just started — first step on air" (the earlier "26 stepped with" was impossible against her day-one state).
The search landing adds three suggested-people faces that exist nowhere else — Tomás Rivera (men/47), Astrid Meyer (women/63), Jonas Weber (men/71) — because suggestions are by definition people Chau doesn't follow yet; reusing the followed cast would mock an impossible state (same logic as the blocked-only faces on settings-privacy, where Hana Sato is women/35).
The first-run mocks add a third viewer (2026-07-11): Minh Trần (@minhtran, men/40, bio "Figuring it out, one step at a time.", 0 followers · 0 following, zero journeys, zero badges) exists only as the viewer of feed-empty.html and the states-empty.html board — his face renders only on the empty profile frame. The feed-empty cards reuse the established cast viewer-relatively: Lena's be-the-first card verbatim, plus Maya Okonkwo's and Idris Kane's cards from feed.html with Follow (not Following) buttons and Discover chips, because Minh follows nobody — same people, different viewer, intentional coexistence. onboarding-people.html (Minh's view too) reuses the search-landing suggestion trio (Tomás men/47, Astrid women/63, Jonas men/71 — suggestion-only faces, consistent) with Chau pinned + pre-selected first. badge-reveal.html is Chau's moment: Celebrator on Marco Ferri's guitar journey, memory line verbatim from recognition_overview.md. Its trending zone reuses the cast honestly: Marco's guitar journey, Bruno's achieved half-marathon (the finale spike is why an Achievement can trend), Maya's live Couch to 5K, Lena's sourdough (which also explains the "sourdough" recent query).
Building new screens (agent checklist)
Future screens (create-step, create-journey, onboarding, chat, …) join this set by following the recipe that built the existing ten:
1. feed.html is the source of truth. Copy its :root verbatim (all tokens incl. --live-text), device frame, SVG status bar (never emoji), fonts. Pure CSS — no Tailwind. 2. One dominant job per screen. Write the job as a question ("Who needs me now?") before designing; everything that doesn't serve it is V2 or cut. One CTA per screen. 3. Sunset taxonomy, one accent per card/row/tile. Marigold = first/Starter, tangerine = live/Companion, rose = final/Celebrator. Sub-12px text on tinted chips uses the darkened variant of the row's accent: --live-text / --starter-text / --celeb-text. Serif = authored voice only (the "could a human have said it" test). 4. People before numbers. Faces land first; big numerals only for the two door stats; counts elsewhere are captions. Density rule: dense in people, light in encoding — one chip + one number per row, detail one tap deep. 5. The 24h window is the loudest read wherever steps appear: live = accent + ring + badge-named-at-action prompt; past = full card, vivid media, quiet chrome (never grayscale). Bursts are tiered: puff / rise / settle. 6. Spec-check before inventing against StepoBackend/docs/project/*_overview.md. Verbatim gamification voice only. No streaks, day-counts, unlock counters, global ranks. Real vocabulary: Heart · Comment · Share, step with, Starter/Celebrator/ Companion (+ its four tier names). 7. Numbers must reconcile with the existing state catalog (12/19 doors, Marco's guitar journey 14 steps · 341/58/19 · 5h left, Chau's cast of people). Reuse the cast; don't mint contradicting data. 8. Wire the nav per the contract (5-slot bar, active-tab rule, drill-ins get back arrows) and update the gallery (scripts/build-gallery.py FEATURE_BLURBS + ORDER), then re-run it. 9. Verify before done: tag balance, every href resolves, no banned patterns, real photos everywhere. 10. Copy rules (2026-07-13, PO): user-facing copy shows, never claims — no "never X / no Y" slogan constructions (state what the product does; a positive-exhaustive "That's the whole list." beats a never-triad). And no em-dashes in user-facing copy — restructure into two sentences, or use a colon or comma (a plain "-" only when a dash is truly needed). Marketing surfaces (store, landing, screenshots captions) hold this strictly; existing canon memory-lines are swept opportunistically, new copy complies from birth.
Handover to implementation (Revamp V1)
For the agents building this in stepo-mobile. The mocks and this doc are the design authority; they are not the only authority:
Three sources of truth, by question. How should it look/feel? → these mocks + this doc. How should it behave? → StepoBackend/docs/project/*_overview.md (the mocks render the spec; if they ever disagree, the spec wins — then report the mock as a bug). What data exists? → the generated StepoService / OpenAPI DTOs; regenerate the client when backend endpoints change. Follow stepo-mobile/CLAUDE.md for architecture (BLoC/Cubit, auto_route, BaseListCubit for feeds/lists).
The revamp supersedes the current mobile design system. New tokens (this doc's palette incl. the three contrast text variants), new type stack — Archivo (display) · Inter (body) · Fraunces italic (authored voice only). All three are Google Fonts (free, bundleable via google_fonts or asset fonts) and replace Cabin. The existing "cards lift, chrome stays flat" shadow doctrine carries over unchanged. Keep the pill mark assets from brand/ (mono glyph = Feed tab icon at currentColor).
Suggested build order (each stage leaves the app shippable): 1. Theme foundation — tokens, type stack, shadows, the 5-slot tab scaffold + nav contract (composers modal, drill-ins push, tab-active rule). 2. Feed — On air / Earlier zones, the three live hero cards, countdown rings (static state), prompt lines, bursts, full-quiet past cards. 3. Journey + Step detail — people panel, trail, finish banner; comments + sticky composer. 4. Profile + doors + Badges/Supporters + profile-steps grid. 5. Create flows (step, journey two-screen) wired to the FAB. 6. Search + Activity (notifications) + signup/onboarding reskin.
Known dependencies / flags for V1:
- Backend search is fully shipped (
/api/v1/search/{accounts,steps,journeys}+
suggested-users + popular-journeys) — V1 uses accounts + journeys; the steps endpoint is already there for V2's step-content search.
- "On air sorts first steps first" is an optional backend ranking rule — the feed
must not depend on it.
- Chat, avatar rail, away-digest, weekly pulse, grid toggle: V2 — visible in mocks,
do not build; leave Feed's top-right slot empty.
- Micro-labels in mocks run 8.5–10px; implement at ≥11px and test Vietnamese
diacritics (the mocks are English-only).
- App icon: generate PNG ladders from
logo/stepo-icon-store.svg(light), the carbon
variant as iOS dark appearance, mono glyph for tinted/themed.
- Feed media clamp (see the ratio ruling in Q&A + its 2026-07-13 revision):
displayAR = sourceAR.clamp(1.0, 1.91) — the tall bound is SQUARE, not 4:5 — with center cover-crop. Pre-size each media box from StepMediaDto.width/height (already in the API) before the image loads — no layout shift while scrolling, and no backend work needed. Step detail shows the media whole (natural AR, fit-width); only the detail's media tap opens the viewer.
For the Flutter port
- Motion: burst particles (tiered), live-pill blink, and card entrance rise are
prototyped in the HTML — those timings are the spec. Countdown rings are static state, not animation: fill fraction = hours-left / 24 (the stroke-dashoffset math in the SVGs); they update as time passes, never visibly sweep.
- The 8.5–10px micro-labels in the mocks need a legibility pass (≥11px) and testing with
Vietnamese diacritics.
- Dark theme: not designed yet; the carbon "moment" card is the natural seed for it.
- Accent halos on live cards =
box-shadowcomposition (soft ring + colored glow + base
shadow) — see .hero in feed.html.
- Continuous ("squircle") corners run on Flutter's native
RoundedSuperellipseBorder/
ClipRSuperellipse (2026-07-13), not a third-party package — border and clip are drawn from the same path, so a bordered, clipped card (an avatar ring, a media thumbnail) never shows the corner shaved short that a separate Border.all + ClipRRect pair produces. AppShapes.shape(radius, borderColor:) is the one call site; use it (or RoundedSuperellipseBorder/ClipRSuperellipse directly) for any new bordered or clipped surface — never plain BoxDecoration.border + ClipRRect.
Store kit (../store/, 2026-07-12)
Submission marketing assets, generated from the mocks themselves so the store never shows a UI the app doesn't ship. store/shot.html?id=01…06 composes a caption block (Archivo 800 headline, one Fraunces-italic accent word, kicker pill in -text twins) over the real mock iframed inside its device frame; shots.js holds the copy and names each mock from the repo root (app/feed.html); store/render.sh exports App Store 6.7" (1284×2778), Play phone (1080×2160), the Play feature graphic (1024×500, carbon field + paper pills with the newest pill on air), and the 1024/512 store icons from ../brand/stepo-icon-store.svg. Shot order = the pitch: what it is → the 24h mechanic → the quiet promise → journeys → recognition → profile. All store metadata (names, descriptions, rating-questionnaire guidance, data safety, review notes) lives in store/store-copy.md.
Screenshot size (2026-07-14). App Store Connect accepts 1284×2778 and 1242×2688 portrait; the 6.9" native size (1320×2868) is rejected. The shot template is viewport-relative, so it recomposes at any accepted size without layout work — the 6.7" set covers the largest-iPhone slot and Apple down-scales it for the smaller classes.