UX: Kalas with 14 default categories, 33 icons, plum and sand colour tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2TeboHB8AtnzTrt3bWFzY
This commit is contained in:
2026-10-18 07:55:00 +02:00
parent 4d3c343f16
commit 0006bd79be
+29 -26
View File
@@ -327,8 +327,8 @@ function mix(a, b, t){ const A = hexToRgb(a), B = hexToRgb(b); return '#' + A.ma
/* ---------- tokens (single source: CSS + review tables + contrast). Muted revision 2026-10-18 ---------- */
const CAT_BASE = {
light: { green:'#4E7A55', orange:'#B26A3E', brown:'#7A5F50', yellow:'#9C7C2A', blue:'#4A78A8', teal:'#367F82', purple:'#865891', red:'#A84B45', pink:'#AE5D7C', grey:'#6C7177', indigo:'#45508D', lime:'#73822F' },
dark: { green:'#8FB896', orange:'#E8AA7E', brown:'#B8A090', yellow:'#E6C67A', blue:'#98C0E6', teal:'#8CCAC8', purple:'#C59CDA', red:'#DC8F86', pink:'#EFB0BE', grey:'#B0B2B3', indigo:'#969DD8', lime:'#B9CC86' }
light: { green:'#4E7A55', orange:'#B26A3E', brown:'#6B5244', yellow:'#9C7C2A', blue:'#4A78A8', teal:'#367F82', purple:'#865891', red:'#A84B45', pink:'#AE5D7C', grey:'#6C7177', indigo:'#45508D', lime:'#73822F', plum:'#5E2D4E', sand:'#937D63' },
dark: { green:'#8FB896', orange:'#E8AA7E', brown:'#B8A090', yellow:'#E6C67A', blue:'#98C0E6', teal:'#8CCAC8', purple:'#C59CDA', red:'#DC8F86', pink:'#EFB0BE', grey:'#B0B2B3', indigo:'#969DD8', lime:'#B9CC86', plum:'#B0719A', sand:'#DFCFC0' }
};
const CAT_CONTAINER_MIX = { light: 0.18, dark: 0.24 };
const TOK = {
@@ -508,19 +508,23 @@ const iconBtn = (name, label, o = {}) => `<button class="ib${o.tonal ? ' tonal'
const CAT = {
groceries: { sv:'Mat & hushåll', en:'Groceries', icon:'shopping_cart', c:'green' },
restaurants: { sv:'Restaurang & café', en:'Restaurants', icon:'restaurant', c:'orange' },
housing: { sv:'Boende', en:'Housing', icon:'key', c:'indigo' },
home: { sv:'Hem', en:'Home', icon:'home', c:'brown' },
utilities: { sv:'Räkningar', en:'Utilities', icon:'bolt', c:'yellow' },
insurance: { sv:'Försäkringar', en:'Insurance', icon:'shield', c:'plum' },
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' },
children: { sv:'Barn', en:'Children', icon:'child_care', c:'lime' },
shopping: { sv:'Shopping', en:'Shopping', icon:'shopping_bag', c:'pink' },
fees: { sv:'Avgifter & skatter', en:'Fees & taxes', icon:'account_balance', c:'sand' },
other: { sv:'Övrigt', en:'Other', icon:'category', c:'grey' },
pant: { sv:'Pant', en:'Pant', icon:'water_drop', c:'lime', archived:true }
pant: { sv:'Pant', en:'Pant', icon:'water_drop', c:'teal', archived:true }
};
const DEFAULT_CATS = ['groceries','restaurants','home','utilities','transport','travel','entertainment','health','shopping','other'];
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'];
const COLORS12 = ['green','orange','brown','yellow','blue','teal','purple','red','pink','grey','indigo','lime'];
const DEFAULT_CATS = ['groceries','restaurants','housing','home','utilities','insurance','transport','travel','entertainment','health','children','shopping','fees','other'];
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','key','shield','account_balance'];
const COLORS12 = ['green','orange','brown','yellow','blue','teal','purple','red','pink','grey','indigo','lime','plum','sand']; // 14 tokens (plum, sand proposed 2026-10-18)
const ACC = {
vardag: { name:'Vardag', net: 118450, split:[50,50], n:11, chart:1 },
@@ -567,10 +571,10 @@ const monthTotal = m => m.total != null ? m.total : m.rows.filter(r => r.t === '
const MONTHS = ['maj','jun','jul','aug','sep','okt'];
const AN = {
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],
housing:[1240000,1240000,1240000,1240000,1240000,0], home:[75000,142000,59000,106000,98000,393350], utilities:[149100,140100,125100,133100,158500,118600], insurance:[28900,28900,28900,28900,28900,28900],
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]
children:[0,0,0,0,0,0], shopping:[0,62000,0,136000,74000,0], fees:[0,0,0,0,0,0], other:[0,0,0,0,0,0]
};
const AN_ACC = [['vardag',6695140],['huset',7986000],['japan',2702000],['gotland',438000]];
const WEEKS = ['v. 35','v. 36','v. 37','v. 38','v. 39','v. 40','v. 41','v. 42'];
@@ -815,7 +819,6 @@ function scrEditor(fsBig){
const b = `${amountBlock(41200, '412', { caret: true })}
${field({ label: 'Beskrivning', value: 'Systembolaget' })}
${catQuick('entertainment', null, fsBig)}
${autoCap()}
<div class="chips">${chip('Vardag', { dot: 'chart-cat-1', drop: true, aria: 'Konto: Vardag' })}${chip('Idag', { icon: 'event', drop: true, aria: 'Datum: Idag' })}${chip('Du', { k: 'Betalt av', avatar: 'jacob', drop: true })}${chip('Kontots standard 50/50', { k: 'Delning', icon: 'call_split', drop: true })}</div>
<div class="hstack" style="gap:12px;padding:2px 4px"><div style="width:72px">${splitBar(50, { thin: true, aria: 'Hälften var' })}</div><span class="t-body-medium">Du <b class="amt">${kr(20600)}</b> · Anna <b class="amt">${kr(20600)}</b></span></div>
${btn('Lägg till anteckning', { kind: 'text', icon: 'notes', style: 'align-self:flex-start;margin-left:-8px' })}`;
@@ -826,7 +829,7 @@ function splitModes(on){
return `<div class="chips" role="radiogroup" aria-label="Delning">${m.map((x, i) => chip(x, { sel: i === on })).join('')}</div>`;
}
function scrEditorSplit(){
const compact = `<div class="hstack" style="padding:6px 20px 10px;gap:12px;border-bottom:1px solid var(--outline-variant)">${catTile('shopping', 36)}<div class="grow"><div class="t-title-medium ell">Ullared</div><div class="t-body-small muted hstack" style="gap:4px">${ic('auto_awesome', { size: 14, color: 'secondary' })}Shopping, vald utifrån beskrivningen</div></div><span class="t-amount-medium">${kr(106000)}</span></div>`;
const compact = `<div class="hstack" style="padding:6px 20px 10px;gap:12px;border-bottom:1px solid var(--outline-variant)">${catTile('shopping', 36)}<div class="grow"><div class="t-title-medium ell">Ullared</div><div class="t-body-small muted">Shopping</div></div><span class="t-amount-medium">${kr(106000)}</span></div>`;
const b = `<div class="chips">${chip('Exakta belopp', { k: 'Delning', icon: 'call_split', sel: true, drop: 'up' })}</div>
<section class="flat" style="padding:16px;display:flex;flex-direction:column;gap:12px">
${splitModes(2)}
@@ -840,7 +843,6 @@ function scrEditorFull(){
const b = `${amountBlock(106000, '1' + NB + '060,00', { pad: '4px 0 2px' })}
${field({ label: 'Beskrivning', value: 'SL-kort 30 dagar, Anna' })}
${catQuick('transport')}
${autoCap()}
<div class="chips">${chip('Vardag', { dot: 'chart-cat-1', drop: true })}${chip('14 okt.', { icon: 'event', drop: true })}${chip('Du', { k: 'Betalt av', avatar: 'jacob', drop: true })}</div>
<section class="flat" style="padding:16px;display:flex;flex-direction:column;gap:12px">
<div class="hstack"><span class="t-title-small grow">Delning</span>${ic('expand_less', { color: 'on-surface-variant' })}</div>
@@ -877,7 +879,7 @@ function scrExpense(kind){
const acts = kind === 'deleted' ? '' : iconBtn('edit', 'Ändra') + iconBtn('delete', 'Ta bort');
let b = '';
if (kind === 'recurring') {
b = detailHero({ cat: 'home', desc: 'Hyra september', acc: 'Huset', amt: 1240000, rec: 'Hyra' }) +
b = detailHero({ cat: 'housing', desc: 'Hyra september', acc: 'Huset', amt: 1240000, rec: 'Hyra' }) +
infoRows([['event', 'Datum', 'fredag 25 september 2026'], ['person', 'Betalt av', `<span class="hstack">${avatar('anna', 24)}Anna</span>`], ['call_split', 'Delning', sharesRow(744000, 496000, 'Kontots standard 60/40')]]) +
timeline([{ t: 'Skapad automatiskt (återkommande) · 25 sep. 06:00' }]);
} else if (kind === 'deleted') {
@@ -974,7 +976,7 @@ function scrAnalytics(v){
<section class="card" style="padding:18px 16px 16px"><div class="t-title-medium">Utgifter över tid</div>${stackChart(buckets, series, MONTHS, { max: 4500000, ticks: [0, 1500000, 3000000, 4500000], hl: 5, aria: 'Utgifter över tid' })}<div class="legend">${series.map(s => `<span><i style="background:var(--${s.color})"></i>${CAT[s.key].sv}</span>`).join('')}</div></section>
<section class="card" style="padding:14px 16px"><div class="t-title-medium" style="margin-bottom:4px">Per kategori</div>${catBreakdown(3)}</section>`;
}
return screen({ top: topBar({ nav: null, title: 'Statistik', bg: v === 'scrolled' ? 'surface-container' : '' }), body: b, nav: 1, padStyle: 'gap:10px', scroll: v === 'scrolled' ? 360 : 0 });
return screen({ top: topBar({ nav: null, title: 'Statistik', bg: v === 'scrolled' ? 'surface-container' : '' }), body: b, nav: 1, padStyle: 'gap:10px', scroll: v === 'scrolled' ? 470 : 0 });
}
function scrDrill(){
const rows = VARDAG[0].rows.filter(r => r.cat === 'groceries');
@@ -998,17 +1000,17 @@ function scrProfile(){
${field({ label: 'Namn', value: 'Jacob', focus: true, sup: 'Så här ser Anna dig i Kvitt.' })}`;
return screen({ top: topBar({ title: 'Profil', actions: btn('Spara') }), body: b, padStyle: 'gap:14px' });
}
function scrCategories(){
function scrCategories(scrolled){
const row = (k, o = {}) => `<div class="it row" style="min-height:56px;padding:4px 12px 4px 8px;gap:12px">${o.noDrag ? '<span style="width:32px"></span>' : `<span class="ib" role="button" aria-label="Flytta ${CAT[k].sv}" style="width:32px;height:48px">${ic('drag_indicator')}</span>`}${catTile(k, 36, { muted: o.muted })}<span class="t-title-medium grow">${CAT[k].sv}</span>${ic('chevron_right', { color: 'on-surface-variant' })}</div>`;
const b = `<div class="grp">${DEFAULT_CATS.map(k => row(k)).join('')}</div>${archivedRow(true)}<div class="grp">${row('pant', { noDrag: true, muted: true })}</div>`;
return screen({ top: topBar({ title: 'Kategorier', actions: iconBtn('add', 'Ny kategori') }), body: b, padStyle: 'gap:4px' });
return screen({ top: topBar({ title: 'Kategorier', actions: iconBtn('add', 'Ny kategori'), bg: scrolled ? 'surface-container' : '' }), body: b, padStyle: 'gap:4px', scroll: scrolled ? 330 : 0 });
}
function scrCategoryEditor(){
const b = `<div class="hstack" style="gap:14px;align-items:flex-start">${catTile('groceries', 64)}<div class="grow">${field({ label: 'Namn', value: 'Matvaror', focus: true })}<div class="hstack" style="justify-content:space-between;padding:2px 0 0 16px"><span class="t-body-small muted">Standardnamn:<br>Mat&nbsp;&amp;&nbsp;hushåll</span>${btn('Återställ namn', { kind: 'text', icon: 'restart_alt' })}</div></div></div>
<div class="t-title-small" style="padding:0 4px">Ikon</div>
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:6px" role="radiogroup" aria-label="Ikon">${ICONS30.map(n => `<span role="radio" aria-checked="${n === 'shopping_cart'}" aria-label="${n}" style="height:48px;display:grid;place-items:center;border-radius:16px;${n === 'shopping_cart' ? 'background:var(--cat-green);color:var(--on-cat)' : 'background:var(--surface-container-high);color:var(--on-surface-variant)'}">${ic(n, { f: n === 'shopping_cart' })}</span>`).join('')}</div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:5px" role="radiogroup" aria-label="Ikon">${ICONS30.map(n => `<span role="radio" aria-checked="${n === 'shopping_cart'}" aria-label="${n}" style="height:48px;display:grid;place-items:center;border-radius:16px;${n === 'shopping_cart' ? 'background:var(--cat-green);color:var(--on-cat)' : 'background:var(--surface-container-high);color:var(--on-surface-variant)'}">${ic(n, { f: n === 'shopping_cart' })}</span>`).join('')}</div>
<div class="t-title-small" style="padding:4px 4px 0">Färg</div>
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:8px;justify-items:center" role="radiogroup" aria-label="Färg">${COLORS12.map(c => `<span role="radio" aria-checked="${c === 'green'}" aria-label="${c}" style="width:44px;height:44px;border-radius:50%;background:var(--cat-${c});display:grid;place-items:center;color:var(--on-cat);${c === 'green' ? 'box-shadow:0 0 0 3px var(--surface),0 0 0 5px var(--on-surface)' : ''}">${c === 'green' ? ic('check', { size: 22 }) : ''}</span>`).join('')}</div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:8px;justify-items:center" role="radiogroup" aria-label="Färg">${COLORS12.map(c => `<span role="radio" aria-checked="${c === 'green'}" aria-label="${c}" style="width:44px;height:44px;border-radius:50%;background:var(--cat-${c});display:grid;place-items:center;color:var(--on-cat);${c === 'green' ? 'box-shadow:0 0 0 3px var(--surface),0 0 0 5px var(--on-surface)' : ''}">${c === 'green' ? ic('check', { size: 22 }) : ''}</span>`).join('')}</div>
<div class="hstack" style="gap:12px;flex-wrap:wrap;padding-top:6px">${btn('Arkivera', { kind: 'outlined', icon: 'inventory_2' })}<span class="hstack t-body-small muted" style="gap:6px">${ic('lock', { size: 16 })}Kan inte tas bort: standardkategori</span></div>`;
return screen({ top: topBar({ nav: 'close', title: 'Ändra kategori', actions: btn('Spara') }), body: b, padStyle: 'gap:10px' });
}
@@ -1032,14 +1034,14 @@ function scrRemoved(){
return screen({ body: b, padStyle: 'gap:0;min-height:100%', bottom: `<div style="padding:12px 16px 0">${btn('Para ihop igen', { m: true, full: true, icon: 'link' })}</div>` });
}
function scrRecurring(){
const T = [['home', 'Hyra', 1240000, 'Huset · Anna betalar', 'Varje månad den 25:e<br>Nästa 25\u00A0okt.'], ['utilities', 'Bahnhof bredband', 39900, 'Huset · Du betalar', 'Varje månad den 28:e<br>Nästa 28\u00A0okt.'], ['entertainment', 'Netflix', 17900, 'Vardag · Du betalar', 'Varje månad den 3:e<br>Nästa 3\u00A0nov.'], ['entertainment', 'Spotify Family', 25900, 'Vardag · Anna betalar', 'Varje månad den 12:e', true]];
const T = [['housing', 'Hyra', 1240000, 'Huset · Anna betalar', 'Varje månad den 25:e<br>Nästa 25\u00A0okt.'], ['utilities', 'Bahnhof bredband', 39900, 'Huset · Du betalar', 'Varje månad den 28:e<br>Nästa 28\u00A0okt.'], ['entertainment', 'Netflix', 17900, 'Vardag · Du betalar', 'Varje månad den 3:e<br>Nästa 3\u00A0nov.'], ['entertainment', 'Spotify Family', 25900, 'Vardag · Anna betalar', 'Varje månad den 12:e', true]];
const b = `<div class="grp">${T.map(([k, d, a, w, s, p]) => `<div class="it row r72" style="align-items:flex-start;padding-top:14px;padding-bottom:14px">${catTile(k, 40, { muted: p })}<div class="rc"><div class="t-title-medium ell" style="${p ? 'opacity:.7' : ''}">${d}</div><div class="t-body-medium muted">${w}</div><div class="t-body-small ${p ? '' : 'muted'}" style="margin-top:2px">${p ? `<span class="badge lantern">${ic('pause', { size: 14 })}Pausad</span> Varje månad den 12:e` : s}</div></div><span class="t-amount-small" style="${p ? 'opacity:.7' : ''}">${kr(a)}</span></div>`).join('')}</div>`;
return screen({ top: topBar({ title: 'Återkommande utgifter' }), body: b, padStyle: 'gap:10px', fab: fab('Ny återkommande') });
}
function scrRecurringEditor(){
const b = `${amountBlock(1240000, '12' + NB + '400,00', { pad: '2px 0 0' })}
${field({ label: 'Beskrivning', value: 'Hyra' })}
${catQuick('home', ['groceries', 'home', 'utilities', 'transport', 'entertainment'])}
${catQuick('housing', ['groceries', 'housing', 'home', 'utilities', 'transport'])}
<div class="chips">${chip('Huset', { dot: 'chart-cat-2', drop: true })}${chip('Anna', { k: 'Betalt av', avatar: 'anna', drop: true })}${chip('Kontots standard 60/40', { k: 'Delning', icon: 'call_split', drop: true })}</div>
<section class="flat" style="padding:16px;display:flex;flex-direction:column;gap:10px"><div class="t-title-small">Schema</div>
<div style="display:grid;grid-template-columns:1fr 1.5fr;gap:10px">${field({ label: 'Dag i månaden', value: '25' })}${field({ label: 'Första månad', value: 'september 2026', trail: ic('expand_more') })}</div>
@@ -1100,9 +1102,9 @@ const FRAMES = [
{ id:'S-5-account-archived-menu', title:'S-5 Gotland 2026 · meny (extra)', note:'Extra ram: Ta bort konto är avstängt eftersom kontot har utgifter och en betalning.', themes:LD, render:() => scrAccount({ archived:true, menu:true }) },
{ id:'S-6-account-editor', title:'S-6 Nytt konto', note:'Delningen som Expressive-reglage (tjock delad bana, smalt handtag) + två procentfält som alltid summerar till 100 + snabbval i en ButtonGroup.', themes:LD, render:() => scrAccountEditor(false) },
{ id:'S-6-account-editor-offline', title:'S-6 Nytt konto · offline', note:'Kräver anslutning: fälten och Spara är avstängda.', themes:LD, render:() => scrAccountEditor(true) },
{ id:'S-7-editor', title:'S-7 Ny utgift · 10 sekunder', note:'Egen siffertangentbordspanel i appen (beskrivningen använder systemtangentbordet). Kategoriraden visar de mest använda; Nöje valdes automatiskt utifrån beskrivningen (Systembolaget) och bildtexten "Vald utifrån beskrivningen" försvinner så fort man väljer kategori själv. Delning-chippet öppnar delningen med ett tryck. Spara i appfältet.', themes:LD, render:() => scrEditor(false) },
{ id:'S-7-editor-split', title:'S-7 Ny utgift · exakta belopp med fel', note:'Beloppet fälls ihop till en kompakt rad när man scrollar; raden visar också den automatiskt valda kategorin. Alla fyra lägen syns som valchips. Annas andel räknas ut och visas skrivskyddad; Spara är avstängd.', themes:LD, render:scrEditorSplit },
{ id:'S-7-editor-full', title:'S-7 Ändra utgift · allt på en person', note:'Transport valdes utifrån beskrivningen (SL-kort). Allt på Anna: personväljare som ButtonGroup, delningsstapeln helt i Annas färg, anteckningen utfälld, Ta bort i ändringsläget.', themes:LD, render:scrEditorFull },
{ id:'S-7-editor', title:'S-7 Ny utgift · 10 sekunder', note:'Egen siffertangentbordspanel i appen (beskrivningen använder systemtangentbordet). Kategoriraden visar de mest använda; Nöje är vald automatiskt utifrån beskrivningen (Systembolaget), utan markör; användaren kan alltid byta. Delning-chippet öppnar delningen med ett tryck. Spara i appfältet.', themes:LD, render:() => scrEditor(false) },
{ id:'S-7-editor-split', title:'S-7 Ny utgift · exakta belopp med fel', note:'Beloppet fälls ihop till en kompakt rad när man scrollar; raden visar också den valda kategorin. Alla fyra lägen syns som valchips. Annas andel räknas ut och visas skrivskyddad; Spara är avstängd.', themes:LD, render:scrEditorSplit },
{ id:'S-7-editor-full', title:'S-7 Ändra utgift · allt på en person', note:'Transport är vald utifrån beskrivningen (SL-kort). Allt på Anna: personväljare som ButtonGroup, delningsstapeln helt i Annas färg, anteckningen utfälld, Ta bort i ändringsläget.', themes:LD, render:scrEditorFull },
{ id:'S-7-editor-200', title:'S-7 Ny utgift · 200 %', note:'fontScale 2: beloppet och Spara syns, mellandelen scrollar, tangentbordet växer.', themes:L, fontScale:2, render:() => scrEditor(true) },
{ id:'S-8-expense', title:'S-8 Utgift ICA Maxi Lindhagen', note:'Kategorifärgen bär kortet. Historiken har sista raden "Din ändring väntar på synk" med ihålig punkt i pending-färg.', themes:LD, render:() => scrExpense('normal') },
{ id:'S-8-expense-recurring', title:'S-8 Utgift · återkommande', note:'Återkommande: Hyra som havregul etikett.', themes:LD, render:() => scrExpense('recurring') },
@@ -1113,15 +1115,16 @@ const FRAMES = [
{ id:'S-9-settle-account', title:'S-9 Gör upp · ett konto (Huset)', note:'Beloppet går att ändra (förifyllt med kontots skuld); raden är fast och följer beloppet. Ingen Lägg till konto.', themes:LD, render:() => scrSettle('account') },
{ id:'S-9-confirm-dialog', title:'S-9 Bekräfta-dialog', note:'Dialogen upprepar varningen (AC-11.5).', themes:LD, render:() => scrSettle('dialog') },
{ id:'S-10-settlement', title:'S-10 Betalning', note:'Avatar → avatar visar riktningen; orden säger den.', themes:LD, render:scrSettlement },
{ id:'S-11-analytics', title:'S-11 Statistik', note:'TalkBack: "Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr. Hem är störst i maj till augusti, Resor i september och oktober." Per kategori: "Hem 39,7 %, Mat & hushåll 20,6 %, Resor 18,3 %."', themes:LD, render:() => scrAnalytics('default') },
{ id:'S-11-analytics', title:'S-11 Statistik', note:'TalkBack: "Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr. Boende är störst i maj till augusti, Resor i september och oktober." Per kategori: "Boende 34,8 %, Mat & hushåll 20,6 %, Resor 18,3 %."', themes:LD, render:() => scrAnalytics('default') },
{ id:'S-11-analytics-scrolled', title:'S-11 Statistik · scrollad (extra)', note:'Extra ram, scrollad: slutet av diagram 2 och hela diagram 3. TalkBack per konto: "Huset 44,8 %, Vardag 37,6 %, Japan 2027 15,2 %, Gotland 2026 2,5 %."', themes:LD, render:() => scrAnalytics('scrolled') },
{ id:'S-11-analytics-week', title:'S-11 Statistik · vecka, min del, per konto', note:'TalkBack: "Min del per vecka: högst vecka 41, 4 300 kr; lägst vecka 42, 1 730 kr." Kontofärgerna är desamma som på Översikt.', themes:LD, render:() => scrAnalytics('week') },
{ id:'S-11-analytics-empty', title:'S-11 Statistik · tom period', note:'Anpassad period utan utgifter.', themes:LD, render:() => scrAnalytics('empty') },
{ id:'S-12-drilldown', title:'S-12 Mat & hushåll, oktober 2026', note:'Summa och rader som i S-5.', themes:LD, render:scrDrill },
{ id:'S-13-settings', title:'S-13 Inställningar', note:'Varje rad har en egen tonad ikonplatta, grupperade i segmenterade kort.', themes:LD, render:scrSettings },
{ id:'S-14-profile', title:'S-14 Profil', note:'', themes:LD, render:scrProfile },
{ id:'S-15-categories', title:'S-15 Kategorier', note:'Dra-handtag med TalkBack-etikett "Flytta <namn>"; Pant ligger under Arkiverade.', themes:LD, render:scrCategories },
{ id:'S-15-category-editor', title:'S-15 Ändra kategori', note:'Alla 30 ikoner och 12 färger. Standardkategorier kan arkiveras men inte tas bort.', themes:LD, render:scrCategoryEditor },
{ id:'S-15-categories', title:'S-15 Kategorier', note:'14 standardkategorier i sorteringsordning. Dra-handtag med TalkBack-etikett "Flytta <namn>". Listan scrollar; slutet med Arkiverade syns i nästa ram.', themes:LD, render:() => scrCategories(false) },
{ id:'S-15-categories-scrolled', title:'S-15 Kategorier · scrollad (extra)', note:'Extra ram: slutet av listan med Arkiverade (1) och den arkiverade egna kategorin Pant.', themes:LD, render:() => scrCategories(true) },
{ id:'S-15-category-editor', title:'S-15 Ändra kategori', note:'Alla 33 ikoner (nya: key, shield, account_balance) och 14 färger (nya: plum, sand). Standardkategorier kan arkiveras men inte tas bort.', themes:LD, render:scrCategoryEditor },
{ id:'S-16-sync', title:'S-16 Synkstatus', note:'', themes:LD, render:() => scrSync(false) },
{ id:'S-16-sync-clock', title:'S-16 Synkstatus · klockan går fel', note:'', themes:LD, render:() => scrSync(true) },
{ id:'S-17-update', title:'S-17 Uppdatering · tillåt installation', note:'Visas första gången, före systemets inställning.', themes:LD, render:scrUpdateDialog },
@@ -1206,7 +1209,7 @@ function reviewPage(){
<p>Contrast is computed in JavaScript from the same token object that themes the phones. Red marks text below 4.5:1 or graphics below 3:1. <code>owed</code> (sage green, partner owes you) and <code>owing</code> (dusty plum, you owe) are a purple/green pair, distinguishable in deuteranopia, protanopia and tritanopia, and always come with words. <code>settled</code> uses the straw/oat family. Extras beyond the brief's names: <code>--card</code> (card fill: white on off-white in light, surface-container in dark), <code>--on-cat</code> (icon colour on a solid category/chart tile), <code>--shadow</code>, <code>--photo-*</code> (Jacob's picture stand-in), <code>--sys-*</code> (neutral Android notification shade). Dark containers for straw/oat and warning are pushed toward brown (hue ≈ 32°) so they never read olive.</p>
${paletteTable()}
<h3>Category colours (§9.7 tokens)</h3>
<p>Muted, dusty versions of all twelve §9.7 tokens (token names unchanged). In Kalas the category colour is used <b>solid</b> as the tile fill with the icon in <code>--on-cat</code> (white in light, brown-black in dark), so every row still starts with a colour block, just a quieter one. The container (<code>--cat-*-container</code>) is the colour at 18 % over surface in light and 24 % in dark; it fills unselected picker tiles (icon in the category colour) and the expense-detail hero card. Category colours are never used as text. To keep muted tiles apart, lightness is spread on purpose (indigo, brown and purple darker; yellow, orange and lime lighter) and every pair is checked with CIEDE2000 below.</p>
<p>Muted, dusty versions of the twelve §9.7 tokens plus two proposed new tokens, <code>plum</code> (Försäkringar) and <code>sand</code> (Avgifter &amp; skatter), so each of the 14 default categories (owner decision 2026-10-18: new Boende/key/indigo, Försäkringar/shield/plum, Barn/child_care/lime, Avgifter &amp; skatter/account_balance/sand) has its own colour. The icon set grows to 33 with <code>key</code>, <code>shield</code> and <code>account_balance</code>. Light <code>brown</code> was darkened from #7A5F50 to #6B5244 to keep it apart from <code>sand</code>; the archived example category Pant moved from lime to teal so it no longer shares Barn's colour. In Kalas the category colour is used <b>solid</b> as the tile fill with the icon in <code>--on-cat</code> (white in light, brown-black in dark), so every row still starts with a colour block, just a quieter one. The container (<code>--cat-*-container</code>) is the colour at 18 % over surface in light and 24 % in dark; it fills unselected picker tiles (icon in the category colour) and the expense-detail hero card. Category colours are never used as text. To keep muted tiles apart, lightness is spread on purpose (indigo, brown and purple darker; yellow, orange and lime lighter) and every pair is checked with CIEDE2000 below.</p>
${deTable()}
${catTable()}
<h3>Chart colours</h3>