Skip to the galleries

Composed screens

Three real screens, static data, built only from the components in the sections below. If the system works, these needed no new parts — and that is the whole test. They also have to look like Picky Eaters: Figma's warm beige carries the page, the one yellow surface carries the personal list, and the brand olive carries the titles.

Your list

Only you can see this

14 of 14 foods

Your list
1 — Your listSearch, a 7-level filter facet and a sort control are part of the list, not hidden behind an icon. Try them: they run on local state over the sample data.

Supper club

Sun 12 Oct · 7:30 pm

4 people coming

Alex's place · 1 invite still open

1 dishBlocked2 dishesCaution1 dishBetter avoid1 dishAll good

Menu

5 dishes

Based on the ingredients you listed. Hidden ingredients and cross-contact aren’t detected.

Event menu — host
2 — Event menu (host view)The host sees dishes and what to change about them. No name, no avatar and no count of people appears next to any flag.

Add a food

However you say it. “Shrimp”, “cow's milk”, “anything with cilantro”.
How much does it matter?
Add a food
3 — Add a foodOne task, no bottom nav, and a Save button that stays enabled — validation happens on submit, never by greying out the way forward.

What each screen proves

  • Your list. That a safety-critical list can be searched, filtered and sorted without any of those controls competing with the data. Sorting defaults to “By level” so allergies sit at the top of the screen, where a person glancing at their phone in a restaurant will actually see them. The list surface is the one yellow card in the product, which is how you know at a glance that what you are looking at is yours and private.
  • Event menu. That the host gets everything they need to cook — per-dish status, the ingredient at fault, and what to do about it — without learning a single thing about any individual guest. A blocked dish carries the hatch band on its top edge as well as the chip, so “do not serve this” survives greyscale and both dichromacies. The privacy rule is structural, not editorial: the data shape carries states and dish counts, so there is nothing on this screen that could name a person even by accident.
  • Add a food. That the seven levels fit in one honest choice, each with its own shape, word and one-line explanation, so a person picks “Intolerance” over “Allergy” because they understand the difference, not because one looks more dramatic. The primary action is never disabled, so the screen never leaves someone guessing what is missing.

Severity, compatibility & people

Severity is not taste. An allergy blocks a dish, an intolerance asks for an alternative, a dislike is a preference, and ARFID is information — each one gets its own shape, word and weight. A guest is never described as the problem: the host sees what to cook, not who to blame.

The 7 levels — SeverityBadge

Ordered by family, then by how much attention the level asks for: allergy · intolerance · hate · dislike · ARFID · like · favorite. Weight carries the level — allergy is filled and also carries the hatch, while ARFID sits at the quietest tier because it is neutral information, not a warning.

AllergyIntoleranceHateDislikeARFIDLikeFavorite
size md (default)
AllergyIntoleranceHateDislikeARFIDLikeFavorite
size sm — for dense rows
AllergyIntoleranceHateDislikeARFIDLikeFavorite
showLabel={false}The word is still there for assistive tech — it is hidden visually, never removed.
AllergyNever serve this · Prohibit — an octagon with a slashAllergy
IntoleranceOffer an alternative · Warning — a triangleIntolerance
HateBetter to avoid · ThumbsDown — a thumb, pointing downHate
DislikeBetter to avoid · MinusCircle — a circle with a minusDislike
ARFIDDietary information shared · Info — a circle with an iARFID
LikeGood to include · ThumbsUp — a thumb, pointing upLike
FavoriteA safe bet · Star — a starFavorite
What each level means, and the shape it uses

Does it survive without color?

The right-hand strip is the same markup with `filter: grayscale(1)`. If a reader can still tell the seven levels apart there, color vision is not a requirement for safety. Every level keeps a distinct glyph shape and a distinct English word; allergy keeps its hatch as a third channel.

Allergy
Intolerance
Hate
Dislike
ARFID
Like
Favorite
In color
Allergy
Intolerance
Hate
Dislike
ARFID
Like
Favorite
Greyscale (filter: grayscale(1))

SeverityIndicator

The compact form for rows with no room for a pill. It is never interactive and never carries a focus ring — it is a label, not a control. The accessible name defaults to the level and can be enriched with the food.

size sm (default)
size md
With a richer accessible nameInspect with a screen reader: “Peanuts — allergy”.

The allergy hatch

45° stripes, 3px on an 8px period. Allergy is the only level that blocks a dish, so it always carries four channels at once: color, the Prohibit glyph, the word “Allergy”, and this pattern. Color alone is never allowed to carry it — and the pattern survives forced-colors mode, where tints do not.

Allergy
On the badge
Allergy
On the badge, greyscale
On the compact indicatorThe compact indicator drops the hatch and keeps the Prohibit glyph plus a solid ring, so it stays legible at small sizes.
Blocked
On a blocked dish`blocked` inherits the hatch, because a block is always an allergy.

CompatibilityLight

The host's traffic light. It takes a state and a number of DISHES — no name, no person, no guest count. The host sees the aggregate only: what to change about the dish, never whose list caused it. Names reach a host solely through a guest's own consent flow, and never through this component.

BlockedCautionBetter avoidAll good
BlockedCautionBetter avoidAll good
BlockedCautionBetter avoidAll good
Four states, three sizes
  • BlockedProhibit — octagon with a slash, plus the allergy hatch
  • CautionWarning — a triangle
  • Better avoidThumbsDown — a thumb, pointing down
  • All goodCheckCircle — a tick in a circle
Shapes, named
1 dishBlocked2 dishesCaution1 dishBetter avoid1 dishAll good
Summary strip — counts of dishes“1 blocked · 2 caution · 1 better avoid · 1 all good.” Five dishes, five counts. Never “2 guests”.
Pad thaiRice noodles · Peanuts · Egg · Tamarind · ScallionsBlocked
Mushroom risottoArborio rice · Butter · Milk · Mushrooms · ParmesanCaution
Flourless chocolate cakeDark chocolate · Eggs · Butter · SugarCaution
Green papaya saladGreen papaya · Lime · Chili · Fish sauce · Raw onionBetter avoid
Roast chicken with lemonChicken · Lemon · Rosemary · Garlic · Olive oilAll good
Per-dish rows — where it actually livesThe guidance line talks about the ingredient and what to do with it. There is no row, no tooltip and no expansion on this screen that names a guest.

Avatar & AvatarStack

Initials are the default, not the fallback of last resort — most people never upload a picture. The initials logic has to cope with one-word names and with names that have no Latin letters at all.

Sizes xs · sm · md · lg
Two-word name
One-word name“Kwame” → a single initial, still centred.
Non-Latin name中村陽子 — the glyphs are used as-is; nothing is transliterated.
With an imageLocal placeholder art; the initials show while it loads and if it fails.
AvatarStack — fitsFour people, four faces.
AvatarStack — overflowmax=3 over five people → “+2”.
AvatarStack — small, in a card header

MemberRow

Role is a badge with its own glyph (crown · two people · one person), so “who can edit this event” never depends on color. The actions slot takes a menu button — its own ≥44px target, inside the row but not inside the row's own click target.

Alex MoreauAcceptedHost
Priya RaghunathanAcceptedCo-host
KwameAcceptedGuest
Host · co-host · guest, with an actions slot
Pending RSVP
Sam O'Donnell-ReyesPendingGuest

GroupCard

One card, one target: the whole card becomes the link, so there is never a nested control fighting it for the tap.

With members and a next event
Empty — no members yet

Sunday supper club

4 membersSun 12 Oct · 7:30 pm
Static (not tappable)

TasteSlider

Four stops over the EXISTING preference levels — I hate it · I don't like it · I like it · I love it → hate/dislike/like/favorite (D-066 §5). It writes nothing but an existing FoodLevel: there is no intensity field, the data contracts stay fixed and the compatibility engine and its 25 tests are untouched. Underneath it is a native <input type="range">, so arrow keys, Home/End, Page Up/Down and touch drag are correct for free; aria-valuetext announces the visible sentence instead of “2 of 3”. The four labels are pointer shortcuts in a sibling row that is aria-hidden and out of the tab order, so the range stays the single announced control.

Writes FoodLevel = like · announced as “I like it”

Live — drag it, or press Tab then the arrow keysFocus-visible lands on the THUMB, where the eye already is. The thumb keeps the brand yellow but carries a concentric --action-primary-fg core: the yellow alone is 1.20:1 on the track, which no one could locate (WCAG 1.4.11). It is a fill, not a stroke — the system stays borderless.

value="hate" — announced as “I hate it”

value="dislike" — announced as “I don't like it”

value="like" — announced as “I like it”

value="favorite" — announced as “I love it”

Every stop, pinnedThe current stop is semibold and --text-primary; the others stay --text-secondary. At 390px the long label wraps to two lines instead of colliding, and the row reserves that height so promoting one label never shifts the other three.
Disabled

InviteLinkCard

The link carries the event, never anyone's food list — the card says so out loud. Press Copy to see the confirmed state: it is internal state that flashes for two seconds and announces itself politely, so there is no prop to pin it open for a screenshot.

Invite link

Anyone with this link can RSVP. It carries the event only — never anyone's food list.

https://pickyeaters.app/invite/QK4T-9ZB2

Default

Invite people to Sunday supper club

Anyone with this link can RSVP. Their food list stays theirs — you only ever see what to cook.

https://pickyeaters.app/invite/QK4T-9ZB2

Custom title and description

Shell & layout

The frame every screen lives in: a fixed-height column of header → scrollable content → bottom nav, sized with 100dvh and padded for the device safe areas, so the nav never drifts out of the viewport when a mobile toolbar appears.

AppShell

The nav is a flex sibling, not a fixed overlay, so the scroll region is intrinsically clear of it — no magic bottom padding and no layout shift when the nav mounts. Inside this gallery the frame is height-constrained (h-full) so it fits the phone mock; the shipped default is h-dvh.

