{"id":93,"date":"2025-11-10T16:12:40","date_gmt":"2025-11-10T16:12:40","guid":{"rendered":"https:\/\/hylledev.dk\/?p=93"},"modified":"2025-11-10T22:07:52","modified_gmt":"2025-11-10T22:07:52","slug":"dash1","status":"publish","type":"post","link":"https:\/\/hylledev.dk\/index.php\/2025\/11\/10\/dash1\/","title":{"rendered":"dash1"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"93\" class=\"elementor elementor-93\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-95494f0 elementor-section-full_width elementor-section-height-default elementor-section-height-default\" data-id=\"95494f0\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-4e9bac3\" data-id=\"4e9bac3\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-10ff66e elementor-widget elementor-widget-html\" data-id=\"10ff66e\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!doctype html>\n<html lang=\"da\" data-theme=\"light\">\n<head>\n<meta charset=\"utf-8\" \/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,viewport-fit=cover,maximum-scale=1\" \/>\n<title>SaveOps\u2122 \u2013 Mobil<\/title>\n<meta name=\"description\" content=\"SaveOps \u2013 mobil-first kommando-central for regninger, radar, trends, handlinger og viden.\" \/>\n<style>\n\/* ===== Tokens ===== *\/\n:root{\n  --bg:#ffffff; --ink:#0b1220; --muted:#667085; --grid:#eef2f7;\n  --brand:#0062FF; --save:#00C49A; --border:#e6eaf0; --card:#ffffff;\n  --ok:#166534; --warn:#915930; --bad:#b42318; --shadow:0 10px 30px rgba(16,24,40,.06);\n\n  \/* Layout vars (JS s\u00e6tter --navH dynamisk) *\/\n  --safe: env(safe-area-inset-bottom, 0px);\n  --navH: 72px;\n  --stickyH: 64px;\n}\nhtml[data-theme=\"dark\"]{\n  --bg:#0B0F19; --ink:#ffffff; --muted:#A1A1AA; --grid:#1E2534;\n  --brand:#4d86ff; --save:#00C49A; --border:#222833; --card:#0e1321;\n  --ok:#9ad0ff; --warn:#ffd699; --bad:#ff9a9a; --shadow:0 10px 30px rgba(0,0,0,.35);\n}\n*{box-sizing:border-box}\nbody{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Arial;background:var(--bg);color:var(--ink);line-height:1.55;-webkit-font-smoothing:antialiased}\n.wrap{max-width:860px;margin:0 auto;padding:0 14px}\nh1,h2{margin:0 0 8px}\nh1{font-size:clamp(18px,5vw,22px);font-weight:900;letter-spacing:-.02em}\nh2{font-size:16px;font-weight:900}\n.small{font-size:12px;color:var(--muted)}\n.mono{font-variant-numeric:tabular-nums}\n\n\/* ===== Topbar ===== *\/\n.top{position:sticky;top:0;z-index:40;border-bottom:1px solid var(--border);\n  backdrop-filter:saturate(140%) blur(8px);\n  background:color-mix(in oklab,var(--bg) 90%, transparent)}\n.top .in{height:52px;display:flex;align-items:center;justify-content:space-between;gap:8px}\n.brand{font-weight:900}\n.iconbtn{border:1px solid var(--border);background:var(--card);border-radius:12px;padding:8px 10px;font-weight:800}\n\n\/* ===== Hero ===== *\/\n.hero{position:relative;padding:12px 0 8px;overflow:hidden;border-bottom:1px solid var(--border)}\n.hero:before{content:\"\";position:absolute;inset:0;opacity:.8;background-image:\n  linear-gradient(to right,var(--grid) 1px,transparent 1px),\n  linear-gradient(to bottom,var(--grid) 1px,transparent 1px);\n  background-size:36px 36px}\n.hero:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:\n  radial-gradient(600px 280px at 70% 60%, color-mix(in oklab,var(--save) 16%, transparent), transparent 60%),\n  radial-gradient(500px 240px at 20% 20%, color-mix(in oklab,var(--brand) 14%, transparent), transparent 60%)}\n.hero .in{position:relative;z-index:1}\n\n\/* ===== KPI ===== *\/\n.kpis{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}\n.kpi{border:1px solid var(--border);border-radius:12px;background:var(--card);padding:10px}\n.kpi .label{color:var(--muted);font-size:11px}\n.kpi .value{font-weight:900}\n\n\/* ===== Screens (plads til bundnav\/sticky via vars) ===== *\/\n.screen{display:none;padding:12px 0 calc(var(--navH) + var(--safe) + 12px)}\nbody.has-sticky .screen{padding-bottom:calc(var(--navH) + var(--stickyH) + var(--safe) + 18px)}\n.screen.active{display:block}\n\n\/* ===== Cards \/ Lists ===== *\/\n.card{border:1px solid var(--border);border-radius:14px;background:var(--card);padding:12px;box-shadow:var(--shadow)}\n.row{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;border:1px solid var(--border);border-radius:12px;background:var(--card);padding:10px}\n.badge{border:1px solid var(--border);border-radius:999px;padding:4px 8px;font-size:11px}\n.badge.save{border-color:color-mix(in oklab,var(--save) 34%, var(--border));background:color-mix(in oklab,var(--save) 10%, var(--card));color:color-mix(in oklab,var(--save) 70%, var(--ink));font-weight:800}\n.chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:999px;padding:3px 8px;font-size:11px}\n.st-ok{color:var(--ok);font-weight:800}\n.st-warn{color:var(--warn);font-weight:800}\n.st-bad{color:var(--bad);font-weight:800}\n.btn{display:inline-block;padding:10px 12px;border-radius:12px;font-weight:800;text-decoration:none}\n.btn.primary{background:var(--ink);color:var(--bg)}\n.btn.ghost{border:1px solid var(--border);background:var(--card);color:var(--ink)}\n.list{display:grid;gap:8px}\n.inline{display:flex;gap:8px;flex-wrap:wrap}\n.spark{display:block;width:100%;height:48px}\n\n\/* ===== Sticky CTA (ligger ALTID over nav-h\u00f8jde) ===== *\/\n.sticky{position:fixed;left:0;right:0;bottom:calc(var(--navH) + var(--safe) + 8px);z-index:48;display:none}\n.sticky .bar{max-width:860px;margin:0 auto;padding:0 14px}\n.sticky .in{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--border);background:var(--card);border-radius:14px;padding:10px;box-shadow:var(--shadow)}\n.sticky.show{display:block}\n\n\/* ===== Bottom nav (chip-look + safe-area) ===== *\/\n.nav{\n  position:fixed;left:0;right:0;bottom:0;z-index:50;\n  background:color-mix(in oklab,var(--card) 92%, transparent);\n  backdrop-filter:saturate(140%) blur(8px);\n  border-top:1px solid var(--border);\n  padding-bottom:var(--safe);\n}\n.nav .in{max-width:860px;margin:0 auto;padding:10px 14px;display:grid;grid-template-columns:repeat(5,1fr);gap:8px}\n.tabbtn{display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 12px;font-size:11px;line-height:1;border:1px solid var(--border);border-radius:12px;background:var(--card);color:var(--ink);box-shadow:var(--shadow);transition:.18s ease}\n.tabbtn:active{transform:translateY(1px)}\n.tabbtn.active{outline:2px solid color-mix(in oklab,var(--brand) 28%, transparent)}\n.tabbtn .ico{width:18px;height:18px;display:inline-block;opacity:.9}\n\n\/* ===== FAB (+) \u2013 hopper n\u00e5r sticky er synlig ===== *\/\n.fab{\n  position:fixed;right:14px;bottom:calc(var(--navH) + var(--safe) + 16px);z-index:60;\n  width:48px;height:48px;border-radius:999px;border:1px solid var(--border);\n  background:var(--ink);color:var(--bg);font-weight:900;font-size:22px;box-shadow:var(--shadow)\n}\nhtml[data-theme=\"dark\"] .fab{background:var(--card);color:var(--ink)}\nbody.has-sticky .fab{bottom:calc(var(--navH) + var(--stickyH) + var(--safe) + 20px)}\n\n\/* Kompakt p\u00e5 sm\u00e5 sk\u00e6rme *\/\n@media (max-width:360px){\n  .tabbtn{padding:8px 10px;font-size:10px;gap:4px}\n  .tabbtn .ico{width:16px;height:16px}\n}\n\n\/* Desktop *\/\n@media (min-width:900px){\n  h1{font-size:24px}\n  .kpis{grid-template-columns:repeat(4,1fr)}\n}\n\/* Fremh\u00e6v besparelses-KPI *\/\n.kpi.save {\n  position: relative;\n  background: color-mix(in oklab, var(--save) 12%, var(--card));\n  border-color: color-mix(in oklab, var(--save) 38%, var(--border));\n  box-shadow: 0 8px 22px color-mix(in oklab, var(--save) 20%, transparent), var(--shadow);\n}\n.kpi.save .label { color: color-mix(in oklab, var(--save) 60%, var(--ink)); font-weight: 700 }\n.kpi.save .value { font-weight: 900 }\n\n\/* Diskret \u201cscan\u201d\/gl\u00f8d hen over kortet *\/\n.kpi.save::after{\n  content:\"\"; position:absolute; inset:-40% -20% auto -20%; height:140%;\n  background: linear-gradient(90deg, transparent,\n    color-mix(in oklab, var(--save) 18%, transparent), transparent);\n  transform: skewX(-20deg);\n  animation: saveSweep 3.6s linear infinite;\n  pointer-events:none;\n}\n@keyframes saveSweep { from{ left:-60% } to{ left:160% } }\n\n<\/style>\n<\/head>\n<body>\n<header class=\"top\">\n  <div class=\"wrap in\">\n    <div class=\"brand\">SaveOps\u2122<\/div>\n    <button id=\"themeToggle\" class=\"iconbtn\" aria-pressed=\"false\" title=\"Skift tema\">\u2600\ufe0f\/\ud83c\udf19<\/button>\n  <\/div>\n<\/header>\n\n<section class=\"hero\">\n  <div class=\"wrap in\">\n    <h1 id=\"title\">Kommando-central<\/h1>\n    <div class=\"small\" id=\"subtitle\">Mobil overblik \u00b7 SaveScore\u2122 \u00b7 Hurtige gevinster<\/div>\n    <div class=\"kpis\" id=\"kpis\"><!-- js --><\/div>\n  <\/div>\n<\/section>\n\n<main id=\"screens\" class=\"wrap\">\n  <!-- Overblik -->\n  <section id=\"s-overblik\" class=\"screen active\" role=\"region\" aria-label=\"Overblik\">\n    <div class=\"inline\" style=\"margin-bottom:8px\">\n      <select id=\"filter\" class=\"chip\" aria-label=\"Filtrer kategori\">\n        <option value=\"alle\">Alle<\/option><option value=\"el\">El<\/option><option value=\"mobil\">Mobil<\/option>\n        <option value=\"internet\">Internet<\/option><option value=\"streaming\">Streaming<\/option><option value=\"forsikring\">Forsikring<\/option>\n      <\/select>\n      <select id=\"sort\" class=\"chip\" aria-label=\"Sorter\">\n        <option value=\"due\">Forfald<\/option><option value=\"amount\">Bel\u00f8b<\/option><option value=\"name\">Navn<\/option>\n      <\/select>\n    <\/div>\n    <div id=\"billList\" class=\"list\"><!-- js --><\/div>\n  <\/section>\n\n  <!-- Radar -->\n  <section id=\"s-radar\" class=\"screen\" role=\"region\" aria-label=\"Hidden Fees Radar\">\n    <div class=\"list\" id=\"radarList\"><!-- js --><\/div>\n  <\/section>\n\n  <!-- Trends -->\n  <section id=\"s-trends\" class=\"screen\" role=\"region\" aria-label=\"Trends\">\n    <div id=\"trendList\" class=\"list\"><!-- js --><\/div>\n  <\/section>\n\n  <!-- Handlinger -->\n  <section id=\"s-handlinger\" class=\"screen\" role=\"region\" aria-label=\"Anbefalinger\">\n    <div id=\"recList\" class=\"list\"><!-- js --><\/div>\n  <\/section>\n\n  <!-- Viden -->\n  <section id=\"s-viden\" class=\"screen\" role=\"region\" aria-label=\"Viden\">\n    <div id=\"knowList\" class=\"list\"><!-- js --><\/div>\n  <\/section>\n\n  <!-- Tilf\u00f8j (\u00e5bnes via FAB) -->\n  <section id=\"s-tilfoej\" class=\"screen\" role=\"region\" aria-label=\"Tilf\u00f8j regning\">\n    <div class=\"card\">\n      <h2>Tilf\u00f8j regning<\/h2>\n      <div class=\"small\">Upload billede\/PDF eller inds\u00e6t tekst \u2192 Scan \u2192 Ret til \u2192 Gem<\/div>\n      <div class=\"form\" style=\"margin-top:8px\">\n        <div class=\"input\">\n          <label for=\"file\">Upload (foto\/PDF)<\/label>\n          <input type=\"file\" id=\"file\" accept=\"image\/*,.pdf\" \/>\n          <div class=\"preview\" id=\"filePreview\" style=\"display:none\">\n            <img id=\"thumb\" alt=\"preview\" \/><div class=\"small\" id=\"fileName\">\u2014<\/div>\n          <\/div>\n          <div class=\"help\">Tip: Foto af regning eller direkte PDF fra e-mail.<\/div>\n        <\/div>\n\n        <div class=\"input\">\n          <label for=\"raw\">Eller inds\u00e6t tekst fra regning<\/label>\n          <textarea id=\"raw\" placeholder=\"Inds\u00e6t tekst\u2026 fx:\nAndel Energi\nBel\u00f8b: 2.150,81 kr\nForfald: 25-11-2025\nAftale: Variabel timepris\"><\/textarea>\n          <div><button class=\"btn ghost\" id=\"scanBtn\">Scan tekst<\/button><\/div>\n        <\/div>\n\n        <div class=\"input\">\n          <label for=\"name\">Udbyder \/ navn<\/label>\n          <input id=\"name\" placeholder=\"fx Andel \u2013 Variabel timepris\" \/>\n        <\/div>\n\n        <div class=\"input\">\n          <label for=\"category\">Kategori<\/label>\n          <select id=\"category\">\n            <option value=\"el\">El<\/option><option value=\"mobil\">Mobil<\/option><option value=\"internet\">Internet<\/option>\n            <option value=\"streaming\">Streaming<\/option><option value=\"forsikring\">Forsikring<\/option>\n          <\/select>\n        <\/div>\n\n        <div class=\"input\">\n          <label for=\"amount\">Bel\u00f8b pr. m\u00e5ned (kr)<\/label>\n          <input id=\"amount\" type=\"number\" inputmode=\"decimal\" placeholder=\"fx 349\" \/>\n        <\/div>\n\n        <div class=\"input\">\n          <label for=\"due\">N\u00e6ste forfald<\/label>\n          <input id=\"due\" type=\"date\" \/>\n        <\/div>\n\n        <div class=\"input\">\n          <label for=\"status\">Status<\/label>\n          <select id=\"status\">\n            <option value=\"ok\">OK<\/option><option value=\"warn\">WARN<\/option><option value=\"bad\">BAD<\/option>\n          <\/select>\n        <\/div>\n\n        <div class=\"inline\">\n          <button class=\"btn primary\" id=\"saveBill\">Gem regning<\/button>\n          <button class=\"btn ghost\" id=\"resetForm\" type=\"button\">Nulstil<\/button>\n        <\/div>\n\n        <div class=\"small\" id=\"scanNote\" style=\"margin-top:4px;color:var(--muted)\"><\/div>\n      <\/div>\n    <\/div>\n  <\/section>\n<\/main>\n\n<!-- Sticky next action -->\n<div id=\"sticky\" class=\"sticky\" aria-hidden=\"true\">\n  <div class=\"bar\"><div class=\"in\">\n    <div class=\"mono\" id=\"stickyText\">N\u00e6ste: \u2014<\/div>\n    <a href=\"#\" id=\"stickyDo\" class=\"btn primary\">G\u00f8r det nu<\/a>\n  <\/div><\/div>\n<\/div>\n\n<!-- Bottom nav -->\n<nav class=\"nav\" role=\"tablist\" aria-label=\"Navigation\">\n  <div class=\"in\">\n    <button class=\"tabbtn active\" data-tab=\"overblik\" aria-controls=\"s-overblik\" aria-current=\"page\"><span class=\"ico\">\ud83c\udfe0<\/span><span>Overblik<\/span><\/button>\n    <button class=\"tabbtn\" data-tab=\"radar\" aria-controls=\"s-radar\"><span class=\"ico\">\u26a1<\/span><span>Radar<\/span><\/button>\n    <button class=\"tabbtn\" data-tab=\"trends\" aria-controls=\"s-trends\"><span class=\"ico\">\ud83d\udcc8<\/span><span>Trends<\/span><\/button>\n    <button class=\"tabbtn\" data-tab=\"handlinger\" aria-controls=\"s-handlinger\"><span class=\"ico\">\u2705<\/span><span>Handlinger<\/span><\/button>\n    <button class=\"tabbtn\" data-tab=\"viden\" aria-controls=\"s-viden\"><span class=\"ico\">\ud83d\udcda<\/span><span>Viden<\/span><\/button>\n  <\/div>\n<\/nav>\n\n<!-- FAB: Tilf\u00f8j -->\n<button id=\"fabAdd\" class=\"fab\" aria-label=\"Tilf\u00f8j regning\">\uff0b<\/button>\n\n<script>\n\/* ===== Utils ===== *\/\nconst $ = (id)=>document.getElementById(id);\nconst byQS=(s,r=document)=>r.querySelector(s);\nconst byQSA=(s,r=document)=>[...r.querySelectorAll(s)];\nconst uid=()=>Math.random().toString(36).slice(2,9);\nfunction kr(n, cur='kr'){ if(n==null||isNaN(n)) return '\u2014'; return Math.round(n).toLocaleString('da-DK') + ' ' + cur }\nfunction d2(d){ const t=new Date(d); return t.toLocaleDateString('da-DK',{day:'2-digit',month:'2-digit'}) }\nfunction statusClass(s){ return s==='ok'?'st-ok':(s==='warn'?'st-warn':'st-bad') }\nfunction spark(points){ const w=100,h=48,p=4,min=Math.min(...points),max=Math.max(...points),rng=max-min||1,step=(w-2*p)\/(points.length-1);\n  const d=points.map((v,i)=>`${i?'L':'M'}${(p+i*step).toFixed(2)},${(h-p-((v-min)\/rng)*(h-2*p)).toFixed(2)}`).join(' ');\n  return `<svg class=\"spark\" viewBox=\"0 0 ${w} ${h}\" preserveAspectRatio=\"none\"><path d=\"${d}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><\/svg>`;}\n\n\/* ===== Nav height \u2192 CSS var ===== *\/\nfunction setNavHeightVar(){\n  const nav = document.querySelector('.nav');\n  const h = nav ? nav.offsetHeight : 72;\n  document.documentElement.style.setProperty('--navH', h + 'px');\n}\nwindow.addEventListener('load', setNavHeightVar);\nwindow.addEventListener('resize', setNavHeightVar);\n\n\/* ===== Theme toggle ===== *\/\n(function initTheme(){\n  const html=document.documentElement, btn=$('themeToggle');\n  const saved=localStorage.getItem('saveops_theme');\n  html.setAttribute('data-theme', saved || 'light');\n  btn.addEventListener('click', ()=>{\n    const next = html.getAttribute('data-theme')==='dark' ? 'light':'dark';\n    html.setAttribute('data-theme', next);\n    localStorage.setItem('saveops_theme', next);\n    setNavHeightVar(); \/\/ iOS kan \u00e6ndre h\u00f8jde pga. safe-area\n  });\n})();\n\n\/* ===== Demo data ===== *\/\nconst CTX = {\n  currency:'kr',\n  kpis:[\n    {label:'Faste udgifter (md)', value:7268},\n    {label:'Kommende forfald (30 dage)', value:4998},\n    {label:'\u00c5rlig besparelse (est.)', value:4380},\n    {label:'SaveScore\u2122', value:'66 \/ 100'},\n  ],\n  bills:[\n    {id:'el', cat:'el', label:'El', name:'Andel \u2013 Variabel timepris', amount:2150, due:'2025-11-25', status:'warn'},\n    {id:'mob1', cat:'mobil', label:'Mobil', name:'Telenor \u2013 80 GB', amount:199, due:'2025-12-02', status:'warn'},\n    {id:'mob2', cat:'mobil', label:'Mobil', name:'CBB \u2013 20 GB (barn)', amount:99, due:'2025-11-29', status:'ok'},\n    {id:'net', cat:'internet', label:'Internet', name:'YouSee \u2013 Fiber 1000\/100', amount:349, due:'2025-11-28', status:'ok'},\n    {id:'str1', cat:'streaming', label:'Streaming', name:'Netflix \u2013 Standard', amount:119, due:'2025-11-18', status:'warn'},\n    {id:'str2', cat:'streaming', label:'Streaming', name:'Disney+ \u2013 Standard', amount:89, due:'2025-11-21', status:'ok'},\n    {id:'str3', cat:'streaming', label:'Streaming', name:'TV 2 Play \u2013 Basis (ads)', amount:79, due:'2025-11-23', status:'ok'},\n    {id:'str4', cat:'streaming', label:'Streaming', name:'Spotify Family', amount:179, due:'2025-11-18', status:'ok'},\n    {id:'ins1', cat:'forsikring', label:'Forsikring', name:'Tryg \u2013 Indbo', amount:189, due:'2025-12-01', status:'ok'},\n    {id:'ins2', cat:'forsikring', label:'Forsikring', name:'Tryg \u2013 Ulykke', amount:129, due:'2025-11-30', status:'ok'},\n  ],\n  radar:[\n    {tag:'Betaling', title:'PBS-gebyr 10 kr\/md (el)', text:'Skift til MobilePay (ofte 0 kr).', cost:{type:'per_month', value:10}},\n    {tag:'Service',  title:'\u201cServiceaftale\u201d 29 kr\/md (el)', text:'Frav\u00e6lg\/forhandl till\u00e6g.', cost:{type:'per_month', value:29}},\n    {tag:'Papir',    title:'Papirfaktura 39 kr\/md (forsikring)', text:'V\u00e6lg e-mail\/e-Boks.', cost:{type:'per_month', value:39}},\n    {tag:'Router',   title:'Leje af router 49 kr\/md', text:'K\u00f8b egen \u2013 betalt < 12 mdr.', cost:{type:'per_month', value:49}},\n  ],\n  trends:[\n    {label:'El (md)', points:[2150,2210,1980,2090,2312,2250], avg:2165, delta:+4},\n    {label:'Mobil (md)', points:[298,298,298,298,298,298], avg:298, delta:0},\n    {label:'Internet (md)', points:[349,349,349,349,349,349], avg:349, delta:0},\n    {label:'Streaming (md)', points:[396,436,436,466,466,466], avg:444, delta:+2},\n  ],\n  recs:[\n    {id:'r1', title:'Fjern PBS + serviceaftale (el)', saveYear:468, effort:'Lav', winrate:85,\n     steps:['Log ind hos elselskab','Skift til MobilePay','Frav\u00e6lg serviceaftale','Gem kvittering']},\n    {id:'r2', title:'Skift mobil til lavpris 60\u201380 GB', saveYear:600, effort:'Middel', winrate:70,\n     steps:['V\u00e6lg lavprisudbyder','Port\u00e9r nummer','Opsig add-ons']},\n    {id:'r3', title:'Streaming: behold 2, pause 2', saveYear:960, effort:'Lav', winrate:75,\n     steps:['Se forbrug','Opsig\/pause 2 tjenester','S\u00e6t p\u00e5mindelse']},\n  ],\n  know:[\n    {title:'PBS vs. MobilePay', text:'PBS 8\u201312 kr\/md pr. regning. MobilePay ofte 0 kr.'},\n    {title:'Roterende streaming', text:'Hold 1\u20132 aktive. Pause resten.'},\n    {title:'Routerleje', text:'49 kr\/md = 588 kr\/\u00e5r. Egen router betaler sig typisk < 12 mdr.'},\n    {title:'El: fast vs. variabel', text:'Fastpris giver ro, men pas p\u00e5 prishop efter binding.'},\n  ]\n};\n\n\/* ===== Renderers ===== *\/\nfunction paintKPIs(){\n  $('kpis').innerHTML = CTX.kpis.map(k=>{\n    const isSave = \/besparelse\/i.test(k.label); \/\/ fx \"\u00c5rlig besparelse (est.)\"\n    const val = typeof k.value==='number' ? kr(k.value) : k.value;\n    return `\n      <div class=\"kpi${isSave ? ' save' : ''}\" aria-label=\"${k.label}\">\n        <div class=\"label\">${k.label}<\/div>\n        <div class=\"value mono\">${val}<\/div>\n      <\/div>`;\n  }).join('');\n}\n\n\nlet filter='alle', sortBy='due';\nfunction paintBills(){\n  let list=[...CTX.bills];\n  if(filter!=='alle') list=list.filter(x=>x.cat===filter);\n  if(sortBy==='due')   list.sort((a,b)=>new Date(a.due)-new Date(b.due));\n  if(sortBy==='amount')list.sort((a,b)=>b.amount-a.amount);\n  if(sortBy==='name')  list.sort((a,b)=>a.name.localeCompare(b.name));\n  $('billList').innerHTML = list.map(b=>`\n    <div class=\"row\">\n      <div>\n        <div class=\"inline\">\n          <span class=\"chip\">${b.label}<\/span>\n          <span class=\"${statusClass(b.status)}\">${b.status.toUpperCase()}<\/span>\n        <\/div>\n        <div style=\"margin-top:2px\"><strong>${b.name}<\/strong><\/div>\n        <div class=\"small\">Forfald: ${d2(b.due)}<\/div>\n      <\/div>\n      <div style=\"text-align:right\">\n        <div class=\"mono\" style=\"font-weight:900\">${kr(b.amount)}\/md<\/div>\n        <a href=\"#\" class=\"btn ghost\" data-bill=\"${b.id}\" style=\"margin-top:6px;display:inline-block\">Detaljer<\/a>\n      <\/div>\n    <\/div>`).join('');\n}\n\nfunction annualize(f){ const c=f.cost||{}, cur='kr'; if(c.type==='per_month') return `${(c.value*12).toLocaleString('da-DK')} ${cur}\/\u00e5r`; return '\u2014'; }\nfunction paintRadar(){ $('radarList').innerHTML = CTX.radar.map(f=>`\n  <div class=\"card\"><div class=\"inline\"><span class=\"badge save\">\u26a1 Fundet gebyr<\/span><span class=\"badge\">${f.tag}<\/span><\/div>\n  <div style=\"margin-top:6px\"><strong>${f.title}<\/strong><\/div><div class=\"small\">${f.text}<\/div>\n  <div class=\"small\" style=\"margin-top:6px\">\u2248 ${annualize(f)}<\/div><\/div>`).join(''); }\nfunction paintTrends(){ $('trendList').innerHTML = CTX.trends.map(t=>`\n  <div class=\"card\"><div class=\"inline\" style=\"justify-content:space-between\"><strong>${t.label}<\/strong><span class=\"small mono\">${t.delta>=0?'+':''}${t.delta}%<\/span><\/div>\n  ${spark(t.points)}<div class=\"small\">Gns: <span class=\"mono\">${kr(t.avg)}<\/span> \u00b7 Seneste: <span class=\"mono\">${kr(t.points.at(-1))}<\/span><\/div><\/div>`).join(''); }\n\n\/* Sticky: central styring *\/\nfunction setStickyState(show){\n  const el = $('sticky');\n  if(!el) return;\n  if(show){\n    el.classList.add('show'); el.setAttribute('aria-hidden','false');\n    document.body.classList.add('has-sticky');\n  }else{\n    el.classList.remove('show'); el.setAttribute('aria-hidden','true');\n    document.body.classList.remove('has-sticky');\n  }\n}\nfunction paintRecs(){\n  $('recList').innerHTML = CTX.recs.map(r=>`\n    <div class=\"card\">\n      <div class=\"inline\" style=\"justify-content:space-between;align-items:center\">\n        <strong>${r.title}<\/strong><span class=\"badge save\">Spar \u2248 ${kr(r.saveYear)}\/\u00e5r<\/span>\n      <\/div>\n      <div class=\"small\" style=\"margin:6px 0\">Indsats: <strong>${r.effort}<\/strong> \u00b7 Succes: <strong>${r.winrate}%<\/strong><\/div>\n      <div class=\"inline\">\n        <a href=\"#\" class=\"btn primary\" data-rec=\"${r.id}\">G\u00f8r det nu<\/a>\n        <a href=\"#\" class=\"btn ghost\" data-why=\"${r.id}\">Hvorfor?<\/a>\n      <\/div>\n    <\/div>`).join('');\n\n  const first = CTX.recs[0];\n  if(first){\n    $('stickyText').textContent = `N\u00e6ste: ${first.title}`;\n    $('stickyDo').setAttribute('data-rec', first.id);\n    setStickyState(true);\n  } else {\n    setStickyState(false);\n  }\n}\nfunction paintKnow(){ $('knowList').innerHTML = CTX.know.map(k=>`<div class=\"card\"><strong>${k.title}<\/strong><div class=\"small\">${k.text}<\/div><\/div>`).join(''); }\n\n\/* ===== Events ===== *\/\n$('filter').addEventListener('change', e=>{ filter=e.target.value; paintBills(); });\n$('sort').addEventListener('change', e=>{ sortBy=e.target.value; paintBills(); });\nbyQS('#s-overblik').addEventListener('click', e=>{\n  const a=e.target.closest('[data-bill]'); if(!a) return; e.preventDefault();\n  const b=CTX.bills.find(x=>x.id===a.dataset.bill); if(!b) return;\n  alert(`${b.name}\\nKategori: ${b.label}\\nBel\u00f8b: ${kr(b.amount)}\/md\\nForfald: ${d2(b.due)}\\nStatus: ${b.status.toUpperCase()}`);\n});\nbyQS('#s-handlinger').addEventListener('click', e=>{\n  const go=e.target.closest('[data-rec]'); if(go){ e.preventDefault(); openRec(go.dataset.rec); }\n  const why=e.target.closest('[data-why]'); if(why){ e.preventDefault(); showWhy(why.dataset.why); }\n});\n$('stickyDo').addEventListener('click', e=>{ const id=e.target.getAttribute('data-rec'); if(id){ e.preventDefault(); openRec(id); }});\nfunction openRec(id){ const r=CTX.recs.find(x=>x.id===id); if(!r) return; alert(`Trin-for-trin:\\n\u2022 ${r.steps.join('\\n\u2022 ')}`); }\nfunction showWhy(id){ const r=CTX.recs.find(x=>x.id===id); if(!r) return; alert(`Hvorfor:\\n\u2022 Besparelse: \u2248 ${kr(r.saveYear)}\/\u00e5r\\n\u2022 Succes-rate: ${r.winrate}%`); }\n\n\/* Tabs (nav) *\/\nbyQSA('.tabbtn').forEach(b=>{\n  b.addEventListener('click', ()=>{\n    byQSA('.tabbtn').forEach(x=>{ x.classList.remove('active'); x.removeAttribute('aria-current'); });\n    b.classList.add('active'); b.setAttribute('aria-current','page');\n    const tab=b.dataset.tab;\n    byQSA('.screen').forEach(s=>s.classList.remove('active'));\n    byQS('#s-'+tab).classList.add('active');\n    \/\/ Sticky er aktiv p\u00e5 normale faner\n    setStickyState(true);\n    window.scrollTo({top:0,behavior:'instant'});\n  });\n});\n\n\/* Sticky fade p\u00e5 scroll (uden at sl\u00e5 den helt fra) *\/\nwindow.addEventListener('scroll', ()=>{\n  const y = window.scrollY || document.documentElement.scrollTop;\n  const el=$('sticky'); if(!el) return;\n  el.style.opacity = y>220 ? 1 : 0;\n  el.style.pointerEvents = y>220 ? 'auto' : 'none';\n});\n\n\/* FAB \u2192 Tilf\u00f8j *\/\n$('fabAdd').addEventListener('click', ()=>{\n  byQSA('.screen').forEach(s=>s.classList.remove('active'));\n  byQS('#s-tilfoej').classList.add('active');\n  byQSA('.tabbtn').forEach(x=>{ x.classList.remove('active'); x.removeAttribute('aria-current'); });\n  setStickyState(false); \/\/ ingen sticky i Tilf\u00f8j-flow\n  window.scrollTo({top:0,behavior:'instant'});\n});\n\n\/* Skjul sticky n\u00e5r keyboard er \u00e5bent (inputs i fokus) *\/\ndocument.addEventListener('focusin', e=>{\n  if(e.target.matches('input, textarea, select')) setStickyState(false);\n});\ndocument.addEventListener('focusout', e=>{\n  const addOpen = byQS('#s-tilfoej').classList.contains('active');\n  if(!addOpen) setStickyState(true);\n});\n\n\/* ===== Tilf\u00f8j: upload + (mock) scan + gem ===== *\/\nconst fileInput=$('file'), filePreview=$('filePreview'), thumb=$('thumb'), fileName=$('fileName'), raw=$('raw'),\n      scanBtn=$('scanBtn'), saveBtn=$('saveBill'), resetBtn=$('resetForm'),\n      fName=$('name'), fCat=$('category'), fAmount=$('amount'), fDue=$('due'), fStatus=$('status'), scanNote=$('scanNote');\n\nfunction guessCategory(text){\n  const s=(text||'').toLowerCase();\n  if(\/andel|norlys|nrgi|ok el|ewii|energi|nettarif|kwh\/.test(s)) return 'el';\n  if(\/telenor|telia|cbb| oister|yousee mobil|3 |3\\-|tre \/.test(s)) return 'mobil';\n  if(\/fiber|router|coax|yousee|stofa|waoo|internet|bredb\u00e5nd\/.test(s)) return 'internet';\n  if(\/netflix|disney\\+|hbo|max|viaplay|tv ?2|spotify|youtube premium|apple tv\\+\/.test(s)) return 'streaming';\n  if(\/tryg|gjensidige|alm.?brand|topdanmark|if forsikring\/.test(s)) return 'forsikring';\n  return 'el';\n}\nfunction parseFrom(text){\n  let nameLine=(text.split('\\n').map(s=>s.trim()).filter(Boolean)[0]||'').slice(0,80);\n  const amountMatch=text.match(\/(\\d{1,3}(?:[.\\s]\\d{3})*|\\d+)[,\\.]\\d{2}\\s*kr\/i)||text.match(\/bel[o\u00f8]b[:\\s]*([0-9]+)\/i);\n  const amount = amountMatch ? Number((amountMatch[1]||amountMatch[0]).replace(\/[^\\d,\\.]\/g,'').replace(\/\\.\/g,'').replace(',','.')) : null;\n  const dateMatch = text.match(\/(\\d{1,2})[-\\.\\\/](\\d{1,2})[-\\.\\\/](\\d{2,4})\/);\n  let due=null;\n  if(dateMatch){\n    const [_,d,m,y]=dateMatch; const yyyy = (String(y).length===2?('20'+y):y);\n    const mm = String(m).padStart(2,'0'); const dd = String(d).padStart(2,'0'); \n    due = `${yyyy}-${mm}-${dd}`;\n  }\n  return {name:nameLine||'', amount:amount?Math.round(amount):null, due, cat:guessCategory(text)};\n}\nfileInput.addEventListener('change', ()=>{\n  const f=fileInput.files?.[0]; if(!f){ filePreview.style.display='none'; return; }\n  fileName.textContent = f.name + ` (${Math.round(f.size\/1024)} KB)`;\n  filePreview.style.display='flex';\n  if(f.type.startsWith('image\/')){\n    const r=new FileReader(); r.onload=()=>{ thumb.src=r.result; }; r.readAsDataURL(f);\n  } else {\n    thumb.src='data:image\/svg+xml;utf8,<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"56\" height=\"56\"><rect width=\"56\" height=\"56\" rx=\"8\" fill=\"%23eee\"\/><text x=\"50%\" y=\"54%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-family=\"Arial\" font-size=\"10\" fill=\"%23999\">PDF<\/text><\/svg>';\n  }\n});\nscanBtn.addEventListener('click', (e)=>{\n  e.preventDefault();\n  const text = raw.value.trim();\n  if(!text){ scanNote.textContent='Inds\u00e6t tekst f\u00f8rst.'; return; }\n  const {name, amount, due, cat} = parseFrom(text);\n  if(name) fName.value = name;\n  if(amount!=null) fAmount.value = amount;\n  if(due) fDue.value = due;\n  fCat.value = cat;\n  scanNote.textContent = `Scannet: kategori=${cat}, bel\u00f8b=${amount!=null?amount+' kr':'ukendt'}, forfald=${due||'ukendt'}`;\n});\nsaveBtn.addEventListener('click', (e)=>{\n  e.preventDefault();\n  const name=fName.value.trim();\n  const amount=Number(fAmount.value);\n  const due=fDue.value;\n  const cat=fCat.value;\n  const status=fStatus.value||'ok';\n  if(!name || !amount || !due){ alert('Udfyld navn, bel\u00f8b og forfald.'); return; }\n  const label = ({el:'El', mobil:'Mobil', internet:'Internet', streaming:'Streaming', forsikring:'Forsikring'})[cat]||'Andet';\n  CTX.bills.push({id:uid(), cat, label, name, amount, due, status});\n  const k=CTX.kpis.find(x=>x.label.includes('Faste udgifter')); if(k && typeof k.value==='number'){ k.value += amount; }\n  resetFormFields();\n  paintKPIs(); paintBills();\n  \/\/ tilbage til Overblik\n  byQSA('.tabbtn').forEach(x=>{ x.classList.remove('active'); x.removeAttribute('aria-current'); });\n  const obBtn = byQS('.tabbtn[data-tab=\"overblik\"]'); if(obBtn){ obBtn.classList.add('active'); obBtn.setAttribute('aria-current','page'); }\n  byQSA('.screen').forEach(s=>s.classList.remove('active')); byQS('#s-overblik').classList.add('active');\n  setStickyState(true);\n  window.scrollTo({top:0,behavior:'instant'});\n  alert('Regning gemt og tilf\u00f8jet til Overblik.');\n});\nresetBtn.addEventListener('click', (e)=>{ e.preventDefault(); resetFormFields(); });\nfunction resetFormFields(){\n  $('file').value=''; $('filePreview').style.display='none'; $('thumb').removeAttribute('src'); $('fileName').textContent='\u2014';\n  $('raw').value=''; $('name').value=''; $('amount').value=''; $('due').value=''; $('category').value='el'; $('status').value='ok'; $('scanNote').textContent='';\n}\n\n\/* ===== Boot ===== *\/\n(function init(){\n  $('title').textContent = 'Kommando-central';\n  $('subtitle').textContent = 'Mobil overblik \u00b7 SaveScore\u2122 \u00b7 Hurtige gevinster';\n  paintKPIs(); paintBills(); paintRadar(); paintTrends(); paintRecs(); paintKnow();\n  setNavHeightVar();\n})();\n<\/script>\n\n\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>SaveOps\u2122 \u2013 Mobil SaveOps\u2122 \u2600\ufe0f\/\ud83c\udf19 Kommando-central Mobil overblik \u00b7 SaveScore\u2122 \u00b7 Hurtige gevinster AlleElMobilInternetStreamingForsikring ForfaldBel\u00f8bNavn Tilf\u00f8j regning Upload billede\/PDF eller inds\u00e6t tekst \u2192 Scan \u2192 Ret til \u2192 Gem Upload (foto\/PDF) \u2014 Tip: Foto af regning eller direkte PDF fra e-mail. Eller inds\u00e6t tekst fra regning Scan tekst Udbyder \/ navn Kategori ElMobilInternetStreamingForsikring Bel\u00f8b pr. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"elementor_canvas","format":"standard","meta":{"_themeisle_gutenberg_block_has_review":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-93","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/posts\/93","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/comments?post=93"}],"version-history":[{"count":22,"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/posts\/93\/revisions"}],"predecessor-version":[{"id":118,"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/posts\/93\/revisions\/118"}],"wp:attachment":[{"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/media?parent=93"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/categories?post=93"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hylledev.dk\/index.php\/wp-json\/wp\/v2\/tags?post=93"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}