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

# Combos

# Combos

Build a combo from 2 to 50 legs, get a quote from the market makers, accept it inside its
window, and manage combo positions (Phase 8). Terms are in [glossary.md](../glossary.md):
Combo, Leg, Combo mode, Slip, Use as leg, Remove, Market maker, Quote, No quote, Accept,
Decline, Expired, Combo position.

Venue facts that shape the design ([polymarket.md §16](../../venue/polymarket.md#16-phase-8-combos)):

* A quote arrives about 400 ms after the request, or the request ends with no quote.
* The window to accept is 5–10 s, given by the server's `expires_at`. Only Accept trades.
* There is no venue decline. Decline dismisses the quote in the app; an unaccepted quote
  expires on its own.
* A requester only buys the combo (a budget in pUSD, fees included) or sells combo shares
  (Exit). There is no combo book and no combo price other than the quote.
* Get quote works in Safe. Accept needs Armed, trading on and a quote with 1 s or more left.

## Leg builder

### Decision: combo mode, no hover affordance

The `Combo` toggle turns on **combo mode**. While it is on, the Sell and Buy buttons of
every outcome line on the board, the watchlist and the market view's outcomes panel are
replaced by one **leg toggle** in the same box. A click on it uses that outcome as a leg,
or removes it. The pressed `Combo` toggle and the slip's `Combo` heading are on screen
exactly while those buttons build legs. Nothing else changes: the ladder, the portfolio
and the orders table keep placing and cancelling orders as usual.

Why this and not the draft's hover `+ leg`:

* **What you see is what a click does.** A price button's click sends a GTC limit at
  that price (principle 4). A mode that silently changed that would be dangerous. Here the
  green and red buttons disappear and a neutral leg toggle takes their place, so in either
  mode the control on screen states its effect. The dangerous mistake (meaning to use a
  leg, placing an order) needs the user to ignore a green Buy button.
* **No small target beside a one-click Buy.** A hover `+ leg` next to Buy puts a
  near-miss one pixel from a real order.
* **One path on both devices.** Hover does not exist on touch, and long-press already
  cancels your orders or opens the action sheet (interactions.md, "The one rule"). A mode
  plus a tap works the same on a phone.
* **Copy rules.** `+ leg` is icon and text, which is reserved for navigation and Cancel
  all, and `Add` means buying more of a held outcome (glossary).

### The Combo toggle

| Where | Desktop | Mobile |
| - | - | - |
| Live board | First item in the panel header, left-anchored, then a 1 px vertical divider, then the filters | First chip in the filter row (so it never scrolls out of view) |
| Watchlist | In the header after `Watchlist 9` | Same |
| Market view | Header, left of `[star] [bell] [external-link]` | Score row, left of `[star] [ellipsis]` |

* `outline` button at `--control-h` (44 on touch), label `Combo`, `aria-pressed`. On:
  `--primary` bg and `--primary-foreground` text, like the active stake segment.
* One global state: every toggle shows it. It survives route changes for the session and
  is off after a reload (legs are kept for the browser session).
* Tooltip: `Combo mode · L`. Disabled when combos are unavailable, with the reason
  (`Combos unavailable · Session Key lacks the combo scope`).
* Every placement is left-anchored or in a full-width header, so the toggle does not move
  when the slip opens.

### Leg toggle

Replaces an outcome line's Sell and Buy buttons while combo mode is on. It takes their
whole box, so nothing else on the line moves.

| | Desktop | Mobile |
| - | - | - |
| Size | 136 × 32 (Sell 64 + gap 8 + Buy 64) | 156 × 44 (76 + 4 + 76) |
| Content | Best ask, numeric-price: `43¢` | Two lines: caption `Leg`, then the ask |
| Column header | `Leg` spanning the two columns | none |
| Accessible name | `Lakers leg, ask 43 cents`, `aria-pressed` | same |

| State | Treatment |
| - | - |
| off | `outline`: 1 px `--input` border, `--foreground` text. No side colour: it neither buys nor sells |
| hover (fine pointer) | `--accent` bg. Cursor tag `Use as leg · Lakers · ask 43¢` |
| on (a leg) | `--primary` bg, `--primary-foreground` text, lucide `[check]` at the inner left edge, positioned absolutely so the price stays centred. Cursor tag `Remove leg · Lakers` |
| other outcome of a leg's market | Looks off. Click replaces that leg in place (same row in the slip, no reorder). Cursor tag `Replace Celtics leg with Lakers` |
| not eligible (Gamma `comboStatus` `disabled` or unknown) | Disabled (50%), price still readable. Tooltip `Not available for combos` |
| `comboStatus` `pending` | Disabled. Tooltip `Not yet available for combos` |
| market closed or resolved | Disabled. Tooltip `Market closed` |
| slip full | Toggles that would make one more leg are disabled. Tooltip `10 legs at most (risk limit)`, or `50 legs at most` when the risk limit is 50 |
| accept in flight or checking | All toggles disabled. Tooltip `Combo being accepted` |
| stale book | Price stale-styled; **the toggle still works** (a leg places nothing, and the market makers price the quote) |
| empty ask | Blank price, toggle still works |

* "Same market" for the replace rule is the Gamma market. Three board lines that are one
  neg-risk event (soccer home, draw, away) count as one market for this rule, since two of
  their Yes outcomes cannot both win. Any other contradiction is the venue's to report
  (`Rejected · legs contradict each other`).
* On touch there are no tooltips: a tap on a disabled toggle shows its reason in the
  combo bar's text slot for 3 s instead.

## Slip

The slip holds the legs, the stake, the quote and the actions. There is one slip. It has
two states:

* **Build** (heading `Combo`): the legs used in combo mode; a buy quote for the stake.
* **Exit** (heading `Exit combo`): opened by a combo position's `Exit`. Legs are the
  position's and read-only, the size is the whole position, and the quote is a sell. The
  build legs are set aside and come back when the exit ends (filled, declined, expired,
  rejected or closed). An exit slip does not turn combo mode on; the board's price buttons
  stay Buy and Sell.

### Desktop (right dock, 320 px)

Where it shows: Live, Watchlist and market view while combo mode is on (or an exit is
loaded); always on Portfolio › Combos, so `Exit` fills it without moving the table. It is
hidden, with its state kept, on other routes.

* It is the rightmost column, from the top bar to the bottom dock, 1 px `--border` on its
  left. Ladder slots become `floor((width − rail − 440 − 320) / 232)`, at least 1: two at
  1440\. Docked ladders that no longer fit are hidden, not closed, and return when the slip
  closes. Below 1280 px the slip replaces the ladder dock.
* Opening and closing re-lay the page in one frame, on the toggle click, like docking a
  ladder. The toggle does not move, and while the pointer is over the dock area the closing
  rule of app-shell.md applies (the gap stays until the pointer leaves).
* It is a focusable panel with the panel focus indicator (`F6` and `Q` reach it).

```
 x: 1120                                  1440
 ┌──────────────────────────────────────────┐ y 48
 │ Combo  3 legs              [Clear]   [x] │ 36 header: heading, count (muted)
 ├──────────────────────────────────────────┤
 │ Leg                                  Ask │ 24 column labels (caption, muted)
 │ Lakers                          43¢  [x] │ 44 leg row: outcome (body)
 │ Lakers v Celtics · Moneyline             │    market (body-sm, muted)
 │ Over 221.5                      52¢  [x] │ 44
 │ Lakers v Celtics · Total                 │
 │ Fed decision December · Yes     62¢  [x] │ 44
 │ Fed decision December                    │
 │                                          │ leg list: takes the free height, scrolls
 ├──────────────────────────────────────────┤
 │ Stake $ [ 10 | 25 | 50 | 100 ]           │ 48
 ├──────────────────────────────────────────┤
 │ Quote                          (ring) 7s │ 24 label and timer
 │ Price         Shares      Cost with fees │ 16 captions
 │ 12.4¢         201.6       $25.00         │ 20 numeric
 │                                          │ 36 status line (two lines reserved)
 ├──────────────────────────────────────────┤
 │ [     Decline     ]  [      Accept     ] │ 48 action row
 └──────────────────────────────────────────┘ y 868 (bottom dock)
   The leg [x] shows on row hover or focus; its slot is reserved. (ring) is the 14 px
   countdown ring.
```

**Header.** `Combo` or `Exit combo` (heading), the leg count (`3 legs`, muted, slot fits
`50 legs`), `Clear` (ghost, text; removes every leg, with the undo toast
`Legs cleared · Undo`, 5 s; hidden in an exit slip, width kept), and `[x]` (close; turns
combo mode off in build, ends the exit in exit; tooltip `Close · L` in build).

**Leg rows** (44 px, two lines, 12 px side padding):

| Part | Width | Content |
| - | - | - |
| Outcome and market | flex | Line 1 the outcome (body); for Yes/No outcomes `{market} · Yes`. Line 2 the event or market type (body-sm, muted). Each line truncates with `…` and the full text in a tooltip |
| Price | 48 | The leg's live best ask in build, best bid in exit (column label `Ask` / `Bid`). Price cell with flash, from `engine/books` like every price. Stale: greyed. Closed market: `Closed` in `--muted-foreground` |
| Remove | 32 | `[x]` icon button, `aria-label` `Remove Lakers leg`, tooltip `Remove leg`. Build only; in exit the slot stays empty |

* Order is the order legs were used; a replaced leg keeps its row. Rows never reorder.
* A new leg is scrolled into view only when the pointer is not over the slip.
* No combined price is shown for the legs. The quote is the one source of a combo's
  price, and multiplying leg prices in the browser would be a second one (and money
  arithmetic).

**Stake row.** The stake selector (components.md), the same state as the top bar: one
active stake for the session. Six presets fit (`Stake $` + 6 × 40). In exit it reads
`Size  201.6 sh` (the whole position) instead.

**Quote area** (fixed 120 px, never grows or shrinks):

| Part | Content |
| - | - |
| Label and timer | `Quote`, and the quote timer at the right |
| Figures | Three figures (caption label over numeric value) in equal thirds. Build: `Price`, `Shares`, `Cost with fees`. Exit: `Price`, `Shares`, `Receive after fees`. Values come from the quote as sent by the backend: `blended_price_e6`, then shares and `total_required_e6` (build) or `net_receive_e6` (exit). Price shows to 0.1¢ (`12.4¢`) with the exact value in a tooltip. Slots reserve `99.9¢`, `999,999.99` and `$99,999.99`. Blank when there is no quote. Tooltip on Shares: `Pays $1 a share if every leg wins` |
| Status line | Two lines of body-sm reserved; the state text below. Longer text is clamped with the full text in a tooltip |

**Quote timer.** The [countdown ring](../components.md#countdown-ring) at 14 px, then whole
seconds left (`7s`), in a slot that fits `99s`. It runs linearly from the remaining time
the server gives (never from a browser clock guess) and shows seconds rounded down.
`--muted-foreground` until under 1 s; then, while it reads `0s`, it sits on a `--warning`
chip with `--warning-foreground`, the moment Accept disables. Reduced motion: the ring is
hidden and the seconds stay.

**Action row** (48 px): two equal slots, 8 px apart, both `--control-h`. The left slot is
`Decline` (ghost) while a quote is live and empty otherwise. The right slot is the one
filled button; its label and colour change with the state, its size never does.

### Slip states

| State | Figures | Status line | Left slot | Right slot |
| - | - | - | - | - |
| empty (no legs) | blank | blank. The leg list shows the empty state `No legs` / `Click a price to use its outcome as a leg` (mobile: `Tap`) | | `Get quote`, disabled: `Needs at least 2 legs` |
| one leg | blank | | | `Get quote`, disabled: `Needs at least 2 legs` |
| ready | blank | | | `Get quote` (`primary`) |
| requesting | blank | | | `Getting quote` with the 14 px spinner, inert |
| quoted | values | | `Decline` | `Accept` (buy: `--bid` fill, `--bid-foreground`; exit: `--ask` fill). Timer runs |
| quoted, under 1 s | values | | `Decline` | `Accept` disabled: `Under 1s left` |
| quoted, Safe | values | | `Decline` | `Accept` disabled: `Safe: arm to trade`. A click gives the Safe/Armed switch its one-off ring |
| quoted, trading off or geoblocked | values | | `Decline` | `Accept` disabled: `Trading off` / `Geoblocked (US)` |
| accepting | values | `Accepting` | | `Accept` with the spinner, inert. Legs and stake locked |
| unknown | values, greyed | `[circle-help] Checking…` / `Checking combo · no response after 10s` | | inert, tooltip `Checking combo` |
| filled | blank | `Filled · Buy 201.6 sh at 12.4¢ · $25.00` | | `Get quote` |
| expired | values, greyed | `Expired` | | `Get new quote` |
| declined | blank | `Declined` (muted) | | `Get quote` |
| no quote | blank | `No quote · no market maker quoted` | | `Get quote` |
| rejected | values greyed if a quote was shown, else blank | `Rejected · {reason}` in `--destructive` | | `Get new quote` if values are shown, else `Get quote` |
| combos unavailable | blank | `Combos unavailable` / `Session Key lacks the combo scope` | | `Get quote`, disabled with the same reason |
| quote limit | as before | | | `Get quote` disabled: `12 quotes a minute · next in 8s` |
| leg market closed | blank | | | `Get quote` disabled: `A leg's market is closed` |
| trading off or geoblocked | blank | | | `Get quote` disabled: `Trading off` / `Geoblocked (US)` |

Rules:

* **Accept is inert for its first 500 ms.** It appears in the slot the user just clicked
  for Get quote, so a double click or a repeated `Q` cannot accept a quote nobody saw.
* **Changing the legs or the stake while a quote is live declines it** (in the app;
  nothing is sent), and the status line reads `Declined · legs changed` or
  `Declined · stake changed`.
* **Accept is sent once.** The UI never resends it. After a timeout the backend reads the
  RFQ status; the slip shows `Checking…` until it knows (interactions.md, error feedback).
* **Filled keeps the legs**, so the user sees what they bought. The next buy takes
  `Get quote` then `Accept` again; `Clear` empties the slip.
* **Cancel all also declines a live quote** in the app. It cannot stop an accept in
  flight.
* A quote is announced once through a polite live region: `Quote 12.4 cents, 201.6
  shares, cost $25.00 with fees, 7 seconds`. Expiry and results are announced; the
  seconds are not.

Reasons shown after `Rejected ·` or `No quote ·` extend `reject-messages.ts`:

| Code | Text |
| - | - |
| `combo_no_quotes` | `No quote · no market maker quoted` |
| `combo_size_too_large` | `No quote · size too large for the market makers` |
| `combo_contradictory_legs` | `Rejected · legs contradict each other` |
| `combo_maker_declined` | `Rejected · market maker declined` |
| `combo_quote_expired` | `Rejected · quote expired` |
| `max_combo_notional` | `Rejected · max combo $5,000.00 (combo $6,000.00)`; without figures `Rejected · over the max combo notional` |
| `max_combo_exposure` | `Rejected · max combo exposure $25,000.00 (exposure $25,100.00)`; without figures `Rejected · over the max combo exposure` |
| `max_combo_legs` | `Rejected · max combo legs 10 (12 legs)`; without figures `Rejected · over the max combo legs` |
| `max_quotes_per_minute` | `Rejected · max quotes per minute 12, retry after 5s`; without figures `Rejected · over the max quotes per minute` |
| `combo_positions_unavailable` | `Rejected · combo positions unavailable` (the combo positions could not be read, or the last read is stale; P8-02) |
| existing codes | `Rejected · ` + the existing text (`not enough cash or allowance`, `rate limited (429), retry after 2s`, `Safe`, …) |

### Mobile (390 px): combo bar and sheet

**Combo bar** (44 px, full width, above the tab bar). It shows while combo mode is on or
an exit is loaded. On Live, Watchlist and market view it takes the stake bar's place, at
the same height, so nothing moves; on other tabs it sits above the tab bar and the content
gets 44 px of bottom padding.

```
 ├──────────────────────────────────────┤
 │ 3 legs · $25              [Get quote]│ 44 ready
 │ (ring) 7s  12.4¢ · $25.00  [ Accept ]│ 44 quoted (Accept in the side colour)
 │ Expired               [Get new quote]│ 44 expired
 ├──────────────────────────────────────┤
 │ tab bar                              │ 56
```

* Left: a text slot (body, truncates) that opens the sheet on tap. It shows `3 legs · $25`
  (build) or `Exit · 201.6 sh` (exit); with a live quote, the timer, the price and the cost
  or receive figure; otherwise the status word (`Checking…`, `Filled`, `Declined`,
  `No quote`, `Rejected`), and for 3 s the reason for a tapped disabled toggle.
* Right: the primary slot, 136 × 44, with the same labels and rules as the desktop right
  slot (including the 500 ms guard). There is no Decline on the bar: letting a quote
  expire sends nothing either, and the sheet has `Decline`.
* The bar hides while the on-screen keyboard is open (the palette covers the screen
  anyway) and returns when it closes. A live quote keeps its timer meanwhile.

**Sheet** (vaul, below the app header so Safe/Armed and Cancel all stay reachable; full
height to the bottom safe area; handle-only dragging so the leg list scrolls):

```
 ┌──────────────────────────────────────┐
 │ app header                           │ 52
 ├──────────────────────────────────────┤
 │               ──                     │ 20 handle
 │ Combo  3 legs        [Clear] [x]     │ 44
 │ Leg                              Ask │ 24
 │ Lakers                     43¢  [x]  │ 52 leg row, [x] 44 × 44, always visible
 │ Lakers v Celtics · Moneyline         │
 │ Over 221.5                 52¢  [x]  │ 52
 │ Lakers v Celtics · Total             │
 │ …                                    │ leg list scrolls
 ├──────────────────────────────────────┤
 │ Stake $ [  10 |  25  |  50  | 100 ]  │ 44
 ├──────────────────────────────────────┤
 │ Quote                      (ring) 7s │ 120 quote area, as desktop
 │ Price       Shares    Cost with fees │
 │ 12.4¢       201.6     $25.00         │
 │ status line                          │
 ├──────────────────────────────────────┤
 │ [    Decline    ] [     Accept     ] │ 60: 44 buttons, 8 px gap, 8 px padding
 │          (bottom safe area)          │
 └──────────────────────────────────────┘
```

* About 7 legs are visible at 390 × 844; the list scrolls, and the stake, quote and action
  rows stay pinned.
* A long-press on a leg row opens the action sheet, headed by the full outcome and market
  title (the touch equivalent of the tooltip): `Open market`, `Remove leg`.
* Closing the sheet (swipe down, `[x]`) keeps the slip and any live quote; the bar shows
  it.

## Combo positions (Portfolio › Combos)

Portfolio tabs become `Positions 12`, `Combos 3`, `Activity`. The count is open combo
positions. Data: Data API combo positions through the backend, every 60 s and after a
fill (PLAN §3.7); valued at cost, since there is no combo book. Leg prices in expanded
rows come from `engine/books` like every price.

### Desktop

The table fills the width left of the slip (1056 px at 1440).

```
 ┌────────────────────────────────────────────────────────────────────────────────────────┬─────────────┐
 │ Positions 12   Combos 3   Activity                       [ ] Show resolved (2)         │ slip (320)  │ 36
 ├────────────────────────────────────────────────────────────────────────────────────────┤             │
 │ Legs                              Size    Avg     Cost  Status  Open legs              │             │ 32 column headers
 ├────────────────────────────────────────────────────────────────────────────────────────┤             │
 │ Lakers, Over 221.5, Fed dec…     201.6  12.4¢   $25.00  Open          2/3 [Exit]       │             │ 36
 │ Chiefs, Bills, Eagles, Ravens      500   4.1¢   $20.50  Open          3/4 [Exit]       │             │ 36 expanded
 │   Chiefs   Chiefs v Jets · Moneyline        Open    61¢                                │             │ 32 leg lines
 │   Bills    Bills v Dolphins · Moneyline     Won                                        │             │
 │ Resolved                                                                               │             │ 32 group
 │ Knicks, Heat                        80    30¢   $24.00  Won    Claim [external-link]   │             │ 36
 └────────────────────────────────────────────────────────────────────────────────────────┴─────────────┘
   Drawn without the expand chevrons and narrower than 1056 px, to fit. `Claim` stands for
   `Claim on polymarket.com [external-link]`.
```

| Column | Width | Content |
| - | - | - |
| Expand | 32 | Chevron; also row click and `Enter` |
| Legs | flex (≥ 240) | Leg outcomes joined with `, ` (body). Truncates only when the row is full; tooltip lists each leg on its own line (`Lakers · Lakers v Celtics · Moneyline`), the first 10 then `+40 more` |
| Size | 96 | Shares, numeric |
| Avg | 56 | Average price in cents |
| Cost | 96 | Entry cost with fees |
| Status | 80 | `Open`, `Won`, `Lost`, `Partial` (tooltip `Resolved with a partial payout`) |
| Open legs | 72 | `2/3`: legs not yet resolved, of all legs. Blank once resolved |
| Actions | 104 | `Exit` (72, ask-tinted like a sell price button, text only, no price: the price comes from the quote). Tooltip `Get a sell quote for 201.6 sh`. Won: `Claim on polymarket.com [external-link]` (a link). Lost and Partial: blank |

* Exit requests the sell quote at once (it works in Safe) and loads the exit slip. A second
  Exit on another row replaces the exit (the first quote is declined in the app).
* Exit is disabled with the reason when combos are unavailable, while an accept is in
  flight (`Combo being accepted`), or when a leg's market is closed and not resolved
  (`A leg's market is closed`).
* Below 1280 px, Avg then Open legs hide, from the right.
* **Expanded row:** one 32 px line per leg, inset 32 px, no nested box: outcome, market
  (muted), leg status (`Open`, `Won`, `Lost`), the live best bid for open legs (price cell
  with flash). The market text links to the market view. 50 legs expand to 50 lines; the table scrolls, never an inner box. Height
  appears in one frame and the lines fade in (motion.md, portfolio row).
* Keys: `↑` `↓` select, `Enter` expand, `E` exit (requests the quote and focuses the slip;
  works in Safe because it trades nothing).
* Resolved rows sit in a `Resolved` group at the bottom, shown with `Show resolved`, as on
  the Positions tab.
* The Activity tab lists combo trades with type `Combo` and the legs summary in the market
  column.

### Mobile

```
 │ Positions  Combos 3  Activity        │ 44
 │ ┌──────────────────────────────────┐ │
 │ │ Lakers, Over 221.5, Fed decisi…  │ │ 20 legs (body, one line)
 │ │ 3 legs · 2 open            Open  │ │ 20 body-sm muted; status right
 │ │ 201.6 sh at 12.4¢ · $25.00       │ │ 20 numeric
 │ │ [             Exit             ] │ │ 44
 │ └──────────────────────────────────┘ │
```

* Cards as on the Positions tab (358 px, 12 px padding). Tap the card body: the legs
  expand inside the card (44 px lines, same content as desktop). Tap `Exit`: the sheet
  opens with the sell quote being requested.
* Won cards show `Claim on polymarket.com [external-link]` instead of `Exit`; Lost and
  Partial cards show no action row (the card keeps its height with the status line only).

## Four states per view

| View | Loading | Empty | Error | Stale |
| - | - | - | - | - |
| Leg toggles (board, watchlist, market view) | The price buttons' skeleton (same box) | Blank price for an empty ask; toggle works | Eligibility unknown (Gamma metadata not loaded): disabled, `Not available for combos` | Price greyed; toggle works |
| Slip, build | Leg rows restored from the session show skeleton lines until their market metadata loads | `No legs` / `Click a price to use its outcome as a leg` | `Combos unavailable` state above; quote errors in the status line | Leg prices greyed; `Stale 6s` pill (20 px) in the slip header; Get quote still works |
| Slip, exit | Legs from the position row, so none | n/a | as build | as build |
| Combo positions | Skeleton rows with the real column widths | `No combo positions` / `Combo positions appear here after your first combo fill` | `Combo positions failed to load` + `Retry` (secondary) | When reads have failed since the last success: values greyed, `Stale 3m` pill in the tab header (age of the last good read); `Exit` still works (it asks the market makers, not our data) |
| Expanded legs | Skeleton lines | n/a | as the table | Leg prices greyed per book |

## Hotkeys

Registered in `lib/hotkeys`; also listed in [interactions.md](../interactions.md#hotkey-map).

| Keys | Class | Scope | Action |
| - | - | - | - |
| `L` | Navigation (works in Safe) | Anywhere outside text inputs | Toggle combo mode |
| `Space` | Navigation | Board, watchlist, market-view outcome list, combo mode on | Use the outcome under the row cursor as a leg, or remove it |
| `Q` | Navigation | Anywhere while the slip holds legs | Get quote / Get new quote, and focus the slip |
| `Mod+Enter` | Trading (Armed) | The focused slip | Accept. Same guards as the button (500 ms, under 1 s, Safe nudge) |
| `Esc` | Navigation | The focused slip, a quote live | Decline. Esc still closes an open overlay first |
| `↑` `↓`, `Delete` / `Backspace` | Navigation | The focused slip | Move the leg cursor; remove the leg under it |
| `E` | Navigation | Portfolio › Combos | Exit the selected combo (request the sell quote) |
| `1`–`6` | Trading (as today) | The focused slip | Pick stake preset 1–6 |

* While combo mode is on, `B` and `S` do nothing on the board, watchlist and market-view
  outcome list (their buttons are leg toggles). The ladder keeps all its keys.
* The palette gets `Combo mode` (toggles it) and `Get quote` commands.
* Keyboard path for a 3-leg buy: `L`, arrows and `Space` three times, `Q`, `Mod+Enter`.

## Click budgets

"Armed" is a precondition (one click per session) and is not counted.

| Task | Budget | Desktop path | Clicks | Mobile path | Taps |
| - | - | - | - | - | - |
| Build a 3-leg combo and buy it | 6 | `Combo`, three prices, `Get quote`, `Accept` | **6** | `Combo` chip, three prices, `Get quote` on the bar, `Accept` on the bar | **6** |
| The same, combo mode already on | 5 | Three prices, `Get quote`, `Accept` | **5** | Same on the bar | **5** |
| Exit a combo | 2 | Portfolio › Combos row `Exit`, then `Accept` (or `E`, `Mod+Enter`) | **2** | Card `Exit`, then `Accept` in the sheet | **2** |
| Accept after the quote expired | 2 | `Get new quote`, `Accept` | **2** | Same on the bar | **2** |
| Swap a leg to the other outcome | 1 | Click the other outcome's leg toggle | **1** | Tap it | **1** |
| Remove a leg | 1 | Row `[x]` (on hover) | **1** | Tap the leg toggle again (1), or open the sheet and `[x]` (2) | **1** |
| Decline a quote | 1 | `Decline` or `Esc` | **1** | Let it expire (0), or open the sheet and `Decline` (2) | **0** |
| Back to one-click trading | 1 | `Combo` or `L` | **1** | `Combo` chip | **1** |

## Stress cases

Checked with `SESAME_FAKE_VENUE_STRESS=1` at 1440 and 390.

* **50 legs.** The leg list scrolls inside the slip (about 12 rows at 1440 × 900, 7 at
  390 × 844); the header, stake, quote and action rows stay put and the count reads
  `50 legs`. Toggles that would add a 51st leg are disabled. The combo row's legs cell
  truncates, and its tooltip lists 10 legs then `+40 more`; the expanded row shows all 50.
* **Long titles.** Every leg line truncates with `…` and a tooltip (desktop) or the
  long-press action sheet heading (touch). Prices, the count, the timer and the figures
  never truncate; their slots reserve `99.9¢`, `50 legs`, `99s`, `999,999.99` and
  `$99,999.99`.
* **390 px with the keyboard open.** The slip has no text input (the stake is presets), so
  the keyboard only opens from the palette or another screen's input. The combo bar hides
  under it rather than riding above it, a live quote keeps its timer, and the bar returns
  when the keyboard closes. If a text input is ever added to the sheet, the sheet sizes to
  the visual viewport with the stake, quote and action rows pinned above the keyboard and
  the leg list shrinking to at least two rows.
* **A quote arrives while the pointer is over the slip.** The quote area, both action
  slots and every figure slot already have their final size: values fill blanks, the right
  slot's label and colour change in place, and nothing moves. Accept ignores input for its
  first 500 ms. The leg list never scrolls on its own while hovered, and leg prices update
  in place with the usual flash.
* **Ladders docked when the slip opens.** At 1440 with four ladders, two stay and two are
  hidden until the slip closes; none is closed.
* **Stake at the max.** Six presets fit the slip's 296 px row; a `$99,999.99` cost fits
  its figure slot.

## Motion

* Desktop slip: its column appears in one frame; its contents fade in (`--dur-base`,
  `--ease-out`), like a ladder dock slot. No width animation.
* Mobile sheet: vaul with the motion tokens (`--dur-slow`), as the ladder sheet.
* Quote figures, the status line and the right slot's label change instantly. The timer
  ring is a timer, not a transition (motion.md). Leg prices flash like any price cell.
* Nothing waits for an animation: Get quote and Accept fire on click, and Accept's 500 ms
  guard is a behaviour constant, not a motion token.

## Notifications

Two notification kinds (copy in [notifications.md](notifications.md#the-table)):
`combo_filled` (`Combo filled`) and `combo_failed` (`Combo rejected` or `Combo failed`).
They also reach the user when the slip is hidden or closed.

## Tokens

None added. The slip reuses `--card`, `--border`, `--primary`, `--bid`, `--ask`,
`--warning`, the control and touch heights, and the motion tokens. The 320 px slip width
and the 500 ms Accept guard are layout and behaviour constants in the component, like the
232 px ladder slot and the 350 ms double-click guard.

## Open questions for the tech lead

1. **Timer source.** The contract should carry the remaining time (`expires_in_ms`) or the
   server's clock with `expires_at`, so a skewed browser clock cannot lengthen the
   countdown. The UI disables Accept under 1 s left either way.
2. **One stake.** The slip's stake is the session's active stake, shared with one-click
   orders (one concept, one control). The alternative is a separate combo stake; it would
   need its own glossary term.
3. **Get quote while trading is off or geoblocked** is disabled in the UI (a quote that
   cannot be accepted is noise, and the request would come from a blocked place).
   `engine/risk` `CheckQuote` should refuse it too.
4. **`RESOLVED_PARTIAL` and `REDEEMABLE`.** The spec maps them to `Partial` and `Won`
   (with the claim link). Confirm against the live run.
5. **Portfolio summary.** Proposal: combo cost counts in Exposure; Liq value and Unrealised
   P\&L leave combos out (no combo book). Otherwise the strip needs a combo line.
6. **Cancel all declines a live quote** in the app. Confirm this reading of "kill
   everything".


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