Your list

14 foods

Recently added

3 foods

PeanutsAllergy
Cow's milkIntolerance
Raw onionHate
MushroomsDislike
CeleryARFID
TofuLike
Margherita pizzaFavorite

Active tab: list.

AppShell
Header + content + navScroll the content: the header and the nav stay put.

New food

One task, no destinations competing with it.

AppShell — no nav
No navFull-screen tasks (/add, /invite) hide the nav; the shell then keeps the bottom safe inset itself.

BottomNav

Four destinations — list · groups · scan · explore — plus a separate circular Add action that is never “active”. The active tab is not color-only: the Phosphor weight switches duotone → fill, so the state survives greyscale and deuteranopia.

active="groups"

active="list"

active="scan"

active="explore"

Each tab active in turn
Greyscale — the active tab is still readable

Groups

Content scrolls behind nothing: the nav is a sibling, not an overlay.
BottomNav
In situInside the phone frame the bar owns the bottom safe inset, so its surface reaches the screen edge.

ScreenHeader

Back control, title (+ subtitle) and a trailing actions slot in one row, so short screens keep their vertical space. The caller picks h1 or h2 rather than the component guessing the document outline.

Your list

Title only

Your list

14 foods · only you can see this

Title + subtitle

Sunday supper club

With back

Sunday supper club

Sunday 12 October · 7:30 pm

With actions
Chrome-only — title omitted (D-078)No title means NO heading element at all: logo at the start, 48px chrome actions at the end. That is what keeps one h1 per page once the title moved into the body — see PageTitle below. The row is floored at 48px so a chrome action is never clamped.

Menu

Back + actions + sticky

PageTitle

The in-body page heading (D-078 / D-079): 32/40/700 in --text-brand-display, with an optional count line underneath. It sits at the top of the SCROLLABLE body, so it announces the screen on arrival and then scrolls away under the chrome veil — the nav's active state keeps the “where am I” answer permanently. The screen owns its gutter: PageTitle renders a bare block and takes whatever inset the caller passes.

My list

9 foods

tone="brand" (default), with a count--text-brand-display is LARGE-TEXT-ONLY: 3.44:1 on the app ground, which is AA at 32px/700 (large = 3:1). Never reuse it below 24px bold.

Guests

4 accepted · 1 pending

tone="default" — neutral inkFor screens where the green would compete with branded content beneath it.

Explore

No countThe count line is absent, not empty — it never collapses into the title's line box.

SectionHeader

Titles a block inside a screen, with one optional trailing action.

Allergies

h2

Allergies

3 foods

h2 + subtitle + action

Starters

2 dishes

h3 — nested deeper

Divider

Decorative by default, so screen readers stay quiet. Inset pulls the rule in by one spacing step at each end — that is the list-row rule used across the product.

Horizontal, plain
Horizontal, inset
Vertical, plain
14 foods
3 allergies
Vertical, inset
14 foods
3 allergies
Semantic (decorative={false})

HScroll

The one horizontally scrollable row in the system. The edge fades are a mask, not a painted gradient, so they work on any surface and in either theme without knowing what is behind them — and they are stateful: the leading fade only appears once you have scrolled, the trailing one disappears at the end, so a fade always means there is more this way. A row that fits centres itself; a row that overflows left-aligns and slides.

Overflowing (trailing fade)
Fits (centred, no fades)
On a card (mask shows the card through)

Content & status

The surfaces everything else sits on: cards, list rows, the food tile grid, and the three things a screen can be doing instead of showing data — loading, empty, broken.

Card

Four variants. `list` is the ONLY yellow surface in the product (D-011): it marks a personal list, and using it anywhere else would make yellow mean nothing.

Plain card

The neutral container.

plainDefault. Card surface on the app background.

Elevated card

Uses the e1 shadow token.

elevatedOne elevation step. For things that float above the page.

Tinted card

Recedes instead of lifting.

tintedSunken surface. Groups secondary content.

Your food list

14 foods · only you can see this.

list — the only yellowReserved for "this is your list". Do not reach for it to add warmth.
No padding — the child owns it.
padding: none

Default padding.

padding: md (default)

Roomier padding for hero cards.

padding: lg

Card — interactive

`interactive` turns the whole card into one target with hover, pressed and focus-visible affordances. Render it `as="button"` or `as="a"` so it is actually reachable by keyboard — an interactive div is not.

Default
Hover
Focus-visible
Disabled

ListRow & ListRowGroup

The group draws the inset divider between rows and never after the last one, so a list never ends on a hanging rule. Leading slot is a fixed 40px icon column; the trailing slot here holds the severity.

Peanuts
Cow's milk
Raw onion
Titles only
PeanutsCarries an adrenaline pen
Cow's milkOat milk is fine
CeleryTexture, not taste
Title + subtitle
Food glyph + severity badge — the production rowGlyph and severity mark stay visually separate: the food says WHAT, the badge says HOW MUCH it matters.

FoodCategoryTile

The Uber services-grid unit: neutral tile surface, art on top, label centred below, the whole tile is the target. Selection is not color-only — a check mark appears.

Category gridTap a tile; selection is single-choice here.
Default
Selected

Skeleton & Spinner

Skeleton for a layout we can predict, Spinner for a wait we cannot. The shimmer stops under `prefers-reduced-motion`.

variant: text
variant: circle
variant: rect
Loading list — the real shapeThe skeleton mirrors the row it replaces (40px glyph column, two text lines, trailing badge), so nothing jumps when data lands.
Loading
Spinner sm
Loading
Spinner md
Checking the menu
Spinner lg`role="status"` with a label — screen readers hear the wait.

EmptyState & ErrorState

Both take a food glyph or a Phosphor icon, a title, a description and one action. Copy is warm and blames the situation, never the reader.

Your list is empty

Add the foods that matter to you. Hosts will see what they need to cook, and nothing more.

EmptyState — first run

No foods match “cashew”

Nothing in your list uses that word yet.

EmptyState — no search results
ErrorState — connection
ErrorState — menu check failed

Actions

Button, IconButton, Chip, Badge, SegmentedControl and Tabs, in every state CONTRACT §5 requires: default · hover · pressed · focus-visible · selected/checked · disabled · loading · error/invalid where applicable.

Button

4 variants × 3 sizes, with/without icons, fullWidth, loading.

variant="primary" — default, sizes sm/md/lgHover/pressed: live — point and click. Focus-visible: live — press Tab to reach these buttons.
variant="secondary" — default, sizes sm/md/lgHover/pressed: live — point and click. Focus-visible: live — press Tab to reach these buttons.
variant="tertiary" — default, sizes sm/md/lgHover/pressed: live — point and click. Focus-visible: live — press Tab to reach these buttons.
variant="destructive" — default, sizes sm/md/lgHover/pressed: live — point and click. Focus-visible: live — press Tab to reach these buttons.
With icons — leading and trailing
fullWidth
loading — live, click to triggerWidth never shifts; label fades, spinner takes over. Element stays focusable during loading (aria-disabled, not disabled).
Disabled

IconButton

Variants — ghost (default), primary, secondary, tertiary, destructiveHover/pressed/focus-visible: live.
Sizes — sm (36px visual, 44px hit area), md (44px), lg (48px), chrome (48px)chrome renders the same box as lg on purpose: it is a SEMANTIC size — “a tab-topbar action, which D-078 floors at 48×48” — so the chrome floor can move later without dragging every large control with it.
loading — live, click to trigger
Disabled

Chip

All 3 kinds: filter (independent toggle), choice (one-of-many), input (committed token with remove).

kind="filter" — live, click to toggleSelection is never color-only: a Check glyph replaces the leading icon and the fill flips to the gold action tone.
kind="choice" — live, one-of-manyrole=radio; wrap the group in role=radiogroup in real usage.
PeanutShellfishCilantro
kind="input" — removable, liveThe remove button is its own ≥44px target, nested in a <span> root (never a <button> inside a <button>).
Disabled

IllustrationChip

Despite the name it is NOT a Chip: a 48px illustration with the label underneath — no background, no stroke, no pill (D-066 §1, D-076). Seven coloured pills turned the top of My list into a rainbow, and a coloured pill also implies severity, which a filter does not carry. Selection runs on three channels — brand label + semibold + a 4px indicator bar — never colour alone; the bar's height is reserved when unselected, so toggling a facet can never reflow the row.

Default · selected · emptyFacet — live, click to select72×80 at rest (48 illustration + 4 + 20 label + 4 + 4 indicator). emptyFacet is a --text-tertiary swap (5.89:1 light / 7.63:1 dark), never opacity — the facet stays interactive, so its label has to keep passing WCAG 1.4.3.
The three states side by sideSame row, pinned: default, selected, emptyFacet. The art is the real /food-icons PNG resolved through filterIllustration(); a facet whose art has not been generated yet falls back to the level's Phosphor glyph rather than leaving a hole.
Disabled

Badge

Non-interactive status label — every tone pairs a verified bg/fg pair.

3 guestsHostAcceptedPendingBlocked dishCo-host
All tones
SmMd
Sizes

SegmentedControl

Radix ToggleGroup — arrow-key nav, roving tabindex, deselection swallowed (never empty).

Live — click or use arrow keys once focused
fullWidth

Tabs

Real tabs with role=tabpanel content — use SegmentedControl instead when there are no panels.

6 guests invited — 2 pending, 4 accepted.
Live

Forms

Input, Textarea, SearchField, Select, Switch, Checkbox, RadioGroup — always paired with FormField for label/hint/error. Validation happens on submit; the primary button is never disabled to signal invalid input.

Input

Visible to guests
Default, with hint
With leading icon
Enter a name so your host can find you.
invalid — error state, livearia-invalid + aria-describedby wired via FormField context; error text has role=alert.
Assigned automatically
Disabled

