Files
kvitt/docs/ux/directions/2-kvitto/index.html
T
jacobh2 b72ac5bb9e UX sprint U1: three directions, IA, screens, flows, components, tokens, strings
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 <noreply@anthropic.com>
2026-10-18 08:27:51 +02:00

1204 lines
132 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kvitt · 2 Kvitto</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&display=block" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@20..48,300,0..1,0&display=block" rel="stylesheet">
<style>
:root{--sans:'IBM Plex Sans',system-ui,sans-serif;--mono:'IBM Plex Mono',ui-monospace,monospace}
*{box-sizing:border-box}
html,body{margin:0}
body{background:#E8E5DE;color:#22201C;font-family:var(--sans)}
body.frame-mode{background:transparent}
.ms{font-family:'Material Symbols Sharp';font-weight:normal;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;direction:ltr;-webkit-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-variation-settings:'FILL' 0,'wght' 300,'GRAD' 0,'opsz' 24;flex:none;overflow:hidden;width:1em;height:1em;text-align:center}
.ms.fill{font-variation-settings:'FILL' 1,'wght' 300,'GRAD' 0,'opsz' 24}
.ms.s20{font-variation-settings:'FILL' 0,'wght' 300,'GRAD' 0,'opsz' 20}
.ms.fill.s20{font-variation-settings:'FILL' 1,'wght' 300,'GRAD' 0,'opsz' 20}
/* ---------- phone shell ---------- */
.phone{--fs:1;width:411px;height:891px;overflow:hidden;position:relative;display:flex;flex-direction:column;background:var(--surface);color:var(--on-surface);font-family:var(--sans);font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;flex:none}
:where(.phone *){min-width:0}
:where(.phone,.sample) :where(button){font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;text-align:inherit}
.review .phone{border-radius:28px;box-shadow:0 1px 2px rgba(0,0,0,.18),0 12px 32px rgba(40,30,10,.16)}
.sb{flex:none;height:32px;display:flex;align-items:center;justify-content:space-between;padding:0 18px 0 24px;font:500 calc(min(var(--fs),1.15)*13px)/1 var(--sans);letter-spacing:.01em;color:var(--on-surface)}
.sb .r{display:flex;gap:3px;align-items:center}
.content{flex:1 1 auto;min-height:0;overflow:hidden;position:relative;display:flex;flex-direction:column}
.content > *{flex:none}
.gest{flex:none;height:24px;display:flex;align-items:center;justify-content:center;background:var(--surface)}
.gest i{display:block;width:108px;height:4px;border-radius:2px;background:var(--on-surface);opacity:.38}
.gest.on-nav{background:var(--surface-container)}
.gest.on-kp{background:var(--surface-container-low)}
.gest.on-bar{background:var(--surface-container-low)}
/* app bar */
.ab{flex:none;min-height:64px;display:flex;align-items:center;padding:8px 4px}
.ab .ttl{flex:1 1 auto;padding:0 12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ab .ttl.first{padding-left:12px}
.ab.ruled{box-shadow:inset 0 -1px 0 var(--outline-variant)}
.ab .acts{display:flex;align-items:center;gap:0;padding-right:4px}
.ib{width:48px;height:48px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;color:var(--on-surface-variant);flex:none}
.ib.on{color:var(--on-surface)}
.wordmark{font-family:var(--mono);font-weight:600;font-size:calc(var(--fs)*20px);line-height:calc(var(--fs)*28px);letter-spacing:.14em;display:flex;align-items:center;gap:8px;padding-left:12px}
.wordmark .ms{color:var(--primary)}
/* nav bar (Expressive short navigation bar, 64dp) */
.nav{flex:none;min-height:64px;display:flex;background:var(--surface-container);box-shadow:inset 0 1px 0 var(--outline-variant);padding:6px 0 4px}
.nav a{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:var(--on-surface-variant);text-decoration:none}
.nav .ind{width:56px;height:32px;border-radius:16px;display:flex;align-items:center;justify-content:center}
.nav a.sel{color:var(--on-surface)}
.nav a.sel .ind{background:var(--secondary-container);color:var(--on-secondary-container)}
.nav .lb{font:500 calc(min(var(--fs),1.5)*12px)/calc(min(var(--fs),1.5)*16px) var(--sans);text-align:center;padding:0 2px}
.nav a.sel .lb{font-weight:600}
/* FAB (extended, 56dp, medium radius 12) */
.fab{position:absolute;right:16px;height:56px;padding:0 20px 0 16px;border-radius:12px;background:var(--primary);color:var(--on-primary);display:flex;align-items:center;gap:12px;font:500 calc(var(--fs)*15px)/1 var(--sans);letter-spacing:.01em;box-shadow:0 1px 3px rgba(0,0,0,.18),0 4px 8px rgba(0,0,0,.10)}
.fab .ms{font-size:24px}
/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 20px;border-radius:8px;font:500 calc(var(--fs)*14px)/calc(var(--fs)*20px) var(--sans);letter-spacing:.01em;text-align:center;flex:none;white-space:nowrap}
.btn .ms{font-size:18px}
.btn.f{background:var(--primary);color:var(--on-primary)}
.btn.o{box-shadow:inset 0 0 0 1px var(--outline);color:var(--primary)}
.btn.t{color:var(--primary);padding:8px 12px}
.btn.tn{background:var(--secondary-container);color:var(--on-secondary-container)}
.btn.er{color:var(--error)}
.btn.erf{background:var(--error);color:var(--on-error)}
.btn.dis,.btn.f.dis{background:color-mix(in srgb,var(--on-surface) 10%,transparent);color:color-mix(in srgb,var(--on-surface) 40%,transparent);box-shadow:none}
.btn.o.dis{background:none;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--on-surface) 14%,transparent)}
.btn.t.dis{background:none}
.btn.wide{width:100%}
.btn.wrap{white-space:normal}
.hit{min-height:48px;display:flex;align-items:center}
/* type helpers */
.v{color:var(--on-surface-variant)}
.owed{color:var(--owed)} .owing{color:var(--owing)} .settled{color:var(--settled)} .warn{color:var(--warning)} .err{color:var(--error)} .pri{color:var(--primary)} .pend{color:var(--pending)}
.mono,.phone .mono{font-family:var(--mono)}
.caps{text-transform:uppercase;letter-spacing:.08em}
.amt{flex:none;white-space:nowrap;font-family:var(--mono);font-variant-numeric:tabular-nums;text-align:right}
.nowrap{white-space:nowrap}
.ell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* rules */
.dash{height:1px;flex:none;background:repeating-linear-gradient(90deg,var(--outline) 0 4px,transparent 4px 8px);opacity:.55}
.dash.in{margin:0 16px}
.rule{height:1px;flex:none;background:var(--outline-variant)}
.rule.in{margin:0 16px}
.dbl{height:4px;flex:none;border-top:1px solid var(--on-surface);border-bottom:1px solid var(--on-surface);opacity:.75}
/* paper slip with zig-zag edges */
.paper{background:var(--paper);position:relative}
.zz-b{-webkit-mask:conic-gradient(from -45deg at bottom,#0000,#000 1deg 89deg,#0000 90deg) 50% 100%/10px 100%;mask:conic-gradient(from -45deg at bottom,#0000,#000 1deg 89deg,#0000 90deg) 50% 100%/10px 100%;padding-bottom:12px}
.zz-tb{-webkit-mask:conic-gradient(from 135deg at top,#0000,#000 1deg 89deg,#0000 90deg) top/10px 51% repeat-x,conic-gradient(from -45deg at bottom,#0000,#000 1deg 89deg,#0000 90deg) bottom/10px 51% repeat-x;mask:conic-gradient(from 135deg at top,#0000,#000 1deg 89deg,#0000 90deg) top/10px 51% repeat-x,conic-gradient(from -45deg at bottom,#0000,#000 1deg 89deg,#0000 90deg) bottom/10px 51% repeat-x;padding-top:14px;padding-bottom:14px}
/* markers */
.mk{display:inline-block;width:8px;height:8px;flex:none;margin-right:8px;vertical-align:1px}
.mk.owed{background:var(--owed)} .mk.owing{background:var(--owing)} .mk.settled{box-shadow:inset 0 0 0 1.5px var(--settled)}
/* avatars */
.av{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;flex:none;overflow:hidden;position:relative}
.av.init{background:var(--avatar-2);color:var(--on-avatar-2);font-family:var(--sans);font-weight:600}
.av.init1{background:var(--avatar-1);color:var(--on-avatar-1);font-family:var(--sans);font-weight:600}
.av.photo{background:linear-gradient(160deg,var(--photo-a),var(--photo-b))}
.av.photo svg{width:100%;height:100%;display:block}
.pair{display:flex;flex:none}
.pair .av + .av{margin-left:-10px;box-shadow:0 0 0 2px var(--paper)}
.ring{box-shadow:0 0 0 2px var(--paper)}
/* stamp */
.stamp{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--primary);border:2px solid var(--primary);outline:1px solid var(--primary);outline-offset:2px;border-radius:4px;padding:4px 10px;transform:rotate(-5deg);font-size:calc(var(--fs)*15px);line-height:calc(var(--fs)*20px);opacity:.92;flex:none}
.stamp.sm{font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*14px);padding:2px 6px;border-width:1.5px;outline:none;transform:rotate(-3deg);letter-spacing:.1em}
.stamp.muted{color:var(--on-surface-variant);border-color:var(--on-surface-variant);outline-color:var(--on-surface-variant)}
.stamp.red{color:var(--error);border-color:var(--error);outline-color:var(--error)}
.badge{display:inline-flex;align-items:center;font-family:var(--mono);font-weight:500;font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);letter-spacing:.06em;text-transform:uppercase;padding:1px 6px;border-radius:4px;box-shadow:inset 0 0 0 1px var(--outline);color:var(--on-surface-variant);flex:none;white-space:nowrap}
.badge.pri{box-shadow:inset 0 0 0 1px var(--primary);color:var(--primary)}
/* category marker */
.cm{width:28px;height:28px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;flex:none}
.cm .ms{font-size:18px}
.cm.lg{width:40px;height:40px;border-radius:8px}
.cm.lg .ms{font-size:24px}
/* sync line */
.syncl{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 16px;color:var(--on-surface-variant)}
.syncl .ms{font-size:18px}
.syncl .tx{flex:1}
/* balance block */
.bal{margin:0 16px;padding:16px 16px 18px;border-radius:8px 8px 0 0}
.bal-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
.bal-txt{flex:1 1 auto;min-width:0}
.bal-row .btn{margin-left:auto}
.bal-cap{display:flex;flex-wrap:wrap;gap:4px 8px;align-items:center;justify-content:space-between;margin-bottom:12px;color:var(--on-surface-variant)}
/* section header */
.sh{display:flex;align-items:center;justify-content:space-between;min-height:48px;padding:0 4px 0 16px;gap:8px}
.sh .cap{color:var(--on-surface-variant)}
/* home account rows */
.acct{display:block;padding:10px 16px 10px;min-height:60px}
.acct .l1{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:12px}
.acct .nm{flex:1 1 auto}
.acct .amt{margin-left:auto}
.acct .l2{color:var(--on-surface-variant);margin-top:2px}
.arch{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 12px 0 16px;color:var(--on-surface-variant)}
.arch .tx{flex:1}
/* ledger rows */
.colh{display:flex;align-items:center;gap:8px;padding:6px 16px;color:var(--on-surface-variant)}
.mh{display:flex;align-items:baseline;gap:0;padding:14px 16px 6px}
.leader{flex:1 1 auto;min-width:12px;border-bottom:1.5px dotted var(--outline);margin:0 8px;align-self:center;transform:translateY(3px);opacity:.8}
.lr{display:flex;align-items:flex-start;gap:0;padding:10px 16px;min-height:60px}
.lr .dt{width:40px;flex:none;display:flex;flex-direction:column;align-items:flex-start;margin-top:-1px}
.lr .dt .d{font-family:var(--mono);font-weight:500;font-size:calc(var(--fs)*15px);line-height:calc(var(--fs)*20px)}
.lr .dt .m{font-family:var(--mono);font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*14px);color:var(--on-surface-variant)}
.lr .cm{margin:2px 12px 0 4px}
.lr .mn{flex:1 1 auto}
.lr .l1{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px}
.lr .desc{flex:1 1 110px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lr .amt{margin-left:auto}
.lr .l2{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px;margin-top:1px}
.lr .l2 .eff{flex:1 1 auto}
.lr .l2 .pq{margin-left:auto;display:inline-flex;align-items:center;gap:3px;white-space:nowrap}
.lr .l2 .pq .ms{font-size:14px}
.lr.set .cm{background:var(--primary-container);color:var(--on-primary-container)}
/* key/value receipt lines */
.kv{display:flex;flex-wrap:wrap;column-gap:12px;row-gap:0;align-items:baseline;padding:3px 0}
.kv .k{flex:0 0 96px;color:var(--on-surface-variant);font-family:var(--mono);font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*20px);letter-spacing:.08em;text-transform:uppercase}
.kv .val{flex:1 1 160px}
.kv .r{margin-left:auto}
/* editor rows */
.er{display:flex;flex-wrap:wrap;align-items:center;column-gap:12px;min-height:48px;padding:4px 8px 4px 16px}
.er .k{flex:0 0 auto;min-width:88px;white-space:nowrap;color:var(--on-surface-variant);font-family:var(--mono);font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);letter-spacing:.08em;text-transform:uppercase}
.er .val{flex:1 1 150px;display:flex;align-items:center;gap:8px}
.er .chev{color:var(--on-surface-variant);font-size:20px;margin-left:auto}
.er + .er{box-shadow:inset 0 1px 0 color-mix(in srgb,var(--outline-variant) 70%,transparent)}
.seg{display:inline-flex;border-radius:8px;box-shadow:inset 0 0 0 1px var(--outline);overflow:hidden;flex:none}
.seg > *{display:inline-flex;align-items:center;gap:6px;min-height:40px;padding:0 12px;font:500 calc(var(--fs)*14px)/calc(var(--fs)*20px) var(--sans);color:var(--on-surface)}
.seg > * + *{box-shadow:inset 1px 0 0 var(--outline)}
.seg > .on{background:var(--secondary-container);color:var(--on-secondary-container)}
.seg .ms{font-size:18px}
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:6px 12px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--outline);font:500 calc(var(--fs)*14px)/calc(var(--fs)*20px) var(--sans);color:var(--on-surface-variant);white-space:nowrap}
.chip.on{background:var(--secondary-container);color:var(--on-secondary-container);box-shadow:none}
.chip .ms{font-size:18px}
.chip.drop{color:var(--on-surface)}
/* text fields */
.tf{position:relative;border-radius:4px;box-shadow:inset 0 0 0 1px var(--outline);padding:16px 16px;min-height:56px;display:flex;align-items:center;gap:8px}
.tf .lb{position:absolute;top:-9px;left:12px;padding:0 4px;background:var(--fld-bg,var(--surface));color:var(--on-surface-variant);font:400 calc(var(--fs)*12px)/calc(var(--fs)*16px) var(--sans)}
.tf.foc{box-shadow:inset 0 0 0 2px var(--primary)}
.tf.foc .lb{color:var(--primary)}
.tf.bad{box-shadow:inset 0 0 0 2px var(--error)}
.tf.bad .lb{color:var(--error)}
.tf.dis{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--on-surface) 14%,transparent);color:color-mix(in srgb,var(--on-surface) 40%,transparent)}
.tf.dis .lb{color:color-mix(in srgb,var(--on-surface) 40%,transparent)}
.tf .in{flex:1 1 auto}
.tf .sfx{color:var(--on-surface-variant)}
.sup{padding:4px 16px 0;color:var(--on-surface-variant)}
.sup.err{color:var(--error);display:flex;gap:6px;align-items:flex-start}
.sup.err .ms{font-size:16px;margin-top:2px}
.caret{display:inline-block;width:2px;height:1em;background:var(--primary);vertical-align:-0.12em;margin-left:1px}
.uf{padding:8px 16px 8px;box-shadow:inset 0 -1px 0 var(--outline)}
.uf.foc{box-shadow:inset 0 -2px 0 var(--primary)}
.uf .lb{color:var(--on-surface-variant);font:400 calc(var(--fs)*12px)/calc(var(--fs)*16px) var(--sans)}
/* keypad (in-app till keypad) */
.kp{flex:none;background:var(--surface-container-low);box-shadow:inset 0 1px 0 var(--outline-variant);padding:8px 8px 6px;display:grid;grid-template-columns:repeat(3,1fr) 1.2fr;grid-auto-rows:52px;gap:6px}
.kp .k{border-radius:8px;background:var(--key);display:flex;align-items:center;justify-content:center;font:500 calc(var(--fs)*22px)/1 var(--mono);color:var(--on-surface)}
.kp .k.fn{background:var(--secondary-container);color:var(--on-secondary-container)}
.kp .k.go{grid-row:span 3;background:var(--primary-container);color:var(--on-primary-container);flex-direction:column;gap:4px;font:500 calc(var(--fs)*14px)/1.2 var(--sans)}
.kp .k .ms{font-size:24px}
/* lists in settings */
.li{display:flex;align-items:center;gap:16px;padding:8px 16px 8px 16px;min-height:56px}
.li > .ms{color:var(--on-surface-variant);font-size:24px}
.li .tx{flex:1 1 auto}
.li .tr{color:var(--on-surface-variant);text-align:right}
.li .chev{color:var(--on-surface-variant);font-size:20px}
/* banners */
.banner{margin:4px 16px 8px;border-radius:8px;background:var(--surface-container-high);padding:12px 8px 6px 16px}
.banner .top{display:flex;gap:12px;align-items:flex-start;padding-right:8px}
.banner .top > .ms{font-size:24px;color:var(--primary);margin-top:2px}
.banner .acts{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:0 4px;margin-top:4px}
.banner.warnb{background:var(--warning-container);color:var(--on-warning-container)}
.banner.warnb .top > .ms{color:var(--on-warning-container)}
.banner.errb{background:var(--error-container);color:var(--on-error-container)}
.banner.errb .top > .ms{color:var(--on-error-container)}
.prog{height:4px;border-radius:2px;background:var(--secondary-container);overflow:hidden;margin:8px 0 4px}
.prog i{display:block;height:100%;background:var(--primary)}
/* menus & dialogs */
.scrim{position:absolute;inset:0;background:color-mix(in srgb,var(--scrim) 42%,transparent)}
.menu{position:absolute;background:var(--surface-container);border-radius:4px;padding:8px 0;min-width:200px;box-shadow:0 2px 6px rgba(0,0,0,.18),0 8px 24px rgba(0,0,0,.14)}
.menu .mi{display:flex;align-items:center;gap:12px;min-height:48px;padding:0 16px}
.menu .mi .ms{font-size:22px;color:var(--on-surface-variant)}
.menu .mi.dis{color:color-mix(in srgb,var(--on-surface) 40%,transparent)}
.menu .mi.dis .ms{color:color-mix(in srgb,var(--on-surface) 40%,transparent)}
.menu .why{padding:0 16px 8px 50px;color:var(--on-surface-variant)}
.dlg{position:absolute;left:28px;right:28px;background:var(--surface-container-high);border-radius:16px;padding:24px 24px 16px}
.dlg .acts{display:flex;justify-content:flex-end;gap:8px;margin-top:20px;flex-wrap:wrap}
/* charts */
.chart{display:grid;grid-template-columns:auto 1fr;column-gap:8px;padding:0 16px}
.yax{position:relative;height:var(--ph);width:calc(var(--fs)*40px)}
.yax span{display:block;position:absolute;right:0;transform:translateY(-50%);white-space:nowrap;color:var(--chart-axis-label);font:400 calc(var(--fs)*10px)/1 var(--mono)}
.plot{position:relative;height:var(--ph);display:flex;align-items:flex-end;gap:0}
.plot .gl{position:absolute;left:0;right:0;height:1px;background:var(--chart-grid)}
.plot .gl.base{background:var(--outline)}
.plot .col{flex:1;display:flex;flex-direction:column-reverse;align-items:center;position:relative;z-index:1;height:100%}
.plot .col .stk{width:58%;display:flex;flex-direction:column-reverse}
.plot .col .stk i{display:block;width:100%;box-shadow:inset 0 1px 0 var(--surface)}
.plot .col .stk i:last-child{border-radius:2px 2px 0 0}
.xax{display:flex;grid-column:2;padding-top:6px}
.xax span{flex:1;text-align:center;color:var(--chart-axis-label);font:400 calc(var(--fs)*11px)/calc(var(--fs)*14px) var(--mono)}
.xax span.cur{color:var(--on-surface);font-weight:600}
.legend{display:grid;grid-template-columns:repeat(3,1fr);gap:4px 8px;padding:10px 16px 4px}
.legend span{display:flex;align-items:center;gap:6px;color:var(--on-surface-variant);font:400 calc(var(--fs)*11px)/calc(var(--fs)*16px) var(--sans)}
.legend i{width:10px;height:10px;flex:none;border-radius:2px}
.brk{padding:8px 16px 8px;min-height:52px}
.brk .l1{display:flex;flex-wrap:wrap;align-items:center;column-gap:10px}
.brk .l1 .nm{flex:1 1 100px}
.brk .l1 .amt{margin-left:auto}
.brk .l1 .pc{width:52px;text-align:right;color:var(--on-surface-variant)}
.brk .bar{height:6px;margin:6px 0 0 30px;background:var(--surface-container-high);border-radius:1px}
.brk .bar i{display:block;height:100%;border-radius:1px}
.chartcap{display:flex;align-items:baseline;justify-content:space-between;padding:16px 16px 10px}
/* misc */
.pad{padding:0 16px}
.grow{flex:1 1 auto}
.row{display:flex;align-items:center;gap:8px}
.col{display:flex;flex-direction:column}
.bottombar{flex:none;padding:12px 16px;background:var(--surface-container-low);box-shadow:inset 0 1px 0 var(--outline-variant)}
.sk{background:var(--surface-container-high);border-radius:4px;display:block}
.sw{width:44px;height:44px;border-radius:4px;display:flex;align-items:center;justify-content:center}
.switch{width:52px;height:32px;border-radius:16px;box-shadow:inset 0 0 0 2px var(--outline);background:var(--surface-container-highest);position:relative;flex:none}
.switch i{position:absolute;left:8px;top:8px;width:16px;height:16px;border-radius:50%;background:var(--outline)}
.switch.on{background:var(--primary);box-shadow:none}
.switch.on i{left:24px;top:4px;width:24px;height:24px;background:var(--on-primary)}
/* review page */
.review{max-width:1880px;margin:0 auto;padding:40px 32px 80px}
.review h1{font:600 34px/1.15 var(--mono);letter-spacing:.04em;margin:0 0 6px}
.review h2{font:600 22px/1.3 var(--sans);margin:48px 0 12px;padding-top:20px;border-top:1px dashed #9C958A}
.review h3{font:600 16px/1.3 var(--sans);margin:28px 0 8px}
.review p, .review li{font:400 15px/1.55 var(--sans);max-width:900px}
.review .idea{font:400 18px/1.5 var(--sans);color:#4A463E;margin:0 0 14px}
.review table{border-collapse:collapse;font:400 13px/1.4 var(--sans);margin:8px 0 16px;background:#FBF9F3}
.review th,.review td{border:1px solid #D4CEC2;padding:5px 8px;text-align:left;vertical-align:top}
.review th{background:#EFEBE2;font-weight:600}
.review td.n{font-family:var(--mono);text-align:right;white-space:nowrap}
.review code{font:400 12.5px var(--mono);background:#EFEBE2;padding:1px 4px;border-radius:3px}
.fail{color:#B0261B;font-weight:600}
.swatch{display:inline-block;width:28px;height:18px;border-radius:3px;vertical-align:middle;box-shadow:inset 0 0 0 1px rgba(0,0,0,.15)}
.group{margin-top:28px}
.group h3{margin-bottom:4px}
.pairs{display:flex;flex-wrap:wrap;gap:28px;margin-top:10px}
.fr{display:flex;flex-direction:column;gap:8px}
.fr .cap{font:500 13px/1.4 var(--mono);color:#4A463E;max-width:411px}
.fr .note{font:400 13px/1.45 var(--sans);color:#5D584E;max-width:411px}
.samples{display:flex;flex-wrap:wrap;gap:24px}
.sample{width:411px;padding:16px 0;border-radius:16px}
.cols2{display:grid;grid-template-columns:1fr 1fr;gap:24px}
@media (max-width:900px){.cols2{grid-template-columns:1fr}}
</style>
<style id="tokens"></style>
</head>
<body>
<div id="root"></div>
<script>
'use strict';
/* =========================================================================
TOKENS
========================================================================= */
const BASE = {
light:{
'primary':'#23408E','on-primary':'#FFFFFF','primary-container':'#DCE3F6','on-primary-container':'#14295E',
'secondary':'#4E5466','on-secondary':'#FFFFFF','secondary-container':'#DFE2EB','on-secondary-container':'#1B2030',
'tertiary':'#73552A','on-tertiary':'#FFFFFF','tertiary-container':'#F1E2C8','on-tertiary-container':'#2E1F06',
'error':'#B0261B','on-error':'#FFFFFF','error-container':'#F9DCD6','on-error-container':'#5A0E08',
'surface':'#F7F4EC','on-surface':'#1D1B17','surface-variant':'#E7E2D6','on-surface-variant':'#58534A',
'surface-container-lowest':'#FFFDF8','surface-container-low':'#F2EEE5','surface-container':'#EDE9DF','surface-container-high':'#E6E1D6','surface-container-highest':'#E0DBCF','surface-dim':'#D9D4C8','surface-bright':'#FBF9F3',
'inverse-surface':'#32302B','inverse-on-surface':'#F4F0E7','inverse-primary':'#B3C3FF',
'outline':'#7E786C','outline-variant':'#D3CDC0','scrim':'#000000',
'owed':'#2C6A38','on-owed':'#FFFFFF','owed-container':'#DCEBD8','on-owed-container':'#123A1C',
'owing':'#A23B25','on-owing':'#FFFFFF','owing-container':'#F6DDD3','on-owing-container':'#4D170A',
'settled':'#58534A','settled-container':'#E6E1D6','on-settled-container':'#2A2823',
'warning':'#875400','on-warning':'#FFFFFF','warning-container':'#F7E3BF','on-warning-container':'#3B2600',
'pending':'#875400',
'avatar-1':'#3F5E4C','on-avatar-1':'#FFFFFF','avatar-2':'#6B4C8A','on-avatar-2':'#FFFFFF',
'cat-green':'#2E7D32','cat-orange':'#B84A00','cat-brown':'#795548','cat-yellow':'#8A6A00','cat-blue':'#1565C0','cat-teal':'#00796B',
'cat-purple':'#7B1FA2','cat-red':'#C62828','cat-pink':'#AD1457','cat-grey':'#5F6B73','cat-indigo':'#3949AB','cat-lime':'#5B6B00',
'chart-cat-1':'#23408E','chart-cat-2':'#B4602A','chart-cat-3':'#2D7A6C','chart-cat-4':'#7B4E9E','chart-cat-5':'#7F7225','chart-cat-6':'#AE3A5C','chart-cat-7':'#4D6A7A','chart-cat-8':'#73552A',
'chart-seq-1':'#D6DDF2','chart-seq-2':'#A9B8E3','chart-seq-3':'#7890CF','chart-seq-4':'#4865B1','chart-seq-5':'#23408E',
'chart-grid':'#E3DED2',
'photo-a':'#9DB2C4','photo-b':'#55697C','photo-fg':'#EDE7DC'
},
dark:{
'primary':'#B3C3FF','on-primary':'#112A6A','primary-container':'#2B4590','on-primary-container':'#DDE3FF',
'secondary':'#C2C6D8','on-secondary':'#2B3040','secondary-container':'#3B4050','on-secondary-container':'#DEE1F1',
'tertiary':'#DCC39A','on-tertiary':'#3E2D10','tertiary-container':'#574425','on-tertiary-container':'#F7E1BD',
'error':'#FFB4A8','on-error':'#690004','error-container':'#8E1E14','on-error-container':'#FFDAD4',
'surface':'#1C1A17','on-surface':'#ECE6DA','surface-variant':'#4A463E','on-surface-variant':'#C0B9AB',
'surface-container-lowest':'#161512','surface-container-low':'#211F1B','surface-container':'#26241F','surface-container-high':'#302D28','surface-container-highest':'#3B3832','surface-dim':'#1C1A17','surface-bright':'#42403A',
'inverse-surface':'#ECE6DA','inverse-on-surface':'#32302B','inverse-primary':'#23408E',
'outline':'#8D877A','outline-variant':'#48443C','scrim':'#000000',
'owed':'#92CC9A','on-owed':'#0E3818','owed-container':'#1E3A24','on-owed-container':'#BFE8C4',
'owing':'#F2A890','on-owing':'#4F1708','owing-container':'#4B2418','on-owing-container':'#FFDACD',
'settled':'#C0B9AB','settled-container':'#302D28','on-settled-container':'#E5DFD2',
'warning':'#EBC06C','on-warning':'#3E2900','warning-container':'#4D3708','on-warning-container':'#FCE3AE',
'pending':'#EBC06C',
'avatar-1':'#A5C9B0','on-avatar-1':'#10291A','avatar-2':'#CDB4E8','on-avatar-2':'#2F1B45',
'cat-green':'#81C784','cat-orange':'#FFB74D','cat-brown':'#C8B4AC','cat-yellow':'#F2D45C','cat-blue':'#90CAF9','cat-teal':'#80CBC4',
'cat-purple':'#D1A0DB','cat-red':'#EF9A9A','cat-pink':'#F48FB1','cat-grey':'#B0BEC5','cat-indigo':'#A5AEE0','cat-lime':'#C5D86D',
'chart-cat-1':'#B3C3FF','chart-cat-2':'#F0A673','chart-cat-3':'#7DCAB8','chart-cat-4':'#CDA8EE','chart-cat-5':'#D6C66B','chart-cat-6':'#F29CB6','chart-cat-7':'#A6BFCD','chart-cat-8':'#DCC39A',
'chart-seq-1':'#2A3352','chart-seq-2':'#3C4C80','chart-seq-3':'#5A70B0','chart-seq-4':'#8499DC','chart-seq-5':'#B3C3FF',
'chart-grid':'#37342E',
'photo-a':'#6F8496','photo-b':'#3A4A59','photo-fg':'#D9D2C5'
}};
const CATS = ['green','orange','brown','yellow','blue','teal','purple','red','pink','grey','indigo','lime'];
const CAT_MIX = {light:0.16, dark:0.24}; // container = colour at 16 % (light) / 24 % (dark) over --surface
function hex2rgb(h){h=h.replace('#','');return [0,2,4].map(i=>parseInt(h.substr(i,2),16));}
function rgb2hex(a){return '#'+a.map(v=>Math.round(v).toString(16).padStart(2,'0')).join('').toUpperCase();}
function mix(c,bg,p){const a=hex2rgb(c),b=hex2rgb(bg);return rgb2hex(a.map((v,i)=>v*p+b[i]*(1-p)));}
function lum(h){return hex2rgb(h).map(v=>{v/=255;return v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4);}).reduce((s,v,i)=>s+v*[0.2126,0.7152,0.0722][i],0);}
function cr(a,b){const x=lum(a),y=lum(b);return (Math.max(x,y)+0.05)/(Math.min(x,y)+0.05);}
const T = {};
for (const th of ['light','dark']){
const t = {...BASE[th]};
for (const c of CATS) t[`cat-${c}-container`] = mix(t[`cat-${c}`], t['surface'], CAT_MIX[th]);
t['chart-axis-label'] = t['on-surface-variant'];
T[th] = t;
}
/* aliases used by components (not part of the binding token list) */
const ALIAS = {
light:{paper:'surface-container-lowest', key:'surface-container-lowest'},
dark:{paper:'surface-container-high', key:'surface-container-highest'}
};
(function emitCss(){
let css='';
for (const th of ['light','dark']){
css += `[data-theme="${th}"]{` + Object.entries(T[th]).map(([k,v])=>`--${k}:${v};`).join('') +
Object.entries(ALIAS[th]).map(([k,v])=>`--${k}:var(--${v});`).join('') + `}\n`;
}
document.getElementById('tokens').textContent = css;
})();
const TYPE = [
// name, family, size sp, line sp, weight, tracking em, use
['display-large','Plex Sans',57,64,400,-0.01,'not used'],
['display-medium','Plex Sans',45,52,400,0,'not used'],
['display-small','Plex Sans',36,44,400,0,'not used'],
['headline-large','Plex Sans',30,38,500,0,'empty and removed states'],
['headline-medium','Plex Sans',26,34,500,0,'pairing title'],
['headline-small','Plex Sans',22,30,500,0,'dialog titles, receipt description'],
['title-large','Plex Sans',20,28,500,0,'top app bar titles'],
['title-medium','Plex Sans',16,24,500,0.005,'account names, list primary text'],
['title-small','Plex Sans',14,20,600,0.005,'banner titles, card titles'],
['body-large','Plex Sans',16,24,400,0,'expense descriptions, field values'],
['body-medium','Plex Sans',14,20,400,0,'balance wording, body copy'],
['body-small','Plex Sans',12,16,400,0.01,'effect lines, supporting text'],
['label-large','Plex Sans',14,20,500,0.01,'buttons, chips'],
['label-medium','Plex Sans',12,16,500,0.02,'navigation labels'],
['label-small','Plex Mono',11,16,500,0.08,'receipt captions and column heads (set in caps by the component)'],
['amount-hero','Plex Mono',44,52,500,-0.02,'editor amount, receipt total'],
['amount-large','Plex Mono',28,36,500,-0.01,'balance block, settle amount, range total'],
['amount-medium','Plex Mono',15,22,500,0,'amounts in rows'],
['amount-small','Plex Mono',12,16,400,0,'amounts in captions, axis labels, timeline times'],
];
(function emitType(){
let css='';
for (const [n,f,s,l,w,tr] of TYPE){
const fam = f==='Plex Mono'?'var(--mono)':'var(--sans)';
const fsz = s>=24?`max(${s}px, min(calc(var(--fs)*${s}px), calc(var(--fs)*24px)))`:`calc(var(--fs)*${s}px)`;
const lh = s>=24?(l/s).toFixed(3):`calc(var(--fs)*${l}px)`;
css += `.t-${n}{font-family:${fam};font-weight:${w};font-size:${fsz};line-height:${lh};letter-spacing:${tr}em;${f==='Plex Mono'?'font-variant-numeric:tabular-nums;':''}}\n`;
}
css += `.t-label-small{text-transform:uppercase}\n`;
document.getElementById('tokens').textContent += css;
})();
/* =========================================================================
FORMATTING
========================================================================= */
const NB=' ';
function grp(n,sep){return String(n).replace(/\B(?=(\d{3})+(?!\d))/g,sep);}
function kr(ore){ore=Math.abs(Math.round(ore));return grp(Math.floor(ore/100),NB)+','+String(ore%100).padStart(2,'0')+NB+'kr';}
function krn(ore){ore=Math.abs(Math.round(ore));return grp(Math.floor(ore/100),NB)+','+String(ore%100).padStart(2,'0');}
function sek(ore){ore=Math.abs(Math.round(ore));return 'SEK'+NB+grp(Math.floor(ore/100),',')+'.'+String(ore%100).padStart(2,'0');}
function wrapAmt(t){const i=t.lastIndexOf(NB);const j=t.indexOf(NB);if(t.startsWith('SEK'))return `<span class="nowrap">SEK</span> <span class="nowrap">${t.slice(4)}</span>`;return `<span class="nowrap">${t.slice(0,i)}</span> <span class="nowrap">${t.slice(i+1)}</span>`;}
function krInt(kr_){return grp(kr_,NB);}
function pct(part,total){const p=Math.floor((2*part*1000+total)/(2*total));return Math.floor(p/10)+','+(p%10)+NB+'%';}
const MON_S=['jan.','feb.','mars','apr.','maj','juni','juli','aug.','sep.','okt.','nov.','dec.'];
const MON_L=['januari','februari','mars','april','maj','juni','juli','augusti','september','oktober','november','december'];
const MON_EN=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const WD_L=['söndag','måndag','tisdag','onsdag','torsdag','fredag','lördag'];
const WD_S=['sön','mån','tis','ons','tors','fre','lör'];
function ymd(iso){return iso.split('-').map(Number);}
function dList(iso){const [y,m,d]=ymd(iso);return `${d}${NB}${MON_S[m-1]}${y!==2026?' '+y:''}`;}
function dDetail(iso){const [y,m,d]=ymd(iso);const w=new Date(Date.UTC(y,m-1,d)).getUTCDay();return `${WD_L[w]} ${d} ${MON_L[m-1]} ${y}`;}
function dShortWd(iso){const [y,m,d]=ymd(iso);const w=new Date(Date.UTC(y,m-1,d)).getUTCDay();return `${WD_S[w]} ${d}${NB}${MON_S[m-1]}`;}
/* =========================================================================
DATA
========================================================================= */
const CAT = {
groceries:{sv:'Mat & hushåll',icon:'shopping_cart',c:'green'},
restaurants:{sv:'Restaurang & café',icon:'restaurant',c:'orange'},
home:{sv:'Hem',icon:'home',c:'brown'},
utilities:{sv:'Räkningar',icon:'bolt',c:'yellow'},
transport:{sv:'Transport',icon:'directions_car',c:'blue'},
travel:{sv:'Resor',icon:'flight',c:'teal'},
entertainment:{sv:'Nöje',icon:'movie',c:'purple'},
health:{sv:'Hälsa',icon:'medical_services',c:'red'},
shopping:{sv:'Shopping',icon:'shopping_bag',c:'pink'},
other:{sv:'Övrigt',icon:'category',c:'grey'},
};
const ACC = {
vardag:{name:'Vardag',split:[50,50],net:118450,oct:11,arch:false,chart:1},
huset:{name:'Huset',split:[60,40],net:-308450,oct:4,arch:false,chart:2},
gotland:{name:'Gotland 2026',split:[50,50],net:0,oct:0,arch:true,chart:3},
japan:{name:'Japan 2027',split:[50,50],net:-135000,oct:2,arch:false,chart:4},
};
const GLOBAL = ACC.vardag.net+ACC.huset.net+ACC.gotland.net+ACC.japan.net; // -325000
// expense rows: [date, description, cat, amountOre, effect('l'|'b'|'n'|'s'), effectOre, pending]
const VARDAG = [
{month:'Oktober',total:745790,rows:[
['2026-10-16','ICA Maxi Lindhagen','groceries',124760,'b',62380],
['2026-10-15','Systembolaget','entertainment',41200,'l',20600],
['2026-10-14','SL-kort 30 dagar, Anna','transport',106000,'l',106000,true],
['2026-10-12','Willys Hornstull','groceries',86435,'l',43217],
['2026-10-10','Babette','restaurants',118000,'b',59000],
['2026-10-09','Apotek Hjärtat','health',18990,'s'],
['2026-10-07','ICA Nära Hornstull','groceries',78560,'b',39280],
{set:true,date:'2026-10-06',from:'Anna',to:'dig',amt:80000},
['2026-10-04','Clas Ohlson','home',45900,'b',22950],
['2026-10-03','Netflix','entertainment',17900,'l',8950],
['2026-10-02','Pressbyrån','restaurants',6800,'n'],
['2026-10-01','Lidl Liljeholmen','groceries',101245,'l',50622],
]},
{month:'September',total:941240,rows:[
['2026-09-28','Coop Södermalm','groceries',73420,'b',36710],
['2026-09-26','Hemköp','groceries',51280,'l',25640],
]},
];
const GOTLAND = [
{month:'Juli',total:438000,rows:[
{set:true,date:'2026-07-20',from:'Du',to:'Anna',amt:55000},
['2026-07-14','Visby Strandby, stuga','travel',274000,'b',137000],
['2026-07-12','Destination Gotland, färja','travel',164000,'l',82000],
]},
];
// sanity: Vardag October total
(function(){const s=VARDAG[0].rows.filter(r=>Array.isArray(r)).reduce((a,r)=>a+r[3],0);if(s!==745790)console.warn('Vardag total',s);})();
const AN_MONTHS=['maj','juni','juli','aug.','sep.','okt.'];
const AN = [ // category, values in öre per month maj..okt
['groceries',[642000,698000,587000,661000,694000,391000]],
['restaurants',[189000,264000,398000,172000,211000,124800]],
['home',[1315000,1382000,1299000,1346000,1338000,393350]],
['utilities',[178000,169000,154000,162000,187400,147500]],
['transport',[106000,106000,245000,106000,106000,106000]],
['travel',[0,125000,438000,0,1486000,1216000]],
['entertainment',[41000,59000,64000,38000,52000,59100]],
['health',[0,29000,0,45000,0,18990]],
['shopping',[0,62000,0,136000,74000,0]],
['other',[0,0,0,0,0,0]],
];
const AN_ACC = [['huset',7986000],['vardag',6695140],['japan',2702000],['gotland',438000]];
const WEEKS = ['35','36','37','38','39','40','41','42'];
const WK = { vardag:[138000,172000,124000,156000,131000,187000,112000,98000], huset:[52000,31000,89000,42000,226000,16000,129000,67000], japan:[0,0,115000,0,0,240000,0,234000] };
const WK_CAT_SPLIT = [['home',0.31],['groceries',0.24],['travel',0.22],['restaurants',0.08],['utilities',0.06],['transport',0.05],['entertainment',0.02],['health',0.01],['shopping',0.01]];
/* =========================================================================
COMPONENTS
========================================================================= */
function ic(name,o={}){const s=o.size||24;return `<span class="ms${o.fill?' fill':''}${s<=20?' s20':''}${o.cls?' '+o.cls:''}" style="font-size:${s}px" ${o.label?`role="img" aria-label="${o.label}"`:'aria-hidden="true"'}>${name}</span>`;}
function ib(name,label,o={}){return `<button class="ib${o.on?' on':''}" aria-label="${label}">${ic(name,{fill:o.fill,size:o.size})}</button>`;}
function avJ(s=40,ring=false){return `<span class="av photo${ring?' ring':''}" style="width:${s}px;height:${s}px" role="img" aria-label="Jacob"><svg viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="15.5" r="6.6" style="fill:var(--photo-fg)"/><path d="M6.5 40c1.2-8.6 6.6-13.2 13.5-13.2S32.3 31.4 33.5 40z" style="fill:var(--photo-fg)"/></svg></span>`;}
function avA(s=40,ring=false){return `<span class="av init${ring?' ring':''}" style="width:${s}px;height:${s}px;font-size:calc(min(var(--fs),1.25)*${Math.round(s*0.42)}px)" role="img" aria-label="Anna">A</span>`;}
function catMark(key,lg=false){const c=CAT[key];return `<span class="cm${lg?' lg':''}" style="background:var(--cat-${c.c}-container);color:var(--cat-${c.c})">${ic(c.icon,{size:lg?24:18})}</span>`;}
function statusBar(){return `<div class="sb"><span>14:02</span><span class="r">${ic('signal_cellular_alt',{size:16,fill:true})}${ic('wifi',{size:16,fill:true})}${ic('battery_5_bar',{size:16,fill:true})}</span></div>`;}
function appBar({nav,title,acts='',ruled=false,wordmark=false,titleCls='t-title-large'}){
const navBtn = nav==='back'?ib('arrow_back','Tillbaka',{on:true}):nav==='close'?ib('close','Stäng',{on:true}):'';
const t = wordmark?`<div class="wordmark grow">${ic('receipt_long',{size:22})}KVITT</div>`:`<div class="ttl ${titleCls}${nav?'':' first'}">${title}</div>`;
return `<header class="ab${ruled?' ruled':''}">${navBtn}${t}<div class="acts">${acts}</div></header>`;
}
const NAV_SV=[['receipt_long','Översikt'],['bar_chart','Statistik'],['settings','Inställningar']];
const NAV_EN=[['receipt_long','Home'],['bar_chart','Analytics'],['settings','Settings']];
function navBar(sel,lang='sv'){const it=lang==='en'?NAV_EN:NAV_SV;return `<nav class="nav">${it.map(([i,l],k)=>`<a class="${k===sel?'sel':''}" ${k===sel?'aria-current="page"':''}><span class="ind">${ic(i,{fill:k===sel})}</span><span class="lb">${l}</span></a>`).join('')}</nav>`;}
function fab(label,bottom,icon='add'){return `<button class="fab" style="bottom:${bottom}px">${ic(icon)}<span>${label}</span></button>`;}
function screen({appbar='',body='',nav=null,navLang='sv',fabHtml='',bottom='',overlay='',gestCls=''}){
const g = nav!==null?'on-nav':gestCls;
return `${statusBar()}${appbar}<main class="content">${body}</main>${bottom}${nav!==null?navBar(nav,navLang):''}<div class="gest ${g}"><i></i></div>${fabHtml}${overlay}`;
}
function dash(cls='in'){return `<div class="dash ${cls}"></div>`;}
/* ---- wording ---- */
function wording(net,lang='sv',scope='account'){
if(lang==='en'){if(net>0)return 'Anna owes you';if(net<0)return 'You owe Anna';return scope==='global'?'All settled up':'Settled up';}
if(net>0)return 'Anna är skyldig dig';if(net<0)return 'Du är skyldig Anna';return scope==='global'?'Ni är kvitt':'Kvitt';
}
const sem = n=>n>0?'owed':n<0?'owing':'settled';
/* ---- balance block (home) ---- */
function balanceBlock(net,o={}){
const lang=o.lang||'sv', money=lang==='en'?sek:kr;
const cap = `<div class="bal-cap"><span class="row" style="gap:10px"><span class="pair">${avJ(24)}${avA(24)}</span><span class="t-label-small">${lang==='en'?'Balance · all accounts':'Saldo · alla konton'}</span></span><span class="t-amount-small">${lang==='en'?'17 Oct':'lör 17 okt.'}</span></div>`;
if(net===0){
return `<section class="bal paper zz-b" aria-label="Ni är kvitt">${cap}<div class="bal-row"><div class="bal-txt"><div class="t-headline-small">${wording(0,lang,'global')}</div><div class="t-body-small v">Alla konton går jämnt ut.</div></div><span class="stamp" aria-hidden="true">Kvitt</span></div></section>`;
}
return `<section class="bal paper zz-b" aria-label="${wording(net,lang)} ${money(net)}">${cap}<div class="bal-row"><div class="bal-txt"><div class="t-body-medium"><span class="mk ${sem(net)}"></span>${wording(net,lang)}</div><div class="t-amount-large ${sem(net)}" style="font-family:var(--mono)">${wrapAmt(money(net))}</div></div><button class="btn o">${lang==='en'?'Settle up':'Gör upp'}</button></div></section>`;
}
function syncLine(kind='ok',lang='sv'){
if(kind==='offline') return `<button class="syncl" style="width:100%" aria-label="Offline, 2 ändringar väntar på synk. Öppna synkstatus">${ic('cloud_off',{size:18,cls:'pend'})}<span class="tx t-body-small"><span class="pend" style="font-weight:600">Offline</span> · 2 ändringar väntar på synk</span>${ic('chevron_right',{size:18})}</button>`;
if(kind==='loading') return `<div class="syncl">${ic('sync',{size:18,cls:'pri'})}<span class="tx t-body-small">Hämtar era utgifter…</span></div>`;
return `<button class="syncl" style="width:100%">${ic('cloud_done',{size:18})}<span class="tx t-body-small">${lang==='en'?'Synced 3 min ago':'Synkad för 3 min sedan'}</span>${ic('chevron_right',{size:18})}</button>`;
}
function acctRow(a,lang='sv',settledAll=false){
const net = settledAll?0:a.net, money=lang==='en'?sek:kr;
const n = settledAll?a.oct:a.oct;
const cnt = lang==='en'?`${n} expense${n===1?'':'s'} in October`:`${n} ${n===1?'utgift':'utgifter'} i oktober`;
const amt = net===0?`<span class="t-amount-medium amt settled">${lang==='en'?'Settled up':'Kvitt'}</span>`:`<span class="t-amount-medium amt ${sem(net)}">${money(net)}</span>`;
const w = net===0?'':`${wording(net,lang)} · `;
return `<button class="acct" style="width:100%" aria-label="${a.name}, ${net===0?(lang==='en'?'Settled up':'Kvitt'):wording(net,lang)+' '+money(net)}, ${cnt}"><div class="l1"><span class="t-title-medium nm ell">${a.name}</span>${amt}</div><div class="l2 t-body-small"><span class="mk ${sem(net)}"></span>${w}${cnt}</div></button>`;
}
function updateBanner(kind){
if(kind==='avail') return `<div class="banner"><div class="top">${ic('system_update')}<div class="grow"><div class="t-title-small">Version 1.2 finns</div><div class="t-body-small v">Ny version av Kvitt · 6,4${NB}MB</div></div></div><div class="acts"><button class="btn t">Senare</button><button class="btn t">Uppdatera</button></div></div>`;
if(kind==='progress') return `<div class="banner" style="padding-bottom:12px"><div class="top">${ic('downloading')}<div class="grow"><div class="row" style="justify-content:space-between"><span class="t-title-small">Version 1.2</span><span class="t-amount-small v">64${NB}%</span></div><div class="t-body-small v">Laddar ner… 64${NB}%</div><div class="prog" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100"><i style="width:64%"></i></div></div></div></div>`;
if(kind==='failed') return `<div class="banner errb"><div class="top">${ic('error')}<div class="grow"><div class="t-title-small">Version 1.2</div><div class="t-body-small">Nedladdningen misslyckades. Försök igen.</div></div></div><div class="acts"><button class="btn t" style="color:var(--on-error-container)">Senare</button><button class="btn t" style="color:var(--on-error-container)">Försök igen</button></div></div>`;
return '';
}
/* ---- ledger ---- */
function effectOf(r){switch(r[4]){case 'l':return [`Du lånade ut ${kr(r[5])}`,'owed'];case 'b':return [`Du lånade ${kr(r[5])}`,'owing'];case 'n':return ['Inte inblandad','v'];case 's':return ['Betalade för dig själv','v'];}}
function ledgerRow(r){
if(!Array.isArray(r)){
const [d,m]=dList(r.date).split(NB);
const txt = r.from==='Anna'?`Anna betalade dig`:`Du betalade Anna`;
return `<div class="lr set" role="button" aria-label="${dList(r.date)}, ${txt} ${kr(r.amt)}${r.count?`, ${r.count} konton`:''}"><div class="dt"><span class="d">${d}</span><span class="m">${m}</span></div><span class="cm">${ic('payments',{size:18})}</span><div class="mn"><div class="l1"><span class="t-body-large desc">${txt}</span><span class="t-amount-medium amt">${kr(r.amt)}</span></div><div class="l2 t-body-small v"><span class="eff">Betalning${r.count?` · ${r.count} konton`:''}</span></div></div></div>`;
}
const [d,m]=dList(r[0]).split(NB); const [eff,cls]=effectOf(r);
const pend = r[6]?`<span class="pq t-label-small pend">${ic('cloud_upload',{size:14})}Väntar på synk</span>`:'';
return `<div class="lr" role="button" aria-label="${dList(r[0])}, ${r[1]}, ${CAT[r[2]].sv}, ${kr(r[3])}, ${eff}${r[6]?', väntar på synk':''}"><div class="dt"><span class="d">${d}</span><span class="m">${m}</span></div>${catMark(r[2])}<div class="mn"><div class="l1"><span class="t-body-large desc">${r[1]}</span><span class="t-amount-medium amt">${kr(r[3])}</span></div><div class="l2 t-body-small"><span class="eff ${cls}">${eff}</span>${pend}</div></div></div>`;
}
function monthHead(name,total){return `<div class="mh" role="heading" aria-label="${name} · ${kr(total)}"><span class="t-label-small" style="color:var(--on-surface)">${name}</span><span class="leader"></span><span class="t-amount-small">${kr(total)}</span></div>`;}
function ledger(months){return months.map((g,gi)=>`${gi?'<div class="dash in" style="margin-top:4px"></div>':''}${monthHead(g.month,g.total)}${g.rows.map((r,i)=>(i?dash():'')+ledgerRow(r)).join('')}`).join('');}
/* ---- receipt (detail) helpers ---- */
function kv(k,v,o={}){return `<div class="kv"><span class="k">${k}</span><span class="val t-body-medium ${o.cls||''}">${v}</span>${o.r?`<span class="r t-amount-medium amt">${o.r}</span>`:''}</div>`;}
function trail(lines){
return `<div class="pad" style="padding-top:20px"><div class="row" style="justify-content:space-between;margin-bottom:6px"><span class="t-label-small v">Historik</span><span class="t-label-small v">${ic('history',{size:16})}</span></div><div class="dash"></div>${lines.map(l=>`<div style="display:flex;gap:12px;padding:8px 0;align-items:baseline" ${l.pending?'aria-label="Din ändring väntar på synk"':''}><span class="t-amount-small v" style="flex:0 0 ${l.pending?'auto':'92px'};white-space:nowrap">${l.pending?ic('cloud_upload',{size:16,cls:'pend'}):l.t}</span><span class="t-body-small ${l.pending?'pend':''}" style="flex:1 1 auto;${l.pending?'font-weight:600':''}">${l.x}</span></div>`).join('<div class="dash" style="opacity:.3"></div>')}</div>`;
}
/* ---- editor helpers ---- */
function keypad(go='Nästa'){
const k = x=>`<button class="k" aria-label="${x}">${x}</button>`;
return `<div class="kp" role="group" aria-label="Sifferknappar">${k(7)}${k(8)}${k(9)}<button class="k fn" aria-label="Radera">${ic('backspace')}</button>${k(4)}${k(5)}${k(6)}<button class="k go">${ic('arrow_downward')}<span>${go}</span></button>${k(1)}${k(2)}${k(3)}${k('00')}${k(0)}${k(',')}</div>`;
}
function payerSeg(sel='me'){return `<div class="seg" role="radiogroup" aria-label="Betalt av"><button class="${sel==='me'?'on':''}" role="radio" aria-checked="${sel==='me'}">${sel==='me'?ic('check',{size:18}):avJ(20)}Du</button><button class="${sel==='anna'?'on':''}" role="radio" aria-checked="${sel==='anna'}">${sel==='anna'?ic('check',{size:18}):avA(20)}Anna</button></div>`;}
function amountLine({amt,caret=false,desc,compact=false,muted=false}){
return `<div class="pad" style="padding-top:${compact?6:10}px;padding-bottom:${compact?6:4}px"><div class="row" style="justify-content:space-between"><span class="t-label-small v">Belopp</span><span class="t-label-small v">SEK</span></div><div style="display:flex;align-items:baseline;justify-content:flex-end;gap:8px;flex-wrap:wrap;padding:${compact?0:2}px 0 6px"><span class="t-amount-hero ${muted?'v':''}" style="${compact?'font-size:max(32px,min(calc(var(--fs)*32px),calc(var(--fs)*24px)));line-height:1.25':''}">${amt}${caret?'<span class="caret"></span>':''}</span><span class="t-title-large v mono">kr</span></div><div class="dbl"></div></div>`;
}
function modeChips(sel){const m=['Kontots standard 50/50','Egen fördelning','Exakta belopp','Allt på en person'];return `<div class="chips" role="radiogroup" aria-label="Delning">${m.map(x=>`<button class="chip${x===sel?' on':''}" role="radio" aria-checked="${x===sel}">${x===sel?ic('check',{size:18}):''}${x}</button>`).join('')}</div>`;}
/* =========================================================================
SCREENS
========================================================================= */
const HOME_FAB = fab('Lägg till utgift',104);
function homeBody({net=GLOBAL,lang='sv',sync='ok',banner='',settled=false,archOpen=false}){
const accts = ['vardag','huset','japan'].map(k=>acctRow(ACC[k],lang,settled));
const arch = archOpen
? `<div class="arch" style="margin-top:4px"><span class="tx t-label-large">${lang==='en'?'Archived (1)':'Arkiverade (1)'}</span>${ic('expand_less',{size:22})}</div><button class="acct" style="width:100%;opacity:.92"><div class="l1"><span class="t-title-medium nm ell v">Gotland 2026</span><span class="badge">Arkiverat</span><span class="t-amount-medium amt settled">Kvitt</span></div><div class="l2 t-body-small"><span class="mk settled"></span>0 utgifter i oktober</div></button>`
: `<button class="arch" style="width:100%;margin-top:4px" aria-expanded="false"><span class="tx t-label-large">${lang==='en'?'Archived (1)':'Arkiverade (1)'}</span>${ic('expand_more',{size:22})}</button>`;
return `${syncLine(sync,lang)}${banner}${balanceBlock(settled?0:net,{lang})}
<div class="sh" style="margin-top:12px"><span class="t-label-small cap">${lang==='en'?'Accounts':'Konton'}</span><button class="btn t">${ic('add')}${lang==='en'?'New account':'Nytt konto'}</button></div>
<div class="dash in"></div>
${accts.join(dash())}
<div class="dash in"></div>
${arch}`;
}
function homeScreen(o={}){
return screen({appbar:appBar({wordmark:true,acts:''}),body:homeBody(o),nav:0,navLang:o.lang||'sv',fabHtml:fab(o.lang==='en'?'Add expense':'Lägg till utgift',104),overlay:o.overlay||''});
}
function accountHeader({name,split,net,arch=false,settle=true}){
return `<div class="pad" style="padding-top:2px;padding-bottom:14px">
<div class="row" style="flex-wrap:wrap;gap:8px"><span class="t-amount-small v">${split}</span>${arch?'<span class="stamp sm muted">Arkiverat</span>':''}</div>
<div style="display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 12px;margin-top:10px">
<div style="flex:1 1 160px"><div class="t-body-medium"><span class="mk ${sem(net)}"></span>${net===0?'Kvitt':wording(net)}</div>${net===0?`<div class="t-amount-large amt settled" style="text-align:left">0,00${NB}kr</div>`:`<div class="t-amount-large amt ${sem(net)}" style="text-align:left">${kr(net)}</div>`}</div>
${settle?`<button class="btn o">Gör upp det här kontot</button>`:''}
${arch?`<button class="btn o">${ic('unarchive')}Återställ från arkiv</button>`:''}
</div></div>`;
}
function accountScreen({menu=false}={}){
const acts = ib('more_vert','Fler alternativ');
const colh = `<div class="dash in"></div><div class="colh t-label-small"><span style="width:40px">Datum</span><span style="margin-left:44px" class="grow">Beskrivning</span><span>Belopp</span></div><div class="rule in"></div>`;
const overlay = menu?`<div class="menu" style="top:84px;right:12px;width:268px" role="menu"><div class="mi">${ic('edit')}<span class="t-body-large">Ändra konto</span></div><div class="mi">${ic('archive')}<span class="t-body-large">Arkivera</span></div><div class="mi dis" aria-disabled="true">${ic('delete')}<span class="t-body-large">Ta bort konto</span></div><div class="why t-body-small">Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</div></div>`:'';
return screen({appbar:appBar({nav:'back',title:'Vardag',acts}),body:accountHeader({name:'Vardag',split:'50 / 50 · Jacob och Anna',net:ACC.vardag.net})+colh+ledger(VARDAG),fabHtml:fab('Lägg till utgift',40),overlay});
}
function accountArchived(){
const colh = `<div class="dash in"></div><div class="colh t-label-small"><span style="width:40px">Datum</span><span style="margin-left:44px" class="grow">Beskrivning</span><span>Belopp</span></div><div class="rule in"></div>`;
const note = `<div class="pad" style="padding-top:20px"><div class="row v t-body-small" style="align-items:flex-start;gap:8px">${ic('info',{size:18})}<span>Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter.</span></div></div>`;
return screen({appbar:appBar({nav:'back',title:'Gotland 2026',acts:ib('more_vert','Fler alternativ')}),body:accountHeader({split:'50 / 50 · Jacob och Anna',net:0,arch:true,settle:false})+colh+ledger(GOTLAND)+note});
}
function splitControl(j,disabled=false){
const a=100-j; const dc=disabled?' dis':'';
return `<div class="pad" style="padding-top:22px"><div class="t-label-small v" style="margin-bottom:12px">Standardfördelning</div>
<div style="position:relative;height:44px;margin:0 2px;${disabled?'opacity:.38':''}" role="slider" aria-label="Jacobs andel" aria-valuenow="${j}" aria-valuemin="0" aria-valuemax="100">
<div style="position:absolute;left:0;top:14px;height:16px;width:calc(${j}% - 6px);background:var(--primary);border-radius:8px 2px 2px 8px"></div>
<div style="position:absolute;right:0;top:14px;height:16px;width:calc(${a}% - 6px);background:var(--secondary-container);border-radius:2px 8px 8px 2px"></div>
<div style="position:absolute;left:calc(${j}% - 2px);top:0;width:4px;height:44px;border-radius:2px;background:var(--primary)"></div>
<div style="position:absolute;right:6px;top:20px;width:4px;height:4px;border-radius:2px;background:var(--primary)"></div>
</div>
<div style="display:flex;gap:12px;margin-top:18px;flex-wrap:wrap">
<div style="flex:1 1 140px"><div class="tf${dc}"><span class="lb">Jacob</span><span class="in t-body-large mono">${j}</span><span class="sfx">%</span></div></div>
<div style="flex:1 1 140px"><div class="tf${dc}"><span class="lb">Anna</span><span class="in t-body-large mono">${a}</span><span class="sfx">%</span></div></div>
</div>
<div class="sup t-body-small" style="padding-left:0">Andelarna blir alltid 100${NB}% tillsammans. Två decimaler går bra.</div>
<div class="paper zz-tb" style="margin-top:20px;padding-left:16px;padding-right:16px;${disabled?'opacity:.5':''}"><div class="t-label-small v" style="margin-bottom:4px">Förhandsvisning</div><div class="t-body-medium">Av ${kr(10000)}: Jacob ${kr(j*100)}, Anna ${kr(a*100)}</div></div>
</div>`;
}
function accountEditor(offline=false){
const save = `<button class="btn f${offline?' dis':''}" style="margin-right:8px">Spara</button>`;
const off = offline?`<div class="banner warnb" style="margin-top:0"><div class="top">${ic('cloud_off')}<div class="grow"><div class="t-title-small">Kräver anslutning</div><div class="t-body-small">Konton sparas direkt på servern. Anslut telefonen till internet för att skapa kontot.</div></div></div><div style="height:6px"></div></div>`:'';
return screen({appbar:appBar({nav:'close',title:'Nytt konto',acts:save}),body:`${off}<div class="pad" style="padding-top:12px"><div class="tf${offline?' dis':' foc'}"><span class="lb">Namn</span><span class="in t-body-large">Sommarstugan${offline?'':'<span class="caret"></span>'}</span></div><div class="sup t-body-small" style="padding-left:16px">Till exempel Vardag, Huset eller en resa.</div></div>${splitControl(60,offline)}`});
}
function editorRows({konto='Vardag',cat='entertainment',datum=`Idag · ${dShortWd('2026-10-17')}`,payer='me',deln='Kontots standard 50/50',preview,open=false}){
const c=CAT[cat];
return `<div class="er" role="button"><span class="k">Konto</span><span class="val t-body-large">${konto}</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er" role="button"><span class="k">Kategori</span><span class="val t-body-large">${catMark(cat)}${c.sv}</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er" role="button"><span class="k">Datum</span><span class="val t-body-large">${datum}</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er"><span class="k">Betalt av</span><span class="val">${payerSeg(payer)}</span></div>
<div class="er" role="button" aria-expanded="${open}"><span class="k">Delning</span><span class="val" style="flex-direction:column;align-items:flex-start;gap:0"><span class="t-body-large">${deln}</span>${preview?`<span class="t-amount-small v">${preview}</span>`:''}</span>${ic(open?'expand_less':'expand_more',{size:20,cls:'chev'})}</div>`;
}
function editorNew(){
const save=`<button class="btn f" style="margin-right:8px">Spara</button>`;
const body = `${amountLine({amt:'412',caret:true})}
<div class="uf" style="margin:2px 16px 6px;padding-left:0;padding-right:0"><div class="lb">Beskrivning</div><div class="t-body-large">Systembolaget</div></div>
${editorRows({preview:`Du ${kr(20600)} · Anna ${kr(20600)}`})}
<div style="padding:0 8px"><button class="btn t">${ic('notes')}Lägg till anteckning</button></div>`;
return screen({appbar:appBar({nav:'close',title:'Ny utgift',acts:save}),body,bottom:keypad(),gestCls:'on-kp'});
}
function editorSplit(){
const save=`<button class="btn f dis" style="margin-right:8px" aria-disabled="true">Spara</button>`;
const body = `<div class="pad" style="padding-top:2px;padding-bottom:8px"><div style="display:flex;align-items:baseline;gap:12px"><span class="t-body-medium v ell grow">SL-kort 30 dagar, Anna</span><span class="t-amount-large amt">1${NB}060,00${NB}kr</span></div></div><div class="dbl in" style="margin:0 16px"></div>
<div class="er" style="margin-top:4px"><span class="k">Betalt av</span><span class="val">${payerSeg('me')}</span></div>
<div class="er" aria-expanded="true"><span class="k">Delning</span><span class="val t-body-large">Exakta belopp</span>${ic('expand_less',{size:20,cls:'chev'})}</div>
<div class="pad" style="padding-top:4px;padding-bottom:12px">${modeChips('Exakta belopp')}</div>
<div class="pad"><div class="tf bad" style="--fld-bg:var(--surface)"><span class="lb">Jacob (du)</span><span class="in t-body-large mono" style="text-align:right">1${NB}200,00<span class="caret" style="background:var(--error)"></span></span><span class="sfx">kr</span></div><div class="sup err t-body-small">${ic('error',{size:16})}<span>Andelen är större än beloppet</span></div>
<div class="tf" style="margin-top:14px;box-shadow:inset 0 0 0 1px var(--outline-variant);background:var(--surface-container-low)"><span class="lb" style="background:linear-gradient(var(--surface) 50%,var(--surface-container-low) 50%)">Anna · räknas ut</span><span class="in t-body-large mono v" style="text-align:right">—</span><span class="sfx">kr</span></div>
<div class="sup t-body-small" style="padding-left:16px">Annas andel är beloppet minus din.</div></div>
<div class="pad" style="padding-top:10px"><div class="paper zz-tb" style="padding-left:14px;padding-right:14px"><div class="row" style="justify-content:space-between"><span class="t-label-small v">Förhandsvisning</span><span class="t-label-small err">Går inte ihop</span></div><div class="t-amount-medium" style="margin-top:2px">Du —${NB}· Anna —</div></div></div>`;
return screen({appbar:appBar({nav:'close',title:'Ny utgift',acts:save,ruled:true}),body,bottom:keypad('Klar'),gestCls:'on-kp'});
}
function editorFull(){
const save=`<button class="btn f" style="margin-right:8px">Spara</button>`;
const body = `${amountLine({amt:`1${NB}060,00`})}
<div class="uf" style="margin:2px 16px 4px;padding-left:0;padding-right:0;box-shadow:inset 0 -1px 0 var(--outline)"><div class="lb">Beskrivning</div><div class="t-body-large">SL-kort 30 dagar, Anna</div></div>
${editorRows({konto:'Vardag',cat:'transport',datum:`Idag · ${dShortWd('2026-10-17')}`,payer:'me',deln:'Allt på en person',open:true})}
<div class="pad" style="padding-top:2px;padding-bottom:10px">${modeChips('Allt på en person')}</div>
<div class="er" style="box-shadow:none"><span class="k">Allt på</span><span class="val"><div class="seg" role="radiogroup" aria-label="Allt på"><button role="radio" aria-checked="false">${avJ(20)}Du</button><button class="on" role="radio" aria-checked="true">${ic('check',{size:18})}Anna</button></div></span></div>
<div class="pad" style="padding-top:6px"><div class="paper zz-tb" style="padding-left:14px;padding-right:14px"><div class="t-label-small v">Förhandsvisning</div><div class="kv" style="padding:2px 0 0"><span class="t-body-medium grow">Du</span><span class="t-amount-medium amt">${kr(0)}</span></div><div class="kv" style="padding:0"><span class="t-body-medium grow">Anna</span><span class="t-amount-medium amt">${kr(106000)}</span></div></div></div>
<div class="pad" style="padding-top:18px"><div class="tf foc"><span class="lb">Anteckningar</span><span class="in t-body-large">Annas busskort för november. Jag lägger ut.<span class="caret"></span></span></div></div>`;
return screen({appbar:appBar({nav:'close',title:'Ny utgift',acts:save}),body});
}
function expenseReceipt({cat,desc,amt,date,konto,payer,mode,shares,note,recurring,muted=false}){
return `<div class="paper zz-tb" style="margin:6px 16px 0;padding-left:18px;padding-right:18px;${muted?'opacity:.62':''}">
<div style="text-align:center;padding-top:8px">${catMark(cat,true)}<div class="t-label-small v" style="margin-top:8px">${CAT[cat].sv}</div>
<div class="t-headline-small" style="margin-top:2px">${desc}</div>
<div class="t-amount-hero" style="margin:4px 0 10px">${kr(amt)}</div></div>
<div class="dash"></div><div style="padding:8px 0">
${kv('Datum',dDetail(date))}${kv('Konto',konto)}${kv('Kategori',CAT[cat].sv)}${kv('Betalt av',payer)}${kv('Delning',mode)}
${recurring?kv('Återkommande',`<span class="row" style="gap:6px">${ic('event_repeat',{size:18,cls:'pri'})}Återkommande: ${recurring}</span>`):''}
</div><div class="dash"></div><div style="padding:8px 0">
${shares.map(([n,o])=>`<div class="kv"><span class="t-body-medium grow">${n}</span><span class="t-amount-medium amt">${kr(o)}</span></div>`).join('')}
</div>${note?`<div class="dash"></div><div style="padding:10px 0 4px"><div class="t-label-small v">Anteckning</div><div class="t-body-medium" style="margin-top:2px">${note}</div></div>`:''}
</div>`;
}
function expenseDetail(kind='default'){
const acts = kind==='deleted'?'':ib('edit','Ändra')+ib('delete','Ta bort');
let body='';
if(kind==='default'){
body = expenseReceipt({cat:'groceries',desc:'ICA Maxi Lindhagen',amt:124760,date:'2026-10-16',konto:'Vardag',payer:'Anna',mode:'Kontots standard 50/50',shares:[['Du',62380],['Anna',62380]],note:'Storhandling inför middagen med Lindqvists'})
+ trail([{t:`16 okt. 14:02`,x:'Anna skapade den här'},{t:'16 okt. 19:40',x:`Du ändrade beloppet från ${kr(114760)} till ${kr(124760)}`},{t:'16 okt. 20:05',x:'Anna ändrade kategorin från Övrigt till Mat & hushåll'},{pending:true,x:'Din ändring väntar på synk'}]);
} else if(kind==='recurring'){
body = expenseReceipt({cat:'home',desc:'Hyra september',amt:1240000,date:'2026-09-25',konto:'Huset',payer:'Anna',mode:'Kontots standard 60/40',shares:[['Du',744000],['Anna',496000]],recurring:'Hyra'})
+ trail([{t:'25 sep. 06:00',x:'Skapad automatiskt (återkommande)'}]);
} else {
body = `<div class="banner errb" style="margin-top:2px"><div class="top">${ic('delete')}<div class="grow"><div class="t-title-small">Anna tog bort den här utgiften</div><div class="t-body-small">17 okt. 09:12 · Den räknas inte längre i saldon eller statistik.</div></div></div><div style="height:6px"></div></div>
<div style="position:relative">${expenseReceipt({cat:'shopping',desc:'Åhléns City',amt:34900,date:'2026-10-13',konto:'Vardag',payer:'Anna',mode:'Kontots standard 50/50',shares:[['Du',17450],['Anna',17450]],muted:true})}<span class="stamp red" style="position:absolute;top:44px;right:34px;transform:rotate(-8deg);font-size:calc(var(--fs)*20px);line-height:calc(var(--fs)*26px)">Borttagen</span></div>`
+ trail([{t:'13 okt. 17:48',x:'Anna skapade den här'},{t:'17 okt. 09:12',x:'Anna tog bort den här'}]);
}
return screen({appbar:appBar({nav:'back',title:kind==='deleted'?'Borttagen utgift':'Utgift',acts}),body});
}
/* ---- settle up ---- */
function settleScreen(kind='proposal',dialog=false){
const perAcct = kind==='account';
const amt = perAcct?308450:325000;
const rows = perAcct?[['Huset',308450,308450,false,true]]
: kind==='adjusted'?[['Huset',308450,175000,false],['Japan 2027',135000,150000,true]]
: kind==='remaining'?[['Huset',308450,200000,false],['Japan 2027',135000,100000,false]]
: [['Huset',308450,308450,false],['Japan 2027',135000,16550,false]];
const sum = rows.reduce((a,r)=>a+r[2],0), rem = amt-sum;
const ok = rem===0;
const rowHtml = rows.map(([n,debt,val,warn,fixed])=>`<div style="padding:8px 16px 8px"><div style="display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px"><div style="flex:1 1 130px"><div class="t-body-large">${n}</div><div class="t-body-small v">Skuld <span class="mono">${kr(debt)}</span></div></div>${fixed?`<span class="t-amount-medium amt" style="margin-left:auto;padding-right:4px">${kr(val)}</span>`:`<div class="tf${warn?' ':''}" style="flex:0 0 150px;min-height:48px;padding:10px 12px;margin-left:auto;${warn?'box-shadow:inset 0 0 0 1.5px var(--warning)':''}"><span class="in t-amount-medium" style="text-align:right">${krn(val)}</span><span class="sfx t-body-medium">kr</span></div>`}</div>${warn?`<div class="row warn t-body-small" style="gap:6px;margin-top:6px;align-items:flex-start">${ic('warning',{size:16})}<span>Det här reglerar mer än som är skyldigt i ${n} (${kr(val-debt)} för mycket)</span></div>`:''}</div>`).join(dash());
const remLine = perAcct?'':`<div class="pad" style="padding-top:2px"><div class="dbl"></div><div style="display:flex;align-items:baseline;padding:10px 0 4px" role="status"><span class="t-label-small ${ok?'':'warn'}" style="${ok?'color:var(--on-surface)':''}">Kvar att fördela</span><span class="leader"></span><span class="t-amount-medium amt ${ok?'':'warn'}">${kr(rem)}</span></div>${ok?'':`<div class="t-body-small warn" style="padding-bottom:4px">Fördelningen måste bli ${kr(amt)} innan du kan bekräfta.</div>`}</div>`;
const body = `<div style="display:flex;align-items:center;gap:12px;padding:4px 8px 8px 16px"><div class="pair" style="align-items:center">${avJ(36)}<span class="ms" style="font-size:20px;margin:0 6px;color:var(--on-surface-variant)">arrow_forward</span>${avA(36)}</div><div class="grow"><div class="t-title-medium">Du betalar Anna</div>${perAcct?'<div class="t-body-small v">Bara kontot Huset</div>':''}</div>${ib('swap_horiz','Byt riktning')}</div>
<div class="pad"><div class="row" style="justify-content:space-between"><span class="t-label-small v">Belopp</span><span class="t-label-small v">SEK</span></div><div style="display:flex;justify-content:flex-end;align-items:baseline;gap:8px;padding:2px 0 6px;box-shadow:inset 0 -1px 0 var(--outline)"><span class="t-amount-large">${krn(amt)}</span><span class="t-title-medium v mono">kr</span></div></div>
<div class="row t-label-small v" style="padding:18px 16px 6px;justify-content:space-between"><span>Fördelning</span><span>Belopp</span></div><div class="rule in"></div>
${rowHtml}
${perAcct?'<div class="rule in"></div>':`<div class="rule in"></div><div style="padding:0 8px"><button class="btn t">${ic('add')}Lägg till konto</button></div>`}
${remLine}
${perAcct?'':`<div class="pad"><div class="row v t-body-small" style="align-items:flex-start;gap:8px;padding:8px 0">${ic('info',{size:16})}<span>Vardag ingår inte: där är Anna skyldig dig ${kr(118450)}. Det kontot står kvar som det är.</span></div></div>`}
<div class="dash in"></div>
<div class="er" style="padding-left:16px"><span class="k">Datum</span><span class="val t-body-large">Idag · ${dShortWd('2026-10-17')}</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er" style="padding-left:16px;box-shadow:inset 0 1px 0 color-mix(in srgb,var(--outline-variant) 70%,transparent)"><span class="k">Anteckning</span><span class="val t-body-large v">Till exempel Swish</span></div>`;
const bottom = `<div class="bottombar"><button class="btn f wide${ok?'':' dis'}" ${ok?'':'aria-disabled="true"'}>Bekräfta</button></div>`;
let overlay='';
if(dialog){
overlay = `<div class="scrim"></div><div class="dlg" style="top:228px" role="alertdialog" aria-label="Bekräfta betalningen"><div class="t-headline-small">Bekräfta betalningen</div>
<div class="t-body-medium v" style="margin-top:12px">Du betalar Anna <span class="mono" style="color:var(--on-surface)">${kr(325000)}</span> idag.</div>
<div style="margin-top:14px"><div class="dash"></div>${rows.map(r=>`<div class="kv" style="padding:6px 0"><span class="t-body-medium grow">${r[0]}</span><span class="t-amount-medium amt">${kr(r[2])}</span></div>`).join('')}<div class="dash"></div></div>
<div class="row warn t-body-small" style="gap:8px;margin-top:14px;align-items:flex-start">${ic('warning',{size:18})}<span>Det här reglerar mer än som är skyldigt i Japan 2027. Efteråt är Anna skyldig dig ${kr(15000)} där.</span></div>
<div class="acts"><button class="btn t">Avbryt</button><button class="btn t">Bekräfta</button></div></div>`;
}
return screen({appbar:appBar({nav:'close',title:perAcct?'Gör upp · Huset':'Gör upp'}),body,bottom,gestCls:'on-bar',overlay});
}
function settlementDetail(){
const body = `<div class="paper zz-tb" style="margin:6px 16px 0;padding-left:18px;padding-right:18px">
<div style="text-align:center;padding-top:8px"><div class="pair" style="justify-content:center;align-items:center">${avA(40)}<span class="ms" style="font-size:22px;margin:0 8px;color:var(--on-surface-variant)">arrow_forward</span>${avJ(40)}</div>
<div class="t-label-small v" style="margin-top:10px">Betalning</div><div class="t-headline-small" style="margin-top:2px">Anna betalade dig</div><div class="t-amount-hero" style="margin:4px 0 10px">${kr(80000)}</div></div>
<div class="dash"></div><div style="padding:8px 0">${kv('Datum',dDetail('2026-10-06'))}${kv('Från','Anna')}${kv('Till','Du')}</div>
<div class="dash"></div><div style="padding:8px 0"><div class="t-label-small v" style="padding-bottom:2px">Fördelning</div><div class="kv"><span class="t-body-medium grow">Vardag</span><span class="t-amount-medium amt">${kr(80000)}</span></div></div>
<div class="dash"></div><div style="padding:10px 0 4px"><div class="t-label-small v">Anteckning</div><div class="t-body-medium" style="margin-top:2px">Swish</div></div></div>`
+ trail([{t:'6 okt. 21:15',x:'Anna skapade den här'}]);
return screen({appbar:appBar({nav:'back',title:'Betalning',acts:ib('edit','Ändra')+ib('delete','Ta bort')}),body});
}
/* ---- analytics ---- */
function filterRow(items){return `<div style="display:flex;gap:8px;padding:4px 16px 8px;overflow:hidden;flex-wrap:nowrap">${items.map(([t,on,i])=>`<button class="chip drop${on?' on':''}" style="flex:none">${i?ic(i,{size:18}):''}${t}${ic('arrow_drop_down',{size:18})}</button>`).join('')}</div>`;}
function stackedChart(cols,labels,{max,ticks,height=150,cur=-1}){
const yl = ticks.map(t=>`<span style="top:${height-(t/max)*height}px">${t===0?'0':krInt(t/100)}</span>`).join('');
const gl = ticks.map(t=>`<div class="gl${t===0?' base':''}" style="bottom:${(t/max)*height}px"></div>`).join('');
const bars = cols.map(segs=>`<div class="col"><div class="stk">${segs.filter(s=>s[0]>0).map(([v,col])=>`<i style="height:${(v/max)*height}px;background:var(${col})"></i>`).join('')}</div></div>`).join('');
return `<div class="chart" style="--ph:${height}px"><div class="yax">${yl}</div><div class="plot">${gl}${bars}</div><div class="xax">${labels.map((l,i)=>`<span class="${i===cur?'cur':''}">${l}</span>`).join('')}</div></div>`;
}
function breakdownRows(items,total){
const mx = Math.max(...items.map(i=>i.v));
return items.map((it,i)=>`${i?dash():''}<div class="brk" role="button" aria-label="${it.name}, ${kr(it.v)}, ${pct(it.v,total)}"><div class="l1">${it.mark}<span class="t-body-medium nm ell">${it.name}${it.badge?` <span class="badge" style="margin-left:4px">${it.badge}</span>`:''}</span><span class="t-amount-medium amt">${kr(it.v)}</span><span class="t-amount-small pc">${pct(it.v,total)}</span></div><div class="bar"><i style="width:${(it.v/mx)*100}%;background:var(${it.col})"></i></div></div>`).join('');
}
function accMark(k){return `<span class="cm" style="width:20px;height:20px;background:none"><i style="display:block;width:12px;height:12px;border-radius:2px;background:var(--chart-cat-${ACC[k].chart})"></i></span>`;}
function catSmall(key){const c=CAT[key];return `<span class="cm" style="width:20px;height:20px;background:none;color:var(--cat-${c.c})">${ic(c.icon,{size:18})}</span>`;}
function analytics(view='default'){
const app = appBar({title:'Statistik'});
if(view==='empty'){
const body = `${filterRow([['1 juni–14 juni 2025',true,'date_range'],['Månad',false],['Hushållet',false],['Kategori',false]])}
<div class="pad" style="padding-top:8px"><div class="t-label-small v">Totalt · 1 juni–14 juni 2025</div><div class="t-amount-large" style="margin-top:2px">${kr(0)}</div></div>
<div class="paper zz-tb" style="margin:24px 16px 0;padding:36px 24px;text-align:center"><span class="ms" style="font-size:40px;color:var(--on-surface-variant)">receipt_long</span><div class="t-title-medium" style="margin-top:12px">Inga utgifter i den här perioden</div><div class="t-body-medium v" style="margin-top:6px">Det finns inga utgifter mellan 1 juni och 14 juni 2025. Välj en annan period för att se statistik.</div><button class="btn o" style="margin-top:18px">${ic('date_range')}Byt period</button></div>`;
return screen({appbar:app,body,nav:1});
}
if(view==='week'){
const cols = WEEKS.map((_,i)=>['vardag','huset','japan'].map(k=>[WK[k][i],`--chart-cat-${ACC[k].chart}`]));
const accTot = ['vardag','huset','japan'].map(k=>[k,WK[k].reduce((a,b)=>a+b,0)]);
const total = accTot.reduce((a,b)=>a+b[1],0);
// category split for Min del (invented, sums to total)
let rest=total; const cats = WK_CAT_SPLIT.map(([k,f],i)=>{const v=i===WK_CAT_SPLIT.length-1?0:Math.round(total*f/100)*100;rest-=v;return {k,v};}); cats[cats.length-1].v+=rest; cats.sort((a,b)=>b.v-a.v);
const body = `${filterRow([['24 aug.–17 okt.',true,'date_range'],['Vecka',true],['Min del',true],['Konto',true]])}
<div class="pad" style="padding-top:4px"><div class="t-label-small v">Min del · 24 aug.–17 okt.</div><div class="t-amount-large" style="margin-top:2px">${kr(total)}</div></div>
<div class="chartcap"><span class="t-title-small">Utgifter över tid</span><span class="t-label-small v">per vecka · v. 35–42</span></div>
${stackedChart(cols,WEEKS,{max:500000,ticks:[0,250000,500000],height:140,cur:7})}
<div class="legend">${['vardag','huset','japan'].map(k=>`<span><i style="background:var(--chart-cat-${ACC[k].chart})"></i>${ACC[k].name}</span>`).join('')}</div>
<div class="dash in" style="margin-top:10px"></div>
<div class="chartcap"><span class="t-title-small">Per konto</span><span class="t-label-small v">Min del</span></div>
${breakdownRows(accTot.sort((a,b)=>b[1]-a[1]).map(([k,v])=>({name:ACC[k].name,v,mark:accMark(k),col:`--chart-cat-${ACC[k].chart}`})),total)}
<div class="dash in" style="margin-top:6px"></div>
<div class="chartcap"><span class="t-title-small">Per kategori</span><span class="t-label-small v">Min del</span></div>
${breakdownRows(cats.slice(0,3).map(c=>({name:CAT[c.k].sv,v:c.v,mark:catSmall(c.k),col:`--cat-${CAT[c.k].c}`})),total)}`;
return screen({appbar:app,body,nav:1});
}
const cols = AN_MONTHS.map((_,mi)=>AN.map(([k,vals])=>[vals[mi],`--cat-${CAT[k].c}`]));
const catTot = AN.map(([k,v])=>({k,v:v.reduce((a,b)=>a+b,0)})).filter(c=>c.v>0).sort((a,b)=>b.v-a.v);
const total = catTot.reduce((a,c)=>a+c.v,0);
const head = `${filterRow([['Senaste 6 mån',true,'date_range'],['Månad',false],['Hushållet',false],['Kategori',false]])}
<div class="pad" style="padding-top:4px"><div class="t-label-small v">Totalt · maj–okt. 2026</div><div class="t-amount-large" style="margin-top:2px">${kr(total)}</div></div>`;
const chart1 = `<div class="chartcap"><span class="t-title-small">Utgifter över tid</span><span class="t-label-small v">per månad</span></div>
${stackedChart(cols,AN_MONTHS,{max:4500000,ticks:[0,1500000,3000000,4500000],height:150,cur:5})}
<div class="legend">${AN.filter(([k])=>catTot.find(c=>c.k===k)).map(([k])=>`<span><i style="background:var(--cat-${CAT[k].c})"></i>${CAT[k].sv}</span>`).join('')}</div>`;
const brkCat = `<div class="chartcap"><span class="t-title-small">Per kategori</span><span class="t-label-small v">${catTot.length} kategorier</span></div>${breakdownRows(catTot.map(c=>({name:CAT[c.k].sv,v:c.v,mark:catSmall(c.k),col:`--cat-${CAT[c.k].c}`})),total)}`;
const brkAcc = `<div class="chartcap"><span class="t-title-small">Per konto</span><span class="t-label-small v">4 konton</span></div>${breakdownRows(AN_ACC.map(([k,v])=>({name:ACC[k].name,v,mark:accMark(k),col:`--chart-cat-${ACC[k].chart}`,badge:ACC[k].arch?'Arkiverat':''})),total)}`;
if(view==='scrolled'){
const brkCatTail = breakdownRows(catTot.map(c=>({name:CAT[c.k].sv,v:c.v,mark:catSmall(c.k),col:`--cat-${CAT[c.k].c}`})),total).split('<div class="dash in"></div>').slice(2).join('<div class="dash in"></div>');
return screen({appbar:appBar({title:'Statistik',ruled:true}),body:`<div style="margin-top:-30px">${brkCatTail}</div><div class="dash in" style="margin-top:6px"></div>${brkAcc}`,nav:1});
}
return screen({appbar:app,body:`${head}${chart1}<div class="dash in" style="margin-top:10px"></div>${brkCat}`,nav:1});
}
function drilldown(){
const rows = VARDAG[0].rows.filter(r=>Array.isArray(r)&&r[2]==='groceries');
const total = rows.reduce((a,r)=>a+r[3],0);
const body = `<div class="pad" style="padding-bottom:12px"><div class="t-amount-small v">Hushållet · alla konton</div><div style="display:flex;align-items:baseline;gap:12px;margin-top:6px;flex-wrap:wrap"><span class="t-label-small v grow">Totalt · 4 utgifter</span><span class="t-amount-large amt">${kr(total)}</span></div></div><div class="dbl in" style="margin:0 16px"></div>
${rows.map((r,i)=>(i?dash():'')+ledgerRow(r)).join('')}<div class="dash in"></div>`;
return screen({appbar:appBar({nav:'back',title:'Mat & hushåll, oktober 2026'}),body});
}
/* ---- settings ---- */
function li(icon,title,o={}){return `<button class="li" style="width:100%">${icon?ic(icon):''}<span class="tx"><span class="t-body-large" style="display:block">${title}</span>${o.sub?`<span class="t-body-small v" style="display:block">${o.sub}</span>`:''}</span>${o.tr?`<span class="tr t-amount-small">${o.tr}</span>`:''}${o.chev===false?'':ic('chevron_right',{size:20,cls:'chev'})}</button>`;}
function settings(){
const body = `<div class="paper zz-b" style="margin:4px 16px 0;padding:14px 16px 18px;border-radius:8px 8px 0 0">
<div class="t-label-small v" style="margin-bottom:10px">Hushållet</div>
<div class="row" style="gap:12px">${avJ(48)}<div class="grow"><div class="t-title-medium">Jacob <span class="v t-body-medium">(du)</span></div><div class="t-body-small v">Administratör</div></div></div>
<div class="dash" style="margin:12px 0"></div>
<div class="row" style="gap:12px">${avA(48)}<div class="grow"><div class="t-title-medium">Anna</div><div class="t-body-small v">Partner</div></div></div></div>
<div style="height:12px"></div>
${li('person','Profil')}${li('category','Kategorier',{tr:'10'})}${li('event_repeat','Återkommande utgifter',{tr:'4'})}${li('devices','Enheter',{tr:'2'})}${li('qr_code_2','Para ihop en enhet')}
<div class="dash in" style="margin:4px 16px"></div>
${li('cloud_done','Synkstatus',{sub:'Synkad för 3 min sedan'})}${li('info','Om Kvitt',{tr:'1.1'})}`;
return screen({appbar:appBar({title:'Inställningar'}),body,nav:2});
}
function profile(){
const body = `<div style="display:flex;flex-direction:column;align-items:center;padding:16px 16px 8px">${avJ(112)}<div class="row" style="gap:8px;margin-top:16px;flex-wrap:wrap;justify-content:center"><button class="btn o">${ic('photo_library')}Byt bild</button><button class="btn t">${ic('delete')}Ta bort bild</button></div></div>
<div class="pad" style="padding-top:20px"><div class="tf foc"><span class="lb">Namn</span><span class="in t-body-large">Jacob<span class="caret"></span></span></div><div class="sup t-body-small" style="display:flex;justify-content:space-between"><span>Visas för Anna i listor och notiser.</span><span class="mono">5/40</span></div></div>
<div class="pad" style="padding-top:24px"><div class="row v t-body-small" style="gap:8px;align-items:flex-start">${ic('cloud',{size:16})}<span>Namn och bild sparas på servern och kräver anslutning.</span></div></div>`;
return screen({appbar:appBar({nav:'back',title:'Profil',acts:`<button class="btn f" style="margin-right:8px">Spara</button>`}),body});
}
const DEFAULT_CATS=['groceries','restaurants','home','utilities','transport','travel','entertainment','health','shopping','other'];
function categories(){
const row=(key,name,arch=false)=>`<div class="li" style="min-height:52px;padding-top:4px;padding-bottom:4px;gap:12px">${arch?'':`<button class="ib" style="width:40px;margin-left:-8px" aria-label="Flytta ${name}">${ic('drag_indicator',{size:22})}</button>`}${catMark(key)}<span class="tx t-body-large ${arch?'v':''}">${name}</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>`;
const body = `<div class="dash in"></div>${DEFAULT_CATS.map(k=>row(k,CAT[k].sv)).join('<div class="dash in" style="opacity:.35;margin-left:96px"></div>')}<div class="dash in"></div>
<div class="arch" style="padding-top:4px"><span class="tx t-label-large">Arkiverade (1)</span>${ic('expand_less',{size:22})}</div>
<div class="li" style="min-height:52px;gap:12px;opacity:.85"><span class="cm" style="background:var(--cat-lime-container);color:var(--cat-lime)">${ic('water_drop',{size:18})}</span><span class="tx t-body-large v">Pant</span><span class="badge">Arkiverad</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>`;
return screen({appbar:appBar({nav:'back',title:'Kategorier',acts:ib('add','Ny kategori',{on:true})}),body});
}
const ICONS=['shopping_cart','restaurant','local_cafe','home','bolt','water_drop','wifi','directions_car','train','local_gas_station','flight','hotel','luggage','movie','music_note','sports_esports','fitness_center','medical_services','medication','shopping_bag','checkroom','pets','child_care','school','card_giftcard','celebration','build','chair','subscriptions','category'];
function categoryEditor(){
const grid = ICONS.map(i=>`<button class="sw" style="width:100%;height:46px;${i==='shopping_cart'?'background:var(--cat-green-container);color:var(--cat-green);box-shadow:inset 0 0 0 2px var(--cat-green)':'color:var(--on-surface-variant)'}" aria-label="${i}">${ic(i,{size:22})}</button>`).join('');
const sw = CATS.map(c=>`<button class="sw" style="width:100%;height:40px;background:var(--cat-${c})" aria-label="${c}">${c==='green'?`<span class="ms" style="font-size:22px;color:var(--surface)">check</span>`:''}</button>`).join('');
const body = `<div class="pad" style="padding-top:12px"><div class="tf foc"><span class="lb">Namn</span><span class="in t-body-large">Matvaror<span class="caret"></span></span></div><div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap"><span class="sup t-body-small">Standardnamn: Mat & hushåll</span><button class="btn t" style="margin-right:-8px">${ic('restart_alt')}Återställ namn</button></div></div>
<div class="pad"><div class="t-label-small v" style="margin:6px 0 8px">Ikon</div><div style="display:grid;grid-template-columns:repeat(8,1fr);gap:4px">${grid}</div>
<div class="t-label-small v" style="margin:16px 0 8px">Färg</div><div style="display:grid;grid-template-columns:repeat(6,1fr);gap:8px">${sw}</div></div>
<div class="dash in" style="margin-top:18px"></div>
<div class="pad" style="padding-top:12px"><button class="btn o">${ic('archive')}Arkivera</button><div class="row v t-body-small" style="gap:8px;margin-top:10px;align-items:flex-start">${ic('lock',{size:16})}<span>Kan inte tas bort: standardkategori. Arkivera den om ni inte vill använda den.</span></div></div>`;
return screen({appbar:appBar({nav:'back',title:'Ändra kategori',acts:`<button class="btn f" style="margin-right:8px">Spara</button>`}),body});
}
function syncStatus(clock=false){
const pend = [['2026-10-14','SL-kort 30 dagar, Anna','transport',106000,'Ny'],['2026-10-12','Willys Hornstull','groceries',86435,'Ändrad']];
const body = `${clock?`<div class="banner warnb"><div class="top">${ic('schedule')}<div class="grow"><div class="t-title-small">Synken är stoppad</div><div class="t-body-small">Telefonens klocka går fel. Rätta den och synka igen.</div></div></div><div style="height:6px"></div></div>`:''}
<div class="paper zz-b" style="margin:4px 16px 0;padding:14px 16px 18px;border-radius:8px 8px 0 0"><div class="t-label-small v">Senast synkad</div><div style="display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;margin-top:4px"><span class="t-title-medium grow">${clock?'Synken stoppades':'För 3 min sedan'}</span><span class="t-amount-small v">lör 17 okt. 13:59</span></div><button class="btn f" style="margin-top:14px">${ic('sync')}Synka nu</button></div>
<div class="sh" style="margin-top:10px"><span class="t-label-small cap">Väntande ändringar (2)</span></div><div class="dash in"></div>
${pend.map(([d,n,c,a,k],i)=>`${i?dash():''}<div class="li" style="gap:12px;min-height:60px">${catMark(c)}<span class="tx"><span class="t-body-large" style="display:block">${n}</span><span class="t-body-small v">${k} · ${dList(d)}</span></span><span class="tr t-amount-medium amt" style="color:var(--on-surface)">${kr(a)}</span></div>`).join('')}
<div class="dash in"></div>
<div class="sh" style="margin-top:10px"><span class="t-label-small cap">Ändringar som inte synkades (1)</span></div>
<div style="margin:0 16px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--outline-variant);padding:12px 12px 6px 16px"><div class="row" style="gap:12px;align-items:flex-start">${catMark('restaurants')}<div class="grow"><div class="t-title-small">Babette</div><div class="t-body-small" style="margin-top:2px">Din ändring ersattes av en nyare ändring från Anna</div><div class="t-amount-small v" style="margin-top:2px">17 okt. 08:31</div><div class="t-body-small v" style="margin-top:8px">Ni ändrade samma utgift nästan samtidigt. Annas version gäller och din ändring sparades inte. Öppna utgiften för att lägga in den igen.</div></div></div><div class="row" style="justify-content:flex-end;gap:0;margin-top:4px"><button class="btn t">Visa</button><button class="btn t">Ta bort från listan</button></div></div>`;
return screen({appbar:appBar({nav:'back',title:'Synkstatus'}),body});
}
function updateDialog(){
const overlay = `<div class="scrim"></div><div class="dlg" style="top:250px" role="alertdialog"><span class="ms" style="font-size:24px;color:var(--primary)">install_mobile</span><div class="t-headline-small" style="margin-top:12px">Tillåt installation</div><div class="t-body-medium v" style="margin-top:12px">Kvitt uppdateras utan Play Butik. Därför frågar Android en gång om Kvitt får <b style="color:var(--on-surface);font-weight:600">Installera okända appar</b>.</div><div class="t-body-medium v" style="margin-top:10px">Slå på det för Kvitt och gå tillbaka, så fortsätter uppdateringen till version 1.2.</div><div class="acts"><button class="btn t">Senare</button><button class="btn t">Fortsätt</button></div></div>`;
return screen({appbar:appBar({wordmark:true}),body:homeBody({banner:updateBanner('avail')}),nav:0,fabHtml:HOME_FAB,overlay});
}
function removed(){
const body = `<div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 24px 40px"><div class="paper zz-tb" style="padding:28px 24px">
<span class="ms" style="font-size:40px;color:var(--on-surface-variant)">phonelink_erase</span>
<div class="t-headline-small" style="margin-top:14px">Den här telefonen har tagits bort</div>
<div class="t-body-medium v" style="margin-top:10px">Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats.</div>
<div class="t-body-medium v" style="margin-top:10px">Ingenting försvann från hushållet: utgifterna finns kvar på servern och hos Anna. Para ihop telefonen igen med en ny kod.</div>
<button class="btn f" style="margin-top:22px">Para ihop igen</button></div></div>`;
return screen({appbar:'',body:`<div style="flex:1;display:flex;flex-direction:column">${body}</div>`});
}
function recurringList(){
const T=[['Hyra',1240000,'Huset','Anna betalar','Varje månad den 25:e','Nästa 25 okt.','home',false],['Bahnhof bredband',39900,'Huset','Du betalar','Varje månad den 28:e','Nästa 28 okt.','utilities',false],['Netflix',17900,'Vardag','Du betalar','Varje månad den 3:e','Nästa 3 nov.','entertainment',false],['Spotify Family',25900,'Vardag','Anna betalar','Varje månad den 12:e','','entertainment',true]];
const body = `<div class="dash in"></div>${T.map(([n,a,acc,p,when,next,cat,paused],i)=>`${i?dash():''}<div class="li" style="align-items:flex-start;gap:12px;padding-top:12px;padding-bottom:12px;${paused?'opacity:.8':''}">${catMark(cat)}<span class="tx"><span style="display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px"><span class="t-body-large grow ${paused?'v':''}">${n}</span><span class="t-amount-medium amt">${kr(a)}</span></span><span class="t-body-small v" style="display:block">${acc} · ${p}</span><span style="display:flex;flex-wrap:wrap;gap:4px 8px;align-items:center;margin-top:2px"><span class="t-body-small v">${when}</span>${paused?'<span class="badge">Pausad</span>':`<span class="t-amount-small" style="color:var(--on-surface)">· ${next}</span>`}</span></span></div>`).join('')}<div class="dash in"></div>
<div class="pad" style="padding-top:14px"><div class="row v t-body-small" style="gap:8px;align-items:flex-start">${ic('schedule',{size:16})}<span>Utgifterna skapas automatiskt kl. 06:00 på dagen och syns för er båda.</span></div></div>`;
return screen({appbar:appBar({nav:'back',title:'Återkommande utgifter',acts:ib('add','Ny återkommande utgift',{on:true})}),body});
}
function recurringEditor(){
const body = `${amountLine({amt:`12${NB}400,00`,compact:true})}
<div class="uf" style="margin:0 16px 4px;padding-left:0;padding-right:0"><div class="lb">Beskrivning</div><div class="t-body-large">Hyra</div></div>
<div class="er"><span class="k">Konto</span><span class="val t-body-large">Huset</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er"><span class="k">Kategori</span><span class="val t-body-large">${catMark('home')}Hem</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er"><span class="k">Betalt av</span><span class="val">${payerSeg('anna')}</span></div>
<div class="er"><span class="k">Delning</span><span class="val" style="flex-direction:column;align-items:flex-start;gap:0"><span class="t-body-large">Kontots standard 60/40</span><span class="t-amount-small v">Du ${kr(744000)} · Anna ${kr(496000)}</span></span>${ic('expand_more',{size:20,cls:'chev'})}</div>
<div class="dbl in" style="margin:6px 16px 4px;opacity:.4"></div>
<div class="t-label-small v" style="padding:10px 16px 2px">Upprepning</div>
<div class="er"><span class="k">Dag i mån.</span><span class="val t-body-large"><span class="mono">25</span><span class="v t-body-small">varje månad</span></span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er"><span class="k">Första</span><span class="val t-body-large">september 2026</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er"><span class="k">Sista</span><span class="val t-body-large v">Sista månad (valfritt)</span>${ic('chevron_right',{size:20,cls:'chev'})}</div>
<div class="er" style="padding-right:16px"><span class="k">Pausad</span><span class="val t-body-medium v">Skapar inga nya utgifter</span><span class="switch" role="switch" aria-checked="false" aria-label="Pausad"><i></i></span></div>
<div style="padding:4px 8px"><button class="btn t">${ic('notes')}Lägg till anteckning</button></div>`;
return screen({appbar:appBar({nav:'close',title:'Ändra återkommande',acts:`<button class="btn f" style="margin-right:8px">Spara</button>`}),body});
}
function devices(dialog=false){
const dev=(name,owner,seen,cur)=>`<div class="li" style="align-items:flex-start;gap:14px;padding-top:12px;padding-bottom:12px">${ic(cur?'smartphone':'smartphone')}<span class="tx"><span style="display:flex;flex-wrap:wrap;gap:4px 8px;align-items:center"><span class="t-body-large">${name}</span>${cur?'<span class="badge pri">Den här enheten</span>':''}</span><span class="t-body-small v" style="display:block">${owner}</span><span class="t-amount-small v" style="display:block;margin-top:4px">Parad 3 okt. · Senast sedd ${seen}</span><span class="t-amount-small v" style="display:block">Kvitt 1.1</span>${cur?'':`<button class="btn o" style="margin-top:10px;color:var(--error)">${ic('link_off')}Ta bort åtkomst</button>`}</span></div>`;
const body = `<div class="t-body-small v" style="padding:0 16px 10px">Telefoner som är parade med hushållet.</div><div class="dash in"></div>${dev('Galaxy S26','Jacob','nyss',true)}${dash()}${dev('Galaxy S26+','Anna','17 okt. 13:47',false)}<div class="dash in"></div>`;
const overlay = dialog?`<div class="scrim"></div><div class="dlg" style="top:260px" role="alertdialog"><span class="ms" style="font-size:24px;color:var(--error)">link_off</span><div class="t-headline-small" style="margin-top:12px">Ta bort åtkomst för Galaxy S26+?</div><div class="t-body-medium v" style="margin-top:12px">Annas telefon loggas ut och allt Kvitt-data på den raderas. Hushållets utgifter finns kvar. Anna kan para ihop telefonen igen med en ny kod.</div><div class="acts"><button class="btn t">Avbryt</button><button class="btn t er">Ta bort åtkomst</button></div></div>`:'';
return screen({appbar:appBar({nav:'back',title:'Enheter'}),body,overlay});
}
function invites(){
const body = `<div class="paper zz-tb" style="margin:4px 16px 0;padding-left:18px;padding-right:18px;text-align:center">
<div class="t-label-small v" style="padding-top:6px">Ny enhet för Jacob</div>
<div class="t-amount-large" style="margin-top:10px;letter-spacing:.06em;white-space:normal" aria-label="Kod 7K3M QX9D 2HPA">7K3M<span class="v">-</span>QX9D<span class="v">-</span>2HPA</div>
<div class="t-amount-small v" style="margin-top:8px;word-break:break-all">https://icehack.asuscomm.com:8443/i/7K3M-QX9D-2HPA</div>
<div class="dash" style="margin:14px 0 10px"></div>
<div class="row t-body-small" style="justify-content:center;gap:6px">${ic('schedule',{size:16,cls:'v'})}<span>Gäller till tis 20 okt. 14:02</span></div>
<div class="row" style="justify-content:center;gap:8px;margin:14px 0 6px;flex-wrap:wrap"><button class="btn f">${ic('share')}Dela</button></div>
<div class="t-body-small v" style="padding-bottom:4px">Öppna länken på den nya telefonen, eller skriv in koden där. Koden fungerar en gång.</div></div>
<div class="sh" style="margin-top:14px"><span class="t-label-small cap">Väntande inbjudningar (2)</span></div><div class="dash in"></div>
<div class="li" style="gap:12px">${ic('smartphone')}<span class="tx"><span class="t-body-large" style="display:block">Ny enhet · Jacob</span><span class="t-body-small v">Skapad idag · gäller till tis 20 okt. 14:02</span></span><button class="btn t er">Avbryt</button></div>${dash()}
<div class="li" style="gap:12px">${ic('smartphone')}<span class="tx"><span class="t-body-large" style="display:block">Ny enhet · Anna</span><span class="t-body-small v">Skapad 16 okt. · gäller till mån 19 okt. 21:40</span></span><button class="btn t er">Avbryt</button></div><div class="dash in"></div>`;
return screen({appbar:appBar({nav:'back',title:'Para ihop en enhet'}),body});
}
function about(){
const body = `<div class="paper zz-tb" style="margin:4px 16px 0;padding-left:20px;padding-right:20px"><div style="text-align:center;padding:10px 0 12px"><span class="ms" style="font-size:44px;color:var(--primary)">receipt_long</span><div class="mono" style="font-weight:600;letter-spacing:.14em;font-size:calc(var(--fs)*22px);line-height:calc(var(--fs)*30px);margin-top:6px">KVITT</div><div class="t-body-small v">Delade utgifter för två</div></div><div class="dash"></div><div style="padding:10px 0">
${kv('Version','<span class="mono">Kvitt 1.1 (11)</span>')}<div style="padding:3px 0"><div class="kv" style="padding:0"><span class="k">Server</span></div><div class="t-amount-small" style="font-size:calc(var(--fs)*13px);line-height:calc(var(--fs)*20px);overflow-wrap:anywhere">https://icehack.asuscomm.com:8443</div></div>${kv('Serverversion','<span class="mono">1.1.0</span>')}</div><div class="dash"></div>
<div style="padding:12px 0 2px;text-align:center"><button class="btn o">${ic('system_update')}Sök efter uppdatering</button></div></div>
<div style="height:12px"></div>${li('description','Licenser för öppen källkod')}`;
return screen({appbar:appBar({nav:'back',title:'Om Kvitt'}),body});
}
function pairing(err=false){
const body = `<div style="padding:24px 16px 0"><div class="paper zz-b" style="padding:22px 20px 26px;border-radius:8px 8px 0 0">
<div class="row" style="gap:10px"><span class="ms" style="font-size:34px;color:var(--primary)">receipt_long</span><span class="mono" style="font-weight:600;letter-spacing:.14em;font-size:max(28px,min(calc(var(--fs)*28px),calc(var(--fs)*24px)));line-height:1.29">KVITT</span></div>
<div class="t-body-large v" style="margin-top:6px">Delade utgifter för två</div>
<div class="dash" style="margin:18px 0 22px"></div>
<div class="tf ${err?'bad':''}" style="--fld-bg:var(--paper)"><span class="lb">Inbjudningskod</span><span class="in t-body-large mono" style="letter-spacing:.06em">7K3M-QX9D-2HPA</span><button class="ib" style="margin:-12px -10px -12px 0;color:var(--primary)" aria-label="Klistra in">${ic('content_paste')}</button></div>
${err?`<div class="sup err t-body-small">${ic('error',{size:16})}<span>Koden fungerar inte. Den kan ha gått ut eller redan använts. Be Jacob om en ny.</span></div>`:`<div class="sup t-body-small">12 tecken, i grupper om fyra</div>`}
<div class="tf" style="--fld-bg:var(--paper);margin-top:18px"><span class="lb">Ditt namn</span><span class="in t-body-large">Anna</span></div>
<div class="row v t-body-small" style="gap:8px;margin-top:14px;align-items:flex-start">${ic('info',{size:16})}<span>Koden får du av Jacob: från en länk eller från Inställningar › Bjud in partner.</span></div>
<div class="dash" style="margin:16px 0 4px"></div>
<button class="er" style="width:100%;padding-left:0;padding-right:0;box-shadow:none"><span class="k">Serveradress</span><span class="val t-amount-small" style="display:block;overflow-wrap:anywhere">icehack.asuscomm.com:8443</span>${ic('expand_more',{size:20,cls:'chev'})}</button>
<button class="btn f wide" style="margin-top:10px">Para ihop</button>
</div></div>`;
return screen({appbar:'',body});
}
function profileStep(){
const body = `<div style="padding:24px 16px 0">
<div class="paper zz-b" style="padding:18px 20px 22px;border-radius:8px 8px 0 0"><div class="row" style="gap:14px">${avJ(48)}<div><div class="t-label-small v">Inbjudan från Jacob</div><div class="t-title-large">Du går med i Jacobs hushåll</div></div></div></div>
<div style="display:flex;flex-direction:column;align-items:center;padding:36px 0 8px">${avA(104)}<button class="btn o" style="margin-top:16px">${ic('add_a_photo')}Lägg till bild</button><div class="t-body-small v" style="margin-top:6px">Valfritt. Annars visas din initial.</div></div>
<div style="padding-top:22px"><div class="tf foc"><span class="lb">Ditt namn</span><span class="in t-body-large">Anna<span class="caret"></span></span></div><div class="sup t-body-small">Så här ser Jacob dig i Kvitt.</div></div>
</div>`;
return screen({appbar:'',body,bottom:`<div class="bottombar" style="background:var(--surface);box-shadow:none"><button class="btn f wide">Fortsätt</button></div>`});
}
function waiting(){
const body = `${syncLine('ok')}<div class="paper zz-tb" style="margin:8px 16px 0;padding:24px 22px">
<div class="pair">${avJ(48)}<span class="av" style="width:48px;height:48px;margin-left:-10px;box-shadow:inset 0 0 0 1.5px var(--outline),0 0 0 2px var(--paper);background:var(--paper);color:var(--on-surface-variant)" aria-label="Partner saknas">${ic('person_add',{size:22})}</span></div>
<div class="t-headline-small" style="margin-top:16px">Väntar på din partner</div>
<div class="t-body-medium v" style="margin-top:8px">Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk.</div>
<div class="dash" style="margin:18px 0 12px"></div>
<div class="kv"><span class="k">Inbjudan</span><span class="val t-body-medium">Inbjudan gäller till mån 19 okt. 21:40</span></div>
<button class="btn f" style="margin-top:16px">${ic('share')}Bjud in partner</button></div>`;
return screen({appbar:appBar({wordmark:true}),body,nav:0});
}
function homeEmpty(){
const body = `${syncLine('ok')}<div class="paper zz-tb" style="margin:8px 16px 0;padding:28px 22px">
<span class="ms" style="font-size:40px;color:var(--primary)">post_add</span>
<div class="t-headline-small" style="margin-top:14px">Skapa ert första konto</div>
<div class="t-body-medium v" style="margin-top:8px">Ett konto samlar utgifter som ni delar på samma sätt, till exempel Vardag, Huset eller en resa. Ni kan ha flera konton med olika fördelning.</div>
<button class="btn f" style="margin-top:20px">${ic('add')}Nytt konto</button></div>`;
return screen({appbar:appBar({wordmark:true}),body,nav:0});
}
function homeLoading(){
const sk=(w,h=12,m='')=>`<span class="sk" style="width:${w};height:${h}px;${m}"></span>`;
const body = `<div class="prog" style="margin:0;border-radius:0;height:3px" role="progressbar" aria-label="Hämtar era utgifter"><i style="width:38%;margin-left:22%"></i></div>${syncLine('loading')}
<div class="paper zz-b" style="margin:0 16px;padding:16px 16px 18px;border-radius:8px 8px 0 0"><div style="display:flex;gap:12px;align-items:center">${sk('36px',36,'border-radius:50%')}<div class="grow">${sk('60%',12)}${sk('42%',22,'margin-top:8px')}</div>${sk('84px',40,'border-radius:8px')}</div></div>
<div class="sh" style="margin-top:12px"><span class="t-label-small cap">Konton</span></div><div class="dash in"></div>
${[1,2,3].map(i=>`${i>1?dash():''}<div style="padding:14px 16px;display:flex;flex-direction:column;gap:8px"><div class="row" style="justify-content:space-between">${sk((40+i*12)+'%',14)}${sk('88px',14)}</div>${sk('62%',10)}</div>`).join('')}
<div class="dash in"></div>
<div class="t-body-small v" style="padding:18px 16px;text-align:center">Hämtar era utgifter… Första synken kan ta en stund. Sedan finns allt på telefonen, även offline.</div>`;
return screen({appbar:appBar({wordmark:true}),body,nav:0});
}
function notifications(){
const n=(title,body,when)=>`<div style="background:var(--surface-container-lowest);border-radius:20px;padding:14px 16px;margin:0 10px 4px"><div class="row" style="gap:8px;align-items:center"><span style="width:22px;height:22px;border-radius:50%;background:var(--primary);color:var(--on-primary);display:inline-flex;align-items:center;justify-content:center">${ic('receipt_long',{size:14})}</span><span class="t-body-small v">Kvitt · ${when}</span><span class="grow"></span>${ic('expand_more',{size:20,cls:'v'})}</div><div class="t-title-small" style="margin-top:8px;font-family:system-ui,var(--sans)">${title}</div><div class="t-body-medium v" style="font-family:system-ui,var(--sans)">${body}</div></div>`;
const tiles = ['wifi','bluetooth','flashlight_off','do_not_disturb_on','dark_mode','screen_rotation'];
const body = `<div style="background:var(--surface-container-high);flex:1;display:flex;flex-direction:column">
<div style="padding:18px 22px 12px;display:flex;align-items:baseline;justify-content:space-between"><span style="font:400 max(40px,min(calc(var(--fs)*40px),calc(var(--fs)*24px)))/1 system-ui,var(--sans)">14:02</span><span class="t-body-medium v">lör 17 okt.</span></div>
<div style="display:flex;gap:10px;padding:0 18px 18px;justify-content:space-between">${tiles.map((t,i)=>`<span style="width:52px;height:52px;border-radius:26px;display:flex;align-items:center;justify-content:center;background:${i<1?'var(--inverse-surface)':'var(--surface-container-highest)'};color:${i<1?'var(--inverse-on-surface)':'var(--on-surface)'}">${ic(t,{size:22,fill:i<1})}</span>`).join('')}</div>
${n('Anna lade till en utgift',`ICA Maxi Lindhagen · ${kr(124760)} · Vardag`,'nu')}
${n('Anna registrerade en betalning',`Anna betalade dig ${kr(80000)}`,'2 min')}
<div style="display:flex;justify-content:flex-end;padding:8px 14px"><span class="btn" style="background:var(--surface-container-highest);color:var(--on-surface);border-radius:20px">Rensa alla</span></div>
</div>`;
return `${statusBar().replace('class="sb"','class="sb" style="background:var(--surface-container-high)"')}<main class="content" style="display:flex">${body}</main><div class="gest" style="background:var(--surface-container-high)"><i></i></div>`;
}
/* =========================================================================
FRAMES
========================================================================= */
const LD=['light','dark'];
const FRAMES = [
{id:'S-1-pairing',title:'S-1 Para ihop',note:'Fälten fylls i från länken. Ordning: kod (med Klistra in), Ditt namn, förklaring, serveradress som hopfälld rad, Para ihop. Kortet är ett kvitto med tandad underkant: appens enda "logga".',themes:LD,render:()=>pairing(false)},
{id:'S-1-pairing-error',title:'S-1 Para ihop · fel (invite_invalid)',note:'Felet står under kodfältet med ikon och text; knappen är aktiv igen så att en ny kod kan provas direkt.',themes:LD,render:()=>pairing(true)},
{id:'S-2-profile-step',title:'S-2 Partnerns profil (Anna ser detta)',note:'Jacobs bild bekräftar vems hushåll det är. Annas avatar är initialen på --avatar-2 tills hon lägger till en bild.',themes:LD,render:()=>profileStep()},
{id:'S-3-waiting',title:'S-3 Väntar på partner',note:'Inga konton eller utgifter erbjuds förrän partnern gått med (AC-5.1). Inställningar nås via navigeringsfältet.',themes:LD,render:()=>waiting()},
{id:'S-4-home',title:'S-4 Översikt',note:'Kvittoblocket överst: ordalydelse och belopp på två rader, Gör upp på samma block. Kontona som en liggare: namn, belopp i mono till höger, ordalydelse och antal under. Rader 60 dp, streckade linjer.',themes:LD,render:()=>homeScreen()},
{id:'S-4-home-offline',title:'S-4 Översikt · offline med väntande ändringar och uppdatering',note:'Synkraden blir bärnstensfärgad och leder till S-16. Saldon räknas lokalt och inkluderar väntande ändringar.',themes:LD,render:()=>screen({appbar:appBar({wordmark:true}),body:homeBody({sync:'offline',banner:updateBanner('avail')}),nav:0,fabHtml:HOME_FAB})},
{id:'S-4-home-settled',title:'S-4 Översikt · kvitt, arkiv öppet',note:'Kvitt-läget får en stämpel i stället för Gör upp. Arkiverade konton visas med märket Arkiverat.',themes:LD,render:()=>homeScreen({settled:true,archOpen:true})},
{id:'S-4-home-empty',title:'S-4 Översikt · inga konton',note:'Ingen FAB: det finns inget konto att lägga utgifter i.',themes:LD,render:()=>homeEmpty()},
{id:'S-4-home-loading',title:'S-4 Översikt · första synken',note:'Skelettrader i samma geometri som liggaren, så att inget hoppar när data kommer.',themes:LD,render:()=>homeLoading()},
{id:'S-4-home-en',title:'S-4 Home · English',note:'Same layout; English copy and SEK 3,250.00 money format. Mono amounts keep their right edge in both languages.',themes:['light'],render:()=>homeScreen({lang:'en'})},
{id:'S-4-home-200',title:'S-4 Översikt · 200 % textstorlek',note:'Rader växer; när namn och belopp inte får plats bryts beloppet till en egen rad, högerställt (FlowRow). Belopp kortas aldrig. Gör upp hamnar under beloppet.',themes:['light'],fontScale:2,render:()=>homeScreen()},
{id:'S-5-account',title:'S-5 Konto · Vardag',note:'Läses som ett kontoutdrag: datumkolumn, kategorimarkör, beskrivning, belopp; månadsrubrik med prickad ledare till månadens hushållssumma. SL-kort väntar på synk (ikon + ord).',themes:LD,render:()=>accountScreen()},
{id:'S-5-account-menu',title:'S-5 Konto · överflödesmeny (extra)',note:'Ta bort konto är avstängt och skälet står direkt under (AC-6.5).',themes:LD,render:()=>accountScreen({menu:true})},
{id:'S-5-account-archived',title:'S-5 Konto · Gotland 2026, arkiverat',note:'Stämpeln Arkiverat, Kvitt, Återställ från arkiv. Ingen FAB och ingen Gör upp när saldot är noll. Kan inte tas bort: kontot har utgifter.',themes:LD,render:()=>accountArchived()},
{id:'S-6-account-editor',title:'S-6 Nytt konto · 60 / 40',note:'Skjutreglage (Material 3-reglage med stapelhandtag) plus två procentfält som alltid blir 100. Förhandsvisningen är en kvittoremsa.',themes:LD,render:()=>accountEditor(false)},
{id:'S-6-account-editor-offline',title:'S-6 Nytt konto · offline',note:'Fält och Spara avstängda; förklaringen säger varför (AC-6.7).',themes:LD,render:()=>accountEditor(true)},
{id:'S-7-editor',title:'S-7 Ny utgift · 10-sekundersflödet',note:'Belopp först, stort och högerställt som en kassadisplay. Kassaknappsats i appen (00 och , alltid där). Spara i appfältet, alltid nåbart. Konto och kategori är förval (AC-8.6: senast använda i kontot). Delning öppnas med ett tryck.',themes:LD,render:()=>editorNew()},
{id:'S-7-editor-split',title:'S-7 Ny utgift · Exakta belopp med fel',note:'Beloppsraden fälls ihop till en kvittorad när man rullar. Alla fyra lägen som rektangulära valchips. Annas andel räknas ut och visas skrivskyddad; förhandsvisningen säger att den inte går ihop. Spara avstängd.',themes:LD,render:()=>editorSplit()},
{id:'S-7-editor-full',title:'S-7 Ny utgift · Allt på en person',note:'Knappsatsen är stängd (fokus i anteckningar): hela formuläret syns. Förhandsvisning från användarens sida: Du 0,00 kr, Anna 1 060,00 kr.',themes:LD,render:()=>editorFull()},
{id:'S-7-editor-200',title:'S-7 Ny utgift · 200 % textstorlek',note:'Etiketterna bryts över värdet; Spara ligger kvar i appfältet så att det alltid nås utan att rulla. Knappsatsens tangenter är 52 dp och rymmer siffrorna i 200 %.',themes:['light'],fontScale:2,render:()=>editorNew()},
{id:'S-8-expense',title:'S-8 Utgift · ICA Maxi Lindhagen',note:'Ett utskrivet kvitto med tandade kanter; historiken under är en revisionsremsa (tid till vänster, händelse till höger). Sista raden: din ändring väntar på synk.',themes:LD,render:()=>expenseDetail('default')},
{id:'S-8-expense-recurring',title:'S-8 Utgift · Hyra september (återkommande)',note:'Återkommande: Hyra med ikon. Historiken visar att servern skapade den 06:00.',themes:LD,render:()=>expenseDetail('recurring')},
{id:'S-8-expense-deleted',title:'S-8 Utgift · borttagen (från notis)',note:'Banderoll överst, kvittot nedtonat med stämpeln Borttagen. Ingen Ändra eller Ta bort.',themes:LD,render:()=>expenseDetail('deleted')},
{id:'S-9-settle',title:'S-9 Gör upp · förslag',note:'Fördelningen är en tabell med skuld per konto och redigerbart belopp; "Kvar att fördela" står under en dubbellinje som en kvittosumma. Infotext om Vardag.',themes:LD,render:()=>settleScreen('proposal')},
{id:'S-9-settle-adjusted',title:'S-9 Gör upp · justerat',note:'Varning per rad (ikon, ord, bärnstensram) när raden reglerar mer än skulden. Bekräfta är aktiv.',themes:LD,render:()=>settleScreen('adjusted')},
{id:'S-9-settle-remaining',title:'S-9 Gör upp · summan stämmer inte',note:'Kvar att fördela 250,00 kr i varningsfärg med förklaring; Bekräfta avstängd.',themes:LD,render:()=>settleScreen('remaining')},
{id:'S-9-settle-account',title:'S-9 Gör upp · bara Huset',note:'En fast rad, ingen Lägg till konto.',themes:LD,render:()=>settleScreen('account')},
{id:'S-9-confirm-dialog',title:'S-9 Bekräfta-dialog med varning',note:'Dialogen upprepar fördelningen och varningen (AC-11.5).',themes:LD,render:()=>settleScreen('adjusted',true)},
{id:'S-10-settlement',title:'S-10 Betalning',note:'Samma kvittoform som utgiften: Anna → du, belopp, datum, fördelning, anteckning, historik.',themes:LD,render:()=>settlementDetail()},
{id:'S-11-analytics',title:'S-11 Statistik · standardfilter',note:'TalkBack: "Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr. Per kategori: Hem störst med 70 733,50 kr, 39,7 %." Filterchips rullar i sidled. Diagrammet först, siffror i mono.',themes:LD,render:()=>analytics('default')},
{id:'S-11-analytics-scrolled',title:'S-11 Statistik · nedrullad (extra)',note:'TalkBack: "Per konto: Huset 79 860 kr, 44,8 %; Vardag 66 951,40 kr; Japan 2027 27 020 kr; Gotland 2026, arkiverat, 4 380 kr." Arkiverade konton räknas med (AC-14.9).',themes:LD,render:()=>analytics('scrolled')},
{id:'S-11-analytics-week',title:'S-11 Statistik · vecka, min del, per konto',note:'TalkBack: "Min del per vecka, v. 35 till v. 42: högst v. 40, 4 430 kr; lägst v. 35, 1 900 kr." Kontofärgerna är --chart-cat-1…4.',themes:LD,render:()=>analytics('week')},
{id:'S-11-analytics-empty',title:'S-11 Statistik · tom period',note:'Tom period: total 0,00 kr och en uppmaning att byta period.',themes:LD,render:()=>analytics('empty')},
{id:'S-12-drilldown',title:'S-12 Mat & hushåll, oktober 2026',note:'Samma rader som kontoutdraget; summan överst under en dubbellinje.',themes:LD,render:()=>drilldown()},
{id:'S-13-settings',title:'S-13 Inställningar',note:'Hushållet som ett litet kvitto; raderna har siffror till höger i mono (antal, version).',themes:LD,render:()=>settings()},
{id:'S-14-profile',title:'S-14 Profil',note:'',themes:LD,render:()=>profile()},
{id:'S-15-categories',title:'S-15 Kategorier',note:'Dra-handtag har TalkBack-etiketten "Flytta <namn>". Ny kategori via + i appfältet.',themes:LD,render:()=>categories()},
{id:'S-15-category-editor',title:'S-15 Ändra kategori · Matvaror',note:'Alla 30 ikoner i ett 8-kolumnsrutnät, 12 färger som fyrkantiga rutor. Standardkategorier kan bara arkiveras.',themes:LD,render:()=>categoryEditor()},
{id:'S-16-sync',title:'S-16 Synkstatus',note:'Avvisad ändring: vad som hände, när, och vad användaren kan göra.',themes:LD,render:()=>syncStatus(false)},
{id:'S-16-sync-clock',title:'S-16 Synkstatus · klockan går fel',note:'',themes:LD,render:()=>syncStatus(true)},
{id:'S-17-update',title:'S-17 Uppdatering · förklaring första gången',note:'Visas innan Android frågar om "Installera okända appar".',themes:LD,render:()=>updateDialog()},
{id:'S-17-update-progress',title:'S-17 Uppdatering · laddar ner',note:'',themes:LD,render:()=>screen({appbar:appBar({wordmark:true}),body:homeBody({banner:updateBanner('progress')}),nav:0,fabHtml:HOME_FAB})},
{id:'S-17-update-failed',title:'S-17 Uppdatering · misslyckades',note:'',themes:LD,render:()=>screen({appbar:appBar({wordmark:true}),body:homeBody({banner:updateBanner('failed')}),nav:0,fabHtml:HOME_FAB})},
{id:'S-18-removed',title:'S-18 Enheten borttagen',note:'',themes:LD,render:()=>removed()},
{id:'S-19-recurring',title:'S-19 Återkommande utgifter',note:'',themes:LD,render:()=>recurringList()},
{id:'S-19-recurring-editor',title:'S-19 Ändra återkommande · Hyra',note:'Samma rader som utgiftseditorn (utan datum) plus upprepningen.',themes:LD,render:()=>recurringEditor()},
{id:'S-20-devices',title:'S-20 Enheter',note:'Den här enheten kan inte tas bort (AC-3.2).',themes:LD,render:()=>devices(false)},
{id:'S-20-devices-revoke',title:'S-20 Enheter · bekräfta',note:'',themes:LD,render:()=>devices(true)},
{id:'S-21-invites',title:'S-21 Para ihop en enhet',note:'Koden i mono, grupper om fyra.',themes:LD,render:()=>invites()},
{id:'S-22-about',title:'S-22 Om Kvitt',note:'',themes:LD,render:()=>about()},
{id:'N-notifications',title:'N Notiser (Android 16, neutralt)',note:'Notisytan ritas neutralt, inte som One UI. Text byggs lokalt efter synk (AC-13.5).',themes:LD,render:()=>notifications()},
];
/* =========================================================================
RENDER
========================================================================= */
function phone(f,theme){return `<div class="phone" data-frame-id="${f.id}-${theme}" data-theme="${theme}" style="--fs:${f.fontScale||1}" lang="${f.id.endsWith('-en')?'en':'sv'}">${f.render(theme)}</div>`;}
const q = new URLSearchParams(location.search);
const root = document.getElementById('root');
if (q.get('list')){
document.body.innerHTML = `<pre id="frame-list">${FRAMES.flatMap(f=>f.themes.map(t=>`${f.id}-${t}`)).join('\n')}</pre>`;
} else if (q.get('frame')){
document.body.classList.add('frame-mode');
const id=q.get('frame'); const th=id.endsWith('-dark')?'dark':'light'; const fid=id.slice(0,-(th.length+1));
const f=FRAMES.find(x=>x.id===fid);
root.innerHTML = f?phone(f,th):`<p>Unknown frame ${id}</p>`;
} else {
root.className='review';
root.innerHTML = reviewPage();
}
function reviewPage(){
const textRoles=['on-surface','on-surface-variant','primary','owed','owing','settled','warning','pending','error','tertiary','secondary'];
const onPairs=[['on-primary','primary'],['on-primary-container','primary-container'],['on-secondary','secondary'],['on-secondary-container','secondary-container'],['on-tertiary','tertiary'],['on-tertiary-container','tertiary-container'],['on-error','error'],['on-error-container','error-container'],['on-owed','owed'],['on-owed-container','owed-container'],['on-owing','owing'],['on-owing-container','owing-container'],['on-settled-container','settled-container'],['on-warning','warning'],['on-warning-container','warning-container'],['on-avatar-1','avatar-1'],['on-avatar-2','avatar-2'],['inverse-on-surface','inverse-surface'],['on-surface','surface-container-high'],['on-surface-variant','surface-container-high'],['on-surface','paper*']];
const cell=(th,k)=>`<span class="swatch" style="background:${T[th][k]}"></span> <code>${T[th][k]}</code>`;
const crc=(a,b,min)=>{const r=cr(a,b);return `<td class="n${r<min?' fail':''}">${r.toFixed(2)}</td>`;};
const paperHex=th=>T[th][ALIAS[th].paper];
let rows='';
for (const [a,b] of onPairs){const bb=b==='paper*'?null:b;rows+=`<tr><td><code>--${a}</code> on <code>--${b==='paper*'?'paper (alias)':b}</code></td>${['light','dark'].map(th=>{const bg=bb?T[th][bb]:paperHex(th);return `<td>${cell(th,a)}</td><td><span class="swatch" style="background:${bg}"></span> <code>${bg}</code></td>${crc(T[th][a],bg,4.5)}`;}).join('')}</tr>`;}
let trows='';
for (const k of textRoles){trows+=`<tr><td><code>--${k}</code> (text)</td>${['light','dark'].map(th=>`<td>${cell(th,k)}</td>${crc(T[th][k],T[th].surface,4.5)}${crc(T[th][k],paperHex(th),4.5)}`).join('')}</tr>`;}
trows+=`<tr><td><code>--outline</code> (graphic)</td>${['light','dark'].map(th=>`<td>${cell(th,'outline')}</td>${crc(T[th].outline,T[th].surface,3)}${crc(T[th].outline,paperHex(th),3)}`).join('')}</tr>`;
let crows='';
for (const c of CATS){crows+=`<tr><td><code>--cat-${c}</code></td>${['light','dark'].map(th=>`<td>${cell(th,'cat-'+c)}</td><td>${cell(th,'cat-'+c+'-container')}</td>${crc(T[th]['cat-'+c],T[th].surface,3)}${crc(T[th]['cat-'+c],T[th]['surface-container-high'],3)}${crc(T[th]['cat-'+c],T[th]['cat-'+c+'-container'],3)}`).join('')}</tr>`;}
let chrows='';
for (const k of ['chart-cat-1','chart-cat-2','chart-cat-3','chart-cat-4','chart-cat-5','chart-cat-6','chart-cat-7','chart-cat-8','chart-seq-1','chart-seq-2','chart-seq-3','chart-seq-4','chart-seq-5','chart-grid','chart-axis-label']){chrows+=`<tr><td><code>--${k}</code></td>${['light','dark'].map(th=>`<td>${cell(th,k)}</td>${crc(T[th][k],T[th].surface,k.startsWith('chart-seq')||k==='chart-grid'?1:(k==='chart-axis-label'?4.5:3))}`).join('')}</tr>`;}
const all=Object.keys(BASE.light).filter(k=>!k.startsWith('photo')).concat(CATS.map(c=>`cat-${c}-container`),['chart-axis-label']);
const full = all.map(k=>`<tr><td><code>--${k}</code></td><td>${cell('light',k)}</td><td>${cell('dark',k)}</td></tr>`).join('');
const type = TYPE.map(([n,f,s,l,w,tr,u])=>`<tr><td><code>${n}</code></td><td>${f}</td><td class="n">${s}</td><td class="n">${l}</td><td class="n">${w}</td><td class="n">${tr}</td><td><span class="t-${n}" style="--fs:1;${s>30?'font-size:28px;line-height:34px':''}">${n.startsWith('amount')?kr(325000):'Gör upp · Återkommande'}</span></td><td>${u}</td></tr>`).join('');
const sample=(net,th)=>`<div class="sample" data-theme="${th}" style="--fs:1;background:var(--surface);color:var(--on-surface);font-family:var(--sans);font-variant-numeric:tabular-nums">${balanceBlock(net)}</div>`;
const groups = FRAMES.map(f=>`<div class="group"><h3>${f.title}</h3><div class="pairs">${f.themes.map(t=>`<div class="fr"><div class="cap">${f.id}-${t}${f.fontScale?` · ${f.fontScale*100} %`:''}</div>${phone(f,t)}</div>`).join('')}</div>${f.note?`<p class="note" style="font:400 13px/1.45 var(--sans);color:#5D584E;max-width:850px;margin-top:8px">${f.note}</p>`:''}</div>`).join('');
return `
<h1>2 · KVITTO</h1>
<p class="idea">A till receipt and a household ledger book: off-white paper, ink-black text, one ink-blue accent, monospaced amounts, dashed rules. Dense and calm; expressive only in motion.</p>
<p>Kvitto is list-first. Home opens with a small receipt-style balance block (wording and amount on two lines, Gör upp on the same block) and then the accounts as a ledger table. Account detail reads like a bank statement with a date column and dotted-leader month totals; expense and settlement detail are printed receipts with zig-zag edges and an audit-trail printout below. Analytics puts the chart first and sets every number in IBM Plex Mono. The editor is a till: amount first, right-aligned like a register display, typed on an in-app till keypad, then one-tap key/value rows. Rows are 56 to 64 dp, so more of the ledger fits per screen than in the other directions, while every target stays at least 48 dp.</p>
<h2>Colour</h2>
<p><b>Accent: ink blue</b> (<code>${T.light.primary}</code> / carbon-copy blue <code>${T.dark.primary}</code> in dark). The brief allows stamp red-orange instead, but in a money app red already means "you owe" and "error". A red accent on Gör upp, the FAB and Spara would make every main action look like a warning and fight the owing colour. Ink blue is a ballpoint on a receipt, a ledger's ruling and the blue of a carbon copy, so the dark theme ("carbon copy") gets its identity for free. Owed and owing are muted green and brick, always next to the words, with a small square marker. Category colours appear only as small icon markers on a tinted tile (container = ${CAT_MIX.light*100} % of the colour over <code>--surface</code> in light, ${CAT_MIX.dark*100} % in dark). <code>--paper</code> is a component alias (light: surface-container-lowest, dark: surface-container-high) used for receipts. Contrast ratios below are computed in the page; red means under 4.5:1 for text or 3:1 for graphics.</p>
<h3>On-colours on their colour</h3>
<table><tr><th>Pair</th><th>Light fg</th><th>Light bg</th><th>Ratio</th><th>Dark fg</th><th>Dark bg</th><th>Ratio</th></tr>${rows}</table>
<h3>Text roles on surface and on paper</h3>
<table><tr><th>Role</th><th>Light</th><th>vs surface</th><th>vs paper</th><th>Dark</th><th>vs surface</th><th>vs paper</th></tr>${trows}</table>
<h3>Category colours (graphics only: icons on their tile, never text)</h3>
<table><tr><th>Token</th><th>Light</th><th>Light container</th><th>vs surface</th><th>vs s-c-high</th><th>vs container</th><th>Dark</th><th>Dark container</th><th>vs surface</th><th>vs s-c-high</th><th>vs container</th></tr>${crows}</table>
<h3>Charts</h3>
<p>Accounts use <code>--chart-cat-n</code> in <code>created_at</code> order (Vardag 1, Huset 2, Gotland 2026 3, Japan 2027 4). Categories use their own category colour. Sequential and grid colours are fills, not marks that need 3:1 by themselves; the ratio is shown for reference.</p>
<table><tr><th>Token</th><th>Light</th><th>vs surface</th><th>Dark</th><th>vs surface</th></tr>${chrows}</table>
<details><summary style="font:600 14px var(--sans);cursor:pointer;margin:8px 0">All tokens (light / dark)</summary><table><tr><th>Token</th><th>Light</th><th>Dark</th></tr>${full}</table></details>
<h3>Dynamic colour (Material You)</h3>
<p><b>Not offered.</b> The paper palette is the identity: warm off-white with ink in light, warm carbon in dark. Wallpaper colours would tint the paper and replace the single ink accent with a hue that can land on green or red. That would collide with the owed and owing colours, which are the one thing the screen must not muddle. Compose cost of leaving it out is zero; the theme is a fixed <code>lightColorScheme</code>/<code>darkColorScheme</code> plus an <code>ExtendedColors</code> CompositionLocal for the Kvitt roles.</p>
<h2>Typography</h2>
<p><b>IBM Plex Sans</b> for text and <b>IBM Plex Mono</b> for amounts, dates in lists, captions and codes. Both are SIL Open Font License 1.1 and on Google Fonts, so they can be bundled as <code>res/font</code> resources (static TTFs, weights 400/500/600). Why: Plex Mono is the till-receipt voice and makes every figure tabular, so columns of amounts line up to the öre without any trick. Plex Sans is a sober grotesque with a large x-height and good Swedish diacritics (å ä ö sit inside the line height at 200 %). Amounts still set <code>fontFeatureSettings = "tnum"</code>. Receipt captions are <code>label-small</code> in Plex Mono, set in capitals with 0.08 em tracking by the component (the string stays mixed case for TalkBack).</p>
<table><tr><th>Style</th><th>Family</th><th>Size sp</th><th>Line sp</th><th>Weight</th><th>Tracking em</th><th>Sample</th><th>Used for</th></tr>${type}</table>
<h2>Spacing, shape, elevation</h2>
<div class="cols2"><div>
<h3>Spacing (dp)</h3><table><tr><th>Token</th><th>dp</th><th>Use</th></tr>
${[['--space-1',4,'icon-to-text inside chips, marker gaps'],['--space-2',8,'between chips, between buttons'],['--space-3',12,'row inner gaps, leading tile to text'],['--space-4',16,'screen gutter, row padding'],['--space-5',20,'receipt side padding'],['--space-6',24,'dialog padding, section gaps'],['--space-8',32,'empty-state padding'],['--space-12',48,'minimum touch target']].map(r=>`<tr><td><code>${r[0]}</code></td><td class="n">${r[1]}</td><td>${r[2]}</td></tr>`).join('')}</table>
<p>Rows: home account 60 dp, ledger row 60 dp, settings row 56 dp, editor key/value row 48 dp, breakdown row 52 dp.</p>
</div><div>
<h3>Radii (dp)</h3><table><tr><th>Token</th><th>M3 shape</th><th>dp</th><th>Components</th></tr>
${[['--radius-xs','extraSmall',4,'text fields, badges, category tiles, colour swatches, stamps, menus'],['--radius-sm','small',8,'buttons, chips, segmented buttons, banners, keypad keys, balance block top corners, large category tile'],['--radius-md','medium',12,'FAB (extended)'],['--radius-lg','large',16,'dialogs'],['--radius-lg-inc','largeIncreased',20,'not used'],['--radius-xl','extraLarge',28,'not used'],['--radius-xl-inc','extraLargeIncreased',32,'not used'],['--radius-xxl','extraExtraLarge',48,'not used'],['--radius-full','full',999,'avatars, navigation indicator pill, switch']].map(r=>`<tr><td><code>${r[0]}</code></td><td>${r[1]}</td><td class="n">${r[2]}</td><td>${r[3]}</td></tr>`).join('')}</table>
<p>Receipts have no radius: the top and bottom edges are zig-zag (10 dp teeth, a <code>GenericShape</code> in Compose).</p>
</div></div>
<h3>Elevation</h3>
<table><tr><th>Level</th><th>Where</th><th>How</th></tr>
<tr><td>0</td><td>Everything on screen: app bar, receipts, cards, nav bar, keypad</td><td>Separation by solid and dashed 1 dp rules and by surface tones (paper on surface). The app bar gets a 1 dp rule, not a tone change, when content scrolls under it.</td></tr>
<tr><td>2</td><td>Menus</td><td>surface-container + shadow 3 dp</td></tr>
<tr><td>3</td><td>FAB, dialogs</td><td>FAB keeps a small shadow so it reads over the ledger; dialogs use surface-container-high over a 42 % scrim</td></tr></table>
<h2>Motion</h2>
<p>Static screens are plain; motion carries the expressiveness. Durations and easing are Material 3 tokens.</p>
<table><tr><th>Moment</th><th>What moves</th><th>Duration</th><th>Easing</th></tr>
<tr><td>Receipt prints (S-8, S-10 open)</td><td>The receipt is revealed top-down from under the app bar, as if fed from a printer; its lines fade in staggered 20 ms each (at most 12 lines), the audit trail follows</td><td>350 ms + stagger</td><td>emphasizedDecelerate (0.05, 0.7, 0.1, 1)</td></tr>
<tr><td>Numbers roll</td><td>When a balance or total changes (after save, sync or settle), each changed digit rolls vertically; digits stagger 15 ms right to left like a register</td><td>250 ms per digit</td><td>standard (0.2, 0, 0, 1)</td></tr>
<tr><td>Stamp</td><td>When a balance reaches zero, the "Kvitt" stamp lands: scale 1.12 → 1, alpha 0 → 1, a light haptic tick</td><td>200 ms</td><td>emphasizedDecelerate</td></tr>
<tr><td>Save expense</td><td>The editor slides down; on the ledger the new row prints in (height 0 → 60 dp, then content fades)</td><td>300 ms</td><td>emphasizedAccelerate (0.3, 0, 0.8, 0.15) out, emphasizedDecelerate in</td></tr>
<tr><td>Push screens</td><td>Shared axis X</td><td>300 ms</td><td>emphasized</td></tr>
<tr><td>Tabs</td><td>Fade through</td><td>200 ms</td><td>standard</td></tr>
<tr><td>Delning expands</td><td>Height expand plus fade of the mode chips</td><td>250 ms</td><td>standard</td></tr>
<tr><td>Reduced motion</td><td>Animator duration scale 0 or "Remove animations": all of the above become a 150 ms crossfade, no roll, no print, no stamp scale</td><td>150 ms</td><td>linear</td></tr></table>
<h2>Iconography</h2>
<p><b>Material Symbols Sharp</b> (Apache 2.0): square corners match the receipt edges and small radii. Weight 300, grade 0, optical size 20 for icons at 18–20 dp and 24 for 24 dp, <b>fill 0 everywhere</b> except the selected navigation item (fill 1) and system status glyphs. Category icons are the 30 tokens of §9.7 drawn at 18 dp on a 28 dp tile (4 dp radius) in lists and 24 dp on a 40 dp tile in detail views. Every icon-only control has a Swedish <code>aria-label</code> in the mock (TalkBack contentDescription in the app). Icons are sized in dp and do not scale with font size; touch targets are 48 dp.</p>
<h2>Balance header: three states</h2>
<p>The real component in both themes. The wording is always in words; the amount carries the semantic colour and the square marker repeats it. Settled up replaces Gör upp with a "Kvitt" stamp.</p>
<div class="samples">${[-325000,118450,0].map(n=>LD.map(th=>sample(n,th)).join('')).join('')}</div>
<h2>200 % font scale</h2>
<p>All text is sized in sp. Text under 24 sp scales linearly (<code>calc(var(--fs) * N)</code>). Text of 24 sp and more follows Android 14+ non-linear font scaling, which Compose applies by default: in these mocks it is approximated as <code>max(N, min(fs × N, fs × 24))</code>, so at 200 % a 28 sp or 44 sp amount renders at 48 sp instead of 56 or 88 (Android's own curve is even gentler, for example 30 sp → 38 sp). Smaller text never becomes larger than bigger text. Rows are FlowRows: when a description and its amount no longer fit on one line, the amount drops to its own line, right-aligned, and is never truncated; descriptions may ellipsize. Key/value labels move above their value. Spara is in the editor's app bar, so it is reachable without scrolling at any scale. See <code>S-4-home-200</code> and <code>S-7-editor-200</code>.</p>
<h2>Components: Material 3 Expressive and fallbacks</h2>
<table><tr><th>Used</th><th>Expressive component</th><th>Stable Material 3 fallback</th></tr>
<tr><td>Bottom navigation</td><td><code>ShortNavigationBar</code> (64 dp, icon above label)</td><td><code>NavigationBar</code> (80 dp) with the same three items</td></tr>
<tr><td>Slider in the account editor</td><td>Material 3 slider with bar handle and track gap (stable since material3 1.3)</td><td>Same, already stable</td></tr>
<tr><td>Everything else</td><td>None. Buttons, chips, segmented buttons, text fields, top app bar, extended FAB, dialogs, menus, switch, linear progress are stable Material 3 with shape overrides (8 dp buttons and chips, 12 dp FAB, 4 dp text fields).</td><td>n/a</td></tr></table>
<p>Custom pieces (all plain Compose, no Expressive API): zig-zag receipt shape, rubber stamp, dotted leader, dashed divider (<code>drawLine</code> with <code>PathEffect.dashPathEffect</code>), in-app till keypad, stacked bar chart and breakdown bars (Canvas).</p>
<p><b>Keypad choice:</b> an in-app till keypad instead of the system numeric keyboard. Reasons: the decimal comma and a "00" key are always there (Samsung's numeric keyboard does not always offer a comma in Swedish); the keypad is there the moment the editor opens, with no keyboard animation and no layout jump, so the 10-second flow is predictable; and it is the till of the Kvitto idea. Costs: a custom component with TalkBack labels per key, and the hand-over to the system keyboard when Beskrivning gets focus. Fallback: <code>KeyboardType.Decimal</code> with the same layout.</p>
<h2>Frames</h2>
${groups}`;
}
</script>
</body>
</html>