Gallery Design notes

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 in app/, and the gallery at the repo root (python3 scripts/build-gallery.pyindex.html) renders every one of them with its dominant job. chat and chat-thread are 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)

ElementSource concept
Live / Earlier feed structure, countdown rings, badge-named-at-action copybroadcast-orange
Serif-italic journey titles, "Needs you now", people carried on journey cardsalongside
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" framingwaymark-2 / broadcast

The system

Palette — the sunset taxonomy. All recognition colors are siblings of the brand orange:

TokenHexMeans
--live#FF6B3CThe 24h window, interactivity, Companion — fills, rings, borders ONLY, never text
--live-text#C4441CText/glyph twin of --live — ALL live-hued text and icons, any size (5.0:1 on white)
--starter-text#9A6100Text/glyph twin of --starter — in every screen's :root since 2026-07-11
--celeb-text#C22860Text/glyph twin of --celeb — in every screen's :root since 2026-07-11
--danger#B3322EDestructive 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#191817Ink; also the inverted "moment" card (neutralized 2026-07-12, was warm #1A1512)
--paper#FAFAF9Canvas — 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).

The screen wash — one token, five screens (2026-08-08). The signed-out stretch — signup.html and the four onboarding*.html beats, and only those — paints radial-gradient(130% 70% at 50% -8%, #FBFBFA 0%, var(--paper) 55%, #EFEFEE 100%) instead of flat paper. It is what makes the pre-app screens read as one continuous room; inside the app the canvas stays flat. All three stops are neutral, like every other piece of chrome. It is a token, not a recipe: it was copied by hand into the Flutter port and drifted exactly as you would expect — two screens kept a warm pre-ruling copy (#FBF9F6#EFEAE4, visibly beige on the welcome screen) and three never received it at all. Held once in AppColors.screenWash, with a test that fails if the stops are written anywhere else.

Rule: one accent per card. A card is marigold OR tangerine OR rose, set by its step type; never mixed.

"Live" is the only name for the window, untranslated (2026-08-08). A step has two states and each has exactly one word: Live inside its 24 hours, Earlier after. "On air" retires entirely, and so does the verb it grew — "After it aired" is now "After it ended". This overrides the mocks, which drew both names at once: the feed zone header carried On air and a Live pill and a 6 windows open tally, three names for one thing on one row, and this file's own note on the step-card pill already conceded the pill was "the same idea as the app's 'On air'".

Live does not translate. Vietnamese borrows it whole, the way it borrows "live stream" — "đang live" is ordinary daily speech, and "lên live" is goes live. The two native candidates both failed: "trực tiếp" is the word for a televised live broadcast and promises streaming the product does not have, and "lên sóng" is broadcast-industry register nobody says about their own day. This is the same rule the badge titles already follow (Starter / Companion / Celebrator): the term is a proper noun in every locale, and Vietnamese prose around it teaches the meaning. The four chips carrying it are locked in dialect as untranslatable.

The one thing "Live" could mis-promise is streaming, and the countdown does that work: nothing anyone calls a livestream runs with "14h left" beside it.

The pulse is a dot, not a word. What the tangerine pill was actually for is the pulsing dot; the word inside it duplicated the heading beside it. The dot moves onto the heading and the pill goes. This is the sanctioned use of tangerine in chrome — inside the app the colour means exactly one thing, and here it is saying it.

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 (badge names like "Companion"), 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?"

Live 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 Live 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 live 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 live" 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 live 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. ENTRY (amended 2026-08-11): the reveal fires at the act. The step-with lands, the cheer animation plays, and a beat later (~900 ms) the full-screen moment arrives in the same session — Starter and Celebrator always, Companion whenever its evaluation has committed by that beat, and otherwise on the next open behind a push that names the journey and withholds the tier. One full reveal per session: an earned-now reveal takes the slot and further earns queue for later sessions, oldest first. The screen holds the step's memento at its authored window, the emblem that is you (your avatar inside the tier ring, the tier glyph as a crest on its edge — raw rose for ring and glow, --celeb-text for every word), the verbatim memory line ("You were there at the finish."), one provenance sentence naming the act, the person and the window ("You stepped with Marco's final step while the finish was live."), and the journey title in Fraunces italic (authored voice — the title is the owner's words). 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, no facepile: 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 live 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 live, 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 a Live 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: arriving is the read. Opening Activity stamps everything waiting, so the bell's count clears at the door and New shows exactly what it cleared, frozen for the visit — nothing the reader came for shifts under them, and what lands mid-visit waits unread for the next one. A tap is travel and nothing more. The Activity tab carries the unseen count (to 9, then "9+"), because a number says how much is waiting where a dot only says "something". 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 Companion of your…") with their badge 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). The inline pills on people rows are server-truth: each row carries the viewer's live relationship to its actor, so a new-follower row offers "Follow back" only to someone who is not already following or waiting, and lands on "Following" or "Requested" as the actor's account decides. 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 on both the app icon and the Activity tab, never +1; the visit stamps both timestamps), the quiet rules, the never-send list, the permission-ask trigger table (which surfaces raise the primer, at what cadence, and what a blocked user gets instead — the cadence ruled in "The one shot at the prompt"), 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.

An empty profile, seen from both sides (states-empty.html §02; 2026-08-09)

The board above rules the owner's empty profile and the app never built half of it: the Steps tab had its quiet block, the Journeys tab had nothing at all, so a new account met a blank page under two tabs that both read 0. The dashed Start a journey tile is the fix and it was already drawn — it belongs to the Journeys tab, because that is the tab whose zero it answers, and it stays the page's only CTA.

The visitor's version is the same page with the ownership module absent: one quiet line naming what is not there, and nothing to press. "No journeys yet", and that when they start one it shows up here. A visitor's screen has no work for them to do, and the one thing a CTA could ask for on someone else's empty profile is that they go and prod that person into posting.

Never here: a CTA aimed at a page that is not yours, a follow-suggestion carousel backfilling the hole, a blurred ghost journey implying content behind a door, or a count dressed up as progress. The empty-state doctrine holds on both sides — honest empty is not filler — and the one loud exception stays the feed, 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.

A returning user enters offline (ruled 2026-08-10, from a prod report)

A signed-in user opening Stepo with no signal lands on their feed as they last saw it, aging truthfully (fetchedAt drives the "last updated" line the snapshot cache already owes) — a stale feed beats a locked door at every age, so there is no TTL on entry. The gate obeys one boundary: only an authoritative denial from the server (a ban, a deleted account, a rejected session) blocks entry; an inability to decide — no response, or a server that answers 5xx during an outage — falls back to what the device already knows. Reads come from cache; writes still need the network and fail fast and quiet, per the no-silent-replay rule above. No global offline bar and no special offline mode: offline is the same staleness every surface already shows per-surface, not a banner (chrome that claims to know reachability usually lies). The one door that stays shut offline is a device that has never synced the account — there is genuinely nothing to show, and the error card says the single requirement plainly: connect once. And the session rule underneath it all, the reason the report existed: only the server may end a session; silence never may. Being offline for a day, a week, a month is never a sign-out.

A shared link opens for someone with no account (ruled 2026-08-10, from a prod report)

A Stepo link is built to leave Stepo. It goes to people who are not here, and the only reason to send one is that the person on the other end can look. So the content opens for anyone the content is already public to, and the app asks for an account at the moment an act needs one, never before. A reader who arrives on a link is a reader, not a half-signed-in user: they get the step, its journey, the person who wrote it, and the public lists hanging off those, drawn exactly as a member sees them. Privacy removes ink, never the room — the same law the visitor doors already obey, applied one ring further out.

A guest never enters the app. The signed-out reader stands in a room of their own: no feed, no search, no create button, no bottom bar, nothing that implies an account they do not have. Back from the content is the sign-in screen, because that is where they came from and it is the only other room they have.

The room holds three things: the step, its journey, and the person who wrote it (PO ruling, 2026-08-10: "just the post, and profile is enough"). Reading one public step is one thing; being handed the gathered-up lists of everyone around it is another, and that second thing is a member surface. So the four doors onto other people all wait for an account: Badges Earned, Support Received, the follower line, the journey's people card, and the step's own guest book. The counts and faces stay on the page — they are part of the content, and a door that shows what is behind it is what makes signing in worth doing — the tap is what asks. This is the same access-versus-aggregation line the visitor doors already draw, applied one ring further out: a stranger with a link may read; gathering people up is for members.

Every act is a door, and a door says where it goes. Heart, comment, follow, report: each one asks, in the words of the act it interrupts, and offers sign-in. The rule that makes this worth building is the one about what happens next — signing in returns the reader to what they were reading. The moment someone decides to join is the moment they were moved by something specific; landing them on an empty feed with that thing gone spends the only intent the app will get. Sharing is exempt: passing a public link along needs no account, so the share sheet opens for a guest and only the record of it waits.

