From b72ac5bb9e26df5e94a34ec2aed20e4ca2e0c087 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Sat, 17 Oct 2026 21:27:51 +0200 Subject: [PATCH] UX sprint U1: three directions, IA, screens, flows, components, tokens, strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three visual directions (Skärgård, Kvitto, Kalas) as self-contained HTML pages with every SPEC §13.3 screen in light and dark, Swedish, plus PNG exports and the exporter script. Direction-independent docs: ia.md, screens.md, flows.md, components.md, tokens.md (values for all three directions), strings.csv (key,en,sv,notes), and the comparison with a recommendation in directions/README.md. Co-Authored-By: Claude Opus 5.5 --- docs/ux/README.md | 49 + docs/ux/components.md | 182 +++ docs/ux/directions/1-skargard/index.html | 1353 ++++++++++++++++++++++ docs/ux/directions/2-kvitto/index.html | 1203 +++++++++++++++++++ docs/ux/directions/3-kalas/index.html | 1169 +++++++++++++++++++ docs/ux/directions/README.md | 52 + docs/ux/directions/export-png.sh | 20 + docs/ux/flows.md | 110 ++ docs/ux/ia.md | 124 ++ docs/ux/screens.md | 266 +++++ docs/ux/strings.csv | 465 ++++++++ docs/ux/tokens.md | 223 ++++ 12 files changed, 5216 insertions(+) create mode 100644 docs/ux/README.md create mode 100644 docs/ux/components.md create mode 100644 docs/ux/directions/1-skargard/index.html create mode 100644 docs/ux/directions/2-kvitto/index.html create mode 100644 docs/ux/directions/3-kalas/index.html create mode 100644 docs/ux/directions/README.md create mode 100755 docs/ux/directions/export-png.sh create mode 100644 docs/ux/flows.md create mode 100644 docs/ux/ia.md create mode 100644 docs/ux/screens.md create mode 100644 docs/ux/strings.csv create mode 100644 docs/ux/tokens.md diff --git a/docs/ux/README.md b/docs/ux/README.md new file mode 100644 index 0000000..120ee5c --- /dev/null +++ b/docs/ux/README.md @@ -0,0 +1,49 @@ +# Kvitt UX (sprint U1) + +The UX deliverables for Kvitt v1 (SPEC §13), produced 2026-10-17. Phase 1 (U1) is here: three visual directions plus the direction-independent design documents. After the owner picks a direction, phase 2 (U2) consolidates `tokens.md`, `components.md` and `screens.md` into `design-spec.md` for the chosen direction (SPEC §13.4) and deletes what it supersedes. + +## How to read + +1. **Look first:** open `directions/1-skargard/index.html`, `directions/2-kvitto/index.html` and `directions/3-kalas/index.html` in a browser (or the PNGs in each `png/` folder). Every screen S-1 to S-22 is there in light and dark, in Swedish, with the same household data, so the same file name compares one screen across directions. +2. **Compare and decide:** `directions/README.md` has the comparison table and the recommendation. +3. **Then the structure,** which holds whichever direction wins: + +| File | What it is | +|---|---| +| `ia.md` | Bottom navigation (three destinations, argued), screen inventory S-1…S-22 with modules and state sources, session routing, navigation map, which screen owns which data, admin vs member | +| `screens.md` | Every screen: purpose, content in priority order, empty/loading/offline/error/long-content states, actions; the add-expense flow in detail | +| `flows.md` | F-1 … F-11: first run as admin, join as partner, add expense online and offline, edit the other's expense, global and per-account settle-up, recurring, archive, in-app update, remove a lost phone | +| `components.md` | The `:core:design` component list: anatomy, variants, states, semantics | +| `tokens.md` | Token names and their values in all three directions (colour roles light/dark, semantic and category colours with contrast, charts, type, spacing, shapes, elevation, motion, icons) | +| `strings.csv` | Every UI string, `key,en,sv,notes` (SPEC §13.4 format). Placeholders are Android format arguments (`%1$s`, `%d`); plurals are two rows `:one` and `:other`, which become one ``. Notification copy (SPEC §13.3, Swedish written here) has the prefix `notif_`. | +| `directions/` | The three directions, their PNGs and `export-png.sh` | + +## The three directions + +**1 Skärgård** — the Stockholm archipelago on a calm day. Material 3 Expressive in sea blue, granite and sand, with large rounded shapes and generous whitespace. Home is a hero: "Du är skyldig Anna" above a large, narrow Roboto Flex figure and a single "Gör upp" button; accounts are a calm grouped list. The editor puts the amount in display size over an in-app keypad with Expressive key shapes. The most Android-native of the three, and the only one that can follow Material You wallpaper colours. + +**2 Kvitto** — a till receipt and a household ledger. Warm paper and ink, one ink-blue accent, every amount in IBM Plex Mono, dashed rules between groups. Home opens with a compact receipt block and then a dense ledger table; account detail reads like a bank statement; an expense is a printed receipt with an "audit trail" below; "Ni är kvitt" is a rubber stamp. The densest and calmest direction; its risk is feeling austere. + +**3 Kalas** — a crayfish party. Cream and white cards, a coral primary, very round shapes, and a solid category-colour tile on every expense so you recognise it before reading it. The balance is a seesaw (whoever owes sits lower, and the words always say it); settling up raises bunting. The editor shows the six most used categories as big tiles under the amount. The most personality; its risk is wearing thin over years of daily use. + +## Where this follows the spec instead of the U1 brief + +The owner's brief for this sprint and SPEC.md differ in places; the spec wins (CLAUDE.md › Source of truth): + +| Brief | Spec (followed) | +|---|---| +| Screen ids `S-01 …` | `S-1 … S-22` as in SPEC §13.3 | +| Bottom nav: recommend up to 4 | SPEC §13.2's three destinations kept, with the argument in `ia.md` §1 | +| `docs/ux/proposals/`, 360 dp frames, S26+ at 384 dp | `docs/ux/directions/-/index.html` at 411 dp (SPEC §13.1/§13.4); both phones are ~411 dp wide, so there is no separate S26+ layout | +| Swedish in one direction, English in the others | Swedish everywhere (§13.4), plus one English Home frame per direction to show English fits | +| `copy.md` table | `strings.csv` in the §13.4 format | +| Category suggested from the description | AC-8.6: last category used in that account on this phone, else Övrigt (a description-based suggestion would be a new rule; open question) | +| `tokens.md` "for the chosen scheme family" | Values for all three directions until the owner picks (§13.4 makes the pick precede the final spec) | + +Additions the spec did not state, flagged for U2: + +* **S-1 has a "Ditt namn" field:** AC-2.4 sends a display name with the accept call, but §13.3's S-1 lists no name field. A partner therefore types the name on S-1 and confirms it on S-2 (AC-2.10). +* **Category colour hex values** changed for contrast (`tokens.md` §4); adopting them needs a §9.7 spec change after the pick. +* **Avatar colours** for initials (AC-4.5) are defined per direction (`avatar-1`, `avatar-2`). +* **Expense save shows no "Ångra"** (an undo would be an audited create + delete that also notifies the partner). +* **Archived account detail hides the FAB** (an archived account is not offered for new expenses, AC-6.4). diff --git a/docs/ux/components.md b/docs/ux/components.md new file mode 100644 index 0000000..205cf2a --- /dev/null +++ b/docs/ux/components.md @@ -0,0 +1,182 @@ +# Kvitt: components for `:core:design` + +Status: U1 proposal (2026-10-17), direction-independent anatomy, states and API shape. The look (colours, shapes, type) comes from the chosen direction's tokens (`tokens.md`). In U2 this list moves into `design-spec.md` with final sizes. Names are Compose function names; every component takes a `Modifier` and gets screenshot tests in light and dark, Swedish and English, at 100 % and 200 % font scale (M6). + +Rules for all components: + +* Colours and type only from `KvittTheme` (Material 3 `ColorScheme` + `KvittColors` extension + `KvittTypography`); no literal values. +* Strings only from resources (`strings.csv` keys); components that need words take them as resolved `String` parameters or take the model value and resolve inside `:core:design` (shared strings live there, §14.7). +* Touch targets ≥ 48 dp (`minimumInteractiveComponentSize`), TalkBack `contentDescription` / `semantics` as stated per component. +* Material 3 Expressive APIs (`@ExperimentalMaterial3ExpressiveApi`) are used only behind a `:core:design` wrapper so the stable fallback named below can be swapped in one place. The implementing agent confirms each API's status from the official documentation at implementation time (CLAUDE.md › Fast-moving APIs). + +## 1. Money and text + +### `MoneyText` +Formats and shows an amount in öre via `MoneyFormatter` (§9.8). Never shows a sign. +* Params: `ore: Long`, `style: AmountStyle` (`Hero`, `Large`, `Medium`, `Small`), `tone: AmountTone` (`Neutral`, `Owed`, `Owing`, `Muted`), `maxLines` (default 1; never ellipsizes: overflow wraps to 2 lines or auto-steps down one type size for `Hero`/`Large`). +* Tabular figures always (`fontFeatureSettings = "tnum"`). +* Semantics: spoken form from `MoneyFormatter.spoken(ore)`: Swedish "3 250,00 kronor", English "3,250.00 Swedish kronor" (TalkBack reads "kr" and "SEK" poorly). + +### `AmountInput` +Large amount entry for S-7, S-9, S-19. Parses per §9.6 (string → öre, never floating point). +* Params: `value: String`, `onValueChange`, `error: String?`, `size` (`Display`, `Field`). +* States: empty (hint "0,00"), typing, error (supporting text, error colour), disabled. +* Accepts `,` and `.`; displays the locale's separator; inserts no-break-space thousands grouping while typing. + +### `AmountKeypad` (only if the chosen direction uses an in-app keypad) +3 × 4 grid: 1–9, decimal separator (locale), 0, backspace (TalkBack "Radera"); keys ≥ 64 dp tall; optional "Klar". Fallback: system keyboard `KeyboardType.Decimal`. + +### `EffectLine` +The one-line effect from the viewer's side (§9.2) for an `ExpenseEffect`. +* Variants: `Lent(ore)` "Du lånade ut X" (owed tone) · `Borrowed(ore)` "Du lånade X" (owing tone) · `NotInvolved` "Inte inblandad" · `PaidForYourself` "Betalade för dig själv" (neutral). + +### `BalanceWording` +Wording + amount for a `BalanceView` (§9.3). +* Variants: `Global` ("Anna är skyldig dig", "Du är skyldig Anna", "Ni är kvitt") and `Account` (… "Kvitt"). +* Semantics: merged into one sentence. + +## 2. People + +### `PersonAvatar` +* Params: `user: User`, `size` (24, 32, 40, 56, 96 dp), `shape` (direction token; fallback `CircleShape`). +* Picture via Coil keyed by `user.id` + `pictureVersion` (AC-4.6); else initials (first letter of the first two words, uppercase, AC-4.5) on `avatar-1`/`avatar-2` chosen by a stable hash of the user id (the two colours are tokens). +* States: loading (initials shown), picture, initials, uploading (progress ring overlay). +* Semantics: the display name (or decorative when the name is next to it). + +### `PersonChip` +Avatar 24 dp + name ("Du" for the viewer). Variants: `Display`, `Selectable` (S-7 payer toggle, S-7 full-split bearer). Selected state per M3 FilterChip. + +### `PayerToggle` +Two `PersonChip`s in a connected group; one tap switches. Label "Betalt av". Semantics: "Betalt av: du. Dubbeltryck för att byta till Anna." + +## 3. Categories and accounts + +### `CategoryIcon` +Material Symbols icon (token from §9.7) on a container in `cat--container` with the icon in `cat-`. Sizes 24/32/40/56 dp. Shape per direction (fallback circle or rounded square). Decorative when a name is shown next to it; else the category name. + +### `CategoryChip` +`CategoryIcon` (24) + name. Variants: `Display`, `Selectable`. Archived variant adds "(arkiverad)". + +### `CategoryGrid` +Picker grid in S-7 and S-19: tiles of icon + name, 4 columns at 411 dp (3 at 200 %), selected with outline/check. Only active categories. + +### `CategoryIconPicker`, `ColorSwatchPicker` +S-15 editor: 30 icons (6 columns), 12 colour swatches (6 columns), each 48 dp, selected with a check mark (not colour alone). Swatches have names for TalkBack ("Grön", "Orange", …). + +### `AccountRow` +Home and pickers: name, `BalanceWording.Account`, amount, secondary "11 utgifter i oktober". Variants: `Home`, `Picker` (no count), `Archived` (badge). 200 %: amount moves under the name. + +## 4. Ledger + +### `LedgerMonthHeader` +"Oktober · 7 457,90 kr" (household spend); sticky in `LazyColumn`. Year added when not the current year. + +### `ExpenseRow` +`CategoryIcon`, description (1 line), date, `MoneyText` amount, `EffectLine`, optional account name (S-12), `SyncPendingMarker`. Heights per direction (56–88 dp). States: normal, pending, pressed. Semantics: "ICA Maxi Lindhagen, 1 247,60 kronor, 16 oktober, du lånade 623,80 kronor, väntar på synk". + +### `SettlementRow` +`payments` icon, "Anna betalade dig 800,00 kr", "· 2 konton", date, pending marker. + +### `SyncPendingMarker` +Small icon (`cloud_upload` or direction equivalent) in `pending`; label "Väntar på synk". + +## 5. Balance and sync + +### `BalanceHero` +Home's global balance (S-4) and the account header (S-5, smaller variant). +* Anatomy: both `PersonAvatar`s, `BalanceWording.Global`, `MoneyText(Hero)`, optional action button "Gör upp" / "Gör upp det här kontot". +* States: `Owed` (partner owes you: owed colours, button), `Owing` (you owe: owing colours, button), `Settled` ("Ni är kvitt"/"Kvitt", no amount, no button), `Empty` (no accounts: welcome text), `Loading` (skeleton). +* 200 %: wording and amount each take a full line; avatars may shrink to 40 dp. +* Semantics: one merged sentence + the button. + +### `SyncStatusLine` +From `SyncSummary` + connectivity. +* Variants: `Synced` ("Synkad för 3 min sedan"), `Syncing` ("Synkar…"), `Offline` ("Offline"), with suffix "· 2 ändringar väntar på synk" (plural); `Warning` (rejections or clock skew) with icon and warning colour. +* Tap → S-16 (whole line is the 48 dp target). Relative time updates every minute. + +### `UpdateBanner` +S-17 states: `Available` (text + "Uppdatera" + "Senare"), `Downloading` (determinate `LinearProgressIndicator` + percent), `Failed` (text + "Försök igen"). Fallback for Expressive wavy progress: standard linear. + +### `NeedsConnectionHint` +Inline supporting text / banner "Kräver anslutning" with `cloud_off` icon; used on every online-only control (§9.9). Variant `Banner` for whole forms (S-6, S-14, S-15, S-19). + +## 6. Split and settle + +### `SplitModeSelector` +Single-select connected buttons (Expressive `ButtonGroup`/connected toggle buttons; fallback `SingleChoiceSegmentedButtonRow`): "Kontots standard 60/40", "Egen fördelning", "Exakta belopp", "Allt på en person". Wraps to 2 rows (Swedish), 4 rows at 200 % (a vertical radio list fallback when the row cannot fit). + +### `RatioEditor` +Slider (0–100, step 1, haptic at 50) + two percent `TextField`s summing to 100 (two decimals) + preset chips (50/50, 60/40, 40/60). Used in S-6 and S-7 `ratio`. Semantics: slider "Jacobs andel, 60 procent". + +### `ExactSplitEditor` +Two money fields: the typed one editable, the other read-only "beloppet minus …"; error "Andelen är större än beloppet". + +### `FullSplitPicker` +Two `PersonChip`s: "Allt på Jacob" / "Allt på Anna". + +### `SplitPreview` +"Du 206,00 kr · Anna 206,00 kr" + the effect the row will show. Data from `ExpenseRepository.preview`; hidden when null. Optional bar showing the two shares as proportions (decorative). + +### `SplitPicker` (composite) +The S-7 split section: `SplitModeSelector` + the mode's editor + `SplitPreview`; collapsed state is a single chip "Delning: Kontots standard 50/50" which expands it in one tap. + +### `DirectionSwitcher` +S-9 "Du betalar Anna" with avatars and an arrow; swap button `swap_horiz` (TalkBack "Byt riktning"). + +### `AllocationEditor` +S-9 list of `AllocationRow`s + "Lägg till konto" + `RemainingIndicator`. +* `AllocationRow`: account name, debt line ("Du är skyldig 3 084,50 kr"), `AmountInput(Field)`, remove (`close`, "Ta bort Huset från fördelningen"), warning line (`warning` icon + "Det här reglerar mer än som är skyldigt i Japan 2027") when `overSettles`. Variant `Fixed` (per-account settle: no remove). +* `RemainingIndicator`: "Kvar att fördela 0,00 kr" (neutral/positive), "250,00 kr kvar" (warning), "250,00 kr för mycket" (error), always words + colour. + +## 7. Timeline + +### `TimelineItem` +Anatomy: actor avatar (24 dp; system icon `autorenew` for recurring, `upload_file` for import), sentence, "· 16 okt. 19:40"; vertical connector between items. +* Variants: `Create`, `Update` (one line per `FieldChange`, sentence built from field + old → new, values formatted by `MoneyFormatter`/`DateFormatter`, ids already names), `Delete`, `PendingLocal` ("Din ändring väntar på synk", pending colour, dashed connector), `System` (recurring/import). +* Field names (sv/en) in `strings.csv` (`timeline_field_*`). + +### `Timeline` +Column of `TimelineItem`, oldest first, newest last (AC-12.4), with the section title "Historik". + +## 8. Charts (`:core:design`, Vico or Canvas fallback per §6.2) + +### `ChartCard` +Container: title, optional subtitle (range), the chart, legend, and an invisible-but-spoken summary sentence (`semantics { contentDescription = summary }`). States: loaded, empty ("Inga utgifter i den här perioden"), loading (shimmer; only when the computation takes > 300 ms). + +### `StackedBarChart` +Buckets on x (month "maj" or week "v. 41"), kronor on y (whole kronor, 3–5 gridlines, compact "20 tkr" labels in sv, "20k" in en), segments coloured by group (category colour or `chart-cat-n` for accounts, stable order = legend order), 2 dp gap between segments; selected bar highlighted, others dimmed; tap → callback(bucketKey). Each bar is a focusable semantics node ("September, 41 484 kronor"). Horizontal scroll when more than ~8 buckets; newest visible. + +### `ChartLegend` +Wrapping row of swatch + label; only groups with data; static (filtering by legend is not in the spec). + +### `BreakdownBarList` +Rows: icon/swatch, name, proportional bar, amount, percent (one decimal, AC-14.10). Sorted descending. Tap → callback(groupId). 200 %: bar on its own line. + +### `FilterBar` +S-11 filters: range chip with menu (`DropdownMenu` of `RangePreset`s), three binary connected toggles. Wraps to two rows. + +## 9. Scaffolding + +### `KvittScaffold` +Top app bar (small, 64 dp; back or close), content, optional FAB slot, snackbar host, edge-to-edge insets. + +### `KvittNavigationBar` +Three items (ia.md §1). Expressive `ShortNavigationBar` (fallback `NavigationBar`). Selected icon filled. Labels always visible. + +### `AddExpenseFab` +Extended FAB "Lägg till utgift" (icon `add`), collapses to icon-only on scroll; Expressive size variants (fallback `ExtendedFloatingActionButton`). + +### `EmptyState` +Icon, title, body, optional primary action. Used by S-3, S-4, S-5, S-11, S-16, S-19. + +### `ConfirmDialog` +Title, body, confirm (destructive variant in `error`), dismiss. + +### `SectionHeader`, `SettingsRow`, `CollapsibleSection` +Settings rows (icon, title, optional trailing value/badge, chevron), section headers ("Konton" + trailing action), "Arkiverade (n)" collapse with rotation of the chevron. + +### `InviteCodeDisplay`, `InviteCodeField` +Code in three groups of four, mono/tabular, large; selectable; TalkBack spells characters ("7 K 3 M, Q X 9 D, 2 H P A"). Field variant normalises as typed (§8.3). + +### `Skeleton` +Shimmer placeholders for the first-sync loading state only. diff --git a/docs/ux/directions/1-skargard/index.html b/docs/ux/directions/1-skargard/index.html new file mode 100644 index 0000000..3075d79 --- /dev/null +++ b/docs/ux/directions/1-skargard/index.html @@ -0,0 +1,1353 @@ + + + + + +Kvitt · 1 Skärgård + + + + + + + + +
+ + + diff --git a/docs/ux/directions/2-kvitto/index.html b/docs/ux/directions/2-kvitto/index.html new file mode 100644 index 0000000..5752c2c --- /dev/null +++ b/docs/ux/directions/2-kvitto/index.html @@ -0,0 +1,1203 @@ + + + + + +Kvitt · 2 Kvitto + + + + + + + + +
+ + + diff --git a/docs/ux/directions/3-kalas/index.html b/docs/ux/directions/3-kalas/index.html new file mode 100644 index 0000000..0791c43 --- /dev/null +++ b/docs/ux/directions/3-kalas/index.html @@ -0,0 +1,1169 @@ + + + + + +Kvitt · 3 Kalas + + + + + + + + + + + + diff --git a/docs/ux/directions/README.md b/docs/ux/directions/README.md new file mode 100644 index 0000000..ba5c581 --- /dev/null +++ b/docs/ux/directions/README.md @@ -0,0 +1,52 @@ +# Kvitt UX directions (U1) + +Three visual directions, each one self-contained HTML file with every screen of SPEC §13.3 in light and dark, plus PNG exports. Open `index.html` in a browser: the top of each page is the direction's spec (palette with live contrast ratios, typography, shapes, motion, icons, the balance header in its three states, Expressive components with fallbacks); below are the phone frames (411 × 891 dp, Galaxy S26), light and dark side by side. + +| # | Direction | Page | PNGs | +|---|---|---|---| +| 1 | **Skärgård** — calm Material 3 Expressive, big balance hero | `1-skargard/index.html` | `1-skargard/png/` (97) | +| 2 | **Kvitto** — till receipt and ledger, dense and list-first | `2-kvitto/index.html` | `2-kvitto/png/` (99) | +| 3 | **Kalas** — playful cards, strong category colour | `3-kalas/index.html` | `3-kalas/png/` (101) | + +All three use the same household data (Jacob and Anna, Vardag / Huset / Japan 2027 / Gotland 2026, ICA, Willys, Systembolaget, SL, Vattenfall, Hyra), the same frame ids and the same Swedish copy, so a screen can be compared across directions by file name (`png/S-4-home-light.png`). One English Home frame (`S-4-home-en`) and two 200 % font-scale frames (`S-4-home-200`, `S-7-editor-200`) per direction. Extra frames beyond the common list: `S-11-analytics-scrolled` (all three; the three charts do not fit one screen), `S-5-account-menu` (Kvitto, Kalas), `S-5-account-archived-menu` (Kalas). + +Re-export after a change: `docs/ux/directions/export-png.sh docs/ux/directions/- [frame-id …]` (needs Google Chrome; fonts load from Google Fonts). + +## Comparison + +| | 1 Skärgård | 2 Kvitto | 3 Kalas | +|---|---|---|---| +| Character | Archipelago on a calm day: sea blue, granite, sand; generous whitespace; few elements per screen | Till receipt and ledger book: paper, ink, one ink-blue accent, mono amounts, dashed rules | Crayfish party: cream and white cards, coral primary, every expense a solid colour tile | +| Home balance | Hero card, words first, huge narrow figure, cookie-shaped avatars, faint islet drawing | Compact receipt block with "Saldo · alla konton", amount in mono, Gör upp beside it; "Kvitt" rubber stamp when settled | Seesaw: whoever owes sits lower, plank in the debt colour; bunting and yellow card when settled | +| Accounts / ledger | Grouped rounded list; 72 dp rows | Ledger table, 60 dp rows, date column, dotted leaders to month totals — most rows per screen | Colourful cards; account colour on a clover tile matches the account chart | +| Add expense | Amount in display size, in-app keypad with Expressive key shapes, default chips above the keypad | Register-style amount on a till keypad (has "00"), one-tap key/value rows | Big amount, keypad, row of the six most used category tiles, chips; split shown as a two-colour bar | +| Expense detail | Card with facts and a timeline | Printed receipt with zig-zag edges and an "audit trail" printout | Colour-tile header card and timeline | +| Analytics | Calm cards, tonal charts | Chart-first, every number in mono | Colourful stacked bars and tile breakdowns | +| Type | Roboto Flex (OFL) | IBM Plex Sans + Plex Mono (OFL) | Nunito (OFL) | +| Shape | Large radii, MaterialShapes for avatars/FAB | Square, 4–8 dp, receipt edges | Very round, MaterialShapes for tiles and avatars | +| Owed / owing colours | sea-green / coral | green / brick + square marker | green / purple | +| Dynamic colour | Offered (off by default) | No (paper palette is the identity) | No (colours carry meaning) | +| Expressive dependence | Highest (ShortNavigationBar, ButtonGroup, MaterialShapes, LoadingIndicator, wavy progress, expressive motion) — all with stable fallbacks | Lowest (ShortNavigationBar only) | Medium (MaterialShapes, ButtonGroup, ShortNavigationBar, expressive motion) | +| Strengths | Most Android-native; balance readable at a glance; survives dynamic colour; least surprising for both users | Densest; amounts line up perfectly; very calm; the name "Kvitt" and the receipt metaphor reinforce each other; strong dark theme | Expenses recognisable before reading; most personality; strong ten-second add flow with category tiles | +| Risks | Can read as generic Material if the shapes and decoration are dropped; the hero takes ~35 % of Home; the 80 dp FAB covers list content until it collapses | Can feel austere, "accounting software"; mono amounts are wide (wrap at 200 %); custom keypad and receipt shapes | Seesaw may feel gimmicky over time; coral primary is close to error red (errors always carry an icon); less dense; dark yellow containers read olive | +| Compose cost | Medium: keypad, hero Canvas, MaterialShapes, Roboto Flex variations, `KvittColors` | Medium: zig-zag `GenericShape`, dashed dividers, stamp, keypad, Canvas charts, print/roll motion | Medium: `RoundedPolygon.toShape()`, seesaw layout with spring, keypad, stacked bars | + +## Recommendation + +**Skärgård**, with one borrowing from Kalas: solid category-colour icon tiles in ledger rows (Skärgård's tinted 12 dp containers are the quieter fallback). + +Why: + +1. **The two jobs that matter most (§13.1) are done best.** The ten-second add flow is equally good in all three, but Skärgård's Home answers "who owes whom" with the least to read: one sentence, one number, one button. +2. **Lowest long-term risk.** It is the closest to stock Material 3, so Compose upgrades, M6's screenshot tests and future screens cost the least; every Expressive part has a stable fallback that still looks right. +3. **Works for both users.** It is neither austere (Kvitto's risk for the spouse) nor novelty-driven (Kalas's seesaw), and it is the only direction that can follow the wallpaper colours if either of you wants that. +4. **Category recognition** is Kalas's real advantage; it transplants cleanly into Skärgård's list rows without the rest of Kalas's playfulness. + +Pick Kvitto if density and the receipt character matter more than warmth; pick Kalas if you both want the app to feel fun every day. Mixing is possible: tell me which parts of which direction you like (e.g. "Skärgård, but Kvitto's receipt detail screen"). + +## Known gaps in the mocks (not design decisions) + +* `S-4-home` shows no pending count although the Vardag ledger has one pending row; real Home shows the count whenever `pendingCount > 0` (`screens.md` S-4). +* `S-11-analytics-week` uses a custom range (24 aug.–17 okt., v. 35–42) with invented weekly values; "Senaste 6 mån" by week would show ~26 bars. +* `S-8-expense-deleted` uses a different expense in each direction (no shared data for it). +* Kalas lets the per-account settle-up amount be edited (AC-11.4 says "defaults to"); Skärgård shows the swap control there too (per-account settle-ups go either way, §9.4). Both are allowed by the spec and should become the rule in U2. diff --git a/docs/ux/directions/export-png.sh b/docs/ux/directions/export-png.sh new file mode 100755 index 0000000..0c59f99 --- /dev/null +++ b/docs/ux/directions/export-png.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Export every phone frame of one direction to /png/.png at 2x. +# Usage: docs/ux/directions/export-png.sh docs/ux/directions/- [frame-id ...] +# Needs Google Chrome (macOS path below or CHROME=...). The page must implement ?list=1 and ?frame=. +set -euo pipefail +dir="$(cd "$1" && pwd)"; shift +chrome="${CHROME:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" +page="file://$dir/index.html" +run() { "$chrome" --headless=new --disable-gpu --hide-scrollbars --no-first-run --virtual-time-budget=10000 "$@" 2>/dev/null; } +if [ "$#" -gt 0 ]; then ids=("$@"); else + ids=() + while IFS= read -r line; do ids+=("$line"); done < <(run --dump-dom "$page?list=1" \ + | sed -n '/
/,/<\/pre>/p' | sed -e 's/<[^>]*>//g' | grep -v '^[[:space:]]*$')
+fi
+[ "${#ids[@]}" -gt 0 ] || { echo "no frames listed by $page?list=1" >&2; exit 1; }
+mkdir -p "$dir/png"
+for id in "${ids[@]}"; do
+  run --window-size=411,891 --force-device-scale-factor=2 --screenshot="$dir/png/$id.png" "$page?frame=$id"
+done
+echo "exported ${#ids[@]} frames to $dir/png"
diff --git a/docs/ux/flows.md b/docs/ux/flows.md
new file mode 100644
index 0000000..0e07d3f
--- /dev/null
+++ b/docs/ux/flows.md
@@ -0,0 +1,110 @@
+# Kvitt: flows
+
+Status: U1 proposal (2026-10-17). Step-by-step flows with screen ids (`ia.md`, `screens.md`). Rules referenced by spec section; nothing here redefines them. Data from the shared mock household (Jacob = owner/admin, Anna = partner; today lördag 17 oktober 2026).
+
+Notation: **User** does · *System* does · `S-n` screen · ⟶ navigation.
+
+---
+
+## F-1 First run as admin (US-1, FR-1, FR-2, FR-5)
+
+1. **Jacob** runs `kvitt-server bootstrap` on the Pi (outside the app); it prints the code `7K3M-QX9D-2HPA` and the link `https://icehack.asuscomm.com:8443/i/7K3MQX9D2HPA` (AC-1.1).
+2. **Jacob** opens the link on the phone. The landing page (AC-2.5) offers "Ladda ner Kvitt" (first time) and "Öppna i Kvitt".
+3. **Jacob** installs the APK (Android asks once to allow installs from the browser; outside the app), then taps "Öppna i Kvitt" ⟶ `S-1` with server and code filled (AC-2.6). Without a link: `S-1` empty, he types the code (server prefilled).
+4. **Jacob** types his name in "Ditt namn" and taps "Para ihop". *Progress "Parar ihop…"; pairs with the phone model as device name ("Galaxy S26").* (`S-1` always has "Ditt namn" on a first pairing because AC-2.4 sends the display name in the accept call and the app cannot know the invite kind beforehand; the server ignores it for `device` invites. The field is hidden in the "Para ihop telefonen igen" variant.)
+5. *Full sync (AC-2.9)* ⟶ `S-4` loading state "Hämtar era utgifter…" ⟶ the system notification-permission prompt.
+6. *Partner not yet joined* ⟶ `S-4` shows the `S-3` content "Väntar på din partner" (AC-5.1).
+7. **Jacob** taps "Bjud in partner" ⟶ `S-21`, a `partner` invite is created and shown: code in groups of 4, link, "Gäller till tis 20 okt. 14:02".
+8. **Jacob** taps "Dela" ⟶ Android share sheet ⟶ sends the message to Anna (SMS, Signal, …). Back ⟶ `S-3` now shows the expiry.
+9. When Anna has joined (F-2), the next sync replaces `S-3` with `S-4` empty state "Skapa ert första konto" ⟶ **Jacob** taps "Nytt konto" ⟶ `S-6` "Vardag", 50/50 ⟶ Spara ⟶ `S-4` with the account and the FAB.
+
+Error branches: `invite_invalid` / network / `rate_limited` inline on `S-1` (screens.md S-1). Lost every phone later: `kvitt-server pair-device` (AC-1.6) then F-1 steps 2–5.
+
+## F-2 Join as spouse (US-2, AC-2.4, AC-2.10)
+
+1. **Anna** receives Jacob's message, taps the link ⟶ landing page ⟶ "Ladda ner Kvitt" ⟶ installs (Android "Install unknown apps" for her browser, once) ⟶ "Öppna i Kvitt" ⟶ `S-1` filled.
+2. **Anna** types her name in "Ditt namn" ("Anna") and taps "Para ihop" ⟶ *pairs, creates the member user*.
+3. ⟶ `S-2`: "Du går med i Jacobs hushåll", her name prefilled, avatar with initials. **Anna** taps "Lägg till bild" ⟶ photo picker ⟶ picks a photo ⟶ *upload with progress*. Taps "Fortsätt".
+4. *Full sync* ⟶ `S-4` loading ⟶ notification permission prompt ⟶ `S-4` empty "Skapa ert första konto" (or Jacob's accounts, if he created any after she joined).
+5. On Jacob's phone the next sync (or push-triggered sync) removes `S-3`.
+
+## F-3 Add an expense online (US-3, AC-8.6) — the ten-second flow
+
+1. **Jacob** opens Kvitt ⟶ `S-4`.
+2. Taps FAB "Lägg till utgift" ⟶ `S-7` with amount focused, keypad up; defaults: Konto Vardag (last used), Kategori (last used in Vardag, else Övrigt), Datum Idag, Betalt av Du, Delning Kontots standard 50/50.
+3. Types `412`.
+4. Taps Beskrivning, types "Systembolaget".
+5. (optional, one tap each) taps the Kategori chip ⟶ grid ⟶ Nöje; taps Betalt av to switch to Anna; taps Delning to open the split section.
+6. Taps "Spara" ⟶ *saved locally, pending* ⟶ back to `S-4`, snackbar "Utgiften sparades"; Vardag's wording/amount and the global hero update immediately (AC-10.4).
+7. *2 s later, sync pushes (AC-15.3)*; the sync line returns to "Synkad nyss"; *the server pushes `expense_created` to Anna's phone*, which syncs and shows "Jacob lade till en utgift · Systembolaget · 412,00 kr · Vardag".
+
+## F-4 Add an expense offline, then sync (US-12, FR-15)
+
+1. **Jacob** is on a plane. `S-4` sync line: "Offline · Synkad 14 okt. 08:12".
+2. F-3 steps 2–6 exactly. Snackbar "Sparad. Synkas när du är online." The row in `S-5` shows the pending marker; `S-4` sync line "Offline · 1 ändring väntar på synk".
+3. He adds more and edits one; the count increases ("3 ändringar väntar på synk"). Account, category and recurring actions show "Kräver anslutning".
+4. Landing: network returns ⟶ *sync on foreground or WorkManager* ⟶ sync line "Synkar…" ⟶ "Synkad nyss"; pending markers disappear.
+5. **Conflict branch:** Anna edited the same expense meanwhile with a later `updated_at` ⟶ *push answers `stale`* ⟶ the row shows Anna's version; sync line "Synkad nyss · 1 ändring synkades inte" (warning style) ⟶ tap ⟶ `S-16` "Ändringar som inte synkades": "Babette · Din ändring ersattes av en nyare ändring från Anna" ⟶ tap opens the current expense (read-only `S-8`) ⟶ "Ta bort från listan".
+6. **Delete branch:** Anna deleted it ⟶ reason `deleted` "Den här togs bort innan din ändring kom fram".
+
+## F-5 Edit an expense the other person created (AC-8.2, AC-12.4)
+
+1. **Jacob** gets a notification "Anna lade till en utgift · ICA Maxi Lindhagen · 1 147,60 kr · Vardag" ⟶ taps ⟶ `S-8` (after sync).
+2. Timeline: "Anna skapade den här · 16 okt. 14:02". He sees the receipt says 1 247,60.
+3. Taps "Ändra" ⟶ `S-7` "Ändra utgift" with all fields loaded; amount focused.
+4. Changes 1 147,60 to 1 247,60 ⟶ "Spara" ⟶ back to `S-8`; timeline gains a last line "Din ändring väntar på synk".
+5. *Sync* ⟶ the pending line is replaced by "Du ändrade beloppet från 1 147,60 kr till 1 247,60 kr · 16 okt. 19:40".
+6. *Anna's phone* gets "Jacob ändrade en utgift · ICA Maxi Lindhagen · 1 247,60 kr"; her `S-8` shows the same timeline (her view: "Jacob ändrade beloppet …").
+
+## F-6 Global settle-up with adjusted allocation (US-7, AC-11.3, AC-11.5, §9.4)
+
+Start: global "Du är skyldig Anna 3 250,00 kr"; Vardag Anna owes Jacob 1 184,50; Huset Jacob owes 3 084,50; Japan 2027 Jacob owes 1 350,00.
+
+1. **Jacob** taps "Gör upp" on `S-4` ⟶ `S-9`: "Du betalar Anna", amount 3 250,00 kr (default = global debt), proposal (oldest owing first): Huset 3 084,50, Japan 2027 165,50; "Kvar att fördela 0,00 kr"; info "Vardag påverkas inte: där är Anna skyldig dig 1 184,50 kr."
+2. They agreed to clear Japan first. **Jacob** edits Japan 2027 to 1 500,00 ⟶ *row warning "Det här reglerar mer än som är skyldigt i Japan 2027"* (1 500 > 1 350); remaining "−" ⟶ "1 334,50 kr för mycket"; Bekräfta disabled.
+3. Edits Huset to 1 750,00 ⟶ "Kvar att fördela 0,00 kr"; Bekräfta enabled; Japan warning remains.
+4. Sets Anteckning "Swish", Datum Idag. Taps "Bekräfta" ⟶ dialog repeating the warning ⟶ "Bekräfta ändå".
+5. *Saved locally* ⟶ back to `S-4`: global −3 250,00 + 3 250,00 = 0 ⟶ "Ni är kvitt"; per account Huset "Du är skyldig Anna 1 334,50 kr", Japan "Anna är skyldig dig 150,00 kr", Vardag unchanged. (This is §9.4's stated consequence; the per-account settle-ups in F-7 clear the rest.)
+6. *Sync; Anna gets "Jacob registrerade en betalning · Jacob betalade dig 3 250,00 kr"* (her view).
+
+Variant: "Lägg till konto" ⟶ picker (all non-deleted accounts, archived included) ⟶ adds a row with 0,00 to fill in. Removing a row increases "Kvar att fördela".
+
+## F-7 Per-account settle-up (US-8, AC-11.4)
+
+1. **Jacob** on `S-4` taps Huset ⟶ `S-5` "Du är skyldig Anna 3 084,50 kr" ⟶ "Gör upp det här kontot".
+2. ⟶ `S-9` per-account variant: "Du betalar Anna", 3 084,50 kr, one fixed row Huset 3 084,50. No "Lägg till konto".
+3. Optional: changes the amount to 2 000,00 ⟶ the single row follows the amount (it is the only allocation) ⟶ Bekräfta.
+4. ⟶ back to `S-5`: a settlement row "Du betalade Anna 3 084,50 kr" at the top of October; header "Kvitt".
+5. To settle an account where the *other* user owes (Vardag): `S-5` Vardag ⟶ "Gör upp det här kontot" ⟶ direction defaults to "Anna betalar dig" (AC-11.4); Jacob records Anna's Swish.
+
+## F-8 Create a recurring expense (US-10, FR-9)
+
+1. **Jacob** ⟶ Inställningar `S-13` ⟶ "Återkommande utgifter" `S-19` ⟶ "Ny".
+2. Editor: amount 12 400,00; description "Hyra"; Konto Huset; Kategori Hem; Betalt av Anna (one tap); Delning Kontots standard 60/40; Dag i månaden 25; Första månad: *defaults to oktober 2026 because the 25th is still ahead (AC-9.8)*; Sista månad empty.
+3. Spara (online) ⟶ list row "Hyra · 12 400,00 kr · Huset · Varje månad den 25:e · Nästa: 25 okt.".
+4. *On 25 Oct at 06:00 the server creates the expense* ⟶ both phones get "Återkommande utgift tillagd · Hyra · 12 400,00 kr · Huset" (AC-9.6) ⟶ `S-8` shows "Återkommande: Hyra" and "Skapad automatiskt (återkommande) · 25 okt. 06:00".
+5. Offline at step 3 ⟶ Spara disabled with "Kräver anslutning".
+
+Pause: list row ⟶ editor ⟶ "Pausad" on ⟶ Spara ⟶ row shows "Pausad". Resume does not backfill (§9.5); the editor shows the hint "Månader som passerat under pausen skapas inte i efterhand."
+
+## F-9 Archive an account (US-9, AC-6.4)
+
+1. **Anna** on `S-4` ⟶ Japan 2027 ⟶ `S-5` ⟶ overflow ⟶ "Arkivera" (online).
+2. *Archived* ⟶ back on `S-4`, snackbar "Japan 2027 arkiverades" with "Ångra". The account disappears from the active list and appears under "Arkiverade (2)" (collapsed) with its balance still shown; the global balance is unchanged (AC-10.5).
+3. In `S-7` the account picker no longer lists it; `S-9` "Lägg till konto" still does; `S-11` still counts it.
+4. Unarchive: Home ⟶ "Arkiverade" ⟶ Japan 2027 ⟶ `S-5` ("Arkiverat" badge) ⟶ "Återställ från arkiv".
+5. Delete branch: overflow "Ta bort konto" is disabled with "Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter" unless the account is empty (AC-6.5).
+
+## F-10 In-app update (US-14, FR-16)
+
+1. *Cold start of a release build* ⟶ `GET /apk/latest` ⟶ version code higher ⟶ `S-4` banner "Version 1.2 finns" [Uppdatera] [Senare].
+2. **Jacob** taps "Uppdatera". First time: dialog "Tillåt installation" explains that Android will ask once ⟶ "Fortsätt" ⟶ system settings "Installera okända appar" for Kvitt ⟶ he allows and taps back.
+3. *Download* ⟶ banner "Laddar ner… 64 %" with progress; the app stays usable.
+4. *Size and SHA-256 verified* ⟶ the system installer dialog ⟶ "Uppdatera" (system) ⟶ the app restarts on the new version.
+5. Failure ⟶ banner "Nedladdningen misslyckades. Försök igen." ⟶ "Försök igen" (AC-16.4).
+6. "Senare" hides the banner until the next cold start. The update is also reachable from `S-22` "Sök efter uppdatering".
+
+## F-11 Remove a lost phone (US-17, FR-3) — supporting flow
+
+1. **Jacob** (admin) ⟶ `S-13` ⟶ "Enheter" `S-20` ⟶ the lost "Galaxy S26+" row ⟶ "Ta bort" ⟶ confirmation ⟶ *revoked*.
+2. The lost phone, if it ever connects, gets `device_revoked` ⟶ wipes local data ⟶ `S-18` "Den här telefonen har tagits bort" ⟶ "Para ihop igen" ⟶ `S-1`.
diff --git a/docs/ux/ia.md b/docs/ux/ia.md
new file mode 100644
index 0000000..35616a8
--- /dev/null
+++ b/docs/ux/ia.md
@@ -0,0 +1,124 @@
+# Kvitt: information architecture
+
+Status: U1 proposal (2026-10-17). Binding inputs: SPEC §13.2 (navigation model), §13.3 (screens S-1 to S-22), §14.2.4 (UI contract), §6.2 (module ownership). Screen ids are the spec's (`S-1` … `S-22`), not renumbered.
+
+## 1. Bottom navigation
+
+**Recommendation: keep the spec's three destinations.**
+
+| Destination | sv | en | Icon (Material Symbols) | Start screen |
+|---|---|---|---|---|
+| Home | Översikt | Home | `home` (filled when selected) | S-4 |
+| Analytics | Statistik | Analytics | `bar_chart` | S-11 |
+| Settings | Inställningar | Settings | `settings` | S-13 |
+
+Plus one floating action button, "Lägg till utgift" / "Add expense", on S-4 and S-5 only (SPEC §13.2).
+
+Why three and not four:
+
+1. **Home already is the account list.** The second most important question ("who owes whom", §13.1) is answered per account on Home (AC-10.2). A separate "Accounts" destination would split the global balance from its breakdown and add a tap to the most common path (Home → account → expense).
+2. **The add flow is the FAB, not a tab.** A tab "Add" would be a navigation item that is really an action; Material 3 discourages that, and the FAB is reachable with the thumb on both phones.
+3. **No fourth destination exists in the spec.** The only candidate that would earn the slot is an **Activity** feed (the other person's latest changes across all accounts, i.e. the audit log as a list). It would be useful together with push notifications (US-13), but it is a new feature, out of scope for the UX sprint (§13.5). It is listed as an owner question in the report, not designed.
+4. Three items keep Swedish labels whole: "Inställningar" (13 characters) fits at 200 % font scale in a three-item bar but not in a four- or five-item bar of 411 dp.
+
+Navigation bar style per direction (Expressive "short navigation bar" 64 dp or classic `NavigationBar` 80 dp) is stated on each direction's review page.
+
+## 2. Screen inventory
+
+Kind: **top** = bottom-nav destination; **pushed** = full screen with top app bar and back (§13.2); **flow** = onboarding screen without bottom nav; **dialog** / **banner** as named.
+
+| Id | Screen (sv / en) | Kind | Feature module (§6.2) | Reached from | State from (§14.2.4) |
+|---|---|---|---|---|---|
+| S-1 | Para ihop / Pair | flow, start when `Unpaired` or `Unrecognised` | `:feature:onboarding` | app start; deep link `kvitt://invite?…` or `https://…/i/` (AC-2.6); S-18 | `SessionRepository` |
+| S-2 | Din profil (partner step) / Your profile | flow | `:feature:onboarding` | S-1 after a `partner` pairing (AC-2.10) | `SessionRepository`, `ProfileRepository` |
+| S-3 | Väntar på din partner / Waiting for your partner | top (replaces S-4 content) | `:feature:onboarding` | Home while `partner == null` (AC-5.1) | `SessionRepository`, `InviteRepository` |
+| S-4 | Översikt / Home | top | `:feature:home` | bottom nav; notification for deletions and `sync_batch` | `HomeRepository` → `HomeState` |
+| S-5 | Konto (account detail) | pushed | `:feature:account` | S-4 account row | `AccountRepository.detail` → `AccountDetailState` |
+| S-6 | Nytt konto / Ändra konto | pushed | `:feature:account` | S-4 "Nytt konto"; S-5 overflow "Ändra konto" | `AccountRepository` |
+| S-7 | Ny utgift / Ändra utgift | pushed, full screen (§13.2) | `:feature:expense` | FAB on S-4 or S-5; S-8 "Ändra"; S-19 editor reuses its fields | `ExpenseRepository.editorOptions`, `preview`, `save` |
+| S-8 | Utgift (expense detail) | pushed | `:feature:expense` | ledger rows (S-5, S-12, S-16); notification | `ExpenseRepository.detail` → `ExpenseDetailState` |
+| S-9 | Gör upp / Settle up | pushed | `:feature:settle` | S-4 "Gör upp" (global); S-5 "Gör upp det här kontot" (per account); S-10 "Ändra" | `SettlementRepository.start`, `recompute`, `save` → `SettleState` |
+| S-10 | Betalning (settlement detail) | pushed | `:feature:settle` | settlement rows; notification | `SettlementRepository.detail` |
+| S-11 | Statistik / Analytics | top | `:feature:analytics` | bottom nav | `AnalyticsRepository.analytics` → `AnalyticsState` |
+| S-12 | Statistik, urval (drill-down) | pushed | `:feature:analytics` | tap on a bar or a breakdown row in S-11 (AC-14.8) | `AnalyticsRepository.drillDown` |
+| S-13 | Inställningar / Settings | top | `:feature:settings` | bottom nav | `ProfileRepository.household`, `SessionRepository` (role) |
+| S-14 | Profil | pushed | `:feature:settings` | S-13 | `ProfileRepository` |
+| S-15 | Kategorier (list + editor) | pushed (editor pushed from list) | `:feature:settings` | S-13 | `CategoryRepository` |
+| S-16 | Synkstatus / Sync status | pushed | `:feature:home` | S-4 sync line; S-13 "Synkstatus" | `SyncRepository.status` |
+| S-17 | Uppdatering (banner + dialogs) | banner on S-4, dialogs | `:feature:home` | S-4 when `UpdateState != None`; S-22 "Sök efter uppdatering" | `UpdateRepository` |
+| S-18 | Telefonen har tagits bort / Device removed | flow, start when `Revoked` | `:feature:onboarding` | any 401 `device_revoked` (AC-3.3) | `SessionRepository.acknowledgeRevoked` |
+| S-19 | Återkommande utgifter (list + editor) | pushed | `:feature:settings` | S-13 | `RecurringRepository` |
+| S-20 | Enheter / Devices | pushed | `:feature:settings` | S-13 | `DeviceRepository` |
+| S-21 | Inbjudningar (Bjud in partner / Para ihop en enhet) | pushed | `:feature:settings` | S-13 (admin only); S-3 "Bjud in partner" | `InviteRepository` |
+| S-22 | Om Kvitt / About | pushed | `:feature:settings` | S-13 | `AboutRepository`, `UpdateRepository.check` |
+
+System notifications (SPEC §13.3 table, §14.6) are not screens; they open S-8, S-10 or S-4 (AC-13.6).
+
+## 3. Start destination and session routing
+
+The app shell (`:app`) chooses the root from `SessionRepository.session`:
+
+| `SessionState` | Root | Notes |
+|---|---|---|
+| `Unpaired` | S-1 | Fields filled when opened from an invite link. |
+| `Paired`, `partner == null` | S-4 showing the S-3 content | Bottom nav visible; Statistik shows its empty state; FAB hidden (AC-5.1). For the member this state cannot occur. |
+| `Paired`, just paired as `partner` | S-2, then S-4 | AC-2.10. Notification permission is asked after the first full sync (AC-2.9), i.e. on arriving at S-4. |
+| `Paired` | S-4 | First sync after pairing shows the S-4 loading state. |
+| `Revoked` | S-18 | Local data already wiped (AC-3.3). |
+| `Unrecognised` | S-1 with "Para ihop telefonen igen" header | Local data kept (§14.4.8). |
+
+Back from a top destination exits the app (Home) or returns to Home (Statistik, Inställningar). Each top destination keeps its own back stack and scroll position when switching tabs.
+
+## 4. Navigation map
+
+```
+            ┌──────────────── bottom nav ─────────────────┐
+            S-4 Översikt            S-11 Statistik        S-13 Inställningar
+            │  ├ sync line → S-16      └ bar/row → S-12      ├ S-14 Profil
+            │  ├ banner → S-17            └ row → S-8         ├ S-15 Kategorier → editor
+            │  ├ Gör upp → S-9 (global)                       ├ S-19 Återkommande → editor (S-7 fields)
+            │  ├ Nytt konto → S-6                             ├ S-20 Enheter
+            │  ├ FAB → S-7                                    ├ S-21 Inbjudningar (admin)
+            │  └ account → S-5                                ├ S-16 Synkstatus
+            │        ├ FAB → S-7                              └ S-22 Om Kvitt → S-17
+            │        ├ Gör upp det här kontot → S-9 (one account)
+            │        ├ overflow: Ändra konto → S-6, Arkivera, Ta bort
+            │        ├ expense row → S-8 → Ändra → S-7
+            │        └ settlement row → S-10 → Ändra → S-9
+ Onboarding: S-1 → (partner) S-2 → S-4 ;  S-1 → (admin) S-4 with S-3 content → S-21
+ Anywhere: 401 device_revoked → S-18 → S-1
+```
+
+After saving in S-7 the app returns to where the editor was opened from (S-4 or S-5, or S-8 when editing). After confirming S-9 it returns to S-4 or S-5.
+
+## 5. Which screen owns which data
+
+"Owns" = the only place where the data is created or edited. Every other screen only reads it. Offline column per AC-15.2 and §9.9.
+
+| Data | Owned (edited) by | Read by | Offline edit |
+|---|---|---|---|
+| Session, pairing, device token | S-1, S-18 | `:app` routing | no (pairing needs the server) |
+| Own display name and picture | S-14 (and S-2 once for the partner) | S-4 hero, S-13, all avatars, timelines | no ("Kräver anslutning") |
+| Partner's name and picture | partner's own S-14 | everywhere | — |
+| Accounts (name, default ratio, archived, deleted) | S-6 (create, edit), S-5 overflow (archive, unarchive, delete) | S-4, S-7 picker (active only), S-9 rows and "Lägg till konto" (all), S-11/S-12, S-19 picker | no |
+| Categories | S-15 | S-7 grid (active only), S-8, ledger rows, S-11/S-12, S-19 | no |
+| Recurring templates | S-19 | S-8 ("Återkommande: Hyra") | no |
+| Expenses | S-7 (create, edit), S-8 (delete) | S-4 (counts, balances), S-5, S-11/S-12, S-16 (pending) | **yes** |
+| Settlements | S-9 (create, edit), S-10 (delete) | S-4/S-5 balances and rows, S-16 (pending) | **yes** |
+| Audit timeline | server only (never edited) | S-8, S-10 | — |
+| Balances, proposals, analytics | computed, never stored (§5.5) | S-4, S-5, S-9, S-11, S-12 | always available |
+| Devices | S-20 (revoke) | — | no |
+| Invites | S-21 (create, share, cancel), S-3 shortcut | S-3 (expiry of the pending invite) | no |
+| Sync state, rejections | system; S-16 dismisses rejections | S-4 sync line | dismiss works offline |
+| App update | S-17 | S-4 banner, S-22 | no |
+
+## 6. Role differences (admin vs member)
+
+| Where | Admin (Jacob) | Member (Anna) |
+|---|---|---|
+| S-13 | row "Bjud in partner" (before the partner joins) or "Para ihop en enhet" | row "Para ihop en enhet" is **not** shown (AC-2.2: only the admin creates invites). |
+| S-20 | all devices of both users; revoke any except the current | own devices only; revoke own except the current (AC-3.2) |
+| S-21 | available | not reachable |
+| S-3 | shown until the partner joins | never |
+
+Everything else is identical for both users (§3.1).
diff --git a/docs/ux/screens.md b/docs/ux/screens.md
new file mode 100644
index 0000000..a1d7477
--- /dev/null
+++ b/docs/ux/screens.md
@@ -0,0 +1,266 @@
+# Kvitt: screens
+
+Status: U1 proposal (2026-10-17), direction-independent. What each screen contains, in which order, in which states, and what each action does. How it looks is the chosen direction's job (`directions/`); the exact words are in `strings.csv`. Rules (rounding, allocation order, sync, limits) are the spec's and are referenced, never restated as new rules.
+
+Conventions:
+
+* **States** per SPEC §13.3: loaded, empty, loading (first sync only), offline (with pending changes), error (a failed online action), long content (long Swedish names, 7-digit amounts, 200 % font scale).
+* "Kräver anslutning" = the action is disabled while offline per §9.9, with that text as a supporting line or tooltip, never a silent disabled control.
+* "Kunde inte nå servern" = an online action failed with a network error; nothing changed locally (§9.9). Shown as a snackbar on the screen that triggered it, with "Försök igen" when the action is repeatable.
+* Server error codes (§14.3.2) map to inline text where a field is at fault (`name_taken` on the name field) and to a snackbar otherwise. Copy keys in `strings.csv` prefix `error_`.
+* Amounts: tabular figures, right-aligned in lists, never ellipsized; at 200 % they move under the description instead. Direction is always words plus colour (§13.1).
+* Pending-sync marker: a small `cloud_upload` (or `schedule`) icon with TalkBack label "Väntar på synk" on every row and detail whose `pending == true`.
+* Every screen reads local data, so after the first sync there is no loading state except where noted.
+
+---
+
+## S-1 Pairing (Para ihop)
+
+**Purpose:** connect this phone to the household with an invite code (US-1, US-2, AC-2.6, AC-2.7).
+
+Content, in priority order:
+1. App name "Kvitt" and the one-line purpose "Delade utgifter för två".
+2. Invite code field: 12 characters, displayed and typed in groups of 4 (`7K3M-QX9D-2HPA`); input is uppercased and normalised per §8.3 as the user types (hyphens inserted automatically, `O`→`0`, `I`/`L`→`1`). Trailing "Klistra in" button (reads the clipboard; accepts a full link and extracts the code and server).
+3. Explanation: "Koden får du av Jacob: från en länk eller från Inställningar › Bjud in partner." For the admin's first run: "Koden skrivs ut när servern sätts upp."
+4. "Ditt namn" field (1–40, §9.6), required on a first pairing: AC-2.4 sends the display name with the accept call and the app cannot know the invite kind beforehand (the server ignores it for `device` invites). Hidden in the "Para ihop telefonen igen" variant. The partner confirms it again on S-2.
+5. Server address field, prefilled `https://icehack.asuscomm.com:8443` (release), collapsed under "Serveradress" as a secondary row since it rarely changes; expanded automatically when the link carried a different server.
+6. Primary button "Para ihop".
+
+States:
+* **Loaded/empty:** code field focused, keyboard open (uppercase letters and digits).
+* **From link:** both fields filled, focus on "Para ihop".
+* **Loading:** button shows progress "Parar ihop…", fields read-only. After success: full sync runs (S-4 loading state), then the notification permission prompt (AC-2.9).
+* **Error** (inline under the code field, button enabled again): `invite_invalid` "Koden fungerar inte. Den kan ha gått ut eller redan använts. Be Jacob om en ny." · network "Kunde inte nå servern. Kontrollera adressen och anslutningen." · `rate_limited` "För många försök. Vänta en stund och försök igen." · `http://` in release (AC-2.8) "Adressen måste börja med https://" on the server field.
+* **Unrecognised variant** (§14.4.8): header "Para ihop telefonen igen" and a line "Dina utgifter finns kvar på telefonen."
+* **Offline:** the button stays enabled; tapping it gives the network error (pairing never queues).
+* **Long content:** single column, scrolls; at 200 % the code field wraps to two lines of groups.
+
+Actions: Para ihop (primary); Klistra in. No FAB, no bottom nav.
+
+## S-2 Partner profile step (Din profil)
+
+**Purpose:** the partner confirms their name and optionally adds a picture before entering the app (AC-2.10).
+
+Content: 1. "Du går med i Jacobs hushåll" with Jacob's avatar and name; 2. own avatar (initials on `avatar-2` until a picture is chosen) with "Lägg till bild" (Android photo picker, AC-4.3); 3. display name field prefilled from the pairing call (1–40 characters, §9.6); 4. "Fortsätt".
+
+States: **uploading picture** (progress on the avatar; Continue waits); **error** `image_invalid` / `unsupported_media_type` / `payload_too_large` as a snackbar "Bilden kunde inte användas" with the reason; network "Kunde inte nå servern" — the user may continue without the picture; **validation** empty name blocks Continue with "Skriv ett namn".
+
+Actions: Fortsätt (primary, saves name if changed); Lägg till bild / Byt bild / Ta bort bild.
+
+## S-3 Waiting for partner (Väntar på din partner)
+
+**Purpose:** the admin's Home until the partner has joined (AC-5.1).
+
+Content: illustration-free empty state on the Home destination: title "Väntar på din partner"; body "Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk."; if an invite is pending: "Inbjudan gäller till mån 19 okt. 21:40"; primary "Bjud in partner" (→ S-21, creates a `partner` invite; if one is pending S-21 shows it and offers a new one). Bottom nav visible; no FAB; no balance; no "Nytt konto".
+
+States: **offline** "Bjud in partner" disabled with "Kräver anslutning". **Partner joins** while the screen is open: the next sync replaces it with S-4 (empty state "Skapa ert första konto").
+
+## S-4 Home (Översikt)
+
+**Purpose:** answer "who owes whom" in one glance and start the two most common actions: add an expense and settle up (US-6, AC-10.1, AC-10.2).
+
+Content, in priority order:
+1. **Update banner** (only when `UpdateState` is `Available`, `Downloading`, `Failed` or `NeedsInstallPermission`; S-17).
+2. **Balance hero:** both avatars; wording per §9.3 from the viewer's side: "Anna är skyldig dig", "Du är skyldig Anna", or "Ni är kvitt"; the amount (`amount-hero`) on its own line; "Gör upp" button when the global balance ≠ 0. TalkBack reads one sentence: "Du är skyldig Anna 3 250,00 kronor".
+3. **Sync line**, small, under the hero: "Synkad för 3 min sedan" (relative time per §9.8); appended "· 2 ändringar väntar på synk" when `pendingCount > 0`; "Offline" prefix when offline; "Synkar…" while running; warning style with "· 1 ändring synkades inte" when `rejectedCount > 0` or "Telefonens klocka går fel" when `clockSkew`. Tap → S-16. Never a blocking banner.
+4. **Konton** section header with trailing "Nytt konto" (text button; "Kräver anslutning" offline).
+5. **Active accounts** in `created_at` order (AC-6.6): name (ellipsize at one line, 2 lines at 200 %), wording ("Anna är skyldig dig", "Du är skyldig Anna", "Kvitt"), amount (omitted when Kvitt), "11 utgifter i oktober" (plural, current month name). Tap → S-5.
+6. **"Arkiverade (1)"** collapsed row; expands in place to the archived accounts with an "Arkiverat" badge (AC-6.4).
+7. FAB "Lägg till utgift" (extended, collapses to icon-only while scrolling down; TalkBack "Lägg till utgift").
+
+States:
+* **Loading** (first sync after pairing only): hero and rows as skeletons, text "Hämtar era utgifter…"; the FAB is hidden until the first sync finishes or fails (then the local, possibly empty, state is shown).
+* **Empty** (partner joined, no accounts): no hero amount ("Ni är kvitt" is not meaningful yet: hero shows both avatars and "Välkommen, Anna och Jacob"), empty state "Skapa ert första konto" with one line ("Ett konto samlar utgifter som hör ihop, till exempel Vardag eller Japan 2027.") and the primary button "Nytt konto". No FAB (AC-8.6 needs an account).
+* **Settled:** hero "Ni är kvitt", no amount, no "Gör upp".
+* **Offline:** everything works; sync line "Offline · 2 ändringar väntar på synk"; "Nytt konto" disabled with "Kräver anslutning".
+* **Error:** not applicable (local data); a failed sync shows as the sync line in warning style ("Synk misslyckades · försök igen om en stund"), never a dialog.
+* **Long content:** account names up to 60 characters ellipsize in one line (two at 200 %); 7-digit amounts ("1 234 567,89 kr") fit the hero on one line at 100 % and wrap under the wording at 200 %.
+
+Actions: FAB → S-7 (account defaulted per AC-8.6); Gör upp → S-9 global; account row → S-5; Nytt konto → S-6; sync line → S-16; banner → S-17.
+
+## S-5 Account detail (Konto)
+
+**Purpose:** one account's balance and its ledger; settle just this account (AC-10.3, US-8).
+
+Content:
+1. Top app bar: account name (ellipsize), overflow menu: "Ändra konto" (→ S-6), "Arkivera" / "Återställ från arkiv", "Ta bort konto" (enabled only when `deletable`; otherwise disabled with the reason "Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter"). All three "Kräver anslutning" offline.
+2. Header: default split "50 / 50 · Jacob och Anna" (names in percent order, viewer as "du" is not used here; names keep the line unambiguous); "Arkiverat" badge if archived; balance wording and amount (§9.3, account wording "Kvitt"); "Gör upp det här kontot" when non-zero.
+3. Ledger grouped by month, newest first: sticky month header "Oktober · 7 457,90 kr" (household spend of the month, settlements excluded); rows ordered per AC-8.10:
+   * **Expense row:** category icon on its colour container, description (one line), date ("16 okt."), amount (right), effect line (§9.2: "Du lånade ut 432,17 kr" in owed colour, "Du lånade 623,80 kr" in owing colour, "Inte inblandad" / "Betalade för dig själv" neutral), pending marker. Tap → S-8.
+   * **Settlement row:** `payments` icon, "Anna betalade dig 800,00 kr" or "Du betalade Anna 800,00 kr", "· 2 konton" when more than one allocation (AC-11.7), date, pending marker. Tap → S-10.
+4. FAB "Lägg till utgift" (account preset to this account).
+
+States: **empty** (new account) "Inga utgifter än" + "Lägg till den första utgiften i Vardag." (FAB still there); **archived** badge, "Återställ från arkiv" in the header as a tonal button, FAB still offered? **No**: archived accounts are hidden from the account picker for new expenses (AC-6.4), so the FAB is hidden on an archived account; settling stays available; **offline** rows work, overflow actions disabled; **error** archive/delete failures as snackbar (`account_not_empty` "Kontot har fortfarande utgifter, betalningar eller återkommande utgifter"); **long content** month totals and row amounts at 7 digits stay on one line at 100 %; at 200 % the amount moves to the line under the description.
+
+Actions: FAB → S-7; Gör upp det här kontot → S-9 (one fixed row); row → S-8/S-10; overflow actions; archive asks no confirmation (reversible) and shows a snackbar "Vardag arkiverades" with "Ångra"; delete asks "Ta bort kontot Vardag? Det går inte att ångra." (soft delete on the server, but not restorable from the app).
+
+## S-6 Account editor (Nytt konto / Ändra konto)
+
+**Purpose:** create or edit an account's name and default split (AC-6.1 to AC-6.3). Online only.
+
+Content: 1. Name field (1–60, unique: `name_taken` → "Det finns redan ett konto som heter Vardag"); 2. "Standarddelning": a slider (0–100 % in steps of 1, snapping to 50) plus two percent fields "Jacob %" and "Anna %" that always sum to 100 (typing in one updates the other; two decimals allowed, comma or point); quick presets as chips "50/50", "60/40", "40/60"; 3. preview "Av 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"; 4. editing only: hint "Ändringen gäller nya utgifter. Befintliga utgifter behåller sin delning." (AC-6.3); 5. "Spara" (top app bar action and/or bottom button, per direction).
+
+States: **offline** all fields disabled, banner "Kräver anslutning. Konton kan bara ändras när telefonen är online." , Spara disabled; **saving** progress on Spara; **error** network snackbar; validation inline.
+
+Actions: Spara; Avbryt (back; asks "Slänga ändringarna?" only if something changed).
+
+## S-7 Expense editor (Ny utgift / Ändra utgift) — the add-expense flow
+
+**Purpose:** record an expense in ten seconds (SPEC §13.1): open app, tap add, type amount, type description, tap save. Full screen, not a sheet (§13.2). Works offline (AC-8.2).
+
+### Layout, top to bottom
+
+1. **Top app bar:** close (`close`, TalkBack "Avbryt"), title "Ny utgift" / "Ändra utgift", trailing "Spara" text button (enabled when valid). Edit mode adds overflow "Ta bort".
+2. **Amount** (focus on open): display-size `amount-large`, tabular, with "kr" suffix; input rules §9.6 (digits, one `,` or `.`, max two decimals, no sign, max 10 000 000,00). Empty shows "0,00" as a hint in `on-surface-variant`, not as a value.
+3. **Description** field "Beskrivning" (1–100), single line, IME action "Spara" (saves when valid, otherwise moves focus to the first invalid field).
+4. **Defaults row**: one chip per field, each showing its current value, each one tap to change:
+   * **Konto:** "Vardag" (AC-8.6: last used on this phone if still active, else the oldest active). Opens a picker of active accounts (bottom sheet list with balances). Changing the account in mode `default` re-copies the ratio (AC-8.8) and changes the category default only if the user has not picked a category yet.
+   * **Kategori:** icon + name of the default (AC-8.6: last used in that account on this phone if active, else Övrigt). Opens the category grid (icon + colour + name, active categories, `sort_order`).
+   * **Datum:** "Idag" (also "Igår", else "16 okt."); opens the Material date picker; range per §9.6.
+   * **Betalt av:** "Du" or "Anna" with the avatar; **one tap toggles** between the two users (no picker: there are exactly two) (AC-8.3).
+   * **Delning:** "Kontots standard 50/50" — **one tap opens the split section** in place (below the chips) and scrolls it into view.
+5. **Split section** (collapsed by default; expanded when the mode is not `default` or the user tapped Delning):
+   * Mode chooser (single-select connected buttons / segmented, wraps to two rows in Swedish): "Kontots standard 60/40" · "Egen fördelning" · "Exakta belopp" · "Allt på en person".
+   * `ratio`: same control as S-6 (slider + two percent fields summing to 100).
+   * `exact`: two money fields; the user types one, the other is read-only "amount minus typed" (AC-8.7). Typed > amount → inline error "Andelen är större än beloppet" and Save disabled.
+   * `full`: choose the bearer: "Jacob" / "Anna" (two avatar toggles), independent of the payer.
+   * **Live preview** for all modes, from `ExpenseRepository.preview` (the domain computes it; the UI never computes shares): "Du 206,00 kr · Anna 206,00 kr", plus the effect line that the row will show ("Du lånade ut 206,00 kr"). Hidden while `preview` returns null.
+6. **Anteckningar:** collapsed as "Lägg till anteckning"; expands to a multi-line field (0–1000).
+7. **Keypad / keyboard:** while the amount has focus, the numeric input is shown at the bottom (each direction states whether it is an in-app keypad with a guaranteed "," key or the system keyboard with `KeyboardType.Decimal`). Focus moves to the description with "Nästa"/IME or by tapping it; the system text keyboard replaces the keypad.
+8. Edit mode only, at the end: a destructive "Ta bort utgift". (Who created the expense is shown on S-8 as the first timeline line; `ExpenseDraft` carries no creator.)
+
+### The ten-second path (binding interaction)
+
+| Step | User does | Screen does |
+|---|---|---|
+| 1 | Taps FAB on S-4 | S-7 opens with amount focused, keypad up, all defaults set |
+| 2 | Types `412` | Amount shows "412", Save still disabled (description missing) |
+| 3 | Taps "Beskrivning", types "Systembolaget" | Text keyboard; Save enabled |
+| 4 | Taps "Spara" (app bar) or IME "Spara" | Saves locally (`SaveResult.Saved`), closes, snackbar "Utgiften sparades" (no "Ångra": an undo would be a create plus a delete, both audited and pushed to the partner), the new row is visible on return; sync runs after 2 s (AC-15.3) |
+
+The split override is one tap from step 3 (the Delning chip). Payer change is one tap. Nothing else needs touching for the common case.
+
+### States
+
+* **Validation** (inline, from `SaveResult.Invalid` field errors, mapped by token): amount missing "Skriv ett belopp"; amount out of range "Beloppet måste vara mellan 0,01 kr och 10 000 000,00 kr"; description empty "Skriv en beskrivning"; date out of range "Datumet ligger för långt fram"; exact mismatch as above.
+* **Offline:** no difference; after saving, the snackbar says "Sparad. Synkas när du är online."
+* **No accounts:** the editor is not reachable (no FAB). **Only archived accounts:** same (FAB hidden; AC-6.4).
+* **Edit mode for an expense the other user created:** same editor, no warning; the save records the change in the timeline ("Du ändrade …").
+* **Discard:** close with changes asks "Slänga utgiften?" (Släng / Fortsätt redigera).
+* **Long content:** 7-digit amount scales down one type step rather than wrapping; at 200 % the chips wrap to several rows and the split section scrolls; Spara stays in the app bar so it is always reachable.
+
+## S-8 Expense detail (Utgift)
+
+**Purpose:** everything about one expense and who changed what (US-13, AC-12.4).
+
+Content: 1. amount (large) and description; category chip; 2. facts list: Konto, Datum ("fredag 16 oktober 2026"), Betalt av (avatar + name), Delning (mode label, e.g. "Kontots standard 50/50", "Egen fördelning 70/30", "Exakta belopp", "Allt på Anna") and each user's share ("Du 623,80 kr · Anna 623,80 kr"), the effect line; "Återkommande: Hyra" when `recurringTemplateName != null` (AC-8.11); notes (full text); 3. **Historik** timeline, oldest first, newest last (AC-12.4): actor avatar, sentence, "· 16 okt. 19:40"; create line "Anna skapade den här"; update lines one per changed field ("Du ändrade beloppet från 1 147,60 kr till 1 247,60 kr"; split changes as "Du ändrade delningen från 50/50 till 70/30"); recurring source "Skapad automatiskt (återkommande)"; import source "Importerad från Splitwise"; pending last line "Din ändring väntar på synk" (AC-12.5).
+
+Actions: "Ändra" (→ S-7 edit), "Ta bort" (confirm "Ta bort utgiften? Den försvinner för er båda." → soft delete, returns to the previous screen with snackbar "Utgiften togs bort").
+
+States: **deleted** (opened from a deletion notification or the row was deleted while open): banner "Anna tog bort den här utgiften · 17 okt. 09:12", content muted, no Edit/Delete; **pending** marker next to the amount; **missing** (entity not in the database yet): "Utgiften finns inte på telefonen än" with "Synka nu"; **offline** fully readable, edit and delete allowed.
+
+## S-9 Settle up (Gör upp)
+
+**Purpose:** record a payment between the two users and spread it over accounts (US-7, US-8, AC-11.3 to AC-11.6). Works offline.
+
+Content, in order:
+1. **Direction:** "Du betalar Anna" with a swap control "Byt riktning" (TalkBack). Default per AC-11.3 / AC-11.4.
+2. **Amount** (large money field), default = absolute global (or account) balance, empty when zero.
+3. **Fördelning** list, one row per account from `SettleState.lines`: account name; "Skuld 3 084,50 kr" (the payer's debt in that account, `payerNetOre`, words: "Du är skyldig 3 084,50 kr" / "Anna är skyldig dig 1 184,50 kr" when the payer is owed there); editable allocated amount; per-row remove; over-settle warning row text "Det här reglerar mer än som är skyldigt i Japan 2027" (AC-11.5) in warning colour with `warning` icon.
+4. **"Kvar att fördela 0,00 kr"** live indicator; when negative: "250,00 kr för mycket"; colour plus words.
+5. **"Lägg till konto"** → picker of all non-deleted accounts not yet listed (active and archived, AC-11.3).
+6. Info line when an account is left non-zero because the payer is owed there (§9.4 consequence): "Vardag påverkas inte: där är Anna skyldig dig 1 184,50 kr."
+7. Datum (Idag), Anteckning (0–200).
+8. **Bekräfta** (primary, enabled only when `canConfirm`), Avbryt.
+
+Behaviour: editing the total amount re-runs the proposal (`recompute` with `allocations = null`) unless the user has edited a row, in which case it keeps the rows and updates "Kvar att fördela" (the user can tap "Föreslå igen" to re-run). Swap direction re-runs the proposal. Confirming with any warned row shows a dialog repeating the warning(s) (one line per warned row, the same text as on the row: "Det här reglerar mer än som är skyldigt i Japan 2027") with "Bekräfta ändå" / "Ändra".
+
+Per-account variant (from S-5): one fixed row for that account (not removable), no "Lägg till konto", no info line.
+
+States: **no accounts** cannot occur (button hidden); **zero balance** amount empty, rows appear after typing; **editing** title "Ändra betalning", proposal computed with this settlement excluded (AC-11.6); **offline** identical; **long content** at 200 % each row stacks name / debt / field vertically.
+
+## S-10 Settlement detail (Betalning)
+
+Content: "Anna betalade dig 800,00 kr" headline with both avatars and an arrow from payer to recipient; Datum ("tisdag 6 oktober 2026"); Anteckning; **Fördelning** per account with amounts; Historik timeline as in S-8 (diff lines "Du ändrade beloppet …", "… ändrade fördelningen"). Actions: Ändra (→ S-9 editing), Ta bort (confirm "Ta bort betalningen? Saldona räknas om för er båda."). States: deleted, pending, missing as S-8.
+
+## S-11 Analytics (Statistik)
+
+**Purpose:** how much the household spends, when, on what and in which account (US-11, FR-14).
+
+Content:
+1. **Filters** (sticky under the app bar, wrap on two rows in Swedish):
+   * Period chip "Senaste 6 mån" → menu with the presets of AC-14.2 (Denna månad, Senaste 3 mån, Senaste 6 mån, Senaste 12 mån, I år, All tid, Anpassad…); Anpassad opens the Material date range picker; the resolved range is shown under the chips ("1 maj – 17 okt. 2026").
+   * Three binary toggles (connected buttons): Månad / Vecka · Hushållet / Min del · Kategori / Konto.
+2. **Totalt** for the range ("178 211,40 kr"), with the measure in words ("Hushållets utgifter" / "Din del").
+3. **Utgifter över tid:** stacked bars, one per bucket (all buckets, empty ones as zero, AC-14.7), stacked by group (category colours or account colours), y-axis in kronor (no decimals on the axis), x-axis "maj, jun …" or "v. 35"; legend below (group colour + name, only groups present); tap a bar → S-12 for that bucket; horizontally scrollable when there are more buckets than fit (weeks), newest at the right and scrolled into view.
+4. **Per kategori:** horizontal bars sorted descending, each row: icon, name, bar, amount, percent (one decimal, half-up, AC-14.10); tap → S-12.
+5. **Per konto:** same form; archived accounts included with "Arkiverat" badge (AC-14.9).
+
+Grouping applies to chart 1 only; charts 2 and 3 are always shown (AC-14.6). Settlements never count (AC-14.6).
+
+Accessibility: each chart has a content description that summarises it in one sentence ("Utgifter över tid, sex månader: högst i september med 41 484 kronor, lägst i maj med 24 710 kronor") and the bars are focusable one by one ("September: 41 484 kronor, Resor största kategori"). Breakdown rows are ordinary list items.
+
+States: **empty** "Inga utgifter i den här perioden" with "Byt period" (charts hidden, filters remain); **loading** none (local); **offline** identical; **long content** category names ellipsize in breakdown rows, amounts never; at 200 % the breakdown row puts the bar on its own line.
+
+No FAB on Statistik.
+
+## S-12 Analytics drill-down
+
+Content: title naming the filter ("Mat & hushåll, oktober 2026", "v. 41, Vardag", "Japan 2027, senaste 6 mån"), the measure ("Hushållet"/"Min del") as subtitle, total, then expense rows exactly as in S-5 (with the account name added to the row's secondary line, since rows come from several accounts), newest first. Tap → S-8. Empty cannot occur (only reachable from a non-zero number); if data changed meanwhile: "Inga utgifter längre".
+
+## S-13 Settings hub (Inställningar)
+
+Content: 1. household card: own avatar, name, "Du · Administratör" / "Du"; partner's avatar and name ("Anna · Partner"); 2. rows with leading icons and chevrons: Profil (`person`), Kategorier (`category`), Återkommande utgifter (`event_repeat`), Enheter (`devices`), Bjud in partner (`person_add`, admin before join) or Para ihop en enhet (`qr_code`/`add_link`, admin after join), Synkstatus (`sync`, with trailing "2 väntar" when pending), Om Kvitt (`info`, trailing "Uppdatering finns" when available). Rows that lead to online-only screens stay enabled; the screens themselves show their offline state.
+
+## S-14 Profile (Profil)
+
+Content: large avatar with "Byt bild" and "Ta bort bild" (when a picture exists); display name field (1–40); Spara. Picture changes apply immediately on success (no Save needed); name needs Spara. States: offline (all disabled, "Kräver anslutning"), uploading (progress over the avatar), error codes as in S-2.
+
+## S-15 Categories (Kategorier)
+
+**List:** active categories in `sort_order`: drag handle (long-press to drag, plus "Flytta upp"/"Flytta ned" TalkBack actions), icon on colour container, name (custom name or localised default), chevron; "Arkiverade (n)" collapsed section; app bar action "Ny kategori". Reorder saves on drop (online; offline: drag handles hidden and a line "Kräver anslutning för att ändra").
+
+**Editor** (pushed): name field (1–40; for a default category, empty name means "use the default name"; "Återställ namn" button shown when a custom name exists, AC-7.4); live preview chip; **icon grid** of the 30 icons of §9.7 (6 per row at 411 dp, 48 dp targets, selected with outline + check); **colour swatches** of the 12 tokens (each 48 dp, selected with check, TalkBack names "Grön", "Orange" …); Spara; overflow "Arkivera" / "Återställ från arkiv"; "Ta bort kategori" only when allowed, otherwise disabled with the reason: default "Standardkategorier kan inte tas bort, bara arkiveras", in use "Kategorin används av utgifter eller återkommande utgifter. Arkivera den i stället."
+
+## S-16 Sync status (Synkstatus)
+
+Content: 1. status headline: "Synkad för 3 min sedan" / "Synkar…" / "Offline"; 2. clock-skew warning card when `clockSkew` ("Telefonens klocka går fel. Rätta den och synka igen.") — shown first; 3. "Väntande ändringar (2)" list of pending ledger rows (as in S-5, with "ny"/"ändrad"/"borttagen" tag), tap → S-8/S-10; 4. "Ändringar som inte synkades" list from `rejections`: summary (description · amount), the reason text per §14.4.6, the time; tap opens the current version (read-only S-8/S-10); "Ta bort från listan" per item; 5. "Synka nu" (primary; disabled with progress while running; offline "Kräver anslutning").
+
+Empty: "Allt är synkat" with the last sync time.
+
+## S-17 Update available (Uppdatering)
+
+* **Banner on S-4** (top of content, dismissible for the session): "Version 1.2 finns" + "Uppdatera" / "Senare".
+* **First time only**, before downloading (`NeedsInstallPermission` or not yet granted): dialog "Tillåt installation" — "Android frågar en gång om Kvitt får installera appar. Välj Tillåt från den här källan och gå tillbaka." — buttons "Fortsätt" (opens the system setting) / "Avbryt".
+* **Downloading:** banner shows "Laddar ner… 64 %" with a linear progress indicator (determinate); the app stays usable.
+* **Verified:** the system installer appears (out of our hands).
+* **Failed** (AC-16.4): banner "Nedladdningen misslyckades. Försök igen." + "Försök igen".
+* **Offline:** "Uppdatera" disabled with "Kräver anslutning".
+
+## S-18 Device removed (Telefonen har tagits bort)
+
+Full screen, no navigation: icon `phonelink_erase`; title "Den här telefonen har tagits bort"; body "Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats. Be Jacob om en ny kod för att para ihop den igen." (member) / "…Skapa en ny kod på servern med kvitt-server pair-device." (admin, rare); primary "Para ihop igen" → S-1.
+
+## S-19 Recurring expenses (Återkommande utgifter)
+
+**List:** rows: description, amount, account, "Varje månad den 25:e", "Nästa: 25 okt." (`next_occurrence_date`) or "Pausad" badge, payer avatar; app bar "Ny"; empty state "Inga återkommande utgifter" + "Hyra, el och streaming kan läggas in automatiskt varje månad." + "Ny återkommande utgift".
+
+**Editor** (pushed): the S-7 fields without date (amount, description, Konto (active accounts), Kategori, Betalt av, Delning, Anteckningar) plus "Dag i månaden" (1–31 picker; for 29–31 a hint "I kortare månader används sista dagen"), "Första månad" (month picker, default per AC-9.8, back to 12 months), "Sista månad (valfritt)", "Pausad" switch; Spara; overflow "Ta bort" (confirm "Ta bort Hyra? Utgifter som redan skapats finns kvar."). Hint under the fields when editing: "Ändringen gäller kommande utgifter." (AC-9.4). Online only (offline state as S-6).
+
+## S-20 Devices (Enheter)
+
+Rows per AC-3.1: device name, owner avatar and name, "Parad 3 okt.", "Senast sedd nyss", "Kvitt 1.1", "Den här enheten" tag; trailing "Ta bort" (not on the current device). Confirmation dialog: "Ta bort Galaxy S26+? Telefonen loggas ut och dess Kvitt-data raderas nästa gång den ansluter." → "Ta bort" / "Avbryt". States: loading (online fetch: progress), offline ("Kräver anslutning" + nothing listed; this list is not stored locally), error `cannot_revoke_current_device` cannot be triggered from the UI.
+
+## S-21 Invites (Bjud in partner / Para ihop en enhet)
+
+Admin only. Content: actions "Bjud in partner" (only while one user exists, AC-2.1) and "Para ihop en enhet" with a choice "För mig" / "För Anna" (AC-2.2). **Created invite:** the code very large in groups of 4 (`7K3M-QX9D-2HPA`, mono/tabular, selectable), the link, "Gäller till tis 20 okt. 14:02", "Dela" (Android share sheet with a Swedish message: "Gå med i vårt hushåll i Kvitt:  Koden är 7K3M-QX9D-2HPA."), "Kopiera kod". **Pending invites** list: kind ("Partner", "Enhet för Anna"), created, expiry, "Avbryt" (confirm). The code is only visible right after creation (the server stores hashes); later rows say "Koden visas bara när den skapas". States: offline ("Kräver anslutning"), errors `household_full`, network.
+
+## S-22 About (Om Kvitt)
+
+Rows: "Version 1.1 (11)", "Server: https://icehack.asuscomm.com:8443", "Serverversion 1.1.0" (or "Okänd" when `/health` failed), "Öppen källkod-licenser" (→ the standard licences screen), button "Sök efter uppdatering" (→ check; result as S-17 banner or a snackbar "Du har den senaste versionen"; debug builds: hidden, AC-16.5).
+
+---
+
+## Notifications (not screens)
+
+Built locally per AC-13.5 and §13.3 (copy in `strings.csv`, prefix `notif_`). Channel "Aktivitet". Small icon: the app's monochrome icon. Large icon: actor avatar if cached. Tap targets per AC-13.6.
diff --git a/docs/ux/strings.csv b/docs/ux/strings.csv
new file mode 100644
index 0000000..64d757b
--- /dev/null
+++ b/docs/ux/strings.csv
@@ -0,0 +1,465 @@
+key,en,sv,notes
+common_save,Save,Spara,
+common_cancel,Cancel,Avbryt,
+common_delete,Delete,Ta bort,
+common_edit,Edit,Ändra,
+common_close,Close,Stäng,TalkBack label of the close icon
+common_back,Back,Tillbaka,TalkBack label of the back icon
+common_continue,Continue,Fortsätt,
+common_retry,Try again,Försök igen,
+common_more_options,More options,Fler alternativ,TalkBack label of the overflow icon
+common_you,You,Du,"the viewer as a name, sentence start"
+common_you_lower,you,du,the viewer inside a sentence
+common_today,Today,Idag,
+common_yesterday,Yesterday,Igår,
+common_needs_connection,Needs connection,Kräver anslutning,§9.9
+common_could_not_reach_server,Could not reach the server,Kunde inte nå servern,§9.9
+common_offline,Offline,Offline,
+common_pending_sync,Waiting to sync,Väntar på synk,TalkBack label of the pending marker
+common_archived,Archived,Arkiverat,badge on an archived account
+common_archived_category,archived,arkiverad,suffix on an archived category chip
+common_discard_title,Discard changes?,Slänga ändringarna?,
+common_discard,Discard,Släng,
+common_keep_editing,Keep editing,Fortsätt redigera,
+common_undo,Undo,Ångra,
+common_amount_spoken,%1$s Swedish kronor,%1$s kronor,TalkBack form of an amount; %1$s = number without currency
+common_kr_suffix,SEK,kr,suffix/prefix shown in the amount field
+common_ratio,%1$s/%2$s,%1$s/%2$s,"ratio label, percents without % sign, e.g. 60/40"
+common_percent,%1$s %%,%1$s %%,"percent with one decimal, e.g. 23,4 %"
+common_user_split,%1$s %2$s,%1$s %2$s,"name + amount in a split preview: ""Du 206,00 kr"""
+common_separator_dot, · , · ,list separator
+common_and,%1$s and %2$s,%1$s och %2$s,
+nav_home,Home,Översikt,
+nav_analytics,Analytics,Statistik,
+nav_settings,Settings,Inställningar,
+fab_add_expense,Add expense,Lägg till utgift,FAB on S-4 and S-5
+balance_partner_owes_you,%1$s owes you,%1$s är skyldig dig,%1$s partner name; amount shown separately
+balance_you_owe_partner,You owe %1$s,Du är skyldig %1$s,
+balance_partner_owes_you_amount,%1$s owes you %2$s,%1$s är skyldig dig %2$s,one-line form and TalkBack sentence
+balance_you_owe_partner_amount,You owe %1$s %2$s,Du är skyldig %1$s %2$s,
+balance_all_settled,All settled up,Ni är kvitt,"global, §9.3"
+balance_account_settled,Settled up,Kvitt,"account screens and rows, §9.3"
+effect_lent,You lent %1$s,Du lånade ut %1$s,
+effect_borrowed,You borrowed %1$s,Du lånade %1$s,
+effect_not_involved,Not involved,Inte inblandad,
+effect_paid_for_yourself,Paid for yourself,Betalade för dig själv,
+settlement_row_partner_paid_you,%1$s paid you %2$s,%1$s betalade dig %2$s,
+settlement_row_you_paid_partner,You paid %1$s %2$s,Du betalade %1$s %2$s,
+settlement_row_accounts:one,%d account,%d konto,"plural one; shown as ""· 2 konton"" only when > 1"
+settlement_row_accounts:other,%d accounts,%d konton,plural other
+sync_synced_relative,Synced %1$s,Synkad %1$s,%1$s = relative time below
+time_just_now,just now,nyss,
+time_minutes_ago,%d min ago,för %d min sedan,
+time_hours_ago,%d h ago,för %d h sedan,
+sync_syncing,Syncing…,Synkar…,
+sync_pending:one,%d change waiting to sync,%d ändring väntar på synk,plural one
+sync_pending:other,%d changes waiting to sync,%d ändringar väntar på synk,plural other
+sync_rejected:one,%d change was not synced,%d ändring synkades inte,plural one
+sync_rejected:other,%d changes were not synced,%d ändringar synkades inte,plural other
+sync_failed_line,Sync failed · trying again soon,Synk misslyckades · försöker igen snart,
+sync_clock_wrong_short,Your phone's clock is wrong,Telefonens klocka går fel,
+pairing_app_name,Kvitt,Kvitt,not translated
+pairing_tagline,Shared expenses for two,Delade utgifter för två,
+pairing_code_label,Invite code,Inbjudningskod,
+pairing_code_paste,Paste,Klistra in,
+pairing_name_label,Your name,Ditt namn,
+pairing_code_help,You get the code from %1$s: from a link or from Settings › Invite partner.,Koden får du av %1$s: från en länk eller från Inställningar › Bjud in partner.,%1$s admin name when known from the link; else use pairing_code_help_generic
+pairing_code_help_generic,"You get the code from a link, or it is printed when the server is set up.","Koden får du från en länk, eller så skrivs den ut när servern sätts upp.",
+pairing_server_label,Server address,Serveradress,
+pairing_pair,Pair,Para ihop,
+pairing_pairing,Pairing…,Parar ihop…,
+pairing_again_title,Pair this phone again,Para ihop telefonen igen,§14.4.8
+pairing_again_body,Your expenses are still on this phone.,Dina utgifter finns kvar på telefonen.,
+pairing_error_invite_invalid,This code does not work. It may have expired or already been used. Ask for a new one.,Koden fungerar inte. Den kan ha gått ut eller redan använts. Be om en ny.,
+pairing_error_network,Could not reach the server. Check the address and your connection.,Kunde inte nå servern. Kontrollera adressen och anslutningen.,
+pairing_error_rate_limited,Too many attempts. Wait a while and try again.,För många försök. Vänta en stund och försök igen.,
+pairing_error_https,The address must start with https://,Adressen måste börja med https://,AC-2.8
+pairing_error_code_length,The code has 12 characters,Koden har 12 tecken,
+pairing_error_name_required,Enter your name,Skriv ditt namn,
+pairing_notification_rationale,Kvitt tells you when %1$s adds or changes something.,Kvitt säger till när %1$s lägger till eller ändrar något.,shown before the system permission prompt (AC-2.9)
+profile_step_title,Your profile,Din profil,
+profile_step_joining,You are joining %1$s's household,Du går med i %1$ss hushåll,sv genitive: name + s (no apostrophe); names ending in s get no extra s (handled in code: Jonas hushåll)
+profile_add_picture,Add picture,Lägg till bild,
+profile_change_picture,Change picture,Byt bild,
+profile_remove_picture,Remove picture,Ta bort bild,
+profile_name_label,Display name,Visningsnamn,
+profile_name_required,Enter a name,Skriv ett namn,
+profile_picture_failed,The picture could not be used,Bilden kunde inte användas,
+profile_picture_too_large,The picture is too large (max 5 MB),Bilden är för stor (högst 5 MB),payload_too_large
+profile_picture_unsupported,"Use a JPEG, PNG or WebP picture","Använd en bild i JPEG, PNG eller WebP",unsupported_media_type
+profile_picture_invalid,The picture could not be read,Bilden kunde inte läsas,image_invalid
+profile_offline_banner,Needs connection. Your profile can only be changed online.,Kräver anslutning. Profilen kan bara ändras när telefonen är online.,
+waiting_title,Waiting for your partner,Väntar på din partner,
+waiting_body,Kvitt needs both of you before you can start. Invite your partner with a link.,Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk.,
+waiting_invite_expires,Invite valid until %1$s,Inbjudan gäller till %1$s,"%1$s weekday + date + time, e.g. mån 19 okt. 21:40"
+waiting_invite_partner,Invite partner,Bjud in partner,
+home_accounts,Accounts,Konton,
+home_new_account,New account,Nytt konto,
+home_archived_section,Archived (%d),Arkiverade (%d),
+home_settle_up,Settle up,Gör upp,
+home_account_expenses_month:one,%1$d expense in %2$s,%1$d utgift i %2$s,plural one; %2$s month name (lowercase in sv)
+home_account_expenses_month:other,%1$d expenses in %2$s,%1$d utgifter i %2$s,plural other
+home_account_no_expenses_month,No expenses in %1$s,Inga utgifter i %1$s,
+home_loading,Getting your expenses…,Hämtar era utgifter…,first sync only
+home_empty_welcome,"Welcome, %1$s and %2$s","Välkommen, %1$s och %2$s",
+home_empty_title,Create your first account,Skapa ert första konto,
+home_empty_body,"An account collects expenses that belong together, for example Everyday or Japan 2027.","Ett konto samlar utgifter som hör ihop, till exempel Vardag eller Japan 2027.",
+home_expense_saved,Expense saved,Utgiften sparades,
+home_expense_saved_offline,Saved. Syncs when you're online.,Sparad. Synkas när du är online.,
+home_settlement_saved,Payment saved,Betalningen sparades,
+account_default_split,%1$s · %2$s,%1$s · %2$s,"""50 / 50 · Jacob och Anna""; %1$s ratio, %2$s names with common_and"
+account_settle_this,Settle this account,Gör upp det här kontot,
+account_month_total,%1$s · %2$s,%1$s · %2$s,"month header: ""Oktober · 7 457,90 kr"""
+account_edit,Edit account,Ändra konto,
+account_archive,Archive,Arkivera,
+account_unarchive,Restore from archive,Återställ från arkiv,
+account_delete,Delete account,Ta bort konto,
+account_delete_blocked,"The account cannot be deleted while it has expenses, payments or recurring expenses","Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter",AC-6.5; also error account_not_empty
+account_delete_confirm_title,Delete the account %1$s?,Ta bort kontot %1$s?,
+account_delete_confirm_body,This cannot be undone.,Det går inte att ångra.,
+account_archived_snackbar,%1$s was archived,%1$s arkiverades,
+account_unarchived_snackbar,%1$s was restored,%1$s återställdes,
+account_empty_title,No expenses yet,Inga utgifter än,
+account_empty_body,Add the first expense in %1$s.,Lägg till den första utgiften i %1$s.,
+account_editor_new,New account,Nytt konto,
+account_editor_edit,Edit account,Ändra konto,
+account_editor_name,Name,Namn,
+account_editor_default_split,Default split,Standarddelning,
+account_editor_percent_of,%1$s %%,%1$s %%,"label of a percent field: ""Jacob %"""
+account_editor_preview,Of %1$s: %2$s,Av %1$s: %2$s,"""Av 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"""
+account_editor_ratio_change_hint,The change applies to new expenses. Existing expenses keep their split.,Ändringen gäller nya utgifter. Befintliga utgifter behåller sin delning.,AC-6.3
+account_editor_offline_banner,Needs connection. Accounts can only be changed online.,Kräver anslutning. Konton kan bara ändras när telefonen är online.,
+account_editor_name_required,Enter a name,Skriv ett namn,
+account_editor_name_taken,There is already an account called %1$s,Det finns redan ett konto som heter %1$s,name_taken
+account_editor_ratio_sum,The two parts must add up to 100 %,De två delarna måste bli 100 %,bp_sum_not_10000
+expense_editor_new,New expense,Ny utgift,
+expense_editor_edit,Edit expense,Ändra utgift,
+expense_editor_amount,Amount,Belopp,
+expense_editor_amount_hint,"0,00","0,00","hint; en shows 0.00 (built from the formatter, not this string)"
+expense_editor_description,Description,Beskrivning,
+expense_editor_account,Account,Konto,
+expense_editor_category,Category,Kategori,
+expense_editor_date,Date,Datum,
+expense_editor_paid_by,Paid by,Betalt av,
+expense_editor_paid_by_toggle,Paid by %1$s. Double-tap to switch to %2$s.,Betalt av %1$s. Dubbeltryck för att byta till %2$s.,TalkBack
+expense_editor_split,Split,Delning,
+split_mode_default,Account default %1$s,Kontots standard %1$s,"%1$s ratio, e.g. 60/40"
+split_mode_ratio,Custom ratio,Egen fördelning,
+split_mode_exact,Exact amounts,Exakta belopp,
+split_mode_full,All on one person,Allt på en person,
+split_full_on,All on %1$s,Allt på %1$s,
+split_exact_rest,%1$s gets the rest,%1$s får resten,read-only field of the other user
+split_preview_effect,This expense: %1$s,Den här utgiften: %1$s,%1$s an effect line
+expense_editor_notes,Notes,Anteckningar,
+expense_editor_add_note,Add note,Lägg till anteckning,
+expense_editor_delete,Delete expense,Ta bort utgift,
+expense_editor_discard_title,Discard the expense?,Slänga utgiften?,
+expense_editor_keypad_delete,Delete,Radera,TalkBack of the keypad backspace
+expense_editor_keypad_decimal,Decimal separator,Decimaltecken,TalkBack
+expense_editor_next,Next,Nästa,IME action on the amount
+expense_editor_pick_account,Choose account,Välj konto,
+expense_editor_pick_category,Choose category,Välj kategori,
+error_amount_required,Enter an amount,Skriv ett belopp,required on amount
+error_amount_out_of_range,The amount must be between %1$s and %2$s,Beloppet måste vara mellan %1$s och %2$s,amount_out_of_range
+error_description_required,Enter a description,Skriv en beskrivning,
+error_description_too_long,At most %d characters,Högst %d tecken,
+error_date_out_of_range,The date is too far ahead or back,Datumet ligger för långt fram eller bak,§9.6
+error_exact_share_too_large,The share is larger than the amount,Andelen är större än beloppet,"AC-8.7, exact_sum_mismatch"
+error_ratio_invalid,The parts must add up to 100 %,Delarna måste bli 100 %,"bp_out_of_range, bp_sum_not_10000"
+error_generic_invalid,This could not be saved. Check the fields.,Det gick inte att spara. Kontrollera fälten.,fallback for other domain tokens
+expense_detail_title,Expense,Utgift,
+expense_detail_account,Account,Konto,
+expense_detail_date,Date,Datum,
+expense_detail_paid_by,Paid by,Betalt av,
+expense_detail_split,Split,Delning,
+expense_detail_notes,Notes,Anteckning,
+expense_detail_recurring,Recurring: %1$s,Återkommande: %1$s,AC-8.11
+expense_detail_delete_title,Delete the expense?,Ta bort utgiften?,
+expense_detail_delete_body,It disappears for both of you.,Den försvinner för er båda.,
+expense_detail_deleted_snackbar,Expense deleted,Utgiften togs bort,
+expense_detail_deleted_banner,%1$s deleted this expense · %2$s,%1$s tog bort den här utgiften · %2$s,"%1$s actor (Du/Anna), %2$s date+time"
+expense_detail_missing,This expense is not on the phone yet,Utgiften finns inte på telefonen än,
+sync_now,Sync now,Synka nu,
+timeline_title,History,Historik,
+timeline_created,%1$s created this,%1$s skapade den här,"%1$s actor: ""Anna"" or common_you"
+timeline_deleted,%1$s deleted this,%1$s tog bort den här,
+timeline_changed,%1$s changed %2$s from %3$s to %4$s,%1$s ändrade %2$s från %3$s till %4$s,%2$s a timeline_field_* (definite form)
+timeline_changed_added,%1$s added %2$s: %3$s,%1$s lade till %2$s: %3$s,old value empty (e.g. notes)
+timeline_changed_removed,%1$s removed %2$s,%1$s tog bort %2$s,new value empty
+timeline_recurring,Created automatically (recurring),Skapad automatiskt (återkommande),source recurring
+timeline_import,Imported from Splitwise,Importerad från Splitwise,source import
+timeline_pending,Your change is waiting to sync,Din ändring väntar på synk,AC-12.5
+timeline_time,· %1$s,· %1$s,"""· 16 okt. 19:40"""
+timeline_field_account,the account,kontot,
+timeline_field_category,the category,kategorin,
+timeline_field_payer,the payer,betalaren,
+timeline_field_amount,the amount,beloppet,
+timeline_field_description,the description,beskrivningen,
+timeline_field_notes,the notes,anteckningen,
+timeline_field_date,the date,datumet,
+timeline_field_split_mode,the split,delningen,split_mode and splits are shown as one change when both differ
+timeline_field_splits,the shares,andelarna,
+timeline_field_from,who paid,vem som betalade,settlement from_user_id
+timeline_field_to,who received,vem som fick,settlement to_user_id
+timeline_field_note,the note,anteckningen,settlement note
+timeline_field_allocations,the allocation,fördelningen,
+settle_title,Settle up,Gör upp,
+settle_title_edit,Edit payment,Ändra betalning,
+settle_you_pay,You pay %1$s,Du betalar %1$s,
+settle_partner_pays,%1$s pays you,%1$s betalar dig,
+settle_swap,Swap direction,Byt riktning,TalkBack and tooltip
+settle_amount,Amount,Belopp,
+settle_allocation,Allocation,Fördelning,
+settle_row_debt_you_owe,You owe %1$s,Du är skyldig %1$s,payer is the viewer
+settle_row_debt_partner_owes,%1$s owes you %2$s,%1$s är skyldig dig %2$s,
+settle_row_debt_payer_owes,%1$s owes %2$s,%1$s är skyldig %2$s,payer is the partner
+settle_row_settled,Settled up,Kvitt,
+settle_row_remove,Remove %1$s from the allocation,Ta bort %1$s från fördelningen,TalkBack
+settle_over_settle_warning,This settles more than is owed in %1$s,Det här reglerar mer än som är skyldigt i %1$s,AC-11.5
+settle_remaining,Left to allocate %1$s,Kvar att fördela %1$s,
+settle_over_allocated,%1$s too much,%1$s för mycket,
+settle_add_account,Add account,Lägg till konto,
+settle_propose_again,Suggest again,Föreslå igen,
+settle_untouched_info,%1$s is not affected: there %2$s.,%1$s påverkas inte: där %2$s.,"%2$s e.g. ""är Anna skyldig dig 1 184,50 kr"" — build with settle_untouched_owes_you"
+settle_untouched_owes_you,%1$s owes you %2$s,är %1$s skyldig dig %2$s,"fragment for settle_untouched_info (sv word order after ""där"")"
+settle_date,Date,Datum,
+settle_note,Note,Anteckning,
+settle_confirm,Confirm,Bekräfta,
+settle_confirm_dialog_title,Confirm the payment?,Bekräfta betalningen?,
+settle_confirm_anyway,Confirm anyway,Bekräfta ändå,
+settle_change,Change,Ändra,
+error_allocation_sum,The accounts must add up to the amount,Kontona måste bli lika med beloppet,allocation_sum_mismatch
+error_allocation_positive,"Each account needs at least 0,01","Varje konto behöver minst 0,01 kr",allocation_not_positive
+error_no_allocations,Add at least one account,Lägg till minst ett konto,no_allocations
+error_too_many_allocations,At most 50 accounts,Högst 50 konton,too_many_allocations
+error_note_too_long,At most %d characters,Högst %d tecken,
+settlement_detail_title,Payment,Betalning,
+settlement_detail_delete_title,Delete the payment?,Ta bort betalningen?,
+settlement_detail_delete_body,Balances are recalculated for both of you.,Saldona räknas om för er båda.,
+settlement_detail_deleted_banner,%1$s deleted this payment · %2$s,%1$s tog bort den här betalningen · %2$s,
+settlement_detail_deleted_snackbar,Payment deleted,Betalningen togs bort,
+analytics_title,Analytics,Statistik,
+analytics_period,Period,Period,
+range_this_month,This month,Denna månad,
+range_last_3_months,Last 3 months,Senaste 3 mån,
+range_last_6_months,Last 6 months,Senaste 6 mån,
+range_last_12_months,Last 12 months,Senaste 12 mån,
+range_this_year,This year,I år,
+range_all_time,All time,All tid,
+range_custom,Custom…,Anpassad…,
+analytics_range_dates,%1$s – %2$s,%1$s – %2$s,
+granularity_month,Month,Månad,
+granularity_week,Week,Vecka,
+measure_household,Household,Hushållet,
+measure_my_share,My share,Min del,
+group_by_category,Category,Kategori,
+group_by_account,Account,Konto,
+analytics_total_household,Household spending,Hushållets utgifter,
+analytics_total_my_share,Your share,Din del,
+analytics_over_time,Spending over time,Utgifter över tid,
+analytics_by_category,By category,Per kategori,
+analytics_by_account,By account,Per konto,
+analytics_week_label,W%1$d,v. %1$d,x-axis week label
+analytics_axis_thousands,%1$sk,%1$s tkr,y-axis compact
+analytics_empty,No expenses in this period,Inga utgifter i den här perioden,
+analytics_change_period,Change period,Byt period,
+analytics_summary_over_time,"Spending over time, %1$s: highest in %2$s with %3$s, lowest in %4$s with %5$s.","Utgifter över tid, %1$s: högst i %2$s med %3$s, lägst i %4$s med %5$s.",TalkBack summary of chart 1
+analytics_summary_breakdown,%1$s: largest is %2$s with %3$s (%4$s).,%1$s: störst är %2$s med %3$s (%4$s).,TalkBack summary of charts 2 and 3
+analytics_bar_spoken,%1$s: %2$s,%1$s: %2$s,TalkBack per bar
+drilldown_empty,No expenses any more,Inga utgifter längre,
+drilldown_title_category_period,"%1$s, %2$s","%1$s, %2$s","""Mat & hushåll, oktober 2026"""
+settings_title,Settings,Inställningar,
+settings_role_admin,You · Admin,Du · Administratör,
+settings_role_member,You,Du,
+settings_partner,%1$s · Partner,%1$s · Partner,
+settings_profile,Profile,Profil,
+settings_categories,Categories,Kategorier,
+settings_recurring,Recurring expenses,Återkommande utgifter,
+settings_devices,Devices,Enheter,
+settings_invite_partner,Invite partner,Bjud in partner,
+settings_pair_device,Pair a device,Para ihop en enhet,
+settings_sync_status,Sync status,Synkstatus,
+settings_about,About Kvitt,Om Kvitt,
+settings_pending_badge,%d waiting,%d väntar,
+settings_update_badge,Update available,Uppdatering finns,
+categories_title,Categories,Kategorier,
+categories_new,New category,Ny kategori,
+categories_archived,Archived (%d),Arkiverade (%d),
+categories_reorder_offline,Needs connection to change,Kräver anslutning för att ändra,
+categories_move_up,Move up,Flytta upp,TalkBack custom action
+categories_move_down,Move down,Flytta ned,TalkBack custom action
+category_editor_new,New category,Ny kategori,
+category_editor_edit,Edit category,Ändra kategori,
+category_editor_name,Name,Namn,
+category_editor_reset_name,Reset name,Återställ namn,AC-7.4
+category_editor_icon,Icon,Ikon,
+category_editor_color,Colour,Färg,
+category_editor_archive,Archive,Arkivera,
+category_editor_unarchive,Restore from archive,Återställ från arkiv,
+category_editor_delete,Delete category,Ta bort kategori,
+category_delete_blocked_default,"Default categories cannot be deleted, only archived","Standardkategorier kan inte tas bort, bara arkiveras",category_is_default
+category_delete_blocked_in_use,The category is used by expenses or recurring expenses. Archive it instead.,Kategorin används av utgifter eller återkommande utgifter. Arkivera den i stället.,category_in_use
+category_name_required,Enter a name,Skriv ett namn,
+category_default_groceries,Groceries,Mat & hushåll,AC-7.2
+category_default_restaurants,Restaurants,Restaurang & café,
+category_default_home,Home,Hem,
+category_default_utilities,Utilities,Räkningar,
+category_default_transport,Transport,Transport,
+category_default_travel,Travel,Resor,
+category_default_entertainment,Entertainment,Nöje,
+category_default_health,Health,Hälsa,
+category_default_shopping,Shopping,Shopping,
+category_default_other,Other,Övrigt,
+color_green,Green,Grön,"TalkBack of colour swatches, §9.7 tokens"
+color_orange,Orange,Orange,
+color_brown,Brown,Brun,
+color_yellow,Yellow,Gul,
+color_blue,Blue,Blå,
+color_teal,Teal,Blågrön,
+color_purple,Purple,Lila,
+color_red,Red,Röd,
+color_pink,Pink,Rosa,
+color_grey,Grey,Grå,
+color_indigo,Indigo,Indigo,
+color_lime,Lime,Lime,
+icon_shopping_cart,Shopping cart,Kundvagn,"TalkBack of icon picker, §9.7 tokens"
+icon_restaurant,Restaurant,Restaurang,
+icon_local_cafe,Café,Kafé,
+icon_home,Home,Hem,
+icon_bolt,Electricity,El,
+icon_water_drop,Water,Vatten,
+icon_wifi,Internet,Internet,
+icon_directions_car,Car,Bil,
+icon_train,Train,Tåg,
+icon_local_gas_station,Fuel,Bränsle,
+icon_flight,Flight,Flyg,
+icon_hotel,Hotel,Hotell,
+icon_luggage,Luggage,Bagage,
+icon_movie,Film,Film,
+icon_music_note,Music,Musik,
+icon_sports_esports,Games,Spel,
+icon_fitness_center,Fitness,Träning,
+icon_medical_services,Health care,Vård,
+icon_medication,Medicine,Medicin,
+icon_shopping_bag,Shopping bag,Shoppingkasse,
+icon_checkroom,Clothes,Kläder,
+icon_pets,Pets,Husdjur,
+icon_child_care,Children,Barn,
+icon_school,School,Skola,
+icon_card_giftcard,Gift,Present,
+icon_celebration,Party,Fest,
+icon_build,Repairs,Reparationer,
+icon_chair,Furniture,Möbler,
+icon_subscriptions,Subscriptions,Prenumerationer,
+icon_category,Other,Övrigt,
+sync_status_title,Sync status,Synkstatus,
+sync_status_all_synced,Everything is synced,Allt är synkat,
+sync_status_pending_title,Pending changes (%d),Väntande ändringar (%d),
+sync_status_tag_new,new,ny,
+sync_status_tag_changed,changed,ändrad,
+sync_status_tag_deleted,deleted,borttagen,
+sync_status_rejected_title,Changes that were not synced,Ändringar som inte synkades,
+sync_status_dismiss,Remove from list,Ta bort från listan,
+sync_reject_stale,Your change was replaced by a newer change from %1$s,Din ändring ersattes av en nyare ändring från %1$s,§14.4.6
+sync_reject_deleted,This was deleted before your change arrived,Den här togs bort innan din ändring kom fram,
+sync_reject_validation,This change was not valid and was not saved,Ändringen var inte giltig och sparades inte,
+sync_reject_reference,Its account or category no longer exists,Dess konto eller kategori finns inte längre,
+sync_reject_clock_skew,Your phone's clock is wrong. Fix it and sync again,Telefonens klocka går fel. Rätta den och synka igen,
+sync_status_clock_warning,Your phone's clock is wrong. Fix it and sync again.,Telefonens klocka går fel. Rätta den och synka igen.,§9.10
+update_available,Version %1$s available,Version %1$s finns,
+update_install,Update,Uppdatera,
+update_later,Later,Senare,
+update_downloading,Downloading… %1$d %%,Laddar ner… %1$d %%,
+update_failed,"Download failed, try again",Nedladdningen misslyckades. Försök igen.,AC-16.4
+update_permission_title,Allow installation,Tillåt installation,
+update_permission_body,Android asks once whether Kvitt may install apps. Choose Allow from this source and go back.,Android frågar en gång om Kvitt får installera appar. Välj Tillåt från den här källan och gå tillbaka.,AC-16.3; Android setting name differs per locale
+update_up_to_date,You have the latest version,Du har den senaste versionen,
+update_check,Check for update,Sök efter uppdatering,
+removed_title,This phone has been removed,Den här telefonen har tagits bort,
+removed_body_member,Access was removed and all Kvitt data on the phone has been deleted. Ask %1$s for a new code to pair it again.,Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats. Be %1$s om en ny kod för att para ihop den igen.,
+removed_body_admin,Access was removed and all Kvitt data on the phone has been deleted. Create a new code on the server to pair it again.,Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats. Skapa en ny kod på servern för att para ihop den igen.,
+removed_pair_again,Pair again,Para ihop igen,
+recurring_title,Recurring expenses,Återkommande utgifter,
+recurring_new,New,Ny,
+recurring_every_month_on,Every month on day %1$d,Varje månad den %1$d:e,"sv ordinal: 1:a, 2:a, otherwise :e — use recurring_every_month_on_1_2 for 1 and 2"
+recurring_every_month_on_1_2,Every month on day %1$d,Varje månad den %1$d:a,"for days 1, 2, 21, 22, 31 (sv ordinal -a)"
+recurring_next,Next: %1$s,Nästa: %1$s,
+recurring_paused,Paused,Pausad,
+recurring_empty_title,No recurring expenses,Inga återkommande utgifter,
+recurring_empty_body,"Rent, electricity and streaming can be added automatically every month.","Hyra, el och streaming kan läggas in automatiskt varje månad.",
+recurring_empty_action,New recurring expense,Ny återkommande utgift,
+recurring_editor_new,New recurring expense,Ny återkommande utgift,
+recurring_editor_edit,Edit recurring expense,Ändra återkommande utgift,
+recurring_day_of_month,Day of month,Dag i månaden,
+recurring_day_clamp_hint,In shorter months the last day is used,I kortare månader används sista dagen,days 29–31
+recurring_first_month,First month,Första månad,
+recurring_last_month,Last month (optional),Sista månad (valfritt),
+recurring_paused_switch,Paused,Pausad,
+recurring_resume_hint,Months that passed while paused are not created afterwards.,Månader som passerat under pausen skapas inte i efterhand.,§9.5
+recurring_edit_hint,The change applies to upcoming expenses.,Ändringen gäller kommande utgifter.,AC-9.4
+recurring_delete_title,Delete %1$s?,Ta bort %1$s?,
+recurring_delete_body,Expenses already created stay.,Utgifter som redan skapats finns kvar.,
+recurring_offline_banner,Needs connection. Recurring expenses can only be changed online.,Kräver anslutning. Återkommande utgifter kan bara ändras när telefonen är online.,
+error_end_before_start,The last month cannot be before the first,Sista månaden kan inte vara före den första,
+error_start_too_early,At most 12 months back,Högst 12 månader bakåt,
+devices_title,Devices,Enheter,
+devices_paired,Paired %1$s,Parad %1$s,
+devices_last_seen,Last seen %1$s,Senast sedd %1$s,
+devices_app_version,Kvitt %1$s,Kvitt %1$s,
+devices_this_device,This device,Den här enheten,AC-3.1
+devices_revoke,Remove,Ta bort,
+devices_revoke_title,Remove %1$s?,Ta bort %1$s?,
+devices_revoke_body,The phone is signed out and its Kvitt data is deleted the next time it connects.,Telefonen loggas ut och dess Kvitt-data raderas nästa gång den ansluter.,
+devices_offline,Needs connection to show devices,Kräver anslutning för att visa enheter,
+invites_title_partner,Invite partner,Bjud in partner,
+invites_title_device,Pair a device,Para ihop en enhet,
+invites_for_me,For me,För mig,
+invites_for_partner,For %1$s,För %1$s,
+invites_valid_until,Valid until %1$s,Gäller till %1$s,
+invites_share,Share,Dela,
+invites_copy_code,Copy code,Kopiera kod,
+invites_code_copied,Code copied,Koden kopierades,
+invites_share_message,Join our household in Kvitt: %1$s The code is %2$s.,Gå med i vårt hushåll i Kvitt: %1$s Koden är %2$s.,
+invites_pending,Pending invites,Väntande inbjudningar,
+invites_kind_partner,Partner,Partner,
+invites_kind_device,Device for %1$s,Enhet för %1$s,
+invites_code_hidden,The code is only shown when it is created,Koden visas bara när den skapas,
+invites_cancel,Cancel invite,Avbryt inbjudan,
+invites_cancel_title,Cancel the invite?,Avbryta inbjudan?,
+error_household_full,Your partner has already joined,Din partner har redan gått med,household_full
+about_title,About Kvitt,Om Kvitt,
+about_version,Version %1$s (%2$d),Version %1$s (%2$d),
+about_server,Server,Server,
+about_server_version,Server version %1$s,Serverversion %1$s,
+about_server_version_unknown,Unknown,Okänd,
+about_licences,Open-source licences,Öppen källkod-licenser,
+error_forbidden,You are not allowed to do this,Du får inte göra det här,forbidden
+error_not_found,This no longer exists,Det här finns inte längre,not_found
+error_household_incomplete,Your partner needs to join first,Din partner behöver gå med först,household_incomplete
+error_account_not_empty,"The account still has expenses, payments or recurring expenses","Kontot har fortfarande utgifter, betalningar eller återkommande utgifter",account_not_empty
+error_rate_limited,Too many attempts. Wait a while and try again.,För många försök. Vänta en stund och försök igen.,rate_limited
+error_server,Something went wrong on the server. Try again later.,Något gick fel på servern. Försök igen senare.,"internal, unavailable, not_implemented"
+error_app_too_old,Update Kvitt to continue,Uppdatera Kvitt för att fortsätta,protocol_unsupported
+error_validation,The server did not accept the values,Servern godtog inte värdena,validation_failed without a mapped field
+notif_channel_activity,Activity,Aktivitet,§14.6
+notif_expense_created_title,%1$s added an expense,%1$s lade till en utgift,
+notif_expense_created_body,%1$s · %2$s · %3$s,%1$s · %2$s · %3$s,description · amount · account
+notif_expense_updated_title,%1$s changed an expense,%1$s ändrade en utgift,
+notif_expense_updated_body,%1$s · %2$s,%1$s · %2$s,description · amount
+notif_expense_deleted_title,%1$s deleted an expense,%1$s tog bort en utgift,
+notif_expense_deleted_body,%1$s · %2$s,%1$s · %2$s,description · amount
+notif_settlement_created_title,%1$s recorded a payment,%1$s registrerade en betalning,
+notif_settlement_body,%1$s paid %2$s %3$s,%1$s betalade %2$s %3$s,"payer, recipient (dig/you when the viewer), amount"
+notif_settlement_body_to_you,%1$s paid you %2$s,%1$s betalade dig %2$s,recipient is the viewer
+notif_settlement_body_from_you,You paid %1$s %2$s,Du betalade %1$s %2$s,payer is the viewer
+notif_settlement_updated_title,%1$s changed a payment,%1$s ändrade en betalning,
+notif_settlement_deleted_title,%1$s deleted a payment,%1$s tog bort en betalning,
+notif_settlement_deleted_body,%1$s,%1$s,amount
+notif_recurring_created_title,Recurring expense added,Återkommande utgift tillagd,
+notif_recurring_created_body,%1$s · %2$s · %3$s,%1$s · %2$s · %3$s,description · amount · account
+notif_sync_batch_title:one,%1$s made %2$d change,%1$s gjorde %2$d ändring,plural one
+notif_sync_batch_title:other,%1$s made %2$d changes,%1$s gjorde %2$d ändringar,plural other
+notif_sync_batch_body,Open Kvitt to see them,Öppna Kvitt för att se dem,
+notif_generic_title_actor,%1$s changed something in Kvitt,%1$s ändrade något i Kvitt,sync failed
+notif_generic_title,Kvitt,Kvitt,"sync failed, actor unknown"
+notif_generic_body,Open Kvitt to see it,Öppna Kvitt för att se det,
diff --git a/docs/ux/tokens.md b/docs/ux/tokens.md
new file mode 100644
index 0000000..6f27bf4
--- /dev/null
+++ b/docs/ux/tokens.md
@@ -0,0 +1,223 @@
+# Kvitt: design tokens
+
+Status: U1 proposal (2026-10-17). One token **set of names** (binding once the owner picks a direction) with **values for all three directions** (`directions/1-skargard`, `2-kvitto`, `3-kalas`), so `:core:design` can be built from whichever is chosen. After the pick, U2 copies the chosen column into `design-spec.md` and deletes the other two here (one source of truth). Each direction's review page recomputes every contrast ratio below in JavaScript and marks failures; the values here were checked there.
+
+Contrast targets (SPEC §13.1): text ≥ 4.5:1 against the surface it sits on; icons and graphics ≥ 3:1; nothing conveyed by colour alone.
+
+## 1. How tokens map to Compose
+
+| Token group | Compose home in `:core:design` | Notes |
+|---|---|---|
+| Material 3 colour roles (§2) | `lightColorScheme(...)` / `darkColorScheme(...)` | names map 1:1 (`on-primary-container` → `onPrimaryContainer`) |
+| Kvitt semantic roles (§3) | `KvittColors` data class in a `CompositionLocal`, light and dark instance | `owed*`, `owing*`, `settled*`, `warning*`, `pending`, `avatar*` |
+| Category colours (§4) | `KvittColors.category(ColorToken)` and `.categoryContainer(ColorToken)` | `ColorToken` enum from `:core:model` (§14.2.4) |
+| Chart colours (§5) | `KvittColors.chartCategorical[0..7]`, `chartSequential[0..4]`, `chartGrid`, `chartAxisLabel` | |
+| Type (§6) | `Typography(...)` + `KvittTypography(amountHero, amountLarge, amountMedium, amountSmall)` | amounts always `fontFeatureSettings = "tnum"` |
+| Spacing (§7) | `object KvittSpacing { val s1 = 4.dp … }` | |
+| Shapes (§8) | `Shapes(extraSmall…extraLarge)` + `KvittShapes` for the Expressive sizes and custom shapes | |
+| Elevation, motion (§9) | `MotionScheme` (Expressive) or durations/easings constants | |
+
+Dynamic colour (Material You): offered only by Skärgård, as an off-by-default option that replaces §2 only; §3–§5 keep their values in every case.
+
+## 2. Material 3 colour roles
+
+`L` = light, `D` = dark.
+
+| Token | Skärgård L | Skärgård D | Kvitto L | Kvitto D | Kalas L | Kalas D |
+|---|---|---|---|---|---|---|
+| primary | #1A6476 | #8ED0E2 | #23408E | #B3C3FF | #B23A1C | #FFB59F |
+| on-primary | #FFFFFF | #00363F | #FFFFFF | #112A6A | #FFFFFF | #5E1604 |
+| primary-container | #C2E8F3 | #0E4D5C | #DCE3F6 | #2B4590 | #FFDBD0 | #8A2A10 |
+| on-primary-container | #0B4C5C | #C2E8F3 | #14295E | #DDE3FF | #3D0B00 | #FFDBD0 |
+| secondary | #4E5F69 | #B6C9D4 | #4E5466 | #C2C6D8 | #4B6A2A | #B2D38A |
+| on-secondary | #FFFFFF | #20333B | #FFFFFF | #2B3040 | #FFFFFF | #1F3701 |
+| secondary-container | #D3E3EE | #374952 | #DFE2EB | #3B4050 | #D3EFAE | #344F15 |
+| on-secondary-container | #374851 | #D3E5F0 | #1B2030 | #DEE1F1 | #112000 | #D3EFAE |
+| tertiary | #7B5C24 | #E8C48A | #73552A | #DCC39A | #7A5900 | #F2C14E |
+| on-tertiary | #FFFFFF | #422C00 | #FFFFFF | #3E2D10 | #FFFFFF | #402D00 |
+| tertiary-container | #F7DFB4 | #5D4410 | #F1E2C8 | #574425 | #FFDF8E | #5A3F00 |
+| on-tertiary-container | #5A410E | #FFDEA8 | #2E1F06 | #F7E1BD | #261A00 | #FFDF8E |
+| error | #B3261E | #F2B8B5 | #B0261B | #FFB4A8 | #BA1A1A | #FFB4AB |
+| on-error | #FFFFFF | #601410 | #FFFFFF | #690004 | #FFFFFF | #690005 |
+| error-container | #F9DEDC | #8C1D18 | #F9DCD6 | #8E1E14 | #FFDAD6 | #93000A |
+| on-error-container | #8C1D18 | #F9DEDC | #5A0E08 | #FFDAD4 | #410002 | #FFDAD6 |
+| surface | #F6FAFC | #0F1417 | #F7F4EC | #1C1A17 | #FFF8F1 | #1D1415 |
+| on-surface | #171C1F | #DEE3E6 | #1D1B17 | #ECE6DA | #231918 | #F1DFDB |
+| surface-variant | #DBE4E8 | #40484C | #E7E2D6 | #4A463E | #F5DED6 | #53433F |
+| on-surface-variant | #40484C | #BFC8CC | #58534A | #C0B9AB | #53433F | #D8C2BB |
+| surface-container-lowest | #FFFFFF | #0A0F11 | #FFFDF8 | #161512 | #FFFFFF | #170F10 |
+| surface-container-low | #F0F4F7 | #171C1F | #F2EEE5 | #211F1B | #FFF1E8 | #251B1C |
+| surface-container | #EAEFF2 | #1B2124 | #EDE9DF | #26241F | #FAEADF | #2A1F20 |
+| surface-container-high | #E4E9EC | #252B2E | #E6E1D6 | #302D28 | #F4E4D8 | #352A2A |
+| surface-container-highest | #DEE3E6 | #303639 | #E0DBCF | #3B3832 | #EEDED2 | #403434 |
+| surface-dim | #D6DBDE | #0F1417 | #D9D4C8 | #1C1A17 | #E6D6CB | #1D1415 |
+| surface-bright | #F6FAFC | #353A3D | #FBF9F3 | #42403A | #FFF8F1 | #463A3A |
+| inverse-surface | #2C3134 | #DEE3E6 | #32302B | #ECE6DA | #392E2C | #F1DFDB |
+| inverse-on-surface | #EDF1F4 | #2C3134 | #F4F0E7 | #32302B | #FFEDE7 | #392E2C |
+| inverse-primary | #8ED0E2 | #1A6476 | #B3C3FF | #23408E | #FFB59F | #B23A1C |
+| outline | #70787C | #899296 | #7E786C | #8D877A | #85736E | #A08C87 |
+| outline-variant | #BFC8CC | #40484C | #D3CDC0 | #48443C | #D8C2BB | #53433F |
+| scrim | #000000 (40 %) | #000000 | #000000 (42 %) | #000000 | #000000 (38 %) | #000000 |
+
+Key contrast (text role vs surface, L / D): on-surface 16.4 / 14.3 · 15.6 / 14.0 · 16.3 / 14.0; on-surface-variant 8.9 / 10.9 · 6.9 / 8.9 · 8.9 / 10.6; primary 6.4 / 10.8 · 8.7 / 10.1 · 5.7 / 10.6. `outline` is used for graphics only (3.99–5.84).
+
+Direction-specific extra aliases: Kvitto `paper` (= surface-container-lowest in light, surface-container-high in dark; the receipt surface). Kalas `card` (#FFFFFF / #2A1F20) and `on-cat` (#FFFFFF / #1D1415, icon colour on solid category tiles).
+
+## 3. Kvitt semantic roles
+
+| Token | Use | Skärgård L | D | Kvitto L | D | Kalas L | D |
+|---|---|---|---|---|---|---|---|
+| owed | text/graphic: partner owes you | #00695C | #7DD8C5 | #2C6A38 | #92CC9A | #1B6B45 | #8BD8A8 |
+| on-owed | | #FFFFFF | #00382F | #FFFFFF | #0E3818 | #FFFFFF | #003920 |
+| owed-container | | #C2EDE4 | #005047 | #DCEBD8 | #1E3A24 | #C4EFD2 | #0F5233 |
+| on-owed-container | | #00443B | #A3F2E0 | #123A1C | #BFE8C4 | #00210F | #A7F5C3 |
+| owing | text/graphic: you owe partner | #A33E1C | #FFB59E | #A23B25 | #F2A890 | #7E2F84 | #F2AEF0 |
+| on-owing | | #FFFFFF | #5E1700 | #FFFFFF | #4F1708 | #FFFFFF | #4C0F53 |
+| owing-container | | #FFDBCF | #7E2C0E | #F6DDD3 | #4B2418 | #F9D7F6 | #66226C |
+| on-owing-container | | #792A0B | #FFDBCF | #4D170A | #FFDACD | #33003A | #FFD6F8 |
+| settled | "Kvitt" / "Ni är kvitt" | #4E5F69 | #B6C9D4 | #58534A | #C0B9AB | #6E5500 | #F2D45C |
+| settled-container | | #DCE5EA | #2C3B43 | #E6E1D6 | #302D28 | #FFE38F | #5A3F00 |
+| on-settled-container | | #36464F | #D3E5F0 | #2A2823 | #E5DFD2 | #241A00 | #FFE38F |
+| warning | over-settle, sync warnings | #855700 | #F5BD62 | #875400 | #EBC06C | #8A4D00 | #FFB86E |
+| on-warning | | #FFFFFF | #452B00 | #FFFFFF | #3E2900 | #FFFFFF | #4A2800 |
+| warning-container | | #FFDEAD | #633F00 | #F7E3BF | #4D3708 | #FFDDBA | #6A3B00 |
+| on-warning-container | | #633F00 | #FFDEAD | #3B2600 | #FCE3AE | #2C1600 | #FFDDBA |
+| pending | sync-pending marker | #7B5C24 | #E8C48A | #875400 | #EBC06C | #00658F | #86CFFF |
+| avatar-1 | initials background, user slot 1 | #C2E8F3 | #0E4D5C | #3F5E4C | #A5C9B0 | #9A4A00 | #FFB870 |
+| on-avatar-1 | | #0B4C5C | #C2E8F3 | #FFFFFF | #10291A | #FFFFFF | #2E1500 |
+| avatar-2 | initials background, user slot 2 | #F2DDB8 | #5D4410 | #6B4C8A | #CDB4E8 | #2C5F9E | #A8C8FF |
+| on-avatar-2 | | #5A400E | #F7DFB4 | #FFFFFF | #2F1B45 | #FFFFFF | #002F64 |
+
+All owed/owing text values ≥ 5.6:1 on surface in both themes in all directions. Owed/owing hue pairs are chosen to stay distinct for red-green colour blindness (Skärgård sea-green vs coral, Kvitto green vs brick with a square marker, Kalas green vs purple); the words carry the meaning anyway. Avatar slot: a stable function of the user id (e.g. the last hex digit's parity), so each person keeps one colour on both phones (AC-4.5).
+
+## 4. Category colours (SPEC §9.7 token names)
+
+Use: the icon colour on its container (`cat-` on `cat--container`), chart segments, Kalas solid tiles (`on-cat` icon on `cat-`). Never text in light themes. Container rule: Skärgård 16 % (L) / 22 % (D) of the colour over surface; Kvitto 16 % / 24 %; Kalas 18 % / 24 %.
+
+Baseline values (used unchanged by Kvitto and Kalas, and by Skärgård in dark):
+
+| Token | Light | Dark | Skärgård light override |
+|---|---|---|---|
+| green | #2E7D32 | #81C784 | #2A732E |
+| orange | #B84A00 | #FFB74D | #A84300 |
+| brown | #795548 | #C8B4AC | — |
+| yellow | #8A6A00 | #F2D45C | #7D6000 |
+| blue | #1565C0 | #90CAF9 | — |
+| teal | #00796B | #80CBC4 | #006F62 |
+| purple | #7B1FA2 | #D1A0DB | — |
+| red | #C62828 | #EF9A9A | — |
+| pink | #AD1457 | #F48FB1 | — |
+| grey | #5F6B73 | #B0BEC5 | #58646C |
+| indigo | #3949AB | #A5AEE0 | — |
+| lime | #5B6B00 | #C5D86D | — |
+
+Measured: icon vs its container ≥ 3.7:1 (light) and ≥ 4.8:1 (dark) in every direction; colour vs surface ≥ 4.6:1 (light) and ≥ 7.7:1 (dark). These differ from the provisional hex values in SPEC §9.7 (orange, brown, yellow, purple, grey, indigo, lime in light; brown, yellow, purple, indigo, lime in dark): adopting them is a spec change (decisions entry + spec version bump) that the orchestrator makes after the pick (SPEC §13.4 U2).
+
+Per-direction container hex values are listed on each review page (computed, not hand-typed).
+
+## 5. Chart colours
+
+| Token | Skärgård L | Skärgård D | Kvitto L | Kvitto D | Kalas L | Kalas D |
+|---|---|---|---|---|---|---|
+| chart-cat-1 | #1A6476 | #8ED0E2 | #23408E | #B3C3FF | #C2462A | #FF9F86 |
+| chart-cat-2 | #A8741A | #E8C48A | #B4602A | #F0A673 | #4F7A2B | #A9D37F |
+| chart-cat-3 | #5B5FA8 | #B8BCF4 | #2D7A6C | #7DCAB8 | #8C6200 | #F2C14E |
+| chart-cat-4 | #4D8559 | #9BD3A3 | #7B4E9E | #CDA8EE | #2D63AE | #9CC2FF |
+| chart-cat-5 | #A0526E | #EDA9C1 | #7F7225 | #D6C66B | #8A3F92 | #E2A6EA |
+| chart-cat-6 | #2F86A0 | #5FB6D0 | #AE3A5C | #F29CB6 | #00796F | #7FD3C8 |
+| chart-cat-7 | #7A6A58 | #CDB9A1 | #4D6A7A | #A6BFCD | #B0306A | #FF9EC3 |
+| chart-cat-8 | #66737C | #AEBDC8 | #73552A | #DCC39A | #5B6670 | #B7C3CC |
+| chart-seq-1 | #D3ECF3 | #1E3A42 | #D6DDF2 | #2A3352 | #FBD8CC | #4A2A23 |
+| chart-seq-2 | #9FD3E2 | #245A68 | #A9B8E3 | #3C4C80 | #F4A68E | #7A3B2C |
+| chart-seq-3 | #62AFC5 | #2F7C8F | #7890CF | #5A70B0 | #E3765A | #B5563C |
+| chart-seq-4 | #2E869E | #5EADC3 | #4865B1 | #8499DC | #C24A2E | #E8826A |
+| chart-seq-5 | #165A6B | #A3DDEC | #23408E | #B3C3FF | #8E2A14 | #FFB59F |
+| chart-grid | #DEE3E6 | #303639 | #E3DED2 | #37342E | #EBDACF | #3E3233 |
+| chart-axis-label | #40484C | #BFC8CC | = on-surface-variant | = on-surface-variant | #53433F | #D8C2BB |
+
+Rules: categorical colours are assigned to **accounts** in `created_at` order (Vardag 1, Huset 2, Gotland 2026 3, Japan 2027 4, cycling after 8); **categories** always use their own category colour, so a category looks the same in lists and charts. Sequential ramps are for single-series views (a highlighted bucket, "Min del" totals); seq-1 and seq-2 are backgrounds only (< 3:1). Stacked segments are separated by a 2 dp gap in the surface colour so adjacent similar hues stay distinct.
+
+## 6. Typography
+
+| Direction | Typeface(s) | Licence | Amounts |
+|---|---|---|---|
+| Skärgård | Roboto Flex (variable: wght, wdth, opsz) | OFL 1.1 | Roboto Flex with `tnum`; hero uses wdth 84 |
+| Kvitto | IBM Plex Sans (text), IBM Plex Mono (amounts, captions) | OFL 1.1 | Plex Mono (tabular by nature) |
+| Kalas | Nunito | OFL 1.1 | Nunito (default figures tabular, verified in renders) |
+
+Type scale (sp / line height / weight):
+
+| Style | Skärgård | Kvitto | Kalas |
+|---|---|---|---|
+| display-large | 57/64 400 | 57/64 400 (unused) | 57/64 800 |
+| display-medium | 45/52 400 | 45/52 400 (unused) | 45/52 800 |
+| display-small | 36/44 400 | 36/44 400 (unused) | 36/44 800 |
+| headline-large | 32/40 500 | 30/38 500 | 32/40 800 |
+| headline-medium | 28/36 500 | 26/34 500 | 28/36 800 |
+| headline-small | 24/32 500 | 22/30 500 | 24/32 800 |
+| title-large | 22/28 500 | 20/28 500 | 22/28 800 |
+| title-medium | 16/24 560 | 16/24 500 | 16/24 700 |
+| title-small | 14/20 560 | 14/20 600 | 14/20 800 |
+| body-large | 16/24 400 | 16/24 400 | 16/24 500 |
+| body-medium | 14/20 400 | 14/20 400 | 14/20 500 |
+| body-small | 12/16 400 | 12/16 400 | 12/16 600 |
+| label-large | 14/20 560 | 14/20 500 | 14/20 800 |
+| label-medium | 12/16 560 | 12/16 500 | 12/16 700 |
+| label-small | 11/16 560 | 11/16 500 Mono caps, tracking 0.08 em | 11/16 700 |
+| amount-hero | 60/64 720 wdth 84, decimals and kr at 50 % | 44/52 500 Mono | 44/52 900 |
+| amount-large | 32/40 650 wdth 92 | 28/36 500 Mono | 28/36 900 |
+| amount-medium | 16/24 620 | 15/22 500 Mono | 18/24 800 |
+| amount-small | 14/20 520 | 12/16 400 Mono | 15/20 800 |
+
+Font scale: sp follows Android's non-linear scaling (Android 14+, Compose default): text ≥ 24 sp grows less than smaller text, so a 60 sp amount does not double at 200 %. The 200 % frames approximate this. Amounts never ellipsize: they wrap or step down one size.
+
+## 7. Spacing (dp)
+
+All three use one 4 dp scale: `space-1` 4, `space-2` 8, `space-3` 12, `space-4` 16, `space-5` 20, `space-6` 24, `space-8` 32, `space-10` 40, `space-12` 48, `space-16` 64 (Kvitto does not use 40 and 64). Screen gutter 16 in all directions.
+
+| Measure | Skärgård | Kvitto | Kalas |
+|---|---|---|---|
+| card padding | 20 | 20 (receipt) | 20 |
+| home account row | ~72 (card row) | 60 | ~76 (card) |
+| ledger row | 72 | 60 | 72 |
+| settings row | 56 | 56 | 56 |
+| keypad key | 64 | 52 | 52 |
+| gap between grouped items | 2 | rule (1 dp dashed) | 12 |
+
+## 8. Shapes (corner radius, dp)
+
+M3 shape tokens: extraSmall 4, small 8, medium 12, large 16, largeIncreased 20, extraLarge 28, extraLargeIncreased 32, extraExtraLarge 48, full.
+
+| Component | Skärgård | Kvitto | Kalas |
+|---|---|---|---|
+| buttons, chips | full | small 8 | full |
+| text fields | large 16 (outlined) | extraSmall 4 | large 16 (filled, no indicator) |
+| cards | extraLarge 28 | none (receipt zig-zag edge, 10 dp teeth) | 24–28 |
+| balance hero | extraLargeIncreased 32 | top corners 8, zig-zag bottom | extraLarge 28 |
+| grouped/segmented lists | outer 20, inner 4 | none, rules | outer 24, inner 8 |
+| category icon container | medium 12 | extraSmall 4 | MaterialShapes Cookie4Sided (fallback 30 %) |
+| avatars | MaterialShapes Cookie9/12 (fallback circle) | full | MaterialShapes Sunny (fallback circle) |
+| FAB | largeIncreased 20 | medium 12 | largeIncreased 20 |
+| dialogs | extraLarge 28 | large 16 | extraLarge 28 |
+| keypad keys | full, morph to 16 on press | small 8 | full |
+
+## 9. Elevation and motion
+
+| | Skärgård | Kvitto | Kalas |
+|---|---|---|---|
+| elevation model | tonal (surface-container levels); 2 dp shadow on menus, 3 dp on FAB | almost none: rules and tones; shadow only on menus, FAB, dialogs | tonal + soft warm shadow on light cards (0 1 2, 0 2 8, 10 % brown); no shadows in dark |
+| motion scheme | `MotionScheme.expressive()` springs: spatial fast 0.6/800, default 0.8/380, slow 0.8/200; effects 1.0/3800, 1600, 800 | standard easing curves: receipt print reveal 350 ms emphasizedDecelerate, number roll 250 ms per digit, "Kvitt" stamp 200 ms, push shared-axis X 300 ms, tabs fade-through 200 ms | `MotionScheme.expressive()`; seesaw re-tilt spring 0.55/300; amount count-up 400 ms emphasizedDecelerate; bunting drop with 40 ms stagger when settled |
+| stable fallback | `MotionScheme.standard()` / tween 200–500 ms emphasized | (already stable) | `MotionScheme.standard()` |
+| reduced motion | everything except fades snaps | everything becomes a 150 ms crossfade | springs critically damped |
+
+## 10. Icons
+
+Material Symbols (Apache 2.0), as a bundled variable font or vector drawables generated from it.
+
+| | Skärgård | Kvitto | Kalas |
+|---|---|---|---|
+| style | Rounded | Sharp | Rounded |
+| weight / grade / optical size | 400 / 0 / 24 | 300 / 0 / 20–24 | 500–600 / 0 / 24 |
+| fill | 1 for selected nav item and category icons, else 0 | 0, except the selected nav item | 1 for category icons and selected nav item |
+
+Category icons: the 30 tokens of SPEC §9.7. Other icons used by all directions: `home`, `bar_chart`, `settings`, `add`, `close`, `arrow_back`, `more_vert`, `edit`, `delete`, `swap_horiz`, `payments`, `cloud_upload` / `cloud_off` / `cloud_done`, `sync`, `warning`, `info`, `history`, `event_repeat`, `devices`, `person_add`, `content_paste`, `share`, `content_copy`, `drag_indicator`, `archive` / `unarchive`, `backspace`, `system_update`, `phonelink_erase`.