Textarea

Optional
Default
Tell your host why you can't make it.
invalid — error state, live

SearchField

Always-visible search — never the icon-triggered overlay pattern. Pass-3 anatomy (D-070): a decorative magnifying glass INSIDE the pill at the leading edge, then the query text, and NO trailing search button — a search button next to an always-live filter was a second, redundant affordance that on device read as the field's primary action. The onSearch and searchLabel props went with it. The only trailing element is the clear (×), and only while there is text. Sort/filter is now one icon button inline to the right of the field, owned by the screen (D-071).

Without a value — liveLeading glyph is aria-hidden and --icon-secondary (8.58:1 on the light app tone, 6.13:1 on the worst light surface step): it duplicates the accessible name, it is not an affordance. Focus is the caret, never a ring — the wrapper shifts one surface step.
With a value — clear button liveThe clear control is a quiet bare X named “Clear search”: no resting fill, a contextual --control-bg circle on hover/press, 36px visual with a 44×44 ::after hit area. Trailing padding steps 16px → 8px via has-[button]: so the pill stays optically symmetric.

Select

Default — live, open it
Choose a meal type before continuing.
invalid — error state, live

Switch

Default, checked, live
Disabled

Checkbox

Default, checked, indeterminate, live
Consent
You must agree before joining the event.
invalid — error state, live
Disabled

RadioGroup

How much do you like tomatoes?
Default, live
Severity
Disabled option

Complete mini-form — add a food item

Validation runs on submit; the button stays enabled the whole time. The first invalid field receives focus so a screen-reader or keyboard user lands exactly where the error is announced.

e.g. Peanuts, cilantro, shellfish
Severity (required)
Optional — shared only if you choose to
Live form

Overlays

Sheet, Dialog, ConfirmDialog, Tooltip and Toast — all Radix-based, all live. A11y guarantees (focus trap, Esc, scrim click, focus return, aria-live) are noted next to each and come from Radix, not hand-rolled ARIA.

Sheet

Bottom sheet with grab handle, side is always bottom.

Sheet demo
Live — inside a phone frameA11y: focus trap, Esc, scrim click and focus-return-to-opener all come from Radix Dialog underneath. prefers-reduced-motion swaps the slide for a cross-fade.

Dialog

Centred modal, reserved for irreversible or blocking decisions — reversible actions use Toast + Undo instead.

Dialog demo
Live — inside a phone frameA11y: focus trap, Esc, scrim click, focus-return and aria-modal all come from Radix Dialog.

ConfirmDialog

The one confirm shape the product uses — destructive confirms default focus to Cancel, never Confirm.

ConfirmDialog demo
Live — inside a phone frameA11y: inherits Dialog's focus trap / Esc / scrim-close / focus-return.

Tooltip

Hover/keyboard-focus affordance only — content must be supplementary, never essential, since touch has no equivalent trigger.

Intolerance
Live — hover or Tab to the info button

Toast

Every reversible action resolves through toast() + Undo — never a ConfirmDialog for these.