What a guest must never meet is a failure. A dead button, an error toast, a screen that half-loads: each of those says the app is broken rather than that the reader is new. Governing line: the link opens; the act asks.

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.

The ink button belongs to the platform's leading provider, not to Apple (2026-08-08). The mock is drawn for iOS, where Apple takes the one ink fill and Google and Facebook are white cards behind it. Ported literally, Android — which has no Apple sign-in, and cannot have one without a web-redirect flow we do not ship — lost the anchor along with the button and showed three identical white cards with nothing leading them. So the ink treatment is positional: exactly one per screen, worn by Apple on iOS and by Google on Android. Google's own branding permits a dark button with the full-colour mark, so nothing is bent to do it.

Spare vertical room splits above the collage and below the promise (2026-08-08). The mock's canvas has 19px to spare and hands all of it to .auth{flex:1; justify-content:flex-end}, which is invisible at that size. A tall Android phone without the Apple button has ~101pt to spare, and all of it in one place tears a hole between the promise and the sign-in stack. Two anchors stay fixed — the wordmark 22 below the status bar, the job at the thumb — and the collage-and-promise block floats centred between them. On iOS there is ~0 to spare, so this is the mock unchanged.

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 earned its own mock on 2026-07-12 when the founder distribution ruling turned it into a selection surface, and from 2026-07-29 the explainer beat sits between the two.

Onboarding — what you're starting (onboarding-how.html, ruled 2026-07-29)

The one explainer beat, added because the concept was arriving nowhere: users come with Instagram priors (a post is permanent, a like is a like), and at this graph size nobody can learn by watching others. In a mature app the feed teaches the app; at n≈0 that burden falls on onboarding whether we like it or not.

The governing line: name the promise, never the arithmetic. The screen teaches two words and one fact — journey, step, and a step is live for 24 hours — and it is exactly THE core pitch's four beats in order (journey → window → remembered → look back). It says nothing about step-with mechanics, supporters, Companions, badges or thresholds, and no tier name may ever appear on it. Recognition is discovered, not operated: a user told the shape of the reward starts working out how to earn it, which is the failure the badge model exists to prevent. The presence line is a plain count of people, never a tier and never a prompt to act.

The example is real, not drawn. It renders the feed's own live-step component filled from the public API at display time, so it cannot go stale and cannot lie — show, don't claim, applied to the explanation of the product itself. Three honest states, all derived from the step's timestamp: live (countdown ticking, preferred), closed (full-quiet past card, past tense, people kept — not a degraded state but the remembered beat drawn instead of claimed), and no card (the parts list carries the screen alone, the "here is one happening right now" line simply isn't said). Nothing is padded to compensate for a missing card; a screen that pads when it has nothing to show is a lying onboarding. Caching an example is safe for the same reason — a stale card becomes a closed card.

Selection comes from the editorial seed pool (EditorialSeedAccountIds, the same list that pins the people beat), never a hardcoded founder account, so keeping the screen alive is a config list rather than one person's posting obligation. The example is the most recent step qualifying on either ground: published within the last week, or someone stepped with it, however long ago. Recency and presence each carry a step on their own — a quiet week still has something to show, and a day people turned up for never ages out (PO ruling 2026-07-29, relaxing the original live-or-attended rule). The 24-hour window is the feed's clock and too tight here: pinned to it, one missed day empties a screen every new user sees. Nothing looser than a week, because past that "here is one from earlier" stops reading as a living app and starts reading as an abandoned one.

Position (PO ruling): the people beat stays mandatory, so this sits before it — knowing what a journey is makes the follow list legible too. One way forward, no secondary link ("Next: find your people"). The authoring-first bias is honored by where the user lands after onboarding, not by reordering these beats.

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 → what you're starting → 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, stated as evidence rather than negation (recast 2026-07-13, the no-claims copy rule): the exhaustive trigger list ("When someone steps with you: a heart, a comment, someone sharing your step. And when a journey you follow goes live.") closed by "That's the whole list." → "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 into the engagement cadence below. Zero accent: onboarding is chrome, and orange never enters the chrome.

Engagement keys the re-ask, and it is a baked-in rule of the app (ruled 2026-07-31; no mock — this ruling is the spec). Stepo is a 24-hour-window product, so push is structural rather than promotional: without it a user cannot know a journey they follow is live, which means they are not experiencing the product at all. The two failure modes are asymmetric. Asking too often costs a sheet swipe: recoverable, bounded, annoying at worst. Never converting permission costs the user, permanently and invisibly. So the cadence leans toward asking, bounded only by what the OS makes mechanically pointless, instrumented end to end, and re-balanced with real cohort data.

A primer may show whenever the OS will still prompt and the cadence allows it. The gate is live OS state, never an app latch that stands in for it: iOS self-latches (after its one alert the state never returns to askable, so the cadence ends exactly where it becomes pointless), and Android allows a second prompt after a soft deny, which the cadence spends. A swipe-away re-arms the counters instead of closing a door forever. The one place the fired latch stays a state input is Android, which cannot tell "never asked" from "permanently denied" on its own — without that bit, fresh installs read as blocked before the native prompt has ever fired.

SurfaceTriggerCadenceVoice
Onboarding beatLast onboarding beatOnce, routedThe whole-list promise
Follow sheetA follow lands1st follow, then every 2nd"You're following {name}." / "Know when their next step goes live."
Interaction sheetHeart / comment / step-with sentEvery 3rd"You showed up for someone." / "They step for 24 hours."
Publish sheetStep goes liveEvery publish, bypasses the cooldown"Your step is live." / the window argument
Settings bannerPermission offPassive

All sheets carry the canon sheet anatomy (grabber, Archivo w800 headline, muted body, ink CTA, muted "Not now" — publish_failed_sheet.dart), share a 5-minute cooldown, and a showing resets every counter. The sheet surface runs under the home indicator and the last row is padded clear of it, never a scrim strip beneath the card. One anatomy, four arguments. Follow starts at the 1st press: the first follow is where push value becomes concrete for a consumer, and making them follow twice before we mention it wastes the warmest beat. Publish repeats and ignores the cooldown — an author putting a live step unpermitted is the highest-stakes miss in the app, every time, and it self-selects to people who have published. The publish sheet leads with the window because that is what makes the ask honest there: 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. Zero accent throughout, same as the beat they repeat. 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.

Blocked users still get one door, and it decays. Once the prompt is spent a tap-through can only reach system settings, so the same triggers convert to a settings-variant sheet ("Notifications are off for Stepo." / "Turn on notifications in Settings, it takes ten seconds." / Open Settings) at one per session, first trigger wins. On iOS that variant is the entire long tail of the system, so excluding it would quietly end the cadence at the first soft "no". A blocked user has already answered at the OS level, though, and every-session-forever is how an app earns "it won't stop nagging" reviews — so after three consecutive dismissals the rate drops to one per seven days, and any tap-through resets it. What makes that variant honest is the resume repair: a grant made in system settings is picked up the moment the user returns, rather than at the next process death.

