# 03 · Components

Every component is built in `starter/components.css` under the class named here. Each one uses tokens only.

Every interactive component has these states unless noted: **default**, **pressed** (scale `--press-scale` with the `press` spring, or a `--surface-sunken` fill for rows), **disabled** (40% opacity, no shadow, not focusable for actions that can't run; give the reason nearby), **loading** (spinner replaces the label, width does not change, not tappable) and **error** (`--danger` text or border plus a plain-words message). Focus is always a 3px `--accent` ring.

"Material" refers to `01-principles.md` §1.

---

## Navigation

### Glass nav bar · `.nav`
- **Anatomy:** a 3-column grid: leading (back button or empty), centre title, trailing (up to 2 round glass buttons). Below it, on tab roots, a **large title** (`.large-title`, 30/800) and one plain-words subtitle (`.large-sub`).
- **Behaviour:** clear at rest. Once content scrolls under it (more than 8px), it gets the glass fill and a hairline, and the centre title fades in (`tint` 180ms). This is concept 2's `.top.stuck`. Detail screens (thread) use `.nav--always`, which is glass from the start.
- **Sizes:** the content row is 44pt tall, plus the safe-area top.
- **States:** at rest, stuck, and with a back button. A trailing button can show a badge.
- **Do:** keep it to a title and at most two icon buttons. **Don't:** put search or filters inside it; they go below the large title.

### Floating glass tab bar · `.tabbar` `.tabs` `.tab` `.tabs__lens` `.remy-orb`
- **Anatomy:** a pill floating 14pt from the sides and the bottom safe area, 66pt tall, holding 5 tabs (**Today · Inbox · Leads · Jobs · More**). A **lens** (a strong glass pill) slides under the active tab with the `lens` spring. Beside it is a separate 66pt round glass **Remy orb** holding the rotating Remy mark.
- **Tab:** a 23pt icon over an 11pt label. Active: icon `--accent` and label `--accent-text`. Inactive: `--text-2`. The Inbox tab carries an unread **badge** (the total of `unread_count`).
- **Behaviour:** visible on tab roots only. Hidden on pushed screens (as in the shipping app) with the `sheet` spring. Tapping the active tab scrolls to the top, or pops to the tab root. Labels stop growing at the Large text size.
- **Remy orb:** opens the Remy sheet (`04-screens.md` §15). When Remy is off for the business, the mark is greyed out and still.
- **Do:** keep exactly 5 tabs. **Don't:** put a primary action (like "+") in the tab bar.

### Sheet · `.sheet` `.scrim` `.grabber`
- **Anatomy:** inset 8pt from the left, right and bottom. Radius 38. Sheet-glass fill. A 40×5 grabber, then a header (`.sheet__head`: optional avatar, `.sheet__title` 22/800, `.sheet__sub`, a round close button), then solid `.panel`s, then actions at the bottom.
- **Behaviour:** rises with the `sheet` spring over a `--scrim`. Closes on a scrim tap, a swipe down, the close button or Escape. Focus moves into the sheet and returns to the opener when it closes. Max height 88%; it scrolls inside.
- **Sizes:** **compact** (actions only, like a "Move to…" list), **standard** (fits its content), **large** (88%, for forms such as Book a job or Teach Remy).
- **Do:** put the primary action last, full width, in the thumb zone. **Don't:** stack one sheet on another. Replace the content instead (with a back chevron in the header).

---

## Lists

### List group · `.solid.list`
A solid surface with radius 30 and 6pt padding, holding rows. A section heading (`.section`) sits above it, with an optional trailing link ("See all").

### List row · `.row`
- **Anatomy:** a 44pt leading visual (avatar, icon tile or none), then a body (title 16/650, text 14 `--text-2`, optional meta line 12.5), then a trailing column (time, badge, chevron or a small button). Minimum height 64 (52 for settings rows, `.row--setting`). Hairline separators start at the text, not the edge.
- **States:** default, pressed (`--surface-sunken` fill, radius 22), disabled, and selected (for pickers, a trailing check).
- **Do:** truncate the text to one line. The whole row is one tap target. **Don't:** put two competing tap targets in a row, except one trailing small button (such as Call), which must keep its own 44pt hit area.

### Conversation row · `.row` (Inbox)
- **Anatomy:**
  - Leading: an avatar in the channel colour (initials, or a waveform glyph for voicemail).
  - Title: the name (or the formatted phone number). Bold 800 when unread. A muted bell-slash appears when `muted`.
  - Preview: `last_message`, prefixed "You: " (owner) or "Remy: " (bot). Voicemail rows show "Voicemail · 0:38".
  - Meta line: the channel dot and label, or a "🔒 {takeover_by_name} replying" chip. Add chips as they apply: "Remy paused" (`paused`), "Opted out" (`opted_out`) and "Unconfirmed" (`is_partial`).
  - Trailing: the row time ("10:02 AM" / "Yesterday" / weekday / "17 Oct"), and the unread badge below it.
- **Actions:** tap opens the thread. Long-press on native, or the "…" button on web, opens: Open customer file · Mark as read/unread · Mute/Unmute · Pause Remy here/Let Remy answer · Call {number}.
- **VoiceOver:** one label: "Dana Whitfield, SMS, 2 unread, It tripped again just now, 10:02 AM".

### Unread badge · `.badge`
A pill at least 22pt tall: white 12/800 text on `--accent-strong`, tabular figures. "99+" above 99. A dot variant (`.badge--dot`, 10pt) is for "something new" without a count. **Don't** use any other colour for unread.

### Status chips · `.chip`
- A 24pt pill with 12/750 text, a soft fill and strong text. Variants: `data-tone="ok|warn|danger|info|accent|remy"`, or `data-stage="<lead status>"`, which adds a coloured dot.
- **Do:** use the friendly display label ("Done", "Booked"). **Don't:** make chips tappable. Use filter chips for that.

### Filter chips · `.filters` `.filter`
A sideways-scrolling row of 36pt glass pills with a count. Selected is solid ink with surface text (`aria-pressed`). Chips with a zero count are hidden, except "All".

---

## Thread

### Message bubbles · `.bubble`
| Variant | Who | Look | Alignment |
|---|---|---|---|
| `.bubble--customer` | role `user` | `--surface-raised`, hairline, ink text | Left |
| `.bubble--owner` | role `owner` | `--accent-strong` fill, white text | Right |
| `.bubble--remy` | role `bot` | `--surface-raised` + `--remy-tint` wash + 1.5px `--remy-edge`, ink text. The first bubble in a group shows a small Remy mark and "Remy" | Right (it speaks for the business) |

- **Anatomy:** radius 22. The last bubble in a group (`is_last_in_group`) gets a 8pt "tail" corner and shows time · delivery status ("Sending…", "Sent", "Delivered", "Read", "Not delivered", "Failed to send"). Max width 80%. Grouping comes from the server's `is_first_in_group` and `is_last_in_group`; the client never recomputes it.
- **Extras:** a photo (`media_url`) shows as a 200×150 rounded image that opens full screen. A voice note shows a mini player. Reactions show as a small pill overlapping the bottom edge. A quoted message shows a tinted strip above the text.
- **States:** sending (70% opacity), delivered, failed. Failed shows a `.bubble__retry` line: "Not sent. Tap to send again".
- **Don't:** show the teammate's name on each bubble. Outbound is always "You" (a product decision).

### Event line · `.event-line`
A centred 13/600 `--text-2` line with a small icon: a call, a voicemail event, a channel move, or any unknown `role`. **Unknown roles must render as an event line, never crash.**

### Private note · `.note-card`
A full-width card with a dashed `--line-strong` border on `--warn-soft`. Header: "🔒 Private note · not sent to the customer". Comes from `is_internal_note: true`.

### Day separator · `.day-sep`
A centred glass pill: "Today", "Yesterday", the weekday, or "17 Oct".

### Remy suggestion card · `.suggest`
- **Anatomy:** a solid card with the `--remy-tint` wash and a 1.5px `--remy-edge` outline. Header: the Remy mark plus "REMY SUGGESTS". Then the suggested text (16/1.45), an optional "Why: …" line, and three actions: **Dismiss** (plain), **Edit** (secondary: moves the text into the composer), **Send** (primary).
- **When it shows:** after the last inbound message, when the mode is "suggest" or higher, and the conversation is not opted out, not held by a teammate and not on a closed WhatsApp window.
- **States:** default, sending (Send shows a spinner), sent (the card fades and the bubble appears), dismissed (gone, and the dismissal is remembered for that suggestion).
- **Auto mode** (`.suggest` with `.autosend__bar`): the header reads "Remy is sending in 5…1" with a **Stop** button, and a 4pt bar drains over **5 seconds**. Stop moves the text into the composer and stops auto-send for this thread. This is native `AutoSendBar` behaviour; never remove the countdown.
- **Data:** proposed (`proposed.suggestions`). There is no suggestion endpoint yet. With no suggestion, nothing is drawn.

### Remy paused banner · `.banner--remy`
- **Anatomy:** the Remy mark in its paused state (greyed, still), "Remy is paused here" / "You're answering this one. Remy won't reply until you resume.", and a **Resume** button.
- **Shows when:** `paused` is true and nobody else holds the conversation.
- **Variants:**
  - **Held by a teammate:** in place of the composer, "🔒 Jess is replying" / "Take over to answer yourself" with a **Take over** button.
  - **Opted out:** a danger banner, "This customer texted STOP…" (the server's words).
  - **Alerts off:** a warn banner on Today and More.

### Voicemail card · `.vm` (solid)
- **Anatomy:**
  - Header: voicemail avatar, "Voicemail", time · duration.
  - **Player** (`.player`): a 46pt round play/pause button, a 40-bar waveform that fills with `--accent` as it plays, and "0:12 / 0:38".
  - **Transcript** panel (`details`, open by default) with the transcript text. Key phrases may be highlighted with `<mark>` (c6).
  - Actions: **Call back** (primary), or "No number to call back" (disabled).
- **States:**
  - Playing / paused.
  - **No audio** (`audio_url` null): the play button is disabled, labelled "The recording is not available on this phone". **Never offer a play button without audio.**
  - **No transcript yet** (`transcript` null): "No transcript. Press play to listen." in italic `--text-2`. This is normal, not an error.
  - Playback error: "That recording could not be played."
- **Data:** `ConversationMeta.voicemail { duration_seconds, transcript, audio_url }`.

### Missed-call card · `.sys-card[data-kind="missed"]`
A solid card: a danger icon tile, "Missed call", "7:09 AM · Remy texted them back", and a **Call back** small button. It comes from a `system` message that starts with "Missed call".

### Lead card · `.sys-card[data-kind="lead"]`
Shown in a thread when the conversation has a lead. An accent icon tile, the service type, "Lead #143 · 47 Larch Lane", and a stage chip. Tapping it opens the lead.

### Booking card · `.sys-card[data-kind="booking"]`
- **Confirmed:** an ok icon tile, "Booked Thu 22 Oct, 2:00 PM", the service · status. Tapping it opens the job.
- **Pending (Remy pencilled it in):** a `--remy-edge` outline, the Remy mark, "Remy pencilled in Thu 22 Oct, 12:30 PM", "not confirmed yet", and two buttons: **Other time** (secondary, opens the calendar) and **Confirm & text Dana** (primary, `POST /appointments/{id}/status` confirmed).

### Composer · `.composer` / `.composer-block`
Glass, sticky at the bottom, carrying the safe area. **One of six states** (the order matters):

| # | State | Shows |
|---|---|---|
| 1 | Held by a teammate | "🔒 Jess is replying / Take over to answer yourself" + **Take over** |
| 2 | Refused (opted out, no phone, not configured) | The server's `unavailable` text, verbatim, in `--danger`. If the reason is `no_consent`, add a **"They agreed on the phone"** button, which opens the consent sheet |
| 3 | WhatsApp window closed | "No reply for 6 days, so WhatsApp only allows a template." + **Send a template** |
| 4 | Web chat | "Remy answers on your website. To reply yourself, continue this by text." + **Continue by text** |
| 5 | Checking / waiting for lead | A spinner with "Checking what you can send…", or "Reply available once Remy has the customer's details." |
| 6 | Ready | **+** (attach; only if the channel allows media), a text field (1–5 lines, "Message"), and a 44pt round send button (disabled when empty). Mic ↔ send morph on native |

Sending a reply yourself **pauses Remy on that conversation** (the server sets an owner takeover). Show a toast saying so.

---

## Pipeline

### Pipeline column · `.col` (glass)
- **Anatomy:** a header with a stage dot (glowing), the stage name, a count, and the money sum only when estimates exist; a 4pt bar showing the stage's share; then the cards, which scroll vertically. The column is the viewport width minus 44, max 330, and snaps horizontally. Stage shortcuts above the board jump to each column.
- **States:** default, drop-target (fill +20%, scale 1.01), empty ("Drop a card here", dashed).

### Job card · `.job-card` (solid)
- **Anatomy:** name + amount (only if known) / service_type / a meta row: source channel chip, days in stage or "today", and an "Urgent" flag chip.
- **States:** default, pressed, **stuck** (amber ring and a "⏳ 9 days" chip after 7+ days without moving), lifted (the original at 25% opacity), dragging (rotated -3°, scale 1.04, `--e-drag`).
- **Interaction:** tap opens the lead. A 280ms long-press picks it up (light haptic), and drop changes its status. **Always offer a "Move to…" sheet as well** (VoiceOver, one hand, gloves).

---

## Calendar

### Event chip · `.event` (solid)
- **Anatomy:** absolutely placed on the day timeline (74pt per hour, 7 AM–7 PM). A 4pt left colour edge, a 12% colour wash, the title (14/700), and "9:00–11:00 AM · Elena Park" (12.5).
- **States:** confirmed (default), completed (55% opacity), cancelled (struck through), pressed.
- **Week view:** a 7-column grid with compact chips (9.5pt label, 3pt edge).
- `time_slot` is free text. Parse common forms ("9:00 AM", "2-4pm", "8-10am"). Unparseable slots go in an "Any time" lane at the top. If the length is unknown, draw 1 hour.

### Remy ghost slot · `.event--ghost`
A pending appointment drawn as a **dashed** 2px outline in its colour on a nearly clear background, breathing (2.4s, off under Reduce Motion), with a "✦ REMY SUGGESTS" tag and an **Accept** pill. Tapping it opens a sheet: why Remy chose it, Worth / Drive / Customer tiles (only when known), **Other time**, **Accept & text {name}**, and "Not this one".

---

## Dashboard

### Stat tile · `.stat` (frosted)
A 36pt icon tile (accent-soft), a 28/800 tabular value, and a 14/600 label. Minimum height 120. The whole tile is a link. **Loading:** skeleton blocks. **Error:** the value becomes "–" and the tile stays tappable.

### Hero card · `.hero` (frosted)
On Today: one 64/800 number, a status chip at the top right, a label with one "Top: …" line, and channel chips. Only one per screen.

### Chart card · `.chart-card` (frosted)
A label, a 46/800 value with a change pill (ok-soft), and 6 bars (radius 9, the accent gradient; future days use `--line-strong`) with day initials. Bars grow in on first view (off under Reduce Motion). **Data is proposed:** omit the card if there is none.

---

## Controls

### Buttons · `.btn`
| Variant | Look | Use |
|---|---|---|
| `--primary` | `--accent-strong` with an `--accent-sheen` top, white label, glow | One per region: Send, Call back, Confirm, Sign in |
| `--secondary` | `--surface-sunken`, hairline | Other actions |
| `--glass` | Strong glass, used on the wallpaper | Nav bar buttons, floating buttons |
| `--plain` | Text only, `--accent-text` | Dismiss, Forgot password? |
| `--danger` | `--danger-soft` fill, `--danger` text | Sign out, Call off |

Sizes: **lg** 54 full width (sheet CTA), **md** 44 (default), **sm** 36 visual with a 44 hit area, **icon** 44×44 round. Loading: a spinner, same width. Disabled: 40%.

### Action tiles · `.acts` `.act`
Three equal 72pt tiles with an icon over a label: **Message · Call · Directions**. Used in the lead, customer and job screens. A missing action is disabled, not hidden, so positions never jump.

### Text field · `.field`
A label above (13/700 `--text-2`), a 50pt input (radius 16, `--line-strong` border, `--surface-raised`), and a hint below. Focus: an accent border plus a 3px accent-soft ring. Error: a danger border and a danger hint, in plain words. Forms always sit on solid surfaces.

### Segmented control · `.seg`
A glass pill with a sliding strong-glass knob (`lens` spring). 2–3 options, each at least 36pt visual and a 44 hit area. Used for Messages | Calls, Calendar | Pipeline, and Day | Week.

### Toggle · `.toggle`
52×32. On is `--ok`, off is `--line-strong`. The knob slides with the `lens` spring. Always paired with a label row; the whole row toggles it. Selection haptic.

### Search · `.search`
A 44pt glass pill: a magnifier, an input and a clear button. It filters live. No match: "No match for “…”", plus what search looks at.

---

## States

### Empty · `.empty`
A 64pt round icon on a surface, a title (17/750) and one or two lines of help (14). Optionally one action. Each screen's copy is in `04-screens.md`. The tone is calm, never "Oops".

### Loading skeleton · `.skeleton` `.sk-row`
Rows shaped like the real content: a 44pt circle and two bars at 55% and 85% width, shimmering at 1.4s (static under Reduce Motion). Use skeletons, not spinners, for lists. A spinner is only for in-button work.

### Error with retry · `UI.error()`
An alert icon, "Couldn't load …", the reason in plain words (for a network failure: "No answer from Remly. Check your signal and try again."), and a **Try again** secondary button. Keep any cached content visible above it when there is some.

### Offline pill · `.offline-pill`
A 32pt ink pill with "📶̸ You're offline", sliding down from the top safe area. Not tappable, and it doesn't push content. Reading still works. Sending is disabled with the reason.

### Toast · `.toast`
A dark pill above the tab bar, shown for 1.9s. One short sentence ("Sent. Remy holds off this conversation."). The danger variant is for failures. Announced politely to VoiceOver. **Don't** put an undo in a toast. Use a confirm step instead.

### Alerts primer · `center-screen` `.primer__icon`
A full screen (or a large sheet): a big frosted bell tile, "Turn on alerts", "Get a buzz the moment a customer calls, texts or chats, so nobody waits on you.", **Turn on alerts** (primary) and **Not now** (plain). Optionally, three sample glass notifications show what will arrive.