Live — fires a real toast at the bottom of the viewport

    Icons

    Two icon systems: Phosphor for UI chrome (through the `Icon` wrapper) and the hand-authored food glyph kit (through `FoodIcon`), driven by `foodIconManifest`.

    Phosphor UI icons

    Weight defaults to duotone (D-012) — every other weight is an explicit override.

    Plus
    ListDashes
    UsersThree
    Camera
    MagnifyingGlass
    Check
    X
    Warning
    Trash
    PencilSimple
    ShareNetwork
    Link
    ArrowLeft
    DotsThree
    Representative set — size md, tone primary, weight duotone (default)
    xs
    sm
    md
    lg
    xl
    Sizes — --icon-size-{xs,sm,md,lg,xl}Plus at every size token.
    primary
    secondary
    tertiary
    disabled
    brand
    inherit
    inverse
    on-brand
    Tones — --icon-* semantic rolesinverse and on-brand are shown on --surface-inverse / --action-primary-bg since they are only legible there.
    duotone (default)
    regular
    bold
    fill
    light
    thin
    Weight comparison — duotone is the default

    Food glyph kit — all 30 P0 ids

    Driven by foodIconManifest; nothing here is a hardcoded list.

    30 of 30 glyphs

    cuisine-japaneseJapanese
    cuisine-mexicanMexican
    diet-dairy-freeDairy-free
    diet-gluten-freeGluten-free
    diet-veganVegan
    diet-vegetarianVegetarian
    dish-burgerBurgers
    dish-dessertDesserts
    dish-pizzaPizza
    dish-saladSalads
    fav-plateFavourites
    ing-celeryCeleryEU-14
    ing-gluten-cerealCereals with glutenEU-14
    ing-crustaceanCrustaceansEU-14
    ing-eggEggsEU-14
    ing-fishFishEU-14
    ing-lupinLupinEU-14
    ing-milkMilkEU-14
    ing-molluscMolluscsEU-14
    ing-mushroomMushrooms
    ing-mustardMustardEU-14
    ing-onionOnion
    ing-peanutPeanutsEU-14
    ing-sesameSesameEU-14
    ing-soySoyEU-14
    ing-sulphiteSulphitesEU-14
    ing-tomatoTomato
    ing-tree-nutTree nutsEU-14
    list-personalMy food list
    place-restaurantRestaurants
    Search + filter + sort

    Food icon notes

    A food glyph (ing-peanut, etc.) is neutral: the same glyph renders whether a user marked peanuts as a favorite or an allergy. Severity is always carried separately, by SeverityBadge / SeverityIndicator, never by swapping the food glyph or its color.

    Severity is never a food iconD-022

    tier="tile" currently falls back to the glyph centred on --surface-tile — no tile illustration exists yet, and none is generated here. See the tile-fallback cells below (dish, cuisine, place and fav groups).

    Tile tier — open decisionD-024

    Tokens

    Everything below is rendered from the runtime-enumerable token modules in @picky/ui — no list on this page is hand-maintained. Hexes and contrast ratios are read from the document with getComputedStyle for the theme you are looking at right now, so switching the theme in the header re-measures the whole gallery.

    Color reference

    The primitive ramps and every semantic token, collapsed so the reference stays a reference. Open any row to read its authored value, its resolved value on the theme you are looking at, and its measured contrast ratio.

    Primitive ramps

    15 families
    Theme-independent raw color. Components never reference these directly — they exist so the semantic layer has somewhere to point. Steps marked “anchor” are the Figma values that must survive verbatim; everything else is interpolated in OKLCH.
    neutral--p-neutral-*
    • 0 · anchor#FFFFFF
    • 50#F7F5F3
    • 100 · anchor#F0EBE8
    • 150#E2DBD6
    • 200 · anchor#D4CBC5
    • 250#C7BCB9
    • 300#BAAEAD
    • 400#9E9497
    • 500#817A81
    • 600 · anchor#64626B
    • 700 · anchor#4C4A51
    • 800 · anchor#393740
    • 900#222127
    yellow--p-yellow-*
    • 0#FFFDF4
    • 50#FFF7DE
    • 100 · anchor#FEF1C8
    • 200#F9E39F
    • 300 · anchor#F5D673
    • 400 · anchor#F0C030
    • 500#CB9F26
    • 600#A77F1C
    • 700#836012
    • 800#60440A
    • 900#3F2A04
    green--p-green-*
    • 0#FAFDF6
    • 50#DFEAD3
    • 100#C5D7AF
    • 200#ADC48B
    • 300 · anchor#96B066
    • 400#8AA24E
    • 500 · anchor#7E9334
    • 600#6D7F2E
    • 700#5D6B28
    • 800 · anchor#4C5822
    • 900#1D2C10
    red--p-red-*
    • 0#FFFBFB
    • 50#FFF5F3
    • 100#FFC1BC
    • 200 · anchor#FF8A85
    • 300#FF6F68
    • 400#FF4E45
    • 500 · anchor#FF0C00
    • 600#D0211D
    • 700#A22824
    • 800#762725
    • 900 · anchor#4D2321
    severity-allergy--p-severity-allergy-*
    • 0#FDFCFC
    • 50#F9F5F6
    • 100#F7EBEC
    • 200#F3D6D8
    • 300#F0B0B6
    • 400#F48492
    • 500#F9496E
    • 600#CF3759
    • 700#A22E46
    • 800#772434
    • 900#3E171D
    severity-hate--p-severity-hate-*
    • 0#FDFCFD
    • 50#F7F6F9
    • 100#EDEDF4
    • 200#DDDBED
    • 300#C1BDE4
    • 400#A69EE0
    • 500#8F7EDD
    • 600#795CD9
    • 700#6145B5
    • 800#473484
    • 900#251E44
    severity-dislike--p-severity-dislike-*
    • 0#FCFDFC
    • 50#EEF8F7
    • 100#D3F5F1
    • 200#91ECE4
    • 300#6CD1C9
    • 400#4CB6AD
    • 500#359A93
    • 600#277F78
    • 700#21645E
    • 800#1B4946
    • 900#132726
    severity-intolerance--p-severity-intolerance-*
    • 0#FCFCFC
    • 50#FAF6F0
    • 100#F6EDDA
    • 200#F2DBA8
    • 300#E3BC60
    • 400#C8A141
    • 500#AB862B
    • 600#8D6E1E
    • 700#6E561B
    • 800#513F16
    • 900#2B2310
    severity-arfid--p-severity-arfid-*
    • 0#FCFDFD
    • 50#F6F7F9
    • 100#EBEEF4
    • 200#D6DCED
    • 300#B2C1E5
    • 400#8EA4E0
    • 500#6B88DD
    • 600#4A6ADA
    • 700#2F4BCD
    • 800#223898
    • 900#14204D
    severity-like--p-severity-like-*
    • 0#FCFCFC
    • 50#EFF9F3
    • 100#D9F5E3
    • 200#9FEDC0
    • 300#6BD59C
    • 400#4ABA81
    • 500#339E6A
    • 600#268255
    • 700#206643
    • 800#1A4A32
    • 900#12281C
    severity-favorite--p-severity-favorite-*
    • 0#FDFCFC
    • 50#F9F6F8
    • 100#F5EBF3
    • 200#EED6EA
    • 300#E9AFDF
    • 400#E881DA
    • 500#D35EC6
    • 600#AF4BA4
    • 700#893C80
    • 800#642E5E
    • 900#341A31
    status-success--p-status-success-*
    • 0#FCFDFC
    • 50#F0F8F2
    • 100#DAF4E0
    • 200#A2EEB8
    • 300#6BD68F
    • 400#4ABA74
    • 500#339E5E
    • 600#25814B
    • 700#20653B
    • 800#1B4B2D
    • 900#13281A
    status-warning--p-status-warning-*
    • 0#FCFCFC
    • 50#F9F6F1
    • 100#F6EDDF
    • 200#F2D9B7
    • 300#EFB761
    • 400#D49B42
    • 500#B5812B
    • 600#96691E
    • 700#76531B
    • 800#563E16
    • 900#2D2210
    status-error--p-status-error-*
    • 0#FDFCFC
    • 50#F9F5F5
    • 100#F7ECEA
    • 200#F3D7D2
    • 300#F2B1A8
    • 400#F78678
    • 500#FD493E
    • 600#D6312A
    • 700#A82A23
    • 800#7B221C
    • 900#401612
    status-info--p-status-info-*
    • 0#FCFDFD
    • 50#F4F7F9
    • 100#E4F0F5
    • 200#C5E2F0
    • 300#86CAEA
    • 400#4CB0DC
    • 500#3595BD
    • 600#277A9D
    • 700#21607A
    • 800#1B475A
    • 900#12262F

    Text

    17 tokens
    Body copy, headings and inline message text.
    • --text-primaryvar(--p-neutral-800)#393740

      Body copy, headings, food names

      9.89:1 · PASS 4.5on --surface-card
    • --text-secondaryvar(--p-neutral-700)#4C4A51

      Supporting copy, metadata, helper text (Figma body colour #4C4A51)

      7.38:1 · PASS 4.5on --surface-card
    • --text-tertiaryvar(--p-neutral-600)#64626B

      Timestamps, counters, least important text

      5.07:1 · PASS 4.5on --surface-card
    • --text-placeholdervar(--p-green-700)#5D6B28

      Input placeholders — the Figma green, stepped darker to clear 4.5:1 on the field surface

      4.93:1 · PASS 4.5on --surface-card
    • --text-disabledvar(--p-neutral-500)#817A81

      Text inside disabled controls

      3.53:1 · exempton --surface-cardDisabled content is AA-exempt (SC 1.4.3); the project floor of 3:1 still applies.
    • --text-inversevar(--p-neutral-0)#FFFFFF

      Text on --surface-inverse (toasts, tooltips)

      11.70:1 · PASS 4.5on --surface-inverse
    • --text-inverse-secondaryvar(--p-neutral-300)#BAAEAD

      De-emphasised text on --surface-inverse — the flat system separates overlays with the inverse surface, not a shadow (D-068)

      1.82:1 · FAIL 4.5on --surface-card
    • --text-brandvar(--p-green-800)#4C5822

      Brand-voiced headings, the "Picky eaters" wordmark

      6.51:1 · PASS 4.5on --surface-card
    • --text-brand-displayvar(--p-green-500)#7E9334

      LARGE BOLD DISPLAY TEXT ONLY (>=24px/700, AA 3:1) — the lighter Figma title green. Never body copy, labels or icons: use --text-brand (D-072)

      2.91:1 · FAIL 4.5on --surface-card
    • --text-brand-display-on-surfacevar(--p-green-600)#6D7F2E

      LARGE BOLD DISPLAY TEXT ONLY, on a #F0EBE8 surface (landing plate bands): 3.76:1 there, where --text-brand-display is 2.91:1. Never body copy

      3.76:1 · FAIL 4.5on --surface-card
    • --text-on-brandvar(--p-yellow-900)#3F2A04

      Text on the brand gold fill (primary CTA, Add button)

      9.55:1 · PASS 4.5on --action-primary-bg
    • --text-linkvar(--p-green-800)#4C5822

      Inline links

      6.51:1 · PASS 4.5on --surface-card
    • --text-link-hovervar(--p-green-900)#1D2C10

      Hovered / pressed inline link

      12.50:1 · PASS 4.5on --surface-card
    • --text-successvar(--p-status-success-700)#20653B

      Inline success message text

      6.50:1 · PASS 4.5on --status-success-bg
    • --text-warningvar(--p-status-warning-800)#563E16

      Inline warning message text

      9.29:1 · PASS 4.5on --status-warning-bg
    • --text-errorvar(--p-status-error-700)#A82A23

      Inline validation error text

      6.43:1 · PASS 4.5on --status-error-bg
    • --text-infovar(--p-status-info-700)#21607A

      Inline informational text

      6.47:1 · PASS 4.5on --status-info-bg

    Icon

    7 tokens
    UI icon tones (the `tone` prop of <Icon>).
    • --icon-primaryvar(--p-neutral-800)#393740

      Default UI icon tone

      9.89:1 · PASS 3.0on --surface-card
    • --icon-secondaryvar(--p-neutral-700)#4C4A51

      Secondary / affordance icons (chevrons)

      7.38:1 · PASS 3.0on --surface-card
    • --icon-tertiaryvar(--p-neutral-600)#64626B

      Faint decorative icons

      5.07:1 · PASS 3.0on --surface-card
    • --icon-disabledvar(--p-neutral-500)#817A81

      Icons in disabled controls

      3.53:1 · exempton --surface-cardDisabled content is AA-exempt (SC 1.4.3); the project floor of 3:1 still applies.
    • --icon-inversevar(--p-neutral-0)#FFFFFF

      Icons on --surface-inverse

      11.70:1 · PASS 3.0on --surface-inverse
    • --icon-brandvar(--p-green-800)#4C5822

      Brand-tinted icons, active nav glyph

      6.51:1 · PASS 3.0on --surface-card
    • --icon-on-brandvar(--p-yellow-900)#3F2A04

      Glyph on the gold Add button / primary CTA

      9.55:1 · PASS 3.0on --action-primary-bg

    Border

    6 tokens
    Component boundaries. `--border-default` is the 3:1 workhorse.
    • --border-subtlevar(--p-neutral-100)#F0EBE8

      Decorative dividers, list separators

      1.00:1 · exempton --surface-cardDecorative divider — SC 1.4.11 covers boundaries needed to identify a component; those use --border-default.
    • --border-defaultvar(--p-neutral-500)#817A81

      Input / card / chip boundary (3:1)

      3.53:1 · PASS 3.0on --surface-card
    • --border-strongvar(--p-neutral-700)#4C4A51

      Selected or emphasised boundary

      7.38:1 · PASS 3.0on --surface-card
    • --border-brandvar(--p-green-800)#4C5822

      Brand-selected boundary

      6.51:1 · PASS 3.0on --surface-card
    • --border-errorvar(--p-status-error-700)#A82A23

      Invalid input boundary

      5.88:1 · PASS 3.0on --surface-card
    • --border-inversevar(--p-neutral-0)#FFFFFF

      Boundary on --surface-inverse

      11.70:1 · PASS 3.0on --surface-inverse

    Surface

    14 tokens
    Neutral surfaces only (D-011). Dark elevates by LIGHTENING.
    • --surface-appvar(--p-neutral-0)#FFFFFF

      App background — step 0 of the borderless surface model

      background / geometry token — no foreground ratio
    • --surface-cardvar(--p-neutral-100)#F0EBE8

      Card / sheet / list-row background — step 1

      background / geometry token — no foreground ratio
    • --surface-card-hovervar(--p-neutral-150)#E2DBD6

      Card hover, inside @media (hover:hover)

      background / geometry token — no foreground ratio
    • --surface-sheetvar(--p-neutral-100)#F0EBE8

      Bottom sheets, dialogs — step 1

      background / geometry token — no foreground ratio
    • --surface-navvar(--p-neutral-100)#F0EBE8

      Bottom-nav pill (Figma #F0EBE8) — step 1

      background / geometry token — no foreground ratio
    • --surface-controlvar(--p-neutral-200)#D4CBC5

      Control sitting INSIDE a step-1 surface (Figma neutral-ctas) — step 2

      background / geometry token — no foreground ratio
    • --surface-control-hovervar(--p-neutral-250)#C7BCB9

      Control hover

      background / geometry token — no foreground ratio
    • --surface-control-pressedvar(--p-neutral-300)#BAAEAD

      Control pressed. Its label must be --control-fg (--text-primary, 5.43:1); --text-secondary is 4.05:1 here

      background / geometry token — no foreground ratio
    • --surface-raisedvar(--p-neutral-0)#FFFFFF

      Highest elevation (menus, popovers). Dark elevates by LIGHTENING, not by shadow

      background / geometry token — no foreground ratio
    • --surface-sunkenvar(--p-neutral-100)#F0EBE8

      Inset wells, empty states

      background / geometry token — no foreground ratio
    • --surface-inversevar(--p-neutral-800)#393740

      Toasts, tooltips

      background / geometry token — no foreground ratio
    • --surface-tilevar(--p-neutral-100)#F0EBE8

      Food-icon tile background — step 1

      background / geometry token — no foreground ratio
    • --surface-fieldvar(--p-neutral-100)#F0EBE8

      Text-entry ground (input, textarea, search) — step 1, never a stroke

      background / geometry token — no foreground ratio
    • --surface-field-hovervar(--p-neutral-150)#E2DBD6

      Hovered / focused field — the one-step shift that replaces the focus ring on text entry

      background / geometry token — no foreground ratio

    List surface

    5 tokens
    The ONE yellow family — personal food-list context only (D-011).
    • --surface-listvar(--p-yellow-100)#FEF1C8

      Background of the personal food-list context ONLY

      background / geometry token — no foreground ratio
    • --surface-list-subtlevar(--p-yellow-50)#FFF7DE

      Alternating rows / nested blocks inside the list

      background / geometry token — no foreground ratio
    • --surface-list-strongvar(--p-yellow-300)#F5D673

      Emphasised list header / active list chip

      background / geometry token — no foreground ratio
    • --surface-list-bordertransparenttransparent

      Kept for parity; the borderless system draws no rule inside the list card

      1.00:1 · FAIL 3.0on --surface-list
    • --surface-list-fgvar(--p-yellow-900)#3F2A04

      Text and icons on the list surfaces

      12.06:1 · PASS 4.5on --surface-list

    Action

    28 tokens
    Button variants: primary / secondary / tertiary / destructive.
    • --action-primary-bgvar(--p-yellow-300)#F5D673

      Primary CTA (Figma gold) — resting fill

      background / geometry token — no foreground ratio
    • --action-primary-bg-hovervar(--p-yellow-400)#F0C030

      Primary CTA (Figma gold) — hover fill

      background / geometry token — no foreground ratio
    • --action-primary-bg-pressedvar(--p-yellow-500)#CB9F26

      Primary CTA (Figma gold) — :active fill

      background / geometry token — no foreground ratio
    • --action-primary-bg-disabledvar(--p-neutral-100)#F0EBE8

      Primary CTA (Figma gold) — disabled fill

      background / geometry token — no foreground ratio
    • --action-primary-fgvar(--p-yellow-900)#3F2A04

      Primary CTA (Figma gold) — label + icon

      9.55:1 · PASS 4.5on --action-primary-bg
    • --action-primary-fg-disabledvar(--p-neutral-500)#817A81

      Primary CTA (Figma gold) — disabled label

      3.53:1 · exempton --action-primary-bg-disabledDisabled content is AA-exempt (SC 1.4.3); the project floor of 3:1 still applies.
    • --action-primary-bordertransparenttransparent

      Kept for parity — the borderless system draws no button stroke

      1.00:1 · FAIL 3.0on --surface-card
    • --action-secondary-bgvar(--p-neutral-200)#D4CBC5

      Secondary button — filled TONAL resting fill (step 2)

      background / geometry token — no foreground ratio
    • --action-secondary-bg-hovervar(--p-neutral-250)#C7BCB9

      Secondary button — hover fill

      background / geometry token — no foreground ratio
    • --action-secondary-bg-pressedvar(--p-neutral-300)#BAAEAD

      Secondary button — :active fill

      background / geometry token — no foreground ratio
    • --action-secondary-bg-disabledvar(--p-neutral-100)#F0EBE8

      Secondary button — disabled fill

      background / geometry token — no foreground ratio
    • --action-secondary-fgvar(--p-neutral-800)#393740

      Secondary button — label + icon

      7.33:1 · PASS 4.5on --action-secondary-bg
    • --action-secondary-fg-disabledvar(--p-neutral-500)#817A81

      Secondary button — disabled label

      3.53:1 · exempton --action-secondary-bg-disabledDisabled content is AA-exempt (SC 1.4.3); the project floor of 3:1 still applies.
    • --action-secondary-bordertransparenttransparent

      Kept for parity — secondary is tonal, not outlined

      1.00:1 · FAIL 3.0on --surface-card
    • --action-tertiary-bgtransparenttransparent

      Tertiary / ghost button — resting fill

      background / geometry token — no foreground ratio
    • --action-tertiary-bg-hovervar(--p-neutral-100)#F0EBE8

      Tertiary / ghost button — hover fill

      background / geometry token — no foreground ratio
    • --action-tertiary-bg-pressedvar(--p-neutral-200)#D4CBC5

      Tertiary / ghost button — :active fill

      background / geometry token — no foreground ratio
    • --action-tertiary-bg-disabledtransparenttransparent

      Tertiary / ghost button — disabled fill

      background / geometry token — no foreground ratio
    • --action-tertiary-fgvar(--p-green-800)#4C5822

      Tertiary / ghost button — label + icon

      7.70:1 · PASS 4.5on --action-tertiary-bg
    • --action-tertiary-fg-disabledvar(--p-neutral-500)#817A81

      Tertiary / ghost button — disabled label

      4.17:1 · exempton --action-tertiary-bg-disabledDisabled content is AA-exempt (SC 1.4.3); the project floor of 3:1 still applies.
    • --action-tertiary-bordertransparenttransparent

      Tertiary / ghost button — boundary

      1.00:1 · exempton --surface-cardThe ghost button has no boundary by design — the token is transparent.
    • --action-destructive-bgvar(--p-status-error-700)#A82A23

      Destructive action (delete a list item, leave an event) — resting fill

      background / geometry token — no foreground ratio
    • --action-destructive-bg-hovervar(--p-status-error-800)#7B221C

      Destructive action (delete a list item, leave an event) — hover fill

      background / geometry token — no foreground ratio
    • --action-destructive-bg-pressedvar(--p-status-error-900)#401612

      Destructive action (delete a list item, leave an event) — :active fill

      background / geometry token — no foreground ratio
    • --action-destructive-bg-disabledvar(--p-neutral-100)#F0EBE8

      Destructive action (delete a list item, leave an event) — disabled fill

      background / geometry token — no foreground ratio
    • --action-destructive-fgvar(--p-neutral-0)#FFFFFF

      Destructive action (delete a list item, leave an event) — label + icon

      6.95:1 · PASS 4.5on --action-destructive-bg
    • --action-destructive-fg-disabledvar(--p-neutral-500)#817A81

      Destructive action (delete a list item, leave an event) — disabled label

      3.53:1 · exempton --action-destructive-bg-disabledDisabled content is AA-exempt (SC 1.4.3); the project floor of 3:1 still applies.
    • --action-destructive-bordertransparenttransparent

      Kept for parity — the borderless system draws no button stroke

      1.00:1 · FAIL 3.0on --surface-card

    Focus / disabled / overlay

    8 tokens
    Focus ring geometry + colour, generic disabled, scrim.
    • --focus-ringvar(--p-green-800)#4C5822

      :focus-visible outline colour

      7.70:1 · PASS 3.0on --surface-app
    • --focus-ring-offset2px2px

      Outline offset — pushes the ring onto the parent SURFACE

      background / geometry token — no foreground ratio
    • --focus-ring-width2px2px

      Outline width

      background / geometry token — no foreground ratio
    • --caret-colorvar(--p-green-800)#4C5822

      Caret on text-entry controls — their ONLY focus indicator (WCAG 2.4.7)

      background / geometry token — no foreground ratio
    • --disabled-bgvar(--p-neutral-100)#F0EBE8

      Generic disabled fill

      background / geometry token — no foreground ratio
    • --disabled-fgvar(--p-neutral-500)#817A81

      Generic disabled content

      3.53:1 · PASS 3.0on --disabled-bg
    • --disabled-bordertransparenttransparent

      Kept for parity — disabled reads through fill + text tone

      1.00:1 · exempton --surface-cardA disabled boundary is never the sole carrier of meaning (aria-disabled + label).
    • --overlay-scrimrgba(35,33,40,0.56)rgba(35,33,40,0.56)

      Scrim behind sheets and dialogs

      background / geometry token — no foreground ratio

    Navigation

    10 tokens
    Bottom nav pill + the gold Add button (D-028).
    • --nav-bgvar(--p-neutral-100)#F0EBE8

      Nav pill fill (Figma #F0EBE8)

      background / geometry token — no foreground ratio
    • --nav-bordertransparenttransparent

      Kept for parity — the nav pill is borderless in both themes

      background / geometry token — no foreground ratio
    • --nav-shadow0 8px 24px rgba(35,33,40,0.22)0 8px 24px rgba(35,33,40,0.22)

      Nav elevation (D-028, pixel-sampled from Figma)

      background / geometry token — no foreground ratio
    • --nav-item-fgvar(--p-green-700)#5D6B28

      Inactive nav icon + 11px label. Lightened one step by D-074 so it sits a real luminance step (2.53:1 light / 2.29:1 dark) away from the active tone

      4.93:1 · PASS 4.5on --nav-bg
    • --nav-item-fg-activevar(--p-green-900)#1D2C10

      Active nav icon + label — DARKER GREEN, no pill (D-061 + D-074). 12.50:1 light / 11.94:1 dark on the pill, and 2.53:1 / 2.29:1 away from the inactive tone, so the state survives greyscale; still always paired with a fill-weight icon and a semibold label

      12.50:1 · PASS 4.5on --nav-bg
    • --nav-item-bg-activetransparenttransparent

      Kept for parity — D-061 removed the selected pill entirely

      1.00:1 · exempton --nav-bgTint only — the active item also carries aria-current, a 4.5:1 label color and the label itself.
    • --nav-add-bgvar(--p-yellow-300)#F5D673

      Add (+) button fill — brand gold in BOTH themes

      background / geometry token — no foreground ratio
    • --nav-add-bg-pressedvar(--p-yellow-500)#CB9F26

      Add button :active

      background / geometry token — no foreground ratio
    • --nav-add-fgvar(--p-yellow-900)#3F2A04

      Plus glyph on the Add button

      9.55:1 · PASS 4.5on --nav-add-bg
    • --nav-add-shadow0 4px 12px rgba(35,33,40,0.28)0 4px 12px rgba(35,33,40,0.28)

      Add button elevation

      background / geometry token — no foreground ratio

    Status

    24 tokens
    System feedback: success / warning / error / info.
    • --status-success-bgvar(--p-status-success-50)#F0F8F2

      Success banner / toast tint

      background / geometry token — no foreground ratio
    • --status-success-fgvar(--p-status-success-700)#20653B

      Success text on the tint and on cards

      6.50:1 · PASS 4.5on --status-success-bg
    • --status-success-bordervar(--p-status-success-600)#25814B

      Success banner boundary — also what makes the solid identifiable

      4.11:1 · PASS 3.0on --surface-card
    • --status-success-iconvar(--p-status-success-700)#20653B

      Success icon — Phosphor CheckCircle (duotone)

      6.50:1 · PASS 3.0on --status-success-bg
    • --status-success-solidvar(--p-status-success-600)#25814B

      Success solid (badge, dot, progress)

      background / geometry token — no foreground ratio
    • --status-success-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the Success solid

      background / geometry token — no foreground ratio
    • --status-warning-bgvar(--p-status-warning-50)#F9F6F1

      Warning banner / toast tint

      background / geometry token — no foreground ratio
    • --status-warning-fgvar(--p-status-warning-700)#76531B

      Warning text on the tint and on cards

      6.44:1 · PASS 4.5on --status-warning-bg
    • --status-warning-bordervar(--p-status-warning-600)#96691E

      Warning banner boundary — also what makes the solid identifiable

      4.09:1 · PASS 3.0on --surface-card
    • --status-warning-iconvar(--p-status-warning-700)#76531B

      Warning icon — Phosphor Warning (duotone)

      6.44:1 · PASS 3.0on --status-warning-bg
    • --status-warning-solidvar(--p-status-warning-300)#EFB761

      Warning solid (badge, dot, progress)

      background / geometry token — no foreground ratio
    • --status-warning-on-solidvar(--p-status-warning-900)#2D2210

      Content on the Warning solid

      background / geometry token — no foreground ratio
    • --status-error-bgvar(--p-status-error-50)#F9F5F5

      Error banner / toast tint

      background / geometry token — no foreground ratio
    • --status-error-fgvar(--p-status-error-700)#A82A23

      Error text on the tint and on cards

      6.43:1 · PASS 4.5on --status-error-bg
    • --status-error-bordervar(--p-status-error-600)#D6312A

      Error banner boundary — also what makes the solid identifiable

      4.10:1 · PASS 3.0on --surface-card
    • --status-error-iconvar(--p-status-error-700)#A82A23

      Error icon — Phosphor XCircle (duotone)

      6.43:1 · PASS 3.0on --status-error-bg
    • --status-error-solidvar(--p-status-error-700)#A82A23

      Error solid (badge, dot, progress)

      background / geometry token — no foreground ratio
    • --status-error-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the Error solid

      background / geometry token — no foreground ratio
    • --status-info-bgvar(--p-status-info-50)#F4F7F9

      Info banner / toast tint

      background / geometry token — no foreground ratio
    • --status-info-fgvar(--p-status-info-700)#21607A

      Info text on the tint and on cards

      6.47:1 · PASS 4.5on --status-info-bg
    • --status-info-bordervar(--p-status-info-600)#277A9D

      Info banner boundary — also what makes the solid identifiable

      4.07:1 · PASS 3.0on --surface-card
    • --status-info-iconvar(--p-status-info-700)#21607A

      Info icon — Phosphor Info (duotone)

      6.47:1 · PASS 3.0on --status-info-bg
    • --status-info-solidvar(--p-status-info-600)#277A9D

      Info solid (badge, dot, progress)

      background / geometry token — no foreground ratio
    • --status-info-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the Info solid

      background / geometry token — no foreground ratio

    Severity

    48 tokens
    The 7 product severity levels. Severity is not taste (/CLAUDE.md).
    • --severity-allergy-bgvar(--p-severity-allergy-50)#F9F5F6

      Allergy chip / row tint (medical — blocks)

      background / geometry token — no foreground ratio
    • --severity-allergy-fgvar(--p-severity-allergy-700)#A22E46

      Allergy text on the tint, on cards and on the list surface

      6.44:1 · PASS 4.5on --severity-allergy-bg
    • --severity-allergy-bordervar(--p-severity-allergy-600)#CF3759

      Allergy chip boundary — also what makes the solid identifiable

      4.09:1 · PASS 3.0on --surface-card
    • --severity-allergy-iconvar(--p-severity-allergy-700)#A22E46

      Allergy icon — Phosphor Prohibit (duotone)

      6.44:1 · PASS 3.0on --severity-allergy-bg
    • --severity-allergy-solidvar(--p-severity-allergy-800)#772434

      Allergy solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-allergy-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the Allergy solid

      background / geometry token — no foreground ratio
    • --severity-intolerance-bgvar(--p-severity-intolerance-50)#FAF6F0

      Intolerance chip / row tint (medical — warns)

      background / geometry token — no foreground ratio
    • --severity-intolerance-fgvar(--p-severity-intolerance-700)#6E561B

      Intolerance text on the tint, on cards and on the list surface

      6.48:1 · PASS 4.5on --severity-intolerance-bg
    • --severity-intolerance-bordervar(--p-severity-intolerance-600)#8D6E1E

      Intolerance chip boundary — also what makes the solid identifiable

      4.05:1 · PASS 3.0on --surface-card
    • --severity-intolerance-iconvar(--p-severity-intolerance-700)#6E561B

      Intolerance icon — Phosphor Warning (duotone)

      6.48:1 · PASS 3.0on --severity-intolerance-bg
    • --severity-intolerance-solidvar(--p-severity-intolerance-500)#AB862B

      Intolerance solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-intolerance-on-solidvar(--p-severity-intolerance-900)#2B2310

      Content on the Intolerance solid

      background / geometry token — no foreground ratio
    • --severity-arfid-bgvar(--p-severity-arfid-50)#F6F7F9

      ARFID chip / row tint (neutral info)

      background / geometry token — no foreground ratio
    • --severity-arfid-fgvar(--p-severity-arfid-700)#2F4BCD

      ARFID text on the tint, on cards and on the list surface

      6.51:1 · PASS 4.5on --severity-arfid-bg
    • --severity-arfid-bordervar(--p-severity-arfid-600)#4A6ADA

      ARFID chip boundary — also what makes the solid identifiable

      4.07:1 · PASS 3.0on --surface-card
    • --severity-arfid-iconvar(--p-severity-arfid-700)#2F4BCD

      ARFID icon — Phosphor Info (duotone)

      6.51:1 · PASS 3.0on --severity-arfid-bg
    • --severity-arfid-solidvar(--p-severity-arfid-400)#8EA4E0

      ARFID solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-arfid-on-solidvar(--p-severity-arfid-900)#14204D

      Content on the ARFID solid

      background / geometry token — no foreground ratio
    • --severity-hate-bgvar(--p-severity-hate-50)#F7F6F9

      Hate chip / row tint (preference — avoid)

      background / geometry token — no foreground ratio
    • --severity-hate-fgvar(--p-severity-hate-700)#6145B5

      Hate text on the tint, on cards and on the list surface

      6.44:1 · PASS 4.5on --severity-hate-bg
    • --severity-hate-bordervar(--p-severity-hate-600)#795CD9

      Hate chip boundary — also what makes the solid identifiable

      4.09:1 · PASS 3.0on --surface-card
    • --severity-hate-iconvar(--p-severity-hate-700)#6145B5

      Hate icon — Phosphor ThumbsDown (duotone)

      6.44:1 · PASS 3.0on --severity-hate-bg
    • --severity-hate-solidvar(--p-severity-hate-700)#6145B5

      Hate solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-hate-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the Hate solid

      background / geometry token — no foreground ratio
    • --severity-dislike-bgvar(--p-severity-dislike-50)#EEF8F7

      Dislike chip / row tint (preference — avoid)

      background / geometry token — no foreground ratio
    • --severity-dislike-fgvar(--p-severity-dislike-700)#21645E

      Dislike text on the tint, on cards and on the list surface

      6.36:1 · PASS 4.5on --severity-dislike-bg
    • --severity-dislike-bordervar(--p-severity-dislike-600)#277F78

      Dislike chip boundary — also what makes the solid identifiable

      4.04:1 · PASS 3.0on --surface-card
    • --severity-dislike-iconvar(--p-severity-dislike-700)#21645E

      Dislike icon — Phosphor MinusCircle (duotone)

      6.36:1 · PASS 3.0on --severity-dislike-bg
    • --severity-dislike-solidvar(--p-severity-dislike-600)#277F78

      Dislike solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-dislike-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the Dislike solid

      background / geometry token — no foreground ratio
    • --severity-like-bgvar(--p-severity-like-50)#EFF9F3

      Like chip / row tint (preference — info)

      background / geometry token — no foreground ratio
    • --severity-like-fgvar(--p-severity-like-700)#206643

      Like text on the tint, on cards and on the list surface

      6.41:1 · PASS 4.5on --severity-like-bg
    • --severity-like-bordervar(--p-severity-like-600)#268255

      Like chip boundary — also what makes the solid identifiable

      4.03:1 · PASS 3.0on --surface-card
    • --severity-like-iconvar(--p-severity-like-700)#206643

      Like icon — Phosphor ThumbsUp (duotone)

      6.41:1 · PASS 3.0on --severity-like-bg
    • --severity-like-solidvar(--p-severity-like-300)#6BD59C

      Like solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-like-on-solidvar(--p-severity-like-900)#12281C

      Content on the Like solid

      background / geometry token — no foreground ratio
    • --severity-favorite-bgvar(--p-severity-favorite-50)#F9F6F8

      Favourite chip / row tint (preference — info)

      background / geometry token — no foreground ratio
    • --severity-favorite-fgvar(--p-severity-favorite-700)#893C80

      Favourite text on the tint, on cards and on the list surface

      6.48:1 · PASS 4.5on --severity-favorite-bg
    • --severity-favorite-bordervar(--p-severity-favorite-600)#AF4BA4

      Favourite chip boundary — also what makes the solid identifiable

      4.07:1 · PASS 3.0on --surface-card
    • --severity-favorite-iconvar(--p-severity-favorite-700)#893C80

      Favourite icon — Phosphor Star (duotone)

      6.48:1 · PASS 3.0on --severity-favorite-bg
    • --severity-favorite-solidvar(--p-severity-favorite-200)#EED6EA

      Favourite solid — legend swatch, dot, badge (same hex in both themes)

      background / geometry token — no foreground ratio
    • --severity-favorite-on-solidvar(--p-severity-favorite-900)#341A31

      Content on the Favourite solid

      background / geometry token — no foreground ratio
    • --severity-intolerance-bg-strongvar(--p-severity-intolerance-300)#E3BC60

      Strong tinted fill — tier 2 of the borderless weight ladder

      background / geometry token — no foreground ratio
    • --severity-intolerance-fg-strongvar(--p-severity-intolerance-900)#2B2310

      Text + glyph on the strong intolerance fill

      background / geometry token — no foreground ratio
    • --severity-hate-bg-mediumvar(--p-severity-hate-200)#DDDBED

      Light tinted fill — tier 3 of the ladder

      background / geometry token — no foreground ratio
    • --severity-hate-fg-mediumvar(--p-severity-hate-800)#473484

      Text + glyph on the medium hate fill

      background / geometry token — no foreground ratio
    • --severity-dislike-bg-mediumvar(--p-severity-dislike-200)#91ECE4

      Light tinted fill — tier 3 of the ladder

      background / geometry token — no foreground ratio
    • --severity-dislike-fg-mediumvar(--p-severity-dislike-800)#1B4946

      Text + glyph on the medium dislike fill

      background / geometry token — no foreground ratio

    Compatibility

    22 tokens
    Host-facing per-dish aggregate traffic light. Aggregate only.
    • --compat-blocked-bgvar(--p-severity-allergy-50)#F9F5F6

      Per-dish traffic light "Blocked" tint (maps to --severity-allergy)

      background / geometry token — no foreground ratio
    • --compat-blocked-fgvar(--p-severity-allergy-700)#A22E46

      "Blocked" label text

      6.44:1 · PASS 4.5on --compat-blocked-bg
    • --compat-blocked-bordervar(--p-severity-allergy-600)#CF3759

      "Blocked" boundary

      4.09:1 · PASS 3.0on --surface-card
    • --compat-blocked-iconvar(--p-severity-allergy-700)#A22E46

      "Blocked" icon — Phosphor Prohibit (duotone)

      6.44:1 · PASS 3.0on --compat-blocked-bg
    • --compat-warning-bgvar(--p-severity-intolerance-50)#FAF6F0

      Per-dish traffic light "Caution" tint (maps to --severity-intolerance)

      background / geometry token — no foreground ratio
    • --compat-warning-fgvar(--p-severity-intolerance-700)#6E561B

      "Caution" label text

      6.48:1 · PASS 4.5on --compat-warning-bg
    • --compat-warning-bordervar(--p-severity-intolerance-600)#8D6E1E

      "Caution" boundary

      4.05:1 · PASS 3.0on --surface-card
    • --compat-warning-iconvar(--p-severity-intolerance-700)#6E561B

      "Caution" icon — Phosphor Warning (duotone)

      6.48:1 · PASS 3.0on --compat-warning-bg
    • --compat-avoid-bgvar(--p-severity-hate-50)#F7F6F9

      Per-dish traffic light "Better avoid" tint (maps to --severity-hate)

      background / geometry token — no foreground ratio
    • --compat-avoid-fgvar(--p-severity-hate-700)#6145B5

      "Better avoid" label text

      6.44:1 · PASS 4.5on --compat-avoid-bg
    • --compat-avoid-bordervar(--p-severity-hate-600)#795CD9

      "Better avoid" boundary

      4.09:1 · PASS 3.0on --surface-card
    • --compat-avoid-iconvar(--p-severity-hate-700)#6145B5

      "Better avoid" icon — Phosphor ThumbsDown (duotone)

      6.44:1 · PASS 3.0on --compat-avoid-bg
    • --compat-ok-bgvar(--p-status-success-50)#F0F8F2

      Per-dish traffic light "All good" tint (maps to --status-success)

      background / geometry token — no foreground ratio
    • --compat-ok-fgvar(--p-status-success-700)#20653B

      "All good" label text

      6.50:1 · PASS 4.5on --compat-ok-bg
    • --compat-ok-bordervar(--p-status-success-600)#25814B

      "All good" boundary

      4.11:1 · PASS 3.0on --surface-card
    • --compat-ok-iconvar(--p-status-success-700)#20653B

      "All good" icon — Phosphor CheckCircle (duotone)

      6.50:1 · PASS 3.0on --compat-ok-bg
    • --compat-blocked-solidvar(--p-severity-allergy-800)#772434

      "Blocked" solid fill — the one state that stops a dish

      background / geometry token — no foreground ratio
    • --compat-blocked-on-solidvar(--p-neutral-0)#FFFFFF

      Content on the blocked solid

      background / geometry token — no foreground ratio
    • --compat-warning-bg-strongvar(--p-severity-intolerance-300)#E3BC60

      "Caution" strong tinted fill

      background / geometry token — no foreground ratio
    • --compat-warning-fg-strongvar(--p-severity-intolerance-900)#2B2310

      Text + glyph on the caution fill

      background / geometry token — no foreground ratio
    • --compat-avoid-bg-mediumvar(--p-severity-hate-200)#DDDBED

      "Better avoid" light tinted fill

      background / geometry token — no foreground ratio
    • --compat-avoid-fg-mediumvar(--p-severity-hate-800)#473484

      Text + glyph on the avoid fill

      background / geometry token — no foreground ratio

    Food-icon roles

    17 tokens
    Food glyph fills (food-icon-kit §4.1). Never a hex in a glyph.
    • --icon-food-primary-lightvar(--p-yellow-300)#F5D673

      Baked / fried / grain highlight

      1.20:1 · exempton --surface-cardEnclosed-only role (food-icon-kit §4.3) — it may never define the outer silhouette.
    • --icon-food-primary-basevar(--p-yellow-700)#836012

      Baked / fried / grain body — silhouette-capable

      4.87:1 · PASS 3.0on --surface-card
    • --icon-food-primary-darkvar(--p-yellow-800)#60440A

      Baked / fried / grain shade

      7.62:1 · PASS 3.0on --surface-card
    • --icon-food-secondary-basevar(--p-red-500)#FF0C00

      Sauce / tomato / berry — kept at the brand red hue, far from severity crimson

      3.35:1 · PASS 3.0on --surface-card
    • --icon-food-secondary-darkvar(--p-red-700)#A22824

      Sauce / meat shade

      6.20:1 · PASS 3.0on --surface-card
    • --icon-food-tertiary-base#8C6143#8C6143

      Meat brown / chocolate / crust

      4.55:1 · PASS 3.0on --surface-card
    • --icon-food-tertiary-dark#6B4124#6B4124

      Deep crust / coffee

      7.38:1 · PASS 3.0on --surface-card
    • --icon-food-green-basevar(--p-green-600)#6D7F2E

      Leaves, herbs, vegetables

      3.76:1 · PASS 3.0on --surface-card
    • --icon-food-green-darkvar(--p-green-800)#4C5822

      Leaf shade

      6.51:1 · PASS 3.0on --surface-card
    • --icon-food-cream-basevar(--p-yellow-100)#FEF1C8

      Dairy, egg white, rice, dough, fish flesh

      1.05:1 · exempton --surface-cardEnclosed-only role (food-icon-kit §4.3) — it may never define the outer silhouette.
    • --icon-food-cream-darkvar(--p-yellow-200)#F9E39F

      Cream shade

      1.07:1 · exempton --surface-cardEnclosed-only role (food-icon-kit §4.3) — it may never define the outer silhouette.
    • --icon-container-lightvar(--p-neutral-200)#D4CBC5

      Ceramic / paper highlight

      1.35:1 · exempton --surface-cardEnclosed-only role (food-icon-kit §4.3) — it may never define the outer silhouette.
    • --icon-container-basevar(--p-neutral-600)#64626B

      Ceramic / paper / metal body — silhouette-capable

      5.07:1 · PASS 3.0on --surface-card
    • --icon-container-darkvar(--p-neutral-800)#393740

      Container shade / cast iron

      9.89:1 · PASS 3.0on --surface-card
    • --icon-container-accentvar(--p-green-600)#6D7F2E

      One optional accent per icon — never a severity hue

      3.76:1 · PASS 3.0on --surface-card
    • --icon-shadowvar(--p-neutral-900)#222127

      Internal shadows and separation slivers

      background / geometry token — no foreground ratio
    • --icon-monovar(--p-neutral-700)#4C4A51

      Single-colour glyph mode (= --text-secondary)

      7.38:1 · PASS 3.0on --surface-card

    Typography

    14 roles
    Uber's type scale verbatim, with Bricolage Grotesque on the display roles and Open Sans on everything else. Each role ships as one Tailwind utility that sets size, line-height, weight and tracking together.
    text-display-xxl--type-display-xxl-* · Bricolage Grotesque · 52px/64px · 700 · -0.03em

    Dinner works when nobody has to explain themselves.

    Hero headline, one per screen at most.
    text-display-xl--type-display-xl-* · Bricolage Grotesque · 36px/44px · 700 · -0.03em

    Dinner works when nobody has to explain themselves.

    Page section headline.
    text-display-lg--type-display-lg-* · Bricolage Grotesque · 32px/40px · 700 · -0.03em

    Dinner works when nobody has to explain themselves.

    Page title (Figma 'Picky eaters' title).
    text-display-md--type-display-md-* · Bricolage Grotesque · 24px/32px · 700 · -0.03em

    Dinner works when nobody has to explain themselves.

    Card title, sheet title.
    text-display-sm--type-display-sm-* · Bricolage Grotesque · 20px/28px · 600 · -0.03em

    Dinner works when nobody has to explain themselves.

    Sub-card heading, list group heading.
    text-body-lg--type-body-lg-* · Open Sans · 18px/24px · 500 · -0.01em

    Dinner works when nobody has to explain themselves.

    Lead paragraph, large body.
    text-body-md--type-body-md-* · Open Sans · 16px/24px · 400 · -0.01em

    Dinner works when nobody has to explain themselves.

    Default paragraph body.
    text-body-md-strong--type-body-md-strong-* · Open Sans · 16px/20px · 600 · -0.01em

    Dinner works when nobody has to explain themselves.

    Emphasised body, list row primary label.
    text-body-sm--type-body-sm-* · Open Sans · 14px/20px · 400 · -0.01em

    Dinner works when nobody has to explain themselves.

    Captions, secondary metadata, input text.
    text-body-sm-strong--type-body-sm-strong-* · Open Sans · 14px/16px · 600 · -0.01em

    Dinner works when nobody has to explain themselves.

    Chip / badge label, bold caption.
    text-caption--type-caption-* · Open Sans · 12px/20px · 400 · -0.01em

    Dinner works when nobody has to explain themselves.

    Fine print, helper text, timestamps.
    text-button-lg--type-button-lg-* · Open Sans · 18px/24px · 600 · -0.01em

    Dinner works when nobody has to explain themselves.

    Large CTA label.
    text-button-md--type-button-md-* · Open Sans · 16px/20px · 600 · -0.01em

    Dinner works when nobody has to explain themselves.

    Default button label.
    text-nav-label--type-nav-label-* · Open Sans · 11px/14px · 500 · -0.02em

    Dinner works when nobody has to explain themselves.

    Bottom-nav item label (Open Sans, 11px — documented deviation).

    Spacing

    Eight Uber steps, shown at true size. Larger gaps compose (32 + 16) rather than adding a ninth step.

    --space-xxs4px — Icon-to-label gap, dense chip padding.
    --space-xs6px — Sub-multiple used inside button padding.
    --space-sm8px — Tight stack inside a card (title/body/CTA).
    --space-md12px — Inline gap between sibling controls.
    --space-lg16px — Mobile gutter, compact form padding.
    --space-xl20px — Roomy control padding.
    --space-2xl24px — Card interior padding.
    --space-3xl32px — Section padding, desktop gutter, card-to-card rhythm.

    Radius

    Corner radii, from square through the card family to the pill.

    --radius-none · 0pxFull-bleed bands, raw image edges.
    --radius-md · 8pxForm inputs.
    --radius-lg · 12pxSecondary card chrome.
    --radius-xl · 16pxCanonical card radius (Uber base).
    --radius-2xl · 20pxFigma card radius — Picky Eaters info cards.
    --radius-pill-tab · 36pxSegmented tab pill.
    --radius-pill · 999pxEvery pill button and chip.
    --radius-full · 9999pxCircular icon containers and avatars.

    Elevation

    Shown at true shadow on the current theme's card surface. Dark deepens the shadow and lightens the surface — it never stacks black on black.

    --elevation-0Flat — the default; lean on surface contrast.
    --elevation-1Subtle drop for cards on light bands.
    --elevation-2Card drop for sheets and form cards.
    --elevation-3Pill float for floating buttons.

    Contextual control surface

    Not a flat token pair: --control-bg and --surface-self resolve against the nearest [data-surface] ancestor, so a chip, a field or a tonal button is the right tone on the white app ground AND on a surface without ever taking a prop. Surface containers (Card, Sheet, Dialog, field wrappers, the nav pill, tiles, GroupCard, InviteLinkCard) set the attribute; controls only read the token. The ladder alternates and caps at level 2, so two equal tones can never touch at any depth.

    Level 0 — on the app ground (:root)
    Hint box
    Level 1 — inside [data-surface="neutral"]
    Hint box
    Level 2+ — a surface inside a surface (capped)
    Hint box
    List — [data-surface="list"], the one yellow surface
    Hint box
    Resolved values per level, both themes.
    Level--surface-self--control-bghoverpressed--control-fg
    0 — on the app ground#FFFFFF / #211E26#F0EBE8 / #322F37#E2DBD6 / #3A3840#D4CBC5 / #413E47#393740 / #F7F5F3
    1 — inside a neutral surface#F0EBE8 / #322F37#D4CBC5 / #413E47#C7BCB9 / #4C4A51#BAAEAD / #64626B#393740 / #F7F5F3
    2+ — a surface inside a surface (capped)#FFFFFF / #413E47#F0EBE8 / #4C4A51#E2DBD6 / #64626B#D4CBC5 / #64626B#393740 / #F7F5F3
    list — the one yellow surface#FEF1C8 / #383014#F5D673 / #493F23#F0C030 / #60440A#CB9F26 / #60440A#3F2A04 / #F9E39F

    Chrome veil

    The bottom nav and the sticky tab header are the same recipe, mirrored: a veil of --surface-app at --chrome-veil-opacity over a --chrome-backdrop-blur backdrop, ramping over --chrome-veil-fade at the edge that meets the content. Tokens are shared so the two edges of the screen can never drift.

    Chrome & horizontal-scroll tokens.
    TokenValueUse
    --chrome-veil-opacity0.7Flat plateau of the veil, measured from Figma 2043:3533 (0% at the outer edge → 70% at 20px → flat 70%). Raised to 0.92 without backdrop-filter support and to 1 under prefers-reduced-transparency.
    --chrome-veil-fade20pxLength of the veil's ramp — --space-xl, a real step of the Uber scale.
    --chrome-backdrop-blur12pxBackground blur radius. PROVISIONAL — Vicente to confirm from Figma (D-065).
    --chrome-nav-height104pxRendered height of the bottom nav excluding the safe inset (20 + 64 + 20). The scroll region reserves it (pb-chrome-nav) so content can pass under the veil and the last row stay reachable.
    --hscroll-fade24pxEdge-fade ramp of an HScroll row — --space-2xl.

    Motion

    Durations and easings, replayable. Press the button to move the dots; press again to send them back.

    prefers-reduced-motion: reading…

    Every duration token collapses to 0.01ms and every transform-based transition is neutralized inside the global prefers-reduced-motion: reduce block in base.css. With the preference on, the dots below jump instead of sliding — that is the intended behavior, not a bug.

    --duration-instant · 75msState flips that must feel immediate (checkbox).
    --duration-fast · 150msHover / press feedback.
    --duration-normal · 200msDefault transition.
    --duration-slow · 300msSheet and dialog enter.
    --duration-slower · 500msLarge choreographed moves; use sparingly.
    --ease-standardcubic-bezier(0.2, 0, 0, 1) — Default for state changes.
    --ease-deceleratecubic-bezier(0, 0, 0, 1) — Entering elements.
    --ease-acceleratecubic-bezier(0.3, 0, 1, 1) — Exiting elements.
    --ease-emphasizedcubic-bezier(0.2, 0, 0, 1) — Hero / celebratory motion.

    Breakpoints, z-index and icon sizes

    7 reference tables
    The reference ladders. Never hand-write a z-index or a media query outside these.
    Breakpoints — Uber's ladder, used as Tailwind screens.
    TokenValueUse
    --bp-sm600pxMobile-large; chip rows gain horizontal scroll.
    --bp-md768pxTablet; 2-up grids begin.
    --bp-lg1120pxDesktop; full nav row.
    --bp-xl1136pxDesktop-large; container caps at ~1200px.
    Z-index — the stacking order of the whole app.
    TokenValueUse
    --z-base0Normal document flow.
    --z-sticky100In-CONTENT stickies: a list's group header, a table head.
    --z-chrome150Screen chrome: the sticky tab header. Above in-content stickies (which pass under it), below the nav.
    --z-nav200Bottom navigation bar.
    --z-sheet300Bottom sheets.
    --z-overlay400Scrims behind sheets and dialogs.
    --z-dialog500Modal dialogs.
    --z-toast600Toasts (must clear dialogs).
    --z-tooltip700Tooltips (always topmost).
    Icon sizes — the size prop of <Icon> and <FoodIcon>.
    TokenValueUse
    --icon-size-xs16pxInline with 12-14px text.
    --icon-size-sm20pxInline with 16px text, chip icons.
    --icon-size-md24pxDefault UI icon, list rows, nav.
    --icon-size-lg32pxFeature icons, empty states.
    --icon-size-xl48pxHero / illustration-scale icons.
    Opacity.
    TokenValueUse
    --opacity-disabled0.4Disabled control content.
    --opacity-muted0.64De-emphasised but still readable content.
    --opacity-scrim0.56Modal / sheet scrim.
    Focus ring geometry.
    TokenValueUse
    --focus-ring-width2pxFocus ring thickness.
    --focus-ring-offset2pxGap between the control and its focus ring.
    Layout — safe-area insets and the app shell height.
    TokenValueUse
    --safe-topenv(safe-area-inset-top, 0px)Status bar / notch inset.
    --safe-rightenv(safe-area-inset-right, 0px)Landscape right inset.
    --safe-bottomenv(safe-area-inset-bottom, 0px)Home indicator inset — the bottom nav sits above it.
    --safe-leftenv(safe-area-inset-left, 0px)Landscape left inset.
    --app-shell-height100dvhFull viewport height that survives the mobile URL bar.
    Font families.
    TokenValueUse
    --font-displayvar(--font-bricolage)Bricolage Grotesque — headings and display only.
    --font-bodyvar(--font-open-sans)Open Sans — body, captions, labels, buttons, nav.

    Transactional emails →