Every showing and every outcome is captured (notif_ask_shown / _accepted / _deferred / _os_granted / _os_denied, each carrying surface, variant, counter value and session number). The cadence was set on judgment; the first real cohort judges it back. What stays untouched: the never-send list and the push trigger matrix (what we push is a separate promise from how we ask), the onboarding beat, and the passive settings banner.

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 — "Live 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 live." (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.

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.

Edit step, and the record it leaves (step-sheet.html; 2026-08-09)

The 2026-07-13 board cut Edit step as a dead tap and parked it "for its own future ruling before anyone re-adds it." This is that ruling, and it re-adds the row with a second one beside it.

Only the note is editable. Not the media, which is authored once through one window and never re-cropped after the fact; not the journey it belongs to, not its kind, not its hour. So Edit is a text surface — the composer's note field with the step's words already in it — and never a second pass at the composer. A typo, a sentence that came out wrong, a name spelled properly the next morning: that is the whole of what this door is for.

A step that changed says so, to everyone. The sheet carries an Edited row for every viewer, not just the author, showing when the note last moved; tapping it lists every edit time, newest first. A step is written to people who then heart it, comment on it, and step with it, and words that move after that arrive somewhere other than where they were answered. The reader is entitled to know that happened.

What is never kept is the replaced text. The record is the moment, and only the moment. Knowing the words changed is what a reader needs; reading a sentence its author withdrew is not something anyone is owed, and storing it would make a permanent second copy of writing someone chose to take back. We show that it moved, never what it used to say.

Two things are not edits: marking or unmarking a milestone, which sends the same note back untouched, and a save that changes nothing. Stamping "edited" on a step whose words nobody touched tells every reader something untrue about its author.

Ink: Edit is ink, and so is Edited. Neither destroys anything. Rows, in order — your step: Share · Edit step · Edited · Mark/Remove milestone · Delete step. Someone else's: Share · Edited · Report step. The Edited row is absent on a step nobody has edited, which is nearly all of them, so the tray does not grow a permanent empty seat: the sheet is a tool tray, not a feature list.

Taking a step-with back (step-with-sheet.html — a spec board, not a screen; 2026-08-10)

A supporter mis-tapped an un-heart and lost the step-with behind it. The heart is a toggle everywhere in the app, and on almost every step it can be pressed twice with nothing lost; on one kind of step it cannot, and nothing in the interface said so. This is the ask that catches it.

It fires on the condition, never on the act. Only the tap that would end the step-with raises it: the heart, or the comment delete, when no other act of that person's is still holding it. A step-with is one fact per person per step, carried by the step's heart, the step's share, or a top-level comment; a heart on a comment never carries one. So un-hearting a step you also commented on is silent, a late heart is silent, your own step is silent, a comment heart is silent, a reply delete is silent. Six taps that remove something, one that asks. The rarity is the guard: a sheet that fires when nothing is at stake teaches the thumb to dismiss it unread.

In the window and past it, both (PO ruling, 2026-08-10: "we should warning no matter what as accident can trigger a badge"). The earlier reading, that an in-window withdrawal is free because it can be re-earned, was wrong on the arithmetic: pulling the act takes the step out of both sides of the coverage ratio and the membership engine re-runs on the spot, so a Companion can fade in the same frame, silently, by design. On a first step it also recomputes whether the person was a starter, moving them between the 70/60 and the 90/80 lines. Recoverable is not the same as harmless when nothing announces the loss.

One anatomy, two states. Same sheet, same two buttons; the window swaps one sentence, the ring and the caption. Live: the step thumb wears the --live ring and the caption counts the hours left, and the copy ends "You can step with it again while it is live." Closed: no ring, the caption dates the close in words, and the copy ends "Its 24 hours are over, so this one cannot be put back." That clause is the only thing separating the harmless tap from the permanent one, so it is the only thing that changes. A context row (thumb, journey, window state) leads the sheet, which no other confirm carries: every other confirm answers something you asked for, and this one arrives out of a mis-tap, so it has to name what it is about.

Keeping is the styled primary, and this is the only place that inverts. The step-sheet ruling puts the destructive verb on the styled primary and never hides it beside a big Cancel, and that is right wherever it applies. The distinction: a confirm honors an act you chose, so the chosen verb leads; this sheet catches an act you may not have chosen, so keeping leads. Swipe, scrim and back all mean Keep it. The quiet way out never destroys anything. Delete step, delete journey and delete comment are untouched.

Recognition is never quoted. No percentage, no "you will lose your badge", no meter. The fade is silent by design and predicting it at the moment of a tap hands people a number to play with. The sheet speaks about the step's record, which is the part a person can see. It does mean withdrawing a heart on a first or final step also ends a Starter or Celebrator, since all three read from the same step-with, and the sheet is what makes that deliberate rather than accidental.

The comment door keeps the delete grammar. It already confirms, and a chosen Delete keeps its styled danger primary; it gains one added sentence when that comment is the last thing holding the step-with, and no stacked sheet.

Nothing moves until the answer. The heart stays filled behind the scrim and the count never flickers, so there is no optimistic un-heart to walk back. And the client is never left to derive the condition: the rule for what carries a step-with lives on the server, which answers it as heartHoldsStepWith and holdsStepWith (interactions_overview.md).

The comment sheet, and what a comment may take back (comment-sheet.html — a spec board, not a screen; 2026-08-10)

A comment was the only thing in Stepo a person wrote and then could not touch. The step it hangs under can be edited and deleted; four words underneath it could not be. The sheet that should have held those doors shipped Reply · Report · Hide, and two of the three did nothing when tapped — the same defect the step sheet was ruled over in July, sitting one surface away. This gives the comment the doors its step has, adds the one every reader reaches for, and clears the dead rows out. Governing line: you can take back a whole step, so you should be able to fix four words.

The step's edit ruling holds here word for word. Only the words are editable — not the step it hangs under, not its time, not who wrote it. A comment that changed says so, to everyone. And what is never kept is the replaced text: the record is the moment, and only the moment. A save that changes nothing is not an edit and stamps nothing.

Editing never moves the clock. A comment carries its step-with from the moment it was posted, and an edit does not touch that: one written at hour two keeps its step-with when it is fixed at hour thirty, and one written at hour thirty never gains one by being edited inside a later window. Otherwise Edit would be a way to buy a place among the people who were there, and the guest book would stop being a record of who showed up. Edit changes the words. It never changes when you said them.

Edited is said on the row, not behind a row. The step keeps its edit record behind a sheet row because a step is a large authored thing edited over days; a comment is one line with a timestamp already on it, so the honest place to say the words moved is beside the time they moved: 2h · Edited 20m ago. Two times, because they are two different facts — the first is when this person showed up, which is what the 24 hours turn on, and collapsing them would quietly restate the arrival. Faint ink, everyone sees it, and it is not a door: there is nothing behind it to open. The Stepped with chip is untouched by an edit, because the arrival was not.

Rows run harmless to permanent. Your comment: Copy text · Edit comment (footnote "Everyone will see that it changed.", because a person fixing a typo at midnight is entitled to know that before the tap) · Delete comment in the sheet's only danger ink. Someone else's: Copy text · Report comment in ink, since protection is not a destructive act. A reply of yours gets the same rows; nothing about a reply differs except what its delete confirm says.

Copy text, and it says so. It leads both sheets because it is the lightest thing in them, and it is the more useful row on someone else's comment — your own words you already have. It is named for what lands on the clipboard rather than for the object, because "Copy comment" is the phrase every other app uses for a link; the precise label beats the consistent one when the consistent one would mislead. This is the app's first clipboard action, and it carries one requirement: a copy that says nothing is indistinguishable from a dead tap, which is the defect this whole board exists to clear. The sheet closes and a quiet "Copied" confirms.

Editing is the composer, already filled. No screen, no modal, no second pass at anything: the composer waiting at the foot of the thread fills with your words, the send glyph becomes a check, and a plain Cancel sits where the prompt was. The live hint the composer normally carries ("Commenting now counts as stepping with…") is replaced while editing, because it is not true of this act.

Delete keeps the delete grammar. Step-sheet panel-03 exactly: an honest sentence and a styled, labeled danger primary, because the person chose Delete from a tray. It gains one added sentence when this comment is the last act holding their step-with — "It is also the only thing keeping your name among the people who stepped with this step" — and the final clause "and its 24 hours are over" only when they are. Never a stacked sheet: the heart raises its own ask because that tap is a toggle someone may have hit by accident, and this one is a chosen Delete. The server answers whether the sentence is true, as holdsStepWith; replies never carry it. Deleting a comment leaves the hearts other people put on it alone.

Cut: Reply (a dead tap, and the comment row already carries a Reply button) and Hide (a dead tap; there is no hide feature, and blocking lives on the profile where the whole relationship is). Two rows is a finished sheet — the sheet is a tool tray, not a feature list.

Profile sheet board (profile-sheet.html — a spec board, not a screen; 2026-07-27)

The profile's overflow sheet ("···" on someone else's profile), drawn after App Review put the safety surfaces under the lamp. profile-other.html had always described this sheet in a kebab comment and pointed at report.html; the sheet itself was never drawn, so the app shipped it in a second, drifting row grammar (OptionWidget: 48px pill rows, 17px regular labels, asset icons). Rows: Share · Report profile · Block, and no row was cut or added — this sheet had already earned all three. There is no owner variant: your own profile carries the settings gear instead. Anatomy is step-sheet.html verbatim, now from one shared component (SheetRow/SheetRows) rather than two copies.

The new ruling is about ink: Report and Block both stay ink. Danger ink marks irreversible destruction, and neither destroys anything — reporting protects someone, and a block is lifted from Settings › Blocked accounts. The block confirm therefore takes a carbon primary, not danger, and its copy states the way back: "You stop seeing each other's steps and journeys, and any follows between you are removed. You can unblock in Settings, but the follows will not come back." That last clause is the honest part — unblocking does not restore severed follows. The legacy body claimed blocking stopped someone tagging you; Stepo has no tagging and never has. Copy on a safety surface must be checkable against the code.

The moderation tray, and the ban ladder (profile-sheet.html §04-06; 2026-08-09)

