Files
kvitt/docs/ux/directions/1-skargard/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

1354 lines
128 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 · 1 Skärgård</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=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000&display=block" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0..1,0&display=block" rel="stylesheet">
<style id="theme-css"></style>
<style>
/* ===================== Review page ===================== */
html,body{margin:0}
body{background:#EEF2F4;color:#1B2226;font-family:'Roboto Flex',Roboto,system-ui,sans-serif;font-optical-sizing:auto}
body.frame-mode{margin:0;background:transparent}
.rv{max-width:1000px;margin:0 auto;padding:48px 24px 96px}
.rv h1{font-size:44px;font-stretch:90%;font-weight:700;letter-spacing:-.5px;margin:0 0 4px}
.rv h2{font-size:26px;font-weight:600;margin:56px 0 12px;padding-top:16px;border-top:1px solid #CBD4D9}
.rv h3{font-size:18px;font-weight:600;margin:28px 0 8px}
.rv p,.rv li{font-size:15px;line-height:1.55;max-width:760px}
.rv .idea{font-size:20px;color:#1A6476;margin:0 0 16px}
.rv table{border-collapse:collapse;font-size:13px;margin:8px 0 16px;width:100%;max-width:960px}
.rv th,.rv td{text-align:left;padding:6px 8px;border-bottom:1px solid #D5DDE1;vertical-align:middle}
.rv th{font-weight:600;color:#40484C}
.rv td.n{font-variant-numeric:tabular-nums;white-space:nowrap}
.sw{display:inline-block;width:28px;height:20px;border-radius:6px;border:1px solid rgba(0,0,0,.15);vertical-align:middle;margin-right:6px}
.bad{color:#B3261E;font-weight:700}
.ok{color:#1F6B3A}
.cols{display:flex;flex-wrap:wrap;gap:24px}
.cols>div{flex:1 1 440px;min-width:0}
.frames{display:flex;flex-wrap:wrap;gap:32px;margin:12px 0 8px}
.fr{display:flex;flex-direction:column;gap:8px}
.fr .cap{font-size:13px;color:#40484C;max-width:411px}
.fr .cap b{color:#1B2226}
.rv .phone{border-radius:28px;box-shadow:0 1px 3px rgba(0,0,0,.18),0 10px 28px rgba(16,40,52,.14)}
.note{font-size:14px;color:#40484C;max-width:860px;margin:4px 0 0}
.heroes{display:flex;flex-wrap:wrap;gap:16px}
.heroes .hbox{width:411px;border-radius:28px;overflow:hidden}
/* ===================== Phone ===================== */
.phone,.hbox{--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;
--radius-xs:4px;--radius-sm:8px;--radius-md:12px;--radius-lg:16px;--radius-lg-inc:20px;--radius-xl:28px;--radius-xl-inc:32px;--radius-xxl:48px;--radius-full:9999px}
.phone{width:411px;height:891px;position:relative;overflow:hidden;display:flex;flex-direction:column;
background:var(--surface);color:var(--on-surface);font-family:'Roboto Flex',Roboto,system-ui,sans-serif;font-optical-sizing:auto;
-webkit-font-smoothing:antialiased;text-align:left}
.phone *{box-sizing:border-box}
:where(.phone,.hbox) button{font:inherit;color:inherit;border:0;background:none;padding:0;margin:0;cursor:pointer;text-align:inherit}
.hbox{position:relative;background:var(--surface);color:var(--on-surface);font-family:'Roboto Flex',Roboto,sans-serif;padding:16px}
.hbox *{box-sizing:border-box}
.ms{font-family:'Material Symbols Rounded';font-weight:normal;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;
font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24;-webkit-font-feature-settings:'liga';font-feature-settings:'liga';flex:none;overflow:hidden;width:1em;height:1em}
.ms.f{font-variation-settings:'FILL' 1,'wght' 400,'GRAD' 0,'opsz' 24}
.num{font-variant-numeric:tabular-nums;font-feature-settings:'tnum' 1}
/* type scale (sp: scaled by --fs; display/headline/hero use Android 14+ non-linear factors --fsh/--fsd/--fsx) */
.dl{font-size:calc(var(--fsd)*57px);line-height:calc(var(--fsd)*64px);font-weight:400;letter-spacing:-.25px}
.dm{font-size:calc(var(--fsd)*45px);line-height:calc(var(--fsd)*52px);font-weight:400}
.ds{font-size:calc(var(--fsd)*36px);line-height:calc(var(--fsd)*44px);font-weight:400}
.hl{font-size:calc(var(--fsh)*32px);line-height:calc(var(--fsh)*40px);font-weight:500}
.hm{font-size:calc(var(--fsh)*28px);line-height:calc(var(--fsh)*36px);font-weight:500}
.hs{font-size:calc(var(--fsh)*24px);line-height:calc(var(--fsh)*32px);font-weight:500}
.tl{font-size:calc(var(--fs)*22px);line-height:calc(var(--fs)*28px);font-weight:500}
.tm{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:560;letter-spacing:.15px}
.ts{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;letter-spacing:.1px}
.bl{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:400;letter-spacing:.3px}
.bm{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:400;letter-spacing:.2px}
.bs{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:400;letter-spacing:.3px}
.ll{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;letter-spacing:.1px}
.lm{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:560;letter-spacing:.4px}
.lsm{font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);font-weight:560;letter-spacing:.4px}
.ah{font-size:calc(var(--fsx)*60px);line-height:calc(var(--fsx)*64px);font-weight:720;font-stretch:84%;letter-spacing:-1.2px;font-variant-numeric:tabular-nums}
.ah .dec{font-size:.5em;font-weight:620;letter-spacing:-.3px;font-stretch:90%}
.al{font-size:calc(var(--fsh)*32px);line-height:calc(var(--fsh)*40px);font-weight:650;font-stretch:92%;letter-spacing:-.4px;font-variant-numeric:tabular-nums}
.am{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:620;font-variant-numeric:tabular-nums;letter-spacing:0}
.as{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:520;font-variant-numeric:tabular-nums;letter-spacing:.1px}
.nw{white-space:nowrap}
.v{color:var(--on-surface-variant)}
.c-owed{color:var(--owed)} .c-owing{color:var(--owing)} .c-settled{color:var(--settled)} .c-warn{color:var(--warning)} .c-err{color:var(--error)} .c-pend{color:var(--pending)} .c-pri{color:var(--primary)}
/* system chrome */
.sb{height:32px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 20px 0 24px;color:var(--on-surface);
font-size:calc(var(--fs)*14px);font-weight:500;letter-spacing:.2px;position:relative;z-index:3}
.sb .ic{display:flex;gap:4px;align-items:center}
.sb .bat{transform:rotate(90deg)}
.gest{height:24px;flex:none;display:flex;align-items:center;justify-content:center;position:relative;z-index:3}
.gest i{width:108px;height:4px;border-radius:2px;background:var(--on-surface);opacity:.55}
.app{flex:1;min-height:0;display:flex;flex-direction:column}
.scroll{flex:1;min-height:0;overflow:hidden}
.pad{padding-left:16px;padding-right:16px}
/* top app bar */
.tab{min-height:64px;flex:none;display:flex;align-items:center;gap:4px;padding:0 8px 0 4px}
.tab .tt{flex:1;min-width:0;padding-left:4px}
.tab .tt .tl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tab .tt.wrap .tl{white-space:normal}
.tab .acts{display:flex;align-items:center;gap:4px}
.tab.root{padding-left:16px}
.ib{width:48px;height:48px;border-radius:50%;display:inline-grid;place-items:center;color:var(--on-surface-variant);flex:none}
.ib.on{color:var(--on-surface)}
.ib.ton{background:var(--secondary-container);color:var(--on-secondary-container);width:48px;height:48px}
/* short navigation bar (Expressive) */
.nav{flex:none;min-height:64px;padding:4px 0;background:var(--surface-container);display:flex}
.nav .it{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:var(--on-surface-variant)}
.nav .ind{width:56px;height:32px;border-radius:16px;display:grid;place-items:center}
.nav .it.on .ind{background:var(--secondary-container);color:var(--on-secondary-container)}
.nav .it.on .lab{color:var(--secondary);font-weight:650}
.nav .lab{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:560;letter-spacing:.3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;padding:0 4px}
.big .nav .lab{font-stretch:72%;letter-spacing:-.2px}
/* FAB */
.fab{position:absolute;right:16px;bottom:104px;z-index:4;display:inline-flex;align-items:center;gap:12px;background:var(--primary-container);color:var(--on-primary-container);
box-shadow:0 3px 6px rgba(0,0,0,.16),0 1px 2px rgba(0,0,0,.12)}
.fab.m{min-height:80px;border-radius:20px;padding:0 28px 0 24px}
.fab.m .lb{font-size:calc(var(--fs)*20px);line-height:calc(var(--fs)*26px);font-weight:600;letter-spacing:0}
.fab.s{min-height:56px;border-radius:16px;padding:0 20px 0 16px}
.fab.s .lb{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*22px);font-weight:600}
.fab.nonav{bottom:40px}
/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 20px;border-radius:999px;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:600;letter-spacing:.1px;flex:none;white-space:nowrap}
.btn.m{min-height:56px;padding:0 28px;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);gap:8px}
.btn.xs{min-height:32px;padding:0 12px;font-size:calc(var(--fs)*13px)}
.btn.full{width:100%}
.btn.wrap{white-space:normal;text-align:center;padding-top:6px;padding-bottom:6px}
.btn.filled{background:var(--primary);color:var(--on-primary)}
.btn.tonal{background:var(--secondary-container);color:var(--on-secondary-container)}
.btn.outl{border:1px solid var(--outline-variant);color:var(--on-surface-variant)}
.btn.outl.pri{color:var(--primary);border-color:var(--outline)}
.btn.text{color:var(--primary);padding:0 12px}
.btn.err{color:var(--error)}
.btn.errf{background:var(--error);color:var(--on-error)}
.btn.owing{background:var(--owing);color:var(--on-owing)}
.btn[disabled],.btn.dis{background:color-mix(in srgb,var(--on-surface) 10%,transparent);color:color-mix(in srgb,var(--on-surface) 38%,transparent);border-color:transparent}
.btn.text[disabled]{background:none}
/* cards, groups */
.card{background:var(--surface-container-low);border-radius:28px;padding:20px}
.card.hi{background:var(--surface-container)}
.grp{display:flex;flex-direction:column;gap:2px}
.grp>.gi{background:var(--surface-container);border-radius:4px}
.grp>.gi:first-child{border-top-left-radius:20px;border-top-right-radius:20px}
.grp>.gi:last-child{border-bottom-left-radius:20px;border-bottom-right-radius:20px}
.grp.low>.gi{background:var(--surface-container-low)}
.sec{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:8px 4px 4px 4px}
.sec .ts{color:var(--on-surface-variant)}
.li{display:flex;align-items:center;gap:16px;min-height:56px;padding:8px 16px}
.li .m{flex:1;min-width:0}
.li .m>*{overflow-wrap:anywhere}
.li .lead{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;background:var(--secondary-container);color:var(--on-secondary-container);flex:none}
.li .ch{color:var(--on-surface-variant)}
.divider{height:1px;background:var(--outline-variant);margin:0 16px}
/* chips */
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:4px 16px 4px 10px;border-radius:999px;border:1px solid var(--outline-variant);
color:var(--on-surface-variant);font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;max-width:100%}
.chip .ms{color:var(--primary)}
.chip .cl{color:var(--on-surface-variant);font-weight:450}
.chip .cv{color:var(--on-surface)}
.chip.sel{background:var(--secondary-container);border-color:transparent;color:var(--on-secondary-container)}
.chip.sel .ms,.chip.sel .cl,.chip.sel .cv{color:var(--on-secondary-container)}
.chip.sm{min-height:32px;padding:2px 12px 2px 8px}
.chip.dd{padding-right:10px}
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;background:var(--surface-container-highest);color:var(--on-surface-variant);
font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);font-weight:600;letter-spacing:.4px;white-space:nowrap}
.badge.tert{background:var(--tertiary-container);color:var(--on-tertiary-container)}
.badge.pri{background:var(--primary-container);color:var(--on-primary-container)}
/* fields (outlined, large radius) */
.fld{display:flex;flex-direction:column;gap:4px;min-width:0}
.fbox{position:relative;min-height:56px;border:1px solid var(--outline);border-radius:16px;display:flex;align-items:center;gap:12px;padding:8px 16px;color:var(--on-surface)}
.fld.foc .fbox{border:2px solid var(--primary);padding:7px 15px}
.fld.er .fbox{border:2px solid var(--error);padding:7px 15px}
.fld.dis .fbox{border-color:color-mix(in srgb,var(--on-surface) 12%,transparent);color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
.fld.ro .fbox{border-style:dashed;border-color:var(--outline-variant)}
.flab{position:absolute;left:12px;top:calc(var(--fs)*-8px);padding:0 4px;background:var(--fbg,var(--surface));color:var(--on-surface-variant);
font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:500;letter-spacing:.3px;white-space:nowrap;max-width:calc(100% - 24px);overflow:hidden;text-overflow:ellipsis}
.fld.foc .flab{color:var(--primary)} .fld.er .flab{color:var(--error)}
.fld.dis .flab{color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
.fval{flex:1;min-width:0;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);letter-spacing:.3px;overflow-wrap:anywhere}
.fval.r{text-align:right}
.fval.ph{color:var(--on-surface-variant)}
.caret{display:inline-block;width:2px;height:1.1em;background:var(--primary);vertical-align:-.15em;margin-left:1px;border-radius:1px}
.fsup{padding:0 16px;font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);color:var(--on-surface-variant);letter-spacing:.3px}
.fld.er .fsup{color:var(--error)}
.fbox .ms{color:var(--on-surface-variant)}
.fld.er .fbox .ms.tr{color:var(--error)}
/* avatars */
.av{display:inline-grid;place-items:center;flex:none;overflow:hidden;font-weight:650;letter-spacing:0;line-height:1}
.av svg{width:100%;height:100%;display:block}
.pair{display:inline-flex;align-items:center}
.pair .av+.av{margin-left:-10px}
.pair .ring{border-radius:50%;box-shadow:0 0 0 3px var(--ringc,var(--surface))}
/* category icon */
.ci{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:var(--cc);color:var(--c);flex:none}
.ci.s{width:32px;height:32px;border-radius:10px}
.ci.l{width:56px;height:56px;border-radius:16px}
/* ledger rows */
.mh{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:0 8px;padding:16px 16px 6px 20px;color:var(--on-surface-variant)}
.mh .ts{color:var(--on-surface)}
.row{display:flex;align-items:center;flex-wrap:wrap;gap:4px 14px;padding:10px 16px;min-height:72px}
.row .rm{flex:1 1 150px;min-width:0}
.row .rtop{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px}
.row .rt{flex:1 1 120px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row .rs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;color:var(--on-surface-variant)}
.row .ra{flex:0 0 auto;margin-left:auto;text-align:right}
.eff-lent{color:var(--owed)} .eff-borrowed{color:var(--owing)}
.pend{display:inline-flex;align-items:center;gap:2px;color:var(--pending)}
/* hero */
.hero{position:relative;border-radius:32px;background:var(--surface-container);padding:20px 20px 24px;overflow:hidden}
.hero .top{display:flex;align-items:center;gap:12px;position:relative;z-index:1}
.hero .word{position:relative;z-index:1;margin-top:16px}
.hero .amt{position:relative;z-index:1;margin-top:2px;overflow-wrap:anywhere}
.hero .acts{position:relative;z-index:1;margin-top:16px;display:flex;gap:8px;flex-wrap:wrap}
.hero .deco{position:absolute;right:-4px;top:-6px;width:200px;height:96px;z-index:0;pointer-events:none}
.hero.settled .amt{color:var(--settled)}
/* banners */
.banner{border-radius:24px;background:var(--tertiary-container);color:var(--on-tertiary-container);padding:16px 16px 12px 16px}
.banner .bh{display:flex;gap:12px;align-items:flex-start}
.banner .ba{display:flex;justify-content:flex-end;gap:8px;margin-top:8px;flex-wrap:wrap}
.banner .btn.text{color:var(--on-tertiary-container)}
.banner .btn.filled{background:var(--tertiary);color:var(--on-tertiary)}
.banner.err{background:var(--error-container);color:var(--on-error-container)}
.banner.err .btn.filled{background:var(--error);color:var(--on-error)}
.banner.err .btn.text{color:var(--on-error-container)}
.banner.warn{background:var(--warning-container);color:var(--on-warning-container)}
.syncrow{display:flex;align-items:center;gap:8px;min-height:48px;padding:4px 12px 4px 16px;border-radius:999px;color:var(--on-surface-variant);width:100%}
.syncrow .t{flex:1;min-width:0}
.syncrow.off{background:var(--surface-container-high);color:var(--on-surface)}
.syncrow.off .ms.l{color:var(--pending)}
/* overlays */
.scrim{position:absolute;inset:0;background:color-mix(in srgb,var(--scrim) 40%,transparent);z-index:8}
.dlg{position:absolute;left:28px;right:28px;top:50%;transform:translateY(-50%);z-index:9;background:var(--surface-container-high);border-radius:28px;padding:24px;--fbg:var(--surface-container-high)}
.dlg .di{display:grid;place-items:center;color:var(--secondary);margin-bottom:16px}
.dlg .dt{margin:0 0 16px}
.dlg .db{color:var(--on-surface-variant)}
.dlg .da{display:flex;justify-content:flex-end;gap:8px;margin-top:24px;flex-wrap:wrap}
.menu{position:absolute;z-index:9;background:var(--surface-container);border-radius:16px;padding:8px 0;min-width:200px;
box-shadow:0 2px 6px rgba(0,0,0,.14),0 6px 20px rgba(0,0,0,.12)}
.menu .mi{display:flex;align-items:flex-start;gap:12px;min-height:48px;padding:12px 16px;color:var(--on-surface)}
.menu .mi .ms{color:var(--on-surface-variant)}
.menu .mi.dis{color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
.menu .mi.dis .ms{color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
.menu .mi .sup{color:var(--on-surface-variant);margin-top:2px}
/* keypad */
.kp{flex:none;padding:8px 12px 8px;display:grid;grid-template-columns:repeat(3,1fr);gap:8px;background:var(--surface-container-low)}
.key{height:64px;border-radius:999px;background:var(--surface-container-highest);color:var(--on-surface);display:grid;place-items:center;
font-size:calc(var(--fsh)*26px);line-height:1;font-weight:500;font-variant-numeric:tabular-nums}
.key.sec2{background:var(--secondary-container);color:var(--on-secondary-container);border-radius:20px}
.key.del{background:var(--tertiary-container);color:var(--on-tertiary-container);border-radius:20px}
.key.pressed{border-radius:16px;background:var(--primary);color:var(--on-primary)}
.kp.sm .key{height:52px}
/* segmented / button group */
.bg{display:inline-flex;gap:2px}
.bg .b{min-height:40px;padding:0 14px;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:var(--surface-container-high);color:var(--on-surface-variant);
font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;border-radius:8px;white-space:nowrap}
.bg .b:first-child{border-top-left-radius:20px;border-bottom-left-radius:20px}
.bg .b:last-child{border-top-right-radius:20px;border-bottom-right-radius:20px}
.bg .b.on{background:var(--secondary);color:var(--on-secondary);border-radius:20px}
.bg.full{display:flex}.bg.full .b{flex:1}
/* misc */
.tl-list{position:relative;padding-left:4px}
.tl-it{display:flex;gap:14px;position:relative;padding-bottom:14px}
.tl-dot{width:12px;height:12px;border-radius:50%;background:var(--outline);margin-top:4px;flex:none;position:relative;z-index:1;box-shadow:0 0 0 4px var(--tlbg,var(--surface))}
.tl-it:not(:last-child)::before{content:"";position:absolute;left:5px;top:12px;bottom:-2px;width:2px;background:var(--outline-variant)}
.tl-it.pending .tl-dot{background:var(--pending)}
.tl-it.first .tl-dot{background:var(--primary)}
.kv{display:flex;gap:16px;align-items:center;min-height:56px;padding:8px 16px}
.kv .k{color:var(--on-surface-variant)}
.sk{background:var(--surface-container-high);border-radius:12px}
.sw-sel{outline:3px solid var(--on-surface);outline-offset:3px}
.switch{width:52px;height:32px;border-radius:16px;background:var(--surface-container-highest);border:2px solid var(--outline);position:relative;flex:none}
.switch::after{content:"";position:absolute;left:6px;top:6px;width:16px;height:16px;border-radius:50%;background:var(--outline)}
.switch.on{background:var(--primary);border-color:var(--primary)}
.switch.on::after{left:22px;top:2px;width:24px;height:24px;background:var(--on-primary)}
.slider{position:relative;height:44px;display:flex;align-items:center}
.slider .tr{position:absolute;left:0;right:0;height:16px;display:flex;gap:6px;align-items:center}
.slider .a{height:16px;border-radius:8px 4px 4px 8px;background:var(--primary)}
.slider .b2{flex:1;height:16px;border-radius:4px 8px 8px 4px;background:var(--secondary-container)}
.slider .h{width:4px;height:44px;border-radius:2px;background:var(--primary);flex:none}
.stop{position:absolute;right:4px;width:4px;height:4px;border-radius:50%;background:var(--on-secondary-container)}
</style>
</head>
<body>
<div id="root"></div>
<script>
'use strict';
/* =====================================================================
Tokens
===================================================================== */
const TOKENS = {
light: {
'primary':'#1A6476','on-primary':'#FFFFFF','primary-container':'#C2E8F3','on-primary-container':'#0B4C5C',
'secondary':'#4E5F69','on-secondary':'#FFFFFF','secondary-container':'#D3E3EE','on-secondary-container':'#374851',
'tertiary':'#7B5C24','on-tertiary':'#FFFFFF','tertiary-container':'#F7DFB4','on-tertiary-container':'#5A410E',
'error':'#B3261E','on-error':'#FFFFFF','error-container':'#F9DEDC','on-error-container':'#8C1D18',
'surface':'#F6FAFC','on-surface':'#171C1F','surface-variant':'#DBE4E8','on-surface-variant':'#40484C',
'surface-container-lowest':'#FFFFFF','surface-container-low':'#F0F4F7','surface-container':'#EAEFF2',
'surface-container-high':'#E4E9EC','surface-container-highest':'#DEE3E6','surface-dim':'#D6DBDE','surface-bright':'#F6FAFC',
'inverse-surface':'#2C3134','inverse-on-surface':'#EDF1F4','inverse-primary':'#8ED0E2',
'outline':'#70787C','outline-variant':'#BFC8CC','scrim':'#000000',
'owed':'#00695C','on-owed':'#FFFFFF','owed-container':'#C2EDE4','on-owed-container':'#00443B',
'owing':'#A33E1C','on-owing':'#FFFFFF','owing-container':'#FFDBCF','on-owing-container':'#792A0B',
'settled':'#4E5F69','settled-container':'#DCE5EA','on-settled-container':'#36464F',
'warning':'#855700','on-warning':'#FFFFFF','warning-container':'#FFDEAD','on-warning-container':'#633F00',
'pending':'#7B5C24',
'avatar-1':'#C2E8F3','on-avatar-1':'#0B4C5C','avatar-2':'#F2DDB8','on-avatar-2':'#5A400E',
'cat-green':'#2A732E','cat-orange':'#A84300','cat-brown':'#795548','cat-yellow':'#7D6000','cat-blue':'#1565C0','cat-teal':'#006F62',
'cat-purple':'#7B1FA2','cat-red':'#C62828','cat-pink':'#AD1457','cat-grey':'#58646C','cat-indigo':'#3949AB','cat-lime':'#5B6B00',
'chart-cat-1':'#1A6476','chart-cat-2':'#A8741A','chart-cat-3':'#5B5FA8','chart-cat-4':'#4D8559','chart-cat-5':'#A0526E','chart-cat-6':'#2F86A0','chart-cat-7':'#7A6A58','chart-cat-8':'#66737C',
'chart-seq-1':'#D3ECF3','chart-seq-2':'#9FD3E2','chart-seq-3':'#62AFC5','chart-seq-4':'#2E869E','chart-seq-5':'#165A6B',
'chart-grid':'#DEE3E6','chart-axis-label':'#40484C'
},
dark: {
'primary':'#8ED0E2','on-primary':'#00363F','primary-container':'#0E4D5C','on-primary-container':'#C2E8F3',
'secondary':'#B6C9D4','on-secondary':'#20333B','secondary-container':'#374952','on-secondary-container':'#D3E5F0',
'tertiary':'#E8C48A','on-tertiary':'#422C00','tertiary-container':'#5D4410','on-tertiary-container':'#FFDEA8',
'error':'#F2B8B5','on-error':'#601410','error-container':'#8C1D18','on-error-container':'#F9DEDC',
'surface':'#0F1417','on-surface':'#DEE3E6','surface-variant':'#40484C','on-surface-variant':'#BFC8CC',
'surface-container-lowest':'#0A0F11','surface-container-low':'#171C1F','surface-container':'#1B2124',
'surface-container-high':'#252B2E','surface-container-highest':'#303639','surface-dim':'#0F1417','surface-bright':'#353A3D',
'inverse-surface':'#DEE3E6','inverse-on-surface':'#2C3134','inverse-primary':'#1A6476',
'outline':'#899296','outline-variant':'#40484C','scrim':'#000000',
'owed':'#7DD8C5','on-owed':'#00382F','owed-container':'#005047','on-owed-container':'#A3F2E0',
'owing':'#FFB59E','on-owing':'#5E1700','owing-container':'#7E2C0E','on-owing-container':'#FFDBCF',
'settled':'#B6C9D4','settled-container':'#2C3B43','on-settled-container':'#D3E5F0',
'warning':'#F5BD62','on-warning':'#452B00','warning-container':'#633F00','on-warning-container':'#FFDEAD',
'pending':'#E8C48A',
'avatar-1':'#0E4D5C','on-avatar-1':'#C2E8F3','avatar-2':'#5D4410','on-avatar-2':'#F7DFB4',
'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':'#8ED0E2','chart-cat-2':'#E8C48A','chart-cat-3':'#B8BCF4','chart-cat-4':'#9BD3A3','chart-cat-5':'#EDA9C1','chart-cat-6':'#5FB6D0','chart-cat-7':'#CDB9A1','chart-cat-8':'#AEBDC8',
'chart-seq-1':'#1E3A42','chart-seq-2':'#245A68','chart-seq-3':'#2F7C8F','chart-seq-4':'#5EADC3','chart-seq-5':'#A3DDEC',
'chart-grid':'#303639','chart-axis-label':'#BFC8CC'
}
};
const CAT_TOKENS = ['green','orange','brown','yellow','blue','teal','purple','red','pink','grey','indigo','lime'];
/* category container rule: 16 % (light) / 22 % (dark) of the category colour over --surface */
const CAT_MIX = {light:.16, dark:.22};
/* ----- colour maths ----- */
function hex2rgb(h){h=h.replace('#','');return [0,2,4].map(i=>parseInt(h.substr(i,2),16));}
function rgb2hex(c){return '#'+c.map(v=>Math.round(v).toString(16).padStart(2,'0')).join('').toUpperCase();}
function mix(a,b,t){const x=hex2rgb(a),y=hex2rgb(b);return rgb2hex(x.map((v,i)=>v*t+y[i]*(1-t)));}
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 l1=lum(a),l2=lum(b);return (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05);}
for(const th of ['light','dark']){ for(const c of CAT_TOKENS){ TOKENS[th]['cat-'+c+'-container']=mix(TOKENS[th]['cat-'+c],TOKENS[th]['surface'],CAT_MIX[th]); } }
/* theme CSS from tokens */
(function(){
let css='';
for(const th of ['light','dark']){
const sel=`.phone[data-theme="${th}"],.hbox[data-theme="${th}"]`;
css+=sel+'{'+Object.entries(TOKENS[th]).map(([k,v])=>`--${k}:${v}`).join(';')+'}\n';
}
document.getElementById('theme-css').textContent=css;
})();
/* =====================================================================
Formatting helpers
===================================================================== */
const NB=' ';
function grp(n,sep){return String(n).replace(/\B(?=(\d{3})+(?!\d))/g,sep);}
function krn(ore){ore=Math.abs(Math.round(ore));return grp(Math.floor(ore/100),NB)+','+String(ore%100).padStart(2,'0');}
function kr(ore){return krn(ore)+NB+'kr';}
function sek(ore){ore=Math.abs(Math.round(ore));return 'SEK'+NB+grp(Math.floor(ore/100),',')+'.'+String(ore%100).padStart(2,'0');}
function pct1(part,total){ // half-up to one decimal (AC-14.10), integer maths on öre
const x=Math.floor((part*2000+total)/(2*total)); // round(part*1000/total)
return String(Math.floor(x/10))+','+(x%10)+NB+'%';
}
/* non-linear font scaling (Android 14+ FontScaleConverter table for 200 %), used for text >= 24 sp only */
const NL2=[[8,16],[10,20],[12,24],[14,26],[18,30],[20,34],[24,36],[30,38],[100,100]];
function nlf(sp,fs){ if(fs<=1) return 1; let out=sp*2;
for(let i=0;i<NL2.length-1;i++){const [a,A]=NL2[i],[b,B]=NL2[i+1]; if(sp>=a&&sp<=b){out=A+(B-A)*(sp-a)/(b-a);break;}}
if(sp>100) out=sp; const f2=out/sp; return +(1+(f2-1)*(fs-1)).toFixed(3); }
/* =====================================================================
Shapes (Material 3 Expressive MaterialShapes, approximated as masks)
===================================================================== */
function polarPath(n,amp,rot){const pts=[];const N=240;for(let i=0;i<N;i++){const t=i/N*Math.PI*2;const r=46*(1+amp*Math.cos(n*(t+rot)));pts.push([50+r*Math.cos(t),50+r*Math.sin(t)]);}
return 'M'+pts.map(p=>p[0].toFixed(2)+' '+p[1].toFixed(2)).join('L')+'Z';}
function maskUrl(d){return `url('data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path d='${d}'/></svg>`).replace(/'/g,'%27')}')`;}
const SHAPE={ cookie9: maskUrl(polarPath(9,0.065,0)), cookie12: maskUrl(polarPath(12,0.045,0)), clover4: maskUrl(polarPath(4,0.16,Math.PI/4)) };
function shp(name){return `-webkit-mask:${SHAPE[name]} center/100% 100% no-repeat;mask:${SHAPE[name]} center/100% 100% no-repeat;`;}
/* =====================================================================
Primitive components
===================================================================== */
const esc=s=>String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
function I(n,o={}){const s=o.s||24;return `<span class="ms${o.f?' f':''}${o.c?' '+o.c:''}" style="font-size:${s}px${o.st?';'+o.st:''}"${o.l?` role="img" aria-label="${esc(o.l)}"`:' aria-hidden="true"'}>${n}</span>`;}
function IB(icon,label,cls='',o={}){return `<button class="ib ${cls}" aria-label="${esc(label)}">${I(icon,o)}</button>`;}
/* avatars: who = 'J' (picture stand-in) or 'A' (initials on --avatar-2) */
function av(who,size,shape){
const sh = shape ? shp(shape) : 'border-radius:50%;';
const fsz = Math.round(size*0.42);
if(who==='A') return `<span class="av" role="img" aria-label="Anna" style="width:${size}px;height:${size}px;${sh}background:var(--avatar-2);color:var(--on-avatar-2);font-size:min(calc(var(--fs)*${fsz}px),${Math.round(size*0.6)}px)">A</span>`;
if(who==='J?') return `<span class="av" role="img" aria-label="Din partner" style="width:${size}px;height:${size}px;${sh}background:var(--surface-container-highest);color:var(--on-surface-variant);border:2px dashed var(--outline)">${I('person_add',{s:Math.round(size*0.45)})}</span>`;
return `<span class="av" role="img" aria-label="Jacob" style="width:${size}px;height:${size}px;${sh}background:linear-gradient(165deg,var(--tertiary-container) 0%,var(--primary-container) 52%,var(--primary) 100%)"><svg viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="16.5" r="6.6" style="fill:var(--on-primary-container)"/><path d="M6.5 41c0-9 6-13.6 13.5-13.6S33.5 32 33.5 41z" style="fill:var(--on-primary-container)"/></svg></span>`;
}
function pairAv(size,shape,ring){return `<span class="pair" style="--ringc:var(${ring||'--surface'})">${av('J',size,shape)}${av('A',size,shape)}</span>`;}
function catIc(key,cls=''){const c=CAT[key];return `<span class="ci ${cls}" style="--c:var(--cat-${c.c});--cc:var(--cat-${c.c}-container)">${I(c.icon,{f:1,s:cls==='s'?18:(cls==='l'?28:22)})}</span>`;}
function btn(label,cls='filled',icon,o={}){return `<button class="btn ${cls}"${o.dis?' disabled':''}${o.al?` aria-label="${esc(o.al)}"`:''}>${icon?I(icon,{s:cls.includes(' m')?24:18}):''}${label}</button>`;}
function field(o){
const cls=['fld',o.foc?'foc':'',o.err?'er':'',o.dis?'dis':'',o.ro?'ro':''].join(' ');
const sup=o.err?`<div class="fsup">${o.err}</div>`:(o.sup?`<div class="fsup">${o.sup}</div>`:'');
return `<div class="${cls}" style="${o.st||''}"><div class="fbox">${o.label?`<span class="flab">${o.label}</span>`:''}${o.lead?I(o.lead,{s:22}):''}<span class="fval${o.r?' r':''}${o.ph?' ph':''}${o.num?' num':''}" ${o.vcls?`style="${o.vcls}"`:''}>${o.value||''}${o.foc&&!o.nocaret?'<span class="caret"></span>':''}</span>${o.trail||''}${o.err?I('error',{f:1,s:22,c:'tr'}):''}</div>${sup}</div>`;
}
function chip(o){return `<button class="chip${o.sel?' sel':''}${o.sm?' sm':''}${o.dd?' dd':''}"${o.al?` aria-label="${esc(o.al)}"`:''}>${o.lead||''}${o.icon?I(o.icon,{s:18,f:o.f}):''}<span>${o.k?`<span class="cl">${o.k} </span>`:''}<span class="cv">${o.v}</span></span>${o.dd?I(o.ddi||'arrow_drop_down',{s:18}):''}</button>`;}
function statusBar(){return `<div class="sb" style="background:var(--sbbg)"><span class="num">14:02</span><span class="ic">${I('signal_cellular_alt',{s:17})}${I('wifi',{s:17,f:1})}${I('battery_full',{s:17,f:1,c:'bat'})}</span></div>`;}
function appBar(o){
return `<header class="tab${o.root?' root':''}" style="${o.bg?`background:var(--${o.bg})`:''}">${o.nav?IB(o.nav,o.navL||'Tillbaka','on'):''}<div class="tt${o.wrap?' wrap':''}"><div class="tl">${o.title||''}</div>${o.sub?`<div class="bm v">${o.sub}</div>`:''}</div><div class="acts">${o.acts||''}</div></header>`;
}
function navBar(active,en){
const items=[['home','Översikt','Home'],['bar_chart','Statistik','Analytics'],['settings','Inställningar','Settings']];
return `<nav class="nav" aria-label="${en?'Navigation':'Navigering'}">${items.map(([ic,sv,e],i)=>`<button class="it${i===active?' on':''}"><span class="ind">${I(ic,{f:i===active})}</span><span class="lab">${en?e:sv}</span></button>`).join('')}</nav>`;
}
function fab(label,o={}){return `<button class="fab ${o.s?'s':'m'}${o.nonav?' nonav':''}"${o.st?` style="${o.st}"`:''}>${I('add',{s:o.s?24:28})}<span class="lb">${label}</span></button>`;}
function kvittMark(size){return `<span style="width:${size}px;height:${size}px;${shp('cookie12')}background:var(--primary);display:inline-grid;place-items:center;flex:none"><svg viewBox="0 0 40 40" width="${size*0.62}" height="${size*0.62}" aria-hidden="true"><path d="M7 23c4-4 7-4 11 0s7 4 11 0 5-3 5-3" style="fill:none;stroke:var(--on-primary);stroke-width:3.4;stroke-linecap:round"/><path d="M7 15c4-4 7-4 11 0s7 4 11 0 5-3 5-3" style="fill:none;stroke:var(--on-primary);stroke-width:3.4;stroke-linecap:round;opacity:.6"/></svg></span>`;}
function islands(){ /* decorative archipelago in the balance hero: granite islets and a calm swell */
return `<svg class="deco" viewBox="0 0 210 120" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
<path d="M96 88c10-17 24-26 38-26 16 0 22 12 34 14 12 2 22-6 42-4v40H96z" style="fill:var(--surface-container-highest)"/>
<path d="M150 60c6-12 15-18 24-18 10 0 14 8 22 8 6 0 10-3 14-3v36h-62z" style="fill:var(--surface-container-high)"/>
<path d="M34 104c10-6 20-6 30 0s20 6 30 0 20-6 30 0 20 6 30 0 20-6 30 0 20 6 26 2" style="fill:none;stroke:var(--outline-variant);stroke-width:2;stroke-linecap:round"/>
<path d="M74 116c10-6 20-6 30 0s20 6 30 0 20-6 30 0 20 6 30 0" style="fill:none;stroke:var(--outline-variant);stroke-width:2;stroke-linecap:round;opacity:.6"/>
</svg>`;
}
function scroll(inner,st=''){return `<div class="scroll" style="${st}">${inner}</div>`;}
/* =====================================================================
Shared data (brief §5)
===================================================================== */
const CAT={
groceries:{sv:'Mat & hushåll',en:'Groceries',icon:'shopping_cart',c:'green'},
restaurants:{sv:'Restaurang & café',en:'Restaurants',icon:'restaurant',c:'orange'},
home:{sv:'Hem',en:'Home',icon:'home',c:'brown'},
utilities:{sv:'Räkningar',en:'Utilities',icon:'bolt',c:'yellow'},
transport:{sv:'Transport',en:'Transport',icon:'directions_car',c:'blue'},
travel:{sv:'Resor',en:'Travel',icon:'flight',c:'teal'},
entertainment:{sv:'Nöje',en:'Entertainment',icon:'movie',c:'purple'},
health:{sv:'Hälsa',en:'Health',icon:'medical_services',c:'red'},
shopping:{sv:'Shopping',en:'Shopping',icon:'shopping_bag',c:'pink'},
other:{sv:'Övrigt',en:'Other',icon:'category',c:'grey'},
pant:{sv:'Pant',icon:'water_drop',c:'lime'}
};
const ACCOUNTS=[
{name:'Vardag',net:118450,oct:11,arch:false,split:'50 / 50'},
{name:'Huset',net:-308450,oct:4,arch:false,split:'60 / 40'},
{name:'Gotland 2026',net:0,oct:0,arch:true,split:'50 / 50'},
{name:'Japan 2027',net:-135000,oct:2,arch:false,split:'50 / 50'}
];
const GLOBAL_NET=ACCOUNTS.reduce((s,a)=>s+a.net,0); // -325000
// effect: ['lent',ore] ['borrowed',ore] ['none'] ['self']
const VARDAG=[
{m:'Oktober',total:745790,rows:[
{d:'16 okt.',t:'ICA Maxi Lindhagen',c:'groceries',a:124760,e:['borrowed',62380]},
{d:'15 okt.',t:'Systembolaget',c:'entertainment',a:41200,e:['lent',20600]},
{d:'14 okt.',t:'SL-kort 30 dagar, Anna',c:'transport',a:106000,e:['lent',106000],p:1},
{d:'12 okt.',t:'Willys Hornstull',c:'groceries',a:86435,e:['lent',43217]},
{d:'10 okt.',t:'Babette',c:'restaurants',a:118000,e:['borrowed',59000]},
{d:'9 okt.',t:'Apotek Hjärtat',c:'health',a:18990,e:['self']},
{d:'7 okt.',t:'ICA Nära Hornstull',c:'groceries',a:78560,e:['borrowed',39280]},
{d:'6 okt.',settle:'Anna betalade dig',a:80000},
{d:'4 okt.',t:'Clas Ohlson',c:'home',a:45900,e:['borrowed',22950]},
{d:'3 okt.',t:'Netflix',c:'entertainment',a:17900,e:['lent',8950]},
{d:'2 okt.',t:'Pressbyrån',c:'restaurants',a:6800,e:['none']},
{d:'1 okt.',t:'Lidl Liljeholmen',c:'groceries',a:101245,e:['lent',50622]}]},
{m:'September',total:941240,rows:[
{d:'28 sep.',t:'Coop Södermalm',c:'groceries',a:73420,e:['borrowed',36710]},
{d:'26 sep.',t:'Hemköp',c:'groceries',a:51280,e:['lent',25640]}]}
];
const GOTLAND=[
{m:'Augusti',total:0,rows:[{d:'3 aug.',settle:'Du betalade Anna',a:35000}]},
{m:'Juli',total:438000,rows:[
{d:'18 jul.',t:'Clarion Hotel Wisby, 3 nätter',c:'travel',a:254000,e:['borrowed',127000]},
{d:'15 jul.',t:'Destination Gotland, färja t/r',c:'travel',a:184000,e:['lent',92000]}]}
];
function effText(e,en){
if(e[0]==='lent') return `<span class="eff-lent">${en?'You lent '+sek(e[1]):'Du lånade ut '+kr(e[1])}</span>`;
if(e[0]==='borrowed') return `<span class="eff-borrowed">${en?'You borrowed '+sek(e[1]):'Du lånade '+kr(e[1])}</span>`;
if(e[0]==='none') return 'Inte inblandad';
return 'Betalade för dig själv';
}
function ledgerRow(r){
const pend = r.p?`<span class="pend" role="img" aria-label="Väntar på synk">${I('cloud_upload',{s:16})}</span>`:'';
if(r.settle) return `<div class="row"><span class="ci" style="--c:var(--settled);--cc:var(--settled-container)">${I('handshake',{f:1,s:22})}</span><div class="rm"><div class="tm" style="overflow-wrap:anywhere">${r.settle} <span class="num nw">${kr(r.a)}</span></div><div class="rs bm">${pend}${r.d} · Betalning</div></div></div>`;
return `<div class="row">${catIc(r.c)}<div class="rm"><div class="rtop"><span class="tm rt">${r.t}</span><span class="ra am">${kr(r.a)}</span></div><div class="rs bm">${pend}<span class="nw">${r.d}</span><span aria-hidden="true">·</span>${effText(r.e)}</div></div></div>`;
}
function monthHead(m,total){return `<div class="mh" aria-label="${m}${total?' · '+kr(total):''}"><span class="ts">${m}</span><span class="as num">${total?kr(total):''}</span></div>`;}
/* =====================================================================
Balance hero (S-4 header): owing / owed / settled
===================================================================== */
function heroAmount(ore,en){
if(en){const s=sek(ore).replace('SEK'+NB,'');const i=s.lastIndexOf('.');return `<span class="dec">SEK${NB}</span><span>${s.slice(0,i)}</span><span class="dec">${s.slice(i)}</span>`;}
const s=krn(ore);const i=s.indexOf(',');return `<span>${s.slice(0,i)}</span><span class="dec">${s.slice(i)}${NB}kr</span>`;
}
function hero(state,ore,o={}){
const en=o.en;
let word,amt,cls,act='';
if(state==='owing'){word=en?'You owe Anna':'Du är skyldig Anna';amt=`<div class="amt ah c-owing" aria-label="${en?sek(ore):kr(ore)}">${heroAmount(ore,en)}</div>`;cls='owing';
act=`<div class="acts">${btn(en?'Settle up':'Gör upp','filled m',null)}</div>`;}
else if(state==='owed'){word=en?'Anna owes you':'Anna är skyldig dig';amt=`<div class="amt ah c-owed" aria-label="${en?sek(ore):kr(ore)}">${heroAmount(ore,en)}</div>`;cls='owed';
act=`<div class="acts">${btn(en?'Settle up':'Gör upp','filled m',null)}</div>`;}
else {word=en?'Jacob and Anna':'Jacob och Anna';amt=`<div class="amt ds" style="font-weight:700;font-stretch:88%;letter-spacing:-.6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap"><span style="${shp('cookie9')}background:var(--settled);color:var(--surface);width:44px;height:44px;display:inline-grid;place-items:center;flex:none">${I('check',{s:26})}</span>${en?'All settled up':'Ni är kvitt'}</div>`;cls='settled';}
if(cls==='settled') return `<section class="hero settled" aria-label="${en?'Balance':'Saldo'}">${islands()}
<div class="top">${pairAv(44,'cookie9','--surface-container')}<div class="tm" style="color:var(--on-surface-variant)">${word}</div></div>${amt.replace('class="amt ds"','class="amt ds" ').replace('display:flex;','display:flex;margin-top:14px;')}</section>`;
return `<section class="hero ${cls}" aria-label="${en?'Balance':'Saldo'}">${islands()}
<div class="top">${pairAv(44,'cookie9','--surface-container')}</div>
<div class="word tl" style="color:var(--on-surface)">${word}</div>${amt}${act}</section>`;
}
/* =====================================================================
S-4 Home
===================================================================== */
const EN={konton:'Accounts',ny:'New account',arch:n=>`Archived (${n})`,cnt:n=>`${n} expense${n===1?'':'s'} in October`,sync:'Synced 3 min ago',fab:'Add expense',
owes:o=>['Anna owes you',sek(o)],owe:o=>['You owe Anna',sek(o)],settled:'Settled up'};
const SV={konton:'Konton',ny:'Nytt konto',arch:n=>`Arkiverade (${n})`,cnt:n=>`${n} ${n===1?'utgift':'utgifter'} i oktober`,sync:'Synkad för 3 min sedan',fab:'Lägg till utgift',
owes:o=>['Anna är skyldig dig',kr(o)],owe:o=>['Du är skyldig Anna',kr(o)],settled:'Kvitt'};
function accItem(a,L,settledAll){
const net=settledAll?0:a.net;
let right;
if(net>0){const [w,v]=L.owes(net);right=`<div class="ab"><div class="lm c-owed">${w}</div><div class="am c-owed">${v}</div></div>`;}
else if(net<0){const [w,v]=L.owe(-net);right=`<div class="ab"><div class="lm c-owing">${w}</div><div class="am c-owing">${v}</div></div>`;}
else right=`<div class="ab"><div class="lm c-settled" style="display:flex;align-items:center;gap:4px;justify-content:flex-end">${I('check',{s:16})}${L.settled}</div></div>`;
return `<button class="gi" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;padding:14px 16px 14px 14px;min-height:72px;width:100%">
<span style="width:44px;height:44px;border-radius:14px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center;flex:none" class="tm" aria-hidden="true">${a.name[0]}</span>
<span style="flex:1 1 120px;min-width:0"><span class="tm" style="display:block;overflow-wrap:anywhere">${a.name}${a.arch?` <span class="badge" style="vertical-align:2px">Arkiverat</span>`:''}</span><span class="bs v" style="display:block">${a.arch&&!a.oct?'Inga utgifter i oktober':L.cnt(a.oct)}</span></span>
<span style="flex:0 1 auto;margin-left:auto;text-align:right">${right}</span></button>`;
}
function home(o={}){
const en=!!o.en; const L=en?EN:SV;
const settled=o.state==='settled';
const active=ACCOUNTS.filter(a=>!a.arch), archived=ACCOUNTS.filter(a=>a.arch);
let banner='';
if(o.banner==='update') banner=`<div class="banner" style="margin:0 16px 12px">${`<div class="bh">${I('system_update',{s:24})}<div style="flex:1"><div class="ts">Version 1.2 finns</div><div class="bm">Ny version av Kvitt · 18,4 MB</div></div></div><div class="ba">${btn('Senare','text')}${btn('Uppdatera','filled')}</div>`}</div>`;
if(o.banner==='progress') banner=`<div class="banner" style="margin:0 16px 12px"><div class="bh">${I('downloading',{s:24})}<div style="flex:1;min-width:0"><div class="ts">Version 1.2 finns</div><div class="bm">Laddar ner… 64${NB}%</div>
<svg viewBox="0 0 300 12" style="width:100%;height:12px;margin-top:10px;display:block" role="progressbar" aria-label="Laddar ner, 64 procent" aria-valuenow="64"><path d="${wave(2,186,6,5,6)}" style="fill:none;stroke:var(--tertiary);stroke-width:4;stroke-linecap:round"/><path d="M196 6H296" style="stroke:var(--surface-container-lowest);stroke-width:4;stroke-linecap:round;opacity:.7"/><circle cx="296" cy="6" r="2" style="fill:var(--tertiary)"/></svg></div></div></div>`;
if(o.banner==='failed') banner=`<div class="banner err" style="margin:0 16px 12px"><div class="bh">${I('error',{s:24,f:1})}<div style="flex:1"><div class="ts">Version 1.2 finns</div><div class="bm">Nedladdningen misslyckades. Försök igen.</div></div></div><div class="ba">${btn('Senare','text')}${btn('Försök igen','filled')}</div></div>`;
const sync = o.offline
? `<button class="syncrow off" aria-label="Synkstatus: Offline, 2 ändringar väntar på synk">${I('cloud_off',{s:20,c:'l'})}<span class="t bm">Offline · 2 ändringar väntar på synk</span>${I('chevron_right',{s:20})}</button>`
: `<button class="syncrow">${I('cloud_done',{s:20})}<span class="t bm">${L.sync}</span>${I('chevron_right',{s:20})}</button>`;
let body;
if(o.state==='empty'){
body=`<div class="pad" style="padding-top:4px">
<section class="hero settled" style="padding-bottom:20px">${islands()}<div class="top">${pairAv(44,'cookie9','--surface-container')}<div><div class="tm">Jacob och Anna</div><div class="bm v">Ni är båda med</div></div></div></section>
<div style="padding:${o.fsBig?20:40}px 12px 0;text-align:center;display:flex;flex-direction:column;align-items:center">
<div style="position:relative;width:132px;height:112px;margin-bottom:20px" aria-hidden="true">
<span style="position:absolute;left:0;top:18px;width:84px;height:84px;${shp('clover4')}background:var(--primary-container)"></span>
<span style="position:absolute;right:0;top:0;width:68px;height:68px;${shp('cookie9')}background:var(--tertiary-container)"></span>
<span style="position:absolute;left:30px;top:44px;width:48px;height:48px;border-radius:16px;background:var(--primary);color:var(--on-primary);display:grid;place-items:center">${I('account_balance_wallet',{s:26,f:1})}</span>
</div>
<div class="hs">Skapa ert första konto</div>
<p class="bm v" style="margin:8px 0 24px;max-width:320px">Ett konto samlar det ni delar på, till exempel Vardag, Huset eller en resa. Ni väljer hur kostnaderna delas.</p>
${btn('Nytt konto','filled m','add')}
</div></div>`;
} else if(o.state==='loading'){
body=`<div class="pad" style="padding-top:4px">
<div class="hero" style="height:200px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:16px">
<span style="width:48px;height:48px;border-radius:50%;background:var(--primary-container);display:grid;place-items:center" role="progressbar" aria-label="Hämtar era utgifter">
<span style="width:34px;height:34px;${shp('cookie9')}background:var(--primary);transform:rotate(18deg)"></span></span>
<div class="tm">Hämtar era utgifter…</div>
<div class="bm v" style="text-align:center;max-width:280px">Första synken efter parning kan ta en liten stund.</div>
</div>
<div class="sec"><span class="ts">Konton</span></div>
<div class="grp">${[0,1,2].map(()=>`<div class="gi" style="display:flex;gap:14px;align-items:center;padding:14px;min-height:72px"><span class="sk" style="width:44px;height:44px;border-radius:14px"></span><span style="flex:1"><span class="sk" style="display:block;width:46%;height:14px;border-radius:7px"></span><span class="sk" style="display:block;width:64%;height:10px;border-radius:5px;margin-top:10px"></span></span><span class="sk" style="width:76px;height:14px;border-radius:7px"></span></div>`).join('')}</div>
</div>`;
} else {
const heroH = settled ? hero('settled',0,{en}) : hero('owing',-GLOBAL_NET,{en});
const archOpen = o.archOpen;
body=`<div class="pad" style="padding-top:4px">${heroH}
<div style="margin:8px 0 0">${sync}</div>
<div class="sec"><h2 class="ts" style="margin:0">${L.konton}</h2>${btn(L.ny,'text','add')}</div>
<div class="grp">${active.map(a=>accItem(a,L,settled)).join('')}</div>
<button class="sec" style="width:100%;margin-top:6px;min-height:56px" aria-expanded="${archOpen?'true':'false'}"><span class="ts" style="color:var(--on-surface-variant)">${L.arch(archived.length)}</span>${I(archOpen?'expand_less':'expand_more',{st:'color:var(--on-surface-variant)'})}</button>
${archOpen?`<div class="grp low">${archived.map(a=>accItem(a,L,true)).join('')}</div>`:''}
<div style="height:120px"></div></div>`;
}
const topbar=`<header class="tab root" style="gap:12px">${kvittMark(32)}<div class="tt"><div class="tl" style="font-weight:650">Kvitt</div></div></header>`;
const showFab = !(o.state==='empty'||o.state==='loading');
return {html: topbar+(banner?`<div style="flex:none">${banner}</div>`:'')+scroll(body)+(showFab?fab(L.fab):'')+navBar(0,en), gb:'surface-container', over:o.over||''};
}
function wave(x0,x1,y,amp,len){ let d=`M${x0} ${y}`; for(let x=x0;x+len*2<=x1;x+=len*2){ d+=` q${len/2} ${-amp} ${len} 0 t${len} 0`; } return d; }
/* =====================================================================
S-1 … S-3, S-18 onboarding
===================================================================== */
function pairing(err){
const body=`<div class="pad" style="padding-top:28px;display:flex;flex-direction:column;min-height:100%">
<div style="display:flex;align-items:center;gap:16px">${kvittMark(64)}<div><div class="dm" style="font-weight:700;font-stretch:90%;letter-spacing:-.8px">Kvitt</div></div></div>
<p class="bl v" style="margin:8px 0 28px">Delade utgifter för två</p>
<div style="display:flex;flex-direction:column;gap:${err?16:20}px">
${field({label:'Inbjudningskod',value:'<span class="num" style="letter-spacing:1.6px">7K3M-QX9D-2HPA</span>',err:err?'Koden fungerar inte. Den kan ha gått ut eller redan använts. Be Jacob om en ny.':'',foc:!!err,nocaret:1,trail:err?'':btn('Klistra in','text','content_paste')})}
${field({label:'Ditt namn',value:'Anna'})}
<p class="bm v" style="margin:0 4px;display:flex;gap:10px">${I('info',{s:20})}<span>Koden får du av Jacob: från en länk eller från Inställningar › Bjud in partner.</span></p>
<div class="grp low" style="margin-top:4px"><button class="gi li" style="width:100%;padding:12px 16px"><span class="lead" style="background:var(--surface-container-highest);color:var(--on-surface-variant)">${I('dns',{s:22})}</span><span class="m"><span class="bs v" style="display:block">Serveradress</span><span class="bm" style="display:block;overflow-wrap:anywhere">https://icehack.asuscomm.com:8443</span></span>${I('edit',{s:20,c:'ch',l:'Ändra serveradress'})}</button></div>
</div>
<div style="flex:1;min-height:24px;position:relative" aria-hidden="true"><svg viewBox="0 50 210 70" style="position:absolute;left:-16px;right:-16px;bottom:8px;width:calc(100% + 32px);height:auto">
<path d="M0 98c18-22 34-30 50-30 18 0 26 14 40 14s24-12 40-12 30 16 44 16 22-6 36-6v40H0z" style="fill:var(--surface-container-high)"/>
<path d="M118 84c8-14 18-20 28-20 12 0 16 10 26 10 8 0 14-4 20-4 8 0 12 4 18 4v46h-92z" style="fill:var(--surface-container-highest)"/>
<path d="M0 112c10-5 20-5 30 0s20 5 30 0 20-5 30 0 20 5 30 0 20-5 30 0 20 5 30 0 20-5 30 0" style="fill:none;stroke:var(--outline-variant);stroke-width:1.6;stroke-linecap:round"/></svg></div>
${btn('Para ihop','filled m full')}
<div style="height:16px"></div>
</div>`;
return {html:scroll(body,'display:flex;flex-direction:column')};
}
function profileStep(){
const body=`<div class="pad" style="padding-top:20px;display:flex;flex-direction:column;height:100%">
<div class="card" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:20px">${av('J',48,'cookie9')}<div><div class="bs v">Hushåll</div><div class="tm">Du går med i Jacobs hushåll</div></div></div>
<div class="hm" style="margin:32px 0 6px">Så här syns du</div>
<p class="bm v" style="margin:0 0 28px">Jacob ser ditt namn och din bild bredvid det du lägger till.</p>
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;margin-bottom:28px">
${av('A',120,'cookie9')}
${btn('Lägg till bild','tonal','add_a_photo')}
</div>
${field({label:'Ditt namn',value:'Anna',foc:1,sup:'Visas för Jacob. Du kan ändra det senare.'})}
<div style="flex:1"></div>
${btn('Fortsätt','filled m full')}<div style="height:16px"></div></div>`;
return {html:scroll(body)};
}
function waiting(){
const body=`<div class="pad" style="padding-top:4px">
<section class="hero" style="padding:28px 24px 28px">${islands()}
<div class="top" style="gap:0"><span class="pair" style="--ringc:var(--surface-container)">${av('J',56,'cookie9')}${av('J?',56)}</span></div>
<div class="hm" style="margin-top:20px;position:relative">Väntar på din partner</div>
<p class="bm v" style="margin:8px 0 0;position:relative">Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk.</p>
<div style="margin-top:20px;position:relative">${btn('Bjud in partner','filled m','person_add')}</div>
</section>
<div class="grp" style="margin-top:16px"><div class="gi li">${I('schedule',{s:22,st:'color:var(--on-surface-variant)'})}<div class="m"><div class="ts">Inbjudan skickad</div><div class="bm v">Inbjudan gäller till mån 19 okt. 21:40</div></div></div></div>
<p class="bs v" style="margin:16px 8px">Konton, utgifter och betalningar blir tillgängliga när din partner har gått med.</p>
</div>`;
return {html:`<header class="tab root" style="gap:12px">${kvittMark(32)}<div class="tt"><div class="tl" style="font-weight:650">Kvitt</div></div></header>`+scroll(body)+navBar(0),gb:'surface-container'};
}
function removed(){
const body=`<div class="pad" style="height:100%;display:flex;flex-direction:column;padding-top:96px">
<div style="display:flex;justify-content:center;margin-bottom:32px"><span style="width:120px;height:120px;${shp('cookie9')}background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center">${I('phonelink_erase',{s:52})}</span></div>
<div class="hm" style="text-align:center">Den här telefonen har tagits bort</div>
<p class="bl v" style="text-align:center;margin:12px 8px 0">Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats.</p>
<div style="flex:1"></div>${btn('Para ihop igen','filled m full')}<div style="height:16px"></div></div>`;
return {html:scroll(body)};
}
/* =====================================================================
S-5 Account detail
===================================================================== */
function accountHeader(kind){
if(kind==='gotland') return `<div class="card hi" style="border-radius:28px;margin:4px 16px 0;padding:20px">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span class="badge">${I('inventory_2',{s:14})}Arkiverat</span><span class="bm v">50 / 50 · Jacob och Anna</span></div>
<div style="display:flex;align-items:center;gap:12px;margin-top:14px">${pairAv(36,null,'--surface-container')}<div class="hs c-settled" style="display:flex;align-items:center;gap:6px">${I('check',{s:24})}Kvitt</div></div>
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">${btn('Återställ från arkiv','tonal','unarchive')}</div>
<div class="bs v" style="display:flex;gap:6px;margin-top:12px">${I('info',{s:16})}<span>Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</span></div></div>`;
return `<div class="card hi" style="border-radius:28px;margin:4px 16px 0;padding:20px">
<div class="bm v">50 / 50 · Jacob och Anna</div>
<div style="display:flex;align-items:flex-start;gap:12px;margin-top:14px">${pairAv(36,null,'--surface-container')}<div style="min-width:0"><div class="ts">Anna är skyldig dig</div><div class="al c-owed">${kr(118450)}</div></div></div>
<div style="margin-top:16px">${btn('Gör upp det här kontot','tonal','handshake')}</div></div>`;
}
function account(o={}){
const g=o.kind==='gotland';
const months=g?GOTLAND:VARDAG;
const list=months.map(m=>monthHead(m.m,m.total)+m.rows.map(ledgerRow).join('')).join('');
const acts=IB('edit','Ändra konto')+IB('more_vert','Fler åtgärder','on');
let over='';
if(o.menu) over=`<div class="menu" style="right:12px;top:84px;width:282px" role="menu">
<div class="mi">${I('edit',{s:22})}<span class="bl">Ändra konto</span></div>
<div class="mi">${I('inventory_2',{s:22})}<span class="bl">Arkivera</span></div>
<div class="mi dis" aria-disabled="true">${I('delete',{s:22})}<span><span class="bl" style="display:block">Ta bort konto</span><span class="bs sup" style="display:block">Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</span></span></div></div>`;
if(false) over=`<div class="menu" style="right:12px;top:84px;width:282px" role="menu">
<div class="mi">${I('edit',{s:22})}<span class="bl">Ändra konto</span></div>
<div class="mi">${I('unarchive',{s:22})}<span class="bl">Återställ från arkiv</span></div>
<div class="mi dis" aria-disabled="true">${I('delete',{s:22})}<span><span class="bl" style="display:block">Ta bort konto</span><span class="bs sup" style="display:block">Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</span></span></div></div>`;
return {html:appBar({nav:'arrow_back',title:g?'Gotland 2026':'Vardag',acts})+scroll(accountHeader(g?'gotland':'')+list+'<div style="height:120px"></div>')+(g?'':fab('Lägg till utgift',{nonav:1,s:1})), over};
}
/* =====================================================================
S-6 Account editor
===================================================================== */
function accountEditor(off){
const d=off;
const body=`<div class="pad" style="padding-top:12px;display:flex;flex-direction:column;gap:20px">
${off?`<div class="banner warn" style="border-radius:20px"><div class="bh">${I('cloud_off',{s:22})}<div><div class="ts">Kräver anslutning</div><div class="bm">Konton sparas på servern. Anslut telefonen till internet för att spara.</div></div></div></div>`:''}
${field({label:'Namn',value:'Sommarstugan',foc:!off,dis:d})}
<div class="card" style="${d?'opacity:.5':''}">
<div class="ts">Standardfördelning</div>
<p class="bs v" style="margin:2px 0 12px">Gäller nya utgifter i kontot. Fältet för Anna räknas ut så att summan blir 100.</p>
<div style="display:flex;justify-content:space-between" class="lm v"><span>Jacob</span><span>Anna</span></div>
<div class="slider" role="slider" aria-label="Jacobs andel" aria-valuenow="60"><div class="tr"><div class="a" style="width:58%"></div><div class="h"></div><div class="b2"></div></div></div>
<div style="display:flex;gap:12px;margin-top:12px;--fbg:var(--surface-container-low)">
${field({label:'Jacob',value:'60,00',r:1,num:1,trail:'<span class="bl v">%</span>',st:'flex:1',dis:d})}
${field({label:'Anna',value:'40,00',r:1,num:1,trail:'<span class="bl v">%</span>',st:'flex:1',ro:1,dis:d})}
</div>
</div>
<div class="card hi" style="border-radius:20px;padding:16px;display:flex;gap:12px;align-items:flex-start;${d?'opacity:.5':''}">${I('calculate',{s:22,st:'color:var(--primary)'})}<div><div class="lm v">Förhandsvisning</div><div class="bm num">Av 100,00${NB}kr: Jacob 60,00${NB}kr, Anna 40,00${NB}kr</div></div></div>
</div>`;
return {html:appBar({nav:'close',navL:'Avbryt',title:'Nytt konto',acts:btn('Spara','filled',null,{dis:off})})+scroll(body)};
}
/* =====================================================================
S-7 Expense editor
===================================================================== */
function keypad(o={}){
const k=(t,cls='',al)=>`<button class="key ${cls}"${al?` aria-label="${al}"`:''}>${t}</button>`;
return `<div class="kp${o.sm?' sm':''}" role="group" aria-label="Sifferknappar">${['1','2','3','4','5','6','7','8','9'].map(n=>k(n,o.pressed===n?'pressed':'')).join('')}${k(',','sec2','Decimaltecken')}${k('0')}${k(I('backspace',{s:26}),'del','Radera')}</div>`;
}
function amountBlock(ore,typed,o={}){
const val = typed!=null ? typed : krn(ore);
return `<div style="padding:${o.compact?'4px 20px 12px':'12px 20px 16px'}">
<div class="lm v">Belopp</div>
<div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap"><span class="${o.compact?'al':'ah'}" style="color:var(--on-surface)">${val}${o.caret?'<span class="caret" style="width:3px;margin-left:3px"></span>':''}</span><span class="${o.compact?'hs':'hm'} v" style="font-weight:500">kr</span></div></div>`;
}
function defaultsChips(o){
return `<div class="chips" role="group" aria-label="Standardval">
${chip({icon:'account_balance_wallet',v:o.acc||'Vardag',al:'Konto: '+(o.acc||'Vardag')})}
${chip({lead:catIc(o.cat||'entertainment','s').replace('class="ci s"','class="ci s" style="width:24px;height:24px;border-radius:8px;--c:var(--cat-'+CAT[o.cat||'entertainment'].c+');--cc:var(--cat-'+CAT[o.cat||'entertainment'].c+'-container)"'),v:CAT[o.cat||'entertainment'].sv,al:'Kategori: '+CAT[o.cat||'entertainment'].sv})}
${chip({icon:'event',v:'Idag',al:'Datum: Idag'})}
${chip({lead:av('J',24),k:'Betalt av',v:'Du'})}
${chip({icon:'call_split',k:'Delning',v:o.split||'Kontots standard 50/50',sel:o.splitOpen,dd:1,ddi:o.splitOpen?'expand_less':'expand_more'})}
${o.noNotes?'':chip({icon:'notes',v:'Lägg till anteckning'})}
</div>`;
}
function editor(o={}){
const fsBig=o.big;
const top=appBar({nav:'close',navL:'Avbryt',title:'Ny utgift',acts:btn('Spara','filled',null,{dis:o.dis})});
let mid,bottom='';
if(o.mode==='split'){
mid=`${amountBlock(106000,null,{compact:1})}
<div class="pad" style="display:flex;flex-direction:column;gap:12px">
${field({label:'Beskrivning',value:'Biljetter Dramaten'})}
${defaultsChips({cat:'entertainment',split:'Exakta belopp',splitOpen:1,noNotes:1})}
<section class="card" style="padding:16px;border-radius:28px;--fbg:var(--surface-container-low)" aria-label="Delning">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px" role="radiogroup" aria-label="Delningsläge">
${[['Kontots standard 50/50',0],['Egen fördelning',0],['Exakta belopp',1],['Allt på en person',0]].map(([t,on],i)=>`<button role="radio" aria-checked="${on?'true':'false'}" class="bm" style="min-height:44px;padding:6px 12px;display:flex;align-items:center;gap:6px;border-radius:${on?'22px':(['20px 8px 8px 8px','8px 20px 8px 8px','8px 8px 8px 20px','8px 8px 20px 8px'][i])};background:var(${on?'--secondary':'--surface-container-high'});color:var(${on?'--on-secondary':'--on-surface'});font-weight:${on?600:450}">${on?I('check',{s:18}):''}${t}</button>`).join('')}
</div>
<div style="display:flex;gap:10px;margin-top:16px;align-items:flex-start">
${field({label:'Du',value:'1 200,00',num:1,r:1,foc:1,err:'Andelen är större än beloppet',trail:'<span class="bl v">kr</span>',st:'flex:1.15'})}
${field({label:'Anna',value:'—',r:1,ro:1,trail:'<span class="bl v">kr</span>',st:'flex:.85',sup:'Räknas ut'})}
</div>
<div class="bm v" style="margin-top:10px;display:flex;gap:8px;align-items:center">${I('calculate',{s:18})}<span>Du 1${NB}200,00${NB}kr · Anna —</span></div>
</section></div>`;
bottom=keypad({sm:1});
} else if(o.mode==='full'){
mid=`${amountBlock(106000,null,{compact:1})}
<div class="pad" style="display:flex;flex-direction:column;gap:12px">
${field({label:'Beskrivning',value:'SL-kort 30 dagar, Anna'})}
${defaultsChips({cat:'transport',split:'Allt på Anna',splitOpen:1,noNotes:1})}
<section class="card" style="padding:16px;border-radius:28px" aria-label="Delning">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px" role="radiogroup" aria-label="Delningsläge">
${[['Kontots standard 50/50',0],['Egen fördelning',0],['Exakta belopp',0],['Allt på en person',1]].map(([t,on],i)=>`<button role="radio" aria-checked="${on?'true':'false'}" class="bm" style="min-height:44px;padding:6px 12px;display:flex;align-items:center;gap:6px;border-radius:${on?'22px':(['20px 8px 8px 8px','8px 20px 8px 8px','8px 8px 8px 20px','8px 8px 20px 8px'][i])};background:var(${on?'--secondary':'--surface-container-high'});color:var(${on?'--on-secondary':'--on-surface'});font-weight:${on?600:450}">${on?I('check',{s:18}):''}${t}</button>`).join('')}
</div>
<div class="lm v" style="margin:16px 0 8px">Vem står för hela kostnaden?</div>
<div class="bg full" role="radiogroup"><button class="b" role="radio" aria-checked="false">${av('J',24)} Du</button><button class="b on" role="radio" aria-checked="true">${I('check',{s:18})}${av('A',24)} Anna</button></div>
<div class="bm" style="margin-top:14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">${I('calculate',{s:18,st:'color:var(--primary)'})}<span class="num">Du 0,00${NB}kr · Anna 1${NB}060,00${NB}kr</span></div>
</section>
${field({label:'Anteckningar',value:'Annas månadskort för oktober–november.',foc:1})}
<div style="height:24px"></div></div>`;
} else {
mid=`${amountBlock(41200,'412',{caret:1})}
<div class="pad" style="display:flex;flex-direction:column;gap:14px">
${field({label:'Beskrivning',value:'Systembolaget'})}
${defaultsChips({cat:'entertainment'})}
<div class="bm v" style="display:flex;gap:8px;align-items:center;padding:0 4px">${I('calculate',{s:18})}<span class="num">Du 206,00${NB}kr · Anna 206,00${NB}kr</span></div>
</div>`;
bottom=keypad({pressed:'2'});
}
return {html:top+scroll(mid)+bottom, gb: bottom?'surface-container-low':'surface'};
}
/* =====================================================================
S-8 Expense detail, S-10 Settlement detail
===================================================================== */
function timeline(lines){
return `<ol class="tl-list" style="list-style:none;margin:0;padding:4px 0 0 4px">${lines.map((l,i)=>`<li class="tl-it${l.p?' pending':''}${i===0?' first':''}"><span class="tl-dot"></span><div><div class="bm"${l.p?' style="color:var(--pending);font-weight:560"':''}>${l.p?I('cloud_upload',{s:16,st:'vertical-align:-3px;margin-right:4px'}):''}${l.t}</div>${l.at?`<div class="bs v num">${l.at}</div>`:''}</div></li>`).join('')}</ol>`;
}
function kvRow(icon,k,v,extra=''){return `<div class="gi kv">${I(icon,{s:22,st:'color:var(--on-surface-variant)'})}<div style="flex:1;min-width:0"><div class="bs v">${k}</div><div class="bl" style="overflow-wrap:anywhere">${v}</div></div>${extra}</div>`;}
function expenseDetail(kind){
const dl=kind==='deleted', rec=kind==='recurring';
const d = rec ? {t:'Hyra september',a:1240000,acc:'Huset',c:'home',date:'fredag 25 september 2026',payer:'A',mode:'Kontots standard 60/40',sh:`Du ${kr(744000)} · Anna ${kr(496000)}`,note:'',
tl:[{t:'Skapad automatiskt (återkommande)',at:'25 sep. 06:00'}]}
: dl ? {t:'Pressbyrån',a:6800,acc:'Vardag',c:'restaurants',date:'fredag 2 oktober 2026',payer:'A',mode:'Allt på en person',sh:`Du ${kr(0)} · Anna ${kr(6800)}`,note:'',
tl:[{t:'Anna skapade den här',at:'2 okt. 08:14'},{t:'Anna tog bort den här',at:'17 okt. 09:12'}]}
: {t:'ICA Maxi Lindhagen',a:124760,acc:'Vardag',c:'groceries',date:'fredag 16 oktober 2026',payer:'A',mode:'Kontots standard 50/50',sh:`Du ${kr(62380)} · Anna ${kr(62380)}`,note:'Storhandling inför middagen med Lindqvists',
tl:[{t:'Anna skapade den här',at:'16 okt. 14:02'},{t:`Du ändrade beloppet från ${kr(114760)} till ${kr(124760)}`,at:'16 okt. 19:40'},{t:'Anna ändrade kategorin från Övrigt till Mat & hushåll',at:'16 okt. 20:05'},{t:'Din ändring väntar på synk',p:1}]};
const acts = dl?'':IB('edit','Ändra')+IB('delete','Ta bort');
const muted = dl?'opacity:.62;':'';
const body=`
${dl?`<div class="banner err" style="margin:4px 16px 16px;border-radius:20px"><div class="bh">${I('delete',{s:22})}<div class="ts">Anna tog bort den här utgiften · 17 okt. 09:12</div></div></div>`:''}
<div class="pad" style="${muted}">
<div style="display:flex;align-items:center;gap:16px;padding:4px 4px 0">${`<span style="width:64px;height:64px;${shp('cookie9')}background:var(--cat-${CAT[d.c].c}-container);color:var(--cat-${CAT[d.c].c});display:grid;place-items:center;flex:none">${I(CAT[d.c].icon,{f:1,s:30})}</span>`}
<div style="min-width:0"><div class="hs" style="overflow-wrap:anywhere;${dl?'text-decoration:line-through;text-decoration-thickness:1px':''}">${d.t}</div>${rec?`<span class="badge tert" style="margin-top:4px">${I('event_repeat',{s:14})}Återkommande: Hyra</span>`:''}</div></div>
<div class="al" style="margin:16px 4px 16px">${kr(d.a)}</div>
<div class="grp">
${kvRow('account_balance_wallet','Konto',d.acc)}
${kvRow(CAT[d.c].icon,'Kategori',CAT[d.c].sv)}
${kvRow('event','Datum',d.date)}
${kvRow('person','Betalt av','Anna',av('A',32))}
${kvRow('call_split','Delning · '+d.mode,`<span class="num">${d.sh}</span>`)}
${d.note?kvRow('notes','Anteckning',d.note):''}
</div>
<h2 class="ts" style="margin:24px 4px 12px">Historik</h2>
${timeline(d.tl)}
<div style="height:24px"></div>
</div>`;
return {html:appBar({nav:'arrow_back',title:'',acts})+scroll(body)};
}
function settlementDetail(){
const body=`<div class="pad">
<section class="hero owed" style="padding:24px">${islands()}
<div class="top">${av('A',48,'cookie9')}${I('arrow_forward',{s:22,st:'color:var(--on-surface-variant)'})}${av('J',48,'cookie9')}</div>
<div class="word tl">Anna betalade dig</div>
<div class="amt ah c-owed" style="font-size:calc(var(--fsx)*52px);line-height:calc(var(--fsx)*58px)">${heroAmount(80000)}</div>
</section>
<div class="grp" style="margin-top:16px">
${kvRow('event','Datum','tisdag 6 oktober 2026')}
${kvRow('notes','Anteckning','Swish')}
</div>
<h2 class="ts" style="margin:24px 4px 8px">Fördelning</h2>
<div class="grp"><div class="gi kv"><span style="width:32px;height:32px;border-radius:10px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center" class="ts" aria-hidden="true">V</span><div class="bl" style="flex:1">Vardag</div><div class="am">${kr(80000)}</div></div></div>
<h2 class="ts" style="margin:24px 4px 12px">Historik</h2>
${timeline([{t:'Anna skapade den här',at:'6 okt. 21:15'}])}
</div>`;
return {html:appBar({nav:'arrow_back',title:'Betalning',acts:IB('edit','Ändra')+IB('delete','Ta bort')})+scroll(body)};
}
/* =====================================================================
S-9 Settle up
===================================================================== */
function settle(o={}){
const rows = o.account ? [['Huset',308450,308450]]
: o.v==='adjusted' ? [['Huset',308450,175000],['Japan 2027',135000,150000,1]]
: o.v==='remaining' ? [['Huset',308450,200000],['Japan 2027',135000,100000]]
: [['Huset',308450,308450],['Japan 2027',135000,16550]];
const total=325000, alloc=rows.reduce((s,r)=>s+r[2],0), rem=(o.account?308450:total)-alloc;
const amt=o.account?308450:total;
const rowH=rows.map(([n,debt,a,warn])=>`
<div class="gi" style="padding:12px 8px 12px 16px">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<div style="flex:1 1 120px;min-width:0"><div class="tm">${n}</div><div class="bs v num">Skuld ${kr(debt)}</div></div>
${field({value:krn(a),num:1,r:1,trail:'<span class="bm v">kr</span>',st:'width:148px;--fbg:var(--surface-container)',err:'',foc:o.v==='adjusted'&&warn})}
${o.account?'<span style="width:4px"></span>':IB('close','Ta bort '+n+' från fördelningen','',{s:20})}
</div>
${warn?`<div class="bs" style="display:flex;gap:6px;align-items:flex-start;color:var(--warning);margin-top:8px;font-weight:560">${I('warning',{s:16,f:1})}<span>Det här reglerar mer än som är skyldigt i ${n}</span></div>`:''}
</div>`).join('');
const remRow = `<div style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:16px;background:var(${rem===0?'--surface-container-low':'--error-container'});color:var(${rem===0?'--on-surface-variant':'--on-error-container'})">${I(rem===0?'check_circle':'error',{s:20,f:1,st:rem===0?'color:var(--owed)':''})}<span class="bm" style="flex:1">Kvar att fördela</span><span class="am">${kr(Math.abs(rem))}</span></div>`;
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:14px;padding-top:4px">
<div class="card hi" style="padding:16px 12px 16px 16px;border-radius:28px;display:flex;align-items:center;gap:12px">
<span class="pair" style="--ringc:var(--surface-container)">${av('J',40,'cookie9')}${av('A',40,'cookie9')}</span>
<div style="flex:1;min-width:0"><div class="lm v">Riktning</div><div class="tl">Du betalar Anna</div></div>
<button class="ib ton" aria-label="Byt riktning">${I('swap_vert')}</button>
</div>
${field({label:'Belopp',value:`<span class="al" style="color:var(--on-surface)">${krn(amt)}</span>`,trail:'<span class="tl v">kr</span>'})}
<div><div class="sec" style="padding-top:0"><h2 class="ts" style="margin:0">Fördelning</h2></div>
<div class="grp">${rowH}</div></div>
${remRow}
${o.account?'':btn('Lägg till konto','text','add')}
${o.account?'':`<p class="bs v" style="margin:-6px 4px 0;display:flex;gap:8px">${I('info',{s:16})}<span>Vardag ändras inte. Där är det Anna som är skyldig dig.</span></p>`}
<div style="display:flex;gap:10px">${field({label:'Datum',value:'Idag',lead:'event',st:'flex:1'})}${field({label:'Anteckning',value:'Swish',st:'flex:1.3'})}</div>
<div style="height:8px"></div>
</div>`;
const ok=rem===0;
const footer=`<div style="flex:none;padding:12px 16px 12px;background:var(--surface)">${btn('Bekräfta','filled m full',null,{dis:!ok})}</div>`;
let over='';
if(o.dialog){ over=`<div class="scrim"></div><div class="dlg" role="dialog" aria-label="Bekräfta betalningen">
<div class="di">${I('handshake',{s:28})}</div>
<div class="hs dt" style="text-align:center">Bekräfta betalningen?</div>
<div class="bm db">Du betalar Anna <span class="num nw">${kr(325000)}</span>, idag.</div>
<div class="bm db num" style="margin-top:8px">Huset ${kr(175000)}<br>Japan 2027 ${kr(150000)}</div>
<div class="bs" style="display:flex;gap:8px;margin-top:16px;padding:12px;border-radius:16px;background:var(--warning-container);color:var(--on-warning-container);font-weight:560">${I('warning',{s:18,f:1})}<span>Det här reglerar mer än som är skyldigt i Japan 2027</span></div>
<div class="da">${btn('Avbryt','text')}${btn('Bekräfta','filled')}</div></div>`; }
return {html:appBar({nav:'close',navL:'Avbryt',title:o.account?'Gör upp Huset':'Gör upp'})+scroll(body)+footer,over};
}
/* =====================================================================
S-11 Analytics, S-12 Drill-down
===================================================================== */
const MONTHS=['maj','jun','jul','aug','sep','okt'];
const AN={ // kronor*100 (öre)
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 ANACC=[['Huset',7986000,'chart-cat-2'],['Vardag',6695140,'chart-cat-1'],['Japan 2027',2702000,'chart-cat-3'],['Gotland 2026',438000,'chart-cat-4']];
const WEEKS=['35','36','37','38','39','40','41','42'];
const WK={Vardag:[124000,151500,112000,186000,139000,118000,193000,145000],Huset:[98000,41000,163000,54000,225000,69000,128000,76000],'Japan 2027':[0,0,0,120000,0,140000,0,234000]};
const WKCAT=[['groceries',624000],['home',541000],['travel',494000],['utilities',313000],['restaurants',218000],['transport',159000],['entertainment',90500],['health',47000],['shopping',30000]];
const ACC_COL={'Vardag':'chart-cat-1','Huset':'chart-cat-2','Japan 2027':'chart-cat-3','Gotland 2026':'chart-cat-4'};
function niceMax(v,step){return Math.ceil(v/step)*step;}
function stackedChart(labels,series,o={}){ // series: [{name,col,vals}]
const W=347,H=o.h||176,L=44,B=22,T=6;
const totals=labels.map((_,i)=>series.reduce((s,x)=>s+x.vals[i],0));
const step=o.step, max=niceMax(Math.max(...totals),step);
const pw=W-L-4, bw=Math.min(34,pw/labels.length*0.56), gap=pw/labels.length;
const y=v=>T+(H-T-B)*(1-v/max);
let s='';
for(let t=0;t<=max;t+=step){ s+=`<line x1="${L}" x2="${W}" y1="${y(t)}" y2="${y(t)}" style="stroke:var(--chart-grid);stroke-width:1"/><text x="${L-8}" y="${y(t)+4}" text-anchor="end" style="fill:var(--chart-axis-label);font-size:calc(var(--fs)*11px);font-family:inherit" class="num">${t===0?'0':(t/100000)+' tkr'}</text>`; }
labels.forEach((lab,i)=>{
const cx=L+gap*i+gap/2; let acc=0;
const segs=series.filter(x=>x.vals[i]>0);
segs.forEach((x,j)=>{const v=x.vals[i];const y0=y(acc),y1=y(acc+v);acc+=v;const h=Math.max(0,y0-y1-(j<segs.length-1?2:0));
s+=`<rect x="${cx-bw/2}" y="${y1}" width="${bw}" height="${Math.max(h,1)}" rx="${j===segs.length-1?Math.min(8,h/2):3}" style="fill:var(--${x.col})"/>`;});
s+=`<text x="${cx}" y="${H-5}" text-anchor="middle" style="fill:var(--chart-axis-label);font-size:calc(var(--fs)*11px);font-family:inherit">${lab}</text>`;
});
return `<svg viewBox="0 0 ${W} ${H}" width="100%" style="display:block;overflow:visible" role="img" aria-label="${esc(o.summary||'')}">${s}</svg>`;
}
function legend(items){return `<div style="display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px">${items.map(([n,col])=>`<span class="lsm v" style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:var(--${col})"></span>${n}</span>`).join('')}</div>`;}
function breakdown(rows,total){ // rows: [{label, ore, col, icon?}]
const max=Math.max(...rows.map(r=>r.ore));
return rows.map(r=>`<button class="gi" style="display:flex;gap:12px;align-items:center;padding:10px 16px 10px 12px;width:100%">${r.lead}<span style="flex:1;min-width:0"><span style="display:flex;justify-content:space-between;gap:8px;align-items:baseline;flex-wrap:wrap"><span class="bm" style="overflow-wrap:anywhere">${r.label}</span><span class="nw"><span class="as">${kr(r.ore)}</span> <span class="bs v num" style="display:inline-block;min-width:44px;text-align:right">${pct1(r.ore,total)}</span></span></span>
<span style="display:block;height:6px;border-radius:3px;background:var(--surface-container-highest);margin-top:6px"><span style="display:block;height:6px;border-radius:3px;width:${(r.ore/max*100).toFixed(1)}%;background:var(--${r.col})"></span></span></span></button>`).join('');
}
function accLead(n){return `<span style="width:32px;height:32px;border-radius:10px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center;flex:none" class="ts" aria-hidden="true">${n[0]}</span>`;}
function analytics(o={}){
const wk=o.week, empty=o.empty;
const filters=`<div class="pad" style="display:flex;flex-direction:column;gap:8px;padding-bottom:4px">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">${chip({icon:'date_range',v:empty?'1–31 mars 2025':(wk?'24 aug.–17 okt.':'Senaste 6 mån'),k:empty||wk?'Anpassad:':'',dd:1,sm:1,sel:1})}
<div class="bg" role="radiogroup" aria-label="Indelning"><button class="b${wk?'':' on'}">Månad</button><button class="b${wk?' on':''}">Vecka</button></div></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<div class="bg" role="radiogroup" aria-label="Mått"><button class="b${wk?'':' on'}">Hushållet</button><button class="b${wk?' on':''}">Min del</button></div>
<div class="bg" role="radiogroup" aria-label="Gruppera efter"><button class="b${wk?'':' on'}">Kategori</button><button class="b${wk?' on':''}">Konto</button></div></div></div>`;
let body;
if(empty){
body=`${filters}<div class="pad" style="padding-top:12px"><div class="card" style="text-align:center;padding:40px 24px">
<div style="display:flex;justify-content:center;margin-bottom:16px"><span style="width:88px;height:88px;${shp('clover4')}background:var(--surface-container-highest);color:var(--on-surface-variant);display:grid;place-items:center">${I('bar_chart',{s:40})}</span></div>
<div class="lm v">Totalt</div><div class="al">${kr(0)}</div>
<div class="tm" style="margin-top:16px">Inga utgifter i den här perioden</div>
<p class="bm v" style="margin:6px 0 20px">Välj en annan period för att se era utgifter.</p>${btn('Senaste 6 mån','tonal','date_range')}</div></div>`;
} else if(wk){
const series=Object.keys(WK).map(n=>({name:n,col:ACC_COL[n],vals:WK[n]}));
const tot=WEEKS.reduce((s,_,i)=>s+series.reduce((a,x)=>a+x.vals[i],0),0);
body=`${filters}<div class="pad" style="padding-top:12px;display:flex;flex-direction:column;gap:12px">
<div style="padding:4px 4px 0"><div class="lm v">Totalt · min del · 24 aug.–17 okt.</div><div class="al">${kr(tot)}</div></div>
<section class="card" style="padding:16px 16px 14px"><h2 class="ts" style="margin:0 0 10px">Utgifter över tid</h2>${stackedChart(WEEKS.map(w=>'v.'+NB+w),series,{step:150000,summary:'Utgifter över tid, min del per vecka: högst vecka 42, 4 550 kr; lägst vecka 36, 1 925 kr.'})}${legend(series.map(x=>[x.name,x.col]))}</section>
<section><h2 class="ts" style="margin:8px 4px 8px">Per kategori</h2><div class="grp">${breakdown(WKCAT.slice(0,4).map(([k,v])=>({label:CAT[k].sv,ore:v,col:'cat-'+CAT[k].c,lead:catIc(k,'s')})),tot)}</div></section>
<div style="height:20px"></div></div>`;
} else {
const keys=Object.keys(AN).filter(k=>AN[k].some(v=>v>0));
const series=keys.map(k=>({name:CAT[k].sv,col:'cat-'+CAT[k].c,vals:AN[k]}));
const tot=keys.reduce((s,k)=>s+AN[k].reduce((a,b)=>a+b,0),0);
const byCat=keys.map(k=>({k,ore:AN[k].reduce((a,b)=>a+b,0)})).sort((a,b)=>b.ore-a.ore);
body=`${filters}<div class="pad" style="padding-top:12px;display:flex;flex-direction:column;gap:12px">
<div style="padding:4px 4px 0"><div class="lm v">Totalt · maj–okt 2026</div><div class="al">${kr(tot)}</div></div>
<section class="card" style="padding:16px 16px 14px"><h2 class="ts" style="margin:0 0 10px">Utgifter över tid</h2>${stackedChart(MONTHS,series,{step:1500000,h:168,summary:'Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr.'})}${legend(series.map(x=>[x.name,x.col]))}</section>
<section><h2 class="ts" style="margin:8px 4px 8px">Per kategori</h2><div class="grp">${breakdown(byCat.map(r=>({label:CAT[r.k].sv,ore:r.ore,col:'cat-'+CAT[r.k].c,lead:catIc(r.k,'s')})),tot)}</div></section>
<section><h2 class="ts" style="margin:8px 4px 8px">Per konto</h2><div class="grp">${breakdown(ANACC.map(([n,v,col])=>({label:n+(n==='Gotland 2026'?' <span class="badge">Arkiverat</span>':''),ore:v,col,lead:accLead(n)})),tot)}</div></section>
<div style="height:24px"></div></div>`;
}
if(o.scrolled) body=`<div style="transform:translateY(-590px)">${body}</div>`;
return {html:appBar({title:'Statistik',root:1,sub:o.scrolled?'Senaste 6 mån · Månad · Hushållet · Kategori':''})+scroll(body)+navBar(1),gb:'surface-container'};
}
function drill(){
const rows=VARDAG[0].rows.filter(r=>r.c==='groceries');
const body=`<div class="pad" style="padding-top:4px"><div class="card hi" style="display:flex;align-items:center;gap:14px;border-radius:24px;padding:16px">${catIc('groceries','l')}<div><div class="lm v">Totalt · 4 utgifter</div><div class="al">${kr(391000)}</div></div></div></div>
<div style="padding-top:8px">${rows.map(ledgerRow).join('')}</div>`;
return {html:appBar({nav:'arrow_back',title:'Mat & hushåll, oktober 2026',wrap:1})+scroll(body)};
}
/* =====================================================================
Settings and sub-screens
===================================================================== */
function settingsRow(icon,label,sub,o={}){return `<button class="gi li" style="width:100%;min-height:${sub?72:56}px"><span class="lead" style="${o.leadSt||''}">${I(icon,{s:22})}</span><span class="m"><span class="bl" style="display:block">${label}</span>${sub?`<span class="bm v" style="display:block">${sub}</span>`:''}</span>${'trail' in o?o.trail:I('chevron_right',{s:22,c:'ch'})}</button>`;}
function settings(){
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:4px">
<div class="card hi" style="border-radius:28px;padding:16px">
<div style="display:flex;align-items:center;gap:14px">${av('J',56,'cookie9')}<div style="flex:1"><div class="tm">Jacob</div><div class="bm v">Du · Administratör</div></div></div>
<div style="height:1px;background:var(--outline-variant);margin:14px 0"></div>
<div style="display:flex;align-items:center;gap:14px">${av('A',56,'cookie9')}<div style="flex:1"><div class="tm">Anna</div><div class="bm v">Partner</div></div></div>
</div>
<div class="grp">
${settingsRow('person','Profil','Namn och bild')}
${settingsRow('category','Kategorier','10 kategorier')}
${settingsRow('event_repeat','Återkommande utgifter','4 mallar, 1 pausad')}
</div>
<div class="grp">
${settingsRow('devices','Enheter','2 enheter')}
${settingsRow('qr_code_2','Para ihop en enhet','')}
</div>
<div class="grp">
${settingsRow('sync','Synkstatus','Synkad för 3 min sedan')}
${settingsRow('info','Om Kvitt','Version 1.1')}
</div><div style="height:16px"></div></div>`;
return {html:appBar({title:'Inställningar',root:1})+scroll(body)+navBar(2),gb:'surface-container'};
}
function profile(){
const body=`<div class="pad" style="display:flex;flex-direction:column;align-items:center;gap:16px;padding-top:24px;height:100%">
${av('J',144,'cookie9')}
<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center">${btn('Byt bild','tonal','photo_library')}${btn('Ta bort bild','outl','delete')}</div>
<div style="width:100%;margin-top:16px">${field({label:'Namn',value:'Jacob',foc:1,sup:'Visas för Anna i hela appen.'})}</div>
<div style="flex:1"></div>${btn('Spara','filled m full')}<div style="height:16px"></div></div>`;
return {html:appBar({nav:'arrow_back',title:'Profil'})+scroll(body)};
}
function categories(){
const keys=['groceries','restaurants','home','utilities','transport','travel','entertainment','health','shopping','other'];
const row=k=>`<div class="gi li" style="min-height:56px;padding:8px 8px 8px 16px">${catIc(k)}<span class="m bl">${CAT[k].sv}</span><button class="ib" aria-label="Flytta ${CAT[k].sv}">${I('drag_indicator',{s:22})}</button></div>`;
const body=`<div class="pad" style="padding-top:4px"><div class="grp">${keys.map(row).join('')}</div>
<div class="sec" style="margin-top:8px"><span class="ts">Arkiverade (1)</span>${I('expand_less',{st:'color:var(--on-surface-variant)'})}</div>
<div class="grp low"><div class="gi li" style="padding:8px 16px">${catIc('pant')}<span class="m"><span class="bl" style="display:block">Pant</span></span><span class="badge">Arkiverad</span></div></div>
<div style="height:24px"></div></div>`;
return {html:appBar({nav:'arrow_back',title:'Kategorier',acts:IB('add','Ny kategori','on')})+scroll(body)};
}
const ICONS30=['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 body=`<div class="pad" style="display:flex;flex-direction:column;gap:14px;padding-top:12px">
<div style="display:flex;gap:14px;align-items:center">${`<span style="width:56px;height:56px;${shp('cookie9')}background:var(--cat-green-container);color:var(--cat-green);display:grid;place-items:center;flex:none">${I('shopping_cart',{f:1,s:28})}</span>`}
${field({label:'Namn',value:'Matvaror',foc:1,st:'flex:1'})}</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:-6px;flex-wrap:wrap"><span class="bs v" style="padding-left:70px">Standardnamn: Mat & hushåll</span>${btn('Återställ namn','text','restart_alt')}</div>
<div><div class="ts" style="margin:0 4px 8px">Ikon</div>
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:4px;background:var(--surface-container-low);padding:8px;border-radius:24px" role="radiogroup" aria-label="Ikon">${ICONS30.map(n=>`<button role="radio" aria-checked="${n==='shopping_cart'}" aria-label="${n}" style="height:48px;display:grid;place-items:center;border-radius:${n==='shopping_cart'?'16px':'50%'};${n==='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)'}">${I(n,{s:24,f:n==='shopping_cart'})}</button>`).join('')}</div></div>
<div><div class="ts" style="margin:0 4px 8px">Färg</div>
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px;padding:4px 8px" role="radiogroup" aria-label="Färg">${CAT_TOKENS.map(c=>`<button role="radio" aria-checked="${c==='green'}" aria-label="${c}" style="height:44px;display:grid;place-items:center"><span style="width:40px;height:40px;border-radius:50%;background:var(--cat-${c});display:grid;place-items:center;color:var(--surface)${c==='green'?';box-shadow:0 0 0 3px var(--surface),0 0 0 5px var(--on-surface)':''}">${c==='green'?I('check',{s:22}):''}</span></button>`).join('')}</div></div>
<div class="grp" style="margin-top:4px">
${settingsRow('inventory_2','Arkivera','Visas inte för nya utgifter',{trail:''})}
<div class="gi li" style="opacity:.6"><span class="lead" style="background:var(--surface-container-highest);color:var(--on-surface-variant)">${I('delete',{s:22})}</span><span class="m"><span class="bl" style="display:block">Ta bort</span><span class="bm v" style="display:block">Kan inte tas bort: standardkategori</span></span></div>
</div><div style="height:16px"></div></div>`;
return {html:appBar({nav:'close',navL:'Avbryt',title:'Ändra kategori',acts:btn('Spara','filled')})+scroll(body)};
}
function sync(clock){
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:4px">
${clock?`<div class="banner warn" style="border-radius:24px"><div class="bh">${I('schedule',{s:24})}<div><div class="ts">Telefonens klocka går fel. Rätta den och synka igen.</div><div class="bm" style="margin-top:4px">Dina ändringar är sparade på telefonen och skickas när klockan stämmer.</div></div></div></div>`:''}
<div class="card hi" style="border-radius:28px;display:flex;flex-direction:column;gap:14px">
<div style="display:flex;gap:14px;align-items:center"><span style="width:48px;height:48px;${shp('cookie9')}background:var(--primary-container);color:var(--on-primary-container);display:grid;place-items:center">${I(clock?'sync_problem':'cloud_done',{s:26})}</span><div><div class="tm">${clock?'Synken stoppades':'Synkad för 3 min sedan'}</div><div class="bm v">${clock?'Senast synkad 17 okt. 13:59':'17 okt. 13:59'}</div></div></div>
${btn('Synka nu','filled','sync')}
</div>
<section><h2 class="ts" style="margin:4px 4px 8px">Väntande ändringar (2)</h2>
<div class="grp">
<div class="gi row">${catIc('transport')}<div class="rm"><div class="rtop"><span class="tm rt">SL-kort 30 dagar, Anna</span><span class="ra am">${kr(106000)}</span></div><div class="rs bm"><span class="pend">${I('cloud_upload',{s:16})}</span>Ny · 14 okt.</div></div></div>
<div class="gi row">${catIc('groceries')}<div class="rm"><div class="rtop"><span class="tm rt">Willys Hornstull</span><span class="ra am">${kr(86435)}</span></div><div class="rs bm"><span class="pend">${I('cloud_upload',{s:16})}</span>Ändrad · 12 okt.</div></div></div>
</div></section>
${clock?'':`<section><h2 class="ts" style="margin:4px 4px 8px">Ändringar som inte synkades</h2>
<div class="card" style="border-radius:24px;padding:16px">
<div style="display:flex;gap:12px;align-items:flex-start">${catIc('restaurants')}<div style="flex:1;min-width:0"><div class="tm">Babette</div><div class="bm" style="color:var(--on-surface)">Din ändring ersattes av en nyare ändring från Anna</div><div class="bs v">17 okt. 08:31</div></div></div>
<p class="bs v" style="margin:10px 0 0 52px">Annas version gäller nu för er båda. Öppna för att se vad du hade ändrat.</p>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px;flex-wrap:wrap">${btn('Visa','text')}${btn('Ta bort från listan','tonal')}</div>
</div></section>`}
<div style="height:16px"></div></div>`;
return {html:appBar({nav:'arrow_back',title:'Synkstatus'})+scroll(body)};
}
function recurring(){
const items=[['Hyra',1240000,'home','Huset · Anna betalar','Varje månad den 25:e · Nästa 25 okt.'],['Bahnhof bredband',39900,'utilities','Huset · Du betalar','Varje månad den 28:e · Nästa 28 okt.'],['Netflix',17900,'entertainment','Vardag · Du betalar','Varje månad den 3:e · Nästa 3 nov.'],['Spotify Family',25900,'entertainment','Vardag · Anna betalar','Varje månad den 12:e',1]];
const body=`<div class="pad" style="padding-top:4px"><div class="grp">${items.map(([t,a,c,s1,s2,p])=>`<button class="gi" style="display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap;padding:14px 16px;width:100%;${p?'':''}"><span style="${p?'opacity:.55':''}">${catIc(c)}</span><span style="flex:1 1 150px;min-width:0"><span style="display:flex;flex-wrap:wrap;gap:0 12px;align-items:baseline"><span class="tm" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;flex:1 1 120px">${t}${p?`<span class="badge tert">${I('pause',{s:14})}Pausad</span>`:''}</span><span class="am" style="margin-left:auto">${kr(a)}</span></span><span class="bm v" style="display:block">${s1}</span><span class="bs v" style="display:block">${s2}</span></span></button>`).join('')}</div><div style="height:110px"></div></div>`;
return {html:appBar({nav:'arrow_back',title:'Återkommande utgifter'})+scroll(body)+fab('Ny mall',{s:1,nonav:1})};
}
function recurringEditor(){
const body=`${amountBlock(1240000,null,{compact:1})}<div class="pad" style="display:flex;flex-direction:column;gap:16px">
${field({label:'Beskrivning',value:'Hyra'})}
<div class="chips">${chip({icon:'account_balance_wallet',v:'Huset'})}${chip({lead:catIc('home','s').replace('class="ci s"','class="ci s" style="width:24px;height:24px;border-radius:8px;--c:var(--cat-brown);--cc:var(--cat-brown-container)"'),v:'Hem'})}${chip({lead:av('A',24),k:'Betalt av',v:'Anna'})}${chip({icon:'call_split',k:'Delning',v:'Kontots standard 60/40',dd:1,ddi:'expand_more'})}</div>
<div class="card" style="display:flex;flex-direction:column;gap:18px;--fbg:var(--surface-container-low);border-radius:28px">
<div class="ts">Upprepning</div>
${field({label:'Dag i månaden',value:'25',lead:'event_repeat',sup:'Kortare månader använder sista dagen.'})}
<div style="display:flex;gap:10px">${field({label:'Första månad',value:'september 2026',st:'flex:1'})}${field({label:'Sista månad (valfritt)',value:'Ingen',ph:1,st:'flex:1'})}</div>
<div style="display:flex;align-items:center;gap:16px;min-height:48px"><div style="flex:1"><div class="bl">Pausad</div><div class="bs v">Inga nya utgifter skapas medan mallen är pausad</div></div><span class="switch" role="switch" aria-checked="false" aria-label="Pausad"></span></div>
</div>
${btn('Ta bort mall','outl err','delete')}
<div style="height:16px"></div></div>`;
return {html:appBar({nav:'close',navL:'Avbryt',title:'Ändra återkommande',acts:btn('Spara','filled')})+scroll(body)};
}
function devices(dialog){
const dev=(name,who,seen,cur)=>`<div class="gi" style="padding:16px;display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap">
<span class="lead" style="width:44px;height:44px;border-radius:14px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center;flex:none">${I('smartphone',{s:24})}</span>
<div style="flex:1 1 180px;min-width:0"><div class="tm" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">${name}${cur?'<span class="badge pri">Den här enheten</span>':''}</div>
<div class="bm v" style="display:flex;align-items:center;gap:6px;margin-top:2px">${av(who,20)} ${who==='J'?'Jacob':'Anna'}</div>
<div class="bs v" style="margin-top:6px">Parad 3 okt. · ${seen}<br>Kvitt 1.1</div></div>
${cur?'':`<div style="margin-left:auto;align-self:center">${btn('Återkalla','outl err',null,{al:'Återkalla Galaxy S26+'})}</div>`}</div>`;
const body=`<div class="pad" style="padding-top:4px"><div class="grp">${dev('Galaxy S26','J','Senast sedd nyss',1)}${dev('Galaxy S26+','A','Senast sedd 17 okt. 13:47',0)}</div>
<p class="bs v" style="margin:12px 8px">En återkallad enhet loggas ut och allt Kvitt-data på den raderas.</p></div>`;
const over=dialog?`<div class="scrim"></div><div class="dlg" role="dialog"><div class="di">${I('phonelink_erase',{s:28})}</div><div class="hs dt" style="text-align:center">Återkalla Galaxy S26+?</div><div class="bm db">Annas telefon förlorar åtkomsten direkt och allt Kvitt-data på den raderas. Anna kan para ihop den igen med en ny kod.</div><div class="da">${btn('Avbryt','text')}${btn('Återkalla','errf')}</div></div>`:'';
return {html:appBar({nav:'arrow_back',title:'Enheter'})+scroll(body),over};
}
function invites(){
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:4px">
<section class="hero" style="padding:24px;border-radius:32px">
<div style="position:relative"><div class="lm v">Ny enhet för Jacob</div>
<div class="ds num" style="font-weight:650;letter-spacing:2px;margin:10px 0 4px;display:flex;gap:14px;flex-wrap:wrap" aria-label="7K3M QX9D 2HPA"><span>7K3M</span><span>QX9D</span><span>2HPA</span></div>
<div class="bm" style="overflow-wrap:anywhere;color:var(--primary)">https://icehack.asuscomm.com:8443/i/7K3MQX9D2HPA</div>
<div class="bm v" style="display:flex;gap:6px;align-items:center;margin-top:10px">${I('schedule',{s:18})}Gäller till tis 20 okt. 14:02</div>
<div style="display:flex;gap:8px;margin-top:20px;flex-wrap:wrap">${btn('Dela','filled m','share')}${btn('Kopiera','outl pri','content_copy')}</div></div>
</section>
<section><h2 class="ts" style="margin:4px 4px 8px">Väntande inbjudningar</h2>
<div class="grp"><div class="gi li" style="padding:12px 8px 12px 16px"><span class="lead">${I('qr_code_2',{s:22})}</span><span class="m"><span class="bl" style="display:block">Ny enhet för Jacob</span><span class="bm v" style="display:block">Skapad 17 okt. 14:02 · Gäller till tis 20 okt. 14:02</span></span>${btn('Avbryt','text',null,{al:'Avbryt inbjudan'})}</div></div></section>
<p class="bs v" style="margin:0 8px">Koden fungerar en gång och går ut efter 72 timmar.</p>
</div>`;
return {html:appBar({nav:'arrow_back',title:'Para ihop en enhet'})+scroll(body)};
}
function about(){
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:12px">
<div style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:16px 0 8px">${kvittMark(88)}<div class="hm" style="font-weight:700">Kvitt</div><div class="bm v num">Version 1.1 (11)</div></div>
<div class="grp">
${kvRow('dns','Server','https://icehack.asuscomm.com:8443')}
${kvRow('memory','Serverversion','1.1.0')}
${settingsRow('description','Licenser för öppen källkod','',{leadSt:'background:none;color:var(--on-surface-variant);width:22px'})}
</div>
${btn('Sök efter uppdatering','tonal m','system_update')}
</div>`;
return {html:appBar({nav:'arrow_back',title:'Om Kvitt'})+scroll(body)};
}
function updateDialog(){
const h=home({banner:'update'});
h.over=`<div class="scrim"></div><div class="dlg" role="dialog"><div class="di">${I('install_mobile',{s:28})}</div><div class="hs dt" style="text-align:center">Tillåt installation</div>
<div class="bm db">Kvitt uppdateras utan Play Butik. Första gången frågar Android om Kvitt får <b>Installera okända appar</b>.</div>
<div class="bm db" style="margin-top:10px">Slå på <b>Tillåt från den här källan</b> och gå sedan tillbaka till Kvitt. Du behöver bara göra det en gång.</div>
<div class="da">${btn('Senare','text')}${btn('Fortsätt','filled')}</div></div>`;
return h;
}
function notifications(){
const n=(title,body,time,icon)=>`<div style="background:var(--surface-container-lowest);border-radius:24px;padding:14px 16px;display:flex;gap:12px">
<span style="width:32px;height:32px;border-radius:50%;background:var(--primary);color:var(--on-primary);display:grid;place-items:center;flex:none">${I(icon,{s:18})}</span>
<div style="flex:1;min-width:0"><div class="bs v">Kvitt · ${time}</div><div class="ts" style="margin-top:2px">${title}</div><div class="bm v" style="overflow-wrap:anywhere">${body}</div></div>${I('expand_more',{s:20,st:'color:var(--on-surface-variant)'})}</div>`;
const qs=['wifi','bluetooth','flashlight_on','do_not_disturb_on'];
const body=`<div class="pad" style="padding-top:16px">
<div style="display:flex;justify-content:space-between;align-items:flex-end;padding:0 8px 16px"><div><div class="dm num" style="font-weight:400">14:02</div><div class="bm v">lördag 17 oktober</div></div></div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:20px">${qs.map((q,i)=>`<span style="height:56px;border-radius:20px;display:grid;place-items:center;background:var(${i===0?'--primary':'--surface-container-high'});color:var(${i===0?'--on-primary':'--on-surface-variant'})">${I(q,{s:24})}</span>`).join('')}</div>
<div style="display:flex;flex-direction:column;gap:4px">
${n('Anna lade till en utgift',`ICA Maxi Lindhagen · ${kr(124760)} · Vardag`,'nu','receipt_long')}
${n('Anna registrerade en betalning',`Anna betalade dig ${kr(80000)}`,'12 min','handshake')}
</div>
<div style="display:flex;justify-content:flex-end;margin-top:12px">${btn('Rensa alla','tonal')}</div></div>`;
return {html:scroll(body), sb:'surface-dim', bg:'surface-dim', gb:'surface-dim'};
}
/* =====================================================================
FRAMES
===================================================================== */
const LD=['light','dark'];
const FRAMES=[
{id:'S-1-pairing',title:'S-1 Parning',note:'Betraktaren är Anna som går med. Fälten fyllda från länken. Serveradressen är en hopfälld sekundär rad (tryck för att ändra).',themes:LD,render:()=>pairing(false)},
{id:'S-1-pairing-error',title:'S-1 Parning, fel',note:'invite_invalid: inline-fel under koden, Para ihop aktiv igen.',themes:LD,render:()=>pairing(true)},
{id:'S-2-profile-step',title:'S-2 Profilsteg (Anna)',note:'Visas efter en partner-parning (AC-2.10). Betraktaren är Anna.',themes:LD,render:profileStep},
{id:'S-3-waiting',title:'S-3 Väntar på partner',note:'Ingen FAB: utgifter kan inte läggas till förrän partnern är med (AC-5.1).',themes:LD,render:waiting},
{id:'S-4-home',title:'S-4 Översikt',note:'Saldot är hjälten: ord ovanför, belopp i amount-hero (Roboto Flex, smal bredd, tung vikt), skärgårdsmotiv i hörnet. Kontolista som Expressive segmenterad grupp.',themes:LD,render:()=>home({})},
{id:'S-4-home-offline',title:'S-4 Översikt, offline + uppdatering',note:'Synkraden blir en tonad pill (tryck → S-16). Uppdateringsbanner i tertiär sand-färg. Allt annat fungerar som vanligt.',themes:LD,render:()=>home({offline:1,banner:'update'})},
{id:'S-4-home-settled',title:'S-4 Översikt, kvitt',note:'"Ni är kvitt" i stället för belopp, ingen Gör upp-knapp. Arkiverade utfällda.',themes:LD,render:()=>home({state:'settled',archOpen:1})},
{id:'S-4-home-empty',title:'S-4 Översikt, inga konton',note:'Ingen FAB: det finns inget konto att lägga utgiften i.',themes:LD,render:()=>home({state:'empty'})},
{id:'S-4-home-loading',title:'S-4 Översikt, första synk',note:'Expressive LoadingIndicator (formskiftande) i behållare; fallback CircularProgressIndicator. Skelett för kontolistan.',themes:LD,render:()=>home({state:'loading'})},
{id:'S-4-home-en',title:'S-4 Home (English)',note:'Same data, English copy and SEK 3,250.00 format.',themes:['light'],render:()=>home({en:1})},
{id:'S-4-home-200',title:'S-4 Översikt, 200 % teckenstorlek',note:'Text ≤ 22 sp skalas linjärt (×2); rubriker och saldot följer Android 14+ icke-linjär skalning (60 sp → ~64 dp). Belopp bryts till egen rad i stället för att kortas.',themes:['light'],fontScale:2,render:()=>home({})},
{id:'S-5-account',title:'S-5 Konto Vardag',note:'Överflödesmenyn öppen: Ta bort är inaktiverad med skäl. SL-kort har synk-markör (moln-ikon, "Väntar på synk" för TalkBack).',themes:LD,render:()=>account({menu:1})},
{id:'S-5-account-archived',title:'S-5 Konto Gotland 2026, arkiverat',note:'Arkiverat-märke, Kvitt, Återställ från arkiv. Menyn visar att Ta bort inte är tillåtet (kontot har utgifter).',themes:LD,render:()=>account({kind:'gotland'})},
{id:'S-6-account-editor',title:'S-6 Nytt konto',note:'Expressive slider med stopp-markör + två procentfält (Anna räknas ut, summan alltid 100).',themes:LD,render:()=>accountEditor(false)},
{id:'S-6-account-editor-offline',title:'S-6 Nytt konto, offline',note:'Kräver anslutning: fält och Spara inaktiverade.',themes:LD,render:()=>accountEditor(true)},
{id:'S-7-editor',title:'S-7 Ny utgift (10-sekundersflödet)',note:'Egen siffertangentbord i appen (Expressive tangentformer; nedtryckt tangent morfar till fyrkant). Standardval som chips direkt ovanför; Delning öppnas med ett tryck. Kategori = senast använd i Vardag (AC-8.6).',themes:LD,render:()=>editor({})},
{id:'S-7-editor-split',title:'S-7 Ny utgift, Exakta belopp med fel',note:'Delningschippen öppnar sektionen ovanför tangentbordet, som nu skriver i Du-fältet. Anna räknas ut (skrivskyddad). Spara inaktiverad.',themes:LD,render:()=>editor({mode:'split',dis:1})},
{id:'S-7-editor-full',title:'S-7 Ny utgift, Allt på en person',note:'Anteckningar utfällda och i fokus (systemtangentbordet skulle visas här).',themes:LD,render:()=>editor({mode:'full'})},
{id:'S-7-editor-200',title:'S-7 Ny utgift, 200 %',note:'Chips bryts på fler rader; mellandelen rullar medan tangentbordet och Spara (i toppraden) alltid syns.',themes:['light'],fontScale:2,render:()=>editor({big:1})},
{id:'S-8-expense',title:'S-8 Utgift ICA Maxi Lindhagen',note:'Tidslinje äldst först, väntande ändring sist i --pending.',themes:LD,render:()=>expenseDetail('')},
{id:'S-8-expense-recurring',title:'S-8 Utgift Hyra september (återkommande)',note:'',themes:LD,render:()=>expenseDetail('recurring')},
{id:'S-8-expense-deleted',title:'S-8 Utgift, borttagen',note:'Öppnad från en notis. Innehållet dämpat, inga åtgärder.',themes:LD,render:()=>expenseDetail('deleted')},
{id:'S-9-settle',title:'S-9 Gör upp, förslag',note:'Förslaget enligt §9.4: äldsta skulden först.',themes:LD,render:()=>settle({})},
{id:'S-9-settle-adjusted',title:'S-9 Gör upp, justerat',note:'Varning per rad (AC-11.5) i --warning med ikon.',themes:LD,render:()=>settle({v:'adjusted'})},
{id:'S-9-settle-remaining',title:'S-9 Gör upp, summan stämmer inte',note:'Kvar att fördela i error-container; Bekräfta inaktiverad.',themes:LD,render:()=>settle({v:'remaining'})},
{id:'S-9-settle-account',title:'S-9 Gör upp Huset (per konto)',note:'En fast rad och ingen Lägg till konto. Riktningen kan bytas (§9.4: per konto i båda riktningar).',themes:LD,render:()=>settle({account:1})},
{id:'S-9-confirm-dialog',title:'S-9 Bekräfta',note:'Dialogen upprepar varningen.',themes:LD,render:()=>settle({v:'adjusted',dialog:1})},
{id:'S-10-settlement',title:'S-10 Betalning',note:'',themes:LD,render:settlementDetail},
{id:'S-11-analytics',title:'S-11 Statistik',note:'Filter som ButtonGroup (fallback SegmentedButton). TalkBack: "Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr." "Per kategori: störst Hem, 70 733,50 kr, 39,7 %." "Per konto: störst Huset, 79 860 kr, 44,8 %." Listan rullar till Per konto (se S-11-analytics-scrolled).',themes:LD,render:()=>analytics({})},
{id:'S-11-analytics-scrolled',title:'S-11 Statistik, rullad till Per konto',note:'Extra ram (inte i briefens lista): samma skärm rullad så att diagram 2 och 3 syns. Filterraden ligger kvar som en kompakt rad.',themes:LD,render:()=>analytics({scrolled:1})},
{id:'S-11-analytics-week',title:'S-11 Statistik, vecka · min del · konto',note:'Påhittade veckovärden. TalkBack: "Utgifter över tid, min del per vecka: högst vecka 42, 4 550 kr; lägst vecka 36, 1 925 kr."',themes:LD,render:()=>analytics({week:1})},
{id:'S-11-analytics-empty',title:'S-11 Statistik, tom period',note:'',themes:LD,render:()=>analytics({empty:1})},
{id:'S-12-drilldown',title:'S-12 Mat & hushåll, oktober 2026',note:'Titeln får bryta på två rader.',themes:LD,render:drill},
{id:'S-13-settings',title:'S-13 Inställningar',note:'Grupperade Expressive-listor.',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 i handtaget för att sortera (TalkBack: anpassade åtgärder Flytta upp/ned).',themes:LD,render:categories},
{id:'S-15-category-editor',title:'S-15 Ändra kategori',note:'30 ikoner, 12 färger. Vald ikon får morfad form.',themes:LD,render:categoryEditor},
{id:'S-16-sync',title:'S-16 Synkstatus',note:'',themes:LD,render:()=>sync(false)},
{id:'S-16-sync-clock',title:'S-16 Synkstatus, klockan fel',note:'',themes:LD,render:()=>sync(true)},
{id:'S-17-update',title:'S-17 Tillåt installation',note:'Visas före första installationen.',themes:LD,render:updateDialog},
{id:'S-17-update-progress',title:'S-17 Laddar ner',note:'Expressive vågig LinearProgressIndicator (fallback rak).',themes:LD,render:()=>home({banner:'progress'})},
{id:'S-17-update-failed',title:'S-17 Nedladdning misslyckades',note:'',themes:LD,render:()=>home({banner:'failed'})},
{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:recurring},
{id:'S-19-recurring-editor',title:'S-19 Ändra återkommande Hyra',note:'Samma fält som S-7 utom datum.',themes:LD,render:recurringEditor},
{id:'S-20-devices',title:'S-20 Enheter',note:'',themes:LD,render:()=>devices(false)},
{id:'S-20-devices-revoke',title:'S-20 Återkalla enhet',note:'',themes:LD,render:()=>devices(true)},
{id:'S-21-invites',title:'S-21 Para ihop en enhet',note:'Koden i grupper om fyra, länken, giltighet, Dela (systemets delningsark).',themes:LD,render:invites},
{id:'S-22-about',title:'S-22 Om Kvitt',note:'',themes:LD,render:about},
{id:'N-notifications',title:'Notiser',note:'Ritade neutralt som Android 16 (inte One UI).',themes:LD,render:notifications}
];
function renderPhone(f,theme){
const fs=f.fontScale||1;
let r=f.render(theme); if(typeof r==='string') r={html:r};
const vars=`--fs:${fs};--fsh:${nlf(28,fs)};--fsd:${nlf(45,fs)};--fsx:${nlf(60,fs)};--sbbg:var(--${r.sb||'surface'});`;
return `<div class="phone${fs>1.5?' big':''}" data-frame-id="${f.id}-${theme}" data-theme="${theme}" lang="${f.id==='S-4-home-en'?'en':'sv'}" style="${vars}${r.bg?`background:var(--${r.bg})`:''}">${statusBar()}<div class="app">${r.html}</div><div class="gest" style="background:var(--${r.gb||'surface'})"><i></i></div>${r.over||''}</div>`;
}
/* =====================================================================
Review page
===================================================================== */
function swRow(th,name,against,min){
const v=TOKENS[th][name];const a=against?TOKENS[th][against]:null;const c=a?cr(v,a):null;
return `<td><span class="sw" style="background:${v}"></span><code>${v}</code></td><td class="n">${c?`<span class="${c<min?'bad':'ok'}">${c.toFixed(2)}</span> <span style="color:#70787C">vs ${against}</span>`:'—'}</td>`;
}
function colourTable(){
const P=[ // [role, against, min]
['primary','surface',4.5],['on-primary','primary',4.5],['primary-container','surface',0],['on-primary-container','primary-container',4.5],
['secondary','surface',4.5],['on-secondary','secondary',4.5],['secondary-container','surface',0],['on-secondary-container','secondary-container',4.5],
['tertiary','surface',4.5],['on-tertiary','tertiary',4.5],['tertiary-container','surface',0],['on-tertiary-container','tertiary-container',4.5],
['error','surface',4.5],['on-error','error',4.5],['error-container','surface',0],['on-error-container','error-container',4.5],
['surface',null],['on-surface','surface',4.5],['surface-variant',null],['on-surface-variant','surface',4.5],['on-surface-variant','surface-container-high',4.5],
['surface-container-lowest',null],['surface-container-low',null],['surface-container',null],['surface-container-high',null],['surface-container-highest',null],['surface-dim',null],['surface-bright',null],
['inverse-surface',null],['inverse-on-surface','inverse-surface',4.5],['inverse-primary','inverse-surface',3],
['outline','surface',3],['outline-variant','surface',0],['scrim',null],
['owed','surface',4.5],['owed','surface-container',4.5],['on-owed','owed',4.5],['owed-container','surface',0],['on-owed-container','owed-container',4.5],
['owing','surface',4.5],['owing','surface-container',4.5],['on-owing','owing',4.5],['owing-container','surface',0],['on-owing-container','owing-container',4.5],
['settled','surface',4.5],['settled-container','surface',0],['on-settled-container','settled-container',4.5],
['warning','surface',4.5],['on-warning','warning',4.5],['warning-container','surface',0],['on-warning-container','warning-container',4.5],
['pending','surface',4.5],
['avatar-1','surface',0],['on-avatar-1','avatar-1',4.5],['avatar-2','surface',0],['on-avatar-2','avatar-2',4.5]
];
return `<table><tr><th>Role</th><th>Light</th><th>Contrast (light)</th><th>Dark</th><th>Contrast (dark)</th></tr>${P.map(([n,a,m])=>`<tr><td><code>--${n}</code></td>${swRow('light',n,m?a:null,m)}${swRow('dark',n,m?a:null,m)}</tr>`).join('')}</table>`;
}
function catTable(){
return `<table><tr><th>Token</th><th>Light</th><th>vs surface</th><th>vs s-c-high</th><th>vs own container</th><th>Dark</th><th>vs surface</th><th>vs s-c-high</th><th>vs own container</th></tr>${CAT_TOKENS.map(c=>{
const cells=th=>{const v=TOKENS[th]['cat-'+c],cc=TOKENS[th]['cat-'+c+'-container'];const a=cr(v,TOKENS[th].surface),b=cr(v,TOKENS[th]['surface-container-high']),d=cr(v,cc);
return `<td><span class="sw" style="background:${v}"></span><code>${v}</code><br><span class="sw" style="background:${cc}"></span><code style="color:#70787C">${cc}</code></td><td class="n ${a<4.5?'bad':'ok'}">${a.toFixed(2)}</td><td class="n ${b<4.5?'bad':'ok'}">${b.toFixed(2)}</td><td class="n ${d<3?'bad':'ok'}">${d.toFixed(2)}</td>`;};
return `<tr><td><code>--cat-${c}</code></td>${cells('light')}${cells('dark')}</tr>`;}).join('')}</table>`;
}
function chartTable(){
const ks=[...Array(8)].map((_,i)=>'chart-cat-'+(i+1)).concat([...Array(5)].map((_,i)=>'chart-seq-'+(i+1)),['chart-grid','chart-axis-label']);
return `<table><tr><th>Token</th><th>Light</th><th>vs surface-container-low</th><th>Dark</th><th>vs surface-container-low</th></tr>${ks.map(k=>{const min=k==='chart-axis-label'?4.5:(k==='chart-grid'||k.startsWith('chart-seq')?0:3);
const cell=th=>{const v=TOKENS[th][k];const c=cr(v,TOKENS[th]['surface-container-low']);return `<td><span class="sw" style="background:${v}"></span><code>${v}</code></td><td class="n">${min?`<span class="${c<min?'bad':'ok'}">${c.toFixed(2)}</span>`:`${c.toFixed(2)} <span style="color:#70787C">(decorative/ramp)</span>`}</td>`;};
return `<tr><td><code>--${k}</code></td>${cell('light')}${cell('dark')}</tr>`;}).join('')}</table>`;
}
const TYPE=[
['display-large',57,64,400,'—'],['display-medium',45,52,400,'Kvitt wordmark (pairing)'],['display-small',36,44,400,'Invite code, "Ni är kvitt"'],
['headline-large',32,40,500,'—'],['headline-medium',28,36,500,'Onboarding titles, "kr" suffix in editor'],['headline-small',24,32,500,'Expense title, empty-state titles, dialog titles'],
['title-large',22,28,500,'Top app bar, hero wording'],['title-medium',16,24,560,'List item titles, account names'],['title-small',14,20,560,'Section headers'],
['body-large',16,24,400,'Field values, settings rows'],['body-medium',14,20,400,'Supporting text, effect lines'],['body-small',12,16,400,'Meta, timestamps'],
['label-large',14,20,560,'Buttons, chips'],['label-medium',12,16,560,'Nav labels, overlines'],['label-small',11,16,560,'Badges, chart legend/axes'],
['amount-hero',60,64,'720, wdth 84','Global balance on Home (decimals + kr at 50 %)'],['amount-large',32,40,'650, wdth 92','Account balance, detail amount, settle amount, totals'],
['amount-medium',16,24,620,'Amounts in lists'],['amount-small',14,20,520,'Month totals, breakdown amounts']
];
function reviewPage(){
const groups=[];FRAMES.forEach(f=>{const g=f.id.split('-').slice(0,2).join('-');let G=groups.find(x=>x.g===g);if(!G){G={g,items:[]};groups.push(G);}G.items.push(f);});
const heroBox=(th,st)=>`<div class="hbox" data-theme="${th}" style="--fs:1;--fsh:1;--fsd:1;--fsx:1">${hero(st,st==='owing'?325000:(st==='owed'?118450:0))}</div>`;
return `<div class="rv">
<div style="display:flex;align-items:center;gap:16px"><div class="phone" data-theme="light" style="width:auto;height:auto;background:none;box-shadow:none;--fs:1">${kvittMark(56)}</div><div><div style="font-size:14px;color:#40484C;letter-spacing:.5px">KVITT · UX-RIKTNING 1</div><h1>Skärgård</h1></div></div>
<p class="idea">Stockholms skärgård en stilla dag: Material 3 Expressive med hav-och-granit-palett, stora rundade former och ett stort saldo som hjälte. Lekfullt men tyst.</p>
<p>Varje skärm har få element och mycket luft. Översikten domineras av det globala saldot: orden ovanför ("Du är skyldig Anna"), beloppet i en smal, tung Roboto Flex-siffra och båda avatarerna i en Expressive "cookie"-form. Bakom saldot ligger ett diskret skärgårdsmotiv (granitholmar och dyning) i ytans egna toner. Konton och inställningar är segmenterade grupper med stora ytterhörn (20 dp) och små innerhörn (4 dp). Utgiftseditorn är byggd för tio sekunder: beloppet först i displaystorlek, ett eget siffertangentbord med Expressive-tangenter och alla standardval som chips direkt ovanför. Färgen bär lite: havsblå primärfärg, granitgrå sekundär, en varm lav/sand-accent som bara används för uppdateringar, väntande synk och diagram.</p>
<h2>Färger</h2>
<p>Alla färger är roller (CSS custom properties per tema; Compose: <code>ColorScheme</code> + en <code>KvittColors</code>-CompositionLocal för de semantiska rollerna). Kontrast beräknas här i JavaScript mot ytan rollen används på. Röd = under 4,5:1 för text eller 3:1 för grafik.</p>
<p><b>Skyldig/fordran utan rött/grönt:</b> "Anna är skyldig dig" använder havsgrön-blå <code>--owed</code>, "Du är skyldig Anna" använder korall/rost <code>--owing</code>. Paret skiljer sig i både nyans (blå-grön mot orange) och ljushet, så det håller för protanopi och deuteranopi; riktningen står dessutom alltid i ord.</p>
${colourTable()}
<h3>Kategorifärger (§9.7)</h3>
<p>Utgår från briefens baslinje. Ljust tema: green, orange, yellow, teal och grey är något mörkare än baslinjen så att alla klarar 4,5:1 även mot <code>--surface-container-high</code> (Skärgårds kalla grå ytor är mörkare än baslinjens referens). Mörkt tema: baslinjen oförändrad. Behållaren bakom ikonen = ${CAT_MIX.light*100} % av färgen över <code>--surface</code> (ljust) och ${CAT_MIX.dark*100} % (mörkt); ikonen fylls (FILL 1).</p>
${catTable()}
<h3>Diagram</h3>
<p><code>--chart-cat-*</code> används för konton i ordningen Vardag, Huset, Japan 2027, Gotland 2026 …; kategorier använder sina egna kategorifärger. Staplade segment skiljs av 2 dp mellanrum så att färgen aldrig är enda gränsen.</p>
${chartTable()}
<h3>Dynamisk färg (Material You)</h3>
<p>Erbjuds som ett val i Inställningar › Profil (av som standard). Riktningen bygger helt på roller, så en bakgrundsbild-palett byter bara havsblått mot användarens färg; ytor, former, typografi och saldots komposition bär karaktären. <b>Undantag som alltid behåller Skärgårds värden:</b> <code>--owed</code>/<code>--owing</code>/<code>--warning</code>/<code>--pending</code>, kategorifärgerna och diagramfärgerna, eftersom deras betydelse och kontrast inte får bero på en tapet.</p>
<h2>Typografi</h2>
<p><b>Roboto Flex</b> (SIL Open Font License 1.1, Google Fonts; kan buntas som variabel <code>res/font</code>-resurs med <code>FontVariation.Settings</code>). Varför: det är Androids eget formspråk, har tabellsiffror (<code>tnum</code>), och axlarna bredd (wdth 25–151), vikt och optisk storlek låter saldot bli uttrycksfullt utan en andra familj. Bredd/vikt används <b>bara</b> för belopp (amount-hero wdth 84 / vikt 720, amount-large wdth 92 / 650); allt annat är standard-Roboto-proportioner. Alla belopp: <code>font-variant-numeric: tabular-nums</code> (Compose: <code>fontFeatureSettings = "tnum"</code>).</p>
<table><tr><th>Style</th><th>Size (sp)</th><th>Line height</th><th>Weight / axes</th><th>Used for</th></tr>${TYPE.map(t=>`<tr><td><code>${t[0]}</code></td><td class="n">${t[1]}</td><td class="n">${t[2]}</td><td>${t[3]}</td><td>${t[4]}</td></tr>`).join('')}</table>
<p><b>200 %:</b> text upp till 22 sp skalas linjärt i ramarna (×2, hårdare än Android). Rubriker (≥ 24 sp) och amount-hero följer Android 14+:s icke-linjära skalning (<code>FontScaleConverter</code>, som Compose använder automatiskt för sp), annars skulle 60 sp bli 120 dp och inget belopp skulle få plats. Belopp bryts aldrig inuti (hårt mellanslag) och kortas aldrig; de flyttar till en egen rad.</p>
<h2>Form, avstånd, upphöjning, rörelse, ikoner</h2>
<div class="cols"><div>
<h3>Form</h3>
<table><tr><th>Token</th><th>dp</th><th>Used by</th></tr>
<tr><td><code>--radius-xs</code> extraSmall</td><td class="n">4</td><td>Inner corners of segmented groups, stacked bar segments (3)</td></tr>
<tr><td><code>--radius-sm</code> small</td><td class="n">8</td><td>Inner corners of connected button groups, small category tile in chips</td></tr>
<tr><td><code>--radius-md</code> medium</td><td class="n">12</td><td>Category icon container (40 dp), skeletons</td></tr>
<tr><td><code>--radius-lg</code> large</td><td class="n">16</td><td>Text fields, menus, small FAB, pressed keypad key</td></tr>
<tr><td><code>--radius-lg-inc</code> largeIncreased</td><td class="n">20</td><td>Outer corners of segmented lists, medium FAB, keypad function keys, banners in content</td></tr>
<tr><td><code>--radius-xl</code> extraLarge</td><td class="n">28</td><td>Cards, dialogs, split section</td></tr>
<tr><td><code>--radius-xl-inc</code> extraLargeIncreased</td><td class="n">32</td><td>Balance hero, invite card</td></tr>
<tr><td><code>--radius-xxl</code> extraExtraLarge</td><td class="n">48</td><td>Bottom sheets (date picker), not used in frames</td></tr>
<tr><td><code>--radius-full</code> full</td><td class="n">50 %</td><td>Buttons, chips, digit keys, nav indicator, avatars in lists</td></tr></table>
<p>MaterialShapes: <b>Cookie9Sided</b> for large avatars (≥ 40 dp), category hero and confirmation icons; <b>Cookie12Sided</b> for the Kvitt mark; <b>Clover4Leaf</b> for empty-state art. Stable fallback: <code>CircleShape</code> (avatars) and <code>RoundedCornerShape(28.dp)</code> (art).</p>
<h3>Spacing (dp)</h3>
<table><tr><th>Token</th><th>dp</th></tr>${[[1,4],[2,8],[3,12],[4,16],[5,20],[6,24],[8,32],[10,40],[12,48],[16,64]].map(([a,b])=>`<tr><td><code>--space-${a}</code></td><td class="n">${b}</td></tr>`).join('')}</table>
<p>Side gutter 16; card padding 20 (hero 20/24); 2 dp between segmented items; 12–16 between sections.</p>
</div><div>
<h3>Elevation</h3>
<p>Tonal, not shadowed. Level 0 surface (screens, nav bar uses surface-container), level 1 surface-container-low (cards, keypad tray), level 2 surface-container (hero, segmented lists, menus + 2 dp shadow), level 3 surface-container-high (dialogs; FAB with 3 dp shadow). Scrim 40 % of <code>--scrim</code>.</p>
<h3>Motion</h3>
<table><tr><th>Token</th><th>Expressive spring (Compose MotionScheme.expressive)</th><th>Fallback (standard)</th></tr>
<tr><td>spatial-fast</td><td>damping 0.6, stiffness 800</td><td>200 ms, emphasized (0.2, 0, 0, 1)</td></tr>
<tr><td>spatial-default</td><td>damping 0.8, stiffness 380</td><td>300 ms, emphasized</td></tr>
<tr><td>spatial-slow</td><td>damping 0.8, stiffness 200</td><td>500 ms, emphasized</td></tr>
<tr><td>effects-fast</td><td>damping 1.0, stiffness 3800</td><td>150 ms, standard (0.2, 0, 0, 1)</td></tr>
<tr><td>effects-default</td><td>damping 1.0, stiffness 1600</td><td>200 ms, standard</td></tr>
<tr><td>effects-slow</td><td>damping 1.0, stiffness 800</td><td>300 ms, standard</td></tr></table>
<p>Principles: springs only for things the finger moves (key press morphs round → square 16 dp, button groups widen the pressed segment, the Delning section expands from its chip, FAB collapses to icon on scroll). The hero amount counts to a new value over effects-slow; colour changes cross-fade. Screen transitions: shared-axis X for push, fade-through between nav destinations. Reduced motion: springs replaced by 0 ms snaps except fades.</p>
<h3>Iconography</h3>
<p>Material Symbols <b>Rounded</b>, weight 400, grade 0, optical size 24 (20 for 18–20 dp). Fill 1 for: selected navigation item, category icons, status/error icons; fill 0 elsewhere. Icon size 24 dp in bars/lists, 18 in chips and buttons, 16 inline with text. Every icon-only control carries a Swedish <code>aria-label</code> (TalkBack <code>contentDescription</code>).</p>
</div></div>
<h2>Saldohuvudet i tre lägen</h2>
<div class="heroes">${['owing','owed','settled'].map(s=>heroBox('light',s)).join('')}</div>
<div class="heroes" style="margin-top:16px">${['owing','owed','settled'].map(s=>heroBox('dark',s)).join('')}</div>
<p class="note">Ord först, belopp i amount-hero i semantisk färg, Gör upp (filled, 56 dp) bara när saldot inte är noll. Kvitt-läget byter beloppet mot "Ni är kvitt" med en cookie-bock i granitgrått.</p>
<h2>Material 3 Expressive-komponenter och stabila reserver</h2>
<table><tr><th>Expressive component</th><th>Where</th><th>Stable Material 3 fallback</th></tr>
<tr><td>ShortNavigationBar (64 dp)</td><td>Bottom navigation</td><td>NavigationBar (80 dp)</td></tr>
<tr><td>Medium extended FAB (80 dp, 20 dp corners)</td><td>Home "Lägg till utgift"</td><td>ExtendedFloatingActionButton (56 dp)</td></tr>
<tr><td>Small extended FAB</td><td>Account detail, categories, recurring</td><td>ExtendedFloatingActionButton</td></tr>
<tr><td>ButtonGroup, connected toggle buttons</td><td>Analytics: Månad/Vecka, Hushållet/Min del, Kategori/Konto; "Vem står för kostnaden"</td><td>SingleChoiceSegmentedButtonRow</td></tr>
<tr><td>LoadingIndicator (contained)</td><td>Home first sync</td><td>CircularProgressIndicator</td></tr>
<tr><td>Wavy LinearProgressIndicator</td><td>Update download</td><td>LinearProgressIndicator</td></tr>
<tr><td>MaterialShapes (Cookie9, Cookie12, Clover4)</td><td>Avatars ≥ 40 dp, icons, art</td><td>CircleShape / RoundedCornerShape</td></tr>
<tr><td>Slider with inset handle and stop indicator</td><td>Account editor</td><td>Slider (2023 style)</td></tr>
<tr><td>Segmented list (Expressive grouped ListItem shapes)</td><td>Accounts, settings, details</td><td>ListItem in Card with dividers</td></tr>
<tr><td>Menu with supporting text, 16 dp corners</td><td>Account overflow</td><td>DropdownMenu + DropdownMenuItem</td></tr>
<tr><td>MotionScheme.expressive()</td><td>All</td><td>MotionScheme.standard()</td></tr>
<tr><td>Button sizes (S 40, M 56) with shape morph on press</td><td>All buttons</td><td>Button (40 dp), no morph</td></tr></table>
<p>Custom (no Material component): the in-app numeric keypad (a <code>LazyVerticalGrid</code> of 12 <code>Surface</code> buttons, shape animated on press) and the balance hero with its archipelago <code>Canvas</code> decoration.</p>
<h2>Skärmar</h2>
${groups.map(G=>`<h3>${G.items[0].title.split(/[ ,]/)[0]} ${G.items[0].title.replace(/^\S+\s/,'').split(',')[0]}</h3>${G.items.map(f=>`<div class="frames">${f.themes.map(th=>`<div class="fr">${renderPhone(f,th)}<div class="cap"><b>${f.title}</b> · ${th}${f.note?'<br>'+f.note:''}</div></div>`).join('')}</div>`).join('')}`).join('')}
</div>`;
}
/* =====================================================================
Boot: URL modes
===================================================================== */
(function(){
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>';
return;
}
const fr=q.get('frame');
if(fr){
document.body.classList.add('frame-mode');
const m=fr.match(/^(.*)-(light|dark)$/);
const f=m&&FRAMES.find(x=>x.id===m[1]);
root.innerHTML=f?renderPhone(f,m[2]):'<p>Unknown frame</p>';
return;
}
root.innerHTML=reviewPage();
})();
</script>
</body>
</html>