> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# UI/UX review checklist

# UI/UX review checklist

The manual review that the UI/UX designer, QA and the frontend engineers walk at the end of every
phase, with Ghostframe screenshots, before the tech lead tags `phase-<n>`. It complements the
automated suite (`task e2e`, [e2e.md](e2e.md)); items marked **(auto)** are also asserted there, and
the review only confirms them on screenshots.

Specs this checks against: `CLAUDE.md` ("Layout consistency", Motion, TS/React),
[PLAN §4](../PLAN.md) (click budgets, §4.1 motion, §4.2 decisions), [design/README.md](../design/index.md)
principles 1–8, [design/interactions.md](../design/interactions.md), and the screen specs in
[design/screens/](../design/screens/).

How to record results: one row per failed item in `docs/design/review-phase-<n>.md` (the format of
[review-phase-1.md](../design/review-phase-1.md)): id, severity, screen, problem (spec), fix, owner.
Attach the screenshot path.

## Setup

1. Start the app against the fake venue: `task dev:fake` (backend 127.0.0.1:8080, Vite
   [http://localhost:5173](http://localhost:5173)). Log in with your dev password. For the load items (§15) restart the
   backend with `SESAME_FAKE_VENUE_STRESS=1` (worst-case names, extreme prices and balances, 64 orders, 200 fills, a stale book).
2. Screenshots go under `C:\Users\Administrator\Desktop\sesame-screens\<phase>\` (for example
   `phase-3`), named `<screen>-<state>-<width>-<theme>.png`.

### Ghostframe recipe

Every "Ghostframe:" step below uses these calls. Always start from a fresh isolated context, so
no cookie, cached theme or dock layout from an earlier review leaks in.

| Step | Call |
| - | - |
| Open | `mcp__ghostframe__new_page` with `url: "http://localhost:5173/login"` and a new `isolatedContext` name per pass (e.g. `review-p3-1440-dark`) |
| Desktop size | `mcp__ghostframe__resize_page` `{width: 1440, height: 900}` (or the width under test) |
| Phone | `mcp__ghostframe__emulate` `{viewport: "390x844x3,mobile,touch"}`; landscape: `"844x390x3,mobile,touch,landscape"` |
| Theme | `mcp__ghostframe__emulate` `{colorScheme: "dark"}` or `"light"`, with Theme set to System in the app menu; or pick Light/Dark in the menu |
| Offline, slow | `mcp__ghostframe__emulate` `{networkConditions: "Offline"}` / `"Slow 3G"`; clear by omitting it |
| Block the backend | `mcp__ghostframe__set_blocked_urls` `{patterns: ["*/api/*", "*/ws*"]}`; clear with `[]` |
| CPU load | `mcp__ghostframe__emulate` `{cpuThrottlingRate: 4}` |
| Time zone | `mcp__ghostframe__emulate` `{timezone: "America/Los_Angeles"}` (and `Asia/Tokyo`) |
| Measure | `mcp__ghostframe__evaluate_script` with a function that returns `getBoundingClientRect()` heights, `scrollWidth`/`clientWidth`, or computed colours |
| Structure | `mcp__ghostframe__take_snapshot` for roles, names and focus order |
| Capture | `mcp__ghostframe__take_screenshot` with `filePath` under the phase folder (`fullPage: true` for long pages) |
| Look | Read the saved PNG back (the Read tool) and judge it; a screenshot that was not looked at does not count |

Useful measuring script (paste as the `function` of `evaluate_script`; pass the row selector):

```js theme={null}
() => {
  const row = document.querySelector("header"); // the row under review
  const els = [...row.querySelectorAll("button, a[href], input, [role=status]")];
  return {
    heights: els.map((e) => [e.getAttribute("aria-label") ?? e.textContent.trim(), Math.round(e.getBoundingClientRect().height)]),
    overflow: [row.scrollWidth, row.clientWidth],
    pageOverflow: [document.documentElement.scrollWidth, innerWidth],
  };
}
```

## 1. Layout consistency

| Check | How | Pass |
| - | - | - |
| One control height per row **(auto)** | Ghostframe at 1440 and 390: run the measuring script on the top bar, mobile header, board toolbar, portfolio tab rows, orders toolbar, ladder header, dock tab strip | Every control in a row is `--control-h` (32 px) on desktop or `--touch-min` (44 px) on touch. No 24/28/34/36 px mixes. Text that is not a control is visibly smaller inline text |
| Vertical centring | Screenshot each bar at 200% crop | Equal space above and below each control; bar inner padding equals the gap between items |
| Spacing scale | `evaluate_script` returning computed `padding`/`gap` of bars and rows | Only 4, 8, 12, 16, 24 px |
| One container level per section | Look at every panel: count borders and fills from the page surface inward | At most one border or one fill per section; no box in a box in a box |
| Divider or spacing instead of a box | For each bordered or filled container ask: "Could this box be a divider or spacing instead? Is any section boxed only to separate it?" | Sections are separated by plain spacing or a single 1 px low-contrast divider. Cards, borders and fills only for separate, actionable things: dialogs, sheets, popovers, toasts, a mobile position card |
| Tab strips and segmented controls | Screenshot Portfolio, Orders, the board window switch, chart ranges | Fill the bar's height rhythm exactly; never a filled group inside a bordered strip inside a panel |
| Nothing spills out **(auto)** | Measuring script: `overflow` and `pageOverflow` at every width in §10 | `scrollWidth <= clientWidth` for bars and rows; the page never scrolls sideways |
| Nothing squeezed **(auto, stress)** | `evaluate_script` over flex rows: width of each dot, icon, badge, chip and number | No visible element narrower than 1 px or its min-width; icons and dots stay square or round (a live dot squeezed to 0 px fails) |
| Essential values never truncate **(auto, stress)** | Stress mode at 1440 and 390: scores, clocks, prices, sizes, P\&L | `scrollWidth <= clientWidth` for every one of them (a tennis score shown as `7…` fails); they reserve width or move to their own line |
| Truncation is earned **(auto, stress)** | Any text ending in `…` | It has a tooltip with the full value, and its row has under about 24 px free (text truncated while the row has 600 px spare fails) |
| One gap per group **(auto, stress)** | Phone Sell/Buy pairs, stake presets, steppers | Horizontal and vertical gaps equal within 1 px, from the spacing scale (4 px across and 0 px down fails) |
| Optically centred **(auto, stress)** | Price buttons with reduced motion on and off | The value's text centre is the button centre within 1 px; markers and units that come and go are absolutely placed and take no width |
| Scrolling chip rows show that they scroll | Board league chips and phone scope chips at 390 with more chips than fit | A fade edge or a visibly cut chip at the end signals more; the scrollbar is hidden |

## 2. Content-driven sizing

| Check | How | Pass |
| - | - | - |
| Long venue text | Palette search for the longest event title; open it at 390 and 1440 | Titles truncate with an ellipsis (tooltip or full title on the detail page) or wrap on their own line; nothing overlaps a price column |
| Extreme numbers | Fake account positions: 12,000 sh; a $1,234,567.89 balance and a −$99,999.99 P\&L (stress mode) | Columns keep their width; digits are tabular; no wrapped number ("12,000.00 sh" on two lines fails) **(auto for wrapping)** |
| Live values | Watch the board and a ladder for 30 s | No column or row changes width when a price goes from 9¢ to 10¢ or a size from 999 to 1.2k |
| Charts | Event view, every chart range, and an empty range | The chart keeps its height; "No trades in this range" sits inside the plot; attribution does not overlap the plot |
| Loading, empty and stale states keep the loaded geometry | Throttle to Slow 3G, reload each screen; block `*/ws*` to see stale | Skeletons have the real row geometry (no shimmer); empty and stale states do not collapse or grow the panel |

## 3. Trading safety

| Check | How | Pass |
| - | - | - |
| Misclick spacing | Measure gaps between Buy and Sell, ladder bid and ask cells, Exit and Add, Cancel All and the SAFE/ARMED switch | Adjacent opposite actions are visually separated (position, colour and label) and at least 4 px apart; touch targets 44 px |
| Side, size and price visible before a click | Hover a price (desktop) or look at the stake bar (phone) | The cursor tag or stake bar shows side, stake and estimated size (`$25 → 59.52 sh`) and the exact price that the click sends |
| Clear feedback after a click | Click a price in ARMED (fake venue) | Within one frame a `sending` chip appears at that level; then placed/filled/rejected, with a toast that names side, outcome and price |
| SAFE is obvious | Toggle SAFE/ARMED on each screen | The switch state is visible on every screen; trade clicks in SAFE do nothing and ring the switch |
| Cancel All always reachable | Every screen and every open sheet at 390 | The header Cancel All is never covered by a sheet or the palette |
| Price never moves under the cursor | Hover a ladder and the board while the book moves | The ladder does not re-centre; rows do not insert, remove or reorder under the pointer (deferred `N updates` pill instead) |

## 4. One number everywhere

| Check | How | Pass |
| - | - | - |
| Same value, same text | Pick one position; read size, average, P\&L, liquidation value on board, dock, portfolio, ladder header and palette Held badge | Identical rounding, units and sign on every screen |
| Rounding | Prices at the market tick (`42¢`, `42.5¢` on a 0.001 tick); averages rounded for display | Never `28.8571¢`; averages to the tick or one decimal **(auto for more than one decimal)** |
| Signs | All P\&L | Always signed with U+2212 minus: `+$12.40`, `−$3.10`; colour is never the only cue |
| Units | Money `$1,234.56`, compact `$1.2k` in dense cells; shares `sh`; prices `¢` | One unit style per concept across screens |

## 5. Trust

| Check | How | Pass |
| - | - | - |
| Stale looks stale | Ghostframe `set_blocked_urls` `["*/ws*"]`, or stop the backend | Prices stay visible but grey, stop flashing, cannot be clicked, and show their age (`Stale 6s`) **(auto: stale.spec)** |
| No value from before a reconnect | Stop the backend, change nothing, start it; watch the first frames after reconnect | No stale value is shown as live; prices return live only after the new snapshot |
| Age of account data | Portfolio and top bar while the user feed is down | Account figures are marked stale; order updates paused note shows |

## 6. Every state designed

Capture each state at 1440 and 390 (§ Screenshot pass).

| State | How to produce it | Pass |
| - | - | - |
| Backend down | Stop the backend | Status `Disconnected`, whole app stale, trade clicks disabled; no raw `Failed to fetch` |
| Reconnecting | Stop the backend, screenshot within 10 s | Status `Reconnecting` (or the higher-priority state), spinner |
| Venue 429 / maintenance / cancel-only | Stress mode or a 429/503 from the fake venue | Factual toasts (`Not placed · rate limited (429), retry after 2s`), status pill `Cancel-only` |
| No credentials | Run without venue credentials (dev host setup) | Orders and cash panels say which credential is missing; nothing looks broken |
| Geoblocked | Dev host (US) | Status `Geoblocked · US-VA`; switch shows OFF with the reason |
| Empty | New data dir: empty watchlist, no positions, no orders | Designed empty states with one line and at most one action |
| First run | Fresh isolated context, first login | Login focused, theme paints without a flash, board loads with skeletons only on the cold load |

## 7. Complete interaction states

| Check | How | Pass |
| - | - | - |
| Hover | Hover every control type | A visible change on fine pointers only |
| Focus | Tab through each screen | A 2 px `--ring` outline, 1 px offset, on every focusable element |
| Pressed | Press and hold | Pressed state within a frame; nothing waits for an animation |
| Disabled with a reason | Hover or focus a disabled control | Tooltip or accessible name says why (`Trading comes in Phase 3`, `No open orders`) |
| Pending | Slow 3G, then save a setting or pin | Pending state (spinner or `sending` chip) until the server answers |
| Error | Block `*/api/*`, then act | Factual error in place or as a toast; the control returns to a usable state |

## 8. Keyboard and focus

| Check | How | Pass |
| - | - | - |
| Logical order | `take_snapshot`, then Tab through | Order follows the visual order: top bar, rail, page, dock |
| Dialogs and sheets trap and return | Open the palette, menu, hotkey sheet, ladder sheet; Tab around; close | Focus stays inside while open and returns to the opener on close |
| Esc | Esc in each overlay, during a drag, with a selection | Closes the top overlay, else cancels the drag, else clears the selection |
| Hotkeys never fire while typing | Type `g p`, `?`, `` ` ``, `b` into the palette and any input | Text only; no navigation or trade **(auto for `g p`)**. Only Cancel All (`Mod+Shift+X`) works in inputs |
| Navigation hotkeys in SAFE | `Mod+K`, `/`, `?`, `g l/w/p/o/a/s`, `F6`, `` ` `` | Each works in SAFE |

## 9. Scrolling

| Check | How | Pass |
| - | - | - |
| No nested-scroll traps | Wheel and swipe over the board, a docked ladder, portfolio, dock | One scroll container per region; the page itself does not scroll behind a panel |
| Sticky headers | Scroll the board and portfolio | League and group headers stick below the panel header |
| Body lock under sheets | Open a phone sheet, swipe inside and outside | The page under the sheet does not move |
| Preserved position | Scroll the board, open an event, go back | Scroll position restored per route |

## 10. Viewports

At 768, 1024, 1280, 1440, 1920 and 2560 wide (height 900), and at 390 × 844 portrait and
844 × 390 landscape (Ghostframe `emulate` with `,mobile,touch[,landscape]`):

| Check | Pass |
| - | - |
| Top bar and headers fit **(auto at 1024–2560 and 390)** | No control past the viewport; no wrapped labels; page never scrolls sideways |
| Dock slots | 4 ladder slots from 1280 up, fewer below; opening a slot never shifts a price column left of it |
| Breakpoint edges | 767 and 768 both render one layout cleanly |
| Landscape phone | Header, tab bar and a sheet leave usable content height |

## 11. 200% zoom and large system fonts

| Check | How | Pass |
| - | - | - |
| 200% zoom | Desktop at 1440, browser zoom 200% (or `resize_page` to 720 × 450 at DPR 2) | Everything reachable; no clipped text in controls; reflow without sideways scroll |
| Large fonts | Phone with `evaluate_script` setting `document.documentElement.style.fontSize = "20px"` | Labels wrap or truncate cleanly; touch targets stay 44 px |

## 12. Phone specifics

| Check | How | Pass |
| - | - | - |
| Safe areas | iPhone-sized emulation; on a device, notch and home indicator | Header and tab bar pad by `env(safe-area-inset-*)`; nothing under the notch or home bar |
| On-screen keyboard | Open the palette and type (real device or iOS simulator) | Results stay above the keyboard; the input is not covered |
| Long-press vs scroll | Long-press an order row, then scroll starting on a price | Long-press (500 ms) cancels or opens the action sheet; a moving finger scrolls and never trades |
| iOS Safari viewport | Real iPhone: address bar collapsing, `100dvh` | No jump of the tab bar; sheets fit the visible viewport |

## 13. Cross-browser

`task e2e` covers Chromium, Firefox and WebKit at 1440 and Chromium at 390. Mobile WebKit is
configured but crashes on the Windows host ([e2e.md](e2e.md#known-gaps)); check 390 in Safari on a
Mac or iPhone by hand each phase: login, board, a ladder sheet, portfolio.

## 14. Accessibility

| Check | How | Pass |
| - | - | - |
| Contrast in every state and theme **(auto: axe)** | `node docs/design/contrast.mjs`; `evaluate_script` computed colours on stale, disabled, flash, warning chips in both themes | 4.5:1 for text, 3:1 for large text and UI parts, including during the price flash |
| Names | `take_snapshot` | Every button and link has a name that says what it does; icon buttons included |
| Live regions | Screen reader or snapshot while prices tick | Toasts and status changes announce; ticking prices do not |
| Target size | Phone snapshot | 44 px targets (24 px minimum per WCAG 2.2) |

## 15. Performance under live load

| Check | How | Pass |
| - | - | - |
| 50+ live games | `SESAME_FAKE_VENUE_STRESS=1`, board and dock with 4 ladders; `emulate` `cpuThrottlingRate: 4` | Scrolling stays smooth; clicks respond within a frame; no dropped input |
| Long sessions | Leave the board open for an hour | No memory growth trend, no slowed ticks |
| Background tab | Hide the tab 5 minutes, return | Data resyncs at once; no burst of stale animations or toasts |

## 16. Consistency

| Check | Pass |
| - | - |
| Terminology | One word per concept on every screen (Exit, Cancel all, Watchlist, Pin) |
| Icons | One icon per action; outline pin = not pinned, filled = pinned |
| Shared formatters | Every price, size and money value comes from `lib/format` (review the diff for ad-hoc `toFixed`) |

## 17. Time

| Check | How | Pass |
| - | - | - |
| Local display, UTC limits | `emulate` `timezone` to Los Angeles and Tokyo | Start times show in local time; day boundaries (Today, Fills today) are stated and consistent |
| Relative times tick | Watch `Stale 6s`, `1h` ages, countdowns for a minute | They update at least every second (ages) or minute (times); no frozen or negative values |

## 18. Visual simplicity

| Check | How | Pass |
| - | - | - |
| Colour only for meaning | Screenshot every screen in both themes | Green/red for bid/ask, buy/sell, profit/loss, plus one accent; everything else greyscale |
| Hierarchy by weight and muted colour | Compare type across a screen | About 3 sizes and 2 weights; secondary text muted rather than smaller and smaller |
| Two surfaces | Count background colours | At most two surface colours; shadows only on overlays (dialogs, popovers, sheets, toasts) |
| Tables | Portfolio, orders, dock, board | No zebra stripes or vertical lines; numbers right-aligned, text left; muted headers |
| Units in headers, no trailing zeros | Read the size and price columns | `Size (sh)` in the header, `150` in the cell; no `150.00 sh` **(auto)** |
| Blank for nothing | Rows without a position, orders or P\&L | Blank cell, not `0`, `×0` or `—` **(auto on the portfolio table)** |
| No Buy/Sell text under a labelled column | Desktop board and watchlist | Buttons under Buy/Sell headers show only the price **(auto)** |
| Compact numbers | Dense cells | Compact (`$1.2k`, `12.5k`) with full precision in a tooltip |
| Quiet status | Status pill and feed indicators when all is well | A muted dot; colour and words only when something changed or blocks |
| Row actions on hover and focus | Desktop rows; the same rows on touch | Secondary actions and details appear on hover or focus on desktop and stay visible on touch |
| One primary per area | Each panel and dialog | At most one filled primary button per area |

## 19. Icons and text

| Check | How | Pass |
| - | - | - |
| No emoji or pictographs **(auto)** | `evaluate_script` returning `document.body.innerText` minus `<kbd>`, tested against `/\p{Extended_Pictographic}/u` and the arrow, geometric-shape and dingbat ranges; repeat under reduced motion (`emulate` has no switch, so use the OS setting or Playwright's `reducedMotion`) | None in UI, toasts, notifications, Telegram text or docs. Triangles, ticks, crosses, stopwatch, bell and pin are lucide icons. `· – − → ¢` and `↵ ⇧ ⌥` inside `<kbd>` are text |
| Icon only for known repeated actions | Hover each icon-only control on desktop; `take_snapshot` for names | Only close, cancel an order (X), search, settings, chevrons, steppers, pin, alert, overflow, external link, theme; each has an `aria-label` **(auto)** and a desktop tooltip |
| Text only for money and state actions | Look at Buy, Sell, Exit, Join, Add, Arm/Safe, Save, confirmations, tabs, filters, headers, status words | Text, no icon |
| Icon and text only for navigation and Cancel all | Rail, bottom nav, Cancel all | Icon plus label; menus stay text unless every item has an icon |
| No decorative icons; one icon size | Headings, labels, numbers | No icon beside them; 16 px desktop, 20 px touch, `currentColor`, muted unless meaningful |

## 20. Copy

| Check | How | Pass |
| - | - | - |
| Sentence case **(auto)** | Read every title, button, tab, menu, header and badge | `Open orders`, `Cancel all`, `Armed`; no all-caps words (abbreviations such as NBA, USD, GTC, P\&L stay), no `text-transform: uppercase`, no letter-spaced upper-case labels |
| Glossary | Compare labels, toasts and docs with [docs/design/glossary.md](../design/glossary.md) | One term per concept: Cancel (not Remove), Exit (not Close), Stake, Armed/Safe, `sh`; no synonyms |
| British spelling | Read the screens and toasts | colour, unrealised, cancelled, favourite |
| Plain words | Read toasts and empty states | No "instantly", "simply", "just", "seamless"; no "Oops", "Sorry", "Please", "Click here"; no exclamation marks **(auto)** |
| Errors | Trigger rejects (13 sh, SAFE, band) and failures | Brief, factual (`Order rejected: price outside the band`); no blame, no advice |
| Punctuation | Labels, tooltips, toasts | No trailing full stop on single sentences; `…` only on actions that open a dialog; `−` for negatives, `–` for ranges, `·` as separator, curly quotes |
| Times | Order and fill ages, game starts | Relative under 24 h (`2m ago`, `in 12m`), then `30 Sep 14:05` (24-hour), exact local time in a tooltip |

## 21. Numbers and states

| Check | How | Pass |
| - | - | - |
| One format per quantity | Compare prices, money, P\&L and sizes across screens | From `lib/format` only; `$` before, `¢` after; separators; P\&L signed `+`/`−`; tabular digits; truncation ends in `…` with the full text in a tooltip |
| No broken values **(auto)** | Every route, both themes, 1440 and 390 | Never `undefined`, `NaN`, `null`, `[object Object]`, `Invalid Date`, placeholder or debug text; a missing value is blank |
| No console errors **(auto)** | Ghostframe `list_console_messages` on each route | No errors or uncaught exceptions |
| Four states per data view | Slow 3G (loading), new data dir (empty), block `*/api/*` (error), block `*/ws*` (stale) | Skeleton with the final geometry; one line and at most one action; what failed plus Retry; greyed with age. Disabled buttons say why |
| Undo over confirm | Bulk or destructive actions that can be undone | Undo toast, no confirm dialog; Cancel all shows its count and needs no dialog |

## 22. Polish and accessibility

| Check | How | Pass |
| - | - | - |
| Focus ring everywhere **(auto for top bar and one table)** | Tab through each screen | A visible ring on every interactive element, in a logical order |
| Contrast | `node docs/design/contrast.mjs`; axe **(auto)** | AA in both themes: 4.5:1 text, 3:1 controls and focus rings; buy/sell and P\&L never colour only |
| Zoom and motion | 200% zoom; reduced motion | Everything works; nothing moves that should not |
| Two radii, 4 px spacing | Inspect cards, controls, overlays | At most two border-radius values; spacing on the 4 px scale |
| Titles and icons **(auto for titles)** | Tab strip, home-screen install | `document.title` per route (`Orders · sesame`); favicon, `theme-color` per theme, home-screen icon |
| Hotkeys in tooltips | Hover icon-only controls with a hotkey | Tooltip shows it (`Cancel all · Ctrl Shift X`) |

## 23. Alerts and notifications (Phase 4)

Specs: [alerts.md](../design/screens/alerts.md) and [notifications.md](../design/screens/notifications.md).
Items marked **(auto)** are in `alerts.spec.ts`, `notifications.spec.ts` and the Phase 4 rows of
[e2e.md](e2e.md#phase-4-alerts-and-notifications).

| Check | How | Pass |
| - | - | - |
| One-click ladder alert **(auto)** | Hover a ladder level and click the bell; on a phone, long-press a level and choose `Set alert at …` | One action; toast `Alert set · {condition}` with Undo; the level keeps its marker; the row is in `/alerts` |
| Direction from the level **(auto for the midpoint)** | Set a bell above the midpoint and one at or below it | Above reads `bid at or above`, at or below reads `ask at or below`; neither holds when set |
| Fires within 2 s **(auto, fake venue)** | With the exit gate's live run: a price alert on a moving market | Toast (and desktop notification when the tab is hidden) within 2 s of the book change; the history row; Telegram within 2 s when configured |
| Game menu alerts **(auto for score)** | Board `[ellipsis]` or right-click; phone card `[ellipsis]` or long-press | Score change, game start, game end as checkbox items; start disabled with `Game already live`; everything disabled with `Game ended` |
| Alerts list **(auto for pause, resume, delete, 200)** | `/alerts` at 1440 and 390, with the stress venue | Rows 36 px (64 px phone); hover actions on desktop, `[ellipsis]` on touch; Delete leaves at once with Undo and sends nothing until the toast closes; the 200th alert disables `Set alert…` with its tooltip |
| Now column and stale | Block `*/ws*`; also the stress book that never loads | Values grey with `Stale Ns · the alert does not fire while stale`; nothing is filled from elsewhere |
| Edit form | Click a row or a ladder marker; phone: tap a row | Popover on desktop, bottom sheet on a phone; tick stepper, `now` value, validation lines; Esc and `[x]` close without saving |
| History **(auto for paging and mark read)** | Inbox popover, phone menu sheet, `/notifications` | Newest first, unread dot and weight, `Load older` by 50; opening marks read in every tab; rows with a target open it |
| Unread badge **(auto)** | Make over 99 unread | Rail, inbox, phone menu and the Notifications tab all read `99+`, `--secondary`, never red or green |
| Toasts per severity | `Send test`; a fill; a reject | Info 4 s (alerts 6 s), warning 8 s with a stripe, critical stays; one toast per event (see NT-01); nothing covers Cancel all |
| Settings > Notifications **(auto for save, failure, test, Telegram)** | Toggle rules, fail a save (block `*/api/*`), `Send test` | Saves on its own, reverts on failure with `Not saved · …`, test cooldown tooltip counts down; `Not configured · Telegram runbook` without Telegram |
| Desktop permission **(auto in Chromium)** | Fresh browser profile | Never asked on load or login; the first Desktop checkbox asks; denied disables the column with `Blocked in this browser` |
| Desktop notification | Allow, turn a Desktop rule on, hide the tab, `Send test` | One OS notification per notification (several tabs show one), app title only, a click focuses the app on the target; none while a tab is visible |
| Sound | Turn Sound on, set volume 30 and 100, trigger a fill and an alert | One short tone per severity at the set volume, none before the first click or key, one tab plays |
| Feed and account notifications | Stop the backend 40 s (feed down), foreign activity from polymarket.com in a live run | `Feed disconnected` after 30 s and `Feed reconnected` after; `Activity outside this app` sets Safe; copy as in notifications.md |

## Screenshot pass

Capture every row at **1440 × 900** and **390 × 844** (touch), in **dark** and **light**, first with
the fake venue (`task dev:fake`), then again with `SESAME_FAKE_VENUE_STRESS=1`. Save as `C:\Users\Administrator\Desktop\sesame-screens\<phase>\<screen>-<state>-<width>-<theme>.png`
and read each file back before judging it.

| Screen | States to capture |
| - | - |
| Login | empty, wrong password, rate limited (after 5 tries), logging in |
| Live board | live, today, upcoming, empty (league with no games), loading (Slow 3G), stale (WS blocked), deferred `N updates` pill |
| Board with dock (desktop) | 1, 2 and 4 docked ladders; ladder hovered while the book moves |
| Ladder sheet (phone) | open, order selected (order bar), long-press action sheet |
| Palette | empty query (Recent, Pinned, Held, Live now, commands), results, no matches, `>` commands, pinned row |
| Event view | moneyline selected, spread group expanded, closed markets, chart ranges, empty range |
| Watchlist | populated, empty |
| Portfolio | open (group by event, league), redeemable with claim link, closed, activity with P\&L chart, row expanded with ladder, no credentials |
| Orders | open grouped, filter Buy/Sell/Delayed, fills today, empty, order updates paused |
| Bottom dock (desktop) | collapsed, positions, orders, fills |
| Shell | status pill in every state (live, stale, reconnecting, disconnected, geoblocked, trading off), SAFE, ARMED, Cancel All enabled and disabled |
| Mobile header and tab bar | each route, back button on detail routes, menu sheet, status sheet |
| Settings | account (credentials states), notifications (Telegram not configured, desktop blocked, save failed, test cooldown), risk limits (Phase 3) |
| Alerts (Phase 4) | empty, loading, error, list with every kind and status, stale, at 200, row hovered, edit popover or sheet, phone row menu; stress mode (long names, the tennis score) |
| Notifications (Phase 4) | empty, loading, error, history with unread rows, `Load older`, inbox popover (desktop), menu sheet (phone), unread badges at 3 and at 99+ |
| Ladder alerts (Phase 4) | bell on a hovered level, marker, paused marker, phone long-press sheet with `Set alert at …` |
| Toasts | fill, reject, cancel result, error; above the dock; Phase 4: alert set with Undo, alert fired, test, warning and critical stripes, over the phone ladder sheet |
| Trading (Phase 3) | sending, placed, delayed with countdown, partially filled, rejected, checking (unknown outcome), moving (drag) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.