Admin mode ruled "one surface, rows cut by role" and parked which actions a moderator keeps until a moderator existed. One does, so: an account carrying a role sees the visitor sheet unchanged, with a labelled Moderation section appended. The section header earns itself here even though section 01 rules that three rows need no chapters — Block and Ban sit inches apart and mean entirely different things, one personal and one the platform speaking, and a moderator who confuses them at speed does real damage.

The ban ladder is the safety model. Risk arrives faster than certainty, so the tray is shaped to let a moderator stop harm before they are sure: (1) something looks risky → Ban this person, which is the quiet ban — the account keeps working from the inside and reaches nobody from the outside; (2) read the reports, look at the account, no rush; (3a) it was fine → Lift the ban, and the person was never told; or (3b) it was not → Ban them for good.

Step 1 costs nothing to be wrong about: nothing is destroyed, no session is killed, no file leaves the CDN, and lifting restores the person exactly as they were. That is what makes it safe to be fast. Step 3b closes the session and permanently deletes every photo and video the account uploaded, which is the right answer to an account farm and a terrible thing to reach by accident on a product whose promise is looking back. So Ban them for good and Remove everything they made appear only on an account that is already quietly banned. The destructive act is never one tap from a profile; it is one tap from a decision already made. Fast where it is reversible, slow where it is not: the ladder is the safeguard, not the distance.

Ink follows from that and does real teaching work: standing in front of a working account a moderator is offered no danger ink at all, because everything on offer can be taken back. The one ban row is state-aware, reading the account's real standing before it draws itself, and once banned its footnote carries what you need in order to reverse: when, who, and why. The ban confirm states the consequence in the account's terms ("They keep their account and can still post. Nobody else sees them or anything they make.") rather than the system's, and its reason field is required on the client because it is required on the server — a ban with no stated reason is a ban nobody can review later. Lifting asks once and argues nothing: restoring access is the safe direction. Every action writes an audit entry. Nobody bans themselves, and no moderator acts on staff at or above their own tier.

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 live" (arrival order, tiny time captions like "1h in": the book reads as the day unfolded) then "After it ended" (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.

SurfaceVisitor seesOwner sees
Badges Earned doorunranked people/journeys this user showed up forfull personal detail (badges.html)
Support Received doorunranked people who showed up for this userprivate ranked gratitude (supporters.html)
Podium, crown, ordinal, "most", "top"neveryes

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:

LayerSaysWho 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 live, 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 live / After it ended 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:

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 live 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 live 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.)

Addendum, 2026-08-09. Not rendering the invitation is not the same as leaving the line empty. The author's own line — "While it's live, anyone who hearts, comments or shares steps with you." — describes what other people can do, so it never invites the author into an act that will not count, and the ruling above holds. It used to appear only when the row would otherwise be blank, which inverted the intent: the moment somebody stepped with you, your own card lost its second line and went back to one line beside a ring twice its height. The band beside the ring always carries two lines: the audience's invitation, or the author's read of the same window.

The stepped-with line finishes its sentence (2026-08-09)

The live card's l1 — "{first} and {count} others stepped with {author}" — names two people and says the one mechanic the product is built on. It sits in a ~194dp column: the card's inner width less the 56dp ring, the gap, and the stack of faces.

The sentence does not fit one line, and cannot be made to. Its invariant skeleton alone, " and 9 others stepped with ", is wider than the column before either name is placed. So a one-line clamp never shortens this line, it amputates it: what survives is the part that varies (a name) and what is cut is the part that carries the meaning (the verb, and who it was done for). Real cards read "Tina Nhi Nguyen and 9 othe…" and "Chau Cao and 3 others step…" — faces, a name, and no mechanic. Ruling: l1 wraps, capped at two lines. feed.html already ruled this and was simply not read: it sets no nowrap and no clamp, and both of its example cards render this row 32px tall, which is two lines of 13px. The step page and the chapter card allow two. The feed card was the outlier.

On height, since the feed card is already tall. The row is max(ring 56dp, text column). Measured on device against a fixed media anchor: on your own live step the wrap costs nothing — the action row and the card's bottom edge land on the same pixel either way, because the column stays under the ring's 56dp. On a visitor's card, where the prompt sits under it, the column crosses that floor and the card grows 9pt. That is the price, stated plainly, and it is the right way round: a card is long because of the media it carries, and buying 9pt back by deleting the verb from the only sentence that explains the product is not a trade.

(The author's card then spends 13dp of its own on the second line the addendum above restores. Different decision, same band, and worth separating: the wrap buys back a sentence that was being destroyed, the second line buys a balanced block.)

The general rule this is an instance of. When a sentence must truncate, the invariant belongs where it survives. A line that ends in a name degrades to a shorter name; a line that ends in the verb degrades to nonsense. Any future one-line sentence in this app gets read against that before it ships.

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:

TierTemplate (subjectless form)
Step-with (no badge)Stepped with J.
StarterThere when J began.
CelebratorShowed up to celebrate the finish of J.
Starter + CelebratorThere when J began, and back for the finish.
CompanionKept showing up through J.
Companion + StarterThere when J began, and kept showing up.
Companion + CelebratorKept showing up through J, and was there for the finish.
Companion + Starter + CelebratorThere when J began, kept showing up, and was there for the finish.

Repetition between adjacent rows is the truth of a templated system and is fine. All six recognition mocks carry these lines.

Superseded in form by "One Companion, no ladder" (2026-07-24): the four tier names are retired, and the table above is now the output of a generative rule rather than a lookup — one clause per fact held, joined beginning → through → finish. The ruling's intent survives intact and is strengthened: a sentence claiming a moment the person did not have is no longer forbidden, it is unconstructable.

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. Stepo passes, and so does Live — added by the 2026-08-08 window ruling above, on the same grounds the badge titles hold: Vietnamese borrows it whole from "live stream", so it is a proper noun in every locale. The chip reads "Live"; sentences read "đang live".

The framework, proven across 751 keys:

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:

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):

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:

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:

Addendum: the close sheet is one sheet with two states, and closing is a note moment (2026-07-31, founder-approved) The shipped flow raised a confirm dialog on top of the open Manage sheet: two scrims, two handles, and a yes/no question, for an act the ledger already calls non-destructive. Three rulings.

1 · One sheet, two states. Tapping the Close row rearranges the same sheet container in place: the scrim stays the scrim, the grabber stays the grabber, the rows give way to the close state, and a back chevron in the header slot returns to the rows. The sheet is never dismissed and never re-presented. REJECTED, a confirm sheet (or dialog) over the Manage sheet: one decision earns one surface, the second scrim darkens the first into mud, two grabbers disagree about what a downward drag dismisses, and a stack of overlays reads as a warning that a pause has not earned. The same rule governs every in-sheet fork on the owner's own object: swap the container, never stack another one. Delete keeps its own confirm, because a destructive act asks once, and it too swaps in place rather than stacking.

2 · The close state is the trail-end note moment, not a confirm. Its job is the last word, not a yes/no. Close is non-destructive by ruling, so the primary is ink, never --danger and never rose. The exact copy, in order:

The Manage row's caption sheds its reopen sentence and returns to the ruled form, "Pause or park it. No final step needed.", because the consequence now lives where the decision is made.

3 · The closing note renders as the trail's quiet last word. On a Closed journey that carries a note:

Sheet-header spacing (fixed in the mock, so the build has the right reference): 8 above the grabber, 20 between the grabber and the title, 14 under the title before the first row. The old 4/12 crowded the title into the sheet's 28px top curve.

Mock: journey-manage.html, both sheet states side by side in the state-catalog layout (.slot + .cap, the onboarding-how.html idiom).

The journey screen carries its own doors (2026-08-12)

The journey screen is pushed full-screen over the whole scaffold, so it has no tab bar and no Step FAB. Everything the owner does here therefore had to go through the ⋯ sheet — except the one thing that did not: a full-width "Complete journey" button under the hero, drawn in no mock, sitting where the reading starts and asking for the rarest act in the app. Four rulings replace it, and they turn on where an act belongs rather than on how it looks.

1 · The everyday act gets an everyday door: + in the app bar. An owner opens their own ongoing journey to add to it, so the app bar's right cluster is + · ⋯, both in the mock's 38px .iconbtn chrome, 9px apart. The + appears for the owner of an Open journey alone: a finished story is continued or reopened deliberately, through the sheet, and a visitor adds nothing. REJECTED, a dashed stub at the trail head: the frequency argument is the whole case for promoting this act, and a door below the fold is not a door. REJECTED, a floating action button: this page is a reading surface, and new floating chrome over the trail is exactly what the full-screen push was buying us out of.

2 · The ending has an author, and the act sits on the row that names it. The Celebrator row's still-possible state says "The finale hasn't been written yet." to every reader. For the author of that finale it carries a quiet rose "Write it" pill in the .followbtn grammar, in the slot the count and chevron would use — free precisely when the act is offered, because an unwritten finale has nobody behind it yet. Four conditions, all of them: owner · Open · no finale yet · the three-step floor (2 existing steps plus this one, the rule the backend enforces). It opens the same composer the + does, with the final-step mark on and this journey in the slot.

The one-interface law holds: the sentence is the same sentence for every reader; ownership adds the act, never a different row. Below the floor the pill is simply absent — the card states what is true of the people and never explains the mechanics of a rule; the manage sheet keeps that job, as a disabled signpost with the reason in its caption.

3 · A row leaves a sheet when a better door exists, not because the sheet is long. The manage sheet's Add a step row goes on Open journeys, where it is a second tap to the place the + already is. It stays on Closed journeys, which carry no +, and where the act means something more than posting — so its caption says so: "Reopens this journey. It goes live for 24 hours." Achieved journeys keep Continue journey (2026-07-17). Mark as achievement stays on both: the sheet is the deliberate door and the card's pill is the door in the moment, and because both open the same composer state they cannot disagree.

4 · The rows read in story order: Starters · Companions · Celebrators. The card's lead sentence has always read in time order ("there at the beginning… kept showing up… here for the finish"), and every piece of canon prose enumerates the tiers that way, so the old Companions-first order contradicted the card's own voice. Reading it now moves past → present → future, and it puts the row that can carry the owner's act at the card's bottom edge, beside the trail it writes into. The journey-people screen's filter chips follow: Everyone / Starters / Companions / Celebrators.

And a defect the wave closes. Every composer door on this screen used to push a composer with no journey on it, so the page derived its usual default — the open journey the author most recently stepped in — and an owner standing on journey B was handed a composer aimed at journey A. The journey now travels with the route, and the precedence is one line, top down: the author's own pick, the door's journey, a rescued draft's journey, the derived default. The tab bar's Step FAB and the profile's empty state carry no journey and keep the derive, which is what it was written for.

And a second one, found while verifying. The people card held its own one-shot fetch inside itself, so it was a part of the screen the page's pull-to-refresh could not reach: one failed read (a dropped connection is enough) and the card was simply absent until the page was left and re-entered. The people now live beside the detail and the trail on the journey wrapper, bound to the resolved journey the same way the trail is, and the gesture that refreshes the page refreshes all three of its reads. The standing rule behind it: a card that fetches its own data owes the page a way to ask again.

Mocks: journey.html (two devices: visitor/achieved, and owner/ongoing with the +, the description, and the Write-it pill), journey-manage.html (cluster in both backdrops, sheet without the Add-a-step row), people-card-board.html v7 (the act's specimens and rowspec, rows reordered), journey-people.html (chip order).

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:

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. (Revisited 2026-08-07 on that clause's own terms — see "The detail frames, the viewer holds the whole".)

The detail frames, the viewer holds the whole (2026-08-07)

(Amended 2026-08-08 — the page band and the card band collapsed into one. See "One window, authored once".) User feedback from the friends-and-family launch: step-page media dominates the screen vertically — a 9:16 shot makes the conversation a basement nobody reaches. PO proposed a 3:4 vertical bound for image and video plus a composer feed/detail preview toggle with drag-and-zoom framing. Adjudicated; the Page tier of the ladder is amended:

Framing is a place, not a gesture (2026-08-07)

(Overturned 2026-08-08 — the gesture came back to the preview once the arena question had a real answer. See "One window, authored once".) The framing wave first shipped the gesture inline: the composer's preview card was itself the framer, drag and pinch live under the thumb, with a Feed / Step page toggle beneath it. On device that is ambiguous in a way no copy fixes — the composer scrolls, so a drag starting on the media is claimed by two recognizers at once and the gesture arena decides invisibly. Either the page will not scroll where the thumb naturally lands, or the framing drag scrolls the page away. Nothing on screen says which mode you are in, because there is no mode. PO asked whether to lock the page's scroll over the media and dress the card to signal crop mode. Adjudicated:

One window, authored once (2026-08-08)

PO rethought the two-window model a day after it shipped: one shape, the tall cap at 4:5, the framing gesture back on the composer preview with Instagram's fit/fill button, and the feed note clamped to three lines to pay for the extra height. Adjudicated; this amends both rulings above.

A video's wait is a poster, and its failure is the reload field (2026-08-12)

Production reports of videos "loading slow" turned out to be the wait having no face: the player drew nothing at all until the stream opened, so a slow clip and a broken clip were the same blank box, and a broken one stayed blank forever. Ruled with codex review; built the same day.

Choose the journey sheet (create-step-journey.html — a spec board, not a screen; 2026-08-08)

The sheet the composer's ADDING TO row opens. The row was ruled on create-step.html ("its sheet holds the start-a-new-journey door") and the sheet itself was never drawn, so the app kept a pre-revamp screen: a centered title with a left X, a raw-tangerine New Journey label beside a dashed 62px tile, 76px thumbnails, and rows whose only fact was "6 steps". PO called it outdated on sight. Drawn now, with one finding that outranks the visual drift:

Governing line: a picker owes you two things, where you are and what changes if you move.

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):

The picker browses, and says whose photos these are (2026-08-09)

Three pieces of early-user feedback on the composer, one root each.

Amending "the picker is hybrid — native behind the All photos door" (2026-07-18). That ruling pointed the door at the native system picker and said never build a custom full-library browser, because canon chrome is not worth the full-access permission and its state matrix. Both halves of that reasoning have since been spent. The inline recents grid asks for the same photo permission on its own, so no native door can delete the ask; and the state matrix is built, shipped and hardened — loading, granted, limited, denied, permanently denied, restricted, with the Android partial-access grant that 1.0.4 fixed living inside it. Native would still be the cheaper way to get albums, and its Videos smart album would answer the type filter for free. It would also cost what the custom sheet gives: the current pick ring-marked while browsing, a picked asset landing back in the recents strip, and one AssetEntity from pick to publish instead of a second file-backed media path that has to re-derive a video's dimensions and duration. Ruling: the custom sheet stands, and now owes what a browser owes. The 2026-07-18 clause is retired, not reinterpreted — the door is ours.

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.

Companion is living membership (2026-07-24)

Supersedes the same-day "two doors" ruling (cumulative count minting is dead — do not build it). Adjudicated three rounds with codex; adopted as the PO's original vision, correctly understood at last.

Companion is not a medal for past work — it is living membership in a journey's inner circle: "I am caring, and still caring." While the road is open, Companion is membership; when the chapter closes, Stepo remembers who its Companions were.

One Companion, no ladder (2026-07-24)

The close door shipped as four graded distinctions (Dedicated Follower 50 · Early Believer 65 · Consistent Supporter 75 · Die-Hard Fan 85, each keyed to which anchors you caught). Retired. Four thresholds and two anchor flags existed to produce four names for facts the badge set already tells: Starter and Celebrator are permanent badges in their own right, so the ladder re-encoded the anchors inside the Companion's name and then charged a different percentage for each combination. It is the densest object in the system, it invented the phantom "impossible under 14 steps" invariant, and it is the one surface that can ask two people who both stayed why one of them got the lesser word.

The general principle, from the same session that cut the chapter-age arming gate: a rule added to stop a degenerate reading is usually evidence the model allows a state it shouldn't. Prefer deleting the state (or the name) over adding the prohibition.

Why the window is 24 hours (2026-07-24)

The app's load-bearing number, ruled with reasons (it is config — StepWithSettings.WindowHours — so this is a product ruling, not a technical constraint):

Admin mode — the app is the moderation console (2026-07-20)

Stepo's moderators are on the phone — today the founder, someday hired help. Splitting moderation across a web console nobody opens is maintenance without a user; the app — already the best renderer of the content being judged — is the console. The web admin dashboard is frozen (config + desktop fallback only; no further investment).

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, "Live · 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.

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.

Delete account moves to Edit profile — "Destructive lives where identity is edited." (2026-07-31)

Ruling: a destructive account action belongs on the screen where identity is edited, never one row below a routine session action. Delete account sat at the very end of Settings, directly under Sign out — two full-width rows of the same shape, the same tap target, adjacent. The only thing separating "I'm getting off this phone for the night" from "everything I made is gone" was 20px of paper and a colour the thumb never reads on the way down. Proximity like that is the defect; the confirm sheet behind it is a second line of defence, not a reason to keep the first one weak.

The fix is placement, not friction:

Why Edit profile is the right home: deletion is the last operation in the same sequence as changing your name, your handle and your face. Grouping it there reads as one idea ("this is who I am here, and this is how I stop being here") instead of as an item on a list of switches. It also keeps Apple 5.1.1(v) satisfied — the path is still in-app and still reachable in two taps from the profile.

Mocks: settings.html (row removed, Sign out last), edit-profile.html (danger zone added), settings-delete.html (the sheet now dims Edit profile, and dismiss returns there).

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.

Rate Stepo — two rows, no gate (2026-07-28)

Both stores approved, so Settings' Support group gets its second row back: Send feedback · Rate Stepo. Two plain rows, equal weight, always both present. Rate opens the store at its review page (?action=write-review on iOS, market:// on Android).

The proposal it replaced was a star rating inside the app that routed 4–5 stars to the store and 1–3 to the feedback form. Rejected on two grounds, either of which is sufficient:

Not the in-app review API, either. Google's guidelines rule out a call-to-action button that triggers the review flow, and Apple's requestReview() is quota-limited and may show nothing — a settings row that silently does nothing on tap is worse than no row. A row someone taps deliberately owes them the store. Implementation: stepo-mobile/lib/core/store/store_listing.dart.

Send feedback — "Tell the person who makes this what happened." (2026-07-30)

feedback.html, the Support row's destination, drawn as the plan for the Flutter page's revamp. A profile drill-in with a bottom CTA: back chevron to Settings, the short title Feedback, no tab bar, one full-width ink Send pinned at the foot. Chrome is ink, so the screen carries zero sunset color; its one accent is the caret in the focused note, the same interactivity-not-decoration exception edit-profile already holds.

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 status-bar icon wears the app's square (2026-08-09)

Android's notification small icon is a mask. The system throws away every colour in it, keeps the alpha, and paints the shape with its own tint: white on a dark status bar, ink on a light one, notification_color in the shade. Nothing in that file can carry brand colour, so the only thing the icon can vary is which parts are solid.

The shipped icon was drawn at half size and had drifted off the mark. Measured in a real status bar beside Google's G: ours came out 16×18px to the G's 31×32. Material gives system icons a 24dp canvas with the artwork filling a 20dp live area (~83% of the height); ours filled 51%, sitting 6px off-centre. Its four bars also tapered (0.104, 0.083, 0.073, 0.062 of the canvas) where the brand mark's four pills are all the same height. It was not the mark, and it was drawn at half the size of everything around it.

Ruled: the plate, with the pills knocked out of it. Three versions were built and looked at in the same real status bar. The bare glyph, re-cut to the 20dp live area, measures 28×32 — correct by the spec, exactly the G's height, and still too light to find: four thin pills with air between them lose to solid neighbours at a glance. The plate at 22dp reads instantly but comes out 36×36, looming over every other icon in the bar. The plate at 20dp is the one that ships: 32×32, the same footprint as the icons beside it, rx 7 so the square is round without becoming a pebble, pills punched through with about 4dp of plate all round. It carries the launcher icon's figure/ground into the status bar, which is what makes it recognisable as Stepo rather than as a generic mark.

The cost is real and accepted: a filled plate is louder than the silhouettes around it, and the mark inside it is a hole rather than a shape. Checked on a dark status bar as well as a light one — the pills stay legible in both, which was the risk worth checking.

Regenerate, never redraw. The five densities (24/36/48/72/96) come out of the one SVG, brand/stepo-icon-android-notification.svg; hand-editing a PNG is how the taper got in.

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. The one act that joins it there is the journey screen's + for the owner of an ongoing journey, because that screen is pushed full-screen with no tab bar and therefore no Step FAB ("The journey screen carries its own doors", 2026-08-12). 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.

The bar floats, so every page under it owes it room (ruled 2026-08-09, from an early user). The bar paints over the page rather than shortening it, which is what lets content pass behind it as you scroll. The cost is that a list ending at its own padding ends behind the bar with no scroll left to free it: on Support Received the last supporter was half-covered and unreachable. Any scroll view that can reach the bottom of the screen under the bar reserves the bar's full footprint below its last item. The number is read off the framework at build time, never written down, because Profile's drill-ins are one page mounted two ways — under the tabs for the owner, above the whole scaffold for a visitor — and a hard-coded bar height would punch a hole in the visitor's copy.

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-journeycreate-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 Live (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 / Live).

Notifications: revamp or keep? (2026-07-10 ruling) Keep the Activity screen's architecture — SeenAt/ReadAt zones, live creator strip, one-accent rows — it already encodes the system correctly; a visual revamp would be churn without a user outcome. (The 2026-07-31 wave kept that architecture and moved where Read is stamped: the visit reads, the tap travels.) 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, live 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). (The confirm sheet is overturned 2026-08-08 — see "Choose the journey sheet". Informed, yes; interrupted, no: the picker row and the composer row each state that posting reopens the journey, and nothing stands between the choice and Post. Badges permanent stands.) 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):

Accepted as a new ruling:

Held (re-litigated without new evidence — the standing rulings apply):

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):

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.

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).

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.

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:

Addendum — the tense grammar of an empty role row, and the mark of a row you are in (2026-07-30, PO + design lead; amended after codex review and Fable adjudication, and closed by founder rulings on 2026-07-30 and 2026-07-31) The card ships with all three role rows present for the life of every journey (the standing "role rows never come and go" ruling above). This addendum rules what a row says when nobody is behind it yet, the three slots that ride with a row, and the mark a row wears when the viewer belongs to it. It refines row COPY and row GROUND only; row presence is not in question and never becomes conditional. Board: people-card-board.html is the single spec, rendering every state side by side in the adopted treatment.

Two hard rules: 1. A zero never renders as a digit on this card. An empty row states its condition in words and the count slot is absent. "0" is a score, and this card keeps no score. The placeholder is banned with it (it was live on journey-milestones.html's Celebrators row and is now removed). The rule extends to the card's own header: a journey nobody has stepped with reads "No one has stepped with this journey yet." with the total in words, never "0 people". 2. A chevron exists only where a people page with members behind it exists. An empty row carries no chevron and does not tap, because the filtered page it would open has nothing to list. Count and chevron travel together. The row stays; the affordance is what comes and goes, and it returns with the first person.

The tile is the third tense signal, and it reads exactly one thing: is this row still possible (never the journey's broad status). Dashes = someone can still arrive · faces = people came · nothing drawn = the row is settled and nobody can ever arrive. So the dashed ghost tile goes on every still-possible empty row: the Companions invitation while the chapter is unsettled, the Celebrators row while a finale is live with nobody in it yet, and the Celebrators row waiting on a finale nobody has written, whose moment is ahead rather than gone. The founder ruled the widening on 2026-07-31, with the reason in one line: one rule for every possible row beats a special Companions case. The earlier "open right now versus waiting" distinction is retired with it, and where a row's window sits in time is said by the row's sentence, which is where tense belongs. A row that draws nothing keeps its slot's width, so every role label stays in one column and only the sentence gains room, from the absent count. Reserving a face-shaped hole for someone who cannot arrive is the same error as printing a zero; letting the label slide left is a different error, and both are avoided by drawing nothing into a slot that still holds its place.

Self-membership is the ground a row stands on (V1b · Highlight, ADOPTED by the founder 2026-07-31). In any row the viewer belongs to, the row takes a wash in that row's role color, the role title switches to that role's darkened title tone, the sentence stays ink, and the viewer's avatar leads that row's facepile (marigold Starter, tangerine Companion, rose Celebrator). The mark is the whole row, so it survives being shrunk, screenshotted and reposted, which was the requirement the round was judged against. The earlier ring ruling is superseded: the wash and the title carry the mark, and the ring is retired with it. One accent per card was raised against this and overruled on sight: that rule protects a card from competing accent claims, and three role washes make one claim in three shades of the same voice, these rows are yours. The founder judged the three-role card full screen before ruling. REJECTED — elevation for member rows (codex): the card is the lifted object, its rows are not, and "cards lift, chrome stays flat" governs inside the card too. REJECTED — accent spend per role (codex): it collapses precisely on the three-role case, which is the case worth designing for. Implementation is two CSS modifiers on the existing row (.mine plus the role class, or .sealed) and one on the viewer's face (.me), never a new element.

A row with nobody behind it takes the grey wash only once it is settled, and the tile and the ground carry one identical tense (founder, 2026-07-31). That alignment is the whole logic of the rule, and after the tile widening it is literal: dashes always ride with paper, and an empty tile always rides with grey. Grey = closed for good (the window ended, nobody came, nobody ever can): "The first step passed quietly.", "No one became a Companion of this one.", "The finish passed quietly.", "This journey closed without a finale." Dashes on white paper = still possible: the Companions invitation while the chapter is unsettled, the finale live with nobody in it yet, and the Celebrators row waiting on a finale nobody has written. Greying a door that is open would tell the viewer it is shut, and so would leaving its tile bare. The wash lays over the tense grammar rather than replacing any of it: copy, role dot, tile rule and the label column are unchanged.

AA, measured and tuned (2026-07-31). The -text twins are tuned for paper, and on a wash of their own hue they land under AA: at the ruled wash strengths the Companion title measured 4.16:1 and the Starter title 4.38:1. The washes are what the founder judged, so the washes are unchanged and each title took one step deeper instead. The shipping values, each wash written as its raw accent over the white card and stored solid so the contrast is exact:

GroundHexTitleTitle on groundSentence (--muted #666360)
Companion wash (tangerine 18%)#FFE4DC#B43D184.80:14.94:1
Starter wash (marigold 19%)#FCEBCF#8F5A004.94:15.09:1
Celebrator wash (rose 17%)#FCDFE9#B924594.88:14.79:1
Sealed-and-empty wash (ink 7%)#EFEFEFink #19181715.42:15.19:1
The card's paper#FFFFFFink #19181717.73:15.97:1

Every title and every sentence clears 4.5:1. A row with nobody behind it keeps its ink title on white and on grey alike: greying the copy was tried and dropped, because --faint on the grey wash measures 2.16:1 and the wash already carries the absence. A member row's count and chevron step from --faint to --muted, because a wash swallows the fainter tone; the count on an unwashed row keeps the card's standing --faint and that card-wide tone is a separate question.

Companion tense keys on chapter SETTLEMENT, not journey status. recognition_overview.md is the authority: Companion freezing settles when the finale's 24h window closes, so an Achievement whose finale is still live is unsettled and its Companion row stays present tense ("keep showing up"). Only a settled chapter turns the sentence past ("kept showing up"). Spec note, ruled here: on a manual Close, the same law applies by the same logic. Settlement seals when the last open recognition window ends, never at the moment of Close. A journey closed while a step is still live has an open window and unsettled membership until that window expires; sealing at the tap would freeze a membership someone could still be earning inside a window the product promised them.

The tense table. Each line branches further on singular against plural and on viewer among them against viewer not, so these are the shapes rather than the whole set of strings:

RowConditionReadsTile · count · chevron
Companionspeople, chapter unsettled"You, Kofi, Sofia and Ren keep showing up."faces · N · yes
Companionspeople, chapter settled"Kofi, Sofia and Ren kept showing up."faces · N · yes
Companionsnobody yet, chapter unsettled"Stepping with it while it's live is how this row fills."dashes · none · none
Companionsnobody ever, chapter settled"No one became a Companion of this one."none · none · none
Startersone person"Linh was there when everything began."faces · 1 · yes
Startersseveral, viewer among them"You and 4 others were there at the beginning."faces · N · yes
Startersseveral, viewer not among them"Linh and 4 others were there at the beginning."faces · N · yes
Startersnobody, permanent"The first step passed quietly."none · none · none
Celebratorsno finale written yet"The finale hasn't been written yet."dashes · none · none
CelebratorsClosed, no finale ever written"This journey closed without a finale."none · none · none
Celebratorsfinale live, nobody yet"The finale is live. Stepping with it now is how this row fills."dashes · none · none
Celebratorsfinale live, people arriving"You and 30 others are here for the finish."faces · N so far · yes
Celebratorsfinale window closed, people came"13 people were here for the finish."faces · N · yes
Celebratorsfinale window closed, nobody came"The finish passed quietly."none · none · none

Why each tense: Companions are living membership, so the row is present tense while the chapter is unsettled (membership can still be gained and can still fade), future-tense invitation while it is unsettled and empty, and past tense once settlement freezes it. The invitation names the mechanic: stepping with a live step is what fills the row, and viewing fills nothing, so the verb is never "being here". Starters is the only permanently-past row: the first step's 24 hours close once and cannot reopen, so "The first step passed quietly." is written the day that window shuts and never changes again, however full the journey gets behind it. Celebrators waits on a finale that may never be written; while the finale is live the count carries "so far" and the sentence is present tense, and both turn the moment the window ends. The last Celebrator line is the Starters line's mirror, ruled by symmetry so no cell of the table is empty: a closed window with nobody in it is a fact about a day, not a failure.

REJECTED — "This journey was walked alone." (codex caught it, the board proved it): the line is false wherever the journey has supporters, and the board's own settled specimen carries 13 Celebrators. A card that says the journey was walked alone directly above a row of 13 faces calls its own author a liar. The replacement mourns only the missing relationship: "No one became a Companion of this one." Marked a polish candidate; the ruling it must keep is that the sentence names the absent role and never characterizes the journey.

Loudness: how the treatment was picked (founder, 2026-07-31). The requirement changed from correctness to legibility: self-membership must read at screenshot scale, because the shareable artifact is a supporter's screenshot saying "I am in this journey's ledger". The format of that judgment is itself a ruling: the card is judged full screen, in the company it keeps. Four complete journey pages sat side by side in the frame, identical except for the treatment, each carrying the state chip, title, owner, description, meta, the people card and the finale card below it, on the hardest data there is (the viewer holds all three roles, so any per-role treatment has to spend three). V1b · Highlight won, and its ruling is written above. The other three, in one line each:

Standing constraints all four honored: no raw accent ever carries small text, rows never lift, role colors stay in their canon roles. REJECTED earlier in the same round — the place band (a promoted lead sentence on an accent band with a 34px avatar): the founder ruled it costs too much vertical space on a page that already carries the hero and the step cards, and the people card cannot buy its legibility with height it does not own. The four-page board that carried the comparison did its job and was deleted with the round; people-card-board.html is now the single spec for the card.

The page the card opens, and its own grammar (journey-people.html, founder 2026-07-31). Three of this round's rulings land on the destination page rather than on the card, and they are recorded here because they are the same ruling reaching further.

A role filter chip renders only where people stand behind it. This is the mirror of "a chevron promises a page": a filter is an affordance, so it comes and goes with its people, and a zero never renders in a chip any more than it renders on the card. The asymmetry with the card is the whole point rather than an inconsistency. The card's three role rows persist for the life of every journey because they are the ledger, and a ledger says "nobody" out loud; a chip is a way in, and there is no way in to an empty room. Everyone is the page's default view rather than a role filter, so a journey nobody has stepped with has nothing to filter and the strip is absent whole. All three roles are inhabited on Marco's journey, so the flagship mock renders all three chips and the visual change there is nil; the rule is stated in the page's header comment beside the strip.

The viewer's place is one element, and that element is a person row. The standalone grey place block in the header is retired (superseding the 2026-07-13 header inventory above): the pinned-first "You" row IS the viewer's place, and its relationship sentence absorbs anything the block said that the row did not already say. On Marco's journey that is the present tense of living membership: "You were there when this journey began. You keep showing up, 9 steps so far, and you're here for the finish." One relationship in two elements made the viewer read their own place twice, and the row is the copy that also carries the face, the earned chips and the way to the profile.

V1b propagates to the You row with a context-aware wash. Under a role filter the row takes that role's wash exactly, per V1b. Under Everyone no single role can be keyed on, and the three-role viewer is precisely the case that breaks any per-role pick, so context supplies the hue: the journey's own moment. Tangerine while a recognition window is live, rose once the finale has settled it, at the V1b wash strengths and tones from the measured table. The mock renders Everyone with the finale live, so the You row washes tangerine. Closure is invisible on this page (founder ruling, 2026-07-31). The page never reads the journey's status label: a closed journey renders exactly as an open one with the same window facts, so with no settled finale the hue stays tangerine, the same as open. A Companion of a closed journey is a Companion, the counts stand, and the only thing closure withholds is the owner's own path to an achievement. The Celebrators door follows the people, not the state: no people behind it, no chip, closed or not. There is no third neutral hue; two moments, two hues, and everything else is the open look. The role chips on the row are unchanged and keep carrying the multi-role truth, which is why the wash does not have to. The name stays ink: a person row's heading is a name, not a role name, so the card's title-tone switch has nothing to switch here, and every sentence stays ink as on the card. The marigold ring that used to sit on the viewer's avatar is retired with the card's ring, for the same reason the card's was: the whole row carries the mark. One measurement, ruled while propagating: a chip's soft tint is translucent, so a chip left sitting on a wash of its own hue thins toward the ground and takes its -text twin under AA with it (the Companion chip measures 4.49:1 on the card's white and 3.91:1 on the tangerine wash). On a washed row each chip composites its own tint over the card's white, so a chip renders identically wherever it appears. Same precedent as the title re-toning: the wash is what was judged, so the wash stands and the text keeps the contrast it was tuned for.

The order of the list, and the silence around it (founder, 2026-07-31). A list has an order whether or not anyone designs one, so the order is ruled here rather than left to whatever the query returns. It differs by view, because each view is asking a different question.

ViewOrder
Everyonethe viewer's own row first when present, then by how many of this journey's roles a person holds (three, then two, then one), ties broken by step-with count on this journey, descending
Companionsthe same: viewer first when present, then roles held, then step-with count
Startersby the time of the qualifying act, earliest first
Celebratorsby the time of the qualifying act, earliest first

Why the split: Everyone and Companions are asking who this journey's people are, and the honest answer leads with the people who are most of it, which is what holding three roles means. Starters and Celebrators are each one moment, so the only thing left to sort by inside them is when a person arrived in that moment, and the earliest arrival is the one the moment belongs to. Roles held is a count of kinds and never of acts, so a person with one heart at the first step and one at the finale outranks a person with forty step-withs and one role: this page measures the shape of a relationship, not its volume. The viewer is pinned rather than sorted, because a person looking for themselves in a ledger should not have to hunt, and Starters and Celebrators do not pin them because a chronology with a hole punched in it is no longer a chronology.

The order is never labeled (the standing visitor-doors ruling, reaching here): no ordinals, no "top" or "most" language, no rank chips, no rule anywhere on the page that tells the viewer what the sequence means. The sort decides who reads first, and the interface stays silent about it. This is what keeps the page a list of people rather than a leaderboard, and it is the same line the podium sits behind: "Linh supported Maya" is ordinary social context; "Linh supports Maya more than everyone else" turns affection into competition. The sort is a reading order, and a reading order the interface never narrates cannot be read as a score.

The facepiles follow. A card facepile shows the first faces of the page it opens, with the viewer's face leading in any row the viewer belongs to (V1b), so tapping through preserves identity face-for-face. On Marco's journey that means the Companions facepile reads You, Kofi, Sofia (Kofi holds all three roles, Sofia two); the Starters and Celebrators facepiles read the earliest arrivals, which is why the Starters faces match the first step's memory line and the Celebrators faces match the finale's.

Copy voice throughout: interface voice (Inter, never the authored serif), show don't claim, no em-dashes, and the mechanic's verb for step-with.

Canon reconciled to the three-role viewer (2026-07-31). The card is specified on the true three-role case, so the viewer is a Companion on Marco's guitar journey everywhere: journey.html carries the three-tense lead ("You were there at the beginning, you keep showing up, and you're here for the finish."), a Companion count of 4, a Celebrators row reading "You and 30 others are here for the finish.", all three rows washed, and a finale card that states the earned fact instead of inviting an act already taken. journey-people.html lists You under all three filters with an active Companion chip and moves its Companions filter to 4, and its pinned You row carries the viewer's place on the journey's-moment wash. badges.html gives Marco's row an active Companion chip, moving the arithmetic to Starters 3 · Companions 6 · Celebrators 5, total 14, and profile.html's Badges Earned door follows to 14. The first step's memory line reads "Sofia and 4 others" (ruled 2026-07-31): everyone who steps with a first step is a Starter, so the line and the Starters count of 5 must be the same five people; the count held on three surfaces and the memory line took the one-word fix.

The reveal fires at the act, wears your face, and says why (2026-08-11) The badge reveal wave, adjudicated with the founder against badge-reveal-board.html (the three tiers side by side) and built from StepoBackend/docs/stepo/badge_reveal_wave_2026_08.md. Eight rulings; badge-reveal.html renders them.

Anchors fire in the moment they are earned. Starter and Celebrator arrive in session, right after the step-with that earns them: the cheer animation lands first and the reveal follows a beat later (~900 ms), so the reveal reads as the consequence of the tap. This restores the 2026-07-11 entry rule ("at the moment recognition lands") — the shipped app read pending reveals only at app open, which turned every anchor into a next-day surprise with nothing left to connect it to. The one-full-reveal-per-session rule stands: an earned-now reveal takes the session's slot, and further earns queue for later sessions, oldest first.

Companion arrives on the same beat, through the queue rather than the response. A Companion gain is always attached to a specific step-with, and its evaluation commits fast, so the reveal check at the cheer-beat surfaces a just-earned Companion in the same moment under the standing combined-arrival rule (Companion primary, the anchor line second). The evaluation itself stays in the background job: membership is computed in one place, and the app's hottest write does not grow a second one. When the job is slower than the beat, a tier-withheld supporter push carries it — "A badge just arrived on 'Learn to play guitar'." — and the reveal waits for the next open. Naming the journey while withholding the tier is what narrows the 2026-07-11 "recognition is never pushed" rule rather than breaking it: the lock screen points at the discovery and spends none of it. FirstGain only, once per person-journey, silent on fade and on regain; the owner's quiet Activity row is unchanged.

The badge is you. The emblem is the person's own avatar inside the tier ring, with the tier glyph as a crest on the ring's edge: first-step arrow for Starter, footprints for Companion (a new glyph, the walking metaphor the copy already owns — drawn as filled pads, because stroked ellipses read as balloons at the crest's 17px and filled forms are what survive small sizes), finale trophy for Celebrator. The crest artwork is one canon SVG per tier, shared by every surface that depicts the tier at any size; the app imports the SVGs as assets rather than approximating them with platform icons. The screen used to show a generic pill; the person it celebrates is now the thing it shows, and saying so costs no number.

No crowd on the reveal. No facepile, no "you and N others". A facepile is a count wearing faces, this screen refuses counts, and at the scale the app actually has, "you and 2 others" reads as loneliness rather than company. The crowd lives one tap away on the journey People page with the viewer tagged "You", which is where the CTA already goes.

Provenance answers "why did I get this", and the whisper is gone. Under the memory line sits one sentence naming the act, the person and the window: "You stepped with Marco's final step while the finish was live." / "You stepped with Khanh Lương's first step while it was live." / "Again and again, you stepped with Maya while her steps were live." It speaks the feed's own verb, since the feed already teaches that a heart, comment or share on a live step is stepping with it. The permanence whisper ("Yours forever. It lives with your badges.") is deleted with nothing in its place: it promised forever on a screen that also serves living Companion, which fades. Arrival only.

The memento renders the authored window. The step media band [4:5 … 1.91:1] governs the reveal as it governs everywhere else: aspect verbatim, never re-cropped. A tall window scales to the height cap and centers, a wide window rides the full content width, and the reveal payload carries the cover's frame so the client can draw the shape the author chose.

Timing is earn-time, not window-expiry. Nothing waits for the 24h window to close: the window is the rule for earning, not a delay on the gift. Withdrawing the act inside the window takes the badge with it, a cost the withdrawal sheet already names.

Give again, celebrate once. The record is the final state of the window, so a withdraw-and-redo ends with the badge held, and a faded Companion who returns is a Companion again. The celebration is once per person-journey, structurally: reveal acknowledgments outlive the interaction that earned them, and first-gain time makes every later return a regain, which fades quietly, comes back with a whisper, and fires neither push nor full reveal. Three standing guards keep the loop unfarmable — persistent acknowledgments, the Gain/Hold hysteresis pair (a new step dents nobody's coverage until its own window closes unstepped), and silent regains. Firing at the act runs those paths far more often, so this wave pins them rather than touching them.

V1 — ships

V2 — visible in mocks, deliberately deferred

ItemWhereWhy deferred
Windows-open avatar railFeed topRedundant with Live cards until users follow enough people that live cards outnumber the viewport; earns its place at scale
"While you were away" digestFeedNeeds session-gap detection + volume to matter
Weekly pulse strip ("Maya, Ray and 2 others drove 21 step-withs this week")SupportersNeeds aggregation infra; nice-to-have on top of the podium
Media grid view toggleJourneyValuable for long journeys; trail is enough at launch sizes
Discover feed tabFeedFollows the 70/30 spec but needs content volume
Chat / DMsFeed's top-right (empty in V1)Whole feature is V2; the slot is reserved and additive — mocked in chat + chat-thread
Step-content searchSearchSpec supports it; people + journeys carry V1, step results need density thinking
Batched notification rows (face-stack)ActivityNotificationBatch schema designed in the backend README; needs interaction volume
Owner window-recap ("12 stepped with you")Activity, in-app firstGood retention moment; not worth lock-screen space until volume proves it
Finale last-call pushLock screenOnce, closing hours, companion tiers who haven't stepped — needs recognition tiers live first
Lapsed-week digest pushLock screen7+ days away, own-graph events only, self-silencing after two ignored sends
Activity filter tabs (All / People / Replies)ActivityIG-style chrome; earns its place only when mixed-type volume hurts scanning
Expanded journey-picker sheet, draftsCreate stepCollapsed 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 (14/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 ("Live · 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 live" (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 (14/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: ORDER, BLURBS, and the group it belongs to in APP_GROUPS), 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 — Live / 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:

For the Flutter port

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 live), 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.