[{"id":"6a7f529ea26b91ff0c5fec3c","name":"Playbook","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":2,"label":"Playbook"}],"html":"<!-- SaleSys Playbook v2 — realtidsöversikt för dialern. Egen vy (views-v1), ren rå-JS.\n     Nativ SaleSys-design via CSS-variabler. Ingen dubbel-måsvinge i denna fil (handlebars).\n     Plan: vyer/playbook/PLAN.md. API-referens: HAR-verifierad 2026-08-14. -->\n<meta charset=\"utf-8\">\n<div id=\"pb2-root\">\n  <style>\n    #pb2-root { font-family: 'Nunito', 'Segoe UI', sans-serif; color: var(--color-text-2, #333); position: relative; padding-bottom: 60px; }\n    #pb2-root * { box-sizing: border-box; }\n    #pb2-root button { font-family: inherit; }\n\n    /* ====== native SaleSys-designsystem (värden ur s2-frontend) ====== */\n\n    /* ---------- Toppbar: pill-flikar (vald=primary-fylld, ovald=simple) + tidsval + kugghjul ---------- */\n    .pb2-top { display: flex; align-items: center; gap: 24px; padding: 0 0 24px; flex-wrap: wrap; }\n    .pb2-tabrow { display: flex; align-items: center; gap: 4px; }\n    .pb2-pill {\n      border: none; background: transparent; cursor: pointer; font-family: inherit;\n      height: 32px; padding: 0 14px; border-radius: var(--border-radius-button, 8px);\n      font-size: 15px; font-weight: var(--font-weight-default, normal); line-height: 32px;\n      color: var(--color-button-simple, #1565c0); transition: background 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pb2-pill:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-pill.pb2-on { background: var(--color-primary, #1565c0); color: var(--color-background-2, #fff); }\n    .pb2-pill.pb2-big { height: 38px; line-height: 38px; font-size: 16px; padding: 0 18px; }\n    .pb2-top-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }\n    .pb2-updated { font-size: 13px; color: var(--color-text-3, #757575); }\n    .pb2-iconbtn {\n      border: none; background: transparent; color: var(--color-text-3, #757575); font-size: 18px;\n      width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n      transition: background 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pb2-iconbtn:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); color: var(--color-button-normal, #1565c0); }\n\n    /* ---------- Stora statusrutor (icon + tal + etikett) — fullbredd i Företag ---------- */\n    .pb2-statusnav { display: grid; grid-template-columns: repeat(var(--pb2-nav-cols, 4), 1fr); gap: 24px; }\n    .pb2-statustile {\n      background: var(--color-background-2, #fafafa); border-radius: 3px; cursor: pointer;\n      box-shadow: 0 1px 4px rgba(0,0,0,0.1); border: 2px solid transparent;\n      display: flex; align-items: center; gap: 14px; padding: 16px 20px;\n      transition: border-color 250ms cubic-bezier(0.33,1,0.68,1), background 250ms;\n    }\n    .pb2-statustile:hover { background: var(--color-background-3, #eee); }\n    .pb2-statustile.pb2-on { border-color: var(--color-primary, #1565c0); }\n    .pb2-st-icon { width: 46px; height: 46px; border-radius: 50%; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; font-size: 19px; }\n    .pb2-st-meta { min-width: 0; }\n    .pb2-st-count { font-size: 30px; font-weight: 600; line-height: 1; color: var(--color-text-1, #212121); }\n    .pb2-st-label { font-size: 14px; color: var(--color-text-3, #757575); margin-top: 6px; }\n\n    /* kompakta statusrutor inne i projektkort */\n    .pb2-ministatus { display: grid; grid-template-columns: repeat(var(--pb2-nav-cols, 4), 1fr); gap: 6px; padding: 0 14px 8px; flex: 0 0 auto; }\n    .pb2-mini {\n      background: var(--color-background-1, #fff); border-radius: 3px; cursor: pointer;\n      border: none; border-bottom: 2px solid transparent;\n      display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 8px 4px 7px;\n      transition: border-color 200ms, background 200ms;\n    }\n    .pb2-mini:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    /* vald status = bara blå underkant (ingen hel ram) */\n    .pb2-mini.pb2-on { border-bottom-color: var(--color-primary, #1565c0); }\n    .pb2-mini i { font-size: 14px; }\n    .pb2-mini b { font-size: 20px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1.1; }\n    .pb2-mini span { font-size: 11px; color: var(--color-text-3, #757575); }\n\n    /* ---------- Kortgrid ---------- */\n    /* ---------- Kortgrid (fast höjd, intern scroll — ingen masonry; kolumner konfigurerbara) ---------- */\n    .pb2-grid { display: grid; grid-template-columns: repeat(var(--pb2-cols, 4), 1fr); gap: 24px; }\n    @media (max-width: 1500px) { .pb2-grid { grid-template-columns: repeat(min(var(--pb2-cols, 4), 3), 1fr); } }\n    @media (max-width: 1150px) { .pb2-grid { grid-template-columns: repeat(min(var(--pb2-cols, 4), 2), 1fr); } }\n    @media (max-width: 760px)  { .pb2-grid { grid-template-columns: 1fr; } }\n\n    .pb2-card {\n      background: var(--color-background-2, #fafafa); border-radius: 3px;\n      box-shadow: 0 1px 4px rgba(0,0,0,0.1); border: 2px solid transparent;\n      display: flex; flex-direction: column; height: 720px; overflow: hidden;\n    }\n    .pb2-card.pb2-paused { opacity: 0.6; }\n    .pb2-card-head { display: flex; align-items: center; gap: 8px; padding: 14px 16px 10px; flex: 0 0 auto; }\n    .pb2-card-title { font-size: 22px; font-weight: 400; color: var(--color-text-1, #212121); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .pb2-badge { font-size: 11px; border-radius: 3px; padding: 2px 8px; white-space: nowrap; }\n    .pb2-badge.pb2-warn { background: rgba(244, 81, 30, 0.13); color: var(--color-neutral, #f4511e); }\n    .pb2-badge.pb2-stop { background: rgba(229, 57, 53, 0.12); color: var(--color-danger, #e53935); }\n    .pb2-playbtn {\n      margin-left: auto; border: none; background: transparent; color: var(--color-text-3, #757575);\n      width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 15px; flex: 0 0 auto;\n      transition: background 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pb2-playbtn:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); color: var(--color-button-normal, #1565c0); }\n    .pb2-playbtn.pb2-arm { background: var(--color-danger, #e53935); color: #fff; }\n\n    .pb2-card-body { padding: 6px 16px 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }\n    .pb2-card-body::-webkit-scrollbar { width: 6px; }\n    .pb2-card-body::-webkit-scrollbar-thumb { background: var(--color-background-3, #eee); border-radius: 3px; }\n\n    .pb2-subhead { font-size: 13px; color: var(--color-text-3, #757575); padding: 16px 0 8px; }\n    .pb2-subhead:first-child { padding-top: 4px; }\n\n    /* säljarlista — quiet list rows (native storlek) */\n    .pb2-userlist { display: flex; flex-direction: column; }\n    .pb2-user { display: flex; align-items: center; gap: 10px; padding: 5px 8px; margin: 0 -8px; min-height: 34px; border-radius: 3px; cursor: pointer; }\n    .pb2-user:hover, .pb2-grow.pb2-clickable:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-user .pb2-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }\n    .pb2-user-name { font-size: 15px; color: var(--color-text-2, #616161); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; }\n    /* fast smal mittkolumn (ringlista) — left-aligned + ellipsis + tooltip så statuskolumnen inte hoppar */\n    .pb2-user-group { font-size: 12px; color: var(--color-text-3, #757575); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 0 72px; text-align: left; }\n    .pb2-user-sub { font-size: 13px; color: var(--color-text-3, #757575); margin-left: auto; padding-left: 8px; text-align: right; white-space: nowrap; flex: 0 0 auto; }\n    .pb2-listen { flex: 0 0 auto; border: none; background: transparent; cursor: pointer; color: var(--color-text-4, #9e9e9e); width: 22px; height: 22px; border-radius: 50%; font-size: 13px; margin-left: 4px; }\n    .pb2-listen:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); color: var(--color-primary, #1565c0); }\n    .pb2-listen-spacer { flex: 0 0 auto; width: 22px; margin-left: 4px; }\n    .pb2-empty { font-size: 14px; color: var(--color-text-3, #757575); padding: 16px 0; text-align: center; }\n\n    /* ringlistor */\n    .pb2-grow { display: flex; align-items: center; gap: 10px; padding: 6px 8px; margin: 0 -8px; border-radius: 3px; font-size: 15px; }\n    .pb2-grow.pb2-clickable { cursor: pointer; }\n    .pb2-grow i { font-size: 13px; color: var(--color-text-4, #9e9e9e); width: 16px; text-align: center; }\n    .pb2-grow .pb2-gname { color: var(--color-text-2, #616161); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .pb2-grow .pb2-gleft { margin-left: auto; font-size: 13px; color: var(--color-text-3, #757575); white-space: nowrap; }\n\n    /* metrik-tabell (Säljare): rader = säljare, kolumner = metriker, horisontell scroll */\n    .pb2-tblwrap { overflow-x: auto; }\n    .pb2-tblwrap::-webkit-scrollbar { height: 6px; }\n    .pb2-tblwrap::-webkit-scrollbar-thumb { background: var(--color-background-3, #e0e0e0); border-radius: 3px; }\n    .pb2-tbl { border-collapse: collapse; font-size: 15px; white-space: nowrap; }\n    .pb2-tbl th { text-align: right; font-size: 12px; font-weight: var(--font-weight-default, normal); color: var(--color-text-3, #757575); padding: 6px 10px; }\n    .pb2-tbl th:first-child, .pb2-tbl td:first-child {\n      text-align: left; position: sticky; left: 0; background: var(--color-background-2, #fafafa);\n      max-width: 130px; overflow: hidden; text-overflow: ellipsis;\n    }\n    .pb2-tbl td { padding: 7px 10px; text-align: right; color: var(--color-text-2, #616161); }\n    .pb2-tbl tr + tr td { border-top: 1px solid var(--color-background-3, #eee); }\n    .pb2-urow { cursor: pointer; }\n    .pb2-urow:hover td { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    /* åtgärdskolumn (Företag) — fäst till höger så ikonerna alltid syns */\n    .pb2-tbl th.pb2-actcol, .pb2-tbl td.pb2-actcol { position: sticky; right: 0; background: var(--color-background-2, #fafafa); text-align: right; padding: 4px 10px 4px 16px; box-shadow: -6px 0 8px -8px rgba(0,0,0,0.25); }\n    .pb2-urow:hover td.pb2-actcol { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-tacts { display: inline-flex; gap: 2px; }\n    .pb2-tact { border: none; background: transparent; cursor: pointer; color: var(--color-text-3, #757575); width: 26px; height: 26px; border-radius: 6px; font-size: 14px; padding: 0; line-height: 1; transition: background-color 150ms, color 150ms; }\n    .pb2-tact:hover:not(:disabled) { background: var(--color-element-hover, rgba(20,20,20,0.06)); color: var(--color-button-normal, #1565c0); }\n    .pb2-tact.pb2-tdanger:hover:not(:disabled) { background: rgba(229,57,53,0.08); color: var(--color-danger, #e53935); }\n    .pb2-tact:disabled { opacity: 0.32; cursor: not-allowed; }\n    .pb2-good { color: var(--color-good, #43a047) !important; }\n    .pb2-bad { color: var(--color-danger, #e53935) !important; }\n\n    /* utfall-lista */\n    .pb2-outrow { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; font-size: 15px; }\n    .pb2-outrow .pb2-oname { color: var(--color-text-2, #616161); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; }\n    .pb2-outrow .pb2-ocount { color: var(--color-text-1, #212121); }\n    .pb2-outrow .pb2-opct { width: 50px; text-align: right; font-size: 13px; color: var(--color-text-3, #757575); }\n\n    /* bottenzon: statisk fot med FAST höjd — Nyckeltal/Diagram-navet flyttar sig ALDRIG */\n    .pb2-bottomzone { flex: 0 0 auto; height: 184px; display: flex; flex-direction: column; padding: 18px 16px; }\n    .pb2-bottomtabs { display: flex; align-items: center; gap: 4px; margin-bottom: 14px; flex: 0 0 auto; }\n    .pb2-bottomctrls { margin-left: auto; display: flex; gap: 8px; }\n    .pb2-metricrow { display: flex; margin-bottom: 8px; }\n\n    /* pinnade nyckeltal — rena tal, fyller footerns innehållsyta (samma yta som diagrammet) */\n    .pb2-pins { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px 18px; flex: 1 1 auto; align-content: center; }\n    .pb2-pin-val { font-size: 21px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1.1; }\n    .pb2-pin-label { font-size: 12px; color: var(--color-text-3, #757575); margin-top: 2px; }\n\n    /* per-projekt stapeldiagram (tunna, tätt sittande blå staplar) — halva bredden + nuvärde med chevron */\n    .pb2-chartzone { flex: 0 0 auto; padding: 10px 16px 6px; }\n    .pb2-chart-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }\n    .pb2-chart-metrics { display: flex; gap: 2px; }\n    .pb2-chip-mini { border: none; background: transparent; cursor: pointer; font-family: inherit; font-size: 11px; padding: 3px 9px; border-radius: 6px; color: var(--color-text-3, #757575); }\n    .pb2-chip-mini:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-chip-mini.pb2-on { background: var(--color-primary, #1565c0); color: var(--color-background-2, #fff); }\n    .pb2-chart-body { display: flex; align-items: stretch; gap: 14px; flex: 1 1 auto; min-height: 84px; }\n    .pb2-chart { display: flex; align-items: flex-end; gap: 2px; flex: 1 1 55%; align-self: stretch; min-height: 84px; }\n    .pb2-chart-body.pb2-chart-3col .pb2-chart { flex: 0 0 50%; }\n    .pb2-bar { flex: 1 1 0; min-width: 4px; max-width: 12px; background: var(--color-primary, #1565c0); border-radius: 2px 2px 0 0; min-height: 2px; transition: height 300ms cubic-bezier(0.33,1,0.68,1); }\n    .pb2-bar.pb2-cur { animation: pb2shimmer 1.4s ease-in-out infinite; }\n    @keyframes pb2shimmer { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }\n    /* övergångar vid vy-/innehållsbyte (motsvarar SaleSys <transition lock> — spelas när innehållet byts) */\n    @keyframes pb2fadein { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }\n    .pb2-fadein { animation: pb2fadein 260ms cubic-bezier(0.33,1,0.68,1) both; }\n    @keyframes pb2fadeinsm { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }\n    .pb2-fadein-sm { animation: pb2fadeinsm 200ms cubic-bezier(0.33,1,0.68,1) both; }\n    /* mjuk kaskad-entré vid första laddning (ersätter \"Laddar…\"-text) */\n    @keyframes pb2rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }\n    .pb2-boot > *, .pb2-boot .pb2-grid > *, .pb2-boot .pb2-proghero, .pb2-boot .pb2-copanel, .pb2-boot .pb2-copins, .pb2-boot .pb2-cogauges { animation: pb2rise 560ms cubic-bezier(0.22,1,0.36,1) both; }\n    .pb2-boot .pb2-grid > *:nth-child(1) { animation-delay: 40ms; } .pb2-boot .pb2-grid > *:nth-child(2) { animation-delay: 90ms; }\n    .pb2-boot .pb2-grid > *:nth-child(3) { animation-delay: 140ms; } .pb2-boot .pb2-grid > *:nth-child(4) { animation-delay: 190ms; }\n    .pb2-boot .pb2-grid > *:nth-child(5) { animation-delay: 240ms; } .pb2-boot .pb2-grid > *:nth-child(6) { animation-delay: 290ms; }\n    .pb2-boot .pb2-grid > *:nth-child(7) { animation-delay: 330ms; } .pb2-boot .pb2-grid > *:nth-child(8) { animation-delay: 360ms; }\n    .pb2-boot .pb2-grid > *:nth-child(n+9) { animation-delay: 390ms; }\n    .pb2-boot .pb2-viewbody { animation-delay: 60ms; }\n    @media (prefers-reduced-motion: reduce) { .pb2-fadein, .pb2-fadein-sm, .pb2-boot > *, .pb2-boot .pb2-grid > * { animation: none; } }\n    .pb2-chart-cur { flex: 0 0 42%; display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-start; height: 100%; }\n    .pb2-chart-body.pb2-chart-3col .pb2-chart-cur { flex: 0 0 25%; justify-content: center; }\n    .pb2-cur-top { display: flex; align-items: baseline; gap: 6px; }\n    .pb2-cur-val { font-size: 24px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1; }\n    .pb2-cur-chev { font-size: 14px; }\n    .pb2-cur-cap { font-size: 11px; color: var(--color-text-3, #757575); margin-top: 4px; }\n    /* metrik-textrader i chartens högra 25% */\n    .pb2-metriccol { flex: 0 0 25%; display: flex; flex-direction: column; justify-content: center; gap: 2px; height: 100%; }\n    .pb2-metricrow-btn { border: none; background: transparent; cursor: pointer; font-family: inherit; text-align: left; padding: 3px 6px; border-radius: 6px; font-size: 13px; color: var(--color-text-3, #757575); }\n    .pb2-metricrow-btn:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-metricrow-btn.pb2-on { color: var(--color-primary, #1565c0); font-weight: 700; }\n\n    /* ---------- Företag-vyn ---------- */\n    .pb2-company { display: flex; flex-direction: column; gap: 24px; }\n    .pb2-copanel { background: var(--color-background-2, #fafafa); border-radius: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); padding: 16px 20px; }\n    .pb2-cohead { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }\n    .pb2-cotitle { font-size: 16px; font-weight: 600; color: var(--color-text-1, #212121); }\n    .pb2-copins { display: grid; grid-template-columns: repeat(6, 1fr); gap: 1px; background: var(--color-background-3, #eee); border-radius: 3px; overflow: hidden; }\n    @media (max-width: 1150px) { .pb2-copins { grid-template-columns: repeat(3, 1fr); } }\n    .pb2-copin { background: var(--color-background-2, #fafafa); padding: 14px 18px; }\n    .pb2-copin-val { font-size: 26px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1.1; }\n    .pb2-copin-label { font-size: 13px; color: var(--color-text-3, #757575); margin-top: 3px; }\n\n    /* hover-tooltip (CSS, ej native title) */\n    .pb2-tip { position: relative; display: inline-flex; }\n    .pb2-tip .pb2-tipbox {\n      visibility: hidden; opacity: 0; position: absolute; z-index: 50; left: 0; top: 130%;\n      width: 300px; background: var(--color-background-1, #fff); color: var(--color-text-2, #616161);\n      font-size: 13px; font-weight: 400; line-height: 1.5; padding: 12px 14px; border-radius: 4px;\n      box-shadow: 0 8px 24px rgba(0,0,0,0.15); transition: opacity 150ms; pointer-events: none; text-align: left;\n    }\n    .pb2-tip:hover .pb2-tipbox { visibility: visible; opacity: 1; }\n\n    /* bemanning: gauges (ring + halvring) */\n    .pb2-gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n    @media (max-width: 900px) { .pb2-gauges { grid-template-columns: 1fr; } }\n    .pb2-gauge { background: var(--color-background-2, #fafafa); border-radius: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); padding: 16px 18px; display: flex; align-items: center; gap: 16px; }\n    .pb2-gauge svg { flex: 0 0 auto; }\n    .pb2-gauge-meta { min-width: 0; }\n    .pb2-gauge-val { font-size: 26px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1.05; }\n    .pb2-gauge-label { font-size: 13px; color: var(--color-text-3, #757575); margin-top: 3px; }\n    .pb2-gauge-sub { font-size: 12px; color: var(--color-text-4, #9e9e9e); margin-top: 2px; }\n    /* speedometer-kort (Företag + Prognos) */\n    .pb2-cogauges { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }\n    @media (max-width: 1150px) { .pb2-cogauges { grid-template-columns: repeat(2, 1fr); } }\n    .pb2-gcard { background: var(--color-background-2, #fafafa); border-radius: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); padding: 14px 12px 12px; display: flex; flex-direction: column; align-items: center; text-align: center; }\n    .pb2-gcard-title { font-size: 13px; color: var(--color-text-3, #757575); margin-bottom: 4px; }\n    .pb2-gcard svg { display: block; }\n    .pb2-gcard-val { font-size: 24px; font-weight: 600; line-height: 1.05; margin-top: -6px; }\n    .pb2-gcard-sub { font-size: 12px; color: var(--color-text-4, #9e9e9e); margin-top: 3px; }\n    /* diskret setup-uppmaning när gauges inte är konfigurerade (endast admin) */\n    .pb2-setupcta { display: flex; align-items: center; gap: 12px; padding: 14px 18px; background: var(--color-background-2, #fafafa); border: 1px dashed var(--color-background-4, #d5d5d5); border-radius: 8px; font-size: 14px; color: var(--color-text-3, #757575); cursor: pointer; transition: background 150ms, border-color 150ms; }\n    .pb2-setupcta:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); border-color: var(--color-primary, #1565c0); color: var(--color-text-2, #616161); }\n    .pb2-setupcta > i:first-child { color: var(--color-primary, #1565c0); font-size: 16px; }\n    /* kompakt variant för Prognos högerkolumn (3 per rad) */\n    .pb2-gauges3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n    .pb2-gauges3 .pb2-gcard { padding: 10px 6px 8px; }\n    .pb2-gauges3 .pb2-gcard-val { font-size: 18px; }\n    .pb2-gauges3 .pb2-gcard svg { width: 120px; height: auto; }\n\n    /* editerbar tabellcell */\n    .pb2-editcell { cursor: pointer; border-bottom: 1px dashed var(--color-background-4, #bdbdbd); }\n    .pb2-editcell:hover { color: var(--color-primary, #1565c0); }\n    .pb2-cellinput { width: 58px; border: none; border-bottom: 2px solid var(--color-primary, #1565c0); background: transparent; font-family: inherit; font-size: 15px; color: var(--color-text-2, #616161); text-align: right; padding: 2px; }\n    .pb2-cellinput:focus { outline: none; }\n    /* kompakt högerjusterad input i mrows */\n    .pb2-mrow .pb2-num { text-align: right; width: 84px; }\n\n    /* ---------- Prognos-vyn ---------- */\n    .pb2-prog { display: flex; flex-direction: column; gap: 24px; }\n    /* 2/3-split: diagram (2) + stat-kort staplade (1) */\n    .pb2-proghero { display: flex; gap: 24px; align-items: stretch; }\n    @media (max-width: 1000px) { .pb2-proghero { flex-direction: column; } }\n    .pb2-proghero-chart { flex: 2 1 0; min-width: 0; margin: 0; display: flex; flex-direction: column; }\n    .pb2-proghero-kpis { flex: 1 1 0; display: flex; flex-direction: column; gap: 12px; }\n    .pb2-proghero-kpis .pb2-progblock { flex: 1 1 0; justify-content: center; }\n    .pb2-progblock { background: var(--color-background-2, #fafafa); border-radius: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); padding: 14px 18px; display: flex; flex-direction: column; }\n    .pb2-progblock-label { font-size: 13px; color: var(--color-text-3, #757575); }\n    .pb2-progblock-val { font-size: 30px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1.05; margin-top: 4px; display: flex; align-items: center; }\n    .pb2-progblock-val small { font-size: 15px; font-weight: 600; color: var(--color-text-3, #757575); }\n    .pb2-progblock-sub { font-size: 12px; color: var(--color-text-4, #9e9e9e); margin-top: 4px; }\n    .pb2-block-ind { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-left: 10px; flex: 0 0 auto; }\n    .pb2-progchev { font-size: 16px; margin-left: 10px; vertical-align: baseline; }\n    /* kompakt variant för tabellceller & pins */\n    .pb2-ind-sm { width: 7px; height: 7px; margin-left: 6px; vertical-align: middle; }\n    /* Företag-tabellen: positiv avvikelse i blått istället för grönt */\n    .pb2-company .pb2-tbl td.pb2-good { color: var(--color-primary, #1565c0) !important; }\n    /* hover-krysshår + värdebubbla på huvuddiagrammet */\n    .pb2-chartwrap { position: relative; }\n    .pb2-crosshair { position: absolute; top: 6px; bottom: 6px; width: 1px; background: var(--color-background-4, #bdbdbd); opacity: 0; pointer-events: none; }\n    .pb2-charttip { position: absolute; top: 2px; transform: translateX(-50%); background: var(--color-background-1, #fff); box-shadow: 0 4px 14px rgba(0,0,0,0.15); border-radius: 4px; padding: 6px 10px; font-size: 12px; color: var(--color-text-2, #616161); white-space: nowrap; opacity: 0; pointer-events: none; z-index: 5; }\n    .pb2-charttip b { color: var(--color-text-1, #212121); }\n    .pb2-monthnav { display: flex; align-items: center; gap: 10px; }\n    .pb2-monthnav button { border: none; background: transparent; cursor: pointer; color: var(--color-text-3, #757575); width: 32px; height: 32px; border-radius: 50%; font-size: 15px; }\n    .pb2-monthnav button:hover:not(:disabled) { background: var(--color-element-hover, rgba(20,20,20,0.035)); color: var(--color-primary, #1565c0); }\n    .pb2-monthnav button:disabled { opacity: 0.35; cursor: default; }\n    .pb2-monthlbl { font-size: 15px; font-weight: 600; color: var(--color-text-1, #212121); min-width: 130px; text-align: center; text-transform: capitalize; }\n    .pb2-progchart { width: 100%; display: block; }\n    /* per-projekt spark-kort — full bredd, klickbar för att byta huvuddiagram (ingen egen scroll) */\n    /* lönsamhetskurva (intäkt/kostnad/vinst per timme) */\n    .pb2-pnlpanel .pb2-cohead { flex-wrap: wrap; }\n    .pb2-pnlchart { width: 100%; display: block; }\n    .pb2-pnllegend { display: flex; gap: 18px; margin-left: auto; flex-wrap: wrap; }\n    .pb2-pnlleg { font-size: 13px; color: var(--color-text-3, #757575); display: inline-flex; align-items: center; gap: 6px; }\n    .pb2-pnldot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }\n    .pb2-sparklist { display: flex; flex-direction: column; gap: 12px; overflow: visible; }\n    .pb2-sparkcard { background: var(--color-background-1, #fff); border-radius: 3px; border: 2px solid transparent; padding: 12px 14px; cursor: pointer; transition: border-color 150ms, background 150ms; display: flex; align-items: center; gap: 24px; }\n    .pb2-sparkcard:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    /* valt projekt markeras med blå projektnamn (ingen ram) */\n    .pb2-sparkcard.pb2-on .pb2-sparkname { color: var(--color-primary, #1565c0); }\n    /* 2/3-split som hjälten: diagram (2) vänster + info (1) höger, kolumnerna linjerar med hjälten */\n    .pb2-sparkcard-chart { flex: 2 1 0; min-width: 0; display: flex; align-items: center; }\n    .pb2-sparkcard-info { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 2px; }\n    /* högre kort med per-projekt speedometrar i 1/3 → sparklinjen (2/3) blir högre & tydligare */\n    .pb2-sparkcard.pb2-sparktall { align-items: stretch; }\n    .pb2-sparkcard.pb2-sparktall .pb2-sparkcard-chart { align-items: stretch; }\n    .pb2-sparkcard.pb2-sparktall .pb2-sparkcard-info { justify-content: center; }\n    .pb2-sparkgauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 10px; }\n    .pb2-sparkgauges .pb2-gcard { background: transparent; box-shadow: none; padding: 2px 0 0; }\n    .pb2-sparkgauges .pb2-gcard-title { font-size: 11px; margin-bottom: 2px; }\n    .pb2-sparkgauges .pb2-gcard svg { width: 92px; height: auto; }\n    .pb2-sparkgauges .pb2-gcard-val { font-size: 15px; margin-top: -8px; }\n    .pb2-sparkgauges .pb2-gcard-sub { font-size: 10px; }\n    @media (max-width: 1000px) { .pb2-sparkcard { flex-direction: column-reverse; align-items: stretch; gap: 8px; } }\n    .pb2-sparkname { font-size: 15px; font-weight: 600; color: var(--color-text-1, #212121); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .pb2-sparkval b { font-size: 16px; font-weight: 600; color: var(--color-text-1, #212121); }\n    .pb2-sparkval span { font-size: 12px; color: var(--color-text-3, #757575); margin-left: 8px; }\n    .pb2-sparkfull { width: 100%; display: block; }\n\n    /* ---------- Drilldown-modal (säljare / ringlista) ---------- */\n    .pb2-drill-head { display: flex; align-items: center; gap: 12px; }\n    .pb2-drill-dot { width: 12px; height: 12px; border-radius: 50%; flex: 0 0 auto; }\n    .pb2-drill-sub { font-size: 14px; color: var(--color-text-3, #757575); margin-top: 4px; }\n    .pb2-mgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--color-background-3, #eee); border-radius: 3px; overflow: hidden; margin-top: 14px; }\n    @media (max-width: 560px) { .pb2-mgrid { grid-template-columns: repeat(2, 1fr); } }\n    .pb2-mcell { background: var(--color-background-2, #fafafa); padding: 14px 16px; }\n    .pb2-mcell-val { font-size: 22px; font-weight: 600; color: var(--color-text-1, #212121); line-height: 1.1; }\n    .pb2-mcell-label { font-size: 12px; color: var(--color-text-3, #757575); margin-top: 3px; }\n    .pb2-drill-sec { font-size: 13px; color: var(--color-text-3, #757575); padding: 24px 0 8px; }\n    /* per-säljare samtalskontroller (Företag/drilldown) — samma stil som calls/statistics */\n    .pb2-callctl { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }\n    .pb2-cbtn { display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--color-background-4, #e0e0e0); background: var(--color-background-1, #fff); color: var(--color-text-2, #616161); font-family: inherit; font-size: 14px; padding: 8px 13px; border-radius: var(--border-radius-button, 8px); cursor: pointer; transition: background-color 150ms, border-color 150ms, color 150ms; }\n    .pb2-cbtn i { font-size: 14px; }\n    .pb2-cbtn:hover:not(:disabled) { background: var(--color-element-hover, rgba(20,20,20,0.035)); border-color: var(--color-text-4, #bdbdbd); }\n    .pb2-cbtn.pb2-cdanger { color: var(--color-danger, #e53935); border-color: rgba(229,57,53,0.35); }\n    .pb2-cbtn.pb2-cdanger:hover:not(:disabled) { background: rgba(229,57,53,0.06); border-color: var(--color-danger, #e53935); }\n    .pb2-cbtn:disabled { opacity: 0.45; cursor: not-allowed; }\n\n    /* kortflikar — vit bakgrund, vald = ljusblå */\n    .pb2-tabs { display: flex; gap: 6px; padding: 4px 16px 10px; flex: 0 0 auto; }\n    .pb2-tab {\n      flex: 1 1 0; border: none; cursor: pointer; font-family: inherit;\n      height: 32px; border-radius: 6px; font-size: 14px;\n      font-weight: var(--font-weight-default, normal); background: var(--color-background-1, #fff); color: var(--color-text-3, #757575);\n      transition: background 200ms, color 200ms;\n    }\n    .pb2-tab:hover { background: var(--color-background-3, #eee); }\n    .pb2-tab.pb2-on { background: rgba(21, 101, 192, 0.12); color: var(--color-primary, #1565c0); }\n\n    /* ---------- Modal (inställningar) ---------- */\n    /* ===== native modal (s2-frontend core/modal): bg-1, 4px, floating shadow, 24px padding) ===== */\n    .pb2-overlay { position: fixed; inset: 0; background: radial-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.6)); z-index: 1200; display: flex; align-items: center; justify-content: center; padding: 20px; }\n    .pb2-modal {\n      background: var(--color-background-1, #fff); border-radius: 4px; width: 640px; max-width: 96vw;\n      max-height: 88vh; display: flex; flex-direction: column; box-shadow: 0 8px 24px rgba(0,0,0,0.1);\n    }\n    .pb2-modal-head { padding: 24px 24px 8px; }\n    .pb2-modal-title { font-size: 24px; font-weight: var(--font-weight-default, normal); color: var(--color-text-2, #616161); }\n    .pb2-modal-body { padding: 8px 24px 12px; overflow-y: auto; }\n    /* fast höjd — modalen ändrar inte storlek när innehåll togglas (kroppen skrollar) */\n    .pb2-modal.pb2-fixed { height: 78vh; }\n    .pb2-modal.pb2-fixed .pb2-modal-body { flex: 1 1 auto; }\n    .pb2-modal-foot { display: flex; flex-direction: row-reverse; gap: 10px; padding: 12px 24px 24px; }\n    /* native Button (data-weight primary / simple), height 42px, radius 8px, weight normal */\n    .pb2-btn {\n      height: 42px; padding: 0 16px; border-radius: var(--border-radius-button, 8px); border: none; cursor: pointer;\n      font-size: 15px; font-weight: var(--font-weight-default, normal); background: var(--color-primary, #1565c0); color: var(--color-background-2, #fff);\n      font-family: inherit; transition: opacity 250ms;\n    }\n    .pb2-btn:hover { opacity: 0.92; }\n    .pb2-btn.pb2-ghost { background: transparent; color: var(--color-button-simple, #1565c0); }\n    .pb2-btn.pb2-ghost:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); opacity: 1; }\n    .pb2-acc { border-radius: 3px; margin-bottom: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); background: var(--color-background-2, #fafafa); }\n    .pb2-acc-head { display: flex; align-items: center; gap: 12px; padding: 14px 16px; cursor: pointer; font-weight: var(--font-weight-default, normal); font-size: 16px; color: var(--color-text-2, #616161); }\n    .pb2-acc-head i { color: var(--color-text-3, #757575); width: 18px; text-align: center; }\n    .pb2-acc-body { display: none; padding: 4px 16px 16px; }\n    .pb2-acc.pb2-open .pb2-acc-body { display: block; }\n    .pb2-hint { font-size: 14px; color: var(--color-text-3, #757575); padding: 6px 0 10px; }\n\n    /* ===== native ListableSwitch pill (core/ListableSwitch): accent-fyllnad + check/x + slide ===== */\n    .pb2-optcards { display: flex; gap: 4px; flex-wrap: wrap; }\n    .pb2-listable {\n      position: relative; z-index: 1; cursor: pointer; user-select: none; margin: 4px; box-sizing: border-box;\n      display: inline-flex; border-radius: 3px; overflow: hidden;\n    }\n    .pb2-listable::before {\n      content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none;\n      background: var(--color-accent, #1565c0); z-index: 0; border-radius: 3px;\n      transition: transform 250ms cubic-bezier(0.33,1,0.68,1), opacity 250ms cubic-bezier(0.33,1,0.68,1);\n      opacity: 0; transform: scale(0.95);\n    }\n    .pb2-listable.pb2-on::before { opacity: 1; transform: scale(1); }\n    .pb2-listable:hover::after { content:''; position:absolute; inset:0; background: var(--color-element-hover, rgba(20,20,20,0.035)); border-radius:3px; pointer-events:none; }\n    .pb2-listable .pb2-l-inner { position: relative; z-index: 1; display: inline-flex; align-items: center; padding: 8px 14px; padding-right: 38px; white-space: nowrap; font-size: 14px; }\n    .pb2-listable .pb2-l-icon { position: absolute; left: 12px; transition: opacity 250ms cubic-bezier(0.33,1,0.68,1); font-size: 12px; }\n    .pb2-listable .pb2-l-x { color: var(--color-text-2, #616161); }\n    .pb2-listable .pb2-l-check { color: rgba(255,255,255,0.9); opacity: 0; }\n    .pb2-listable.pb2-on .pb2-l-x { opacity: 0; }\n    .pb2-listable.pb2-on .pb2-l-check { opacity: 1; }\n    .pb2-listable .pb2-l-label { position: relative; left: 16px; transition: left 250ms cubic-bezier(0.33,1,0.68,1); color: var(--color-text-2, #616161); display: inline-flex; align-items: center; gap: 8px; }\n    .pb2-listable.pb2-on .pb2-l-label { left: 24px; color: #fafafa; }\n\n    /* ===== native Switch (core/Switch): 40x20 pill + knob, exakta transitioner ===== */\n    .pb2-mrow { display: flex; align-items: center; gap: 8px; padding: 10px 0; font-size: 15px; border-top: 1px solid var(--color-background-3, #eee); }\n    .pb2-mrow:first-child { border-top: none; }\n    .pb2-mrow-label { flex: 1 1 auto; color: var(--color-text-2, #616161); }\n    .pb2-switch {\n      display: inline-block; height: 20px; width: 40px; position: relative; border-radius: 10px; flex: 0 0 auto;\n      background-color: transparent; border: 2px solid var(--color-background-4, #bdbdbd); box-sizing: border-box; cursor: pointer;\n      transition: background-color 250ms cubic-bezier(0.33,1,0.68,1), border-color 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pb2-switch .pb2-knob {\n      position: absolute; left: 2px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background-color: var(--color-primary, #1565c0);\n      transition: left 250ms cubic-bezier(0.33,1,0.68,1), background-color 250ms cubic-bezier(0.33,1,0.68,1), transform 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pb2-switch.pb2-on { border-color: var(--color-primary, #1565c0); background-color: var(--color-primary, #1565c0); }\n    .pb2-switch.pb2-on .pb2-knob { left: 22px; background-color: var(--color-background-1, #fff); }\n    .pb2-switch:hover .pb2-knob { transform: translateX(1px); }\n    .pb2-switch.pb2-on:hover .pb2-knob { transform: translateX(-1px); }\n\n    /* native radio_button (core/radio_button): 24x24 ikon */\n    .pb2-radio { width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; color: var(--color-text-4, #9e9e9e); font-size: 18px; flex: 0 0 auto; }\n    .pb2-radio.pb2-on { color: var(--color-primary, #1565c0); }\n\n    /* riktnings-segment (upp/ned/neutral) — behålls som segment */\n    .pb2-thumb { border: 1px solid var(--color-background-4, #ddd); background: transparent; border-radius: 4px; width: 30px; height: 28px; cursor: pointer; color: var(--color-text-4, #999); font-size: 12px; }\n    .pb2-thumb.pb2-on-up { border-color: var(--color-good, #2e7d32); color: var(--color-good, #2e7d32); background: rgba(46,125,50,0.08); }\n    .pb2-thumb.pb2-on-down { border-color: var(--color-danger, #c62828); color: var(--color-danger, #c62828); background: rgba(198,40,40,0.08); }\n    .pb2-thumb.pb2-on-neutral { border-color: var(--color-text-3, #888); color: var(--color-text-3, #666); background: var(--color-background-3, #f0f0f0); }\n    .pb2-num {\n      width: 76px; border: none; border-bottom: 2px solid var(--color-background-4, #d5d5d5); background: transparent;\n      font-family: inherit; font-size: 13px; color: var(--color-text-2, #333); padding: 4px 2px; text-align: right;\n    }\n    .pb2-num:focus { outline: none; border-bottom-color: var(--color-primary, #1565c0); }\n    /* native text-input (Field, underline-stil) */\n    .pb2-field { margin-bottom: 16px; }\n    .pb2-field-label { display: block; font-size: 13px; color: var(--color-text-3, #757575); margin-bottom: 4px; }\n    .pb2-input, .pb2-textarea {\n      width: 100%; border: none; border-bottom: 2px solid var(--color-background-4, #bdbdbd); background: transparent;\n      font-family: inherit; font-size: 16px; color: var(--color-text-2, #616161); padding: 6px 2px; box-sizing: border-box;\n      transition: border-color 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pb2-textarea { resize: vertical; min-height: 72px; }\n    .pb2-input:focus, .pb2-textarea:focus { outline: none; border-bottom-color: var(--color-primary, #1565c0); }\n    /* notis-ikon (bottenfot) */\n    .pb2-notify { border: none; background: transparent; cursor: pointer; color: var(--color-text-3, #757575); width: 30px; height: 28px; border-radius: 6px; font-size: 15px; flex: 0 0 auto; }\n    .pb2-notify:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); color: var(--color-button-normal, #1565c0); }\n    /* \"Lägg till slinga\"-knapp + slinga-editor */\n    .pb2-addbtn { display: inline-flex; align-items: center; gap: 8px; border: none; background: transparent; cursor: pointer; font-family: inherit; font-size: 14px; color: var(--color-button-simple, #1565c0); padding: 8px 6px; border-radius: 6px; }\n    .pb2-addbtn:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-srcrow { display: flex; align-items: center; gap: 12px; height: 48px; border-top: 1px solid var(--color-background-3, #eee); font-size: 15px; }\n    .pb2-srcrow:first-child { border-top: none; }\n    /* intäktskälla-rad i prognosinställningar — får wrappa fritt, ingen fast höjd */\n    .pb2-revrow { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-height: 48px; padding: 10px 0; border-top: 1px solid var(--color-background-3, #eee); font-size: 15px; }\n    .pb2-revrow:first-of-type { border-top: none; }\n    .pb2-revfield { display: inline-flex; align-items: center; gap: 4px; }\n    .pb2-revunit { font-size: 11px; color: var(--color-text-4, #9e9e9e); }\n    /* scope-väljare (Alla + projekt) i prognosinställningar — horisontellt scrollbar pill-rad */\n    .pb2-scopebar { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 8px; margin-bottom: 4px; scrollbar-width: thin; }\n    .pb2-scopebar::-webkit-scrollbar { height: 5px; }\n    .pb2-scopebar::-webkit-scrollbar-thumb { background: var(--color-background-3, #e0e0e0); border-radius: 3px; }\n    .pb2-scopepill { border: 1px solid var(--color-background-4, #e0e0e0); background: var(--color-background-1, #fff); color: var(--color-text-3, #757575); font-family: inherit; font-size: 14px; padding: 7px 14px; border-radius: 999px; cursor: pointer; white-space: nowrap; flex: 0 0 auto; transition: background-color 150ms, color 150ms, border-color 150ms; }\n    .pb2-scopepill:hover { border-color: var(--color-text-4, #bdbdbd); }\n    .pb2-scopepill.pb2-on { background: var(--color-primary, #1565c0); border-color: var(--color-primary, #1565c0); color: #fff; }\n    .pb2-scopepill .pb2-scopedot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--color-good, #43a047); margin-left: 7px; vertical-align: middle; }\n    .pb2-inheritbar { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: var(--color-background-2, #fafafa); border-radius: 8px; font-size: 14px; color: var(--color-text-3, #757575); margin-bottom: 4px; }\n    .pb2-inheritbar i.pb2-ib-icon { color: var(--color-text-4, #9e9e9e); }\n    .pb2-inlink { border: none; background: transparent; color: var(--color-button-simple, #1565c0); font-family: inherit; font-size: 14px; cursor: pointer; padding: 4px 6px; border-radius: 6px; }\n    .pb2-inlink:hover { background: var(--color-element-hover, rgba(20,20,20,0.035)); }\n    .pb2-srcname { flex: 0 0 130px; color: var(--color-text-2, #616161); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .pb2-srcw { width: 66px !important; text-align: center !important; }\n    /* SaleSys-lik vikt-slider (Range) */\n    .pb2-slider { flex: 1 1 auto; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 2px; background: var(--color-background-4, #bdbdbd); outline: none; cursor: pointer; }\n    .pb2-slider:disabled { opacity: 0.4; cursor: default; }\n    .pb2-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--color-primary, #1565c0); cursor: pointer; border: none; }\n    .pb2-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--color-primary, #1565c0); cursor: pointer; border: none; }\n    .pb2-slider:disabled::-webkit-slider-thumb { background: var(--color-text-4, #9e9e9e); }\n    .pb2-srcpct { flex: 0 0 42px; text-align: right; font-size: 14px; font-weight: 600; color: var(--color-text-1, #212121); }\n    .pb2-modepills { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; }\n    .pb2-modepill { border: none; cursor: pointer; font-family: inherit; font-size: 13px; padding: 6px 12px; border-radius: 6px; background: var(--color-background-2, #fafafa); color: var(--color-text-3, #757575); display: inline-flex; align-items: center; gap: 6px; }\n    .pb2-modepill.pb2-on { background: rgba(21,101,192,0.12); color: var(--color-primary, #1565c0); }\n    .pb2-modepill .fa-star { font-size: 10px; color: var(--color-neutral, #f4511e); }\n\n    .pb2-note { font-size: 12px; color: var(--color-text-4, #999); padding: 14px 0; text-align: center; }\n    .pb2-demobadge {\n      display: inline-flex; align-items: center; gap: 7px;\n      font-size: 11px; font-weight: 800; letter-spacing: 0.4px;\n      background: rgba(160, 92, 194, 0.12); color: #8e44ad;\n      border: 1px solid rgba(160, 92, 194, 0.35); border-radius: 12px; padding: 4px 12px; white-space: nowrap;\n    }\n    .pb2-demopulse { width: 7px; height: 7px; border-radius: 50%; background: #8e44ad; animation: pb2pulse 1.6s ease-in-out infinite; }\n    @keyframes pb2pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }\n    .pb2-toast {\n      position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 1400;\n      background: var(--color-background-2, #fff); color: var(--color-text-2, #333); font-size: 14px; font-weight: 700;\n      padding: 12px 22px; border-radius: 22px; box-shadow: 0 2px 16px rgba(10,10,10,0.25);\n    }\n  </style>\n  <div id=\"pb2-app\"><div class=\"pb2-note\">Laddar Playbook…</div></div>\n</div>\n<script>\n(function () {\n  'use strict';\n  if (window.__pbCleanup) { try { window.__pbCleanup(); } catch (e) {} }\n\n  var API = '/api';\n  var PSEUDO_USER = '5f4d72e00000000000000000';\n  var TAB_FRESH_MS = 45000;\n  var CACHE_PREFIX = 'pb2c:v2:';\n\n  /* ---------------- status- & metrikdefinitioner ---------------- */\n  var STATUSES = [\n    { id: 'connected',              label: 'I samtal',    color: '#43a047', icon: 'fa-phone' },\n    { id: 'predictiveAwaitingCall', label: 'Väntar',      color: '#1e88e5', icon: 'fa-headset' },\n    { id: 'evaluatingCall',         label: 'Efterarbete', color: '#f9a825', icon: 'fa-clipboard-check' },\n    { id: 'inactive',               label: 'Inaktiv',     color: '#9e9e9e', icon: 'fa-pause-circle' }\n  ];\n  function statusDef(id) { for (var i = 0; i < STATUSES.length; i++) if (STATUSES[i].id === id) return STATUSES[i]; return STATUSES[3]; }\n\n  var METRICS = [\n    { id: 'calls',        label: 'Samtal',      dir: 'up',      fmt: 'int' },\n    { id: 'callsPerHour', label: 'Samtal/h',    dir: 'up',      fmt: 'dec' },\n    { id: 'answerRate',   label: 'Svar',        dir: 'up',      fmt: 'pct' },\n    { id: 'avgCallTime',  label: 'Snittsamtal', dir: 'neutral', fmt: 'dur' },\n    { id: 'orders',       label: 'Ordrar',      dir: 'up',      fmt: 'int' },\n    { id: 'ordersPerHour',label: 'Ordrar/h',    dir: 'up',      fmt: 'dec' },\n    { id: 'hits',         label: 'Hits',        dir: 'up',      fmt: 'int' },\n    { id: 'hitRate',      label: 'Hit rate',    dir: 'up',      fmt: 'pct' }\n  ];\n  function metricDef(id) { for (var i = 0; i < METRICS.length; i++) if (METRICS[i].id === id) return METRICS[i]; return null; }\n  //  Globala/icke-personliga begrepp — visas ALDRIG per säljare (drops/droprate = orutade predictive-samtal; tempo).\n  var NON_PERSONAL_METRICS = ['remaining', 'drops', 'dropRate', 'tempo'];\n\n  //  Metriker som kan pinnas i kortens 6 bottenrutor (inkl. några utöver jämförelse-metrikerna).\n  var PINNABLE = [\n    { id: 'answerRate',   label: 'Svar',          fmt: 'pct' },\n    { id: 'callsPerHour', label: 'Samtal/h',      fmt: 'dec' },\n    { id: 'calls',        label: 'Samtal',        fmt: 'int' },\n    { id: 'connected',    label: 'Svarade',       fmt: 'int' },\n    { id: 'ordersPerHour',label: 'Ordrar/h',      fmt: 'dec' },\n    { id: 'orders',       label: 'Ordrar',        fmt: 'int' },\n    { id: 'hitRate',      label: 'Hit rate',      fmt: 'pct' },\n    { id: 'hits',         label: 'Hits',          fmt: 'int' },\n    { id: 'avgCallTime',  label: 'Snittsamtal',   fmt: 'dur' },\n    { id: 'tempo',        label: 'Tempo',         fmt: 'tempo' },\n    { id: 'drops',        label: 'Drops',         fmt: 'int' },\n    { id: 'dropRate',     label: 'Drop rate',     fmt: 'pct' },\n    { id: 'remaining',    label: 'Kvar att ringa',fmt: 'int' }\n  ];\n  var DEFAULT_PINS = ['answerRate', 'ordersPerHour', 'hitRate', 'callsPerHour', 'avgCallTime', 'tempo'];\n  function pinDef(id) { for (var i = 0; i < PINNABLE.length; i++) if (PINNABLE[i].id === id) return PINNABLE[i]; return null; }\n\n  var TIME_FILTERS = [\n    { id: 'today',     label: 'Idag' },\n    { id: 'yesterday', label: 'Igår' },\n    { id: 'week',      label: 'Denna vecka' },\n    { id: 'month',     label: 'Denna månad' }\n  ];\n\n  var OFFER_STATUS_SV = { pending: 'Väntande', distributed: 'Skickat', read: 'Läst', signed: 'Signerat', canceled: 'Avbrutet', expired: 'Utgånget' };\n  var HALT_SV = { disabledByUser: 'Pausad', noAvailableSource: 'Ingen tillgänglig källa', unresponsiveUsers: 'Användare svarar inte', contactRedialed: 'Kontakt återuppringd' };\n  function haltLabel(r) { return HALT_SV[r] || r; }\n\n  /* ---------------- state ---------------- */\n  var state = {\n    booted: false, bootError: null,\n    me: null, viewId: null, view: null,\n    rights: null,\n    users: {}, projects: [], dialGroups: [], groupById: {},\n    dialTags: {}, orderTags: {},\n    tabs: [],                 //  user-tabs-v1 (färska, deduperade)\n    liveStatus: {},           //  userId -> { status, since } (call-statistics-v2)\n    summaries: {},            //  dialGroupId -> summary\n    stats: null,              //  aggregerad fönsterstatistik\n    chartStats: null,         //  serie senaste 8 dagar + idag (för stapeldiagram, oberoende av tidsfilter)\n    statsLoading: false, historyNote: '',\n    forecast: { monthOffset: 0, selProject: null, byMonth: {}, loading: false }, //  prognos: dagsvis intäkt per månad (selProject=null → Alla)\n    cfg: null, prefs: null,\n    ui: { settingsOpen: false, staffingOpen: false, progOpen: false, accOpen: 'projects', armPause: null, drill: null, drillData: null, notify: null, slinga: null, updatedAt: null },\n    renderStamp: 0,\n    timers: []\n  };\n\n  /* ---------------- helpers ---------------- */\n  function cookieGet(name) {\n    var parts = document.cookie.split(';');\n    for (var i = 0; i < parts.length; i++) {\n      var p = parts[i].trim();\n      if (p.indexOf(name + '=') === 0) return decodeURIComponent(p.substring(name.length + 1));\n    }\n    return null;\n  }\n  function token() { return cookieGet('s2_utoken'); }\n\n  function api(path, params, opts) {\n    opts = opts || {};\n    var url = API + path;\n    if (params) {\n      var qs = [];\n      for (var k in params) {\n        if (params[k] === undefined || params[k] === null) continue;\n        qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));\n      }\n      if (qs.length) url += '?' + qs.join('&');\n    }\n    return fetch(url, {\n      method: opts.method || 'GET',\n      headers: (function () {\n        var h = { 'Authorization': 'Bearer ' + token() };\n        if (opts.body) h['Content-Type'] = 'application/json';\n        return h;\n      })(),\n      body: opts.body ? JSON.stringify(opts.body) : undefined\n    }).then(function (r) {\n      if (!r.ok) { var err = new Error('HTTP ' + r.status + ' ' + path); err.status = r.status; throw err; }\n      return r.text().then(function (t) { try { return t ? JSON.parse(t) : null; } catch (e) { return null; } });\n    });\n  }\n\n  function esc(s) {\n    return String(s === undefined || s === null ? '' : s)\n      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n  }\n  function pad2(n) { return (n < 10 ? '0' : '') + n; }\n  function dayKey(d) { return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); }\n  function fmtInt(n) { return String(Math.round(n || 0)); }\n  function fmtDec(n) { return (Math.round((n || 0) * 10) / 10).toFixed(1).replace('.', ','); }\n  function fmtPct(n) { return isFinite(n) ? Math.round((n || 0) * 100) + '%' : '–'; }\n  function fmtDur(ms) {\n    if (!ms || !isFinite(ms)) return '–';\n    var s = Math.round(ms / 1000);\n    if (s < 3600) return Math.floor(s / 60) + ':' + pad2(s % 60);\n    return Math.floor(s / 3600) + 'h ' + pad2(Math.floor((s % 3600) / 60)) + 'm';\n  }\n  function fmtVal(fmt, v) {\n    if (v === undefined || v === null || (typeof v === 'number' && !isFinite(v))) return '–';\n    if (fmt === 'int') return fmtInt(v);\n    if (fmt === 'dec') return fmtDec(v);\n    if (fmt === 'pct') return fmtPct(v);\n    if (fmt === 'dur') return fmtDur(v);\n    if (fmt === 'tempo') return fmtInt(v);\n    return String(v);\n  }\n  function userName(uid) {\n    if (uid === PSEUDO_USER) return 'System';\n    var u = state.users[uid];\n    if (!u) return 'Okänd';\n    return ((u.firstName || '') + ' ' + (u.lastName || '')).trim() || u.username || 'Okänd';\n  }\n  function toast(msg) {\n    var old = document.getElementById('pb2-toast');\n    if (old) old.remove();\n    var el = document.createElement('div');\n    el.className = 'pb2-toast'; el.id = 'pb2-toast'; el.textContent = msg;\n    document.body.appendChild(el);\n    setTimeout(function () { if (el.parentNode) el.remove(); }, 3200);\n  }\n\n  /* ---------------- rättigheter ---------------- */\n  function computeRights() {\n    var me = state.me || {};\n    var isAdmin = me.type === 'admin';\n    var viewRights = null;\n    try {\n      if (window.form && window.form.rights && window.form.rights.length !== undefined) viewRights = window.form.rights;\n    } catch (e) {}\n    function has(r) {\n      if (isAdmin) return true;\n      if (viewRights) return viewRights.indexOf(r) !== -1;\n      //  Fallback-heuristik när vy-rättigheter saknas (äldre miljö):\n      var pr = me.rights || [];\n      if (r === 'viewAllProjects' || r === 'viewUserDrilldown') return pr.indexOf('calls.getAll') !== -1 || pr.indexOf('calls.getGetByOwnTeam') !== -1;\n      if (r === 'viewOwnProjects') return true;\n      return false; //  controlDialer, manageSettings: endast admin i fallback\n    }\n    return {\n      viewAllProjects: has('viewAllProjects'),\n      viewOwnProjects: has('viewOwnProjects'),\n      controlDialer: has('controlDialer'),\n      viewUserDrilldown: has('viewUserDrilldown'),\n      manageSettings: has('manageSettings'),\n      isAdmin: isAdmin\n    };\n  }\n\n  /* ---------------- konfiguration ---------------- */\n  function defaultCfg() {\n    return {\n      configured: false,\n      demoMode: false,       //  sätts i inställningsvyn (settingsHtml) — visar exempeldata\n      projectIds: [],\n      statusesVisible: { connected: true, predictiveAwaitingCall: true, evaluatingCall: true, inactive: true },\n      hitRate: { mode: 'orders', tagId: null },\n      metrics: {},           //  id -> { dir, benchmark }\n      pinnedMetrics: DEFAULT_PINS.slice(),   //  6 pinnade nyckeltal i kortens botten\n      cardsPerRow: 4,        //  kort per rad (1–4)\n      sections: { nav: true, body: true, bottom: true, tabs: true }, //  synliga kortsektioner\n      showUserGroup: false,  //  visa säljarens aktuella ringlista som mittkolumn\n      showOther: true,       //  visa \"Övrigt\"-kortet (inloggade utan projekt)\n      showGauges: true,      //  visa speedometrar (Företag + Prognos) — kräver mål/kostnad i Prognosinställningar\n      tempoBenchmark: 25,    //  samtal/h som ger tempo 10\n      boosted: {},           //  gid -> original aggression (för forward-återställning)\n      staffing: { monthlyHours: 165, defaultRate: 100, rateByProject: {}, targets: {} }, //  bemanningskalkyl (targets: metricId -> {value,dir})\n      revenue: { drivers: [{ type: 'signed', value: 1000 }] }, //  prognos: vad som ger intäkt\n      pausedProjects: {}\n    };\n  }\n  function isDemo() { return !!((state.cfg && state.cfg.demoMode) || state.urlDemo); }\n  function mergedCfg() { return clone(state.cfg || defaultCfg()); }\n  function canEditSettings() { return !!(state.rights.isAdmin || state.rights.manageSettings); }\n  function clone(o) { return JSON.parse(JSON.stringify(o)); }\n  //  Personliga vs globala inställningar. Dessa nycklar (huvud-inställningsmodalen) kan sparas\n  //  \"för mig\" (state.prefs.settings) eller \"för alla\" (presetet). state.baseSettings = allas värden,\n  //  state.cfg = det som faktiskt visas (allas, ev. överlagrat av mina). Övriga cfg-nycklar\n  //  (pausedProjects, boosted, staffing, revenue, demoMode …) är alltid globala.\n  var SETTINGS_KEYS = ['projectIds', 'statusesVisible', 'pinnedMetrics', 'cardsPerRow', 'sections', 'showUserGroup', 'showOther', 'showGauges', 'tempoBenchmark', 'hitRate', 'metrics'];\n  function hasPersonalSettings() {\n    var s = state.prefs && state.prefs.settings;\n    return !!(s && Object.keys(s).length);\n  }\n  //  bygg state.cfg = allas inställningar överlagrat av mina (endast SETTINGS_KEYS)\n  function applyPersonalSettings() {\n    if (!state.baseSettings) return;\n    var p = (state.prefs && state.prefs.settings) || {};\n    SETTINGS_KEYS.forEach(function (k) {\n      state.cfg[k] = clone(p[k] !== undefined ? p[k] : state.baseSettings[k]);\n    });\n  }\n  function footerVisible() { var sec = (state.cfg && state.cfg.sections) || {}; return sec.bottom !== false; }\n  function metricDir(id) {\n    var m = (mergedCfg().metrics || {})[id];\n    if (m && m.dir) return m.dir;\n    var d = metricDef(id);\n    return d ? d.dir : 'neutral';\n  }\n  function metricBenchmark(id) {\n    var m = (mergedCfg().metrics || {})[id];\n    return m && m.benchmark !== undefined && m.benchmark !== null && m.benchmark !== '' ? Number(m.benchmark) : null;\n  }\n\n  function visibleProjects() {\n    if (isDemo()) return state.projects;\n    var cfg = state.cfg || defaultCfg();\n    var list = state.projects.filter(function (p) {\n      return !cfg.projectIds.length || cfg.projectIds.indexOf(p.id) !== -1;\n    });\n    if (state.rights.viewAllProjects) return list;\n    if (!state.rights.viewOwnProjects) return [];\n    //  Egna projekt: projekt där någon av mina team äger/har åtkomst till en ringlista.\n    var me = state.me || {};\n    var myTeams = [me.teamId].concat(me.supervisedTeamIds || []).filter(Boolean);\n    var ok = {};\n    state.dialGroups.forEach(function (g) {\n      var teams = (g.accessTeamIds || []).concat(g.ownerTeamIds || []);\n      var hit = teams.some(function (t) { return myTeams.indexOf(t) !== -1; });\n      if (hit) (g.projectIds || []).forEach(function (pid) { ok[pid] = true; });\n    });\n    return list.filter(function (p) { return ok[p.id]; });\n  }\n\n  /* ---------------- tidsfönster ---------------- */\n  function windowRange() {\n    var tf = (state.prefs && state.prefs.timeFilter) || 'today';\n    var now = new Date();\n    var start = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n    var end = now;\n    if (tf === 'yesterday') {\n      end = start;\n      start = new Date(start.getTime() - 86400000);\n    } else if (tf === 'week') {\n      var dow = (start.getDay() + 6) % 7; //  måndag=0\n      start = new Date(start.getTime() - dow * 86400000);\n    } else if (tf === 'month') {\n      start = new Date(now.getFullYear(), now.getMonth(), 1);\n    }\n    return { from: start, to: end, includesToday: tf !== 'yesterday' };\n  }\n  function pastDaysInWindow() {\n    var w = windowRange();\n    var days = [];\n    var todayStart = new Date(); todayStart = new Date(todayStart.getFullYear(), todayStart.getMonth(), todayStart.getDate());\n    var d = new Date(w.from.getTime());\n    while (d < todayStart && d < w.to) {\n      days.push(new Date(d.getTime()));\n      d = new Date(d.getTime() + 86400000);\n    }\n    return days;\n  }\n\n  /* ---------------- statistikaggregat ----------------\n     agg = { proj:   { pid: bucket },\n             user:   { uid: bucket },\n             pu:     { pid: { uid: bucket } },\n             callTagByProj:  { pid: { tagId: n } },\n             callTagByUser:  { uid: { tagId: n } },\n             orderTagByProj: { pid: { tagId: n } },\n             offerStatusByProj: { pid: { status: n } } }\n     bucket = { calls, connected, duration, drops, hours: {}, orders, signed } */\n  function newBucket() { return { calls: 0, connected: 0, duration: 0, drops: 0, hours: {}, orders: 0, signed: 0 }; }\n  function newAgg() {\n    return { proj: {}, user: {}, pu: {}, callTagByProj: {}, callTagByUser: {}, orderTagByProj: {}, offerStatusByProj: {}, series: { proj: {}, user: {} } };\n  }\n  //  hourly serie för stapeldiagrammet: series[lvl][id][metric][hourKey] = antal\n  function localHourKey(d) { if (typeof d === 'string') d = new Date(d); if (!d || isNaN(d.getTime())) return null; return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + 'T' + pad2(d.getHours()); }\n  function seriesAdd(agg, lvl, id, metric, hk, n) {\n    if (!hk || !id || !n) return;\n    var root = agg.series[lvl]; if (!root[id]) root[id] = { calls: {}, orders: {}, signed: {} };\n    root[id][metric][hk] = (root[id][metric][hk] || 0) + n;\n  }\n  function bucketOf(map, key) { if (!map[key]) map[key] = newBucket(); return map[key]; }\n  function puBucket(agg, pid, uid) { if (!agg.pu[pid]) agg.pu[pid] = {}; return bucketOf(agg.pu[pid], uid); }\n  function incMap(root, k1, k2, n) {\n    if (!k2) return;\n    if (!root[k1]) root[k1] = {};\n    root[k1][k2] = (root[k1][k2] || 0) + n;\n  }\n  function mergeBucket(a, b) {\n    a.calls += b.calls; a.connected += b.connected; a.duration += b.duration;\n    a.drops += b.drops; a.orders += b.orders; a.signed += b.signed;\n    for (var h in b.hours) a.hours[h] = true;\n  }\n  function mergeAgg(target, src) {\n    ['proj', 'user'].forEach(function (lvl) {\n      for (var k in src[lvl]) mergeBucket(bucketOf(target[lvl], k), src[lvl][k]);\n    });\n    for (var pid in src.pu) for (var uid in src.pu[pid]) mergeBucket(puBucket(target, pid, uid), src.pu[pid][uid]);\n    ['callTagByProj', 'callTagByUser', 'orderTagByProj', 'offerStatusByProj'].forEach(function (m) {\n      for (var k1 in src[m]) for (var k2 in src[m][k1]) incMap(target[m], k1, k2, src[m][k1][k2]);\n    });\n    if (src.series) { ['proj', 'user'].forEach(function (lvl) {\n      var sr = src.series[lvl] || {};\n      for (var id in sr) { ['calls', 'orders', 'signed'].forEach(function (metric) {\n        for (var hk in sr[id][metric]) seriesAdd(target, lvl, id, metric, hk, sr[id][metric][hk]);\n      }); }\n    }); }\n    return target;\n  }\n\n  function fetchRangeAgg(from, to) {\n    //  Hämtar och aggregerar ett tidsspann (dial + orders + offers).\n    var agg = newAgg();\n    var fromIso = from.toISOString(), toIso = to.toISOString();\n\n    var pDial = api('/dial/statistics-v1/own/issue_1238_2', { from: fromIso, to: toIso, fixedIntervalType: 'hour' })\n      .then(function (rows) {\n        (rows || []).forEach(function (r) {\n          var pid = r.projectId || 'none';\n          var uid = r.userId || 'none';\n          var hour = r.intervalStart || '';\n          var isPseudo = uid === PSEUDO_USER;\n          var b = bucketOf(agg.proj, pid);\n          if (isPseudo) {\n            b.drops += (r.connectedCount || r.count || 0);\n          } else {\n            var add = function (bk) {\n              bk.calls += (r.count || 0);\n              bk.connected += (r.connectedCount || 0);\n              bk.duration += (r.connectedDuration || 0);\n              if (r.count) bk.hours[pid + '|' + hour] = true;\n            };\n            add(b);\n            var ub = bucketOf(agg.user, uid); ub.calls += (r.count || 0); ub.connected += (r.connectedCount || 0); ub.duration += (r.connectedDuration || 0); if (r.count) ub.hours[uid + '|' + hour] = true;\n            var pb = puBucket(agg, pid, uid); pb.calls += (r.count || 0); pb.connected += (r.connectedCount || 0); pb.duration += (r.connectedDuration || 0); if (r.count) pb.hours[hour] = true;\n            var hk = localHourKey(hour);\n            seriesAdd(agg, 'proj', pid, 'calls', hk, r.count || 0);\n            seriesAdd(agg, 'user', uid, 'calls', hk, r.count || 0);\n            if (r.tagId) {\n              incMap(agg.callTagByProj, pid, r.tagId, r.count || 0);\n              incMap(agg.callTagByUser, uid, r.tagId, r.count || 0);\n            }\n          }\n        });\n      }).catch(function () {});\n\n    function fetchPaged(path, params, cap) {\n      var out = [];\n      function page(offset) {\n        var p = {}; for (var k in params) p[k] = params[k];\n        p.offset = offset; p.count = 100;\n        return api(path, p).then(function (rows) {\n          rows = rows || [];\n          out = out.concat(rows);\n          if (rows.length === 100 && out.length < cap) return page(offset + 100);\n          return out;\n        });\n      }\n      return page(0).catch(function () { return out; });\n    }\n\n    var pOrders = fetchPaged('/orders/orders-v3', { from: fromIso, to: toIso, isTest: false, sortBy: 'serialId', sortOrder: 'desc' }, 500)\n      .then(function (orders) {\n        orders.forEach(function (o) {\n          var pid = o.projectId || 'none', uid = o.userId || 'none';\n          bucketOf(agg.proj, pid).orders += 1;\n          bucketOf(agg.user, uid).orders += 1;\n          puBucket(agg, pid, uid).orders += 1;\n          var hk = localHourKey(o.date);\n          seriesAdd(agg, 'proj', pid, 'orders', hk, 1);\n          seriesAdd(agg, 'user', uid, 'orders', hk, 1);\n          (o.tagIds || []).forEach(function (t) { incMap(agg.orderTagByProj, pid, t, 1); });\n        });\n      });\n\n    var pOffers = fetchPaged('/offers/offers-v2', { from: fromIso, to: toIso, sortOrder: 'desc' }, 500)\n      .then(function (offers) {\n        offers.forEach(function (o) {\n          var pid = o.projectId || 'none', uid = o.userId || 'none';\n          incMap(agg.offerStatusByProj, pid, o.status || 'pending', 1);\n          if (o.status === 'signed') {\n            bucketOf(agg.proj, pid).signed += 1;\n            bucketOf(agg.user, uid).signed += 1;\n            puBucket(agg, pid, uid).signed += 1;\n            var hk = localHourKey(o.date);\n            seriesAdd(agg, 'proj', pid, 'signed', hk, 1);\n            seriesAdd(agg, 'user', uid, 'signed', hk, 1);\n          }\n        });\n      });\n\n    return Promise.all([pDial, pOrders, pOffers]).then(function () { return agg; });\n  }\n\n  function cacheGet(day) {\n    try {\n      var raw = localStorage.getItem(CACHE_PREFIX + day);\n      return raw ? JSON.parse(raw) : null;\n    } catch (e) { return null; }\n  }\n  function cacheSet(day, agg) {\n    try { localStorage.setItem(CACHE_PREFIX + day, JSON.stringify(agg)); } catch (e) {}\n  }\n\n  var statsRun = 0;\n  function refreshStats() {\n    if (isDemo()) return Promise.resolve();\n    var run = ++statsRun;\n    var w = windowRange();\n    var past = pastDaysInWindow();\n    state.statsLoading = true;\n\n    //  1) historiska dagar — från cache, annars hämta EN gång och cacha per dag\n    var missing = past.filter(function (d) { return !cacheGet(dayKey(d)); });\n    var pMissing = Promise.resolve();\n    if (missing.length) {\n      state.historyNote = 'Hämtar historik (' + missing.length + ' dagar)…';\n      scheduleRender();\n      missing.forEach(function (d) {\n        pMissing = pMissing.then(function () {\n          var end = new Date(d.getTime() + 86400000);\n          return fetchRangeAgg(d, end).then(function (agg) { cacheSet(dayKey(d), agg); });\n        });\n      });\n    }\n\n    //  2) idag (om fönstret inkluderar idag) — hämtas färskt varje gång\n    var todayStart = new Date(); todayStart = new Date(todayStart.getFullYear(), todayStart.getMonth(), todayStart.getDate());\n    var pToday = w.includesToday && w.to > todayStart\n      ? fetchRangeAgg(todayStart < w.from ? w.from : todayStart, new Date())\n      : Promise.resolve(null);\n\n    return Promise.all([pMissing, pToday]).then(function (res) {\n      if (run !== statsRun) return; //  fönstret byttes under hämtning\n      var total = newAgg();\n      past.forEach(function (d) {\n        var c = cacheGet(dayKey(d));\n        if (c) mergeAgg(total, c);\n      });\n      if (res[1]) mergeAgg(total, res[1]);\n      state.stats = total;\n      state.statsLoading = false;\n      state.historyNote = '';\n      state.ui.updatedAt = new Date();\n      scheduleRender();\n    }).catch(function () {\n      if (run !== statsRun) return;\n      state.statsLoading = false; state.historyNote = '';\n      scheduleRender();\n    });\n  }\n\n  //  Stapeldiagrammets serie: senaste 8 dagar (cachade) + idag (färskt), oberoende av tidsfiltret.\n  var chartRun = 0;\n  function loadChartStats() {\n    if (isDemo()) return;\n    var run = ++chartRun;\n    var today = new Date(); today = new Date(today.getFullYear(), today.getMonth(), today.getDate());\n    var days = [];\n    for (var i = 15; i >= 1; i--) days.push(new Date(today.getTime() - i * 86400000));\n    var chain = Promise.resolve();\n    days.filter(function (d) { return !cacheGet(dayKey(d)); }).forEach(function (d) {\n      chain = chain.then(function () { var end = new Date(d.getTime() + 86400000); return fetchRangeAgg(d, end).then(function (a) { cacheSet(dayKey(d), a); }); });\n    });\n    var pToday = fetchRangeAgg(today, new Date());\n    Promise.all([chain, pToday]).then(function (res) {\n      if (run !== chartRun) return;\n      var total = newAgg();\n      days.forEach(function (d) { var c = cacheGet(dayKey(d)); if (c) mergeAgg(total, c); });\n      if (res[1]) mergeAgg(total, res[1]);\n      state.chartStats = total;\n      scheduleRender();\n    }).catch(function () {});\n  }\n\n  /* ---------------- metrikberäkning ---------------- */\n  function scopeBucket(pid, uid) {\n    var s = state.stats;\n    if (!s) return newBucket();\n    if (pid && uid) return (s.pu[pid] && s.pu[pid][uid]) || newBucket();\n    if (pid) return s.proj[pid] || newBucket();\n    if (uid) return s.user[uid] || newBucket();\n    return newBucket();\n  }\n  function hitsFor(pid, uid) {\n    var cfg = mergedCfg();\n    var s = state.stats;\n    if (!s) return 0;\n    if (cfg.hitRate.mode === 'callTag' && cfg.hitRate.tagId) {\n      if (pid && !uid) return (s.callTagByProj[pid] || {})[cfg.hitRate.tagId] || 0;\n      if (uid && !pid) return (s.callTagByUser[uid] || {})[cfg.hitRate.tagId] || 0;\n      //  per projekt+user saknar tagg-dimension i aggregatet — approximera med user-nivå\n      return (s.callTagByUser[uid] || {})[cfg.hitRate.tagId] || 0;\n    }\n    var b = scopeBucket(pid, uid);\n    return cfg.hitRate.mode === 'signedOffers' ? b.signed : b.orders;\n  }\n  function computeMetrics(pid, uid) {\n    var b = scopeBucket(pid, uid);\n    var hours = 0; for (var h in b.hours) hours++;\n    var hits = hitsFor(pid, uid);\n    var cph = hours ? b.calls / hours : 0;\n    var bench = metricBenchmark('tempo');\n    var tempoBase = bench || (state.cfg && state.cfg.tempoBenchmark) || 25;\n    return {\n      calls: b.calls,\n      callsPerHour: cph,\n      answerRate: b.calls ? b.connected / b.calls : NaN,\n      avgCallTime: b.connected ? b.duration / b.connected : NaN,\n      orders: b.orders,\n      ordersPerHour: hours ? b.orders / hours : 0,\n      hits: hits,\n      hitRate: b.connected ? hits / b.connected : NaN,\n      tempo: b.calls ? Math.max(1, Math.min(10, Math.round(10 * cph / tempoBase))) : NaN,\n      connected: b.connected,\n      drops: b.drops,\n      dropRate: (b.drops + b.connected) ? b.drops / (b.drops + b.connected) : NaN\n    };\n  }\n  function deviationClass(id, val, avg) {\n    if (val === undefined || val === null || !isFinite(val)) return '';\n    var dir = metricDir(id);\n    if (dir === 'neutral') return '';\n    var ref = metricBenchmark(id);\n    if (ref === null) ref = avg;\n    if (ref === undefined || ref === null || !isFinite(ref) || ref === 0) return '';\n    var diff = (val - ref) / Math.abs(ref);\n    if (Math.abs(diff) < 0.05) return '';\n    var better = dir === 'up' ? diff > 0 : diff < 0;\n    return better ? 'pb2-good' : 'pb2-bad';\n  }\n  //  baslinje-indikator (prick) för en metrik mot dess benchmark — samma synlighetstoggle som prognos\n  function metricInd(id, value, goodColor) {\n    if (!indicatorsOn()) return '';\n    var ref = metricBenchmark(id);\n    if (ref === null || !isFinite(ref)) return '';\n    var dir = metricDir(id);\n    if (dir === 'neutral') return '';\n    if (value == null || !isFinite(value)) return '';\n    var better = dir === 'up' ? value >= ref : value <= ref;\n    var col = better ? (goodColor || 'var(--color-good,#43a047)') : 'var(--color-danger,#e53935)';\n    var def = metricDef(id);\n    return '<span class=\"pb2-block-ind pb2-ind-sm\" style=\"background:' + col + '\" title=\"Baslinje ' + esc(fmtVal(def ? def.fmt : 'int', ref)) + '\"></span>';\n  }\n\n  /* ---------------- live (statusar & närvaro) ---------------- */\n  function freshTabs() {\n    var now = Date.now();\n    var byUser = {};\n    state.tabs.forEach(function (t) {\n      var hb = t.heartbeatAt || t.updatedAt || t.date;\n      var ts = hb ? new Date(hb).getTime() : 0;\n      if (now - ts > TAB_FRESH_MS) return;\n      if (t.isHidden) return;\n      var cur = byUser[t.userId];\n      if (!cur || ts > cur._ts) { t._ts = ts; byUser[t.userId] = t; }\n    });\n    return byUser;\n  }\n  function liveUserList(pid) {\n    //  Användare som just nu är i projektet (färsk flik med projectId).\n    var byUser = freshTabs();\n    var out = [];\n    for (var uid in byUser) {\n      if (uid === PSEUDO_USER) continue;\n      var t = byUser[uid];\n      if (pid && t.projectId !== pid) continue;\n      var ls = state.liveStatus[uid];\n      var status = (ls && ls.status) || t.callStatus || 'inactive';\n      out.push({ userId: uid, status: status, since: ls && ls.since, dialGroupId: t.dialGroupId, isUserActive: !!t.isUserActive });\n    }\n    out.sort(function (a, b) {\n      var oa = STATUSES.findIndex(function (s) { return s.id === a.status; });\n      var ob = STATUSES.findIndex(function (s) { return s.id === b.status; });\n      if (oa !== ob) return oa - ob;\n      return userName(a.userId).localeCompare(userName(b.userId), 'sv');\n    });\n    return out;\n  }\n\n  //  Live-användare som INTE hör till något synligt projekt (för \"Övrigt\"-kortet)\n  function liveOtherUsers(visiblePids) {\n    var byUser = freshTabs();\n    var out = [];\n    for (var uid in byUser) {\n      if (uid === PSEUDO_USER) continue;\n      var t = byUser[uid];\n      if (t.projectId && visiblePids[t.projectId]) continue;\n      var ls = state.liveStatus[uid];\n      var status = (ls && ls.status) || t.callStatus || 'inactive';\n      out.push({ userId: uid, status: status, since: ls && ls.since, dialGroupId: t.dialGroupId, isUserActive: !!t.isUserActive });\n    }\n    out.sort(function (a, b) {\n      var oa = STATUSES.findIndex(function (s) { return s.id === a.status; });\n      var ob = STATUSES.findIndex(function (s) { return s.id === b.status; });\n      if (oa !== ob) return oa - ob;\n      return userName(a.userId).localeCompare(userName(b.userId), 'sv');\n    });\n    return out;\n  }\n  function projName(pid) { for (var i = 0; i < state.projects.length; i++) if (state.projects[i].id === pid) return state.projects[i].name; return null; }\n  function timeFilterLabel() { for (var i = 0; i < TIME_FILTERS.length; i++) if (TIME_FILTERS[i].id === state.prefs.timeFilter) return TIME_FILTERS[i].label; return ''; }\n\n  function refreshLive() {\n    if (isDemo()) return Promise.resolve();\n    var todayStart = new Date(); todayStart = new Date(todayStart.getFullYear(), todayStart.getMonth(), todayStart.getDate());\n    var p1 = api('/users/user-tabs-v1').then(function (rows) {\n      state.tabs = (rows && rows.tabs) || rows || [];\n    }).catch(function () {});\n    var p2 = api('/dial/call-statistics-v2', {\n      after: todayStart.toISOString(), before: new Date().toISOString(),\n      offset: 0, count: 100, connectionState: 'disconnected'\n    }).then(function (d) {\n      var map = {};\n      ((d && d.items) || []).forEach(function (it) {\n        if (it.userId && it.userStatus) map[it.userId] = { status: it.userStatus, since: it.userStatusSince };\n      });\n      state.liveStatus = map;\n    }).catch(function () {});\n    return Promise.all([p1, p2]).then(function () {\n      state.ui.updatedAt = new Date();\n      scheduleRender();\n    });\n  }\n\n  function refreshGroups() {\n    if (isDemo()) return Promise.resolve();\n    //  count max = 100 — paga upp till 300 grupper\n    function pageGroups(offset, acc) {\n      return api('/dial/dial-groups-v2', { offset: offset, count: 100, sortBy: 'serialId', sortOrder: 'desc' })\n        .then(function (rows) {\n          rows = rows || [];\n          acc = acc.concat(rows);\n          if (rows.length === 100 && acc.length < 300) return pageGroups(offset + 100, acc);\n          return acc;\n        });\n    }\n    var p1 = pageGroups(0, [])\n      .then(function (rows) {\n        state.dialGroups = rows.filter(function (g) { return !g.deletedAt; });\n        state.groupById = {};\n        state.dialGroups.forEach(function (g) { state.groupById[g.id] = g; });\n      }).catch(function () {});\n    return p1.then(function () {\n      var ids = state.dialGroups.map(function (g) { return g.id; });\n      if (!ids.length) return;\n      return api('/dial/dial-group-contacts-v1/summaries', { dialGroupIds: ids.join(',') })\n        .then(function (rows) {\n          (rows || []).forEach(function (r) { state.summaries[r.dialGroupId] = r.summary || r; });\n        }).catch(function () {});\n    }).then(scheduleRender);\n  }\n\n  function groupsForProject(pid) {\n    return state.dialGroups.filter(function (g) { return (g.projectIds || []).indexOf(pid) !== -1; });\n  }\n  function remainingForGroup(gid) {\n    var s = state.summaries[gid] || (state.groupById[gid] && state.groupById[gid].contactSummaryCache);\n    if (!s) return null;\n    return Math.max(0, (s.contactCount || 0) - (s.reservedContactCount || 0) - (s.skippedContactCount || 0) - (s.quarantinedContactCount || 0));\n  }\n  function projectPauseState(pid) {\n    var groups = groupsForProject(pid);\n    if (!groups.length) return { paused: false, warnings: [] };\n    var pausedCount = 0, warnings = [];\n    groups.forEach(function (g) {\n      if (g.predictiveDialHaltReason === 'disabledByUser') pausedCount++;\n      else if (g.predictiveDialHaltReason) warnings.push(g.predictiveDialHaltReason);\n    });\n    return { paused: pausedCount === groups.length, partial: pausedCount > 0 && pausedCount < groups.length, warnings: warnings };\n  }\n\n  function setProjectPause(pid, pause) {\n    var groups = groupsForProject(pid);\n    if (!groups.length) { toast('Projektet har inga ringlistor'); return; }\n    if (isDemo()) {\n      groups.forEach(function (g) { g.predictiveDialHaltReason = pause ? 'disabledByUser' : null; });\n      toast((pause ? 'Projektet pausat' : 'Projektet startat') + ' (demo)');\n      render();\n      return;\n    }\n    var chain = Promise.resolve(); var fails = 0;\n    groups.forEach(function (g) {\n      chain = chain.then(function () {\n        return api('/dial/dial-groups-v1/' + g.id, {}, { method: 'PUT', body: { predictiveDialHaltReason: pause ? 'disabledByUser' : null } })\n          .catch(function () { fails++; });\n      });\n    });\n    chain.then(function () {\n      if (fails) toast(fails + ' av ' + groups.length + ' ringlistor kunde inte uppdateras (rättighet saknas?)');\n      else toast(pause ? 'Projektet pausat' : 'Projektet startat');\n      //  logga i presetState (best effort)\n      if (state.cfg && state.viewId && (state.rights.isAdmin || state.rights.manageSettings)) {\n        state.cfg.pausedProjects = state.cfg.pausedProjects || {};\n        if (pause) state.cfg.pausedProjects[pid] = { pausedAt: new Date().toISOString(), byUserId: state.me && state.me.id, byName: state.me ? userName(state.me.id) : '' };\n        else delete state.cfg.pausedProjects[pid];\n        savePresetState().catch(function () {});\n      }\n      render(); refreshGroups();\n    });\n  }\n\n  //  play / pause / forward för en lista eller ett projekt.\n  //  forward = snabba på (höj predictiveDialAggression), original sparas i cfg.boosted[gid] och återställs vid play.\n  var BOOST_AGGRESSION = 100;\n  function setDialControl(action, scope, id) {\n    var groups = scope === 'group' ? [state.groupById[id]].filter(Boolean) : groupsForProject(id);\n    if (!groups.length) { toast('Inga ringlistor'); return; }\n    state.cfg.boosted = state.cfg.boosted || {};\n    var demo = isDemo();\n    var chain = Promise.resolve(), fails = 0;\n    groups.forEach(function (g) {\n      var body = null;\n      if (action === 'pause') {\n        body = { predictiveDialHaltReason: 'disabledByUser' };\n      } else if (action === 'play') {\n        body = { predictiveDialHaltReason: null };\n        if (state.cfg.boosted[g.id] !== undefined) { body.predictiveDialAggression = state.cfg.boosted[g.id]; delete state.cfg.boosted[g.id]; }\n      } else if (action === 'forward') {\n        if (state.cfg.boosted[g.id] === undefined) state.cfg.boosted[g.id] = (g.predictiveDialAggression != null ? g.predictiveDialAggression : 50);\n        body = { predictiveDialHaltReason: null, predictiveDialAggression: BOOST_AGGRESSION };\n      }\n      if (!body) return;\n      if (demo) {\n        if ('predictiveDialHaltReason' in body) g.predictiveDialHaltReason = body.predictiveDialHaltReason;\n        if ('predictiveDialAggression' in body) g.predictiveDialAggression = body.predictiveDialAggression;\n      } else {\n        chain = chain.then(function () { return api('/dial/dial-groups-v1/' + g.id, {}, { method: 'PUT', body: body }).catch(function () { fails++; }); });\n      }\n    });\n    var label = action === 'pause' ? 'Pausat' : (action === 'forward' ? 'Snabbar på' : 'Kör');\n    chain.then(function () {\n      if (fails) toast(fails + ' listor kunde inte uppdateras (rättighet?)');\n      else toast(label + (demo ? ' (demo)' : ''));\n      if (state.cfg && state.viewId && (state.rights.isAdmin || state.rights.manageSettings)) savePresetState().catch(function () {});\n      render(); if (!demo) refreshGroups();\n    });\n  }\n\n  //  Medlyssning (cohearing): POST /dial/cohearing-v1/user/<uid>. Ljud går via appens telefoniwidget.\n  //  Kräver rättighet CallsCreateCohearingAll/OwnTeam + aktiv telefoni (easyTelecomUserCallId hos lyssnaren).\n  function startCohearing(uid) {\n    if (isDemo()) { toast('Medlyssning startad för ' + userName(uid) + ' (demo)'); return; }\n    api('/dial/cohearing-v1/user/' + uid, {}, { method: 'POST', body: { whisper: false } })\n      .then(function () { toast('Medlyssning startad — ljud via telefoniwidgeten'); })\n      .catch(function (e) {\n        if (e && e.status === 403) toast('Du saknar rättighet för medlyssning');\n        else toast('Kunde inte starta medlyssning — kräver aktiv telefoni');\n      });\n  }\n\n  //  Logga ut säljare: POST /users/users-v1/logout/<uid>. Loggar ut användaren från alla enheter.\n  function logoutUser(uid) {\n    var name = userName(uid);\n    if (isDemo()) { toast('Loggade ut ' + name + ' (demo)'); return; }\n    api('/users/users-v1/logout/' + uid, {}, { method: 'POST' })\n      .then(function () { toast(name + ' utloggad'); })\n      .catch(function (e) { toast(e && e.status === 403 ? 'Saknar rättighet att logga ut säljare' : 'Kunde inte logga ut ' + name); });\n  }\n\n  //  Samtalskontroller per säljare (Företag-drilldown). Logga ut + Skicka meddelande fungerar via API.\n  //  Medlyssna/Viska/Ta över kräver LYSSNARENS aktiva easyTelecomUserCallId (telefoniwidgeten) — kan inte\n  //  startas enbart via vy-API, så de visas inaktiverade tills telefoniintegration finns.\n  var COHEAR_HINT = 'Kräver ett aktivt samtal i telefoniwidgeten — starta samtal där först';\n  function renderUserControls(uid, status) {\n    var h = '<div class=\"pb2-callctl\">';\n    h += '<button class=\"pb2-cbtn\" data-pb2-msguser=\"' + uid + '\" title=\"Skicka meddelande till säljaren\"><i class=\"fas fa-comment-dots\"></i>Meddelande</button>';\n    h += '<button class=\"pb2-cbtn\" disabled title=\"' + COHEAR_HINT + '\"><i class=\"fas fa-headphones\"></i>Medlyssna</button>';\n    h += '<button class=\"pb2-cbtn\" disabled title=\"' + COHEAR_HINT + '\"><i class=\"fas fa-microphone\"></i>Viska</button>';\n    h += '<button class=\"pb2-cbtn\" disabled title=\"' + COHEAR_HINT + '\"><i class=\"fas fa-random\"></i>Ta över samtal</button>';\n    h += '<button class=\"pb2-cbtn pb2-cdanger\" data-pb2-logout=\"' + uid + '\" title=\"Logga ut säljaren\"><i class=\"fas fa-sign-out-alt\"></i>Logga ut</button>';\n    h += '</div>';\n    return h;\n  }\n  //  Kompakt ikon-variant för tabellrader (Företag). Samma åtgärder, bara ikoner.\n  function userActionIcons(uid) {\n    return '<div class=\"pb2-tacts\">' +\n      '<button class=\"pb2-tact\" data-pb2-msguser=\"' + uid + '\" title=\"Skicka meddelande\"><i class=\"fas fa-comment-dots\"></i></button>' +\n      '<button class=\"pb2-tact\" disabled title=\"' + COHEAR_HINT + '\"><i class=\"fas fa-headphones\"></i></button>' +\n      '<button class=\"pb2-tact\" disabled title=\"' + COHEAR_HINT + '\"><i class=\"fas fa-microphone\"></i></button>' +\n      '<button class=\"pb2-tact\" disabled title=\"' + COHEAR_HINT + '\"><i class=\"fas fa-random\"></i></button>' +\n      '<button class=\"pb2-tact pb2-tdanger\" data-pb2-logout=\"' + uid + '\" title=\"Logga ut\"><i class=\"fas fa-sign-out-alt\"></i></button>' +\n      '</div>';\n  }\n\n  function fetchGroupStats(gid) {\n    if (isDemo()) {\n      var sum = state.summaries[gid] || {};\n      var called = Math.max(0, (sum.contactCount || 0) - (remainingForGroup(gid) || 0));\n      state.ui.drillData = { calls: called + 12, answered: Math.round((called + 12) * 0.5) };\n      return;\n    }\n    var w = windowRange();\n    var base = { dialGroupIds: gid, after: w.from.toISOString(), before: w.to.toISOString(), connectionState: 'disconnected', sortBy: 'date', sortOrder: 'desc' };\n    var answeredQ = {}; for (var k in base) answeredQ[k] = base[k]; answeredQ.isConnected = true; answeredQ.voicemail = false;\n    Promise.all([\n      api('/dial/calls-v1/count', base).catch(function () { return { count: 0 }; }),\n      api('/dial/calls-v1/count', answeredQ).catch(function () { return { count: 0 }; })\n    ]).then(function (res) {\n      state.ui.drillData = { calls: (res[0] && res[0].count) || 0, answered: (res[1] && res[1].count) || 0 };\n      render();\n    });\n  }\n\n  /* ---------------- persistens ---------------- */\n  function savePresetState() {\n    if (!state.viewId) return Promise.reject(new Error('no view id'));\n    //  Skicka allas värden för de personaliserbara nycklarna (så mina egna aldrig läcker till presetet),\n    //  men state.cfg för allt annat (pausedProjects, boosted, staffing, revenue, demoMode …).\n    var payload = clone(state.cfg);\n    if (state.baseSettings) SETTINGS_KEYS.forEach(function (k) { payload[k] = clone(state.baseSettings[k]); });\n    return api('/users/views-v1/' + state.viewId, {}, { method: 'PUT', body: { presetState: payload } });\n  }\n  var prefsTimer = null;\n  function savePrefs() {\n    if (prefsTimer) clearTimeout(prefsTimer);\n    prefsTimer = setTimeout(function () {\n      api('/users/users-v1/me', {}, { method: 'PUT', body: { reference: { pbPrefs: state.prefs }, replaceReference: false } }).catch(function () {});\n    }, 800);\n  }\n\n  /* ---------------- demoläge ----------------\n     Aktiveras i vyns inställningsvy (Inställningar > Vyer > Playbook > Inställningsvy) via\n     presetState.demoMode, eller med ?pbdemo=1 för snabbtest. Ingen riktig data hämtas.\n     Fem lägen som cyklar var 6:e sekund, från toppfart (flest i samtal) till stiltje. */\n  var DEMO_STATES = [\n    { label: 'Toppfart', dist: [0.60, 0.25, 0.10, 0.05], activity: 1.35, svar: 0.55, drop: 0.02, hit: 0.34 },\n    { label: 'Bra drag', dist: [0.45, 0.25, 0.15, 0.15], activity: 1.15, svar: 0.48, drop: 0.04, hit: 0.27 },\n    { label: 'Stabilt',  dist: [0.30, 0.30, 0.15, 0.25], activity: 0.95, svar: 0.41, drop: 0.06, hit: 0.20 },\n    { label: 'Trögt',    dist: [0.15, 0.25, 0.20, 0.40], activity: 0.70, svar: 0.33, drop: 0.09, hit: 0.13 },\n    { label: 'Stiltje',  dist: [0.05, 0.10, 0.10, 0.75], activity: 0.45, svar: 0.26, drop: 0.13, hit: 0.07 }\n  ];\n  var demo = { idx: 0, cycle: 0 };\n\n  function demoBuildStatics() {\n    var names = ['Elsa Lindqvist', 'Hugo Bergström', 'Alva Nilsson', 'Liam Öberg', 'Maja Ekholm',\n      'Noah Sandberg', 'Vera Lundin', 'Elias Holm', 'Stella Åkesson', 'Adam Sjögren',\n      'Klara Dahl', 'Oscar Wikström', 'Signe Palm', 'Melvin Åström', 'Ines Falk',\n      'Viggo Rehn', 'Juni Almgren', 'Frans Hedberg'];\n    var projDefs = [\n      { id: 'demo-p1', name: 'Fiber Nord' },\n      { id: 'demo-p2', name: 'Elavtal Företag' },\n      { id: 'demo-p3', name: 'Försäkring Privat' },\n      { id: 'demo-p4', name: 'Mötesbokning Syd' },\n      { id: 'demo-p5', name: 'Vinterkampanjen' }\n    ];\n    var groupDefs = [\n      ['Heta leads', 'Återkontakter'], ['Kampanjlistan', 'Kalla listan'],\n      ['Nykund maj', 'Vunna tillbaka'], ['Bokningsbara', 'Eftermiddagslistan'], ['Sprint 1', 'Sprint 2']\n    ];\n    state.projects = projDefs;\n    state.users = {};\n    state.demoUsers = names.map(function (n, i) {\n      var uid = 'demo-u' + (i < 10 ? '0' : '') + i;\n      state.users[uid] = { id: uid, firstName: n.split(' ')[0], lastName: n.split(' ')[1] };\n      return { id: uid, projectId: projDefs[i % projDefs.length].id, base: 34 + ((i * 7) % 30) };\n    });\n    state.dialGroups = [];\n    projDefs.forEach(function (p, pi) {\n      groupDefs[pi].forEach(function (gname, gi) {\n        state.dialGroups.push({\n          id: 'demo-g' + pi + gi, name: gname, projectIds: [p.id], deletedAt: null,\n          isShuffle: gi === 0, isCallback: false, predictiveDialHaltReason: null,\n          accessTeamIds: [], ownerTeamIds: []\n        });\n      });\n    });\n    state.groupById = {};\n    state.dialGroups.forEach(function (g) { state.groupById[g.id] = g; });\n    state.dialTags = { 'demo-t1': 'Ja tack', 'demo-t2': 'Nej tack', 'demo-t3': 'Återkom senare', 'demo-t4': 'Ej svar' };\n    state.orderTags = { 'demo-o1': 'Nyteckning', 'demo-o2': 'Uppgradering', 'demo-o3': 'Vinterkampanj' };\n  }\n\n  function demoApply() {\n    var def = DEMO_STATES[demo.idx];\n    var nowIso = new Date().toISOString();\n    function jit(v, spread) { return v * (1 - spread + Math.random() * spread * 2); }\n\n    //  statusar: fördela per projekt enligt dist, rotera med cykeln så folk byter status\n    var tabs = [], liveStatus = {};\n    state.projects.forEach(function (p) {\n      var members = state.demoUsers.filter(function (u) { return u.projectId === p.id; });\n      var groups = state.dialGroups.filter(function (g) { return g.projectIds[0] === p.id; });\n      members.forEach(function (u, mi) {\n        var slot = (mi + demo.cycle) % members.length;\n        var acc = 0, si = 3;\n        for (var d = 0; d < 4; d++) {\n          acc += def.dist[d];\n          if (slot / members.length < acc) { si = d; break; }\n        }\n        var status = STATUSES[si].id;\n        tabs.push({ userId: u.id, projectId: p.id, dialGroupId: groups[(mi + demo.cycle) % groups.length].id,\n          isUserActive: status !== 'inactive' || Math.random() > 0.5, isHidden: false, heartbeatAt: nowIso });\n        liveStatus[u.id] = { status: status, since: new Date(Date.now() - Math.floor(20000 + Math.random() * 200000)).toISOString() };\n      });\n    });\n    state.tabs = tabs;\n    state.liveStatus = liveStatus;\n\n    //  fönsterstatistik\n    var agg = newAgg();\n    var tagShare = { 'demo-t1': 0.30, 'demo-t2': 0.34, 'demo-t3': 0.22, 'demo-t4': 0.14 };\n    state.demoUsers.forEach(function (u) {\n      var calls = Math.round(jit(u.base * def.activity, 0.10));\n      var connected = Math.round(calls * jit(def.svar, 0.12));\n      var orders = Math.round(connected * jit(def.hit, 0.25));\n      var signed = Math.max(0, Math.round(orders * 0.7));\n      var dur = connected * jit(150000, 0.2);\n      var hours = 6;\n      function fill(b, hp) {\n        b.calls += calls; b.connected += connected; b.duration += dur; b.orders += orders; b.signed += signed;\n        for (var h = 0; h < hours; h++) b.hours[hp + 'h' + h] = true;\n      }\n      fill(bucketOf(agg.proj, u.projectId), u.projectId);\n      fill(bucketOf(agg.user, u.id), u.id);\n      fill(puBucket(agg, u.projectId, u.id), 'pu');\n      for (var t in tagShare) {\n        var n = Math.round(connected * tagShare[t]);\n        incMap(agg.callTagByProj, u.projectId, t, n);\n        incMap(agg.callTagByUser, u.id, t, n);\n      }\n      incMap(agg.orderTagByProj, u.projectId, 'demo-o1', Math.round(orders * 0.6));\n      incMap(agg.orderTagByProj, u.projectId, 'demo-o2', Math.round(orders * 0.25));\n      incMap(agg.orderTagByProj, u.projectId, 'demo-o3', Math.max(0, orders - Math.round(orders * 0.85)));\n      incMap(agg.offerStatusByProj, u.projectId, 'signed', signed);\n      incMap(agg.offerStatusByProj, u.projectId, 'pending', Math.max(0, orders - signed));\n      incMap(agg.offerStatusByProj, u.projectId, 'distributed', Math.round(orders * 0.4));\n    });\n    state.projects.forEach(function (p) {\n      var b = bucketOf(agg.proj, p.id);\n      b.drops = Math.round(b.connected * jit(def.drop, 0.3) / (1 - def.drop));\n    });\n    state.stats = agg;\n\n    //  ringlistornas kvar-räknare — realistiska volymer (behov ~jämförbart med bemanning)\n    state.dialGroups.forEach(function (g, gi) {\n      var total = 11000 + (gi % 5) * 2000;              //  11k–19k adresser per lista\n      var frac = 0.25 + ((demo.cycle * (3 + gi)) % 50) / 100; //  25–75 % bearbetat\n      state.summaries[g.id] = { contactCount: total, reservedContactCount: Math.round(total * frac),\n        skippedContactCount: 30 + (gi % 5), quarantinedContactCount: 10 + (gi % 4) };\n    });\n    state.ui.updatedAt = new Date();\n    scheduleRender();\n  }\n\n  //  Demo: syntetisk timserie senaste 8 dagar + idag (per projekt + per säljare) för stapeldiagrammet.\n  function demoChartSeries() {\n    var agg = newAgg();\n    var now = new Date();\n    var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n    function rnd(seed) { var x = Math.sin(seed) * 10000; return x - Math.floor(x); }\n    state.demoUsers.forEach(function (u, ui) {\n      for (var dOff = 15; dOff >= 0; dOff--) {\n        var d = new Date(today.getTime() - dOff * 86400000);\n        var wend = (d.getDay() === 0 || d.getDay() === 6);\n        var maxH = dOff === 0 ? Math.max(now.getHours(), 17) : 20;\n        //  dag-till-dag-variation + dygnskurva (topp mitt på dagen) → tydliga staplar, inte platt\n        var dayMul = 0.55 + rnd(dOff * 13 + ui * 5) * 0.9;\n        for (var hr = 8; hr <= maxH && hr <= 23; hr++) {\n          var hourMul = 0.3 + 1.15 * Math.exp(-Math.pow((hr - 12.5) / 2.8, 2));\n          var base = (wend ? 0.25 : 1) * (hr > 17 ? 0.5 : 1) * hourMul * dayMul;\n          var seed = ui * 31 + dOff * 7 + hr;\n          var calls = Math.round((2 + rnd(seed) * 9) * base);\n          if (!calls) continue;\n          var hk = dayKey(d) + 'T' + pad2(hr);\n          seriesAdd(agg, 'proj', u.projectId, 'calls', hk, calls);\n          seriesAdd(agg, 'user', u.id, 'calls', hk, calls);\n          var orders = Math.round(calls * (0.15 + rnd(seed + 1) * 0.15));\n          if (orders) { seriesAdd(agg, 'proj', u.projectId, 'orders', hk, orders); seriesAdd(agg, 'user', u.id, 'orders', hk, orders); }\n          var signed = Math.round(orders * 0.7);\n          if (signed) { seriesAdd(agg, 'proj', u.projectId, 'signed', hk, signed); seriesAdd(agg, 'user', u.id, 'signed', hk, signed); }\n        }\n      }\n    });\n    state.chartStats = agg;\n  }\n\n  //  Demo-prognos: dagsvis intäkt per månad (nuvarande + 3 föregående + samma månad förra året)\n  function demoForecast() {\n    var now = new Date();\n    function rnd(seed) { var x = Math.sin(seed) * 10000; return x - Math.floor(x); }\n    state.forecast.byMonth = {};\n    [0, 1, 2, 3, 12].forEach(function (o) {\n      var d = new Date(now.getFullYear(), now.getMonth() - o, 1);\n      var dim = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();\n      var key = d.getFullYear() + '-' + pad2(d.getMonth() + 1);\n      var growth = 1 + (2 - o) * 0.06;                    //  svag tillväxt mot nuet\n      var rev = [], calls = [];\n      var maxDay = (o === 0) ? now.getDate() : dim;\n      for (var day = 1; day <= dim; day++) {\n        var date = new Date(d.getFullYear(), d.getMonth(), day);\n        var wd = date.getDay();\n        var wknd = (wd === 0 || wd === 6) ? 0.15 : 1;\n        var monthEndBump = day > dim - 4 ? 1.4 : 1;        //  månadsslut-topp (säsong)\n        var seed = o * 97 + day * 7;\n        var base = 9000 * growth * wknd * monthEndBump * (0.8 + rnd(seed) * 0.5);\n        if (day > maxDay) { rev.push(0); calls.push(0); }\n        else { rev.push(Math.round(base)); calls.push(Math.round(base / 22)); }\n      }\n      state.forecast.byMonth[key] = { rev: rev, calls: calls, headcount: 18 };\n    });\n    state.forecast.demoBuilt = true;\n  }\n\n  //  demo: förifyll lönsamhets-config (TB + kostnad + lönekostnad + mål/timme + baslinjer) så\n  //  speedometrar, indikatorer och diagram syns direkt utan manuell uppsättning.\n  function demoMoney() {\n    var bs = ensureRevScopes();\n    var all = bs.all;\n    all.drivers = [{ type: 'signed', value: 1000, cost: 300 }];\n    all.laborPerHour = 250;\n    all.hourlyTargets = { sales: 3, calls: 12, contacts: 6, profit: 2000 };\n    all.baselines = all.baselines || {};\n    all.baselines.projected = all.baselines.projected || { value: 200000, dir: 'up' };\n    all.baselines.perEmployee = all.baselines.perEmployee || { value: 12000, dir: 'up' };\n    all.baselines.actual = all.baselines.actual || { value: 150000, dir: 'up' };\n    all.baselines.contactDays = all.baselines.contactDays || { value: 20, dir: 'down' };\n  }\n  function demoStart() {\n    demoBuildStatics();\n    demoChartSeries();\n    demoMoney();\n    demoForecast();\n    demoApply();\n    state.timers.push(setInterval(function () {\n      demo.cycle++;\n      demo.idx = (demo.idx + 1) % DEMO_STATES.length;\n      demoApply();\n    }, 6000));\n  }\n\n  /* ---------------- boot ---------------- */\n  function boot() {\n    var m = (location.pathname || '').match(/\\/views\\/([a-f0-9]{24})/);\n    state.viewId = m ? m[1] : null;\n\n    var pMe = api('/users/users-v1/me').then(function (me) { state.me = me; });\n    var pUsers = api('/users/users-v1').then(function (rows) {\n      (rows || []).forEach(function (u) { if (!u.suspension) state.users[u.id] = u; });\n      //  prefs från egen reference\n      var me = (rows || []).filter(function (u) { return u.id === (state.me && state.me.id); })[0];\n      return null;\n    });\n    var pProjects = api('/users/projects-v1').then(function (rows) {\n      state.projects = (rows && rows.projects) || rows || [];\n    });\n    var pViews = api('/users/views-v1').then(function (rows) {\n      var views = (rows && rows.views) || rows || [];\n      var v = null;\n      for (var i = 0; i < views.length; i++) if (views[i].id === state.viewId) v = views[i];\n      state.view = v;\n      var cfg = defaultCfg();\n      if (v && v.presetState && typeof v.presetState === 'object') {\n        var ps = v.presetState;\n        for (var k in cfg) if (ps[k] !== undefined) cfg[k] = ps[k];\n        //  djup-defaults\n        if (!cfg.statusesVisible) cfg.statusesVisible = defaultCfg().statusesVisible;\n        if (!cfg.hitRate) cfg.hitRate = defaultCfg().hitRate;\n      }\n      state.cfg = cfg;\n      //  allas värden för de personaliserbara nycklarna (baslinje för \"Spara för alla\"/återställning)\n      state.baseSettings = {}; SETTINGS_KEYS.forEach(function (k) { state.baseSettings[k] = clone(cfg[k]); });\n    }).catch(function () { state.cfg = defaultCfg(); state.baseSettings = {}; SETTINGS_KEYS.forEach(function (k) { state.baseSettings[k] = clone(state.cfg[k]); }); });\n    var pDialTags = api('/dial/tags-v1').then(function (d) {\n      var tags = (d && d.tags) || d || [];\n      tags.forEach(function (t) { state.dialTags[t.id] = t.name; });\n    }).catch(function () {});\n    var pOrderTags = api('/orders/tags-v1').then(function (d) {\n      var tags = (d && d.tags) || d || [];\n      tags.forEach(function (t) { state.orderTags[t.id] = t.name; });\n    }).catch(function () {});\n\n    Promise.all([pMe, pUsers, pProjects, pViews, pDialTags, pOrderTags]).then(function () {\n      //  prefs\n      var myRef = (state.me && state.me.reference) || {};\n      state.prefs = myRef.pbPrefs || {};\n      if (!state.prefs.timeFilter) state.prefs.timeFilter = 'today';\n      if (!state.prefs.mainTab) state.prefs.mainTab = 'projects';\n      if (!state.prefs.cardTab) state.prefs.cardTab = {};\n      if (!state.prefs.cardSf) state.prefs.cardSf = {};\n      if (!state.prefs.settings) state.prefs.settings = {};\n      applyPersonalSettings(); //  överlagra allas inställningar med mina (om jag sparat egna)\n      if (!state.prefs.chartMetric) state.prefs.chartMetric = 'calls';\n      if (!state.prefs.chartMode) state.prefs.chartMode = 'hour';\n      if (!state.prefs.bottomTab) state.prefs.bottomTab = 'pins';\n      if (state.prefs.chartWeekends === undefined) state.prefs.chartWeekends = false;\n      state.rights = computeRights();\n      state.urlDemo = /[?&]pbdemo=1/.test(location.search || '');\n      state.booted = true;\n      if (isDemo()) {\n        demoStart();\n      } else {\n        render();\n        refreshGroups();\n        refreshLive();\n        refreshStats();\n        loadChartStats();\n        if (state.prefs.mainTab === 'prognos') ensureForecast();\n        state.timers.push(setInterval(refreshLive, 10000));\n        state.timers.push(setInterval(refreshGroups, 60000));\n        state.timers.push(setInterval(refreshStats, 60000));\n        state.timers.push(setInterval(loadChartStats, 120000));\n      }\n      state.timers.push(setInterval(tick, 1000));\n    }).catch(function (e) {\n      state.bootError = e && e.message;\n      render();\n    });\n  }\n\n  /* ---------------- kontrast-probe ---------------- */\n  function contrastProbe() {\n    var root = document.getElementById('pb2-root');\n    if (!root) return;\n    try {\n      var probe = document.createElement('div');\n      probe.style.cssText = 'position:absolute;visibility:hidden;background:var(--color-primary,#1565c0)';\n      root.appendChild(probe);\n      var bg = getComputedStyle(probe).backgroundColor;\n      root.removeChild(probe);\n      var mm = bg.match(/(\\d+)[, ]+(\\d+)[, ]+(\\d+)/);\n      if (!mm) return;\n      var lum = (0.299 * mm[1] + 0.587 * mm[2] + 0.114 * mm[3]) / 255;\n      var fg = lum > 0.6 ? '#1a2a33' : '#ffffff';\n      if (root.style.getPropertyValue('--pb2-primary-fg') !== fg) root.style.setProperty('--pb2-primary-fg', fg);\n    } catch (e) {}\n  }\n\n  /* ---------------- render ---------------- */\n  //  scheduleRender = debouncad PÅ-PLATS-uppdatering (bevarar scroll/animation/modaler).\n  //  Full render() anropas direkt vid strukturella/användarutlösta ändringar.\n  var renderQueued = false;\n  function scheduleRender() {\n    if (renderQueued) return;\n    renderQueued = true;\n    setTimeout(function () { renderQueued = false; liveUpdate(); }, 30);\n  }\n\n  function tick() {\n    contrastProbe();\n    var app = document.getElementById('pb2-app');\n    if (app && app.getAttribute('data-pb2-stamp') !== String(state.renderStamp)) render();\n    //  ticka status-tider\n    var els = document.querySelectorAll('[data-pb2-since]');\n    for (var i = 0; i < els.length; i++) {\n      var t = new Date(els[i].getAttribute('data-pb2-since')).getTime();\n      if (!isFinite(t)) continue;\n      var s = Math.max(0, Math.floor((Date.now() - t) / 1000));\n      els[i].textContent = (s >= 3600 ? Math.floor(s / 3600) + 'h ' : '') + pad2(Math.floor((s % 3600) / 60)) + ':' + pad2(s % 60);\n    }\n  }\n\n  function statusCounts(projList) {\n    var counts = { connected: 0, predictiveAwaitingCall: 0, evaluatingCall: 0, inactive: 0, total: 0, active: 0 };\n    var pids = {};\n    projList.forEach(function (p) { pids[p.id] = true; });\n    var byUser = freshTabs();\n    for (var uid in byUser) {\n      var t = byUser[uid];\n      if (!t.projectId || !pids[t.projectId]) continue;\n      var ls = state.liveStatus[uid];\n      var st = (ls && ls.status) || t.callStatus || 'inactive';\n      counts.total++;\n      if (t.isUserActive) counts.active++;\n      if (counts[st] !== undefined) counts[st]++;\n    }\n    return counts;\n  }\n\n  //  Toppbar: huvudflikar Projekt/Företag (pill) + tidsval (pill) + kugghjul\n  function renderTop() {\n    var prefs = state.prefs;\n    var mt = prefs.mainTab || 'projects';\n    var h = '<div class=\"pb2-top\">';\n    //  huvudflikar: Projekt / Företag / Bemanning\n    h += '<div class=\"pb2-tabrow\">';\n    h += '<button class=\"pb2-pill pb2-big' + (mt === 'projects' ? ' pb2-on' : '') + '\" data-pb2-main=\"projects\">Projekt</button>';\n    h += '<button class=\"pb2-pill pb2-big' + (mt === 'company' ? ' pb2-on' : '') + '\" data-pb2-main=\"company\">Företag</button>';\n    h += '<button class=\"pb2-pill pb2-big' + (mt === 'staffing' ? ' pb2-on' : '') + '\" data-pb2-main=\"staffing\">Bemanning</button>';\n    h += '<button class=\"pb2-pill pb2-big' + (mt === 'prognos' ? ' pb2-on' : '') + '\" data-pb2-main=\"prognos\">Prognos</button>';\n    h += '</div>';\n    //  höger: tidsval + uppdaterad + kugghjul\n    h += '<div class=\"pb2-top-right\">';\n    //  tidsfiltren dolda tills vidare (behålls i koden) — sätt PB2_SHOW_TIMEFILTERS=true för att visa\n    if (window.PB2_SHOW_TIMEFILTERS) {\n      h += '<div class=\"pb2-tabrow\">';\n      TIME_FILTERS.forEach(function (tf) {\n        h += '<button class=\"pb2-pill' + (prefs.timeFilter === tf.id ? ' pb2-on' : '') + '\" data-pb2-tf=\"' + tf.id + '\">' + tf.label + '</button>';\n      });\n      h += '</div>';\n    }\n    if (state.historyNote) h += '<span class=\"pb2-updated\">' + esc(state.historyNote) + '</span>';\n    else if (state.ui.updatedAt) h += '<span class=\"pb2-updated\">Uppdaterad ' + pad2(state.ui.updatedAt.getHours()) + ':' + pad2(state.ui.updatedAt.getMinutes()) + ':' + pad2(state.ui.updatedAt.getSeconds()) + '</span>';\n    //  kugghjul för alla: admins får fulla inställningar, övriga en personlig mini-panel\n    h += '<button class=\"pb2-iconbtn\" data-pb2-act=\"settings\" title=\"' + (canEditSettings() ? 'Inställningar' : 'Mina inställningar') + '\"><i class=\"fas fa-cog\"></i></button>';\n    h += '</div></div>';\n    return h;\n  }\n\n  //  Stora statusrutor (används i Företag-vyn) — sf-scope avgör filter-nyckel\n  function renderBigStatusNav(projList, activeFilter, sfScope) {\n    var cfg = state.cfg || defaultCfg();\n    var counts = statusCounts(projList);\n    var visible = STATUSES.filter(function (s) { return !(cfg.statusesVisible && cfg.statusesVisible[s.id] === false); });\n    var h = '<div class=\"pb2-statusnav\" style=\"--pb2-nav-cols:' + visible.length + '\">';\n    visible.forEach(function (s) {\n      var on = activeFilter === s.id;\n      var iconStyle = 'background:' + hexA(s.color, on ? 0.22 : 0.12) + ';color:' + s.color;\n      h += '<div class=\"pb2-statustile' + (on ? ' pb2-on' : '') + '\" data-pb2-sf=\"' + sfScope + ':' + s.id + '\">' +\n        '<div class=\"pb2-st-icon\" style=\"' + iconStyle + '\"><i class=\"fas ' + s.icon + '\"></i></div>' +\n        '<div class=\"pb2-st-meta\"><div class=\"pb2-st-count\">' + counts[s.id] + '</div>' +\n        '<div class=\"pb2-st-label\">' + s.label + '</div></div></div>';\n    });\n    h += '</div>';\n    return h;\n  }\n  //  hex (#rrggbb) -> rgba med alpha\n  function hexA(hex, a) {\n    var m = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);\n    if (!m) return hex;\n    return 'rgba(' + parseInt(m[1], 16) + ',' + parseInt(m[2], 16) + ',' + parseInt(m[3], 16) + ',' + a + ')';\n  }\n\n  function renderUserRow(u, pid) {\n    var s = statusDef(u.status);\n    var g = u.dialGroupId ? state.groupById[u.dialGroupId] : null;\n    var sinceHtml = u.since ? '<span data-pb2-since=\"' + esc(u.since) + '\"></span>' : '';\n    var subTitle = g ? g.name + ' · ' + s.label : s.label;\n    var mid = '';\n    if (state.cfg && state.cfg.showUserGroup) {\n      mid = '<span class=\"pb2-user-group\" title=\"' + esc(g ? g.name : '') + '\">' + esc(g ? g.name : '–') + '</span>';\n    }\n    //  reservera lyssna-knappens plats även när den saknas → statuskolumnen hoppar inte\n    var listen = u.status === 'connected' ? '<button class=\"pb2-listen\" data-pb2-cohear=\"' + u.userId + '\" title=\"Lyssna (medlyssning)\"><i class=\"fas fa-headphones\"></i></button>' : '<span class=\"pb2-listen-spacer\" aria-hidden=\"true\"></span>';\n    return '<div class=\"pb2-user\" data-pb2-drill=\"user:' + u.userId + ':' + (pid || '') + '\" title=\"' + esc(userName(u.userId)) + ' — ' + esc(subTitle) + '\">' +\n      '<span class=\"pb2-dot\" style=\"background:' + s.color + '\" title=\"' + s.label + '\"></span>' +\n      '<span class=\"pb2-user-name\">' + esc(userName(u.userId)) + '</span>' +\n      mid +\n      '<span class=\"pb2-user-sub\">' + s.label + (sinceHtml ? ' ' + sinceHtml : '') + '</span>' +\n      listen +\n      '</div>';\n  }\n\n  function activeCardSf(pid) { return (state.prefs.cardSf && state.prefs.cardSf[pid]) || null; }\n\n  //  Säljare-fliken: bara live-säljarlistan (klickbar → drilldown)\n  function renderCardLiveUsers(p) {\n    var list = liveUserList(p.id);\n    var sf = activeCardSf(p.id);\n    if (sf) list = list.filter(function (u) { return u.status === sf; });\n    if (!list.length) return '<div class=\"pb2-empty\">' + (sf ? 'Ingen med den statusen.' : 'Ingen inloggad just nu.') + '</div>';\n    var h = '<div class=\"pb2-userlist\">';\n    list.forEach(function (u) { h += renderUserRow(u, p.id); });\n    h += '</div>';\n    return h;\n  }\n  //  Listor-fliken: bara ringlistorna (klickbara → drilldown)\n  function renderCardLists(p) {\n    var groups = groupsForProject(p.id);\n    var h = '';\n    if (!groups.length) h += '<div class=\"pb2-empty\">Inga ringlistor i projektet.</div>';\n    else groups.forEach(function (g) { h += renderGroupRow(g, p.id); });\n    if (state.rights.controlDialer) h += '<button class=\"pb2-addbtn\" data-pb2-slinga=\"' + p.id + '\"><i class=\"fas fa-plus-circle\"></i>Lägg till slinga</button>';\n    return h;\n  }\n\n  function renderGroupRow(g, pid) {\n    var r = remainingForGroup(g.id);\n    var halt = g.predictiveDialHaltReason;\n    return '<div class=\"pb2-grow pb2-clickable\" data-pb2-drill=\"group:' + g.id + ':' + pid + '\">' +\n      '<i class=\"fas ' + (g.isCallback ? 'fa-undo' : (g.isShuffle ? 'fa-random' : 'fa-list')) + '\"></i>' +\n      '<span class=\"pb2-gname\">' + esc(g.name) + '</span>' +\n      (halt === 'disabledByUser' ? '<i class=\"fas fa-pause-circle\" title=\"Pausad\" style=\"color:var(--color-danger,#e53935);font-size:13px\"></i>' : (halt ? '<i class=\"fas fa-exclamation-triangle\" title=\"' + esc(haltLabel(halt)) + '\" style=\"color:var(--color-neutral,#f4511e);font-size:13px\"></i>' : '')) +\n      '<span class=\"pb2-gleft\">' + (r === null ? '' : r + ' kvar') + '</span></div>';\n  }\n\n  //  Säljare: rader = säljare, kolumner = alla metriker (horisontell scroll), avvikelsefärg mot snitt\n  function renderCardUsers(p) {\n    var s = state.stats;\n    var uids = {};\n    if (s && s.pu[p.id]) for (var uid in s.pu[p.id]) if (uid !== 'none' && uid !== PSEUDO_USER) uids[uid] = true;\n    liveUserList(p.id).forEach(function (u) { uids[u.userId] = true; });\n    var sf = activeCardSf(p.id);\n    var rows = Object.keys(uids);\n    if (sf) {\n      var live = {};\n      liveUserList(p.id).forEach(function (u) { live[u.userId] = u.status; });\n      rows = rows.filter(function (uid) { return live[uid] === sf; });\n    }\n    if (!rows.length) return '<div class=\"pb2-empty\">Ingen data för fönstret.</div>';\n\n    var per = {};\n    rows.forEach(function (uid) { per[uid] = computeMetrics(p.id, uid); });\n    var avg = {};\n    METRICS.forEach(function (c) {\n      var vals = rows.map(function (uid) { return per[uid][c.id]; }).filter(function (v) { return isFinite(v); });\n      avg[c.id] = vals.length ? vals.reduce(function (a, b) { return a + b; }, 0) / vals.length : NaN;\n    });\n    rows.sort(function (a, b) { return (per[b].calls || 0) - (per[a].calls || 0); });\n\n    var h = '<div class=\"pb2-tblwrap\"><table class=\"pb2-tbl\"><thead><tr><th>Säljare</th>';\n    METRICS.forEach(function (c) { h += '<th>' + c.label + '</th>'; });\n    h += '</tr></thead><tbody>';\n    rows.forEach(function (uid) {\n      h += '<tr><td title=\"' + esc(userName(uid)) + '\">' + esc(userName(uid)) + '</td>';\n      METRICS.forEach(function (c) {\n        var v = per[uid][c.id];\n        h += '<td class=\"' + deviationClass(c.id, v, avg[c.id]) + '\">' + fmtVal(c.fmt, v) + metricInd(c.id, v) + '</td>';\n      });\n      h += '</tr>';\n    });\n    h += '</tbody></table></div>';\n    return h;\n  }\n\n  //  Utfall: samtalstaggar / ordertaggar / avtalsstatus med antal + procent\n  function renderCardOutcomes(p) {\n    var s = state.stats;\n    if (!s) return '<div class=\"pb2-empty\">Laddar…</div>';\n    var out = '';\n    function section(title, map, nameMap, totalOverride) {\n      var keys = Object.keys(map || {});\n      if (!keys.length) return;\n      var total = totalOverride;\n      if (!total) { total = 0; keys.forEach(function (k) { total += map[k]; }); }\n      keys.sort(function (a, b) { return map[b] - map[a]; });\n      out += '<div class=\"pb2-subhead\">' + title + '</div>';\n      keys.forEach(function (k) {\n        var name = nameMap ? (nameMap[k] || 'Okänd') : k;\n        out += '<div class=\"pb2-outrow\"><span class=\"pb2-oname\">' + esc(name) + '</span>' +\n          '<span class=\"pb2-ocount\">' + map[k] + '</span>' +\n          '<span class=\"pb2-opct\">' + fmtPct(total ? map[k] / total : NaN) + '</span></div>';\n      });\n    }\n    var b = scopeBucket(p.id, null);\n    section('Samtalstaggar', s.callTagByProj[p.id], state.dialTags, b.calls || null);\n    section('Ordertaggar', s.orderTagByProj[p.id], state.orderTags);\n    section('Avtalsstatus', s.offerStatusByProj[p.id], OFFER_STATUS_SV);\n    return out || '<div class=\"pb2-empty\">Inga utfall i fönstret.</div>';\n  }\n\n  //  Adresser i projektets ringlistor (totalt + kvar/ouppringda)\n  function projectAddresses(pid) {\n    var total = 0, remaining = 0, hasRem = false;\n    groupsForProject(pid).forEach(function (g) {\n      var s = state.summaries[g.id] || g.contactSummaryCache || {};\n      total += s.contactCount || 0;\n      var r = remainingForGroup(g.id);\n      if (r !== null) { remaining += r; hasRem = true; }\n    });\n    return { total: total, remaining: hasRem ? remaining : null };\n  }\n  //  Kontakter/h = bearbetade adresser (samtal) per aktiv timme i fönstret\n  function contactsPerHour(pid) {\n    var b = scopeBucket(pid, null); var hours = 0; for (var h in b.hours) hours++;\n    return hours ? b.calls / hours : 0;\n  }\n  //  Bemanning: Bemanningskalkyl — hur många heltider projektet behöver vid nuvarande takt\n  //  hela bemanningskalkylen för ett projekt\n  function computeStaffing(pid) {\n    var cfg = mergedCfg();\n    var st = cfg.staffing || { monthlyHours: 165, defaultRate: 100, rateByProject: {} };\n    var addr = projectAddresses(pid);\n    var rate = (st.rateByProject && st.rateByProject[pid] != null) ? st.rateByProject[pid] : (st.defaultRate != null ? st.defaultRate : 100);\n    var netto = Math.round(addr.total * rate / 100);\n    var kph = contactsPerHour(pid);\n    var mojliga = kph ? netto / kph : 0;\n    var monthly = st.monthlyHours || 165;\n    var behov = monthly ? mojliga / monthly : 0;\n    var current = liveUserList(pid).length;\n    return { total: addr.total, remaining: addr.remaining, rate: rate, netto: netto, kph: kph, mojliga: mojliga, monthly: monthly, behov: behov, current: current, diff: behov - current, valid: !!(kph && addr.total) };\n  }\n  //  rekommendation: \"+3,9\" (röd, addera) / \"−3,9\" (grön, subtrahera) / \"±0\"\n  function staffingRec(s) {\n    if (!s.valid) return { txt: '–', col: 'var(--color-text-4,#9e9e9e)' };\n    if (s.diff > 0.5) return { txt: '+' + fmtDec(s.diff), col: 'var(--color-danger,#e53935)' };\n    if (s.diff < -0.5) return { txt: '−' + fmtDec(-s.diff), col: 'var(--color-good,#43a047)' };\n    return { txt: '±0', col: 'var(--color-text-3,#757575)' };\n  }\n  var STAFFING_TIP = 'Nettoadresser = antal adresser × bearbetningsgrad. Möjliga arbetstimmar = nettoadresser / kontakter per timme. Behov (heltider) = möjliga arbetstimmar / arbetstimmar per heltid. + betyder addera personal, − betyder att personal kan flyttas. Kontakter/h och inloggade uppdateras live.';\n  //  kolumner i bemanningstabellen. editable = klick öppnar input (Grad). dir = riktning för mål-färg.\n  var STAFFING_COLS = [\n    { id: 'total',    label: 'Adresser',    fmt: 'int', dir: 'up' },\n    { id: 'rate',     label: 'Grad',        fmt: 'pct100', dir: 'up', editable: true },\n    { id: 'netto',    label: 'Netto',       fmt: 'int', dir: 'up' },\n    { id: 'remaining',label: 'Kvar',        fmt: 'int', dir: 'neutral' },\n    { id: 'kph',      label: 'Kontakter/h', fmt: 'dec', dir: 'up' },\n    { id: 'mojliga',  label: 'Möjliga h',   fmt: 'int', dir: 'neutral' },\n    { id: 'behov',    label: 'Behov',       fmt: 'dec', dir: 'neutral' },\n    { id: 'current',  label: 'Inloggade',   fmt: 'int', dir: 'up' }\n  ];\n  function staffingTargets() { return ((mergedCfg().staffing || {}).targets) || {}; }\n  function fmtStaffCell(fmt, v) {\n    if (v === null || v === undefined || (typeof v === 'number' && !isFinite(v))) return '–';\n    if (fmt === 'pct100') return v + '%';\n    if (fmt === 'dec') return fmtDec(v);\n    return fmtInt(v);\n  }\n  //  färgklass mot mål (target + dir); tomt = ingen färg\n  function staffTargetClass(colId, v) {\n    var t = staffingTargets()[colId];\n    if (!t || t.value == null || t.value === '' || !t.dir || t.dir === 'neutral') return '';\n    if (v == null || !isFinite(v)) return '';\n    var target = Number(t.value);\n    if (v === target) return '';\n    var better = t.dir === 'up' ? v >= target : v <= target;\n    return better ? 'pb2-good' : 'pb2-bad';\n  }\n\n  //  ---- gauges (SVG ring + halvring) ----\n  function ringGauge(pct, color, size) {\n    size = size || 76; var r = size / 2 - 6, cx = size / 2, cy = size / 2, C = 2 * Math.PI * r;\n    var off = C * (1 - Math.max(0, Math.min(1, pct)));\n    return '<svg width=\"' + size + '\" height=\"' + size + '\" viewBox=\"0 0 ' + size + ' ' + size + '\">' +\n      '<circle cx=\"' + cx + '\" cy=\"' + cy + '\" r=\"' + r + '\" fill=\"none\" stroke=\"var(--color-background-3,#eee)\" stroke-width=\"8\"/>' +\n      '<circle cx=\"' + cx + '\" cy=\"' + cy + '\" r=\"' + r + '\" fill=\"none\" stroke=\"' + color + '\" stroke-width=\"8\" stroke-linecap=\"round\" stroke-dasharray=\"' + C.toFixed(1) + '\" stroke-dashoffset=\"' + off.toFixed(1) + '\" transform=\"rotate(-90 ' + cx + ' ' + cy + ')\"/></svg>';\n  }\n  function halfRingGauge(pct, color, w) {\n    w = w || 120; var h = w / 2 + 8, r = w / 2 - 8, cx = w / 2, cy = w / 2;\n    var C = Math.PI * r;\n    var off = C * (1 - Math.max(0, Math.min(1, pct)));\n    function arc(stroke, dash, doff) { return '<path d=\"M' + (cx - r) + ' ' + cy + ' A ' + r + ' ' + r + ' 0 0 1 ' + (cx + r) + ' ' + cy + '\" fill=\"none\" stroke=\"' + stroke + '\" stroke-width=\"9\" stroke-linecap=\"round\"' + (dash ? ' stroke-dasharray=\"' + C.toFixed(1) + '\" stroke-dashoffset=\"' + doff.toFixed(1) + '\"' : '') + '/>'; }\n    return '<svg width=\"' + w + '\" height=\"' + h + '\" viewBox=\"0 0 ' + w + ' ' + h + '\">' + arc('var(--color-background-3,#eee)') + arc(color, true, off) + '</svg>';\n  }\n\n  //  Bemanning-vyn (global): gauges + tabell med kalkyl, mål-färg, inline-editerbar grad\n  function renderStaffingView(projList) {\n    var monthly = (mergedCfg().staffing || {}).monthlyHours || 165;\n    //  totaler för gauges\n    var totAddr = 0, totNet = 0, totRem = 0, hasRem = false, totBehov = 0, totCur = 0, anyValid = false, kphSum = 0, kphN = 0;\n    var rows = projList.map(function (p) {\n      var s = computeStaffing(p.id);\n      totAddr += s.total; totNet += s.netto; totCur += s.current;\n      if (s.remaining !== null) { totRem += s.remaining; hasRem = true; }\n      if (s.valid) { totBehov += s.behov; anyValid = true; }\n      if (s.kph) { kphSum += s.kph; kphN++; }\n      return { p: p, s: s };\n    });\n    var balans = totBehov ? totCur / totBehov : (totCur ? 1 : 0);        //  1 = perfekt bemannad\n    var bearbetat = totAddr ? (totAddr - (hasRem ? totRem : 0)) / totAddr : 0;\n    var avgKph = kphN ? kphSum / kphN : 0;\n    var balCol = balans >= 0.95 && balans <= 1.15 ? 'var(--color-good,#43a047)' : (balans < 0.95 ? 'var(--color-danger,#e53935)' : 'var(--color-neutral,#f4511e)');\n\n    var h = '<div class=\"pb2-prog\">';\n    //  gauges\n    h += '<div class=\"pb2-gauges\">';\n    h += '<div class=\"pb2-gauge\">' + halfRingGauge(Math.min(1, balans / 1.5), balCol) +\n      '<div class=\"pb2-gauge-meta\"><div class=\"pb2-gauge-val\" style=\"color:' + balCol + '\">' + fmtPct(balans) + '</div><div class=\"pb2-gauge-label\">Bemanningsbalans</div><div class=\"pb2-gauge-sub\">' + fmtInt(totCur) + ' av ' + (anyValid ? fmtDec(totBehov) : '–') + ' behov</div></div></div>';\n    h += '<div class=\"pb2-gauge\">' + ringGauge(bearbetat, 'var(--color-primary,#1565c0)') +\n      '<div class=\"pb2-gauge-meta\"><div class=\"pb2-gauge-val\">' + fmtPct(bearbetat) + '</div><div class=\"pb2-gauge-label\">Adresser bearbetade</div><div class=\"pb2-gauge-sub\">' + (hasRem ? fmtInt(totRem) + ' kvar' : '–') + '</div></div></div>';\n    var kphTarget = staffingTargets().kph;\n    var kphPct = (kphTarget && kphTarget.value) ? Math.min(1, avgKph / Number(kphTarget.value)) : Math.min(1, avgKph / 40);\n    h += '<div class=\"pb2-gauge\">' + ringGauge(kphPct, 'var(--color-good,#43a047)') +\n      '<div class=\"pb2-gauge-meta\"><div class=\"pb2-gauge-val\">' + fmtDec(avgKph) + '</div><div class=\"pb2-gauge-label\">Snitt kontakter/h</div><div class=\"pb2-gauge-sub\">' + (kphTarget && kphTarget.value ? 'mål ' + kphTarget.value : 'alla projekt') + '</div></div></div>';\n    h += '</div>';\n\n    //  tabell\n    h += '<div class=\"pb2-copanel\"><div class=\"pb2-cohead\"><span class=\"pb2-cotitle\">Bemanningskalkyl</span>' +\n      '<span class=\"pb2-tip\"><i class=\"fas fa-question-circle\" style=\"color:var(--color-text-4,#9e9e9e);cursor:help\"></i><span class=\"pb2-tipbox\">' + esc(STAFFING_TIP) + '</span></span>' +\n      '<span style=\"margin-left:auto;font-size:13px;color:var(--color-text-3,#757575);margin-right:8px\">' + monthly + ' h/heltid</span>';\n    if (state.rights.isAdmin || state.rights.manageSettings) h += '<button class=\"pb2-iconbtn\" data-pb2-act=\"staffing-settings\" title=\"Bemanningsinställningar\"><i class=\"fas fa-cog\"></i></button>';\n    h += '</div>';\n    h += '<div class=\"pb2-tblwrap\"><table class=\"pb2-tbl\"><thead><tr><th style=\"text-align:left\">Projekt</th>';\n    STAFFING_COLS.forEach(function (c) { h += '<th>' + c.label + '</th>'; });\n    h += '<th>+/−</th></tr></thead><tbody>';\n    rows.forEach(function (r) {\n      var s = r.s, rec = staffingRec(s);\n      h += '<tr><td style=\"text-align:left\" title=\"' + esc(r.p.name) + '\">' + esc(r.p.name) + '</td>';\n      STAFFING_COLS.forEach(function (c) {\n        var v = s[c.id];\n        var cls = staffTargetClass(c.id, v);\n        if (c.editable) {\n          h += '<td><span class=\"pb2-editcell ' + cls + '\" data-pb2-editrate=\"' + r.p.id + '\">' + fmtStaffCell(c.fmt, v) + '</span></td>';\n        } else {\n          h += '<td class=\"' + cls + '\">' + fmtStaffCell(c.fmt, v) + '</td>';\n        }\n      });\n      h += '<td style=\"color:' + rec.col + ';font-weight:600\">' + rec.txt + '</td></tr>';\n    });\n    //  totalrad\n    var totDiff = totBehov - totCur;\n    var totRec = anyValid ? (totDiff > 0.5 ? { txt: '+' + fmtDec(totDiff), col: 'var(--color-danger,#e53935)' } : totDiff < -0.5 ? { txt: '−' + fmtDec(-totDiff), col: 'var(--color-good,#43a047)' } : { txt: '±0', col: 'var(--color-text-3,#757575)' }) : { txt: '–', col: 'var(--color-text-4,#9e9e9e)' };\n    h += '<tr style=\"border-top:2px solid var(--color-background-3,#eee)\"><td style=\"text-align:left;color:var(--color-text-1,#212121)\">Totalt</td>' +\n      '<td>' + fmtInt(totAddr) + '</td><td></td><td>' + fmtInt(totNet) + '</td><td>' + (hasRem ? fmtInt(totRem) : '–') + '</td><td>' + fmtDec(avgKph) + '</td><td></td>' +\n      '<td>' + (anyValid ? fmtDec(totBehov) : '–') + '</td><td>' + fmtInt(totCur) + '</td>' +\n      '<td style=\"color:' + totRec.col + ';font-weight:600\">' + totRec.txt + '</td></tr>';\n    h += '</tbody></table></div></div></div>';\n    return h;\n  }\n\n  /* ================= Prognos (intäktsprognos med säsong) ================= */\n  var DEFAULT_DRIVERS = [{ type: 'signed', value: 1000 }];\n  //  Prognosinställningar är per scope: 'all' (Alla projekt) + varje projekt-id.\n  //  byScope[scope] = { drivers:[...]?, baselines:{...} }. Saknar ett projekt egna drivers ärver det 'all'.\n  //  Läs-väg (mergedCfg, migreringssäker); skriv-väg = ensureRevScopes/ensureScopeCfg mot state.cfg.\n  function revScopes() {\n    var r = (mergedCfg().revenue) || {};\n    if (r.byScope) return r.byScope;\n    return { all: { drivers: (r.drivers && r.drivers.length ? r.drivers : DEFAULT_DRIVERS), baselines: r.baselines || {} } };\n  }\n  function ensureRevScopes() {\n    var r = state.cfg.revenue = state.cfg.revenue || {};\n    if (!r.byScope) {\n      r.byScope = { all: { drivers: (r.drivers && r.drivers.length ? r.drivers.slice() : DEFAULT_DRIVERS.slice()), baselines: r.baselines || {} } };\n    }\n    return r.byScope;\n  }\n  function ensureScopeCfg(scope) {\n    var bs = ensureRevScopes();\n    if (!bs[scope]) bs[scope] = { baselines: {} };\n    bs[scope].baselines = bs[scope].baselines || {};\n    return bs[scope];\n  }\n  //  intäktskällor för ett projekt (egna om satta, annars ärvda från 'all').\n  //  En explicit array (även tom) respekteras → man kan ta bort ALLA källor och få tomt läge.\n  function scopeDrivers(pid) {\n    var bs = revScopes();\n    if (pid && pid !== 'all' && bs[pid] && bs[pid].drivers) return bs[pid].drivers;\n    if (bs.all && bs.all.drivers) return bs.all.drivers;\n    return DEFAULT_DRIVERS;\n  }\n  function scopeBaselines(scope) {\n    var bs = revScopes();\n    return (bs[scope] && bs[scope].baselines) || {};\n  }\n  //  har projektet egna intäktskällor (annars ärver det Alla projekt)?\n  function scopeHasOwnDrivers(scope) {\n    if (scope === 'all') return true;\n    var bs = revScopes();\n    return !!(bs[scope] && bs[scope].drivers);\n  }\n  function indicatorsOn() { return ((mergedCfg().revenue || {}).showIndicators) !== false; }\n  function fmtKr(n) {\n    n = Math.round(n || 0);\n    return String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ' ') + ' kr';\n  }\n  /* ===== Lönsamhet: intäkt/kostnad/vinst per aktiv agent-timme + gauges ===== */\n  //  driver.value = Täckningsbidrag (intäkt kr/konvertering); driver.cost = kostnad kr/konvertering.\n  //  laborPerHour = lönekostnad kr/aktiv agent-timme (per scope, ärver 'all'). hourlyTargets på 'all' = gauge-mål.\n  function scopeLaborPerHour(scope) {\n    var bs = revScopes();\n    if (bs[scope] && bs[scope].laborPerHour != null && bs[scope].laborPerHour !== '') return Number(bs[scope].laborPerHour) || 0;\n    if (bs.all && bs.all.laborPerHour != null && bs.all.laborPerHour !== '') return Number(bs.all.laborPerHour) || 0;\n    return 0;\n  }\n  function hourlyTargets() { return (revScopes().all || {}).hourlyTargets || {}; }\n  var HOURLY_KEYS = ['sales', 'calls', 'contacts', 'profit'];\n  function moneyConfigured(scope) {\n    var d = scopeDrivers(scope);\n    var anyIncome = d.some(function (x) { return Number(x.value) > 0; });\n    var anyCost = d.some(function (x) { return Number(x.cost) > 0; }) || scopeLaborPerHour(scope) > 0;\n    return anyIncome && anyCost;\n  }\n  function gaugesOn() { return (mergedCfg().showGauges) !== false; }\n  function gaugesConfigured() {\n    if (!gaugesOn()) return false;\n    var t = hourlyTargets();\n    return HOURLY_KEYS.every(function (k) { return Number(t[k]) > 0; }) && moneyConfigured('all');\n  }\n  //  konverteringar/intäkt/kostnad för ett scope över projList (live-buckets idag). Använder varje projekts drivers.\n  function moneyRates(projList, scope) {\n    var pids = scope === 'all' ? projList.map(function (p) { return p.id; }) : [scope];\n    var income = 0, cost = 0, conv = 0, calls = 0, connected = 0, hoursSet = {};\n    pids.forEach(function (pid) {\n      var b = scopeBucket(pid, null);\n      calls += b.calls || 0; connected += b.connected || 0;\n      for (var hk in b.hours) hoursSet[hk] = true;\n      scopeDrivers(pid).forEach(function (d) {\n        var c = 0;\n        if (d.type === 'signed') c = b.signed || 0;\n        else if (d.type === 'order') c = d.tagId ? (((state.stats && state.stats.orderTagByProj[pid]) || {})[d.tagId] || 0) : (b.orders || 0);\n        else if (d.type === 'callTag') c = ((state.stats && state.stats.callTagByProj[pid]) || {})[d.tagId] || 0;\n        conv += c; income += c * (Number(d.value) || 0); cost += c * (Number(d.cost) || 0);\n      });\n    });\n    var hours = 0; for (var h in hoursSet) hours++;\n    var labor = scopeLaborPerHour(scope) * hours;\n    var profit = income - cost - labor;\n    return {\n      hours: hours, conv: conv, calls: calls, connected: connected,\n      income: income, cost: cost, labor: labor, profit: profit,\n      salesPerHour: hours ? conv / hours : 0, callsPerHour: hours ? calls / hours : 0,\n      contactsPerHour: hours ? connected / hours : 0, incomePerHour: hours ? income / hours : 0,\n      costPerHour: hours ? (cost + labor) / hours : 0, profitPerHour: hours ? profit / hours : 0\n    };\n  }\n  //  Intäkt/kostnad/vinst per timme idag (för linjediagrammet). Kostnad = konv-kostnad + lönekostnad×aktiva agenter/timme.\n  function hourlyPnl(projList, scope) {\n    var pids = scope === 'all' ? projList.map(function (p) { return p.id; }) : [scope];\n    var now = new Date();\n    var today = dayKey(new Date(now.getFullYear(), now.getMonth(), now.getDate()));\n    var endHr = now.getHours(); if (endHr < 9) endHr = isDemo() ? 14 : 9; if (endHr > 20) endHr = 20;\n    var startHr = 8;\n    var labor = scopeLaborPerHour(scope);\n    var userSet = {};\n    pids.forEach(function (pid) { var pu = (state.stats && state.stats.pu[pid]) || {}; for (var u in pu) if (u !== 'none' && u !== PSEUDO_USER) userSet[u] = true; });\n    var out = { hours: [], income: [], cost: [], profit: [] };\n    for (var hr = startHr; hr <= endHr; hr++) {\n      var hk = today + 'T' + pad2(hr), inc = 0, cst = 0;\n      pids.forEach(function (pid) {\n        var src = chartSource(pid);\n        scopeDrivers(pid).forEach(function (d) {\n          var c = 0;\n          if (d.type === 'signed') c = (src.signed && src.signed[hk]) || 0;\n          else if (d.type === 'order') c = (src.orders && src.orders[hk]) || 0;\n          inc += c * (Number(d.value) || 0); cst += c * (Number(d.cost) || 0);\n        });\n      });\n      var active = 0;\n      for (var uu in userSet) { var us = chartSource(null, uu); if (us.calls && us.calls[hk]) active++; }\n      var lab = labor * active;\n      out.hours.push(hr); out.income.push(inc); out.cost.push(cst + lab); out.profit.push(inc - cst - lab);\n    }\n    return out;\n  }\n  //  Multi-linje-diagram: intäkt (blå), kostnad (röd), vinst (grön). Delad y-skala inkl. negativ vinst.\n  var PNL_SERIES = [['income', 'Intäkt', 'var(--color-primary,#1565c0)'], ['cost', 'Kostnad', 'var(--color-danger,#e53935)'], ['profit', 'Vinst', 'var(--color-good,#43a047)']];\n  function renderPnlChart(pnl, opts) {\n    opts = opts || {};\n    var w = 900, h = opts.height || 210, padL = 8, padR = 8, padT = 12, padB = 20;\n    var n = pnl.hours.length;\n    if (n < 2) return '<div class=\"pb2-empty\" style=\"text-align:center;padding:30px\">För lite data idag för lönsamhetskurva.</div>';\n    var allVals = pnl.income.concat(pnl.cost).concat(pnl.profit);\n    var yMax = Math.max.apply(null, allVals.concat([1]));\n    var yMin = Math.min.apply(null, allVals.concat([0]));\n    if (yMin > 0) yMin = 0;\n    var span = (yMax - yMin) || 1;\n    function X(i) { return padL + (w - padL - padR) * i / (n - 1); }\n    function Y(v) { return padT + (h - padT - padB) * (1 - (v - yMin) / span); }\n    var s = '<svg class=\"pb2-pnlchart\" viewBox=\"0 0 ' + w + ' ' + h + '\" preserveAspectRatio=\"none\" height=\"' + h + '\">';\n    //  nollinje\n    if (yMin < 0) { var zy = Y(0); s += '<line x1=\"' + padL + '\" y1=\"' + zy.toFixed(1) + '\" x2=\"' + (w - padR) + '\" y2=\"' + zy.toFixed(1) + '\" stroke=\"var(--color-background-4,#d5d5d5)\" stroke-width=\"1\" stroke-dasharray=\"3 3\"/>'; }\n    PNL_SERIES.forEach(function (ser) {\n      var arr = pnl[ser[0]];\n      var d = arr.map(function (v, i) { return (i ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(v).toFixed(1); }).join(' ');\n      s += '<path d=\"' + d + '\" fill=\"none\" stroke=\"' + ser[2] + '\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>';\n      var lx = X(n - 1), ly = Y(arr[n - 1]);\n      s += '<circle cx=\"' + lx.toFixed(1) + '\" cy=\"' + ly.toFixed(1) + '\" r=\"3.5\" fill=\"' + ser[2] + '\"/>';\n    });\n    //  tim-etiketter (var 2:a)\n    for (var i = 0; i < n; i++) if (i % 2 === 0 || i === n - 1) s += '<text x=\"' + X(i).toFixed(1) + '\" y=\"' + (h - 6) + '\" font-size=\"11\" fill=\"var(--color-text-4,#9e9e9e)\" text-anchor=\"middle\">' + pad2(pnl.hours[i]) + '</text>';\n    s += '</svg>';\n    return s;\n  }\n  function pnlLegend(pnl) {\n    var h = '<div class=\"pb2-pnllegend\">';\n    PNL_SERIES.forEach(function (ser) {\n      var tot = pnl[ser[0]].reduce(function (a, b) { return a + b; }, 0); //  dagens summa\n      h += '<span class=\"pb2-pnlleg\"><span class=\"pb2-pnldot\" style=\"background:' + ser[2] + '\"></span>' + ser[1] + ' <b style=\"color:' + ser[2] + '\">' + fmtKr(tot) + '</b></span>';\n    });\n    return h + '</div>';\n  }\n  //  Minimalistisk mätare — samma rena halvring som Bemanningsbalans (ingen nål/tick).\n  function gaugeColor(value, target) {\n    if (!(target > 0)) return 'var(--color-primary,#1565c0)';\n    return value >= target ? 'var(--color-good,#43a047)' : (value >= 0.6 * target ? 'var(--color-neutral,#f4511e)' : 'var(--color-danger,#e53935)');\n  }\n  function gaugeCard(label, value, target, fmt, w) {\n    var valS = fmt === 'kr' ? fmtKr(value) : fmtDec(value);\n    var tgtS = fmt === 'kr' ? fmtKr(target) : fmtDec(target);\n    var col = gaugeColor(value, target);\n    var pct = target > 0 ? value / target : (value > 0 ? 1 : 0);\n    return '<div class=\"pb2-gcard\"><div class=\"pb2-gcard-title\">' + esc(label) + '</div>' +\n      halfRingGauge(Math.min(1, pct), col, w || 132) +\n      '<div class=\"pb2-gcard-val\" style=\"color:' + col + '\">' + valS + '</div>' +\n      '<div class=\"pb2-gcard-sub\">mål ' + tgtS + '/tim</div></div>';\n  }\n  //  intäkt för ett dygns-agg (proj-filtrerat) enligt varje projekts egna drivers\n  function dayRevenueFromAgg(agg, pids) {\n    if (!agg) return 0;\n    var rev = 0;\n    pids.forEach(function (pid) {\n      scopeDrivers(pid).forEach(function (d) {\n        var v = d.value || 0, cnt = 0;\n        if (d.type === 'signed') cnt = (agg.proj[pid] && agg.proj[pid].signed) || 0;\n        else if (d.type === 'order') cnt = d.tagId ? ((agg.orderTagByProj[pid] || {})[d.tagId] || 0) : ((agg.proj[pid] && agg.proj[pid].orders) || 0);\n        else if (d.type === 'callTag') cnt = (agg.callTagByProj[pid] || {})[d.tagId] || 0;\n        rev += cnt * v;\n      });\n    });\n    return rev;\n  }\n  function dayCountFromAgg(agg, pids, field) {\n    if (!agg) return 0; var n = 0;\n    pids.forEach(function (pid) { n += (agg.proj[pid] && agg.proj[pid][field]) || 0; });\n    return n;\n  }\n  function monthMeta(offset) {\n    var now = new Date();\n    var y = now.getFullYear(), m = now.getMonth() - offset;\n    var d = new Date(y, m, 1);\n    var daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();\n    var isCurrent = offset === 0;\n    var todayD = isCurrent ? now.getDate() : daysInMonth;\n    return { year: d.getFullYear(), month: d.getMonth(), daysInMonth: daysInMonth, isCurrent: isCurrent, todayD: todayD,\n      label: d.toLocaleDateString('sv-SE', { month: 'long', year: 'numeric' }) };\n  }\n  //  dagsvis intäkt (och samtal/användare) för en månad — demo ur byMonth, annars ur dag-cachen\n  function monthDaily(offset, pids) {\n    var mm = monthMeta(offset);\n    var out = { rev: [], calls: [], users: {} };\n    if (isDemo()) {\n      var key = mm.year + '-' + pad2(mm.month + 1);\n      var dm = (state.forecast.byMonth && state.forecast.byMonth[key]) || null;\n      //  demo-lagret är org-brett → skala per projektandel (+ variation per projekt) för sparklines\n      var share = 1;\n      if (state.projects.length && pids && pids.length < state.projects.length) {\n        share = 0;\n        pids.forEach(function (pid) {\n          var ix = 0; for (var i = 0; i < state.projects.length; i++) if (state.projects[i].id === pid) ix = i;\n          share += (0.6 + ((ix * 37) % 100) / 100) / state.projects.length; //  0,6–1,6× jämnandel\n        });\n      }\n      for (var d = 1; d <= mm.daysInMonth; d++) {\n        var future = mm.isCurrent && d > mm.todayD;\n        out.rev.push(future || !dm ? 0 : Math.round((dm.rev[d - 1] || 0) * share));\n        out.calls.push(future || !dm ? 0 : Math.round((dm.calls[d - 1] || 0) * share));\n      }\n      out.headcount = dm ? Math.round(dm.headcount * share) : 0;\n      return out;\n    }\n    for (var dd = 1; dd <= mm.daysInMonth; dd++) {\n      var date = new Date(mm.year, mm.month, dd);\n      var future = mm.isCurrent && dd > mm.todayD;\n      var agg = future ? null : cacheGet(dayKey(date));\n      out.rev.push(agg ? dayRevenueFromAgg(agg, pids) : 0);\n      out.calls.push(agg ? dayCountFromAgg(agg, pids, 'calls') : 0);\n      if (agg) for (var uid in agg.user) if (uid !== 'none' && uid !== PSEUDO_USER && agg.user[uid].calls) out.users[uid] = true;\n    }\n    out.headcount = Object.keys(out.users).length;\n    return out;\n  }\n  //  normaliserad kumulativ kurva (fraktion av månadens total vid fraktion f av månaden)\n  function cumAtFraction(dailyRev, frac) {\n    var total = dailyRev.reduce(function (a, b) { return a + b; }, 0);\n    if (total <= 0) return null;\n    var n = dailyRev.length, upto = Math.max(1, Math.round(frac * n));\n    var cum = 0; for (var i = 0; i < upto && i < n; i++) cum += dailyRev[i];\n    return cum / total;\n  }\n  //  bygg prognosserie för vald månad\n  function forecastForMonth(offset, pids) {\n    var mm = monthMeta(offset);\n    var md = monthDaily(offset, pids);\n    var daysInMonth = mm.daysInMonth, todayD = mm.todayD;\n    var actualCum = [], cum = 0;\n    for (var d = 1; d <= daysInMonth; d++) { if (d <= todayD) cum += md.rev[d - 1]; actualCum.push(d <= todayD ? cum : null); }\n    var actualTotal = cum;\n    var projCum = actualCum.slice();\n    var projectedTotal = actualTotal;\n    if (mm.isCurrent && todayD < daysInMonth) {\n      //  säsong ur föregående 2 månader + samma månad förra året\n      var prevCurves = [];\n      [1, 2, 12].forEach(function (o) { var dm = monthDaily(offset + o, pids); if (dm.rev.reduce(function (a, b) { return a + b; }, 0) > 0) prevCurves.push(dm.rev); });\n      var fToday = todayD / daysInMonth;\n      function avgCurveAt(f) {\n        if (!prevCurves.length) return f; //  linjär fallback\n        var s = 0, n = 0; prevCurves.forEach(function (c) { var v = cumAtFraction(c, f); if (v !== null) { s += v; n++; } });\n        return n ? s / n : f;\n      }\n      var fracToday = avgCurveAt(fToday);\n      projectedTotal = fracToday > 0.02 ? actualTotal / fracToday : (todayD ? actualTotal * daysInMonth / todayD : 0);\n      for (var d2 = todayD + 1; d2 <= daysInMonth; d2++) {\n        var f = d2 / daysInMonth;\n        var cf = avgCurveAt(f);\n        //  förankra så framtidskurvan börjar exakt vid actualTotal\n        var frac = (cf - fracToday) / Math.max(0.0001, 1 - fracToday);\n        projCum[d2 - 1] = actualTotal + (projectedTotal - actualTotal) * Math.max(0, Math.min(1, frac));\n      }\n    }\n    var monthCalls = 0; for (var i = 0; i < todayD; i++) monthCalls += md.calls[i];\n    return { mm: mm, actualCum: actualCum, projCum: projCum, actualTotal: actualTotal, projectedTotal: projectedTotal,\n      daysInMonth: daysInMonth, todayD: todayD, headcount: md.headcount, monthCalls: monthCalls, dailyRev: md.rev };\n  }\n\n  //  ladda dag-cachen som prognosen behöver (nuvarande månad + 2 föregående), bakgrund\n  var forecastRun = 0;\n  function ensureForecast() {\n    if (isDemo()) { if (!state.forecast.demoBuilt) demoForecast(); return; }\n    var run = ++forecastRun;\n    var days = [];\n    var today = new Date(); today = new Date(today.getFullYear(), today.getMonth(), today.getDate());\n    [0, 1, 2, 12].forEach(function (o) {\n      var mm = monthMeta(o);\n      for (var d = 1; d <= mm.daysInMonth; d++) {\n        var date = new Date(mm.year, mm.month, d);\n        if (date >= today) break;\n        if (!cacheGet(dayKey(date))) days.push(date);\n      }\n    });\n    //  idag hämtas separat (färskt) av refreshStats-cachen; historik cachas permanent\n    if (!days.length) { state.forecast.loading = false; return; }\n    state.forecast.loading = true; scheduleRender();\n    var chain = Promise.resolve();\n    days.forEach(function (date) {\n      chain = chain.then(function () {\n        if (run !== forecastRun) return;\n        var end = new Date(date.getTime() + 86400000);\n        return fetchRangeAgg(date, end).then(function (a) { cacheSet(dayKey(date), a); });\n      });\n    });\n    chain.then(function () { if (run !== forecastRun) return; state.forecast.loading = false; scheduleRender(); });\n  }\n\n  //  SVG-linjediagram med mjuk gradient-fyllnad + lågopak prognoslinje\n  function sparkPath(vals, w, h, pad) {\n    var max = Math.max(1, Math.max.apply(null, vals.filter(function (v) { return v != null; })));\n    var n = vals.length;\n    return vals.map(function (v, i) {\n      if (v == null) return null;\n      var x = pad + (n === 1 ? 0 : (w - 2 * pad) * i / (n - 1));\n      var y = h - pad - (h - 2 * pad) * v / max;\n      return { x: x, y: y, i: i };\n    }).filter(Boolean);\n  }\n  //  prognos-stat-block med konfigurerbar baslinje (grön/röd indikator)\n  var PROG_BLOCKS = [\n    { id: 'projected',   label: 'Prognos denna månad',    fmt: 'kr' },\n    { id: 'perEmployee', label: 'Vinst per anställd',      fmt: 'kr' },\n    { id: 'contactDays', label: 'Dagar kvar av kontakter', fmt: 'dec' },\n    { id: 'actual',      label: 'Intäkt hittills',         fmt: 'kr' }\n  ];\n  //  indikator för ett prognosblock mot scopeens baslinje (scope = 'all' | projekt-id)\n  function progBaselineInd(id, value, scope) {\n    if (!indicatorsOn()) return '';\n    var b = scopeBaselines(scope || 'all')[id];\n    if (!b || b.value == null || b.value === '' || !b.dir || b.dir === 'neutral') return '';\n    if (value == null || !isFinite(value)) return '';\n    var target = Number(b.value);\n    var better = b.dir === 'up' ? value >= target : value <= target;\n    var col = better ? 'var(--color-good,#43a047)' : 'var(--color-danger,#e53935)';\n    return '<span class=\"pb2-block-ind\" style=\"background:' + col + '\" title=\"Baslinje ' + fmtInt(target) + '\"></span>';\n  }\n  //  chevron-indikator för de 4 KPI-korten: grön chevron upp = över baslinjen, röd chevron ner = under\n  function progBaselineChevron(id, value, scope) {\n    if (!indicatorsOn()) return '';\n    var b = scopeBaselines(scope || 'all')[id];\n    if (!b || b.value == null || b.value === '' || !b.dir || b.dir === 'neutral') return '';\n    if (value == null || !isFinite(value)) return '';\n    var target = Number(b.value);\n    var better = b.dir === 'up' ? value >= target : value <= target;\n    var col = better ? 'var(--color-good,#43a047)' : 'var(--color-danger,#e53935)';\n    var icon = better ? 'fa-chevron-up' : 'fa-chevron-down';\n    return '<i class=\"fas ' + icon + ' pb2-progchev\" style=\"color:' + col + '\" title=\"Baslinje ' + fmtInt(target) + '\"></i>';\n  }\n\n  //  full-bredds linjediagram (huvud + spark) — opts: {height, cls, compact, hover}\n  function renderForecastChart(fc, opts) {\n    opts = opts || {};\n    var w = 900, h = opts.height || 220, pad = opts.compact ? 4 : 8;\n    var sw = opts.compact ? 2 : 2.5, uniq = (fc.mm.year * 12 + fc.mm.month) + '_' + Math.round(fc.projectedTotal);\n    var max = Math.max(1, Math.max.apply(null, fc.actualCum.map(function (v) { return v || 0; })), fc.projectedTotal);\n    var n = fc.daysInMonth;\n    function X(i) { return pad + (w - 2 * pad) * i / (n - 1); }\n    function Y(v) { return h - pad - (h - 2 * pad) * (v || 0) / max; }\n    var actPts = [];\n    for (var i = 0; i < fc.todayD; i++) if (fc.actualCum[i] != null) actPts.push({ x: X(i), y: Y(fc.actualCum[i]) });\n    var projPts = [];\n    for (var j = fc.todayD - 1; j < n; j++) { var v = fc.projCum[j] != null ? fc.projCum[j] : fc.actualCum[j]; if (v != null) projPts.push({ x: X(j), y: Y(v) }); }\n    function line(pts) { return pts.map(function (p, k) { return (k ? 'L' : 'M') + p.x.toFixed(1) + ' ' + p.y.toFixed(1); }).join(' '); }\n    var actLine = line(actPts), projLine = line(projPts);\n    var area = actPts.length ? (actLine + ' L' + actPts[actPts.length - 1].x.toFixed(1) + ' ' + (h - pad) + ' L' + actPts[0].x.toFixed(1) + ' ' + (h - pad) + ' Z') : '';\n    var gid = 'pb2grad' + Math.abs(uniq.split('').reduce(function (a, c) { return a + c.charCodeAt(0); }, 0)) + '_' + Math.round(h);\n    var primary = 'var(--color-primary,#1565c0)';\n    var s = '<svg class=\"' + (opts.cls || 'pb2-progchart') + '\" viewBox=\"0 0 ' + w + ' ' + h + '\" preserveAspectRatio=\"none\" height=\"' + h + '\">';\n    s += '<defs><linearGradient id=\"' + gid + '\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\n      '<stop offset=\"0%\" stop-color=\"' + primary + '\" stop-opacity=\"0.28\"/>' +\n      '<stop offset=\"100%\" stop-color=\"' + primary + '\" stop-opacity=\"0\"/></linearGradient></defs>';\n    if (area) s += '<path d=\"' + area + '\" fill=\"url(#' + gid + ')\" stroke=\"none\"/>';\n    if (projLine) s += '<path d=\"' + projLine + '\" fill=\"none\" stroke=\"' + primary + '\" stroke-opacity=\"0.35\" stroke-width=\"' + sw + '\" stroke-dasharray=\"5 5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>';\n    if (actLine) s += '<path d=\"' + actLine + '\" fill=\"none\" stroke=\"' + primary + '\" stroke-width=\"' + sw + '\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>';\n    if (!opts.compact && actPts.length) s += '<circle cx=\"' + actPts[actPts.length - 1].x.toFixed(1) + '\" cy=\"' + actPts[actPts.length - 1].y.toFixed(1) + '\" r=\"3.5\" fill=\"' + primary + '\"/>';\n    s += '</svg>';\n    if (!opts.hover) return s;\n    //  hover: krysshår + värdebubbla. serie = kumulativt värde per dag (proj för framtid) + etikett\n    var series = [];\n    for (var k = 0; k < n; k++) {\n      var vv = fc.actualCum[k] != null ? fc.actualCum[k] : (fc.projCum[k] != null ? fc.projCum[k] : 0);\n      series.push({ d: k + 1, v: Math.round(vv), f: (k > fc.todayD - 1) });\n    }\n    return '<div class=\"pb2-chartwrap\" data-pb2-charthover=\"1\" data-pb2-series=\"' + esc(JSON.stringify(series)) + '\" data-pb2-monthlabel=\"' + esc(fc.mm.label) + '\">' +\n      s + '<div class=\"pb2-crosshair\"></div><div class=\"pb2-charttip\"></div></div>';\n  }\n  //  liten sparkline per projekt\n  function renderProgView(projList) {\n    var allPids = projList.map(function (p) { return p.id; });\n    var offset = state.forecast.monthOffset || 0;\n    var sel = state.forecast.selProject;\n    if (sel && allPids.indexOf(sel) === -1) sel = state.forecast.selProject = null;\n    var scopePids = sel ? [sel] : allPids;\n    var scopeName = sel ? (projName(sel) || 'Projekt') : 'Alla projekt';\n    var fc = forecastForMonth(offset, scopePids);\n    var mm = fc.mm;\n    //  KPI för valt scope\n    var perEmp = fc.headcount ? fc.projectedTotal / fc.headcount : 0;\n    var remainingAddr = 0, hasRem = false;\n    (sel ? [{ id: sel }] : projList).forEach(function (p) { var a = projectAddresses(p.id); if (a.remaining !== null) { remainingAddr += a.remaining; hasRem = true; } });\n    var callsPerDay = fc.todayD ? fc.monthCalls / fc.todayD : 0;\n    var contactDaysLeft = callsPerDay ? remainingAddr / callsPerDay : 0;\n    var h = '<div class=\"pb2-prog\">';\n    //  2/3-split: diagram (2) + stat-kort (1)\n    h += '<div class=\"pb2-proghero\"><div class=\"pb2-copanel pb2-proghero-chart\"><div class=\"pb2-cohead\"><span class=\"pb2-cotitle\">Prognos · ' + esc(scopeName) + '</span>' +\n      '<div class=\"pb2-monthnav\" style=\"margin-left:16px\"><button data-pb2-month=\"-1\" title=\"Föregående månad\"><i class=\"fas fa-chevron-left\"></i></button>' +\n      '<span class=\"pb2-monthlbl\">' + esc(mm.label) + '</span>' +\n      '<button data-pb2-month=\"1\"' + (offset <= 0 ? ' disabled' : '') + ' title=\"Nästa månad\"><i class=\"fas fa-chevron-right\"></i></button></div>' +\n      '<span style=\"margin-left:auto;font-size:13px;color:var(--color-text-3,#757575);margin-right:8px\">' + (mm.isCurrent ? 'Dag ' + fc.todayD + ' av ' + fc.daysInMonth : 'Avslutad månad') + '</span>' +\n      ((state.rights.isAdmin || state.rights.manageSettings) ? '<button class=\"pb2-iconbtn\" data-pb2-act=\"prognos-settings\" title=\"Prognosinställningar\"><i class=\"fas fa-cog\"></i></button>' : '') + '</div>';\n    h += '<div style=\"flex:1 1 auto;display:flex;align-items:center\">' + renderForecastChart(fc, { hover: true }) + '</div></div>';\n    //  stat-kort staplade i 1/3-kolumnen (med baslinjeindikator)\n    h += '<div class=\"pb2-proghero-kpis\">';\n    var scopeKey = sel || 'all';\n    var blockVals = { projected: fc.projectedTotal, perEmployee: perEmp, contactDays: (hasRem && callsPerDay ? contactDaysLeft : null), actual: fc.actualTotal };\n    function block(id, label, val, sub) {\n      return '<div class=\"pb2-progblock\"><span class=\"pb2-progblock-label\">' + esc(label) + '</span><span class=\"pb2-progblock-val\">' + val + progBaselineChevron(id, blockVals[id], scopeKey) + '</span>' + (sub ? '<span class=\"pb2-progblock-sub\">' + esc(sub) + '</span>' : '') + '</div>';\n    }\n    h += block('projected', mm.isCurrent ? 'Prognos denna månad' : 'Utfall månad', fmtKr(fc.projectedTotal), 'hittills ' + fmtKr(fc.actualTotal));\n    h += block('perEmployee', 'Vinst per anställd', fmtKr(perEmp), fc.headcount + ' anställda');\n    h += block('contactDays', 'Dagar kvar av kontakter', hasRem && callsPerDay ? fmtDec(contactDaysLeft) : '–', hasRem ? fmtInt(remainingAddr) + ' adresser kvar' : 'okänt');\n    h += block('actual', 'Intäkt hittills', fmtKr(fc.actualTotal), Math.round(fc.projectedTotal ? 100 * fc.actualTotal / fc.projectedTotal : 0) + '% av prognos');\n    h += '</div></div>';\n    //  lönsamhetskurva idag (intäkt/kostnad/vinst per timme) för valt scope — kräver kostnadsmodell + toggle\n    if (gaugesOn() && moneyConfigured(scopeKey)) {\n      var pnl = hourlyPnl(projList, scopeKey);\n      h += '<div class=\"pb2-copanel pb2-pnlpanel\"><div class=\"pb2-cohead\"><span class=\"pb2-cotitle\">Lönsamhet idag · ' + esc(scopeName) + '</span>' + pnlLegend(pnl) + '</div>' +\n        renderPnlChart(pnl) + '</div>';\n    }\n    //  scope-kort: Alla + varje projekt, full-bredds spark, klickbara\n    h += '<div class=\"pb2-copanel\"><div class=\"pb2-cohead\"><span class=\"pb2-cotitle\">Välj vy</span></div><div class=\"pb2-sparklist\">';\n    var showGauges = gaugesConfigured();\n    var tgAll = hourlyTargets();\n    function sparkCard(id, name, sfc) {\n      var on = (id === null ? sel === null : sel === id);\n      var cs = id === null ? 'all' : id;\n      var gaugesHtml = '';\n      if (showGauges) {\n        var smr = moneyRates(projList, cs);\n        gaugesHtml = '<div class=\"pb2-sparkgauges\">' +\n          gaugeCard('Sälj / tim', smr.salesPerHour, Number(tgAll.sales), 'dec') +\n          gaugeCard('Samtal / tim', smr.callsPerHour, Number(tgAll.calls), 'dec') +\n          gaugeCard('Kontakter / tim', smr.contactsPerHour, Number(tgAll.contacts), 'dec') +\n          '</div>';\n      }\n      return '<div class=\"pb2-sparkcard' + (on ? ' pb2-on' : '') + (showGauges ? ' pb2-sparktall' : '') + '\" data-pb2-progproj=\"' + (id === null ? 'all' : id) + '\">' +\n        '<div class=\"pb2-sparkcard-chart\">' + renderForecastChart(sfc, { height: showGauges ? 120 : 56, cls: 'pb2-sparkfull', compact: true }) + '</div>' +\n        '<div class=\"pb2-sparkcard-info\"><span class=\"pb2-sparkname\">' + esc(name) + '</span>' +\n        '<span class=\"pb2-sparkval\"><b>' + fmtKr(sfc.projectedTotal) + progBaselineInd('projected', sfc.projectedTotal, cs) + '</b><span>hittills ' + fmtKr(sfc.actualTotal) + progBaselineInd('actual', sfc.actualTotal, cs) + '</span></span>' +\n        gaugesHtml + '</div></div>';\n    }\n    h += sparkCard(null, 'Alla projekt', forecastForMonth(offset, allPids));\n    projList.forEach(function (p) { h += sparkCard(p.id, p.name, forecastForMonth(offset, [p.id])); });\n    h += '</div></div>';\n    if (state.forecast.loading) h += '<div class=\"pb2-hint\" style=\"text-align:center\">Laddar historik för säsongsjustering…</div>';\n    h += '</div>';\n    return h;\n  }\n\n  //  Pinnade nyckeltal (6 tal) — bara rutnätet\n  function renderPinsInner(p) {\n    var cfg = mergedCfg();\n    var ids = (cfg.pinnedMetrics && cfg.pinnedMetrics.length ? cfg.pinnedMetrics : DEFAULT_PINS).slice(0, 6);\n    while (ids.length < 6) ids.push(null);\n    var m = computeMetrics(p.id, null);\n    var remaining = 0, hasRemaining = false;\n    groupsForProject(p.id).forEach(function (g) {\n      var r = remainingForGroup(g.id);\n      if (r !== null) { remaining += r; hasRemaining = true; }\n    });\n    var h = '<div class=\"pb2-pins\">';\n    ids.forEach(function (id) {\n      var def = id ? pinDef(id) : null;\n      var val = '–', label = '';\n      if (def) {\n        label = def.label;\n        if (id === 'remaining') val = hasRemaining ? fmtInt(remaining) : '–';\n        else val = fmtVal(def.fmt, m[id]);\n      }\n      h += '<div class=\"pb2-pin\"><div class=\"pb2-pin-val\">' + val + (def && id !== 'remaining' ? metricInd(id, m[id]) : '') + '</div><div class=\"pb2-pin-label\">' + esc(label) + '</div></div>';\n    });\n    h += '</div>';\n    return h;\n  }\n\n  //  Bottenzon: Nyckeltal ELLER Diagram (flikar av varandra, samma yta)\n  function renderBottom(p) {\n    var bt = state.prefs.bottomTab || 'pins';\n    var h = '<div class=\"pb2-bottomzone\"><div class=\"pb2-bottomtabs\">';\n    h += '<button class=\"pb2-chip-mini' + (bt === 'pins' ? ' pb2-on' : '') + '\" data-pb2-bottom=\"pins\">Nyckeltal</button>';\n    h += '<button class=\"pb2-chip-mini' + (bt === 'chart' ? ' pb2-on' : '') + '\" data-pb2-bottom=\"chart\">Diagram</button>';\n    if (bt === 'chart') h += '<div class=\"pb2-bottomctrls\">' + chartModePills() + '</div>';\n    h += '<button class=\"pb2-notify\" style=\"margin-left:auto\" data-pb2-notify=\"' + p.id + '\" title=\"Skicka notis till projektet\"><i class=\"fas fa-comment-dots\"></i></button>';\n    h += '</div>';\n    h += '<div class=\"pb2-bottomswap' + (state.ui._animBottom ? ' pb2-fadein-sm' : '') + '\">';\n    if (bt === 'chart') h += chartBodyHtml(p.id, null, true);\n    else h += renderPinsInner(p);\n    h += '</div></div>';\n    return h;\n  }\n\n  /* ---------------- stapeldiagram (per projekt / per säljare) ---------------- */\n  function chartSource(pid, uid) {\n    var cs = state.chartStats;\n    if (!cs || !cs.series) return {};\n    var root = uid ? cs.series.user : cs.series.proj;\n    return (root && root[uid || pid]) || {};\n  }\n  var CHART_BARS = 10;\n  function buildChartBars(pid, uid, metric, mode) {\n    var src = chartSource(pid, uid);\n    var map = (src && src[metric]) || {};\n    var bars = [], max = 0;\n    var now = new Date();\n    if (mode === 'day') {\n      var weekends = state.prefs.chartWeekends === true; //  default = utan helger\n      var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n      var wd = ['sön', 'mån', 'tis', 'ons', 'tor', 'fre', 'lör'];\n      var days = [], i = 0;\n      while (days.length < CHART_BARS && i < 60) {\n        var d = new Date(today.getTime() - i * 86400000); i++;\n        if (!weekends && (d.getDay() === 0 || d.getDay() === 6)) continue;\n        days.push(d);\n      }\n      days.reverse();\n      days.forEach(function (d) {\n        var dk = dayKey(d), sum = 0;\n        for (var hh = 0; hh < 24; hh++) sum += map[dk + 'T' + pad2(hh)] || 0;\n        if (sum > max) max = sum;\n        bars.push({ label: wd[d.getDay()], value: sum, cur: d.getTime() === today.getTime() });\n      });\n    } else {\n      var H = now.getHours();\n      if (isDemo() && (H < 9 || H > 17)) H = 14; //  demo: visa alltid ett arbetstidsfönster\n      //  nuvarande timme = sista (högraste) stapeln — inga tomma framtidstimmar efter \"nu\"\n      var start = Math.max(0, H - (CHART_BARS - 1));\n      for (var hr = start; hr < start + CHART_BARS; hr++) {\n        var hk = dayKey(now) + 'T' + pad2(hr);\n        var v = map[hk] || 0;\n        if (v > max) max = v;\n        bars.push({ label: pad2(hr), value: v, cur: hr === H });\n      }\n    }\n    return { bars: bars, max: max };\n  }\n  //  metrik-/läges-väljare (delas av modal-chart och kortets diagram-flik)\n  var CHART_METRICS = [['calls', 'Samtal'], ['orders', 'Ordrar'], ['signed', 'Avtal']];\n  function chartMetricPills() {\n    var metric = state.prefs.chartMetric || 'calls';\n    var h = '<div class=\"pb2-chart-metrics\">';\n    CHART_METRICS.forEach(function (mm) {\n      h += '<button class=\"pb2-chip-mini' + (metric === mm[0] ? ' pb2-on' : '') + '\" data-pb2-cmetric=\"' + mm[0] + '\">' + mm[1] + '</button>';\n    });\n    return h + '</div>';\n  }\n  //  metriker som tre textrader (i chartens högra 25%-kolumn)\n  function chartMetricColumn() {\n    var metric = state.prefs.chartMetric || 'calls';\n    var h = '<div class=\"pb2-metriccol\">';\n    CHART_METRICS.forEach(function (mm) {\n      h += '<button class=\"pb2-metricrow-btn' + (metric === mm[0] ? ' pb2-on' : '') + '\" data-pb2-cmetric=\"' + mm[0] + '\">' + mm[1] + '</button>';\n    });\n    return h + '</div>';\n  }\n  function chartModePills() {\n    var mode = state.prefs.chartMode || 'hour';\n    var h = '<div class=\"pb2-chart-metrics\">';\n    [['hour', 'Timme'], ['day', 'Dag']].forEach(function (mm) {\n      h += '<button class=\"pb2-chip-mini' + (mode === mm[0] ? ' pb2-on' : '') + '\" data-pb2-cmode=\"' + mm[0] + '\">' + mm[1] + '</button>';\n    });\n    return h + '</div>';\n  }\n  function chartControls() { return chartMetricPills() + chartModePills(); }\n  //  withMetricCol=true: 50% staplar / 25% tal+chevron / 25% metriker som textrader (kortets diagram)\n  function chartBodyHtml(pid, uid, withMetricCol) {\n    var metric = state.prefs.chartMetric || 'calls';\n    var mode = state.prefs.chartMode || 'hour';\n    var data = buildChartBars(pid, uid, metric, mode);\n    var max = Math.max(1, data.max);\n    var vals = data.bars.map(function (b) { return b.value; });\n    var avg = vals.length ? vals.reduce(function (a, b) { return a + b; }, 0) / vals.length : 0;\n    var cur = data.bars.filter(function (b) { return b.cur; })[0];\n    if (!cur && data.bars.length) cur = data.bars[data.bars.length - 1];\n    var curVal = cur ? cur.value : 0;\n    var above = curVal >= avg;\n    var chevColor = above ? 'var(--color-good,#43a047)' : 'var(--color-danger,#e53935)';\n    var cap = (mode === 'hour' ? 'kl ' + (cur ? cur.label : '') : (cur ? cur.label : 'idag')) + ' · snitt ' + fmtDec(avg);\n    var h = '<div class=\"pb2-chart-body' + (withMetricCol ? ' pb2-chart-3col' : '') + '\"><div class=\"pb2-chart\">';\n    data.bars.forEach(function (b) {\n      var ht = Math.max(2, Math.round(100 * b.value / max));\n      h += '<div class=\"pb2-bar' + (b.cur ? ' pb2-cur' : '') + '\" style=\"height:' + ht + '%\" title=\"' + esc(b.label) + ': ' + b.value + '\"></div>';\n    });\n    h += '</div><div class=\"pb2-chart-cur\"><div class=\"pb2-cur-top\"><span class=\"pb2-cur-val\">' + fmtInt(curVal) + '</span>' +\n      '<i class=\"fas ' + (above ? 'fa-chevron-up' : 'fa-chevron-down') + ' pb2-cur-chev\" style=\"color:' + chevColor + '\"></i></div>' +\n      '<div class=\"pb2-cur-cap\">' + esc(cap) + '</div></div>';\n    if (withMetricCol) h += chartMetricColumn();\n    h += '</div>';\n    return h;\n  }\n  //  full diagram-modul (för drilldown-modalen)\n  function renderChart(pid, uid) {\n    return '<div class=\"pb2-chartzone\"><div class=\"pb2-chart-head\">' + chartControls() + '</div>' + chartBodyHtml(pid, uid) + '</div>';\n  }\n\n  /* ---------------- Företag-vyn (hela organisationen) ---------------- */\n  function computeCompanyMetrics(projList) {\n    var b = newBucket();\n    projList.forEach(function (p) { mergeBucket(b, scopeBucket(p.id, null)); });\n    var hours = 0; for (var h in b.hours) hours++;\n    var cfg = mergedCfg();\n    var hits = 0;\n    if (cfg.hitRate.mode === 'callTag' && cfg.hitRate.tagId) {\n      projList.forEach(function (p) { hits += ((state.stats && state.stats.callTagByProj[p.id]) || {})[cfg.hitRate.tagId] || 0; });\n    } else hits = cfg.hitRate.mode === 'signedOffers' ? b.signed : b.orders;\n    var cph = hours ? b.calls / hours : 0;\n    var tempoBase = metricBenchmark('tempo') || (state.cfg && state.cfg.tempoBenchmark) || 25;\n    return {\n      calls: b.calls, callsPerHour: cph, answerRate: b.calls ? b.connected / b.calls : NaN,\n      avgCallTime: b.connected ? b.duration / b.connected : NaN, orders: b.orders,\n      ordersPerHour: hours ? b.orders / hours : 0, hits: hits, hitRate: b.connected ? hits / b.connected : NaN,\n      tempo: b.calls ? Math.max(1, Math.min(10, Math.round(10 * cph / tempoBase))) : NaN,\n      connected: b.connected, drops: b.drops, dropRate: (b.drops + b.connected) ? b.drops / (b.drops + b.connected) : NaN\n    };\n  }\n\n  function renderCompanyView(projList) {\n    var companySf = state.prefs.companySf || null;\n    var tab = state.prefs.companyTab || 'users';\n    var cfg = mergedCfg();\n    var h = '<div class=\"pb2-company\">';\n    //  stora statusrutor (företagstotaler, klick filtrerar tabellen)\n    h += renderBigStatusNav(projList, companySf, 'company');\n    //  speedometrar (Sälj/Samtal/Kontakter/Vinst per aktiv timme) — bara när lönsamhet + mål är uppsatta\n    if (gaugesConfigured()) {\n      var mr = moneyRates(projList, 'all'); var tg = hourlyTargets();\n      h += '<div class=\"pb2-cogauges\">' +\n        gaugeCard('Sälj / timme', mr.salesPerHour, Number(tg.sales), 'dec') +\n        gaugeCard('Samtal / timme', mr.callsPerHour, Number(tg.calls), 'dec') +\n        gaugeCard('Kontakter / timme', mr.contactsPerHour, Number(tg.contacts), 'dec') +\n        gaugeCard('Vinst / timme', mr.profitPerHour, Number(tg.profit), 'kr') +\n        '</div>';\n    }\n    //  aggregerade pinnade nyckeltal (6, fullbredd)\n    var m = computeCompanyMetrics(projList);\n    var remaining = 0, hasRem = false;\n    projList.forEach(function (p) { groupsForProject(p.id).forEach(function (g) { var r = remainingForGroup(g.id); if (r !== null) { remaining += r; hasRem = true; } }); });\n    var ids = (cfg.pinnedMetrics && cfg.pinnedMetrics.length ? cfg.pinnedMetrics : DEFAULT_PINS).slice(0, 6);\n    while (ids.length < 6) ids.push(null);\n    h += '<div class=\"pb2-copins\">';\n    ids.forEach(function (id) {\n      var def = id ? pinDef(id) : null;\n      var val = '–', label = '';\n      if (def) { label = def.label; val = id === 'remaining' ? (hasRem ? fmtInt(remaining) : '–') : fmtVal(def.fmt, m[id]); }\n      h += '<div class=\"pb2-copin\"><div class=\"pb2-copin-val\">' + val + '</div><div class=\"pb2-copin-label\">' + esc(label) + '</div></div>';\n    });\n    h += '</div>';\n    //  panel med underflikar Säljare / Utfall\n    h += '<div class=\"pb2-copanel\">';\n    h += '<div class=\"pb2-tabrow\" style=\"margin-bottom:14px\">';\n    var subtabs = [];\n    if (state.rights.viewUserDrilldown) subtabs.push(['users', 'Säljare']);\n    subtabs.push(['outcomes', 'Utfall']);\n    if (subtabs.length === 1) tab = subtabs[0][0];\n    subtabs.forEach(function (t) { h += '<button class=\"pb2-pill' + (tab === t[0] ? ' pb2-on' : '') + '\" data-pb2-cotab=\"' + t[0] + '\">' + t[1] + '</button>'; });\n    h += '</div>';\n    h += tab === 'outcomes' ? renderCompanyOutcomes(projList) : renderCompanyUsers(projList, companySf);\n    h += '</div></div>';\n    return h;\n  }\n\n  function renderCompanyUsers(projList, sf) {\n    var s = state.stats;\n    var pids = {}; projList.forEach(function (p) { pids[p.id] = true; });\n    var uids = {};\n    projList.forEach(function (p) { if (s && s.pu[p.id]) for (var uid in s.pu[p.id]) if (uid !== 'none' && uid !== PSEUDO_USER) uids[uid] = true; });\n    var byUser = freshTabs();\n    for (var u in byUser) { if (u !== PSEUDO_USER && byUser[u].projectId && pids[byUser[u].projectId]) uids[u] = true; }\n    var rows = Object.keys(uids);\n    if (sf) rows = rows.filter(function (uid) { var ls = state.liveStatus[uid]; var st = (ls && ls.status) || (byUser[uid] && byUser[uid].callStatus) || 'inactive'; return st === sf; });\n    if (!rows.length) return '<div class=\"pb2-empty\">Ingen data för fönstret.</div>';\n    var per = {}; rows.forEach(function (uid) { per[uid] = computeMetrics(null, uid); });\n    var avg = {};\n    METRICS.forEach(function (c) { var vals = rows.map(function (uid) { return per[uid][c.id]; }).filter(function (v) { return isFinite(v); }); avg[c.id] = vals.length ? vals.reduce(function (a, b) { return a + b; }, 0) / vals.length : NaN; });\n    rows.sort(function (a, b) { return (per[b].calls || 0) - (per[a].calls || 0); });\n    var h = '<div class=\"pb2-tblwrap\"><table class=\"pb2-tbl\"><thead><tr><th>Säljare</th><th>Projekt</th>';\n    METRICS.forEach(function (c) { h += '<th>' + c.label + '</th>'; });\n    h += '<th class=\"pb2-actcol\"></th></tr></thead><tbody>';\n    rows.forEach(function (uid) {\n      var pn = (byUser[uid] && byUser[uid].projectId) ? (projName(byUser[uid].projectId) || '–') : '–';\n      var upid = (byUser[uid] && byUser[uid].projectId) || '';\n      h += '<tr class=\"pb2-urow\" data-pb2-drill=\"user:' + uid + ':' + upid + '\" title=\"Öppna säljare\"><td>' + esc(userName(uid)) + '</td>' +\n        '<td style=\"text-align:left;color:var(--color-text-3,#757575)\">' + esc(pn) + '</td>';\n      METRICS.forEach(function (c) { var v = per[uid][c.id]; h += '<td class=\"' + deviationClass(c.id, v, avg[c.id]) + '\">' + fmtVal(c.fmt, v) + metricInd(c.id, v, 'var(--color-primary,#1565c0)') + '</td>'; });\n      h += '<td class=\"pb2-actcol\">' + userActionIcons(uid) + '</td></tr>';\n    });\n    h += '</tbody></table></div>';\n    return h;\n  }\n\n  function renderCompanyOutcomes(projList) {\n    var s = state.stats; if (!s) return '<div class=\"pb2-empty\">Laddar…</div>';\n    function agg(mapName) { var out = {}; projList.forEach(function (p) { var mm = s[mapName][p.id] || {}; for (var k in mm) out[k] = (out[k] || 0) + mm[k]; }); return out; }\n    var totalCalls = 0; projList.forEach(function (p) { totalCalls += scopeBucket(p.id, null).calls; });\n    var out = '';\n    function section(title, map, nameMap, totalOverride) {\n      var keys = Object.keys(map); if (!keys.length) return;\n      var total = totalOverride; if (!total) { total = 0; keys.forEach(function (k) { total += map[k]; }); }\n      keys.sort(function (a, b) { return map[b] - map[a]; });\n      out += '<div class=\"pb2-subhead\">' + title + '</div>';\n      keys.forEach(function (k) {\n        var name = nameMap ? (nameMap[k] || 'Okänd') : k;\n        out += '<div class=\"pb2-outrow\"><span class=\"pb2-oname\">' + esc(name) + '</span><span class=\"pb2-ocount\">' + map[k] + '</span><span class=\"pb2-opct\">' + fmtPct(total ? map[k] / total : NaN) + '</span></div>';\n      });\n    }\n    section('Samtalstaggar', agg('callTagByProj'), state.dialTags, totalCalls || null);\n    section('Ordertaggar', agg('orderTagByProj'), state.orderTags);\n    section('Avtalsstatus', agg('offerStatusByProj'), OFFER_STATUS_SV);\n    return out || '<div class=\"pb2-empty\">Inga utfall i fönstret.</div>';\n  }\n\n  //  \"Övrigt\"-kort för inloggade utan (synligt) projekt — enklare kort, ingen projektstatistik\n  function renderOtherCard(others) {\n    var cfg = state.cfg || defaultCfg();\n    var counts = { connected: 0, predictiveAwaitingCall: 0, evaluatingCall: 0, inactive: 0 };\n    others.forEach(function (u) { if (counts[u.status] !== undefined) counts[u.status]++; });\n    var visible = STATUSES.filter(function (s) { return !(cfg.statusesVisible && cfg.statusesVisible[s.id] === false); });\n    var h = '<div class=\"pb2-card\" style=\"height:auto;max-height:640px\">';\n    h += '<div class=\"pb2-card-head\"><span class=\"pb2-card-title\" style=\"color:var(--color-text-3,#757575)\">Övrigt · inget projekt</span></div>';\n    h += '<div class=\"pb2-ministatus\" style=\"--pb2-nav-cols:' + visible.length + '\">';\n    visible.forEach(function (s) {\n      h += '<div class=\"pb2-mini\" style=\"cursor:default\"><i class=\"fas ' + s.icon + '\" style=\"color:' + s.color + '\"></i><b>' + counts[s.id] + '</b><span>' + s.label + '</span></div>';\n    });\n    h += '</div><div class=\"pb2-card-body\"><div class=\"pb2-subhead\">Säljare utan projekt</div><div class=\"pb2-userlist\">';\n    others.forEach(function (u) { h += renderUserRow(u, ''); });\n    h += '</div></div></div>';\n    return h;\n  }\n\n  //  kompakta play/pause/forward-ikonknappar i kortets header\n  function renderDialMini(scope, id) {\n    var groups = scope === 'group' ? [state.groupById[id]].filter(Boolean) : groupsForProject(id);\n    if (!groups.length) return '';\n    var anyHalted = groups.some(function (g) { return g.predictiveDialHaltReason === 'disabledByUser'; });\n    var anyBoosted = groups.some(function (g) { return ((state.cfg && state.cfg.boosted) || {})[g.id] !== undefined; });\n    var playOn = !anyHalted && !anyBoosted;\n    var h = '<div style=\"margin-left:auto;display:flex;gap:2px\">';\n    h += '<button class=\"pb2-playbtn' + (anyHalted ? ' pb2-arm' : '') + '\" data-pb2-dial=\"pause:' + scope + ':' + id + '\" title=\"Pausa\"><i class=\"fas fa-pause\"></i></button>';\n    h += '<button class=\"pb2-playbtn\" style=\"' + (playOn ? 'color:var(--color-good,#43a047)' : '') + '\" data-pb2-dial=\"play:' + scope + ':' + id + '\" title=\"Kör\"><i class=\"fas fa-play\"></i></button>';\n    h += '<button class=\"pb2-playbtn\" style=\"' + (anyBoosted ? 'color:var(--color-primary,#1565c0)' : '') + '\" data-pb2-dial=\"forward:' + scope + ':' + id + '\" title=\"Snabba på (predictive)\"><i class=\"fas fa-forward\"></i></button>';\n    h += '</div>';\n    return h;\n  }\n\n  function renderCard(p) {\n    var pauseState = projectPauseState(p.id);\n    var cfg = state.cfg || defaultCfg();\n    var tab = state.prefs.cardTab[p.id] || 'live';\n    if (!state.rights.viewUserDrilldown && tab === 'stats') tab = 'live';\n    var counts = statusCounts([p]);\n    var h = '<div class=\"pb2-card' + (pauseState.paused ? ' pb2-paused' : '') + '\" data-pb2-pid=\"' + p.id + '\">';\n    //  head\n    h += '<div class=\"pb2-card-head\"><span class=\"pb2-card-title\" title=\"' + esc(p.name) + '\">' + esc(p.name) + '</span>';\n    if (pauseState.paused) h += '<span class=\"pb2-badge pb2-stop\">Pausad</span>';\n    else if (pauseState.partial) h += '<span class=\"pb2-badge pb2-warn\">Delvis pausad</span>';\n    if (pauseState.warnings.length) h += '<span class=\"pb2-badge pb2-warn\" title=\"' + esc(pauseState.warnings.join(', ')) + '\"><i class=\"fas fa-exclamation-triangle\"></i></span>';\n    if (state.rights.controlDialer) h += renderDialMini('project', p.id);\n    h += '</div>';\n    var sec = (cfg.sections) || { nav: true, body: true, bottom: true, tabs: true };\n    //  flikar (Säljare / Listor / Statistik / Utfall) — ligger ÖVER statusfiltren\n    if (sec.tabs !== false && sec.body !== false) {\n      var tabs = [['live', 'Säljare'], ['lists', 'Listor']];\n      if (state.rights.viewUserDrilldown) tabs.push(['stats', 'Statistik']);\n      tabs.push(['outcomes', 'Utfall']);\n      h += '<div class=\"pb2-tabs\">';\n      tabs.forEach(function (t) {\n        h += '<button class=\"pb2-tab' + (tab === t[0] ? ' pb2-on' : '') + '\" data-pb2-tab=\"' + p.id + ':' + t[0] + '\">' + t[1] + '</button>';\n      });\n      h += '</div>';\n    }\n    //  kompakta statusrutor (icon + tal + etikett) inne i kortet — klick filtrerar kortets lista\n    if (sec.nav !== false) {\n      var visible = STATUSES.filter(function (s) { return !(cfg.statusesVisible && cfg.statusesVisible[s.id] === false); });\n      var cardSf = activeCardSf(p.id);\n      h += '<div class=\"pb2-ministatus\" style=\"--pb2-nav-cols:' + visible.length + '\">';\n      visible.forEach(function (s) {\n        var on = cardSf === s.id;\n        h += '<div class=\"pb2-mini' + (on ? ' pb2-on' : '') + '\" data-pb2-sf=\"card:' + p.id + ':' + s.id + '\" title=\"' + s.label + '\">' +\n          '<i class=\"fas ' + s.icon + '\" style=\"color:' + s.color + '\"></i>' +\n          '<b>' + counts[s.id] + '</b><span>' + s.label + '</span></div>';\n      });\n      h += '</div>';\n    }\n    //  scrollande kropp (säljare OCH listor visas aldrig samtidigt — egna flikar)\n    if (sec.body !== false) {\n      h += '<div class=\"pb2-card-body\"><div class=\"pb2-cardswap' + (state.ui._animCard === p.id ? ' pb2-fadein-sm' : '') + '\">';\n      if (tab === 'lists') h += renderCardLists(p);\n      else if (tab === 'stats') h += renderCardUsers(p);\n      else if (tab === 'outcomes') h += renderCardOutcomes(p);\n      else h += renderCardLiveUsers(p);\n      h += '</div></div>';\n    } else {\n      h += '<div style=\"flex:1 1 auto\"></div>';\n    }\n    //  bottenzon: Nyckeltal / Diagram (flikar av varandra) — personlig override kan dölja/visa\n    if (footerVisible()) h += renderBottom(p);\n    h += '</div>';\n    return h;\n  }\n\n  /* ---------------- drilldown-modal (säljare / ringlista) ---------------- */\n  function renderDrill() {\n    var d = state.ui.drill;\n    if (!d) return '';\n    var title = '', body = '';\n    if (d.kind === 'user') {\n      var uid = d.id;\n      var byUser = freshTabs();\n      var t = byUser[uid];\n      var ls = state.liveStatus[uid];\n      var status = (ls && ls.status) || (t && t.callStatus) || 'inactive';\n      var sdef = statusDef(status);\n      var g = t && t.dialGroupId ? state.groupById[t.dialGroupId] : null;\n      var sub = sdef.label + (g ? ' · ' + g.name : '') + (d.pid && projName(d.pid) ? ' · ' + projName(d.pid) : '');\n      title = '<div class=\"pb2-drill-head\"><span class=\"pb2-drill-dot\" style=\"background:' + sdef.color + '\"></span>' +\n        '<div><div class=\"pb2-modal-title\" style=\"font-size:20px\">' + esc(userName(uid)) + '</div><div class=\"pb2-drill-sub\">' + esc(sub) + '</div></div></div>';\n      body += renderUserControls(uid, status);\n      body += renderChart(d.pid || null, uid);\n      var m = computeMetrics(d.pid || null, uid);\n      body += '<div class=\"pb2-mgrid\">';\n      //  drops/droprate är globala begrepp (orutade predictive-samtal), aldrig personliga; tempo visas ej per person\n      PINNABLE.filter(function (x) { return NON_PERSONAL_METRICS.indexOf(x.id) === -1; }).forEach(function (pm) {\n        body += '<div class=\"pb2-mcell\"><div class=\"pb2-mcell-val\">' + fmtVal(pm.fmt, m[pm.id]) + '</div><div class=\"pb2-mcell-label\">' + esc(pm.label) + '</div></div>';\n      });\n      body += '</div>';\n      var s = state.stats;\n      if (s && s.callTagByUser[uid]) {\n        var mm = s.callTagByUser[uid], keys = Object.keys(mm);\n        if (keys.length) {\n          var total = 0; keys.forEach(function (k) { total += mm[k]; }); keys.sort(function (a, b) { return mm[b] - mm[a]; });\n          body += '<div class=\"pb2-drill-sec\">Samtalstaggar</div>';\n          keys.forEach(function (k) { body += '<div class=\"pb2-outrow\"><span class=\"pb2-oname\">' + esc(state.dialTags[k] || 'Okänd') + '</span><span class=\"pb2-ocount\">' + mm[k] + '</span><span class=\"pb2-opct\">' + fmtPct(total ? mm[k] / total : NaN) + '</span></div>'; });\n        }\n      }\n    } else if (d.kind === 'group') {\n      var g2 = state.groupById[d.id];\n      if (!g2) { state.ui.drill = null; return ''; }\n      var halt2 = g2.predictiveDialHaltReason;\n      title = '<div class=\"pb2-modal-title\" style=\"font-size:20px\">' + esc(g2.name) + '</div><div class=\"pb2-drill-sub\">Ringlista' + (g2.isShuffle ? ' · Blandning' : '') + (g2.isCallback ? ' · Återkontakt' : '') + (halt2 ? ' · ' + esc(haltLabel(halt2)) : '') + '</div>';\n      //  play/pause/forward per lista\n      if (state.rights.controlDialer) body += renderDialControls('group', g2.id, d.pid);\n      var sum = state.summaries[g2.id] || g2.contactSummaryCache || {};\n      var rem = remainingForGroup(g2.id);\n      var dd = state.ui.drillData || {};\n      function mcell(v, l) { return '<div class=\"pb2-mcell\"><div class=\"pb2-mcell-val\">' + v + '</div><div class=\"pb2-mcell-label\">' + esc(l) + '</div></div>'; }\n      body += '<div class=\"pb2-mgrid\">';\n      body += mcell(rem === null ? '–' : fmtInt(rem), 'Kvar att ringa');\n      body += mcell(fmtInt(sum.contactCount || 0), 'Kontakter totalt');\n      body += mcell(fmtInt(sum.reservedContactCount || 0), 'Reserverade');\n      body += mcell(dd.calls === undefined ? '…' : fmtInt(dd.calls), 'Samtal (' + timeFilterLabel().toLowerCase() + ')');\n      body += mcell(dd.answered === undefined ? '…' : fmtInt(dd.answered), 'Svarade');\n      body += mcell(dd.calls ? fmtPct(dd.answered / dd.calls) : '–', 'Svarsfrekvens');\n      body += '</div>';\n      if (g2.isShuffle && g2.shuffleConfigurations && g2.shuffleConfigurations.length) {\n        var actv = g2.shuffleConfigurations.filter(function (c) { return c.isDefault; })[0] || g2.shuffleConfigurations[0];\n        if (actv && actv.sources && actv.sources.length) {\n          body += '<div class=\"pb2-drill-sec\">Källor · ' + esc(actv.name) + '</div>';\n          actv.sources.forEach(function (src) { var sg = state.groupById[src.dialGroupId]; body += '<div class=\"pb2-grow\"><i class=\"fas fa-list\"></i><span class=\"pb2-gname\">' + esc(sg ? sg.name : src.dialGroupId) + '</span><span class=\"pb2-gleft\">vikt ' + src.weight + '</span></div>'; });\n        }\n      }\n      //  vilka säljare ringer den här listan just nu\n      var byUser = freshTabs();\n      var callers = [];\n      for (var cu in byUser) { if (cu !== PSEUDO_USER && byUser[cu].dialGroupId === g2.id) callers.push(cu); }\n      body += '<div class=\"pb2-drill-sec\">Ringer listan nu' + (callers.length ? ' (' + callers.length + ')' : '') + '</div>';\n      if (!callers.length) body += '<div class=\"pb2-empty\">Ingen ringer listan just nu.</div>';\n      else {\n        body += '<div class=\"pb2-userlist\">';\n        callers.forEach(function (cu) {\n          var lss = state.liveStatus[cu]; var st = (lss && lss.status) || byUser[cu].callStatus || 'inactive'; var sd = statusDef(st);\n          var sinceH = lss && lss.since ? '<span data-pb2-since=\"' + esc(lss.since) + '\"></span>' : '';\n          body += '<div class=\"pb2-user\" data-pb2-drill=\"user:' + cu + ':' + (d.pid || '') + '\">' +\n            '<span class=\"pb2-dot\" style=\"background:' + sd.color + '\"></span>' +\n            '<span class=\"pb2-user-name\">' + esc(userName(cu)) + '</span>' +\n            '<span class=\"pb2-user-sub\">' + sd.label + (sinceH ? ' ' + sinceH : '') + '</span></div>';\n        });\n        body += '</div>';\n      }\n      body += '<div class=\"pb2-drill-sec\">Predictive</div><div style=\"font-size:15px;color:var(--color-text-2,#616161)\">Aggression ' + (g2.predictiveDialAggression != null ? g2.predictiveDialAggression : '–') + ' · ' + (halt2 ? esc(haltLabel(halt2)) : 'Aktiv') + '</div>';\n    }\n    return '<div class=\"pb2-overlay\" data-pb2-drillclose=\"1\"><div class=\"pb2-modal\"><div class=\"pb2-modal-head\">' + title + '</div>' +\n      '<div class=\"pb2-modal-body\">' + body + '</div>' +\n      '<div class=\"pb2-modal-foot\"><button class=\"pb2-btn pb2-ghost\" data-pb2-act=\"close-drill\">Stäng</button></div></div></div>';\n  }\n\n  //  mottagare för notis (inloggade just nu) — hela projektet eller en specifik ringlista/slinga\n  function notifyRecipients(pid, target) {\n    var byUser = freshTabs(); var out = [];\n    for (var uid in byUser) {\n      if (uid === PSEUDO_USER) continue;\n      var t = byUser[uid];\n      if (target && target.indexOf('group:') === 0) { if (t.dialGroupId === target.slice(6)) out.push(uid); }\n      else if (t.projectId === pid) out.push(uid);\n    }\n    return out;\n  }\n  function renderNotify() {\n    var n = state.ui.notify; if (!n) return '';\n    var single = !!n.uid;\n    var p = single ? null : projById(n.pid); if (!single && !p) { state.ui.notify = null; return ''; }\n    var headTitle, body;\n    if (single) {\n      headTitle = 'Skicka meddelande · ' + esc(userName(n.uid));\n      body = '<div class=\"pb2-hint\">Meddelandet visas direkt för säljaren i appen.</div>';\n    } else {\n      var target = state.ui.notifyTarget || 'project';\n      var groups = groupsForProject(p.id);\n      var uids = notifyRecipients(p.id, target);\n      body = '<div class=\"pb2-field-label\">Mottagare</div><div class=\"pb2-optcards\" style=\"margin-bottom:8px\">';\n      body += nListable('data-pb2-notifytarget=\"project\"', target === 'project', 'Hela projektet');\n      groups.forEach(function (g) { body += nListable('data-pb2-notifytarget=\"group:' + g.id + '\"', target === 'group:' + g.id, esc(g.name)); });\n      body += '</div><div class=\"pb2-hint\">' + uids.length + ' mottagare inloggade just nu.</div>';\n      headTitle = 'Skicka notis · ' + esc(p.name);\n    }\n    body += '<div class=\"pb2-field\" style=\"margin-top:8px\"><label class=\"pb2-field-label\">Rubrik</label><input class=\"pb2-input\" id=\"pb2-notify-title\" value=\"' + esc(state.ui.notifyTitle || '') + '\" placeholder=\"T.ex. Kör hårt sista timmen!\"></div>';\n    body += '<div class=\"pb2-field\"><label class=\"pb2-field-label\">Meddelande</label><textarea class=\"pb2-textarea\" id=\"pb2-notify-text\" placeholder=\"Skriv ditt meddelande…\">' + esc(state.ui.notifyText || '') + '</textarea></div>';\n    return '<div class=\"pb2-overlay\" data-pb2-notifyclose=\"1\"><div class=\"pb2-modal\" style=\"width:520px\">' +\n      '<div class=\"pb2-modal-head\"><div class=\"pb2-modal-title\">' + headTitle + '</div></div>' +\n      '<div class=\"pb2-modal-body\">' + body + '</div>' +\n      '<div class=\"pb2-modal-foot\"><button class=\"pb2-btn\" data-pb2-act=\"notify-send\">Skicka</button>' +\n      '<button class=\"pb2-btn pb2-ghost\" data-pb2-act=\"notify-close\">Avbryt</button></div></div></div>';\n  }\n  function collectNotifyInputs() {\n    var t = document.getElementById('pb2-notify-title'); if (t) state.ui.notifyTitle = t.value;\n    var x = document.getElementById('pb2-notify-text'); if (x) state.ui.notifyText = x.value;\n  }\n  function sendNotify() {\n    collectNotifyInputs();\n    var n = state.ui.notify; if (!n) return;\n    var uids = n.uid ? [n.uid] : notifyRecipients(n.pid, state.ui.notifyTarget || 'project');\n    var title = (state.ui.notifyTitle || '').trim();\n    var textMsg = (state.ui.notifyText || '').trim();\n    if (!title && !textMsg) { toast('Skriv en rubrik eller ett meddelande'); return; }\n    if (!uids.length) { toast('Inga mottagare inloggade just nu'); return; }\n    var okMsg = n.uid ? ('Meddelande skickat till ' + userName(n.uid)) : ('Notis skickad till ' + uids.length + ' säljare');\n    var payload = { target: { userIds: uids }, notification: { title: title || 'Playbook', text: textMsg, iconName: 'comment-dots', link: '/' }, expireAfterSeconds: 3600 };\n    if (isDemo()) { toast(okMsg + ' (demo)'); state.ui.notify = null; state.ui.notifyTitle = ''; state.ui.notifyText = ''; render(); return; }\n    api('/users/user-messages-v1', {}, { method: 'POST', body: payload })\n      .then(function () { toast(okMsg); state.ui.notify = null; state.ui.notifyTitle = ''; state.ui.notifyText = ''; render(); })\n      .catch(function (e) { toast(e && e.status === 403 ? 'Saknar rättighet att skicka notiser' : 'Kunde inte skicka notis'); });\n  }\n\n  //  Bemanningsinställningar (kugghjul vid Bemanning-fliken): månadstimmar, standardgrad, grad per projekt\n  function renderStaffingSettings() {\n    if (!state.ui.staffingOpen) return '';\n    var st = (state.cfg && state.cfg.staffing) || { monthlyHours: 165, defaultRate: 100, rateByProject: {} };\n    var body = '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">Arbetstimmar per heltid / månad</span>' +\n      '<input class=\"pb2-num\" type=\"number\" min=\"1\" value=\"' + esc(st.monthlyHours || 165) + '\" data-pb2-staffhours=\"1\"></div>' +\n      '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">Standard bearbetningsgrad (%)</span>' +\n      '<input class=\"pb2-num\" type=\"number\" min=\"0\" max=\"100\" value=\"' + esc(st.defaultRate != null ? st.defaultRate : 100) + '\" data-pb2-staffrate=\"1\"></div>';\n    body += '<div class=\"pb2-field-label\" style=\"margin-top:14px\">Bearbetningsgrad per projekt (tom = standard)</div>';\n    visibleProjects().forEach(function (p) {\n      var v = (st.rateByProject && st.rateByProject[p.id] != null) ? st.rateByProject[p.id] : '';\n      body += '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">' + esc(p.name) + '</span>' +\n        '<input class=\"pb2-num\" type=\"number\" min=\"0\" max=\"100\" placeholder=\"' + (st.defaultRate != null ? st.defaultRate : 100) + '\" value=\"' + esc(v) + '\" data-pb2-projrate=\"' + p.id + '\"></div>';\n    });\n    //  Mål per metrik + riktning (tumme upp = högre bättre)\n    var tg = st.targets || {};\n    body += '<div class=\"pb2-field-label\" style=\"margin-top:14px\">Mål per metrik (grön/röd i tabellen)</div>';\n    STAFFING_COLS.forEach(function (c) {\n      var t = tg[c.id] || {}; var dir = t.dir || (c.dir === 'neutral' ? 'neutral' : c.dir);\n      body += '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">' + c.label + '</span>' +\n        '<button class=\"pb2-thumb' + (dir === 'up' ? ' pb2-on-up' : '') + '\" data-pb2-staffdir=\"' + c.id + ':up\" title=\"Högre är bättre\"><i class=\"fas fa-thumbs-up\"></i></button>' +\n        '<button class=\"pb2-thumb' + (dir === 'down' ? ' pb2-on-down' : '') + '\" data-pb2-staffdir=\"' + c.id + ':down\" title=\"Lägre är bättre\"><i class=\"fas fa-thumbs-down\"></i></button>' +\n        '<button class=\"pb2-thumb' + (dir === 'neutral' ? ' pb2-on-neutral' : '') + '\" data-pb2-staffdir=\"' + c.id + ':neutral\" title=\"Neutral\"><i class=\"fas fa-minus\"></i></button>' +\n        '<input class=\"pb2-num\" type=\"number\" step=\"any\" placeholder=\"mål\" value=\"' + esc(t.value != null ? t.value : '') + '\" data-pb2-stafftgt=\"' + c.id + '\"></div>';\n    });\n    return '<div class=\"pb2-overlay\" data-pb2-staffclose=\"1\"><div class=\"pb2-modal pb2-fixed\" style=\"width:520px\">' +\n      '<div class=\"pb2-modal-head\"><div class=\"pb2-modal-title\">Bemanningsinställningar</div></div>' +\n      '<div class=\"pb2-modal-body\">' + body + '</div>' +\n      '<div class=\"pb2-modal-foot\"><button class=\"pb2-btn\" data-pb2-act=\"staff-save\">Spara</button>' +\n      '<button class=\"pb2-btn pb2-ghost\" data-pb2-act=\"staff-close\">Avbryt</button></div></div></div>';\n  }\n  function collectStaffingSettings() {\n    if (!state.ui.staffingOpen) return;\n    state.cfg.staffing = state.cfg.staffing || { monthlyHours: 165, defaultRate: 100, rateByProject: {} };\n    var st = state.cfg.staffing; st.rateByProject = st.rateByProject || {};\n    var sh = document.querySelector('[data-pb2-staffhours]'); if (sh && sh.value) st.monthlyHours = Number(sh.value) || 165;\n    var sr = document.querySelector('[data-pb2-staffrate]'); if (sr && sr.value !== '') st.defaultRate = Math.max(0, Math.min(100, Number(sr.value)));\n    document.querySelectorAll('[data-pb2-projrate]').forEach(function (el) {\n      var pid = el.getAttribute('data-pb2-projrate');\n      if (el.value === '') delete st.rateByProject[pid];\n      else st.rateByProject[pid] = Math.max(0, Math.min(100, Number(el.value)));\n    });\n    st.targets = st.targets || {};\n    document.querySelectorAll('[data-pb2-stafftgt]').forEach(function (el) {\n      var id = el.getAttribute('data-pb2-stafftgt');\n      st.targets[id] = st.targets[id] || {};\n      st.targets[id].value = el.value === '' ? null : Number(el.value);\n    });\n  }\n\n  //  Prognosinställningar: intäktsmodell (drivers = vad som ger intäkt + värde)\n  var REVENUE_TYPES = [['signed', 'Signerade avtal'], ['order', 'Ordrar'], ['callTag', 'Samtalstagg']];\n  function progScopeKey() {\n    var s = state.ui.progScope || 'all';\n    if (s !== 'all' && !visibleProjects().some(function (p) { return p.id === s; })) s = state.ui.progScope = 'all';\n    return s;\n  }\n  function scopeLabel(scope) { return scope === 'all' ? 'Alla projekt' : (projName(scope) || 'Projekt'); }\n  function renderProgSettings() {\n    if (!state.ui.progOpen) return '';\n    var scope = progScopeKey();\n    var bs = revScopes();\n    var drivers = scopeDrivers(scope);        //  effektiva (egna eller ärvda)\n    var ownDrivers = scopeHasOwnDrivers(scope);\n    var bl = scopeBaselines(scope);\n    //  scope-väljare: Alla + varje projekt (markerar vilka som har egna inställningar)\n    var body = '<div class=\"pb2-scopebar\">';\n    body += '<button class=\"pb2-scopepill' + (scope === 'all' ? ' pb2-on' : '') + '\" data-pb2-progscope=\"all\">Alla projekt</button>';\n    visibleProjects().forEach(function (p) {\n      var custom = !!(bs[p.id] && (bs[p.id].drivers || (bs[p.id].baselines && Object.keys(bs[p.id].baselines).some(function (k) { var t = bs[p.id].baselines[k]; return t && t.value != null && t.value !== ''; }))));\n      body += '<button class=\"pb2-scopepill' + (scope === p.id ? ' pb2-on' : '') + '\" data-pb2-progscope=\"' + p.id + '\">' + esc(p.name) + (custom ? '<span class=\"pb2-scopedot\" title=\"Egna inställningar\"></span>' : '') + '</button>';\n    });\n    body += '</div>';\n    //  global synlighets-toggle för indikatorer\n    body += nSwitchRow('Visa baslinje-indikatorer i vyn', 'data-pb2-indtoggle=\"1\"', indicatorsOn());\n    //  intäktskällor\n    body += '<div class=\"pb2-field-label\" style=\"margin-top:14px\">Intäktskällor · ' + esc(scopeLabel(scope)) + '</div>';\n    if (scope !== 'all' && !ownDrivers) {\n      body += '<div class=\"pb2-inheritbar\"><i class=\"fas fa-link pb2-ib-icon\"></i><span>Ärver intäktskällor från <b>Alla projekt</b>.</span>' +\n        '<button class=\"pb2-inlink\" data-pb2-revcustomize=\"1\" style=\"margin-left:auto\">Anpassa för projektet</button></div>';\n    } else {\n      body += '<div class=\"pb2-hint\">Per konvertering: Täckningsbidrag (intäkt) och Kostnad. Vinst = TB − kostnad − lönekostnad.</div>';\n      drivers.forEach(function (d, i) {\n        body += '<div class=\"pb2-revrow\">';\n        body += '<div class=\"pb2-modepills\" style=\"margin:0\">';\n        REVENUE_TYPES.forEach(function (rt) { body += '<button class=\"pb2-modepill' + (d.type === rt[0] ? ' pb2-on' : '') + '\" data-pb2-revtype=\"' + i + ':' + rt[0] + '\">' + rt[1] + '</button>'; });\n        body += '</div>';\n        if (d.type === 'order' || d.type === 'callTag') {\n          var tags = d.type === 'order' ? state.orderTags : state.dialTags;\n          body += '<select class=\"pb2-input\" style=\"flex:0 0 140px;font-size:14px\" data-pb2-revtag=\"' + i + '\"><option value=\"\">Alla</option>';\n          for (var tid in tags) body += '<option value=\"' + tid + '\"' + (d.tagId === tid ? ' selected' : '') + '>' + esc(tags[tid]) + '</option>';\n          body += '</select>';\n        }\n        body += '<span class=\"pb2-revfield\"><input class=\"pb2-num\" style=\"width:78px\" type=\"number\" min=\"0\" value=\"' + esc(d.value || 0) + '\" data-pb2-revval=\"' + i + '\" title=\"Täckningsbidrag\"><span class=\"pb2-revunit\">TB</span></span>';\n        body += '<span class=\"pb2-revfield\"><input class=\"pb2-num\" style=\"width:70px\" type=\"number\" min=\"0\" value=\"' + esc(d.cost != null ? d.cost : '') + '\" placeholder=\"0\" data-pb2-revcost=\"' + i + '\" title=\"Kostnad per konvertering\"><span class=\"pb2-revunit\">kostn.</span></span>';\n        body += '<button class=\"pb2-notify\" data-pb2-revdel=\"' + i + '\" title=\"Ta bort\" style=\"margin-left:auto\"><i class=\"fas fa-times\"></i></button>';\n        body += '</div>';\n      });\n      if (!drivers.length) body += '<div class=\"pb2-empty\"><i class=\"fas fa-coins\" style=\"opacity:.4;margin-right:8px\"></i>Inga intäktskällor. Lägg till minst en för att följa intäkt &amp; vinst.</div>';\n      body += '<button class=\"pb2-addbtn\" data-pb2-revadd=\"1\"><i class=\"fas fa-plus-circle\"></i>Lägg till intäktskälla</button>';\n      if (scope !== 'all') body += '<button class=\"pb2-inlink\" data-pb2-revinherit=\"1\" style=\"margin-left:8px\">Återgå till Alla projekt</button>';\n    }\n    //  lönekostnad per aktiv agent-timme\n    var laborRaw = (bs[scope] && bs[scope].laborPerHour != null) ? bs[scope].laborPerHour : (scope === 'all' ? '' : '');\n    body += '<div class=\"pb2-mrow\" style=\"margin-top:10px\"><span class=\"pb2-mrow-label\">Lönekostnad per aktiv timme (kr)</span>' +\n      '<input class=\"pb2-num\" type=\"number\" min=\"0\" step=\"any\" placeholder=\"' + (scope !== 'all' ? esc(scopeLaborPerHour('all')) : '0') + '\" value=\"' + esc(laborRaw) + '\" data-pb2-laborhour=\"1\"></div>';\n    //  mål per timme (speedometrar i Företag) — bara på Alla projekt\n    if (scope === 'all') {\n      var tg = hourlyTargets();\n      body += '<div class=\"pb2-field-label\" style=\"margin-top:16px\">Mål per timme (speedometrar i Företag)</div>';\n      [['sales', 'Sälj / timme'], ['calls', 'Samtal / timme'], ['contacts', 'Kontakter / timme'], ['profit', 'Vinst / timme (kr)']].forEach(function (m) {\n        body += '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">' + m[1] + '</span>' +\n          '<input class=\"pb2-num\" type=\"number\" min=\"0\" step=\"any\" placeholder=\"mål\" value=\"' + esc(tg[m[0]] != null ? tg[m[0]] : '') + '\" data-pb2-htarget=\"' + m[0] + '\"></div>';\n      });\n    }\n    //  baslinjer per stat-block (grön/röd indikator) — per scope\n    body += '<div class=\"pb2-field-label\" style=\"margin-top:16px\">Baslinjer · ' + esc(scopeLabel(scope)) + '</div>';\n    PROG_BLOCKS.forEach(function (b) {\n      var t = bl[b.id] || {}; var dir = t.dir || 'up';\n      body += '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">' + b.label + '</span>' +\n        '<button class=\"pb2-thumb' + (dir === 'up' ? ' pb2-on-up' : '') + '\" data-pb2-progdir=\"' + b.id + ':up\" title=\"Högre är bra\"><i class=\"fas fa-thumbs-up\"></i></button>' +\n        '<button class=\"pb2-thumb' + (dir === 'down' ? ' pb2-on-down' : '') + '\" data-pb2-progdir=\"' + b.id + ':down\" title=\"Lägre är bra\"><i class=\"fas fa-thumbs-down\"></i></button>' +\n        '<button class=\"pb2-thumb' + (dir === 'neutral' ? ' pb2-on-neutral' : '') + '\" data-pb2-progdir=\"' + b.id + ':neutral\" title=\"Ingen\"><i class=\"fas fa-minus\"></i></button>' +\n        '<input class=\"pb2-num\" type=\"number\" step=\"any\" placeholder=\"baslinje\" value=\"' + esc(t.value != null ? t.value : '') + '\" data-pb2-progbase=\"' + b.id + '\"></div>';\n    });\n    return '<div class=\"pb2-overlay\" data-pb2-progclose=\"1\"><div class=\"pb2-modal pb2-fixed\" style=\"width:560px\">' +\n      '<div class=\"pb2-modal-head\"><div class=\"pb2-modal-title\">Prognosinställningar</div></div>' +\n      '<div class=\"pb2-modal-body\">' + body + '</div>' +\n      '<div class=\"pb2-modal-foot\"><button class=\"pb2-btn\" data-pb2-act=\"prog-save\">Spara</button>' +\n      '<button class=\"pb2-btn pb2-ghost\" data-pb2-act=\"prog-close\">Avbryt</button></div></div></div>';\n  }\n  //  läs av modalfälten in i det valda scopets config\n  function collectProgInputs() {\n    if (!state.ui.progOpen) return;\n    var scope = progScopeKey();\n    var sc = ensureScopeCfg(scope);\n    if (sc.drivers) {  //  bara om scopet har egna drivers (annars ärver det)\n      document.querySelectorAll('[data-pb2-revval]').forEach(function (el) { var i = Number(el.getAttribute('data-pb2-revval')); if (sc.drivers[i]) sc.drivers[i].value = Math.max(0, Number(el.value) || 0); });\n      document.querySelectorAll('[data-pb2-revcost]').forEach(function (el) { var i = Number(el.getAttribute('data-pb2-revcost')); if (sc.drivers[i]) sc.drivers[i].cost = el.value === '' ? null : Math.max(0, Number(el.value) || 0); });\n      document.querySelectorAll('[data-pb2-revtag]').forEach(function (el) { var i = Number(el.getAttribute('data-pb2-revtag')); if (sc.drivers[i]) sc.drivers[i].tagId = el.value || null; });\n    }\n    var lh = document.querySelector('[data-pb2-laborhour]');\n    if (lh) sc.laborPerHour = lh.value === '' ? null : Math.max(0, Number(lh.value) || 0);\n    if (scope === 'all') {\n      sc.hourlyTargets = sc.hourlyTargets || {};\n      document.querySelectorAll('[data-pb2-htarget]').forEach(function (el) {\n        var k = el.getAttribute('data-pb2-htarget');\n        sc.hourlyTargets[k] = el.value === '' ? null : Math.max(0, Number(el.value) || 0);\n      });\n    }\n    document.querySelectorAll('[data-pb2-progbase]').forEach(function (el) {\n      var id = el.getAttribute('data-pb2-progbase');\n      sc.baselines[id] = sc.baselines[id] || {};\n      sc.baselines[id].value = el.value === '' ? null : Number(el.value);\n      //  matcha UI:ns default (tumme upp) om ett värde satts utan att riktning valts\n      if (sc.baselines[id].value != null && !sc.baselines[id].dir) sc.baselines[id].dir = 'up';\n    });\n  }\n\n  /* ---------------- Skapa slinga (shuffle dial group) ---------------- */\n  //  källor = icke-shuffle ringlistor att blanda ihop; state.ui.slinga.state = redigerbar modell\n  function slingaSources() {\n    return state.dialGroups.filter(function (g) { return !g.isShuffle && !g.isCallback && !g.deletedAt; });\n  }\n  function newSlingaState(pid) {\n    return { pid: pid, name: '', modes: [{ name: 'Standard', isDefault: true, sources: [] }], activeMode: 0, saving: false };\n  }\n  function renderSlinga() {\n    var sl = state.ui.slinga; if (!sl) return '';\n    var st = sl.state; var p = projById(sl.pid);\n    var srcs = slingaSources();\n    var mode = st.modes[st.activeMode] || st.modes[0];\n    var picked = {}; mode.sources.forEach(function (s) { picked[s.dialGroupId] = s.weight; });\n    var body = '';\n    //  namn\n    body += '<div class=\"pb2-field\"><label class=\"pb2-field-label\">Namn på slingan</label><input class=\"pb2-input\" id=\"pb2-slinga-name\" value=\"' + esc(st.name) + '\" placeholder=\"T.ex. ' + esc(p ? p.name : 'Slinga') + '\"></div>';\n    //  viktlägen (flera konfigurationer, ett aktivt/default)\n    body += '<div class=\"pb2-field-label\" style=\"margin-top:6px\">Viktlägen</div><div class=\"pb2-modepills\">';\n    st.modes.forEach(function (m, i) {\n      body += '<button class=\"pb2-modepill' + (i === st.activeMode ? ' pb2-on' : '') + '\" data-pb2-slingamode=\"' + i + '\">' + (m.isDefault ? '<i class=\"fas fa-star\"></i>' : '') + esc(m.name || ('Läge ' + (i + 1))) + '</button>';\n    });\n    body += '<button class=\"pb2-modepill\" data-pb2-slingaddmode=\"1\"><i class=\"fas fa-plus\"></i>Nytt läge</button></div>';\n    body += '<div class=\"pb2-field\" style=\"margin-bottom:8px\"><input class=\"pb2-input\" id=\"pb2-slinga-modename\" value=\"' + esc(mode.name || '') + '\" placeholder=\"Lägesnamn\" style=\"font-size:14px\">' +\n      '<div style=\"display:flex;gap:8px;margin-top:8px\">' + nSwitchRow('Aktivt läge (standard)', 'data-pb2-slingasetdefault=\"1\"', !!mode.isDefault) + '</div></div>';\n    //  källor med vikt-slider — rader alltid samma höjd (slider alltid synlig, inaktiv när av)\n    body += '<div class=\"pb2-field-label\">Ringlistor i slingan (vikt styr fördelningen)</div>';\n    if (!srcs.length) body += '<div class=\"pb2-empty\">Inga vanliga ringlistor att blanda.</div>';\n    srcs.forEach(function (g) {\n      var on = picked[g.id] !== undefined;\n      var w = on ? picked[g.id] : 50;\n      body += '<div class=\"pb2-srcrow\">' + nSwitch('data-pb2-slingasrc=\"' + g.id + '\"', on) +\n        '<span class=\"pb2-srcname\">' + esc(g.name) + '</span>' +\n        '<input class=\"pb2-slider\" type=\"range\" min=\"0\" max=\"100\" value=\"' + w + '\" data-pb2-slingaweight=\"' + g.id + '\"' + (on ? '' : ' disabled') + '>' +\n        '<span class=\"pb2-srcpct\">' + (on ? w : '–') + '</span></div>';\n    });\n    if (mode.sources.length) {\n      var tot = mode.sources.reduce(function (a, s) { return a + (s.weight || 0); }, 0);\n      body += '<div class=\"pb2-hint\" style=\"padding-top:10px\">Fördelning: ';\n      body += mode.sources.map(function (s) { var g = state.groupById[s.dialGroupId]; return esc(g ? g.name : '?') + ' ' + (tot ? Math.round(100 * s.weight / tot) : 0) + '%'; }).join(' · ') + '</div>';\n    }\n    return '<div class=\"pb2-overlay\" data-pb2-slingaclose=\"1\"><div class=\"pb2-modal pb2-fixed\" style=\"width:560px\">' +\n      '<div class=\"pb2-modal-head\"><div class=\"pb2-modal-title\">Skapa slinga · ' + esc(p ? p.name : '') + '</div></div>' +\n      '<div class=\"pb2-modal-body\">' + body + '</div>' +\n      '<div class=\"pb2-modal-foot\"><button class=\"pb2-btn\" data-pb2-act=\"slinga-save\"' + (st.saving ? ' disabled' : '') + '>' + (st.saving ? 'Skapar…' : 'Skapa slinga') + '</button>' +\n      '<button class=\"pb2-btn pb2-ghost\" data-pb2-act=\"slinga-close\">Avbryt</button></div></div></div>';\n  }\n  function collectSlingaInputs() {\n    var sl = state.ui.slinga; if (!sl) return; var st = sl.state;\n    var nm = document.getElementById('pb2-slinga-name'); if (nm) st.name = nm.value;\n    var mn = document.getElementById('pb2-slinga-modename'); if (mn && st.modes[st.activeMode]) st.modes[st.activeMode].name = mn.value;\n    document.querySelectorAll('[data-pb2-slingaweight]').forEach(function (el) {\n      var gid = el.getAttribute('data-pb2-slingaweight');\n      var mode = st.modes[st.activeMode];\n      var src = mode.sources.filter(function (s) { return s.dialGroupId === gid; })[0];\n      if (src) src.weight = Math.max(0, Number(el.value) || 0);\n    });\n  }\n  function saveSlinga() {\n    collectSlingaInputs();\n    var sl = state.ui.slinga; var st = sl.state;\n    if (!st.name.trim()) { toast('Ge slingan ett namn'); return; }\n    var anySrc = st.modes.some(function (m) { return m.sources.length; });\n    if (!anySrc) { toast('Välj minst en ringlista'); return; }\n    //  säkerställ exakt ett default-läge\n    var hasDefault = st.modes.some(function (m) { return m.isDefault; });\n    if (!hasDefault && st.modes.length) st.modes[0].isDefault = true;\n    var p = projById(sl.pid);\n    var ownerTeams = (p && p.teamIds) || [];\n    if (!ownerTeams.length) { var meT = state.me || {}; ownerTeams = [meT.teamId].concat(meT.supervisedTeamIds || []).filter(Boolean); }\n    var body = {\n      name: st.name.trim(),\n      ownerTeamIds: ownerTeams,\n      isShuffle: true,\n      predictiveDialUsers: [],\n      excludeListIds: [],\n      projectIds: [sl.pid],\n      predictiveDialAggression: 50,\n      shuffleConfigurations: st.modes.filter(function (m) { return m.sources.length; }).map(function (m) {\n        return { name: (m.name || 'Standard').trim(), isDefault: !!m.isDefault, sources: m.sources.map(function (s) { return { dialGroupId: s.dialGroupId, weight: s.weight }; }) };\n      })\n    };\n    if (isDemo()) {\n      var fake = { id: 'demo-slinga-' + Date.now(), name: body.name, projectIds: [sl.pid], isShuffle: true, isCallback: false, predictiveDialHaltReason: null, deletedAt: null, accessTeamIds: ownerTeams, ownerTeamIds: ownerTeams, predictiveDialAggression: 50, shuffleConfigurations: body.shuffleConfigurations, contactSummaryCache: { contactCount: 0, reservedContactCount: 0, skippedContactCount: 0, quarantinedContactCount: 0 } };\n      state.dialGroups.push(fake); state.groupById[fake.id] = fake;\n      toast('Slinga skapad (demo)'); state.ui.slinga = null; render(); return;\n    }\n    st.saving = true; render();\n    api('/dial/dial-groups-v1', {}, { method: 'POST', body: body })\n      .then(function () { toast('Slinga skapad'); state.ui.slinga = null; refreshGroups(); render(); })\n      .catch(function (e) { st.saving = false; render(); toast(e && e.status === 403 ? 'Saknar rättighet att skapa ringlistor' : 'Kunde inte skapa slingan'); });\n  }\n\n  //  play/pause/forward-kontroller (scope 'project' eller 'group')\n  function renderDialControls(scope, id, pid) {\n    var groups = scope === 'group' ? [state.groupById[id]].filter(Boolean) : groupsForProject(id);\n    var anyHalted = groups.some(function (g) { return g.predictiveDialHaltReason === 'disabledByUser'; });\n    var anyBoosted = groups.some(function (g) { return (state.cfg.boosted || {})[g.id] !== undefined; });\n    var playOn = !anyHalted && !anyBoosted;\n    function btn(act, icon, label, on) {\n      return '<button class=\"pb2-pill' + (on ? ' pb2-on' : '') + '\" data-pb2-dial=\"' + act + ':' + scope + ':' + id + '\" title=\"' + label + '\"><i class=\"fas ' + icon + '\"></i> ' + label + '</button>';\n    }\n    return '<div class=\"pb2-tabrow\" style=\"margin-bottom:12px\">' +\n      btn('pause', 'fa-pause', 'Pausa', anyHalted) +\n      btn('play', 'fa-play', 'Kör', playOn) +\n      btn('forward', 'fa-forward', 'Snabba på', anyBoosted) +\n      '</div>';\n  }\n\n  //  native-kloner (exakt SCSS + animationer från s2-frontend core-komponenter)\n  function nSwitch(dataAttr, on) {\n    return '<div class=\"pb2-switch' + (on ? ' pb2-on' : '') + '\" ' + dataAttr + '><div class=\"pb2-knob\"></div></div>';\n  }\n  function nListable(dataAttr, on, labelHtml, extraStyle) {\n    return '<div class=\"pb2-listable' + (on ? ' pb2-on' : '') + '\" ' + dataAttr + (extraStyle ? ' style=\"' + extraStyle + '\"' : '') + '>' +\n      '<div class=\"pb2-l-inner\">' +\n      '<i class=\"fas fa-times pb2-l-icon pb2-l-x\"></i>' +\n      '<i class=\"fas fa-check pb2-l-icon pb2-l-check\"></i>' +\n      '<span class=\"pb2-l-label\">' + labelHtml + '</span></div></div>';\n  }\n  function nSwitchRow(label, dataAttr, on) {\n    return '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">' + label + '</span>' + nSwitch(dataAttr, on) + '</div>';\n  }\n\n  /* ---------------- inställningsmodal ---------------- */\n  function renderSettings() {\n    if (!state.ui.settingsOpen) return '';\n    var cfg = state.cfg || defaultCfg();\n    var open = state.ui.accOpen;\n    function acc(id, icon, title, body) {\n      return '<div class=\"pb2-acc' + (open === id ? ' pb2-open' : '') + '\"><div class=\"pb2-acc-head\" data-pb2-acc=\"' + id + '\"><i class=\"fas ' + icon + '\"></i>' + title + '</div><div class=\"pb2-acc-body\">' + body + '</div></div>';\n    }\n\n    //  Projekt\n    var bProjects = '<div class=\"pb2-hint\">Vilka projekt visas i vyn. Inga valda = alla projekt.</div><div class=\"pb2-optcards\">';\n    state.projects.forEach(function (p) {\n      var on = !cfg.projectIds.length ? false : cfg.projectIds.indexOf(p.id) !== -1;\n      bProjects += nListable('data-pb2-cfgproj=\"' + p.id + '\"', on, esc(p.name));\n    });\n    bProjects += '</div>';\n\n    //  Statusknappar\n    var bStatuses = '<div class=\"pb2-hint\">Vilka statusfilter som visas högst upp.</div><div class=\"pb2-optcards\">';\n    STATUSES.forEach(function (s) {\n      var on = !(cfg.statusesVisible && cfg.statusesVisible[s.id] === false);\n      bStatuses += nListable('data-pb2-cfgstatus=\"' + s.id + '\"', on, '<span class=\"pb2-dot\" style=\"width:8px;height:8px;border-radius:50%;background:' + s.color + '\"></span>' + s.label);\n    });\n    bStatuses += '</div>';\n\n    //  Hit rate\n    var hr = cfg.hitRate || { mode: 'orders' };\n    var bHit = '<div class=\"pb2-hint\">Vad som räknas som en hit. Hit rate = hits / svarade samtal.</div><div class=\"pb2-optcards\">';\n    [['orders', 'fa-shopping-cart', 'Ordrar'], ['signedOffers', 'fa-file-signature', 'Signerade avtal'], ['callTag', 'fa-tag', 'Samtalstagg']].forEach(function (o) {\n      bHit += nListable('data-pb2-cfghit=\"' + o[0] + '\"', hr.mode === o[0], '<i class=\"fas ' + o[1] + '\"></i>' + o[2]);\n    });\n    bHit += '</div>';\n    if (hr.mode === 'callTag') {\n      bHit += '<div class=\"pb2-hint\" style=\"padding-top:10px\">Välj tagg:</div><div class=\"pb2-optcards\">';\n      for (var tid in state.dialTags) {\n        bHit += nListable('data-pb2-cfghittag=\"' + tid + '\"', hr.tagId === tid, esc(state.dialTags[tid]));\n      }\n      bHit += '</div>';\n    }\n\n    //  Metriker\n    var bMetrics = '<div class=\"pb2-hint\">Tumme upp = högre är bättre (grönt över snittet). Tumme ner = lägre är bättre. Neutral = ingen färg. Benchmark ersätter snittet som jämförelse (tomt = dynamiskt snitt).</div>';\n    METRICS.forEach(function (m) {\n      var mc = (cfg.metrics || {})[m.id] || {};\n      var dir = mc.dir || m.dir;\n      var bench = mc.benchmark !== undefined && mc.benchmark !== null ? mc.benchmark : '';\n      bMetrics += '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">' + m.label + '</span>' +\n        '<button class=\"pb2-thumb' + (dir === 'up' ? ' pb2-on-up' : '') + '\" data-pb2-dir=\"' + m.id + ':up\" title=\"Högre är bättre\"><i class=\"fas fa-thumbs-up\"></i></button>' +\n        '<button class=\"pb2-thumb' + (dir === 'down' ? ' pb2-on-down' : '') + '\" data-pb2-dir=\"' + m.id + ':down\" title=\"Lägre är bättre\"><i class=\"fas fa-thumbs-down\"></i></button>' +\n        '<button class=\"pb2-thumb' + (dir === 'neutral' ? ' pb2-on-neutral' : '') + '\" data-pb2-dir=\"' + m.id + ':neutral\" title=\"Neutral\"><i class=\"fas fa-minus\"></i></button>' +\n        '<input class=\"pb2-num\" type=\"number\" step=\"any\" placeholder=\"snitt\" value=\"' + esc(bench) + '\" data-pb2-bench=\"' + m.id + '\">' +\n        '</div>';\n    });\n\n    //  Pinnade nyckeltal (6 rutor i botten av varje kort)\n    var pins = (cfg.pinnedMetrics && cfg.pinnedMetrics.length ? cfg.pinnedMetrics : DEFAULT_PINS).slice(0, 6);\n    var bPins = '<div class=\"pb2-hint\">Välj de 6 nyckeltal som visas fast i botten av varje projektkort. Klicka för att välja/välja bort (max 6).</div><div class=\"pb2-optcards\">';\n    PINNABLE.forEach(function (pm) {\n      var ix = pins.indexOf(pm.id);\n      var on = ix !== -1;\n      bPins += nListable('data-pb2-cfgpin=\"' + pm.id + '\"', on, (on ? '<b>' + (ix + 1) + '</b>&nbsp;' : '') + esc(pm.label));\n    });\n    bPins += '</div>';\n\n    //  Tempo\n    var bTempo = '<div class=\"pb2-hint\">Tempo är ett syntetiskt 1–10-tal: 10 = samtal per aktiv timme når riktvärdet nedan.</div>' +\n      '<div class=\"pb2-mrow\"><span class=\"pb2-mrow-label\">Samtal/h som ger tempo 10</span>' +\n      '<input class=\"pb2-num\" type=\"number\" min=\"1\" value=\"' + esc(cfg.tempoBenchmark || 25) + '\" data-pb2-tempobench=\"1\"></div>';\n\n    //  Layout & övrigt\n    var cpr = cfg.cardsPerRow || 4;\n    var sec = cfg.sections || { nav: true, body: true, bottom: true, tabs: true };\n    var bLayout = '<div class=\"pb2-hint\">Kort per rad.</div><div class=\"pb2-optcards\">';\n    [1, 2, 3, 4].forEach(function (n) { bLayout += nListable('data-pb2-cfgcols=\"' + n + '\"', cpr === n, String(n)); });\n    bLayout += '</div><div class=\"pb2-hint\" style=\"padding-top:12px\">Synliga sektioner i korten.</div>';\n    [['nav', 'Statusrutor'], ['body', 'Lista'], ['bottom', 'Footer (nyckeltal / diagram)'], ['tabs', 'Flikar']].forEach(function (s) {\n      bLayout += nSwitchRow(s[1], 'data-pb2-cfgsection=\"' + s[0] + '\"', sec[s[0]] !== false);\n    });\n    bLayout += nSwitchRow('Visa säljarens ringlista (mittkolumn)', 'data-pb2-cfgusergroup=\"1\"', !!cfg.showUserGroup) +\n      nSwitchRow('Visa \"Övrigt\"-kort (inloggade utan projekt)', 'data-pb2-cfgother=\"1\"', cfg.showOther !== false) +\n      nSwitchRow('Visa lönsamhet (speedometrar & diagram)', 'data-pb2-cfggauges=\"1\"', cfg.showGauges !== false) +\n      '<div class=\"pb2-hint\" style=\"padding:2px 0 8px\">Kräver mål/timme + täckningsbidrag/kostnad. <button class=\"pb2-inlink\" data-pb2-act=\"goto-prognos-settings\">Ställ in i Prognosinställningar</button></div>' +\n      nSwitchRow('Räkna helger i dag-diagrammet', 'data-pb2-cfgweekend=\"1\"', state.prefs.chartWeekends !== false);\n\n    //  personlig-status + återställ\n    var personalNote = '';\n    if (hasPersonalSettings())\n      personalNote = '<div class=\"pb2-hint\" style=\"padding:10px 0 0\"><i class=\"fas fa-user\" style=\"margin-right:6px\"></i>Du visar dina egna inställningar. <button class=\"pb2-inlink\" data-pb2-act=\"settings-reset-mine\">Återställ till allas</button></div>';\n    //  Spara för alla = primär (default), Spara för mig = sekundär. Icke-redigerare ser bara \"Spara för mig\".\n    var foot = '';\n    if (canEditSettings()) foot += '<button class=\"pb2-btn\" data-pb2-act=\"save-all\">Spara för alla</button>';\n    foot += '<button class=\"pb2-btn' + (canEditSettings() ? ' pb2-ghost' : '') + '\" data-pb2-act=\"save-mine\">Spara för mig</button>';\n    foot += '<button class=\"pb2-btn pb2-ghost\" data-pb2-act=\"close-settings\">Avbryt</button>';\n    var h = '<div class=\"pb2-overlay\" data-pb2-overlay=\"1\"><div class=\"pb2-modal pb2-fixed\">' +\n      '<div class=\"pb2-modal-head\"><div class=\"pb2-modal-title\">Playbook-inställningar</div></div>' +\n      '<div class=\"pb2-modal-body\">' + personalNote +\n      acc('projects', 'fa-folder-open', 'Projekt', bProjects) +\n      acc('statuses', 'fa-toggle-on', 'Statusknappar', bStatuses) +\n      acc('pins', 'fa-thumbtack', 'Pinnade nyckeltal', bPins) +\n      acc('hit', 'fa-bullseye', 'Hit rate', bHit) +\n      acc('metrics', 'fa-sliders-h', 'Metriker & benchmarks', bMetrics) +\n      acc('tempo', 'fa-tachometer-alt', 'Tempo', bTempo) +\n      acc('layout', 'fa-th-large', 'Layout & övrigt', bLayout) +\n      '</div>' +\n      '<div class=\"pb2-modal-foot\">' + foot + '</div>' +\n      '</div></div>';\n    return h;\n  }\n\n  //  scrollpositioner bevaras över omritning\n  function captureScroll(app) {\n    var s = { win: (window.pageYOffset || document.documentElement.scrollTop || 0) };\n    try {\n      app.querySelectorAll('.pb2-card[data-pb2-pid]').forEach(function (c) { var b = c.querySelector('.pb2-card-body'); if (b) s['b:' + c.getAttribute('data-pb2-pid')] = b.scrollTop; });\n      var tw = app.querySelector('.pb2-tblwrap'); if (tw) s.tw = tw.scrollTop;\n    } catch (e) {}\n    return s;\n  }\n  function restoreScroll(app, s) {\n    try {\n      app.querySelectorAll('.pb2-card[data-pb2-pid]').forEach(function (c) { var b = c.querySelector('.pb2-card-body'); var v = s['b:' + c.getAttribute('data-pb2-pid')]; if (b && v) b.scrollTop = v; });\n      var tw = app.querySelector('.pb2-tblwrap'); if (tw && s.tw) tw.scrollTop = s.tw;\n    } catch (e) {}\n    if (s.win) window.scrollTo(0, s.win);\n  }\n\n  function render() {\n    var app = document.getElementById('pb2-app');\n    if (!app) return;\n    var scroll = captureScroll(app);\n    state.renderStamp++;\n    if (state.bootError) {\n      app.innerHTML = '<div class=\"pb2-note\">Kunde inte ladda Playbook: ' + esc(state.bootError) + '</div>';\n      app.setAttribute('data-pb2-stamp', String(state.renderStamp));\n      return;\n    }\n    if (!state.booted) {\n      app.innerHTML = '';\n      app.setAttribute('data-pb2-stamp', String(state.renderStamp));\n      return;\n    }\n    var projList = visibleProjects();\n    var h = renderTop();\n    var body = '';\n    if (!projList.length) {\n      body = '<div class=\"pb2-note\">' + (state.rights.viewOwnProjects || state.rights.viewAllProjects\n        ? 'Inga projekt att visa. ' + ((state.rights.isAdmin || state.rights.manageSettings) ? 'Välj projekt i inställningarna (kugghjulet).' : '')\n        : 'Du saknar rättighet att se Playbook-projekt. Be din administratör om rättigheten \"Se egna projekt\".') + '</div>';\n    } else if (state.prefs.mainTab === 'company') {\n      body = renderCompanyView(projList);\n    } else if (state.prefs.mainTab === 'staffing') {\n      body = renderStaffingView(projList);\n    } else if (state.prefs.mainTab === 'prognos') {\n      body = renderProgView(projList);\n    } else {\n      var cols = (state.cfg && state.cfg.cardsPerRow) || 4;\n      body = '<div class=\"pb2-grid\" style=\"--pb2-cols:' + cols + '\">';\n      projList.forEach(function (p) { body += renderCard(p); });\n      //  \"Övrigt\"-kort: inloggade som inte hör till något synligt projekt\n      var cfg = state.cfg || defaultCfg();\n      if (cfg.showOther !== false) {\n        var pids = {}; projList.forEach(function (p) { pids[p.id] = true; });\n        var others = liveOtherUsers(pids);\n        if (others.length) body += renderOtherCard(others);\n      }\n      body += '</div>';\n    }\n    //  vy-övergång: animeras bara när ett vy-/scope-byte precis skett (ej vid bakgrundspoll)\n    h += '<div class=\"pb2-viewbody' + (state.ui._animView ? ' pb2-fadein' : '') + '\">' + body + '</div>';\n    state.ui._animView = false; state.ui._animCard = null; state.ui._animBottom = false;\n    h += renderSettings();\n    h += renderStaffingSettings();\n    h += renderProgSettings();\n    h += renderDrill();\n    h += renderNotify();\n    h += renderSlinga();\n    //  första renderingen efter boot: mjuk kaskad-entré\n    var bootAnim = state.booted && !state.ui._didBootAnim;\n    if (bootAnim) state.ui._didBootAnim = true;\n    app.innerHTML = bootAnim ? '<div class=\"pb2-boot\">' + h + '</div>' : h;\n    app.setAttribute('data-pb2-stamp', String(state.renderStamp));\n    restoreScroll(app, scroll);\n    contrastProbe();\n  }\n\n  function projById(pid) { for (var i = 0; i < state.projects.length; i++) if (state.projects[i].id === pid) return state.projects[i]; return null; }\n\n  //  Uppdaterar dynamiska värden PÅ PLATS utan att bygga om DOM:en (bevarar scroll, animationer, öppna modaler).\n  function liveUpdate() {\n    if (!state.booted || state.bootError) return;\n    var app = document.getElementById('pb2-app');\n    if (!app || app.getAttribute('data-pb2-stamp') !== String(state.renderStamp)) { render(); return; }\n    if (state.ui.settingsOpen || state.ui.staffingOpen || state.ui.progOpen || state.ui.drill || state.ui.notify || state.ui.slinga) return; //  frys medan en modal är öppen\n    var up = app.querySelector('.pb2-updated');\n    if (up) up.textContent = state.historyNote ? state.historyNote : (state.ui.updatedAt ? 'Uppdaterad ' + pad2(state.ui.updatedAt.getHours()) + ':' + pad2(state.ui.updatedAt.getMinutes()) + ':' + pad2(state.ui.updatedAt.getSeconds()) : '');\n    //  toppnavigeringens status uppdateras inte (statiskt); företags-/bemanningsvyn byggs om med bevarad tabellscroll\n    if (state.prefs.mainTab === 'company') {\n      var comp = app.querySelector('.pb2-company');\n      if (!comp) { render(); return; }\n      var tw = comp.querySelector('.pb2-tblwrap'); var tws = tw ? tw.scrollTop : 0;\n      comp.outerHTML = renderCompanyView(visibleProjects());\n      var tw2 = app.querySelector('.pb2-tblwrap'); if (tw2) tw2.scrollTop = tws;\n      return;\n    }\n    if (state.prefs.mainTab === 'staffing') {\n      var sp = app.querySelector('.pb2-prog');\n      if (!sp) { render(); return; }\n      var stw = sp.querySelector('.pb2-tblwrap'); var stws = stw ? stw.scrollTop : 0;\n      sp.outerHTML = renderStaffingView(visibleProjects());\n      var stw2 = app.querySelector('.pb2-tblwrap'); if (stw2) stw2.scrollTop = stws;\n      return;\n    }\n    if (state.prefs.mainTab === 'prognos') {\n      //  frys medan användaren hovrar diagrammet (annars återskapas elementet under muspekaren)\n      if (document.querySelector('.pb2-charttip') && document.querySelector('.pb2-charttip').style.opacity === '1') return;\n      var pv = app.querySelector('.pb2-prog');\n      if (!pv) { render(); return; }\n      pv.outerHTML = renderProgView(visibleProjects());\n      return;\n    }\n    var cards = app.querySelectorAll('.pb2-card[data-pb2-pid]');\n    cards.forEach(function (card) {\n      var p = projById(card.getAttribute('data-pb2-pid'));\n      if (!p) return;\n      var counts = statusCounts([p]);\n      card.querySelectorAll('.pb2-mini').forEach(function (mini) {\n        var sf = mini.getAttribute('data-pb2-sf'); if (!sf) return;\n        var sid = sf.split(':')[2]; var b = mini.querySelector('b');\n        if (b && counts[sid] !== undefined) b.textContent = counts[sid];\n      });\n      var body = card.querySelector('.pb2-card-body');\n      //  bygg inte om kroppen om användaren redigerar ett fält i den (t.ex. bearbetningsgrad)\n      if (body && !(document.activeElement && body.contains(document.activeElement))) {\n        var st = body.scrollTop;\n        var tab = state.prefs.cardTab[p.id] || 'live';\n        body.innerHTML = tab === 'lists' ? renderCardLists(p) : tab === 'stats' ? renderCardUsers(p) : tab === 'outcomes' ? renderCardOutcomes(p) : renderCardLiveUsers(p);\n        body.scrollTop = st;\n      }\n      var bt = state.prefs.bottomTab || 'pins';\n      if (bt === 'pins') { var pinsEl = card.querySelector('.pb2-pins'); if (pinsEl) pinsEl.outerHTML = renderPinsInner(p); }\n      else updateChartInPlace(card, p);\n    });\n  }\n  //  uppdaterar staplarnas höjd + nuvärde utan att återskapa dem (shimmer/animation fortsätter)\n  function updateChartInPlace(card, p) {\n    var chartBody = card.querySelector('.pb2-chart-body');\n    if (!chartBody) return;\n    var metric = state.prefs.chartMetric || 'calls', mode = state.prefs.chartMode || 'hour';\n    var data = buildChartBars(p.id, null, metric, mode);\n    var max = Math.max(1, data.max);\n    var bars = chartBody.querySelectorAll('.pb2-bar');\n    if (bars.length !== data.bars.length) { chartBody.outerHTML = chartBodyHtml(p.id, null, true); return; }\n    data.bars.forEach(function (b, i) {\n      bars[i].style.height = Math.max(2, Math.round(100 * b.value / max)) + '%';\n      if (b.cur) bars[i].classList.add('pb2-cur'); else bars[i].classList.remove('pb2-cur');\n    });\n    var vals = data.bars.map(function (b) { return b.value; });\n    var avg = vals.length ? vals.reduce(function (a, b) { return a + b; }, 0) / vals.length : 0;\n    var cur = data.bars.filter(function (b) { return b.cur; })[0];\n    if (!cur && data.bars.length) cur = data.bars[data.bars.length - 1];\n    var curVal = cur ? cur.value : 0, above = curVal >= avg;\n    var vEl = chartBody.querySelector('.pb2-cur-val'); if (vEl) vEl.textContent = fmtInt(curVal);\n    var chev = chartBody.querySelector('.pb2-cur-chev');\n    if (chev) { chev.className = 'fas ' + (above ? 'fa-chevron-up' : 'fa-chevron-down') + ' pb2-cur-chev'; chev.style.color = above ? 'var(--color-good,#43a047)' : 'var(--color-danger,#e53935)'; }\n    var cap = chartBody.querySelector('.pb2-cur-cap');\n    if (cap) cap.textContent = (mode === 'hour' ? 'kl ' + (cur ? cur.label : '') : (cur ? cur.label : 'idag')) + ' · snitt ' + fmtDec(avg);\n  }\n\n  /* ---------------- events (delegerade på document) ---------------- */\n  function onClick(ev) {\n    var el = ev.target;\n    function closest(attr) {\n      var n = el;\n      while (n && n.getAttribute) {\n        if (n.getAttribute(attr) !== null) return n;\n        n = n.parentNode;\n      }\n      return null;\n    }\n    var t;\n    if ((t = closest('data-pb2-main'))) {\n      var nextTab = t.getAttribute('data-pb2-main');\n      if (nextTab !== state.prefs.mainTab) state.ui._animView = true;\n      state.prefs.mainTab = nextTab;\n      savePrefs(); render();\n      if (state.prefs.mainTab === 'prognos') ensureForecast();\n      return;\n    }\n    if ((t = closest('data-pb2-month'))) {\n      var delta = Number(t.getAttribute('data-pb2-month')) || 0;\n      var next = (state.forecast.monthOffset || 0) - delta; //  vänster (-1) = längre bak = större offset\n      if (next < 0) next = 0; if (next > 24) next = 24;\n      if (next !== (state.forecast.monthOffset || 0)) state.ui._animView = true;\n      state.forecast.monthOffset = next;\n      render(); ensureForecast(); return;\n    }\n    if ((t = closest('data-pb2-progproj'))) {\n      var pp = t.getAttribute('data-pb2-progproj');\n      var nextSel = pp === 'all' ? null : pp;\n      if (nextSel !== state.forecast.selProject) state.ui._animView = true;\n      state.forecast.selProject = nextSel;\n      render(); return;\n    }\n    if ((t = closest('data-pb2-tf'))) {\n      state.prefs.timeFilter = t.getAttribute('data-pb2-tf');\n      savePrefs(); render(); refreshStats(); return;\n    }\n    if ((t = closest('data-pb2-sf'))) {\n      //  format: \"card:<pid>:<statusId>\" eller \"company:<statusId>\"\n      var parts0 = t.getAttribute('data-pb2-sf').split(':');\n      if (parts0[0] === 'card') {\n        var cpid = parts0[1], csid = parts0[2];\n        state.prefs.cardSf = state.prefs.cardSf || {};\n        state.prefs.cardSf[cpid] = state.prefs.cardSf[cpid] === csid ? null : csid;\n      } else {\n        var msid = parts0[1];\n        state.prefs.companySf = state.prefs.companySf === msid ? null : msid;\n      }\n      savePrefs(); render(); return;\n    }\n    if ((t = closest('data-pb2-cotab'))) {\n      var nextCoTab = t.getAttribute('data-pb2-cotab');\n      if (nextCoTab !== state.prefs.companyTab) state.ui._animView = true;\n      state.prefs.companyTab = nextCoTab;\n      savePrefs(); render(); return;\n    }\n    if ((t = closest('data-pb2-bottom'))) {\n      var nextBt = t.getAttribute('data-pb2-bottom');\n      if (nextBt !== (state.prefs.bottomTab || 'pins')) state.ui._animBottom = true;\n      state.prefs.bottomTab = nextBt;\n      savePrefs(); render(); return;\n    }\n    if ((t = closest('data-pb2-cmetric'))) {\n      state.prefs.chartMetric = t.getAttribute('data-pb2-cmetric');\n      savePrefs(); render(); return;\n    }\n    if ((t = closest('data-pb2-cmode'))) {\n      state.prefs.chartMode = t.getAttribute('data-pb2-cmode');\n      savePrefs(); render(); return;\n    }\n    if ((t = closest('data-pb2-cohear'))) {\n      startCohearing(t.getAttribute('data-pb2-cohear')); return;\n    }\n    if ((t = closest('data-pb2-logout'))) {\n      logoutUser(t.getAttribute('data-pb2-logout')); return;\n    }\n    if ((t = closest('data-pb2-msguser'))) {\n      var muid = t.getAttribute('data-pb2-msguser');\n      var mtab = freshTabs()[muid];\n      state.ui.notify = { pid: (mtab && mtab.projectId) || null, uid: muid };\n      state.ui.notifyTitle = ''; state.ui.notifyText = '';\n      render(); return;\n    }\n    if ((t = closest('data-pb2-notify'))) {\n      state.ui.notify = { pid: t.getAttribute('data-pb2-notify') };\n      state.ui.notifyTarget = 'project'; state.ui.notifyTitle = ''; state.ui.notifyText = '';\n      render(); return;\n    }\n    if ((t = closest('data-pb2-notifytarget'))) {\n      collectNotifyInputs();\n      state.ui.notifyTarget = t.getAttribute('data-pb2-notifytarget');\n      render(); return;\n    }\n    if ((t = closest('data-pb2-editrate')) && !closest('.pb2-cellinput')) {\n      var epid = t.getAttribute('data-pb2-editrate');\n      var st0 = mergedCfg().staffing || {};\n      var cur = (st0.rateByProject && st0.rateByProject[epid] != null) ? st0.rateByProject[epid] : (st0.defaultRate != null ? st0.defaultRate : 100);\n      t.outerHTML = '<input class=\"pb2-cellinput\" type=\"number\" min=\"0\" max=\"100\" value=\"' + cur + '\" data-pb2-editrateinput=\"' + epid + '\">';\n      var inp = document.querySelector('[data-pb2-editrateinput=\"' + epid + '\"]');\n      if (inp) {\n        inp.focus(); inp.select();\n        var commit = function () {\n          state.cfg.staffing = state.cfg.staffing || { monthlyHours: 165, defaultRate: 100, rateByProject: {} };\n          state.cfg.staffing.rateByProject = state.cfg.staffing.rateByProject || {};\n          state.cfg.staffing.rateByProject[epid] = Math.max(0, Math.min(100, Number(inp.value) || 0));\n          if (state.rights.isAdmin || state.rights.manageSettings) savePresetState().catch(function () {});\n          render();\n        };\n        inp.addEventListener('blur', commit);\n        inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') { inp.blur(); } else if (e.key === 'Escape') { render(); } });\n      }\n      return;\n    }\n    if ((t = closest('data-pb2-slinga'))) {\n      state.ui.slinga = { pid: t.getAttribute('data-pb2-slinga'), state: newSlingaState(t.getAttribute('data-pb2-slinga')) };\n      render(); return;\n    }\n    if ((t = closest('data-pb2-slingasrc'))) {\n      collectSlingaInputs();\n      var gid = t.getAttribute('data-pb2-slingasrc');\n      var mode = state.ui.slinga.state.modes[state.ui.slinga.state.activeMode];\n      var ix = -1; mode.sources.forEach(function (s, i) { if (s.dialGroupId === gid) ix = i; });\n      if (ix === -1) mode.sources.push({ dialGroupId: gid, weight: 50 }); else mode.sources.splice(ix, 1);\n      render(); return;\n    }\n    if ((t = closest('data-pb2-slingamode'))) {\n      collectSlingaInputs();\n      state.ui.slinga.state.activeMode = Number(t.getAttribute('data-pb2-slingamode')) || 0;\n      render(); return;\n    }\n    if ((t = closest('data-pb2-slingaddmode'))) {\n      collectSlingaInputs();\n      var stM = state.ui.slinga.state;\n      stM.modes.push({ name: 'Läge ' + (stM.modes.length + 1), isDefault: false, sources: [] });\n      stM.activeMode = stM.modes.length - 1;\n      render(); return;\n    }\n    if ((t = closest('data-pb2-slingasetdefault'))) {\n      collectSlingaInputs();\n      var stD = state.ui.slinga.state;\n      var cur = stD.modes[stD.activeMode].isDefault;\n      stD.modes.forEach(function (m) { m.isDefault = false; });\n      stD.modes[stD.activeMode].isDefault = !cur;\n      if (!stD.modes.some(function (m) { return m.isDefault; })) stD.modes[0].isDefault = true;\n      render(); return;\n    }\n    if ((t = closest('data-pb2-drill'))) {\n      var dp = t.getAttribute('data-pb2-drill').split(':');\n      state.ui.drill = { kind: dp[0], id: dp[1], pid: dp[2] || null };\n      state.ui.drillData = null;\n      if (dp[0] === 'group') fetchGroupStats(dp[1]);\n      render(); return;\n    }\n    if ((t = closest('data-pb2-dial'))) {\n      var da = t.getAttribute('data-pb2-dial').split(':');\n      setDialControl(da[0], da[1], da[2]); return;\n    }\n    if (closest('data-pb2-drillclose') && el.getAttribute && el.getAttribute('data-pb2-drillclose')) {\n      state.ui.drill = null; state.ui.drillData = null; render(); return;\n    }\n    if (closest('data-pb2-notifyclose') && el.getAttribute && el.getAttribute('data-pb2-notifyclose')) {\n      collectNotifyInputs(); state.ui.notify = null; render(); return;\n    }\n    if (closest('data-pb2-slingaclose') && el.getAttribute && el.getAttribute('data-pb2-slingaclose')) {\n      collectSlingaInputs(); state.ui.slinga = null; render(); return;\n    }\n    if (closest('data-pb2-staffclose') && el.getAttribute && el.getAttribute('data-pb2-staffclose')) {\n      collectStaffingSettings(); state.ui.staffingOpen = false; render(); return;\n    }\n    if (closest('data-pb2-progclose') && el.getAttribute && el.getAttribute('data-pb2-progclose')) {\n      collectProgInputs(); state.ui.progOpen = false; render(); return;\n    }\n    if ((t = closest('data-pb2-progscope'))) {\n      collectProgInputs();\n      state.ui.progScope = t.getAttribute('data-pb2-progscope');\n      render(); return;\n    }\n    if ((t = closest('data-pb2-indtoggle'))) {\n      collectProgInputs();\n      ensureRevScopes();\n      state.cfg.revenue.showIndicators = !indicatorsOn();\n      render(); return;\n    }\n    if ((t = closest('data-pb2-revcustomize'))) {\n      collectProgInputs();\n      var scC = ensureScopeCfg(progScopeKey());\n      scC.drivers = scopeDrivers('all').map(function (d) { return { type: d.type, value: d.value, tagId: d.tagId || null }; });\n      if (!scC.drivers.length) scC.drivers = DEFAULT_DRIVERS.slice();\n      render(); return;\n    }\n    if ((t = closest('data-pb2-revinherit'))) {\n      collectProgInputs();\n      var scI = ensureScopeCfg(progScopeKey());\n      delete scI.drivers;\n      render(); return;\n    }\n    if ((t = closest('data-pb2-revtype'))) {\n      collectProgInputs();\n      var rp = t.getAttribute('data-pb2-revtype').split(':');\n      var scT = ensureScopeCfg(progScopeKey());\n      var drv = scT.drivers && scT.drivers[Number(rp[0])];\n      if (drv) { drv.type = rp[1]; if (drv.type === 'signed') drv.tagId = null; }\n      render(); return;\n    }\n    if ((t = closest('data-pb2-revadd'))) {\n      collectProgInputs();\n      var scA = ensureScopeCfg(progScopeKey());\n      scA.drivers = scA.drivers || [];\n      scA.drivers.push({ type: 'signed', value: 1000 });\n      render(); return;\n    }\n    if ((t = closest('data-pb2-revdel'))) {\n      collectProgInputs();\n      var scD = ensureScopeCfg(progScopeKey());\n      if (scD.drivers) scD.drivers.splice(Number(t.getAttribute('data-pb2-revdel')), 1);\n      render(); return;\n    }\n    if ((t = closest('data-pb2-tab'))) {\n      var parts = t.getAttribute('data-pb2-tab').split(':');\n      if (state.prefs.cardTab[parts[0]] !== parts[1]) state.ui._animCard = parts[0];\n      state.prefs.cardTab[parts[0]] = parts[1];\n      savePrefs(); render(); return;\n    }\n    if ((t = closest('data-pb2-pp'))) {\n      var pid = t.getAttribute('data-pb2-pp');\n      var ps = projectPauseState(pid);\n      if (ps.paused) { state.ui.armPause = null; setProjectPause(pid, false); return; }\n      if (state.ui.armPause === pid) {\n        state.ui.armPause = null;\n        setProjectPause(pid, true);\n      } else {\n        state.ui.armPause = pid;\n        toast('Klicka igen för att pausa projektet');\n        render();\n        setTimeout(function () { if (state.ui.armPause === pid) { state.ui.armPause = null; render(); } }, 4000);\n      }\n      return;\n    }\n    if ((t = closest('data-pb2-act'))) {\n      var act = t.getAttribute('data-pb2-act');\n      if (act === 'settings') { state.ui.settingsOpen = true; render(); return; }\n      if (act === 'close-settings') {\n        //  Avbryt: återställ de personaliserbara nycklarna till senast sparade (allas ev. + mina)\n        collectSettingsInputs(); applyPersonalSettings();\n        state.ui.settingsOpen = false; render(); return;\n      }\n      if (act === 'close-drill') { state.ui.drill = null; state.ui.drillData = null; render(); return; }\n      if (act === 'notify-send') { sendNotify(); return; }\n      if (act === 'notify-close') { state.ui.notify = null; render(); return; }\n      if (act === 'slinga-save') { saveSlinga(); return; }\n      if (act === 'slinga-close') { state.ui.slinga = null; render(); return; }\n      if (act === 'staffing-settings') { state.ui.staffingOpen = true; render(); return; }\n      if (act === 'staff-close') { state.ui.staffingOpen = false; render(); return; }\n      if (act === 'staff-save') {\n        collectStaffingSettings();\n        state.ui.staffingOpen = false;\n        if (state.rights.isAdmin || state.rights.manageSettings) savePresetState().then(function () { toast('Bemanningsinställningar sparade'); }).catch(function () { toast('Kunde inte spara'); });\n        render(); return;\n      }\n      if (act === 'prognos-settings') { state.ui.progOpen = true; state.ui.progScope = state.forecast.selProject || 'all'; render(); return; }\n      if (act === 'goto-prognos-settings') { collectSettingsInputs(); state.ui.settingsOpen = false; state.ui.progOpen = true; state.ui.progScope = 'all'; render(); return; }\n      if (act === 'prog-close') { state.ui.progOpen = false; render(); return; }\n      if (act === 'prog-save') {\n        collectProgInputs();\n        state.ui.progOpen = false;\n        if (state.rights.isAdmin || state.rights.manageSettings) savePresetState().then(function () { toast('Prognosinställningar sparade'); }).catch(function () { toast('Kunde inte spara'); });\n        render(); return;\n      }\n      if (act === 'save-mine') {\n        //  Spara för mig: lagra de personaliserbara nycklarna i mina prefs (påverkar bara mig)\n        collectSettingsInputs();\n        state.prefs.settings = {};\n        SETTINGS_KEYS.forEach(function (k) { state.prefs.settings[k] = clone(state.cfg[k]); });\n        savePrefs();\n        state.ui.settingsOpen = false;\n        toast('Sparat för mig');\n        render(); refreshStats(); return;\n      }\n      if (act === 'save-all') {\n        //  Spara för alla: skriv de personaliserbara nycklarna till allas baslinje + presetet, rensa mina\n        if (!canEditSettings()) { toast('Du saknar rättighet att spara för alla'); return; }\n        collectSettingsInputs();\n        SETTINGS_KEYS.forEach(function (k) { state.baseSettings[k] = clone(state.cfg[k]); });\n        state.prefs.settings = {}; savePrefs();\n        state.cfg.configured = true;\n        savePresetState().then(function () {\n          state.ui.settingsOpen = false;\n          toast('Sparat för alla');\n          render(); refreshStats();\n        }).catch(function () { toast('Kunde inte spara för alla (rättighet saknas?)'); });\n        return;\n      }\n      if (act === 'settings-reset-mine') {\n        //  Återställ mina → följ allas\n        state.prefs.settings = {}; savePrefs();\n        applyPersonalSettings(); renderSettingsOnly(); return;\n      }\n    }\n    if ((t = closest('data-pb2-acc'))) { state.ui.accOpen = t.getAttribute('data-pb2-acc'); renderSettingsOnly(); return; }\n    if ((t = closest('data-pb2-cfgproj'))) {\n      var id = t.getAttribute('data-pb2-cfgproj');\n      var arr = state.cfg.projectIds || [];\n      var ix = arr.indexOf(id);\n      if (ix === -1) arr.push(id); else arr.splice(ix, 1);\n      state.cfg.projectIds = arr;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgstatus'))) {\n      var sid2 = t.getAttribute('data-pb2-cfgstatus');\n      if (!state.cfg.statusesVisible) state.cfg.statusesVisible = defaultCfg().statusesVisible;\n      state.cfg.statusesVisible[sid2] = state.cfg.statusesVisible[sid2] === false;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgpin'))) {\n      var pmid = t.getAttribute('data-pb2-cfgpin');\n      var arr = (state.cfg.pinnedMetrics && state.cfg.pinnedMetrics.length ? state.cfg.pinnedMetrics : DEFAULT_PINS).slice();\n      var pix = arr.indexOf(pmid);\n      if (pix !== -1) arr.splice(pix, 1);\n      else if (arr.length < 6) arr.push(pmid);\n      else toast('Max 6 pinnade nyckeltal — ta bort ett först');\n      state.cfg.pinnedMetrics = arr;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgcols'))) {\n      state.cfg.cardsPerRow = Number(t.getAttribute('data-pb2-cfgcols')) || 4;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgother'))) {\n      state.cfg.showOther = state.cfg.showOther === false;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfggauges'))) {\n      state.cfg.showGauges = state.cfg.showGauges === false;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgusergroup'))) {\n      state.cfg.showUserGroup = !state.cfg.showUserGroup;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgsection'))) {\n      var sk = t.getAttribute('data-pb2-cfgsection');\n      state.cfg.sections = state.cfg.sections || { nav: true, body: true, bottom: true, tabs: true };\n      state.cfg.sections[sk] = state.cfg.sections[sk] === false;\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfgweekend'))) {\n      state.prefs.chartWeekends = state.prefs.chartWeekends === false;\n      savePrefs(); renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfghit'))) {\n      state.cfg.hitRate = state.cfg.hitRate || {};\n      state.cfg.hitRate.mode = t.getAttribute('data-pb2-cfghit');\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-cfghittag'))) {\n      state.cfg.hitRate = state.cfg.hitRate || { mode: 'callTag' };\n      state.cfg.hitRate.tagId = t.getAttribute('data-pb2-cfghittag');\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-dir'))) {\n      var dp = t.getAttribute('data-pb2-dir').split(':');\n      state.cfg.metrics = state.cfg.metrics || {};\n      state.cfg.metrics[dp[0]] = state.cfg.metrics[dp[0]] || {};\n      state.cfg.metrics[dp[0]].dir = dp[1];\n      renderSettingsOnly(); return;\n    }\n    if ((t = closest('data-pb2-staffdir'))) {\n      collectStaffingSettings();\n      var sd = t.getAttribute('data-pb2-staffdir').split(':');\n      state.cfg.staffing = state.cfg.staffing || {}; state.cfg.staffing.targets = state.cfg.staffing.targets || {};\n      state.cfg.staffing.targets[sd[0]] = state.cfg.staffing.targets[sd[0]] || {};\n      state.cfg.staffing.targets[sd[0]].dir = sd[1];\n      render(); return;\n    }\n    if ((t = closest('data-pb2-progdir'))) {\n      collectProgInputs();\n      var pd = t.getAttribute('data-pb2-progdir').split(':');\n      var scB = ensureScopeCfg(progScopeKey());\n      scB.baselines[pd[0]] = scB.baselines[pd[0]] || {};\n      scB.baselines[pd[0]].dir = pd[1];\n      render(); return;\n    }\n    if (closest('data-pb2-overlay') && el.getAttribute && el.getAttribute('data-pb2-overlay')) {\n      state.ui.settingsOpen = false; render(); return;\n    }\n  }\n  function collectSettingsInputs() {\n    var els = document.querySelectorAll('[data-pb2-bench]');\n    state.cfg.metrics = state.cfg.metrics || {};\n    for (var i = 0; i < els.length; i++) {\n      var id = els[i].getAttribute('data-pb2-bench');\n      var v = els[i].value;\n      state.cfg.metrics[id] = state.cfg.metrics[id] || {};\n      state.cfg.metrics[id].benchmark = v === '' ? null : Number(v);\n    }\n    var tb = document.querySelector('[data-pb2-tempobench]');\n    if (tb && tb.value) state.cfg.tempoBenchmark = Number(tb.value) || 25;\n  }\n  function renderSettingsOnly() {\n    collectSettingsInputs();\n    render();\n  }\n\n  //  hover-värde på prognosdiagrammet\n  function onChartHover(ev) {\n    var wrap = ev.target;\n    while (wrap && wrap.getAttribute && wrap.getAttribute('data-pb2-charthover') === null) wrap = wrap.parentNode;\n    if (!wrap || !wrap.getAttribute) return;\n    var cross = wrap.querySelector('.pb2-crosshair'), tip = wrap.querySelector('.pb2-charttip');\n    if (!cross || !tip) return;\n    var series; try { series = JSON.parse(wrap.getAttribute('data-pb2-series')); } catch (e) { return; }\n    if (!series || !series.length) return;\n    var rect = wrap.getBoundingClientRect();\n    var x = ev.clientX - rect.left;\n    var frac = Math.max(0, Math.min(1, x / rect.width));\n    var idx = Math.round(frac * (series.length - 1));\n    var pt = series[idx];\n    var px = (series.length === 1 ? 0 : rect.width * idx / (series.length - 1));\n    cross.style.left = px + 'px'; cross.style.opacity = '1';\n    tip.style.left = px + 'px'; tip.style.opacity = '1';\n    tip.innerHTML = '<b>' + fmtKr(pt.v) + '</b> · ' + (wrap.getAttribute('data-pb2-monthlabel') || '').replace(/ \\d{4}/, '') + ' ' + pt.d + (pt.f ? ' (prognos)' : '');\n  }\n  function onChartLeave(ev) {\n    var wrap = ev.target;\n    while (wrap && wrap.getAttribute && wrap.getAttribute('data-pb2-charthover') === null) wrap = wrap.parentNode;\n    if (!wrap || !wrap.querySelector) return;\n    var c = wrap.querySelector('.pb2-crosshair'), t = wrap.querySelector('.pb2-charttip');\n    if (c) c.style.opacity = '0'; if (t) t.style.opacity = '0';\n  }\n\n  //  slinga vikt-slider: uppdatera vikt + procentläsning live utan omritning (annars tappas dragfokus)\n  function onSlingaSlider(ev) {\n    var el = ev.target;\n    if (!el || !el.getAttribute || el.getAttribute('data-pb2-slingaweight') === null) return;\n    var gid = el.getAttribute('data-pb2-slingaweight');\n    if (!state.ui.slinga) return;\n    var mode = state.ui.slinga.state.modes[state.ui.slinga.state.activeMode];\n    var src = mode.sources.filter(function (s) { return s.dialGroupId === gid; })[0];\n    if (!src) return;\n    src.weight = Math.max(0, Number(el.value) || 0);\n    var pct = el.parentNode.querySelector('.pb2-srcpct'); if (pct) pct.textContent = src.weight;\n    //  uppdatera fördelnings-hinten\n    var hint = el.closest('.pb2-modal-body') && el.closest('.pb2-modal-body').querySelector('.pb2-hint');\n    if (hint) {\n      var tot = mode.sources.reduce(function (a, s) { return a + (s.weight || 0); }, 0);\n      hint.innerHTML = 'Fördelning: ' + mode.sources.map(function (s) { var g = state.groupById[s.dialGroupId]; return esc(g ? g.name : '?') + ' ' + (tot ? Math.round(100 * s.weight / tot) : 0) + '%'; }).join(' · ');\n    }\n  }\n\n  document.addEventListener('click', onClick, true);\n  document.addEventListener('input', onSlingaSlider, true);\n  document.addEventListener('mousemove', onChartHover, true);\n  document.addEventListener('mouseout', onChartLeave, true);\n\n  window.__pbCleanup = function () {\n    document.removeEventListener('click', onClick, true);\n    document.removeEventListener('input', onSlingaSlider, true);\n    document.removeEventListener('mousemove', onChartHover, true);\n    document.removeEventListener('mouseout', onChartLeave, true);\n    state.timers.forEach(function (t) { clearInterval(t); });\n    state.timers = [];\n    if (prefsTimer) clearTimeout(prefsTimer);\n    var toastEl = document.getElementById('pb2-toast');\n    if (toastEl) toastEl.remove();\n  };\n\n  boot();\n})();\n</script>","iconName":"headset","iconColor":"#C62828","settingsHtml":"<!-- Playbook — inställningsvy (settingsHtml på preseten). Renderas i Inställningar > Vyer.\n     Läser window.form.settings, skriver window.form.setSettings (persistas när admin klickar Spara).\n     Ingen dubbel-måsvinge (handlebars). -->\n<div id=\"pbs-root\">\n  <style>\n    #pbs-root { font-family: 'Nunito', sans-serif; color: var(--color-text-2, #333); max-width: 560px; }\n    #pbs-root * { box-sizing: border-box; }\n    .pbs-row { display: flex; align-items: flex-start; gap: 14px; padding: 14px 0; border-bottom: 1px solid var(--color-background-3, #eee); }\n    .pbs-row:last-child { border-bottom: none; }\n    .pbs-info { flex: 1 1 auto; }\n    .pbs-title { font-size: 16px; font-weight: var(--font-weight-default, normal); color: var(--color-text-2, #616161); }\n    .pbs-desc { font-size: 13px; color: var(--color-text-3, #757575); padding-top: 4px; line-height: 1.45; }\n    /* native Switch (core/Switch): 40x20 pill + knob, exakta transitioner */\n    .pbs-toggle {\n      flex: 0 0 auto; width: 40px; height: 20px; border-radius: 10px; box-sizing: border-box; cursor: pointer; margin-top: 4px;\n      background-color: transparent; border: 2px solid var(--color-background-4, #bdbdbd); position: relative;\n      transition: background-color 250ms cubic-bezier(0.33,1,0.68,1), border-color 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pbs-toggle::after {\n      content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%;\n      background-color: var(--color-primary, #1565c0);\n      transition: left 250ms cubic-bezier(0.33,1,0.68,1), background-color 250ms cubic-bezier(0.33,1,0.68,1), transform 250ms cubic-bezier(0.33,1,0.68,1);\n    }\n    .pbs-toggle:hover::after { transform: translateX(1px); }\n    .pbs-toggle.pbs-on { background-color: var(--color-primary, #1565c0); border-color: var(--color-primary, #1565c0); }\n    .pbs-toggle.pbs-on::after { left: 22px; background-color: var(--color-background-1, #fff); }\n    .pbs-toggle.pbs-on:hover::after { transform: translateX(-1px); }\n    .pbs-note { font-size: 12px; color: var(--color-text-4, #999); padding-top: 10px; }\n    .pbs-note i { margin-right: 6px; }\n  </style>\n  <div class=\"pbs-row\">\n    <div class=\"pbs-info\">\n      <div class=\"pbs-title\">Demoläge</div>\n      <div class=\"pbs-desc\">\n        Visar vyn med förifylld exempeldata: flera projekt, säljare och ringlistor som rör sig\n        genom olika lägen (toppfart till stiltje) med några sekunders mellanrum. Perfekt för att\n        förhandsgranska hur Playbook ser ut med full aktivitet — en tydlig \"Demodata\"-badge visas\n        i vyn så länge läget är på. Ingen riktig data hämtas eller påverkas.\n      </div>\n    </div>\n    <button type=\"button\" class=\"pbs-toggle\" id=\"pbs-demo-toggle\" aria-label=\"Demoläge\"></button>\n  </div>\n  <div class=\"pbs-note\"><i class=\"fas fa-info-circle\"></i>Övriga inställningar (projekt, statusar, hit rate, metriker) görs med kugghjulet inne i vyn.</div>\n</div>\n<script>\n(function () {\n  'use strict';\n  function current() {\n    try { return (window.form && window.form.settings) || {}; } catch (e) { return {}; }\n  }\n  function paint() {\n    var btn = document.getElementById('pbs-demo-toggle');\n    if (!btn) return;\n    if (current().demoMode) btn.classList.add('pbs-on');\n    else btn.classList.remove('pbs-on');\n  }\n  document.addEventListener('click', function (ev) {\n    var n = ev.target;\n    while (n && n.getAttribute) {\n      if (n.id === 'pbs-demo-toggle') {\n        var next = !current().demoMode;\n        try { window.form.setSettings({ demoMode: next }); } catch (e) {}\n        paint();\n        return;\n      }\n      n = n.parentNode;\n    }\n  }, true);\n  paint();\n  setInterval(paint, 1000);\n})();\n</script>","description":"","rights":[{"right":"viewOwnProjects","name":"Se egna projekt","description":"Visar Playbook för projekt där användarens team har ringlistor."},{"right":"viewAllProjects","name":"Se alla projekt","description":"Visar Playbook för organisationens alla projekt."},{"right":"controlDialer","name":"Pausa/starta projekt","description":"Pausa och starta projektens ringlistor (predictive). Kräver även rättighet att uppdatera ringlistor."},{"right":"viewUserDrilldown","name":"Se per säljare","description":"Visar flikarna Säljare och Jämför med individuella siffror och jämförelser."},{"right":"manageSettings","name":"Hantera inställningar","description":"Ändra Playbooks inställningar: projekt, statusar, hit rate, metriker och benchmarks."}],"components":[]},{"id":"6a3931eb5b1f8b96ab66acb1","name":"Inställningar","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a6708f3167811841a222c32"],"locations":[{"type":"settings","position":null}],"html":"<div>\n  <!-- Använd settings. för att läsa från inställningsvyn. -->\n  <!-- Använd custom. för att läsa från vyns state, precis som med webbformulär m.m.. -->\n  Detta är en exempelvy. Klicka på \"Inställningsvy\" för att komma till den. I inställningsvyn har du valt: {{settings.option}}\n</div>\n\n<div style=\"margin-top: 24px\">\n  Objektet för inställningar (window.form.settings) är separat från vyns state.\n\n  custom.message är: {{custom.message}}\n</div>\n\n<div style=\"margin-top: 24px; display: flex; flex-direction: row; gap: 16px;\">\n  <PrimaryButton onClick=\"window.form.setState({ message: 'Hej från vyn' })\">\n    Ändra custom.message\n  </PrimaryButton>\n</div>","iconName":"wrench","iconColor":null,"settingsHtml":"<div>\n  Detta är inställningsvyn. Kunden ser denna vyn under Inställningar > Vyer och kan göra egna inställningar för vyn.\n</div>\n\n<div style=\"margin-top: 24px\">\n  <Field select-type=\"dropdown\" label=\"Alternativ\" value=\"{{settings.option}}\" onChange=\"window.form.setSettings({ option: value })\">\n    <option value=\"Alternativ 1\">Alternativ 1</option>\n    <option value=\"Alternativ 2\">Alternativ 2</option>\n    <option value=\"Alternativ 3\">Alternativ 3</option>\n  </Field>\n</div>\n\n<div style=\"margin-top: 24px\">\n  Det går även att ändra vyns state från inställningsvyn.\n</div>\n\n<div style=\"margin-top: 24px\">\n  <PrimaryButton onClick=\"window.form.setState({ message: 'Hej från inställningsvyn' })\">\n    Ändra custom.message\n  </PrimaryButton>\n</div>\n\n<script>\n  //  Sätt standardvärdet.\n  window.form.setSettings(settings => ({\n    option: settings.option || 'Alternativ 1'\n  }));\n\n</script>","description":"","rights":[{"right":"test","name":"Test"}],"components":[]},{"id":"6a6086fbed711973a73e4826","name":"Tooltip","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a6708f3167811841a222c32"],"locations":[{"type":"settings","position":null}],"html":"<div style=\"display: flex; flex-direction: column; gap: 24px; align-items: flex-start;\">\n  <Tooltip tooltip=\"Hej hej\" delay=\"false\">\n    För musen över mig\n  </Tooltip>\n\n  <Tooltip tooltip=\"Hej hej\">\n    med fördröjning\n  </Tooltip>\n\n  <Tooltip tooltip=\"Hej hej\" delay=\"false\" position=\"bottom\">\n    under\n  </Tooltip>\n\n  <Tooltip tooltip=\"Hej hej\" delay=\"false\" position=\"top\">\n    över\n  </Tooltip>\n\n  <Tooltip tooltip=\"Hej hej\" delay=\"false\" position=\"left\">\n    vänster\n  </Tooltip>\n\n  <Tooltip tooltip=\"Hej hej\" delay=\"false\" position=\"right\">\n    höger\n  </Tooltip>\n</div>","iconName":"wrench","iconColor":null,"settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a69ade93e46511962ddb601","name":"Notiser","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a6708f3167811841a222c32"],"locations":[{"type":"settings","position":null}],"html":"<div style=\"display: flex; flex-direction: column; gap: 24px; align-items: flex-start;\">\n  <PrimaryButton onClick=\"window.form.notify('Hej hej', 'ok')\" icon=\"comment\">\n    Visa en enkel notis\n  </PrimaryButton>\n  \n  <PrimaryButton onClick=\"window.form.notify({ title: 'Avancerad notis', body: 'Hej hej', icon: { name: 'comment' } }); window.form.notify('Stäng för att se notisen.');\" icon=\"comment\">\n    Visa en avancerad notis\n  </PrimaryButton>\n</div>","iconName":"wrench","iconColor":null,"settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a69b1ad46ab98b9adfee4d8","name":"Modal","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a6708f3167811841a222c32"],"locations":[{"type":"settings","position":null}],"html":"<div style=\"display: flex; flex-direction: column; gap: 24px; align-items: flex-start;\">\n  <Field label=\"Jag heter\" value=\"{{custom.input}}\" onchange=\"window.form.setState({ input: value })\">\n  </Field>\n\n  <SecondaryButton icon=\"fullscreen\" onClick=\"window.openModal1()\">\n    Öppna modal 1\n  </SecondaryButton>\n\n  <SecondaryButton icon=\"fullscreen\" onClick=\"window.openModal2()\">\n    Öppna modal 2\n  </SecondaryButton>\n\n  <SecondaryButton icon=\"fullscreen\" onClick=\"window.openModal3()\">\n    Öppna modal 3\n  </SecondaryButton>\n\n  <!-- Detta element kommer visas i modalen. Observera attributet \"modal\". Detta element kommer inte finnas i dom:en utanför modalen. -->\n  <div class=\"my-modal-content\" modal style=\"display: flex; flex-direction: column; gap: 24px;\">\n    <div>Ha en fin dag, {{custom.input}}</div>\n\n    <SecondaryButton onClick=\"window.form.closeModal()\" size=\"compact\" style=\"width: 100%\">\n      Stäng modalen\n    </SecondaryButton>\n  </div>\n\n  <div class=\"my-modal-content2\" modal style=\"display: flex; flex-direction: column; gap: 24px;\">\n    <div>Nu ser du en \"Stäng\" knapp längst ner som stänger modalen.</div>\n\n    <SecondaryButton onClick=\"window.form.closeModal()\" size=\"compact\" style=\"width: 100%\">\n      Denna knapp stänger också modalen\n    </SecondaryButton>\n  </div>\n\n  <div class=\"my-modal-content3\" modal style=\"display: flex; flex-direction: column; gap: 24px;\">\n    <div>Klicka Fortsätt för att göra något asynkront och sedan stänga ner modalen.</div>\n    <div>Klicka Ta bort för att göra något asynkront som resulterar i ett fel.</div>\n  </div>\n</div>\n\n<script>\n  window.form.setState(state => ({\n    input: state.input || \"Kalle\"\n  }));\n\n  window.openModal1 = function () {\n    //  Första argumentet är vilket element som ska visas i modalen.\n    //  Andra argumentet är alternativ för modalen (se ModalProps i s2-frontend)\n    window.form.openModal('.my-modal-content', {\n      title: 'Hej ' + window.form.custom.input,\n      size: 'medium', //  'small' | 'small-medium' | 'large' | 'xlarge' | 'medium'\n      //  onClose?: () => { ... } //  Callback när modalen stängs. Returnera false för att förhindra att modalen stängs.\n      //  onHidden?: () => { ... } //  Callback när modalen är helt stängd, efter animationen.\n      //  backdropClassName?: string;\n      //  backdropStyle?: { width: '', ... };\n      //  modalClassName?: string;\n      //  modalStyle?: { width: '', ... };\n      //  hideBackdrop?: boolean;\n      //  transparentBackdrop?: boolean;\n      //  onlyBackdrop?: boolean;\n      //  preventPageScroll?: boolean;\n      //  animation?: 'zoom' | 'slide';\n      //  resizes?: boolean; // Sätt true om modalen ändrar storlek ofta, så blir den fäst längst uppe istället för mitten. \n    });\n  };\n\n  window.openModal2 = function () {\n    //  Första argumentet är vilket element som ska visas i modalen.\n    //  Andra argumentet är alternativ för modalen (se ModalProps i s2-frontend)\n    window.form.openModal('.my-modal-content2', {\n      title: 'Exempel 2',\n      subtitle: 'Jag är en underrubrik.',\n      size: 'medium',\n      cancel: {}\n    });\n  };\n\n  window.openModal3 = function () {\n    //  Första argumentet är vilket element som ska visas i modalen.\n    //  Andra argumentet är alternativ för modalen (se ModalProps i s2-frontend)\n    window.form.openModal('.my-modal-content3', {\n      title: 'Exempel 3',\n      size: 'medium',\n      cancel: {},\n      go: {\n        children: 'Fortsätt',\n        onClick: () => window.doSomethingAsync()\n          .then(() => true), // true stänger modalen.\n        type: 'next', //  'danger' | 'next' | 'nextSimple' | 'ok' | 'okSimple'\n      },\n      cancel: {\n        onClick: () => {\n          window.form.notify('Avbryt-knappen stänger vanligtvis modalen, men är ur funktion just nu!');\n          return false; //  returnera false för att stoppa.\n        }\n      },\n      separated: {\n        children: 'Ta bort',\n        tooltip: 'Något kommer gå snett...',\n        weight: 'danger', //  'primary' | 'normal' | 'dimmed' | 'danger' | 'simple' | 'accent'\n        onClick: () => window.doSomethingAsyncWithError()\n          .then(() => true),\n      }\n    });\n  };\n\n  window.doSomethingAsync = function () {\n    return new Promise(resolve => {\n      setTimeout(() => {\n        window.form.notify(\"Färdig!\");\n        resolve();\n      }, 3500);\n    });\n  }\n\n  window.doSomethingAsyncWithError = function () {\n    return new Promise((resolve, reject) => {\n      setTimeout(() => {\n        reject(new Error(\"Oops\"));\n      }, 3500);\n    });\n  }\n</script>","iconName":"wrench","iconColor":null,"settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a69ae523ae8cb502ac502da","name":"Övriga tips och tricks","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a6708f3167811841a222c32"],"locations":[{"type":"settings","position":null}],"html":"<style>\n  .tips-container {\n    display: flex;\n    flex-direction: column;\n    gap: 24px;\n  }\n\n  .tips-container>div {\n    display: flex;\n    flex-direction: column;\n    gap: 24px;\n    align-items: flex-start;\n    box-sizing: border-box;\n    padding: 24px;\n    border: 1px solid var(--color-background-3);\n    border-radius: 24px;\n  }\n\n  .tips-container h2 {\n    margin: 0;\n  }\n</style>\n\n<div class=\"tips-container\">\n  <div>\n    <h2>preview och window.form.preview</h2>\n\n    <div style=\"color: darkgreen\">Jag syns alltid.</div>\n\n    {{#if preview}}\n    <div style=\"color: darkred\">Men jag syns bara vid förhandsgranskning och inte i själva vyn.</div>\n    {{/if}}\n\n    <SecondaryButton onClick=\"window.example1_onClick()\">\n      Denna knapp funkar bara i faktiska vyn och på admin.salesys.se\n    </SecondaryButton>\n  </div>\n\n  <div>\n    <h2>Dubbelklicka</h2>\n\n    <SecondaryButton onClick=\"window.example2_onClick1()\">\n      Denna knapp kräver dubbelklick\n    </SecondaryButton>\n\n    <SecondaryButton onClick=\"window.example2_onClick2()\">\n      {{either custom.example2 \"Klicka mig\"}}\n    </SecondaryButton>\n  </div>\n\n  <div>\n    <h2>Knappar och underrubrik</h2>\n\n    <div>\n      <SecondaryButton onClick=\"window.example3_onClick1()\">\n        Lägg till knapp brevid titeln\n      </SecondaryButton>\n      <div style=\"marign-top: 8px; font-size: 14px; padding-left: 10px;\">\n        Syns bäst i riktig vy eller live-förhandsgranskning.\n      </div>\n    </div>\n\n    <Field label=\"Ange en underrubrik\" placeholder=\"Skriv något\" onChange=\"window.form.setSubtitle(value);\">\n    </Field>\n  </div>\n\n\n</div>\n\n<script>\n  window.example1_onClick = function () {\n    if (window.form.preview) {\n      alert('Inte tillgänglig under förhandsgranskning!');\n\n    } else {\n      window.form.notify('Funkar!');\n    }\n  };\n\n  window.example2_onClick1 = function () {\n    if (window.form.checkDoubleClick('Du måste dubbelklicka.')) {\n      window.form.notify('Dubbelklickat!');\n\n    } else {\n      return 'doubleClickRequired';\n    }\n  };\n\n  window.example2_onClick2 = function () {\n    const onSingleClick = () => {\n      window.form.setState({ example2: 'Enkelklick' });\n    };\n\n    //  Andra argumentet är vad som händer vid enkelklick.\n    if (window.form.checkDoubleClick(null, onSingleClick)) {\n      window.form.setState({ example2: 'Dubbelklick' })\n\n    } else {\n      return 'doubleClickRequired';\n    }\n  };\n\n  window.example3_onClick1 = function () {\n    //  Första argumentet är ett id på knappen.\n    //  Det  finns bara en knapp per id.\n    //  Om du vill ha flera knappar anger du fler ID.\n    window.form.setButton('add', {\n      children: \"Lägg till\",\n      weight: 'primary',\n      icon: \"plus\",\n      onClick: () => { window.form.notify(\"Klick!\", \"ok\"); }\n    });\n  };\n</script>","iconName":"wrench","iconColor":null,"settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a3932f03d85af4a87ce2b16","name":"Ringlistexport","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["69a306434e630d4dc2855b7e"],"locations":[{"type":"settings","position":null}],"html":"<div style=\"max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; flex-direction: column; gap: 16px;\">\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; align-items: center; gap: 12px;\">\n    <img src=\"https://salesys.se/wp-content/uploads/2021/09/salesys-icon-32.svg\" style=\"width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;\" />\n    <div>\n      <div style=\"color: var(--text-primary); font-weight: 600; font-size: 15px;\">Exportera ringlistor</div>\n      <div style=\"color: var(--text-primary); font-size: 12px; margin-top: 2px;\">Exporterar kontakter med samtal och orderinfo inom valt datumintervall</div>\n    </div>\n  </div>\n\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;\">\n    <div style=\"color: var(--text-primary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;\">Filter</div>\n\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <label for=\"filter-project\" style=\"color: var(--text-secondary); font-size: 12px;\">Projekt</label>\n      <select onChange=\"window.handleChangeProject(event.target.value)\" id=\"filter-project\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font-size: 13px; outline: none; width: 100%; cursor: pointer;\">\n        <option value=\"\">Välj projekt</option>\n        <option value=\"__ALL__\">Alla ringlistor (oavsett projekt)</option>\n        {{#each custom.projects}}\n        <option value=\"{{id}}\">{{name}}</option>\n        {{/each}}\n      </select>\n    </div>\n\n    {{#if custom.selectedProject}}\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <label style=\"color: var(--text-secondary); font-size: 12px;\">Ringlistor</label>\n        <span onclick=\"window.toggleAll('filter-dial-groups')\" style=\"color: var(--text-secondary); font-size: 11px; cursor: pointer; user-select: none;\">Markera alla</span>\n      </div>\n      <div id=\"filter-dial-groups\" style=\"background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 4px; max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px;\">\n      </div>\n    </div>\n\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <label style=\"color: var(--text-secondary); font-size: 12px;\">Taggar</label>\n        <span onclick=\"window.toggleAll('filter-tags')\" style=\"color: var(--text-secondary); font-size: 11px; cursor: pointer; user-select: none;\">Markera alla</span>\n      </div>\n      <div id=\"filter-tags\" style=\"background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 4px; max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px;\">\n      </div>\n    </div>\n\n    <label style=\"display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 13px; cursor: pointer;\">\n      <input type=\"checkbox\" id=\"include-untagged\"> Inkludera samtal utan tagg\n    </label>\n\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <label style=\"color: var(--text-secondary); font-size: 12px;\">Kontaktfält</label>\n        <span onclick=\"window.toggleAll('filter-fields')\" style=\"color: var(--text-secondary); font-size: 11px; cursor: pointer; user-select: none;\">Markera alla</span>\n      </div>\n      <div id=\"filter-fields\" style=\"background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 4px; max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px;\">\n      </div>\n    </div>\n\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <label style=\"color: var(--text-secondary); font-size: 12px;\">Kolumner</label>\n        <span onclick=\"window.toggleAll('filter-columns')\" style=\"color: var(--text-secondary); font-size: 11px; cursor: pointer; user-select: none;\">Markera alla</span>\n      </div>\n      <div id=\"filter-columns\" style=\"background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 4px; max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px;\">\n      </div>\n    </div>\n    {{/if}}\n  </div>\n\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 16px;\">\n    <div style=\"display: flex; gap: 8px; flex-wrap: wrap;\">\n      <SecondaryButton onclick=\"window.setDatePreset(7)\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Senaste 7 dagarna</SecondaryButton>\n      <SecondaryButton onclick=\"window.setDatePreset(30)\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Senaste 30 dagarna</SecondaryButton>\n      <SecondaryButton onclick=\"window.setDatePreset('month')\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Denna månad</SecondaryButton>\n      <SecondaryButton onclick=\"window.setDatePreset('lastMonth')\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Förra månaden</SecondaryButton>\n    </div>\n    <div style=\"display: flex; gap: 12px;\">\n      <field type=\"date\" label=\"Startdatum\" containerStyle=\"flex: 1;\"\n        value=\"{{custom.startDate}}\"\n        onChange=\"window.form.setState({startDate: value})\" max=\"{{formatDate custom.endDate \"YYYY-MM-DD\"}}\">\n      </field>\n      <field type=\"date\" label=\"Slutdatum\" containerStyle=\"flex: 1;\" min=\"{{formatDate custom.startDate \"YYYY-MM-DD\"}}\"\n        value=\"{{custom.endDate}}\"\n        onChange=\"window.form.setState({endDate: value})\">\n      </field>\n    </div>\n\n    <PrimaryButton\n      {{#ifNot custom.selectedProject}} disabled {{/ifNot}}\n      {{#if custom.isBusy}} disabled {{/if}}\n      onClick=\"window.export()\"\n      icon=\"file-excel\"\n      iconPosition=\"before\"\n    >\n      {{#if custom.isBusy}}Exporterar…{{else}}Exportera{{/if}}\n    </PrimaryButton>\n  </div>\n\n  {{#if custom.isBusy}}\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 10px;\">\n    <div style=\"display: flex; justify-content: space-between; align-items: baseline;\">\n      <span style=\"color: var(--text-secondary); font-size: 13px; font-weight: 500;\">{{custom.currentDialGroup}}</span>\n      <span style=\"color: var(--text-muted); font-size: 12px;\">{{custom.currentDialGroupIndex}} / {{custom.totalDialGroups}}</span>\n    </div>\n    <div style=\"background: var(--bg-input); border-radius: 99px; height: 6px; overflow: hidden;\">\n      <div style=\"background: var(--accent); height: 100%; border-radius: 99px; width: {{custom.exportProgressPercent}}%; transition: width 0.3s ease;\"></div>\n    </div>\n  </div>\n  {{/if}}\n</div>\n\n<script src=\"https://unpkg.com/write-excel-file@1.x/bundle/write-excel-file.min.js\"></script>\n\n<script>\n  const isDarkModeEnabled = window.sessionStorage.getItem(\"isDarkModePreferred\") === \"true\";\n  if (!isDarkModeEnabled) {\n    document.documentElement.classList.add('light');\n  }\n\n  (function () {\n    if (!window.form.custom?.token) {\n      getSalesysAccessToken();\n    }\n  })();\n\n  const CALL_DISCONNECT_REASON = {\n    heartbeatTimedOut: \"Heartbeat timeout\",\n    callAction: \"Samtalsåtgärd\",\n    voicemail: \"Röstbrevlåda\",\n    noPredictiveModeUsers: \"Inga användare i prediktivt läge\",\n    other: \"Annat\",\n    contactHangUp: \"Kontakten lade på\",\n    userHangUpNormal: \"Användaren lade på\",\n    userHangUpCohearing: \"Användaren lade på (medhörning)\",\n    userAnswerInboundCall: \"Användaren svarade inkommande samtal\",\n    userLeavePredictiveQueue: \"Användaren lämnade prediktiv kö\",\n    userDistributedOffer: \"Användaren distribuerade ett erbjudande\",\n    userOther: \"Användaren lade på (annat)\",\n  };\n\n  async function getProjects() {\n    const res = await fetch(`https://app.salesys.se/api/users/projects-v1`, {\n      method: \"GET\",\n      headers: {\n        \"Content-type\": \"application/json\",\n        Authorization: `Bearer ${window.form.custom.token}`,\n      },\n    });\n    const data = await res.json();\n    return data.map((entry) => ({id: entry.id, name: entry.name}));\n  }\n\n  async function getDialGroups() {\n    const dialGroups = [];\n    let offset = 0;\n    while (true) {\n      const res = await fetch(\n        `https://app.salesys.se/api/dial/dial-groups-v2?offset=${offset}`,\n        {\n          method: \"GET\",\n          headers: {\n            \"Content-type\": \"application/json\",\n            Authorization: `Bearer ${window.form.custom.token}`,\n          },\n        },\n      );\n      const data = await res.json();\n\n      if (!data.length) break;\n\n      console.log(`Reading dialGroups offset ${offset}`);\n\n      for (const entry of data) {\n        dialGroups.push({\n          id: entry.id,\n          name: entry.name,\n          projectIds: entry.projectIds\n        });\n      }\n      offset += 100;\n    }\n\n    return dialGroups.reverse();\n  }\n\n  async function getDialGroupContacts(id, name) {\n    const contacts = [];\n    let offset = 0;\n    while (true) {\n      const res = await fetch(\n        `https://app.salesys.se/api/contacts/contacts-v1?dialGroupIds=${id}&offset=${offset}`,\n        {\n          method: \"GET\",\n          headers: {\n            \"Content-type\": \"application/json\",\n            Authorization: `Bearer ${window.form.custom.token}`,\n          },\n        },\n      );\n      const data = await res.json();\n\n      if (!data.length) break;\n\n      console.log(`Reading contacts offset ${offset}`);\n\n      for (const entry of data) {\n        contacts.push({\n          id: entry.id,\n          fields: entry.fields,\n          dialGroupName: name,\n        });\n      }\n      offset += 100;\n    }\n\n    return contacts;\n  }\n\n  async function getContactCall(\n    contacts,\n    dialGroupId,\n    userFullName,\n    tagLabels,\n    selectedTags,\n  ) {\n    const contactsWithCalls = [];\n\n    for (let i = 0; i < contacts.length; i += 50) {\n      const batch = contacts.slice(i, i + 50);\n\n      const from = new Date(window.form.custom.startDate).setUTCHours(00, 00, 00, 000);;\n      const to = new Date(window.form.custom.endDate).setUTCHours(23, 59, 59, 999);\n\n      const url = new URL(\"https://app.salesys.se/api/dial/calls-v1\")\n      url.searchParams.set('after', new Date(from).toISOString())\n      url.searchParams.set('before', new Date(to).toISOString())\n      url.searchParams.set('isContactUnique', \"true\")\n      url.searchParams.set('sortOrder', \"desc\")\n      url.searchParams.set('contactIds', batch.map((c) => c.id).join(\",\"));\n\n      const res = await fetch(\n        url.href,\n        {\n          method: \"GET\",\n          headers: {\n            \"Content-type\": \"application/json\",\n            Authorization: `Bearer ${window.form.custom.token}`,\n          },\n        },\n      );\n\n      const { calls } = await res.json();\n\n      for (const call of calls || []) {\n        const contact = contacts.find(\n          (contact) => contact.id === call.contactId,\n        );\n        if (!contact) continue;\n\n        const hasMatchingTag = call?.tagIds?.some(id => selectedTags?.includes(id));\n        const hasNoTags = !call?.tagIds?.length;\n        const includeUntagged = document.getElementById('include-untagged')?.checked;\n        if (!hasMatchingTag && !(hasNoTags && includeUntagged)) {\n          continue;\n        }\n\n        contact.latestCall = {\n          tags: call.tagIds.map(function(id) { return tagLabels[id]; }).filter(Boolean).join(', '),\n          date: call.connectingAt,\n          recipient: call.recipient,\n          caller: call.caller,\n          disconnectReason: CALL_DISCONNECT_REASON[call.disconnectReason],\n          connectedDurationSeconds: call.connectedDuration / 1000,\n        };\n        contact.user = userFullName[call.userId] ?? \"SaleSys (inget svar)\";\n\n        contactsWithCalls.push(contact);\n      }\n\n      console.log(\n        `Processed batch ${i}–${i + batch.length} of ${contacts.length}`,\n      );\n    }\n\n    return contactsWithCalls;\n  }\n\n  const EXTRA_COLUMNS = [\n    {\n        key: \"dialGroupName\",\n        label: \"Ringlista\",\n        getValue: (c) => c.dialGroupName\n    },\n    {\n        key: \"id\",\n        label: \"Kontakt-ID\",\n        getValue: (c) => c.id\n    },\n    {\n        key: \"user\",\n        label: \"Säljare\",\n        getValue: (c) => c.user ?? \"N/A\"\n    },\n    {\n        key: \"tags\",\n        label: \"Samtalstaggar\",\n        getValue: (c) => c.latestCall?.tags || \"N/A\"\n    },\n    {\n        key: \"date\",\n        label: \"Samtalsdatum\",\n        getValue: (c) => c.latestCall?.date ?? \"N/A\"\n    },\n    {\n        key: \"recipient\",\n        label: \"Samtalsmottagare\",\n        getValue: (c) => c.latestCall?.recipient ?? \"N/A\"\n    },\n    {\n        key: \"caller\",\n        label: \"Samtalsuppringare\",\n        getValue: (c) => c.latestCall?.caller ?? \"N/A\"\n    },\n    {\n        key: \"disconnectReason\",\n        label: \"Anledning samtalsslut\",\n        getValue: (c) => c.latestCall?.disconnectReason ?? \"N/A\"\n    },\n    {\n        key: \"duration\",\n        label: \"Samtalstid (sek)\",\n        getValue: (c) => c.latestCall ? (c.latestCall.connectedDurationSeconds === 0 ? \"N/A\" : c.latestCall.connectedDurationSeconds.toString()) : \"N/A\"\n    },\n  ];\n\n  function createExcelDataTables(contacts, fieldLabels, selectedColumns) {\n    const fileName = `contacts.xlsx`;\n\n    if (!contacts.length) return { rows: [], fileName };\n\n    const allFieldIds = [\n      ...new Set(contacts.flatMap((c) => c.fields.map((f) => f.fieldId))),\n    ].filter((fieldId) => fieldId in fieldLabels);\n\n    const rowHeadings = allFieldIds.map((fieldId) => ({\n      value: fieldLabels[fieldId],\n      type: String,\n      fontWeight: \"bold\",\n    }));\n\n    const activeColumns = selectedColumns && selectedColumns.length\n      ? EXTRA_COLUMNS.filter(function(col) { return selectedColumns?.includes(col.key); })\n      : EXTRA_COLUMNS;\n\n    const extraRowHeadings = activeColumns.map(function(col) {\n      return { value: col.label, type: String, fontWeight: \"bold\" };\n    });\n\n    const rows = [[...extraRowHeadings, ...rowHeadings]];\n\n    for (const contact of contacts) {\n      const fieldMap = Object.fromEntries(\n        contact.fields.map((f) => [f.fieldId, f.value]),\n      );\n      const row = allFieldIds.map((fieldId) => ({\n        value: fieldMap[fieldId] ?? \"\",\n        type: String,\n      }));\n\n      const extraValues = activeColumns.map(function(col) {\n        return { value: col.getValue(contact), type: String };\n      });\n\n      rows.push([...extraValues, ...row]);\n    }\n\n    return { rows, fileName };\n  }\n\n  async function createExcelFile(rows, fileName) {\n    try {\n      const blob = await writeXlsxFile(rows, { filePath: fileName });\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = fileName;\n      a.click();\n      URL.revokeObjectURL(url);\n    } catch (err) {\n      console.error(err);\n    }\n  }\n\n  function getSalesysAccessToken() {\n    const cookies = document.cookie.split(\";\").reduce((object, cookie) => {\n      const regex = /(.+)=(.+)/;\n      const parts = regex.exec(cookie.trim());\n      if (!parts || !parts.length) {\n        console.warn(\"Unrecognized cookie %s\", cookie, regex);\n        return object;\n      }\n\n      object[parts[1]] = parts[2];\n      return object;\n    }, {});\n\n    window.form.setState({ token: cookies[\"s2_utoken\"] });\n  }\n\n  window.form.addHook(\"fetchData\", () => {\n    if (!window.form.custom?.token || window.form.custom?.hasFetchedData) {\n      return;\n    }\n\n    window.form.setState({hasFetchedData: true});\n    getDialGroups().then(dialGroups => window.form.setState({dialGroups}));\n\n    const tags = {};\n    window.form.resources.callTags.forEach(tag => tags[tag.id] = tag.name);\n\n    getProjects().then(projects => window.form.setState({projects}));\n\n    window.form.setState({tags})\n  })\n\n  window.setDatePreset = (preset) => {\n    const today = new Date();\n    const pad = n => String(n).padStart(2, '0');\n    const fmt = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n\n    let start, end;\n\n    if (preset === 'month') {\n      start = new Date(today.getFullYear(), today.getMonth(), 1);\n      end = today;\n    } else if (preset === 'lastMonth') {\n      start = new Date(today.getFullYear(), today.getMonth() - 1, 1);\n      end = new Date(today.getFullYear(), today.getMonth(), 0);\n    } else {\n      start = new Date(today);\n      start.setDate(today.getDate() - preset);\n      end = today;\n    }\n\n    window.form.setState({ startDate: fmt(start), endDate: fmt(end) });\n  }\n\n  window.toggleAll = (containerId) => {\n    const checkboxes = [...document.querySelectorAll(`#${containerId} input[type=\"checkbox\"]`)];\n    const allChecked = checkboxes.every(cb => cb.checked);\n    checkboxes.forEach(cb => cb.checked = !allChecked);\n  }\n\n  window.handleChangeProject = (projectId) => {\n    window.form.setState({selectedProject: projectId});\n\n    setTimeout(() => {\n      const dialGroupsFilter = document.getElementById('filter-dial-groups');\n      const tagsFilter = document.getElementById('filter-tags');\n      const fieldsFilter = document.getElementById('filter-fields');\n      const columnsFilter = document.getElementById('filter-columns');\n\n      if (!dialGroupsFilter || !tagsFilter || !fieldsFilter || !columnsFilter) {\n        return;\n      }\n\n      dialGroupsFilter.innerHTML = '';\n      tagsFilter.innerHTML = '';\n      fieldsFilter.innerHTML = '';\n      columnsFilter.innerHTML = '';\n\n      EXTRA_COLUMNS.forEach(function(col) {\n        const label = document.createElement('label');\n        label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n        label.innerHTML = '<input type=\"checkbox\" value=\"' + col.key + '\"> ' + col.label;\n        columnsFilter.appendChild(label);\n      });\n\n      const dialGroups = projectId === '__ALL__'\n        ? (window.form.custom.dialGroups || [])\n        : (window.form.custom.dialGroups || []).filter(dg => dg.projectIds?.includes(projectId));\n      const tags = Object.entries(window.form.custom.tags || {}).map(([id, name]) => ({ id, name }));\n      const fields = (window.form.resources.contactFields || []);\n\n      dialGroups.forEach(group => {\n        const label = document.createElement('label');\n        label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n        label.innerHTML = `<input type=\"checkbox\" value=\"${group.id}\"> ${group.name}`;\n        dialGroupsFilter.appendChild(label);\n      });\n\n      tags.forEach(tag => {\n        const label = document.createElement('label');\n        label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n        label.innerHTML = `<input type=\"checkbox\" value=\"${tag.id}\"> ${tag.name}`;\n        tagsFilter.appendChild(label);\n      });\n\n      fields.forEach(field => {\n        const label = document.createElement('label');\n        label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n        label.innerHTML = `<input type=\"checkbox\" value=\"${field.id}\"> ${field.label}`;\n        fieldsFilter.appendChild(label);\n      });\n    }, 0);\n  }\n\n  window.export = () => {\n    const selectedDialGroups = [...document.querySelectorAll('#filter-dial-groups input:checked')].map(el => el.value);\n    const selectedTags = [...document.querySelectorAll('#filter-tags input:checked')].map(el => el.value);\n    const selectedFields = [...document.querySelectorAll('#filter-fields input:checked')].map(el => el.value);\n    const selectedColumns = [...document.querySelectorAll('#filter-columns input:checked')].map(el => el.value);\n\n    const c = window.form.custom;\n\n    if (!c?.startDate || !c?.endDate) {\n      alert(\"Vänligen välj startdatum och slutdatum\");\n      return;\n    }\n\n    if (!selectedDialGroups?.length) {\n      alert(\"Vänligen välj en eller flera ringlistor att exportera\");\n      return;\n    }\n\n    if (!selectedTags?.length) {\n      alert(\"Vänligen välj en eller flera taggar att filtrera på\");\n      return;\n    }\n\n    if (!selectedFields?.length) {\n      alert(\"Vänligen välj ett eller flera kontaktfält\");\n      return;\n    }\n\n    const fieldLabels = Object.fromEntries(\n      (window.form.resources.contactFields || [])\n        .filter(f => selectedFields?.includes(f.id))\n        .map(f => [f.id, f.label])\n    );\n\n    var allContacts = [];\n\n    window.form.setState({isBusy: true});\n\n    const userFullName = Object.fromEntries(\n      (window.form.resources.users || []).map(u => [u.id, u.fullName])\n    );\n    let dialGroupIndex = 0;\n    const tagLabels = window.form.custom.tags;\n    getDialGroups().then(function(allDialGroups) {\n      var dialGroups = allDialGroups.filter(function(dg) { return selectedDialGroups?.includes(dg.id); });\n      return dialGroups.reduce(function(chain, dialGroup) {\n        return chain.then(function() {\n          dialGroupIndex++;\n\n          window.form.setState({currentDialGroup: dialGroup.name?.slice(0, 20), currentDialGroupIndex: dialGroupIndex, totalDialGroups: dialGroups.length, exportProgressPercent: Math.round((dialGroupIndex / dialGroups.length) * 100)});\n          return getDialGroupContacts(dialGroup.id, dialGroup.name)\n            .then(function(contacts) {\n              return getContactCall(contacts, dialGroup.id, userFullName, tagLabels, selectedTags);\n            })\n            .then(function(contactsWithCalls) {\n              allContacts.push.apply(allContacts, contactsWithCalls);\n            });\n        });\n      }, Promise.resolve());\n    })\n    .then(function() {\n      var contactMap = new Map();\n      allContacts.forEach(function(c) {\n        var existing = contactMap.get(c.id);\n        var hasCall = !!c.latestCall;\n        var existingHasCall = existing && !!existing.latestCall;\n        if (!existing || (!existingHasCall && hasCall)) {\n          contactMap.set(c.id, c);\n        }\n      });\n      var uniqueContacts = Array.from(contactMap.values());\n      var result = createExcelDataTables(uniqueContacts, fieldLabels, selectedColumns);\n      window.form.setState({isBusy: false});\n\n      return createExcelFile(result.rows, result.fileName);\n    });\n  }\n</script>\n\n<style>\n:root {\n  --bg-card: #1b1c2f;\n  --bg-input: #252638;\n  --border: #3a3b58;\n  --card-border: transparent;\n  --text-primary: #e2e3f0;\n  --text-secondary: #c8c9e0;\n  --text-muted: #6668a0;\n  --accent: #4f5bd5;\n}\n:root.light {\n  --bg-card: #ffffff;\n  --bg-input: #f3f4f6;\n  --border: #e5e7eb;\n  --card-border: #e5e7eb;\n  --text-primary: #111827;\n  --text-secondary: #374151;\n  --text-muted: #6b7280;\n  --accent: #4f5bd5;\n}\n.field label {\n  color: var(--text-secondary);\n}\n</style>","iconName":"mobile-alt","iconColor":null,"settingsHtml":"","description":"Exportera en eller flera ringlistor med valfri kontaktinfo. Det är också möjligt att endast exportera ett eller flera olika samtalsutfall, exempelvis endast kontakter med taggen \"Tackade nej\".","rights":[],"components":[]},{"id":"6a3a7f501b5b160fc2596d7e","name":"Samtalsinspelningsexport","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["69a306434e630d4dc2855b7e"],"locations":[{"type":"settings","position":null}],"html":"<div style=\"max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; flex-direction: column; gap: 16px;\">\n\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; align-items: center; gap: 12px;\">\n    <img src=\"https://salesys.se/wp-content/uploads/2021/09/salesys-icon-32.svg\" style=\"width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;\"></img>\n    <div>\n      <div style=\"color: var(--text-primary); font-weight: 600; font-size: 15px;\">Exportera samtalsinspelningar</div>\n      <div style=\"color: var(--text-primary); font-size: 12px; margin-top: 2px;\">Exporterar samtalsinspelningar inom valt datumintervall</div>\n    </div>\n  </div>\n\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;\">\n    <div style=\"color: var(--text-primary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;\">Filter</div>\n\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <label style=\"color: var(--text-secondary); font-size: 12px;\">Ringlistor</label>\n        {{#ifNot custom.allDialGroupsMode}}\n        <span id=\"filter-dial-groups-toggle-all\" onclick=\"window.toggleAll('filter-dial-groups')\" style=\"color: var(--text-secondary); font-size: 11px; cursor: pointer; user-select: none;\">Markera alla</span>\n        {{/ifNot}}\n      </div>\n      <label style=\"display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 12px; cursor: pointer;\">\n        <input type=\"checkbox\" id=\"filter-all-dial-groups\" onchange=\"window.form.setState({allDialGroupsMode: event.target.checked})\"> Alla samtal (oavsett ringlista)\n      </label>\n      {{#ifNot custom.allDialGroupsMode}}\n      <div id=\"filter-dial-groups\" style=\"background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 4px; max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px;\">\n      </div>\n      {{/ifNot}}\n    </div>\n\n    <div style=\"display: flex; flex-direction: column; gap: 6px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <label style=\"color: var(--text-secondary); font-size: 12px;\">Taggar</label>\n        {{#ifNot custom.allTagsMode}}\n        {{#if resources.callTags}}\n        <span onclick=\"window.toggleAll('filter-tags')\" style=\"color: var(--text-secondary); font-size: 11px; cursor: pointer; user-select: none;\">Markera alla</span>\n        {{/if}}\n        {{/ifNot}}\n      </div>\n      <label style=\"display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 12px; cursor: pointer;\">\n        <input type=\"checkbox\" id=\"filter-all-tags\" onchange=\"window.form.setState({allTagsMode: event.target.checked})\"> Alla samtal (oavsett taggar)\n      </label>\n      {{#ifNot custom.allTagsMode}}\n      {{#if resources.callTags}}\n      <div id=\"filter-tags\" style=\"background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 4px; max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px;\">\n      </div>\n      {{/if}}\n      {{/ifNot}}\n    </div>\n  </div>\n\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 16px;\">\n    <div style=\"display: flex; gap: 8px; flex-wrap: wrap;\">\n      <SecondaryButton onclick=\"window.setDatePreset(7)\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Senaste 7 dagarna</SecondaryButton>\n      <SecondaryButton onclick=\"window.setDatePreset(30)\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Senaste 30 dagarna</SecondaryButton>\n      <SecondaryButton onclick=\"window.setDatePreset('month')\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Denna månad</SecondaryButton>\n      <SecondaryButton onclick=\"window.setDatePreset('lastMonth')\" style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;\">Förra månaden</SecondaryButton>\n    </div>\n\n    <div style=\"display: flex; gap: 12px;\">\n      <field type=\"date\" label=\"Startdatum\" containerStyle=\"flex: 1;\"\n        value=\"{{custom.startDate}}\"\n        onChange=\"window.form.setState({startDate: value})\" max=\"{{formatDate custom.endDate \"YYYY-MM-DD\"}}\">\n      </field>\n      <field type=\"date\" label=\"Slutdatum\" containerStyle=\"flex: 1;\" min=\"{{formatDate custom.startDate \"YYYY-MM-DD\"}}\"\n        value=\"{{custom.endDate}}\"\n        onChange=\"window.form.setState({endDate: value})\">\n      </field>\n    </div>\n\n    <PrimaryButton\n      {{#if custom.isBusy}} disabled {{/if}}\n      onClick=\"window.export()\"\n      icon=\"file-excel\"\n      iconPosition=\"before\"\n    >\n      {{#if custom.isBusy}}Exporterar…{{else}}Exportera{{/if}}\n    </PrimaryButton>\n  </div>\n\n  {{#if custom.isBusy}}\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 10px;\">\n    <div style=\"display: flex; justify-content: space-between; align-items: baseline;\">\n      <span style=\"color: var(--text-secondary); font-size: 13px; font-weight: 500;\">{{custom.currentDialGroup}}</span>\n      <span style=\"color: var(--text-muted); font-size: 12px;\">{{custom.currentDialGroupIndex}} / {{custom.totalDialGroups}}</span>\n    </div>\n    <div style=\"background: var(--bg-input); border-radius: 99px; height: 6px; overflow: hidden;\">\n      <div style=\"background: var(--accent); height: 100%; border-radius: 99px; width: {{custom.exportProgressPercent}}%; transition: width 0.3s ease;\"></div>\n    </div>\n  </div>\n  {{/if}}\n</div>\n\n<script>\n  import(\"https://cdn.jsdelivr.net/npm/client-zip@2.5.0/index.min.js\").then(function(m) {\n    window.downloadZip = m.downloadZip;\n  });\n\n  const isDarkModeEnabled = window.sessionStorage.getItem(\"isDarkModePreferred\") === \"true\";\n\n  if (!isDarkModeEnabled) {\n    document.documentElement.classList.add('light');\n  }\n</script>\n\n<script>\n  (function () {\n    if (!window.form.custom?.token) {\n      getSalesysAccessToken();\n    }\n\n  })();\n\n  async function getDialGroups() {\n    const dialGroups = [];\n    let offset = 0;\n    while (true) {\n      const res = await fetch(\n        `https://app.salesys.se/api/dial/dial-groups-v2?offset=${offset}`,\n        {\n          method: \"GET\",\n          headers: {\n            \"Content-type\": \"application/json\",\n            Authorization: `Bearer ${window.form.custom.token}`,\n          },\n        },\n      );\n      const data = await res.json();\n\n      if (!data.length) break;\n\n      for (const entry of data) {\n        dialGroups.push({\n          id: entry.id,\n          name: entry.name,\n        });\n      }\n      offset += 100;\n    }\n\n    return dialGroups.reverse();\n  }\n\n  async function getCalls(dialGroupId, selectedTags) {\n    const allCalls = [];\n    let offset = 0;\n    const from = new Date(window.form.custom.startDate).setUTCHours(0, 0, 0, 0);\n    const to = new Date(window.form.custom.endDate).setUTCHours(23, 59, 59, 999);\n    const queryParams = dialGroupId\n      ? `dialGroupIds=${dialGroupId}`\n      : `before=${encodeURIComponent(new Date(to).toISOString())}&after=${encodeURIComponent(new Date(from).toISOString())}`;\n\n    while (true) {\n      const res = await fetch(\n        `https://app.salesys.se/api/dial/calls-v1?${queryParams}&offset=${offset}`,\n        {\n          method: \"GET\",\n          headers: {\n            \"Content-type\": \"application/json\",\n            Authorization: `Bearer ${window.form.custom.token}`,\n          },\n        },\n      );\n\n      const { calls } = await res.json();\n\n      if (!calls?.length) break;\n\n      for (const call of calls) {\n        const callDate = new Date(call.connectingAt);\n        if (!callDate || callDate < from || callDate > to) continue;\n        if (selectedTags && !call?.tagIds?.some(id => selectedTags.includes(id))) continue;\n        if (!call.recipient) continue;\n\n        allCalls.push({\n          id: call.id,\n          recipient: call.recipient.replace(\"+\", \"\"),\n          date: new Date(callDate).toLocaleString(\"sv-SE\", {\n            timeStyle: \"short\",\n            dateStyle: \"short\"\n          }).replace(\" \", \"_\").replace(/:/g, \"-\")\n        });\n      }\n\n      offset += 100;\n    }\n\n    return allCalls;\n  }\n\n  async function getCallRecordings(calls) {\n    const concurrency = 15;\n    const callsWithRecordings = [];\n    let nextIndex = 0;\n    let completed = 0;\n\n    window.form.setState({\n      currentDialGroup: \"Hämtar inspelningar\",\n      currentDialGroupIndex: 0,\n      totalDialGroups: calls.length,\n      exportProgressPercent: 0\n    });\n\n    async function worker() {\n      while (nextIndex < calls.length) {\n        const call = calls[nextIndex++];\n\n        try {\n          const res = await fetch(\n            `https://app.salesys.se/api/dial/calls-v1/${call.id}/recording.wav`,\n            {\n              method: \"GET\",\n              headers: {\n                \"Content-type\": \"audio/x-wav\",\n                Authorization: `Bearer ${window.form.custom.token}`,\n              },\n            },\n          );\n\n          if (res.ok) {\n            const data = await res.blob();\n            callsWithRecordings.push({recording: data, recipient: call.recipient, date: call.date});\n          }\n        } catch (e) {\n          console.warn(`Failed to fetch recording for call ${call.id}:`, e);\n        }\n\n        completed++;\n        window.form.setState({\n          currentDialGroupIndex: completed,\n          exportProgressPercent: calls.length ? Math.round((completed / calls.length) * 100) : 100\n        });\n      }\n    }\n\n    await Promise.all(Array.from({ length: Math.min(concurrency, calls.length) }, worker));\n\n    return callsWithRecordings;\n  }\n\n  function downloadCallRecordings(callsWithRecordings) {\n    const files = callsWithRecordings.map((call, i) => ({ name: `${call.recipient}-${call.date}.wav`, input: call.recording }));\n    return window.downloadZip(files).blob().then(blob => {\n      const link = document.createElement(\"a\");\n      const url = URL.createObjectURL(blob);\n\n      link.href = url;\n      link.download = \"recordings.zip\";\n      link.click();\n      link.remove();\n\n      URL.revokeObjectURL(url)\n    });\n  }\n\n  function getSalesysAccessToken() {\n    const cookies = document.cookie.split(\";\").reduce((object, cookie) => {\n      const regex = /(.+)=(.+)/;\n      const parts = regex.exec(cookie.trim());\n      if (!parts || !parts.length) {\n        console.warn(\"Unrecognized cookie %s\", cookie, regex);\n        return object;\n      }\n\n      object[parts[1]] = parts[2];\n      return object;\n    }, {});\n\n    window.form.setState({ token: cookies[\"s2_utoken\"] });\n  }\n\n  window.form.addHook(\"fetchData\", () => {\n    if (!window.form.custom?.token || window.form.custom?.hasFetchedData) {\n      return;\n    }\n\n    const dialGroupsFilter = document.getElementById('filter-dial-groups');\n    const tagsFilter = document.getElementById('filter-tags');\n\n    window.form.setState({hasFetchedData: true});\n\n    getDialGroups().then(dialGroups => {\n      window.form.setState({dialGroups});\n\n      const allDialGroupsMode = document.getElementById('filter-all-dial-groups').checked;\n\n      dialGroups.forEach(group => {\n        const label = document.createElement('label');\n        label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n        label.innerHTML = `<input type=\"checkbox\" value=\"${group.id}\"> ${group.name}`;\n        dialGroupsFilter?.appendChild(label);\n      });\n    });\n\n    window.form.resources.callTags.forEach(tag => {\n      const label = document.createElement('label');\n      label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n      label.innerHTML = `<input type=\"checkbox\" value=\"${tag.id}\"> ${tag.name}`;\n      tagsFilter.appendChild(label);\n    });\n  })\n\n  window.form.addHook(\"onAllDialGroupsChange\", () => {\n    if (window.form.custom.allDialGroupsMode === window.form.custom.previousAllDialGroupsMode) {\n      return;\n    }\n\n    const dialGroupsFilter = document.getElementById('filter-dial-groups');\n\n    window.form.setState({ previousAllDialGroupsMode: window.form.custom.allDialGroupsMode });\n    \n    window.form.custom.dialGroups.forEach(group => {\n      const label = document.createElement('label');\n      label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n      label.innerHTML = `<input type=\"checkbox\" value=\"${group.id}\"> ${group.name}`;\n      dialGroupsFilter?.appendChild(label);\n    });\n  })\n\n  window.form.addHook(\"onAllTagsChange\", () => {\n    if (window.form.custom.allTagsMode === window.form.custom.previousAllTagsMode) {\n      return;\n    }\n\n    const tagsFilter = document.getElementById('filter-tags');\n\n    window.form.setState({ previousAllTagsMode: window.form.custom.allTagsMode });\n\n    window.form.resources.callTags.forEach(tag => {\n      const label = document.createElement('label');\n      label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:13px;';\n      label.innerHTML = `<input type=\"checkbox\" value=\"${tag.id}\"> ${tag.name}`;\n      tagsFilter?.appendChild(label);\n    });\n  })\n\n  window.setDatePreset = (preset) => {\n    const today = new Date();\n    const pad = n => String(n).padStart(2, '0');\n    const fmt = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n\n    let start, end;\n\n    if (preset === 'month') {\n      start = new Date(today.getFullYear(), today.getMonth(), 1);\n      end = today;\n    } else if (preset === 'lastMonth') {\n      start = new Date(today.getFullYear(), today.getMonth() - 1, 1);\n      end = new Date(today.getFullYear(), today.getMonth(), 0);\n    } else {\n      start = new Date(today);\n      start.setDate(today.getDate() - preset);\n      end = today;\n    }\n\n    window.form.setState({ startDate: fmt(start), endDate: fmt(end) });\n  }\n\n  window.toggleAll = (containerId) => {\n    const checkboxes = [...document.querySelectorAll(`#${containerId} input[type=\"checkbox\"]`)];\n    const allChecked = checkboxes.every(cb => cb.checked);\n    checkboxes.forEach(cb => cb.checked = !allChecked);\n  }\n\n  window.export = () => {\n    const allDialGroupsMode = document.getElementById('filter-all-dial-groups').checked;\n    const allTagsMode = document.getElementById('filter-all-tags').checked;\n    const selectedDialGroups = [...document.querySelectorAll('#filter-dial-groups input:checked')].map(el => el.value);\n    const selectedTags = allTagsMode ? null : [...document.querySelectorAll('#filter-tags input:checked')].map(el => el.value);\n\n    const c = window.form.custom;\n\n    if (!c?.startDate || !c?.endDate) {\n      alert(\"Vänligen välj startdatum och slutdatum\");\n      return;\n    }\n\n    if (!allDialGroupsMode && !selectedDialGroups?.length) {\n      alert(\"Vänligen välj en eller flera ringlistor att exportera, eller kryssa i \\\"Alla samtal\\\"\");\n      return;\n    }\n\n    if (!allTagsMode && !selectedTags?.length) {\n      alert(\"Vänligen välj en eller flera taggar att filtrera på, eller kryssa i \\\"Alla samtal (oavsett taggar)\\\"\");\n      return;\n    }\n\n    window.form.setState({isBusy: true});\n\n    var callsPromise;\n\n    if (allDialGroupsMode) {\n      window.form.setState({\n        currentDialGroup: \"Alla samtal\",\n        currentDialGroupIndex: 1,\n        totalDialGroups: 1,\n        exportProgressPercent: 0\n      });\n\n      callsPromise = getCalls(null, selectedTags);\n    } else {\n      callsPromise = getDialGroups()\n        .then(function(allDialGroups) {\n          var dialGroups = allDialGroups.filter(function(dg) { return selectedDialGroups.includes(dg.id); });\n          return dialGroups.reduce(function(chain, dialGroup, index) {\n            return chain.then(function(allCallIds) {\n              window.form.setState({\n                currentDialGroup: dialGroup.name?.slice(0, 20),\n                currentDialGroupIndex: index + 1,\n                totalDialGroups: dialGroups.length,\n                exportProgressPercent: Math.round(((index + 1) / dialGroups.length) * 100)\n              });\n              return getCalls(dialGroup.id, selectedTags).then(function(calls) {\n                return allCallIds.concat(calls);\n              });\n            });\n          }, Promise.resolve([]));\n        });\n    }\n\n    callsPromise\n      .then(function(calls) {\n        return getCallRecordings(calls);\n      })\n      .then(function(callsWithRecordings) {\n        return downloadCallRecordings(callsWithRecordings);\n      })\n      .catch(function(err) {\n        console.error(\"Export failed:\", err);\n      })\n      .finally(function() {\n        window.form.setState({isBusy: false});\n      });\n  }\n</script>\n\n<style>\n:root {\n  --bg-card: #1b1c2f;\n  --bg-input: #252638;\n  --border: #3a3b58;\n  --card-border: transparent;\n  --text-primary: #e2e3f0;\n  --text-secondary: #c8c9e0;\n  --text-muted: #6668a0;\n  --accent: #4f5bd5;\n}\n:root.light {\n  --bg-card: #ffffff;\n  --bg-input: #f3f4f6;\n  --border: #e5e7eb;\n  --card-border: #e5e7eb;\n  --text-primary: #111827;\n  --text-secondary: #374151;\n  --text-muted: #6b7280;\n  --accent: #4f5bd5;\n}\n.field label {\n  color: var(--text-secondary);\n}\n</style>","iconName":"microphone","iconColor":null,"settingsHtml":"","description":"Samtalsinspelningar kan användas för att coacha medarbetare och utvärdera säljares prestationer.","rights":[],"components":[]},{"id":"6a3a80731b5b160fc25971d5","name":"Skicka notifikation","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["69a3068e4e630d4dc2855cd3"],"locations":[{"type":"settings","position":null}],"html":"<style>\n  .dark-mode-container {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    min-width: 320px;\n    max-width: 320px;\n    min-height: 68px;\n    padding: 8px 12px;\n    box-sizing: border-box;\n    position: relative;\n    z-index: 2;\n    background-color: #082240;\n    border: 1px solid #133357;\n    border-radius: 12px;\n  }\n\n  .notification-content {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    gap: 12px;\n  }\n\n  .notification-icon-wrapper {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    gap: 0;\n  }\n\n  .notification-text-wrapper {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    gap: 0;\n  }\n\n  .notification-title-row {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    height: 28px;\n  }\n\n  .notification-title {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    pointer-events: auto;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    color: #B0BEC5;\n    font-weight: 400;\n    font-size: 14px;\n    margin: 0;\n    max-width: 100%;\n    overflow: hidden;\n    white-space: nowrap;\n    flex-grow: 1;\n    width: 0;\n  }\n\n  .icon-button {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    pointer-events: auto;\n    font-family: Nunito, sans-serif;\n    white-space: nowrap;\n    margin: 0;\n    font-weight: normal;\n    font-size: 15px;\n    outline: none !important;\n    position: relative;\n    display: inline-flex;\n    gap: 8px;\n    flex-direction: row;\n    align-items: center;\n    justify-content: center;\n    user-select: none;\n    min-width: 28px;\n    max-width: 28px;\n    height: 28px;\n    box-sizing: border-box;\n    padding: 0 !important;\n    border-radius: 14px;\n    background: transparent;\n    border: none;\n    color: #CFD8DC;\n  }\n\n  .main-container {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n  }\n\n  .notification-message {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    font-weight: normal;\n    pointer-events: auto;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    font-size: 14px;\n    color: #B0BEC5;\n    margin: 0;\n    padding-right: 8px;\n    padding-bottom: 3px;\n  }\n\n  .notification-actions {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: flex-end;\n    position: relative;\n    margin-left: 36px;\n    top: 4px;\n  }\n\n  .light-mode-container {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    min-width: 320px;\n    max-width: 320px;\n    min-height: 68px;\n    padding: 8px 12px;\n    box-sizing: border-box;\n    position: relative;\n    z-index: 2;\n    background-color: #fafafa;\n    border: 1px solid #eee;\n    border-radius: 12px;\n  }\n\n  .light-notification-content {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    gap: 12px;\n  }\n\n  .light-notification-icon-wrapper {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    min-width: 36px;\n    max-width: 36px;\n    height: 100%;\n    align-items: center;\n    justify-content: center;\n    align-self: flex-start;\n    margin-top: 16px;\n  }\n\n  .light-notification-text-wrapper {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    gap: 0;\n  }\n\n  .light-notification-title-row {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    height: 28px;\n  }\n\n  .light-notification-title {\n    -webkit-tap-highlight-color: transparent;\n    pointer-events: auto;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    color: #B0BEC5;\n    font-weight: 400;\n    font-size: 14px;\n    margin: 0;\n    max-width: 100%;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    flex-grow: 1;\n    width: 0;\n  }\n\n  .light-message-body {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n  }\n\n  .dark-message-body {\n    -webkit-tap-highlight-color: transparent;\n    color-scheme: dark;\n    font-weight: normal;\n    pointer-events: auto;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    font-size: 14px;\n    color: #B0BEC5;\n    margin: 0;\n    padding-right: 8px;\n    padding-bottom: 3px;\n  }\n\n  .light-notification-actions {\n    -webkit-tap-highlight-color: transparent;\n    color: #B0BEC5;\n    font-weight: normal;\n    pointer-events: auto;\n    margin: 0;\n    font-family: Nunito, sans-serif;\n    padding: 0;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: flex-end;\n    position: relative;\n    margin-left: 36px;\n    top: 4px;\n  }\n\n  #display-settings-container .select-like-container {\n    width: fit-content !important;\n  }\n</style>\n\n{{#ifNot custom.sentNotification}}\n<div style=\"max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; display: flex; flex-direction: column;\">\n\n    <div style=\"padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;\">\n      <div style=\"color: var(--text-primary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;\">Meddelande</div>\n      <Field label=\"Titel\" required containerStyle=\"width: 100%;\" placeholder=\"Grattis!\" value=\"{{custom.title}}\"\n        onChange=\"window.form.setState({ title: value })\"></Field>\n      <Field label=\"Meddelande\" required containerStyle=\"width: 100%;\" placeholder=\"Grattis till ditt sälj!\"\n        value=\"{{custom.text}}\" onChange=\"window.form.setState({ text: value })\"></Field>\n      <Field label=\"Länk (frivilligt)\" containerStyle=\"width: 100%;\" placeholder=\"https://salesys.se\"\n        value=\"{{custom.link}}\" onChange=\"window.form.setState({ link: value })\"></Field>\n      <Field required label=\"Ikon\" value=\"{{custom.iconName}}\" containerStyle=\"width: 100%;\"\n        onChange=\"window.form.setState({ iconName: value })\">\n        <option selected value=\"envelope\">Brev</option>\n        <option value=\"star\">Stjärna</option>\n        <option value=\"magic\">Magi</option>\n        <option value=\"exclamation-triangle\">Varning</option>\n      </Field>\n      <CheckboxField label=\"Visa konfetti\" checked=\"{{custom.showConfetti}}\"\n        onChange=\"window.form.setState(state => ({showConfetti: !state.showConfetti}))\"></CheckboxField>\n    </div>\n\n    <div style=\"height: 1px; background: var(--border); margin: 0 24px;\"></div>\n\n    <div style=\"padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;\">\n      <div style=\"color: var(--text-primary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;\">Mottagare</div>\n      <Field label=\"Grupp\" value=\"{{custom.recipientType}}\" containerStyle=\"width: 100%;\"\n        onChange=\"window.form.setState({ recipientType: value, recipients: [], recipientLabels: [] })\">\n        <option value=\"user\">Användare</option>\n        <option value=\"userTeam\">Team</option>\n      </Field>\n\n      {{#ifEq custom.recipientType \"user\"}}\n      <Field label=\"Sök användare\" containerStyle=\"width: 100%;\"\n        value=\"{{custom.searchQuery}}\" onChange=\"window.form.setState({searchQuery: value})\"></Field>\n      {{#each custom.users}}\n      <CheckboxField label=\"{{fullName}}\" {{#ifIncludes ../custom.recipients id}} checked {{/ifIncludes}}\n        containerStyle=\"width: 100%;\" onChange=\"window.handleRecipientChange('{{id}}', '{{fullName}}')\"></CheckboxField>\n      {{/each}}\n      {{#if custom.recipients.length}}\n      <SecondaryButton weight=\"danger\" onclick=\"window.form.setState({recipients: [], recipientLabels: []})\">Rensa val</SecondaryButton>\n      {{/if}}\n      {{/ifEq}}\n\n      {{#ifEq custom.recipientType \"userTeam\"}}\n      {{#ifNot custom.allTeams.length}}\n      <p style=\"color: var(--text-muted); font-size: 13px; margin: 0;\">Du har inga teams, välj en annan användargrupp</p>\n      {{else}}\n      {{#each custom.allTeams}}\n      <CheckboxField label=\"{{name}}\" {{#ifIncludes ../custom.recipients id}} checked {{/ifIncludes}}\n        containerStyle=\"width: 100%;\" onChange=\"window.handleRecipientChange('{{id}}', '{{name}}')\"></CheckboxField>\n      {{/each}}\n      {{/ifNot}}\n      {{/ifEq}}\n    </div>\n\n    <div style=\"height: 1px; background: var(--border); margin: 0 24px;\"></div>\n\n    <div style=\"padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;\">\n      <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n        <div style=\"color: var(--text-primary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;\">Förhandsgranskning</div>\n        <div id=\"display-settings-container\" style=\"display: flex; align-items: center; gap: 6px;\">\n          <span style=\"color: var(--text-muted); font-size: 12px;\">Visa i</span>\n          <Field value=\"{{custom.previewMode}}\" onChange=\"window.form.setState({previewMode: value})\" containerStyle=\"width: 110px;\">\n            <option value=\"light\" selected>ljust läge</option>\n            <option value=\"dark\">mörkt läge</option>\n          </Field>\n        </div>\n      </div>\n\n      <p style=\"color: var(--text-secondary); font-size: 13px; margin: 0;\">{{custom.recipientsText}}</p>\n\n      {{#if custom.recipientsList.length}}\n      <ul style=\"list-style-type: disc; list-style-position: inside; margin: 0; padding: 0; color: var(--text-secondary); font-size: 13px;\">\n        {{#each custom.recipientsList}}\n        <li>{{../custom.prefix}} <span style=\"font-style: italic;\">{{this}}</span></li>\n        {{/each}}\n      </ul>\n      {{/if}}\n\n      {{#ifEq custom.previewMode \"light\"}}\n      <div style=\"background-color: #b1b1b1; padding: 16px; border-radius: 10px; display: flex; justify-content: center;\">\n        <div class=\"light-mode-container\">\n          <div class=\"light-notification-content\">\n            <div class=\"light-notification-icon-wrapper\">\n              {{#ifEq custom.iconName \"envelope\"}}<i class=\"fas fa-envelope\" style=\"width: 14px; height: 14px; color: #1565C0;\"></i>{{/ifEq}}\n              {{#ifEq custom.iconName \"star\"}}<i class=\"fas fa-star\" style=\"width: 14px; height: 14px; color: #1565C0;\"></i>{{/ifEq}}\n              {{#ifEq custom.iconName \"magic\"}}<i class=\"fas fa-magic\" style=\"width: 14px; height: 14px; color: #1565C0;\"></i>{{/ifEq}}\n              {{#ifEq custom.iconName \"exclamation-triangle\"}}<i class=\"fas fa-exclamation-triangle\" style=\"width: 14px; height: 14px; color: #1565C0;\"></i>{{/ifEq}}\n            </div>\n            <div class=\"light-notification-text-wrapper\" style=\"width: 100%\">\n              <div class=\"light-notification-title-row\">\n                <h3 style=\"color: #616161\" class=\"light-notification-title\">{{#ifNot custom.title}}Titel{{else}}{{custom.title}}{{/ifNot}}</h3>\n                <i class=\"fas fa-times\" style=\"width: 14px; height: 14px; color: #1565C0;\"></i>\n              </div>\n              <div class=\"light-message-body\">\n                <p class=\"notification-message\"><span style=\"color: #616161\">{{#ifNot custom.text}}Meddelande{{else}}{{custom.text}}{{/ifNot}}</span> <i class=\"fas fa-chevron-right\" style=\"width: 12px; height: 12px; color: #1565C0;\"></i></p>\n              </div>\n            </div>\n          </div>\n          <div class=\"light-notification-actions\"><div style=\"flex-grow: 1;\"></div></div>\n        </div>\n      </div>\n      {{else}}\n      <div style=\"background-color: #061629; padding: 16px; border-radius: 10px; display: flex; justify-content: center;\">\n        <div class=\"dark-mode-container\">\n          <div class=\"notification-content\">\n            <div class=\"notification-icon-wrapper\">\n              {{#ifEq custom.iconName \"envelope\"}}<i class=\"fas fa-envelope\" style=\"width: 14px; height: 14px; color: #cfd8dc;\"></i>{{/ifEq}}\n              {{#ifEq custom.iconName \"star\"}}<i class=\"fas fa-star\" style=\"width: 14px; height: 14px; color: #cfd8dc;\"></i>{{/ifEq}}\n              {{#ifEq custom.iconName \"magic\"}}<i class=\"fas fa-magic\" style=\"width: 14px; height: 14px; color: #cfd8dc;\"></i>{{/ifEq}}\n              {{#ifEq custom.iconName \"exclamation-triangle\"}}<i class=\"fas fa-exclamation-triangle\" style=\"width: 14px; height: 14px; color: #cfd8dc;\"></i>{{/ifEq}}\n            </div>\n            <div class=\"notification-text-wrapper\" style=\"width: 100%\">\n              <div class=\"notification-title-row\">\n                <h3 class=\"notification-title\" style=\"color: #b0bec5\">{{#ifNot custom.title}}Titel{{else}}{{custom.title}}{{/ifNot}}</h3>\n                <i class=\"fas fa-times\" style=\"width: 14px; height: 14px; color: #cfd8dc;\"></i>\n              </div>\n              <div class=\"dark-message-body\">\n                <p class=\"notification-message\"><span style=\"color: #b0bec5;\">{{#ifNot custom.text}}Meddelande{{else}}{{custom.text}}{{/ifNot}}</span> <i class=\"fas fa-chevron-right\" style=\"width: 12px; height: 12px; color: #cfd8dc;\"></i></p>\n              </div>\n            </div>\n          </div>\n          <div class=\"notification-actions\"><div style=\"flex-grow: 1;\"></div></div>\n        </div>\n      </div>\n      {{/ifEq}}\n    </div>\n\n    <div style=\"height: 1px; background: var(--border); margin: 0 24px;\"></div>\n\n    <div style=\"padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;\">\n      <div style=\"color: var(--text-primary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;\">Skicka</div>\n      <Field label=\"Tidpunkt\" onChange=\"window.form.setState({sendAt: value})\" value=\"{{custom.sendAt}}\" containerStyle=\"width: 100%;\">\n        <option value=\"now\">Nu</option>\n        <option value=\"custom\">Annan tid och datum</option>\n      </Field>\n      {{#ifEq custom.sendAt \"custom\"}}\n      <input type=\"datetime-local\" onChange=\"window.form.setState({customDate: event.target.value})\" value=\"{{custom.customDate}}\"\n        style=\"background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font-size: 13px; outline: none; width: 100%; box-sizing: border-box;\"/>\n      {{/ifEq}}\n      <PrimaryButton onClick=\"window.sendNotification()\">Skicka</PrimaryButton>\n    </div>\n\n  </div>\n</div>\n\n{{else}}\n<div style=\"max-width: 560px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">\n  <div style=\"background: var(--bg-card); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px 24px;\">\n    <h2 style=\"color: var(--text-primary); font-size: 16px; font-weight: 600; margin: 0;\">\n      Notifikationen har {{#ifEq custom.sendAt \"now\"}}skickats!{{else}}registrerats. Den kommer att skickas {{formatDate custom.customDate \"YYYY-MM-DD HH:mm\"}}{{/ifEq}}\n    </h2>\n  </div>\n</div>\n{{/ifNot}}\n\n<script>\n  const isDarkModeEnabled = window.sessionStorage.getItem(\"isDarkModePreferred\") === \"true\";\n  if (!isDarkModeEnabled) {\n    document.documentElement.classList.add('light');\n  }\n\n  (function () {\n    if (!window.form.custom?.token) {\n      getSalesysAccessToken();\n    }\n\n    if (window.form.custom?.recipientType === undefined) {\n      window.form.setState({\n        recipientType: \"user\"\n      })\n    }\n\n    if (window.form.custom?.showConfetti === undefined) {\n      window.form.setState({\n        showConfetti: false\n      })\n    }\n\n    if (window.form.custom?.previewMode === undefined) {\n      window.form.setState({\n        previewMode: \"light\"\n      })\n    }\n\n    if (window.form.custom?.text === undefined) {\n      window.form.setState({\n        text: \"\"\n      })\n    }\n\n    if (window.form.custom?.title === undefined) {\n      window.form.setState({\n        title: \"\"\n      })\n    }\n\n    if (window.form.custom?.iconName === undefined) {\n      window.form.setState({\n        iconName: \"envelope\"\n      })\n    }\n\n    if (window.form.custom?.recipients === undefined) {\n      window.form.setState({\n        recipients: []\n      })\n    }\n\n    if (!window.form.custom?.sendAt) {\n      window.form.setState({\n        sendAt: \"now\"\n      })\n    }\n  }\n  )();\n\n    window.handleRecipientChange = (id, label) => {\n      let recipients = window.form.custom.recipients || [];\n      let recipientLabels = window.form.custom.recipientLabels || [];\n\n      if (!recipients.includes(id)) {\n        recipients.push(id);\n        recipientLabels.push(label);\n      } else {\n        recipients = recipients.filter(recipient => recipient !== id);\n        recipientLabels = recipientLabels.filter(recipientLabel => recipientLabel !== label);\n      }\n\n      window.form.setState({\n        recipients,\n        recipientLabels,\n      });\n    }\n\n    window.sendNotification = () => {\n      const { recipientType, recipients, showConfetti, text, linkText, title, iconName, sendAt, customDate } = window.form.custom;\n\n      if (!title) {\n        alert(\"Vänligen fyll i en titel.\")\n        return;\n      }\n\n      if (!text) {\n        alert(\"Vänligen fyll i ett meddelande.\")\n        return;\n      }\n\n      if (!recipients?.length) {\n        alert(\"Vänligen välj en eller flera mottagare.\")\n        return;\n      }\n\n      const body = {\n        recipientType,\n        recipients,\n        showConfetti,\n        text,\n        link: linkText,\n        title,\n        iconName,\n      };\n\n      console.log(body)\n\n      if (sendAt && sendAt !== \"now\" && customDate) {\n        body[\"startAt\"] = new Date(customDate).toISOString();\n      }\n\n      fetch(\"https://salesys.se/api/views/send_notification_v2.php\", {\n        method: \"POST\",\n        body: JSON.stringify(body),\n        headers: {\n          \"Content-type\": \"application/json\",\n          \"Authorization\": `Bearer ${window.form.custom.token}`,\n        }\n      })\n        .then(data => {\n        if (data) {\n          window.form.setState({sentNotification: true})\n        }\n      })\n        .catch(error => {\n          console.error(\"Failed to send notification: \", error)\n        })\n    }\n\n    window.form.addHook(\"handleUserSearch\", () => {\n      if (!window.form.custom?.allUsers) {\n        return;\n      }\n\n      const searchQuery = window.form.custom?.searchQuery?.toLowerCase();\n      let users = [];\n      if (!searchQuery) {\n        users = window.form.custom.allUsers?.slice(0, 4);\n      } else {\n        users = window.form.custom.allUsers.filter(user => user.fullName.toLowerCase().includes(searchQuery) || user.id.toLowerCase().includes(searchQuery) || user.username.toLowerCase().includes(searchQuery));\n      }\n      window.form.setState({\n        users\n      });\n    });\n\n    window.form.addHook(\"handleUpdateLink\", () => {\n      const link = window.form.custom?.link;\n\n      if (!link) {\n        window.form.setState({ linkText: null })\n        return;\n      }\n\n      if (!link.startsWith(\"https://\") && !link.startsWith(\"http://\")) {\n        window.form.setState({\n          linkText: \"https://\" + link\n        });\n      } else {\n        window.form.setState({\n          linkText: link\n        });\n      }\n    });\n\n    window.form.addHook(\"updateRecipientsText\", () => {\n      let text = \"Notifikationen kommer att skickas till:\";\n      const recipientType = window.form.custom?.recipientType;\n      const recipientLabels = window.form.custom?.recipientLabels || [];\n      let list = [];\n      let prefix = \"\";\n\n      if (!recipientLabels?.length) {\n        text = \"Notifikationen har inga mottagare.\";\n        window.form.setState({\n          recipientsText: text,\n          recipientsList: list\n        });\n        return;\n      }\n\n      if (recipientType === \"user\") {\n        prefix = \"användaren \"\n      } else if (recipientType === \"userTeam\") {\n        prefix = \"teamet \"\n      }\n\n      list.push(...recipientLabels);\n\n      window.form.setState({\n        recipientsText: text,\n        recipientsList: list,\n        prefix\n      });\n    });\n\n    window.form.addHook(\"getLoggedInUser\", () => {\n      if (!window.form.custom?.token || window.form.custom?.hasSetUsers || !window.form.custom?.userId) {\n        return;\n      }\n\n      getLoggedInUser().then(loggedInUser => {\n        let allUsers, allTeams;\n        \n        if (loggedInUser.type === \"admin\") {\n          allUsers = window.form.resources.users.filter(user => !user.suspension);\n          allTeams = window.form.resources.teams;\n        } else {\n          allUsers = window.form.resources.users.filter(user => loggedInUser.teamIds.includes(user.teamId) && !user.suspension);\n          allTeams = window.form.resources.teams.filter(team => loggedInUser.teamIds.includes(team.id));\n        }\n\n        window.form.setState({\n          users: allUsers?.slice(0, 4),\n          allUsers,\n          allTeams,\n          hasSetUsers: true\n        });\n      });\n    })\n\n    async function getLoggedInUser() {\n      const res = await fetch(`https://app.salesys.se/api/users/users-v1/${window.form.custom.userId}`, {\n        method: \"GET\",\n        headers: {\n          \"Content-type\": \"application/json\",\n          Authorization: `Bearer ${window.form.custom.token}`,\n        },\n      });\n      const data = await res.json();\n\n      const allTeamIds = [data.teamId, ...data.supervisedTeamIds];\n\n      const user = {\n        id: data.id,\n        username: data.username,\n        teamIds: allTeamIds,\n        type: data.type\n      };\n\n      return user;\n    }\n\n    function getSalesysAccessToken() {\n      const cookies = document.cookie.split(';').reduce((object, cookie) => {\n        const regex = /(.+)=(.+)/;\n        const parts = regex.exec(cookie.trim());\n        if (!parts || !parts.length) {\n          console.warn(\"Unrecognized cookie %s\", cookie, regex);\n          return object;\n        }\n\n        object[parts[1]] = parts[2];\n        return object;\n      }, {});\n\n      window.form.setState({ token: cookies[\"s2_utoken\"], userId: cookies[\"s2_uid\"] });\n    }\n\n</script>\n\n<style>\n:root {\n  --bg-card: #1b1c2f;\n  --bg-input: #252638;\n  --border: #3a3b58;\n  --card-border: transparent;\n  --text-primary: #e2e3f0;\n  --text-secondary: #c8c9e0;\n  --text-muted: #6668a0;\n  --accent: #4f5bd5;\n}\n:root.light {\n  --bg-card: #ffffff;\n  --bg-input: #f3f4f6;\n  --border: #e5e7eb;\n  --card-border: #e5e7eb;\n  --text-primary: #111827;\n  --text-secondary: #374151;\n  --text-muted: #6b7280;\n  --accent: #4f5bd5;\n}\n.field label {\n  color: var(--text-secondary);\n}\n</style>","iconName":"exclamation","iconColor":null,"settingsHtml":"","description":"Skicka en notifikation till en eller flera säljare. Det är också möjligt att skicka en notifikation till ett helt team.","rights":[],"components":[]},{"id":"6a482307f6ce326089155fc1","name":"AI-assistent","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","label":"AI-assistent"}],"html":"<div id=\"s2-agent-chat\"></div>\n<script src=\"https://s2-agent-t46he.sevalla.app/widget.js\"></script>","iconName":"comments","settingsHtml":"<div>AI-assistenten kräver inga inställningar. Lägg till vyn i navigationen så kan användarna börja chatta — assistenten använder alltid den inloggade användarens behörigheter och varje ändring måste godkännas i chatten.</div>","description":"","rights":[],"components":[]},{"id":"6a48f804643c247ba6dd57b1","name":"Konversationer","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["69a3068e4e630d4dc2855cd3"],"locations":[{"type":"navigation","position":0,"label":"Konversationer"}],"html":"<div id=\"ksv-root\">\n  <style>\n    #ksv-root {\n      --ksv-bg: var(--color-background-2, #fafafa);\n      --ksv-card: var(--color-background-1, #ffffff);\n      --ksv-secondary: var(--color-background-3, #eeeeee);\n      --ksv-fg: var(--color-text-1, #212121);\n      --ksv-muted-fg: var(--color-text-3, #757575);\n      --ksv-primary: var(--color-primary, #1565C0);\n      --ksv-primary-fg: #ffffff;\n      --ksv-danger: var(--color-danger, #E53935);\n      --ksv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --ksv-border: color-mix(in srgb, var(--ksv-fg) 13%, transparent);\n      --ksv-border-soft: color-mix(in srgb, var(--ksv-fg) 8%, transparent);\n      --ksv-unread-tint: color-mix(in srgb, var(--ksv-primary) 7%, transparent);\n      --ksv-badge-bg: color-mix(in srgb, var(--ksv-primary) 12%, transparent);\n      --ksv-badge-fg: color-mix(in srgb, var(--ksv-primary) 65%, var(--ksv-fg));\n      --ksv-unread: #1EAEDB;\n      --ksv-radius: 8px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--ksv-fg);\n      display: block;\n      background: var(--ksv-bg);\n      border: 1px solid var(--ksv-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #ksv-root * { box-sizing: border-box; }\n    #ksv-root button { font-family: inherit; cursor: pointer; }\n    #ksv-root input, #ksv-root textarea, #ksv-root select { font-family: inherit; font-size: 14px; color: var(--ksv-fg); }\n\n    .ksv-app { display: flex; height: 100%; }\n\n    /* ---------- Left panel ---------- */\n    .ksv-list-panel {\n      width: 340px; min-width: 280px; flex-shrink: 0;\n      display: flex; flex-direction: column;\n      border-right: 1px solid var(--ksv-border-soft);\n      background: var(--ksv-card);\n    }\n    .ksv-list-header { padding: 12px 16px; border-bottom: 1px solid var(--ksv-border-soft); }\n    .ksv-icon-btn {\n      background: transparent; border: none; border-radius: 6px;\n      width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;\n      color: var(--ksv-muted-fg);\n    }\n    .ksv-icon-btn:hover { background: var(--ksv-hover); color: var(--ksv-fg); }\n    .ksv-icon-btn:disabled { opacity: 0.5; cursor: default; }\n    .ksv-search-wrap { position: relative; }\n    .ksv-search-wrap svg.ksv-search-icon {\n      position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n      width: 14px; height: 14px; color: var(--ksv-muted-fg); pointer-events: none;\n    }\n    .ksv-search-input {\n      width: 100%; padding: 8px 36px 8px 32px;\n      border: 1px solid var(--ksv-border); border-radius: var(--ksv-radius);\n      outline: none; background: var(--ksv-bg);\n    }\n    .ksv-search-input::placeholder { color: var(--ksv-muted-fg); }\n    .ksv-search-input:focus { border-color: var(--ksv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ksv-primary) 18%, transparent); }\n    .ksv-filter-toggle { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); }\n    .ksv-filters { padding: 12px 16px; border-bottom: 1px solid var(--ksv-border-soft); background: var(--ksv-bg); display: none; }\n    .ksv-filters.ksv-open { display: block; }\n    .ksv-filters label.ksv-filter-label { font-size: 13px; font-weight: 600; display: block; margin-bottom: 6px; }\n    .ksv-filters select {\n      width: 100%; padding: 7px 10px; border: 1px solid var(--ksv-border);\n      border-radius: var(--ksv-radius); background: var(--ksv-card); outline: none;\n    }\n    .ksv-filter-check { display: flex; align-items: center; gap: 7px; margin-top: 10px; font-size: 13px; user-select: none; cursor: pointer; }\n\n    .ksv-conversations { flex: 1; overflow-y: auto; }\n    .ksv-conv {\n      padding: 12px 16px; border-bottom: 1px solid var(--ksv-border-soft);\n      cursor: pointer; transition: background-color 120ms ease;\n    }\n    .ksv-conv:hover { background: var(--ksv-hover); }\n    .ksv-conv.ksv-active { background: var(--ksv-secondary); }\n    .ksv-conv.ksv-unread { background: var(--ksv-unread-tint); }\n    .ksv-conv.ksv-unread:hover { background: color-mix(in srgb, var(--ksv-primary) 11%, transparent); }\n    .ksv-conv-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }\n    .ksv-conv-title { display: flex; align-items: center; gap: 7px; min-width: 0; }\n    .ksv-conv-name { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .ksv-conv.ksv-unread .ksv-conv-name { font-weight: 800; }\n    .ksv-dot {\n      width: 8px; height: 8px; border-radius: 50%; background: var(--ksv-unread);\n      flex-shrink: 0; animation: ksv-pulse 1.6s ease-in-out infinite;\n    }\n    @keyframes ksv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }\n    .ksv-conv-time { font-size: 11px; color: var(--ksv-muted-fg); white-space: nowrap; flex-shrink: 0; padding-top: 2px; }\n    .ksv-conv-sub { font-size: 11px; color: var(--ksv-muted-fg); margin-top: 1px; }\n    .ksv-conv-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; gap: 8px; }\n    .ksv-conv-preview {\n      font-size: 13px; color: var(--color-text-2, #616161);\n      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;\n    }\n    .ksv-conv.ksv-unread .ksv-conv-preview { color: var(--ksv-fg); font-weight: 600; }\n    .ksv-badge {\n      display: inline-flex; align-items: center; gap: 4px;\n      font-size: 11px; padding: 1px 8px; border-radius: 999px;\n      border: 1px solid var(--ksv-border-soft); background: var(--ksv-badge-bg);\n      color: var(--ksv-badge-fg); white-space: nowrap; max-width: 130px; overflow: hidden; text-overflow: ellipsis;\n    }\n    .ksv-checkcheck { color: var(--ksv-muted-fg); width: 15px; height: 15px; flex-shrink: 0; }\n    .ksv-empty-list { padding: 42px 20px; text-align: center; color: var(--ksv-muted-fg); }\n    .ksv-empty-list svg { width: 42px; height: 42px; margin-bottom: 10px; opacity: 0.5; }\n    .ksv-empty-list h3 { margin: 0 0 4px; font-size: 15px; color: var(--ksv-fg); }\n    .ksv-empty-list p { margin: 0; font-size: 13px; }\n    .ksv-loading { padding: 20px 16px; color: var(--ksv-muted-fg); font-size: 13px; display: flex; align-items: center; gap: 8px; }\n    .ksv-error-box { padding: 14px 16px; color: var(--ksv-danger); font-size: 13px; word-break: break-word; }\n\n    /* ---------- Thread panel ---------- */\n    .ksv-thread-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: var(--ksv-bg); }\n    .ksv-thread-header {\n      padding: 12px 16px; border-bottom: 1px solid var(--ksv-border-soft);\n      display: flex; align-items: center; justify-content: space-between; gap: 10px;\n      background: var(--ksv-card);\n    }\n    .ksv-thread-header-left { display: flex; align-items: center; gap: 11px; min-width: 0; }\n    .ksv-thread-title { min-width: 0; }\n    .ksv-thread-title h2 { margin: 0; font-size: 15px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .ksv-thread-title p { margin: 0; font-size: 12px; color: var(--ksv-muted-fg); }\n    .ksv-thread-header-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }\n\n    .ksv-messages { flex: 1; overflow-y: auto; padding: 16px; }\n    .ksv-date-sep { display: flex; justify-content: center; margin: 14px 0 10px; }\n    .ksv-date-sep span {\n      font-size: 11px; color: var(--ksv-muted-fg); background: var(--ksv-card);\n      border: 1px solid var(--ksv-border-soft); padding: 2px 10px; border-radius: 999px;\n    }\n    .ksv-msg-row { display: flex; margin-bottom: 10px; }\n    .ksv-msg-row.ksv-out { justify-content: flex-end; }\n    .ksv-msg { max-width: 74%; }\n    .ksv-bubble {\n      padding: 9px 13px; border-radius: 16px; font-size: 14px; line-height: 1.45;\n      white-space: pre-wrap; word-break: break-word;\n    }\n    .ksv-msg-row.ksv-in .ksv-bubble {\n      background: var(--ksv-card); border: 1px solid var(--ksv-border-soft);\n      border-bottom-left-radius: 5px; color: var(--ksv-fg);\n    }\n    .ksv-msg-row.ksv-out .ksv-bubble {\n      background: var(--ksv-primary); color: var(--ksv-primary-fg);\n      border-bottom-right-radius: 5px;\n    }\n    .ksv-msg-meta { font-size: 11px; color: var(--ksv-muted-fg); margin-top: 3px; display: flex; gap: 5px; align-items: center; }\n    .ksv-msg-row.ksv-out .ksv-msg-meta { justify-content: flex-end; }\n    .ksv-msg-meta .ksv-error { color: var(--ksv-danger); font-weight: 600; }\n    .ksv-msg-meta .ksv-delivered { color: var(--color-good, #43A047); display: inline-flex; align-items: center; }\n\n    .ksv-composer { padding: 12px 16px; border-top: 1px solid var(--ksv-border-soft); background: var(--ksv-card); }\n    .ksv-composer-row { display: flex; gap: 9px; align-items: flex-end; }\n    .ksv-composer textarea {\n      flex: 1; resize: none; min-height: 40px; max-height: 130px;\n      border: 1px solid var(--ksv-border); border-radius: var(--ksv-radius);\n      padding: 9px 12px; outline: none; background: var(--ksv-bg); line-height: 1.4;\n    }\n    .ksv-composer textarea::placeholder { color: var(--ksv-muted-fg); }\n    .ksv-composer textarea:focus { border-color: var(--ksv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ksv-primary) 18%, transparent); }\n    .ksv-send-btn {\n      background: var(--ksv-primary); color: var(--ksv-primary-fg); border: none;\n      border-radius: var(--ksv-radius); width: 40px; height: 40px; flex-shrink: 0;\n      display: inline-flex; align-items: center; justify-content: center;\n    }\n    .ksv-send-btn i { font-size: 15px; }\n    .ksv-settings-hint { font-size: 12px; color: var(--ksv-muted-fg); margin: 6px 0 0 26px; }\n    .ksv-send-btn:hover { background: color-mix(in srgb, var(--ksv-primary) 88%, #000); }\n    .ksv-send-btn:disabled { opacity: 0.5; cursor: default; }\n    .ksv-composer-hint { font-size: 11px; color: var(--ksv-muted-fg); margin-top: 5px; }\n\n    .ksv-thread-empty {\n      flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;\n      color: var(--ksv-muted-fg); text-align: center; padding: 24px;\n    }\n    .ksv-thread-empty .ksv-empty-circle {\n      background: var(--ksv-secondary); border-radius: 50%; padding: 22px; margin-bottom: 14px;\n    }\n    .ksv-thread-empty svg { width: 40px; height: 40px; color: var(--ksv-primary); display: block; }\n    .ksv-thread-empty h3 { margin: 0 0 4px; font-size: 16px; color: var(--ksv-fg); }\n    .ksv-thread-empty p { margin: 0; font-size: 13px; max-width: 340px; }\n\n    .ksv-spin { animation: ksv-rotate 0.9s linear infinite; }\n    @keyframes ksv-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n\n    .ksv-conversations::-webkit-scrollbar, .ksv-messages::-webkit-scrollbar { width: 8px; }\n    .ksv-conversations::-webkit-scrollbar-thumb, .ksv-messages::-webkit-scrollbar-thumb {\n      background: color-mix(in srgb, var(--ksv-fg) 20%, transparent); border-radius: 4px;\n    }\n    .ksv-conversations::-webkit-scrollbar-track, .ksv-messages::-webkit-scrollbar-track { background: transparent; }\n\n    @media (max-width: 760px) {\n      .ksv-list-panel { width: 100%; }\n      .ksv-app.ksv-mobile-thread .ksv-list-panel { display: none; }\n      .ksv-app:not(.ksv-mobile-thread) .ksv-thread-panel { display: none; }\n      .ksv-back-btn { display: inline-flex !important; }\n    }\n    .ksv-back-btn { display: none; }\n  </style>\n\n  <div class=\"ksv-app\" id=\"ksv-app\">\n    <aside class=\"ksv-list-panel\">\n      <div class=\"ksv-list-header\">\n        <div style=\"display:flex;align-items:center;gap:4px;\">\n          <div class=\"ksv-search-wrap\" style=\"flex:1;\">\n            <svg class=\"ksv-search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line></svg>\n            <input type=\"search\" class=\"ksv-search-input\" id=\"ksv-search\" placeholder=\"Sök konversationer...\" autocomplete=\"off\">\n            <button class=\"ksv-icon-btn ksv-filter-toggle\" id=\"ksv-filter-btn\" title=\"Filter\">\n              <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"></polygon></svg>\n            </button>\n          </div>\n          <button class=\"ksv-icon-btn\" id=\"ksv-settings-btn\" title=\"Fler inställningar\">\n            <i class=\"fas fa-cog\" style=\"font-size:14px;\"></i>\n          </button>\n          <button class=\"ksv-icon-btn\" id=\"ksv-refresh-btn\" title=\"Uppdatera\">\n            <svg id=\"ksv-refresh-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-2.64-6.36\"></path><polyline points=\"21 3 21 9 15 9\"></polyline></svg>\n          </button>\n        </div>\n      </div>\n      <div class=\"ksv-filters\" id=\"ksv-filters\">\n        <label class=\"ksv-filter-label\">Filtrera efter användare</label>\n        <select id=\"ksv-user-filter\">\n          <option value=\"all\">Alla användare</option>\n        </select>\n        <label class=\"ksv-filter-check\">\n          <input type=\"checkbox\" id=\"ksv-unread-filter\"> Visa endast olästa\n        </label>\n      </div>\n      <div class=\"ksv-filters\" id=\"ksv-settings-panel\">\n        <label class=\"ksv-filter-check\" style=\"margin-top:0;\">\n          <input type=\"checkbox\" id=\"ksv-enrich-toggle\"> Hämta namn från senast importerade kontakten\n        </label>\n        <div class=\"ksv-settings-hint\">Slår upp numret bland kontakter och visar namnet från den senaste importen.</div>\n      </div>\n      <div class=\"ksv-conversations\" id=\"ksv-conversations\">\n        <div class=\"ksv-loading\">\n          <svg class=\"ksv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg>\n          Hämtar konversationer...\n        </div>\n      </div>\n    </aside>\n\n    <main class=\"ksv-thread-panel\">\n      <div class=\"ksv-thread-empty\" id=\"ksv-thread-empty\">\n        <div class=\"ksv-empty-circle\">\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"></path></svg>\n        </div>\n        <h3>Följ och svara på SMS-konversationer</h3>\n        <p>Här samlas alla SMS-konversationer mellan er och era kontakter &mdash; både utskick och inkommande svar. Välj en konversation i listan för att läsa hela tråden och svara direkt.</p>\n      </div>\n\n      <div id=\"ksv-thread\" style=\"display:none; flex:1; flex-direction:column; min-height:0;\">\n        <div class=\"ksv-thread-header\">\n          <div class=\"ksv-thread-header-left\">\n            <button class=\"ksv-icon-btn ksv-back-btn\" id=\"ksv-back-btn\" title=\"Tillbaka\">\n              <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 18 9 12 15 6\"></polyline></svg>\n            </button>\n            <div class=\"ksv-thread-title\">\n              <h2 id=\"ksv-thread-name\"></h2>\n              <p id=\"ksv-thread-phone\"></p>\n            </div>\n          </div>\n          <div class=\"ksv-thread-header-right\">\n            <span class=\"ksv-badge\" id=\"ksv-thread-seller\" style=\"display:none;\">\n              <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"></path><circle cx=\"12\" cy=\"7\" r=\"4\"></circle></svg>\n              <span id=\"ksv-thread-seller-name\"></span>\n            </span>\n            <button class=\"ksv-icon-btn\" id=\"ksv-thread-refresh\" title=\"Uppdatera konversation\">\n              <svg id=\"ksv-thread-refresh-icon\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-2.64-6.36\"></path><polyline points=\"21 3 21 9 15 9\"></polyline></svg>\n            </button>\n          </div>\n        </div>\n        <div class=\"ksv-messages\" id=\"ksv-messages\"></div>\n        <div class=\"ksv-composer\" id=\"ksv-composer\">\n          <div class=\"ksv-composer-row\">\n            <textarea id=\"ksv-input\" rows=\"1\" placeholder=\"Skriv ett SMS...\"></textarea>\n            <button class=\"ksv-send-btn\" id=\"ksv-send-btn\" title=\"Skicka\">\n              <i class=\"fas fa-paper-plane\"></i>\n            </button>\n          </div>\n          <div class=\"ksv-composer-hint\" id=\"ksv-composer-hint\">Enter för att skicka, Shift+Enter för ny rad</div>\n        </div>\n      </div>\n    </main>\n  </div>\n\n  <script>\n  (function () {\n    var API = 'https://app.salesys.se/api';\n    var POLL_MS = 10000;\n    var PAGE_SIZE = 30;\n\n    function el(id) { return document.getElementById(id); }\n\n    function esc(str) {\n      return String(str == null ? '' : str)\n        .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n        .replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n    }\n\n    function showFatal(message) {\n      var container = el('ksv-conversations');\n      if (container) {\n        container.innerHTML = '<div class=\"ksv-error-box\">Vyn kunde inte starta: ' + esc(message) + '</div>';\n      }\n    }\n\n    /* Marker so a stuck screen tells us whether the script executed at all. */\n    try {\n      var marker = el('ksv-conversations');\n      if (marker) {\n        marker.innerHTML = '<div class=\"ksv-loading\"><svg class=\"ksv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Initierar vy...</div>';\n      }\n    } catch (markerError) { /* ignore */ }\n\n    /* Clean up any previous mount of this view (the app can re-render sections). */\n    if (window.__ksvCleanup) {\n      try { window.__ksvCleanup(); } catch (cleanupError) { /* ignore */ }\n      window.__ksvCleanup = null;\n    }\n\n    /* ---------- Auth ---------- */\n    function getToken() {\n      var cookies = document.cookie.split(';').reduce(function (object, cookie) {\n        var parts = /(.+?)=(.+)/.exec(cookie.trim());\n        if (parts) object[parts[1]] = parts[2];\n        return object;\n      }, {});\n      var raw = cookies['s2_utoken'];\n      if (!raw) return null;\n      try { return decodeURIComponent(raw); } catch (decodeError) { return raw; }\n    }\n\n    function api(path, params) {\n      var qs = '';\n      if (params) {\n        var pairs = [];\n        Object.keys(params).forEach(function (key) {\n          if (params[key] === undefined || params[key] === null) return;\n          pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));\n        });\n        if (pairs.length) qs = '?' + pairs.join('&');\n      }\n      return fetch(API + path + qs, {\n        headers: { 'Authorization': 'Bearer ' + getToken() }\n      }).then(function (res) {\n        if (!res.ok) throw new Error('API ' + res.status + ' ' + path);\n        return res.json();\n      });\n    }\n\n    function apiPost(path, body) {\n      return fetch(API + path, {\n        method: 'POST',\n        headers: {\n          'Authorization': 'Bearer ' + getToken(),\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(body)\n      }).then(function (res) {\n        if (!res.ok) throw new Error('API ' + res.status + ' ' + path);\n        return res.text().then(function (t) { try { return JSON.parse(t); } catch (parseError) { return null; } });\n      });\n    }\n\n    /* ---------- Helpers ---------- */\n    function normalizePhone(phone) {\n      if (!phone) return '';\n      var digits = String(phone).replace(/\\D/g, '');\n      if (digits.indexOf('46') === 0 && digits.length > 9) digits = '0' + digits.substring(2);\n      return digits;\n    }\n\n    function phoneKey(phone) {\n      var n = normalizePhone(phone);\n      return n.length >= 8 ? n.slice(-8) : n;\n    }\n\n    function formatPhone(phone) {\n      if (!phone) return '';\n      var p = String(phone);\n      if (p.indexOf('+46') === 0) p = '0' + p.substring(3);\n      if (/^07\\d{8}$/.test(p)) {\n        return p.substring(0, 3) + '-' + p.substring(3, 6) + ' ' + p.substring(6, 8) + ' ' + p.substring(8);\n      }\n      return p;\n    }\n\n    function sameDay(a, b) {\n      return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n    }\n\n    function fmtListDate(value) {\n      var d = new Date(value);\n      var now = new Date();\n      if (sameDay(d, now)) return d.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });\n      var yesterday = new Date(now.getTime() - 86400000);\n      if (sameDay(d, yesterday)) return 'Igår';\n      if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' });\n      return d.toLocaleDateString('sv-SE');\n    }\n\n    function fmtTime(value) {\n      return new Date(value).toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });\n    }\n\n    function fmtDateHeading(dateStr) {\n      return new Date(dateStr).toLocaleDateString('sv-SE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n    }\n\n    /* ---------- State ---------- */\n    function loadSettings() {\n      var s = { enrich: true };\n      try {\n        var saved = JSON.parse(localStorage.getItem('ksv-view-settings') || '{}');\n        if (typeof saved.enrich === 'boolean') s.enrich = saved.enrich;\n      } catch (e) { /* ignore */ }\n      return s;\n    }\n\n    function saveSettings() {\n      try { localStorage.setItem('ksv-view-settings', JSON.stringify(state.settings)); } catch (e) { /* ignore */ }\n    }\n\n    var state = {\n      conversations: [],\n      users: {},\n      contactNames: {},\n      lookedUp: {},\n      contactFieldsPromise: null,\n      settings: loadSettings(),\n      active: null,\n      messages: [],\n      sms: {},\n      sending: false,\n      loadingMore: false,\n      hasMore: true,\n      pollTimer: null\n    };\n\n    /* ---------- Data loading ---------- */\n    function loadUsers() {\n      return api('/users/users-v1').then(function (users) {\n        (users || []).forEach(function (u) {\n          state.users[u.id] = u.fullName || u.firstName || u.email || 'Okänd';\n        });\n        var select = el('ksv-user-filter');\n        if (!select) return;\n        Object.keys(state.users)\n          .sort(function (a, b) { return state.users[a].localeCompare(state.users[b], 'sv'); })\n          .forEach(function (id) {\n            var opt = document.createElement('option');\n            opt.value = id;\n            opt.textContent = state.users[id];\n            select.appendChild(opt);\n          });\n      }).catch(function () { /* users are only cosmetic */ });\n    }\n\n    function conversationKey(message) {\n      if (message.sessionId) return 's:' + message.sessionId;\n      var local = (message.local && (message.local.phoneNumber || message.local.smsSenderId)) || '';\n      var remote = (message.remote && message.remote.phoneNumber) || '';\n      return 'p:' + local + '|' + remote;\n    }\n\n    function isUnreadMessage(message) {\n      return !!message.isRemote && !message.localSeenAt;\n    }\n\n    function mapConversation(m) {\n      return {\n        key: conversationKey(m),\n        sessionId: m.sessionId || null,\n        localSms: (m.local && (m.local.phoneNumber || m.local.smsSenderId)) || null,\n        localPhone: (m.local && m.local.phoneNumber) || null,\n        remotePhone: (m.remote && m.remote.phoneNumber) || '',\n        remoteName: (m.remote && m.remote.name) || '',\n        latest: m,\n        unread: isUnreadMessage(m),\n        userId: m.userId || null\n      };\n    }\n\n    /* Reloads the window of conversations loaded so far (at least one page). */\n    function loadConversations() {\n      var count = Math.max(PAGE_SIZE, state.conversations.length);\n      return api('/users/chat-messages-v1', {\n        isLatest: true,\n        sortOrder: 'desc',\n        includeNonSms: true,\n        offset: 0,\n        count: count\n      }).then(function (messages) {\n        messages = messages || [];\n        state.conversations = messages.map(mapConversation);\n        state.hasMore = messages.length >= count;\n        renderList();\n        lookupContactNames();\n      });\n    }\n\n    function loadMoreConversations() {\n      if (state.loadingMore || !state.hasMore) return;\n      state.loadingMore = true;\n      renderList();\n      api('/users/chat-messages-v1', {\n        isLatest: true,\n        sortOrder: 'desc',\n        includeNonSms: true,\n        offset: state.conversations.length,\n        count: PAGE_SIZE\n      }).then(function (messages) {\n        messages = messages || [];\n        state.hasMore = messages.length >= PAGE_SIZE;\n        var existing = {};\n        state.conversations.forEach(function (c) { existing[c.key] = true; });\n        var added = 0;\n        messages.forEach(function (m) {\n          var c = mapConversation(m);\n          if (!existing[c.key]) { state.conversations.push(c); existing[c.key] = true; added++; }\n        });\n        if (!added) state.hasMore = false;\n        state.loadingMore = false;\n        renderList();\n        lookupContactNames();\n      }).catch(function () {\n        state.loadingMore = false;\n        renderList();\n      });\n    }\n\n    /* ---------- Contact name enrichment ---------- */\n    /* Contact fields have no semantic types — the app matches them by label\n       (see getContactTitleFromFields in the main frontend). We do the same. */\n    var FIRST_LABELS = ['förnamn', 'first name', 'firstname'];\n    var LAST_LABELS = ['efternamn', 'last name', 'lastname', 'surname'];\n    var FULL_LABELS = ['namn', 'fullständigt namn', 'full name', 'fullname', 'name'];\n\n    function loadContactNameFields() {\n      if (state.contactFieldsPromise) return state.contactFieldsPromise;\n      state.contactFieldsPromise = api('/contacts/fields-v1').then(function (result) {\n        var ids = { first: [], last: [], full: [] };\n        ((result && result.fields) || []).forEach(function (f) {\n          var label = String(f.label || '').toLowerCase().trim();\n          if (FIRST_LABELS.indexOf(label) !== -1) ids.first.push(f.id);\n          else if (LAST_LABELS.indexOf(label) !== -1) ids.last.push(f.id);\n          else if (FULL_LABELS.indexOf(label) !== -1) ids.full.push(f.id);\n        });\n        return ids;\n      });\n      return state.contactFieldsPromise;\n    }\n\n    function fieldValue(contact, fieldIds) {\n      for (var i = 0; i < fieldIds.length; i++) {\n        for (var j = 0; j < (contact.fields || []).length; j++) {\n          var f = contact.fields[j];\n          if (f.fieldId === fieldIds[i] && f.value) return f.value;\n        }\n      }\n      return '';\n    }\n\n    function contactName(contact, ids) {\n      var first = fieldValue(contact, ids.first);\n      var last = fieldValue(contact, ids.last);\n      if (first && last) return first + ' ' + last;\n      return fieldValue(contact, ids.full) || first || '';\n    }\n\n    function contactPhoneKeys(contact) {\n      var keys = [];\n      (contact.fields || []).forEach(function (f) {\n        if (!f.value) return;\n        var digits = String(f.value).replace(/\\D/g, '');\n        if (digits.length < 8 || digits.length > 15) return;\n        var key = phoneKey(f.value);\n        if (key.length >= 7 && keys.indexOf(key) === -1) keys.push(key);\n      });\n      return keys;\n    }\n\n    function contactTimestamp(contact) {\n      return new Date(contact.importDate || contact.date || 0).getTime() || 0;\n    }\n\n    function lookupContactNames() {\n      if (!state.settings.enrich) return;\n      var pending = [];\n      state.conversations.forEach(function (c) {\n        if (!c.remotePhone) return;\n        var key = phoneKey(c.remotePhone);\n        if (!key || state.lookedUp[key]) return;\n        if (pending.indexOf(c.remotePhone) === -1) pending.push(c.remotePhone);\n      });\n      if (!pending.length) return;\n\n      loadContactNameFields().then(function (ids) {\n        var batches = [];\n        for (var i = 0; i < pending.length; i += 15) batches.push(pending.slice(i, i + 15));\n\n        var chain = Promise.resolve();\n        batches.forEach(function (batch) {\n          chain = chain.then(function () {\n            batch.forEach(function (num) { state.lookedUp[phoneKey(num)] = true; });\n            return api('/contacts/contacts-v1', {\n              phoneNumbers: batch.join(','),\n              offset: 0,\n              count: 100\n            }).then(function (contacts) {\n              (contacts || []).forEach(function (contact) {\n                var name = contactName(contact, ids);\n                if (!name) return;\n                var ts = contactTimestamp(contact);\n                contactPhoneKeys(contact).forEach(function (key) {\n                  var existing = state.contactNames[key];\n                  if (!existing || ts >= existing.ts) {\n                    state.contactNames[key] = { name: name, ts: ts };\n                  }\n                });\n              });\n            }).catch(function () { /* lookup is best-effort */ });\n          });\n        });\n\n        chain.then(function () {\n          renderList();\n          if (state.active) renderThreadHeader();\n        });\n      }).catch(function () { /* fields def missing */ });\n    }\n\n    /* ---------- Rendering: list ---------- */\n    function resolvedName(c) {\n      if (state.settings.enrich && c.remotePhone) {\n        var hit = state.contactNames[phoneKey(c.remotePhone)];\n        if (hit && hit.name) return hit.name;\n      }\n      return c.remoteName || '';\n    }\n\n    function displayName(c) {\n      return resolvedName(c) || formatPhone(c.remotePhone) || 'Okänd';\n    }\n\n    function filteredConversations() {\n      var query = el('ksv-search').value.trim().toLowerCase();\n      var userFilter = el('ksv-user-filter').value;\n      var unreadOnly = el('ksv-unread-filter').checked;\n\n      return state.conversations.filter(function (c) {\n        if (unreadOnly && !c.unread) return false;\n        if (userFilter !== 'all' && c.userId !== userFilter) return false;\n        if (query) {\n          var haystack = (resolvedName(c) + ' ' + c.remotePhone + ' ' + normalizePhone(c.remotePhone)).toLowerCase();\n          if (haystack.indexOf(query) === -1) return false;\n        }\n        return true;\n      });\n    }\n\n    function renderList() {\n      var container = el('ksv-conversations');\n      if (!container) return;\n      var items = filteredConversations();\n\n      if (!items.length) {\n        container.innerHTML =\n          '<div class=\"ksv-empty-list\">' +\n          '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z\"></path></svg>' +\n          '<h3>Inga konversationer hittades</h3>' +\n          '<p>Försök justera sökning eller filter</p>' +\n          '</div>';\n        return;\n      }\n\n      var html = '';\n      items.forEach(function (c) {\n        var classes = 'ksv-conv';\n        if (state.active && state.active.key === c.key) classes += ' ksv-active';\n        if (c.unread) classes += ' ksv-unread';\n\n        var sellerName = c.userId && state.users[c.userId];\n        var preview = (c.latest.text || '').replace(/\\s+/g, ' ');\n        if (preview.length > 64) preview = preview.substring(0, 64) + '...';\n\n        html += '<div class=\"' + classes + '\" data-key=\"' + esc(c.key) + '\">';\n        html += '<div class=\"ksv-conv-top\"><div class=\"ksv-conv-title\">';\n        html += '<span class=\"ksv-conv-name\">' + esc(displayName(c)) + '</span>';\n        if (c.unread) html += '<span class=\"ksv-dot\" title=\"Nytt meddelande\"></span>';\n        html += '</div>';\n        html += '<span class=\"ksv-conv-time\">' + esc(fmtListDate(c.latest.sentAt || c.latest.createdAt)) + '</span></div>';\n\n        var cname = resolvedName(c);\n        if (cname && c.remotePhone) {\n          html += '<div class=\"ksv-conv-sub\">' + esc(formatPhone(c.remotePhone)) + '</div>';\n        } else if (!cname) {\n          html += '<div class=\"ksv-conv-sub\">Okänd kontakt</div>';\n        }\n\n        html += '<div class=\"ksv-conv-bottom\">';\n        html += '<span class=\"ksv-conv-preview\">' + esc(preview || 'Inga meddelanden') + '</span>';\n        html += '<span style=\"display:inline-flex;align-items:center;gap:5px;flex-shrink:0;\">';\n        if (!c.latest.isRemote) {\n          html += '<svg class=\"ksv-checkcheck\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 7 17l-5-5\"></path><path d=\"m22 10-7.5 7.5L13 16\"></path></svg>';\n        }\n        if (sellerName) {\n          html += '<span class=\"ksv-badge\"><svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"></path><circle cx=\"12\" cy=\"7\" r=\"4\"></circle></svg>' + esc(sellerName) + '</span>';\n        }\n        html += '</span></div></div>';\n      });\n\n      if (state.loadingMore) {\n        html += '<div class=\"ksv-loading\"><svg class=\"ksv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Hämtar fler...</div>';\n      }\n\n      container.innerHTML = html;\n    }\n\n    /* ---------- Rendering: thread ---------- */\n    function renderThreadHeader() {\n      var c = state.active;\n      if (!c) return;\n      var cname = resolvedName(c);\n      el('ksv-thread-name').textContent = displayName(c);\n      el('ksv-thread-phone').textContent = cname ? formatPhone(c.remotePhone) : '';\n\n      var sellerId = null;\n      for (var i = state.messages.length - 1; i >= 0; i--) {\n        var m = state.messages[i];\n        if (!m.isRemote && m.userId) { sellerId = m.userId; break; }\n      }\n      var badge = el('ksv-thread-seller');\n      if (sellerId && state.users[sellerId]) {\n        el('ksv-thread-seller-name').textContent = state.users[sellerId];\n        badge.style.display = 'inline-flex';\n      } else {\n        badge.style.display = 'none';\n      }\n    }\n\n    function renderMessages(keepScroll) {\n      var container = el('ksv-messages');\n      if (!container) return;\n      var wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 60;\n\n      var sorted = state.messages.slice().sort(function (a, b) {\n        return new Date(a.sentAt || a.createdAt) - new Date(b.sentAt || b.createdAt);\n      });\n\n      var html = '';\n      var lastDate = '';\n      sorted.forEach(function (m) {\n        var when = m.sentAt || m.createdAt;\n        var dateStr = new Date(when).toLocaleDateString('sv-SE');\n        if (dateStr !== lastDate) {\n          html += '<div class=\"ksv-date-sep\"><span>' + esc(fmtDateHeading(when)) + '</span></div>';\n          lastDate = dateStr;\n        }\n\n        var out = !m.isRemote;\n        html += '<div class=\"ksv-msg-row ' + (out ? 'ksv-out' : 'ksv-in') + '\"><div class=\"ksv-msg\">';\n        html += '<div class=\"ksv-bubble\">' + esc(m.text || '') + '</div>';\n        html += '<div class=\"ksv-msg-meta\">';\n        if (m.pending) {\n          html += '<span>Skickar...</span>';\n        } else if (m.error) {\n          html += '<span class=\"ksv-error\">Kunde inte skickas</span>';\n        } else {\n          html += '<span>' + esc(fmtTime(when)) + '</span>';\n          if (out) {\n            var who = m.userId && state.users[m.userId] ? state.users[m.userId] : '';\n            if (who) html += '<span>&middot; ' + esc(who) + '</span>';\n            var delivery = smsDeliveryState(m);\n            if (delivery === 'delivered') {\n              html += '<span class=\"ksv-delivered\" title=\"Levererat\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg></span>';\n            } else if (delivery === 'failed') {\n              html += '<span class=\"ksv-error\">&middot; Levererades inte</span>';\n            }\n          }\n        }\n        html += '</div></div></div>';\n      });\n\n      container.innerHTML = html;\n      if (!keepScroll || wasAtBottom) container.scrollTop = container.scrollHeight;\n    }\n\n    function threadQuery(c) {\n      var params = { sortOrder: 'asc', includeNonSms: true };\n      if (c.sessionId) {\n        params.sessionId = c.sessionId;\n      } else {\n        params.localSms = c.localSms;\n        params.remotePhoneNumber = c.remotePhone;\n      }\n      return params;\n    }\n\n    function openConversation(c) {\n      state.active = c;\n      state.messages = [];\n      el('ksv-thread-empty').style.display = 'none';\n      el('ksv-thread').style.display = 'flex';\n      el('ksv-app').classList.add('ksv-mobile-thread');\n      el('ksv-composer').style.display = c.remotePhone ? 'block' : 'none';\n      renderThreadHeader();\n      el('ksv-messages').innerHTML = '<div class=\"ksv-loading\"><svg class=\"ksv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Hämtar meddelanden...</div>';\n      renderList();\n      refreshThread().then(markThreadSeen);\n    }\n\n    function refreshThread() {\n      var c = state.active;\n      if (!c) return Promise.resolve();\n      return api('/users/chat-messages-v1', threadQuery(c)).then(function (messages) {\n        if (!state.active || state.active.key !== c.key) return;\n        state.messages = messages || [];\n        renderThreadHeader();\n        renderMessages(true);\n        refreshSmsStatus();\n      }).catch(function () {\n        var container = el('ksv-messages');\n        if (container) container.innerHTML = '<div class=\"ksv-loading\">Kunde inte hämta meddelanden.</div>';\n      });\n    }\n\n    /* Delivery status lives on the SMS record (dial/sms-v1), not the chat\n       message. Fetch statuses for outgoing SMS in the open thread; records\n       marked final never change so they are only fetched once. */\n    function refreshSmsStatus() {\n      var ids = [];\n      state.messages.forEach(function (m) {\n        if (m.isRemote || m.sessionId || !m.smsId) return;\n        var known = state.sms[m.smsId];\n        if (known && known.final) return;\n        if (ids.indexOf(m.smsId) === -1) ids.push(m.smsId);\n      });\n      if (!ids.length) return;\n      api('/dial/sms-v1', { ids: ids.join(',') }).then(function (list) {\n        (list || []).forEach(function (sms) { state.sms[sms.id] = sms; });\n        renderMessages(true);\n      }).catch(function () { /* non-critical: meta just omits status */ });\n    }\n\n    function smsDeliveryState(m) {\n      if (m.isRemote || m.sessionId || !m.smsId) return null;\n      var sms = state.sms[m.smsId];\n      if (!sms) return null;\n      if (sms.updatedAt) return sms.delivered ? 'delivered' : 'failed';\n      /* No status update within 10 s of sending counts as not delivered\n         (same rule as the app's chat widget). */\n      if (new Date(sms.sentAt) < new Date(Date.now() - 10000)) return 'failed';\n      return null;\n    }\n\n    function markThreadSeen() {\n      var c = state.active;\n      if (!c) return;\n      var unseenIds = state.messages\n        .filter(function (m) { return isUnreadMessage(m); })\n        .map(function (m) { return m.id; });\n      if (!unseenIds.length) {\n        if (c.unread) { c.unread = false; renderList(); }\n        return;\n      }\n      apiPost('/users/chat-messages-v1/see', {\n        ids: unseenIds,\n        seen: true,\n        notified: true\n      }).then(function () {\n        var now = new Date().toISOString();\n        state.messages.forEach(function (m) {\n          if (unseenIds.indexOf(m.id) !== -1) m.localSeenAt = now;\n        });\n        c.unread = false;\n        renderList();\n      }).catch(function () { /* non-critical */ });\n    }\n\n    /* ---------- Sending ---------- */\n    function sendMessage() {\n      var c = state.active;\n      var input = el('ksv-input');\n      var text = input.value.trim();\n      if (!c || !text || state.sending || !c.remotePhone) return;\n\n      state.sending = true;\n      el('ksv-send-btn').disabled = true;\n\n      var optimistic = {\n        id: 'tmp-' + Date.now(),\n        text: text,\n        isRemote: false,\n        sentAt: new Date().toISOString(),\n        pending: true\n      };\n      state.messages.push(optimistic);\n      renderMessages(false);\n      input.value = '';\n      input.style.height = 'auto';\n\n      var body = { to: c.remotePhone, text: text };\n      if (c.localPhone) body.from = c.localPhone;\n\n      apiPost('/dial/sms-v1', body).then(function () {\n        return refreshThread();\n      }).catch(function () {\n        optimistic.pending = false;\n        optimistic.error = true;\n        renderMessages(true);\n      }).then(function () {\n        state.sending = false;\n        el('ksv-send-btn').disabled = false;\n        input.focus();\n      });\n    }\n\n    /* ---------- Polling ---------- */\n    function poll() {\n      if (document.hidden) return;\n      if (!el('ksv-root')) return; /* view unmounted */\n      applyContrast();\n      loadConversations().catch(function () { /* retry next tick */ });\n      if (state.active) refreshThread().then(markThreadSeen);\n    }\n\n    /* ---------- Layout ---------- */\n    /* The app's dark theme can resolve --color-primary to a light color, so\n       white-on-primary text is not a safe assumption. Probe the resolved color\n       and pick a readable foreground. */\n    function applyContrast() {\n      var root = el('ksv-root');\n      if (!root) return;\n      try {\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;background-color:var(--ksv-primary);';\n        root.appendChild(probe);\n        var rgb = /rgba?\\((\\d+)[,\\s]+(\\d+)[,\\s]+(\\d+)/.exec(getComputedStyle(probe).backgroundColor);\n        root.removeChild(probe);\n        if (!rgb) return;\n        var luminance = (0.299 * rgb[1] + 0.587 * rgb[2] + 0.114 * rgb[3]) / 255;\n        root.style.setProperty('--ksv-primary-fg', luminance > 0.6 ? '#1b2430' : '#ffffff');\n      } catch (e) { /* keep default */ }\n    }\n\n    function fitHeight() {\n      var root = el('ksv-root');\n      if (!root) return;\n      var top = root.getBoundingClientRect().top;\n      var height = window.innerHeight - top - 24;\n      if (height < 460) height = 460;\n      root.style.height = height + 'px';\n    }\n\n    /* ---------- Events ----------\n       The app can re-render the view's DOM after this script has run, which\n       detaches any listeners bound directly to elements. All wiring therefore\n       uses delegated listeners on `document`, matched by id at event time. */\n    function within(target, id) {\n      var node = target;\n      while (node && node !== document) {\n        if (node.id === id) return node;\n        node = node.parentNode;\n      }\n      return null;\n    }\n\n    function toggleSettingsPanel() {\n      var panel = el('ksv-settings-panel');\n      if (!panel) return;\n      var toggle = el('ksv-enrich-toggle');\n      if (toggle) toggle.checked = state.settings.enrich;\n      panel.classList.toggle('ksv-open');\n      var filters = el('ksv-filters');\n      if (filters) filters.classList.remove('ksv-open');\n    }\n\n    function toggleFilterPanel() {\n      var filters = el('ksv-filters');\n      if (filters) filters.classList.toggle('ksv-open');\n      var panel = el('ksv-settings-panel');\n      if (panel) panel.classList.remove('ksv-open');\n    }\n\n    function onDocClick(event) {\n      if (!el('ksv-root')) return;\n      var target = event.target;\n      if (!target) return;\n\n      if (within(target, 'ksv-settings-btn')) { toggleSettingsPanel(); return; }\n      if (within(target, 'ksv-filter-btn')) { toggleFilterPanel(); return; }\n\n      if (within(target, 'ksv-refresh-btn')) {\n        var icon = el('ksv-refresh-icon');\n        if (icon) icon.classList.add('ksv-spin');\n        loadConversations().then(function () { if (icon) icon.classList.remove('ksv-spin'); },\n          function () { if (icon) icon.classList.remove('ksv-spin'); });\n        return;\n      }\n\n      if (within(target, 'ksv-thread-refresh')) {\n        var tIcon = el('ksv-thread-refresh-icon');\n        if (tIcon) tIcon.classList.add('ksv-spin');\n        refreshThread().then(function () { if (tIcon) tIcon.classList.remove('ksv-spin'); markThreadSeen(); },\n          function () { if (tIcon) tIcon.classList.remove('ksv-spin'); });\n        return;\n      }\n\n      if (within(target, 'ksv-back-btn')) {\n        var app = el('ksv-app');\n        if (app) app.classList.remove('ksv-mobile-thread');\n        return;\n      }\n\n      if (within(target, 'ksv-send-btn')) { sendMessage(); return; }\n\n      var container = el('ksv-conversations');\n      if (container && container.contains(target)) {\n        var node = target;\n        while (node && node !== container) {\n          if (node.classList && node.classList.contains('ksv-conv')) {\n            var key = node.getAttribute('data-key');\n            var conv = state.conversations.filter(function (c) { return c.key === key; })[0];\n            if (conv) openConversation(conv);\n            return;\n          }\n          node = node.parentNode;\n        }\n      }\n    }\n\n    function onDocChange(event) {\n      var target = event.target;\n      if (!target || !target.id) return;\n      if (target.id === 'ksv-user-filter' || target.id === 'ksv-unread-filter') {\n        renderList();\n      } else if (target.id === 'ksv-enrich-toggle') {\n        state.settings.enrich = !!target.checked;\n        saveSettings();\n        renderList();\n        if (state.active) renderThreadHeader();\n        lookupContactNames();\n      }\n    }\n\n    function onDocInput(event) {\n      var target = event.target;\n      if (!target || !target.id) return;\n      if (target.id === 'ksv-search') {\n        renderList();\n      } else if (target.id === 'ksv-input') {\n        target.style.height = 'auto';\n        target.style.height = Math.min(target.scrollHeight, 130) + 'px';\n      }\n    }\n\n    function onDocKeydown(event) {\n      var target = event.target;\n      if (target && target.id === 'ksv-input' && event.key === 'Enter' && !event.shiftKey) {\n        event.preventDefault();\n        sendMessage();\n      }\n    }\n\n    /* Scroll does not bubble — use capture phase. */\n    function onDocScroll(event) {\n      var target = event.target;\n      if (target && target.id === 'ksv-conversations') {\n        if (target.scrollHeight - target.scrollTop - target.clientHeight < 140) {\n          loadMoreConversations();\n        }\n      }\n    }\n\n    function init() {\n      document.addEventListener('click', onDocClick);\n      document.addEventListener('change', onDocChange);\n      document.addEventListener('input', onDocInput);\n      document.addEventListener('keydown', onDocKeydown);\n      document.addEventListener('scroll', onDocScroll, true);\n      window.addEventListener('resize', fitHeight);\n\n      var enrichToggle = el('ksv-enrich-toggle');\n      if (enrichToggle) enrichToggle.checked = state.settings.enrich;\n\n      if (document.fonts && !document.getElementById('ksv-font-link')) {\n        var fontLink = document.createElement('link');\n        fontLink.id = 'ksv-font-link';\n        fontLink.rel = 'stylesheet';\n        fontLink.href = 'https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap';\n        document.head.appendChild(fontLink);\n      }\n\n      fitHeight();\n      applyContrast();\n      setTimeout(function () { fitHeight(); applyContrast(); }, 300);\n\n      /* The theme can switch at runtime (light/dark) which changes the resolved\n         --color-primary — re-probe contrast when html/body attributes change. */\n      var themeObserver = null;\n      if (window.MutationObserver) {\n        themeObserver = new MutationObserver(function () { applyContrast(); });\n        themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n        if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      }\n\n      if (!getToken()) {\n        showFatal('inloggnings-token saknas (s2_utoken).');\n        return;\n      }\n\n      var loading = el('ksv-conversations');\n      if (loading) {\n        loading.innerHTML = '<div class=\"ksv-loading\"><svg class=\"ksv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Hämtar konversationer...</div>';\n      }\n\n      loadUsers().then(loadConversations).catch(function (error) {\n        showFatal(error && error.message ? error.message : 'okänt fel');\n      });\n\n      state.pollTimer = setInterval(poll, POLL_MS);\n\n      window.__ksvCleanup = function () {\n        if (themeObserver) themeObserver.disconnect();\n        clearInterval(state.pollTimer);\n        window.removeEventListener('resize', fitHeight);\n        document.removeEventListener('click', onDocClick);\n        document.removeEventListener('change', onDocChange);\n        document.removeEventListener('input', onDocInput);\n        document.removeEventListener('keydown', onDocKeydown);\n        document.removeEventListener('scroll', onDocScroll, true);\n      };\n    }\n\n    try {\n      init();\n    } catch (error) {\n      showFatal(error && error.message ? error.message : String(error));\n      if (window.console) console.error('KSV init error', error);\n    }\n  })();\n  </script>\n</div>","iconName":"comment-dots","iconColor":"#1565c0","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a4942f8e58b9e7a9eb33706","name":"Rapportverktyg","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["6a4cc9c8704935b4ffb551ca"],"locations":[{"type":"navigation","label":"Rapportverktyg"}],"html":"<div id=\"s2rv-root\"><div class=\"s2rv-boot\">Läser in rapportverktyget…</div></div>\n<style>\n/* Rapportverktyg — allt prefixat s2rv för att inte läcka in i SPA:n.\n   Färger mappas mot SPA:ns temavariabler (mörkt läge/orgteman följer med). */\n\n#s2rv-root {\n  --s2rv-bg-1: var(--color-background-1, #fff);\n  --s2rv-bg-2: var(--color-background-2, #fafafa);\n  --s2rv-bg-3: var(--color-background-3, #eee);\n  --s2rv-text-1: var(--color-text-1, #212121);\n  --s2rv-text-2: var(--color-text-2, #616161);\n  --s2rv-text-3: var(--color-text-3, #757575);\n  --s2rv-text-4: var(--color-text-4, #9e9e9e);\n  --s2rv-text-1-rgb: var(--color-text-1-rgb, 33, 33, 33);\n  --s2rv-primary: var(--color-primary, #1565c0);\n  --s2rv-primary-rgb: var(--color-primary-rgb, 21, 101, 192);\n  --s2rv-danger: var(--color-danger, #e53935);\n  --s2rv-danger-rgb: var(--color-danger-rgb, 229, 57, 53);\n  --s2rv-good: var(--color-good, #43a047);\n  --s2rv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n  --s2rv-active: var(--color-element-active, rgba(20, 20, 20, 0.07));\n  --s2rv-border: rgba(var(--s2rv-text-1-rgb), 0.14);\n  --s2rv-border-soft: rgba(var(--s2rv-text-1-rgb), 0.08);\n  --s2rv-radius: var(--border-radius-button, 8px);\n\n  font-family: var(--font-family-default, Nunito, -apple-system, sans-serif);\n  color: var(--s2rv-text-1);\n  font-size: 14px;\n  line-height: 1.45;\n}\n\n#s2rv-root *, #s2rv-root *::before, #s2rv-root *::after { box-sizing: border-box; }\n\n.s2rv-boot { padding: 32px; color: var(--s2rv-text-3); }\n\n.s2rv-app { padding: 8px 0 40px; max-width: 1280px; margin: 0 auto; }\n\n.s2rv-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 4px; min-height: 16px; }\n.s2rv-version { color: var(--s2rv-text-4); font-size: 11px; }\n.s2rv-save-indicator { color: var(--s2rv-text-3); font-size: 12px; }\n.s2rv-save-error { color: var(--s2rv-danger); }\n\n/* Flikar */\n.s2rv-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--s2rv-border-soft); margin-bottom: 16px; }\n.s2rv-tab {\n  border: none; background: none; padding: 8px 14px; cursor: pointer;\n  font: inherit; color: var(--s2rv-text-2); border-bottom: 2px solid transparent;\n}\n.s2rv-tab:hover { color: var(--s2rv-text-1); }\n.s2rv-tab-active { color: var(--s2rv-primary); border-bottom-color: var(--s2rv-primary); font-weight: 600; }\n\n/* Knappar */\n.s2rv-btn {\n  display: inline-block; border: 1px solid var(--s2rv-border); background: transparent; color: var(--s2rv-text-1);\n  border-radius: var(--s2rv-radius); padding: 6px 12px; font: inherit; font-size: 13px; cursor: pointer;\n  margin: 2px 4px 2px 0;\n}\n.s2rv-btn:hover { background: var(--s2rv-hover); }\n.s2rv-btn-primary { background: var(--s2rv-primary); border-color: var(--s2rv-primary); color: #fff; }\n.s2rv-btn-primary:hover { background: var(--s2rv-primary); filter: brightness(0.9); }\n.s2rv-btn-danger { color: var(--s2rv-danger); border-color: rgba(var(--s2rv-danger-rgb), 0.4); }\n.s2rv-btn-danger:hover { background: rgba(var(--s2rv-danger-rgb), 0.08); }\n.s2rv-btn-small { padding: 2px 7px; font-size: 12px; }\n\n/* Formulär */\n#s2rv-root select, #s2rv-root input[type='text'], #s2rv-root input[type='date'] {\n  font: inherit; font-size: 13px; padding: 6px 8px; border: 1px solid var(--s2rv-border);\n  border-radius: var(--s2rv-radius); background: var(--s2rv-bg-1); color: var(--s2rv-text-1); max-width: 100%;\n}\n.s2rv-form { background: var(--s2rv-bg-2); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 14px 16px; margin-bottom: 16px; }\n.s2rv-form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px 16px; }\n.s2rv-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n.s2rv-field-label { font-size: 11px; font-weight: 600; color: var(--s2rv-text-2); text-transform: uppercase; letter-spacing: 0.03em; }\n.s2rv-form-section { padding: 10px 0; border-bottom: 1px solid var(--s2rv-border-soft); }\n.s2rv-form-section:first-child { padding-top: 0; }\n.s2rv-form-section:last-of-type { border-bottom: none; }\n.s2rv-form-actions { margin-top: 8px; display: flex; gap: 8px; align-items: center; }\n.s2rv-inline { margin-left: 10px; white-space: nowrap; }\n.s2rv-range { display: inline-flex; gap: 8px; flex-wrap: wrap; align-items: center; }\n.s2rv-range-custom { display: inline-flex; gap: 8px; align-items: center; }\n.s2rv-section-title { margin: 0 0 10px; font-size: 13px; font-weight: 700; color: var(--s2rv-text-1); }\n\n/* Flervalsdropdown */\n.s2rv-msel { position: relative; display: inline-block; min-width: 160px; }\n.s2rv-msel-button {\n  width: 100%; text-align: left; border: 1px solid var(--s2rv-border); background: var(--s2rv-bg-1);\n  border-radius: var(--s2rv-radius); padding: 6px 8px; font: inherit; font-size: 13px; cursor: pointer;\n  color: var(--s2rv-text-1);\n  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.s2rv-msel-list {\n  display: none; position: absolute; z-index: 40; top: calc(100% + 2px); left: 0; min-width: 100%;\n  max-height: 260px; overflow: auto; background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border);\n  border-radius: var(--s2rv-radius); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); padding: 4px;\n}\n.s2rv-msel-open .s2rv-msel-list { display: block; }\n.s2rv-msel-item { display: flex; gap: 8px; align-items: center; padding: 4px 6px; border-radius: 4px; cursor: pointer; white-space: nowrap; color: var(--s2rv-text-1); }\n.s2rv-msel-item:hover { background: var(--s2rv-hover); }\n\n/* Byggare: sidopanel + huvudyta */\n.s2rv-builder { display: grid; grid-template-columns: 220px 1fr; gap: 20px; align-items: start; }\n.s2rv-sidebar h3 { margin: 16px 0 6px; font-size: 12px; text-transform: uppercase; color: var(--s2rv-text-2); letter-spacing: 0.03em; }\n.s2rv-report-list { display: flex; flex-direction: column; gap: 2px; }\n.s2rv-list-item {\n  text-align: left; border: none; background: none; padding: 6px 8px; border-radius: var(--s2rv-radius);\n  font: inherit; font-size: 13px; cursor: pointer; color: var(--s2rv-text-1);\n  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.s2rv-list-item:hover { background: var(--s2rv-hover); }\n.s2rv-list-item-active { background: rgba(var(--s2rv-primary-rgb), 0.12); color: var(--s2rv-primary); font-weight: 600; }\n@media (max-width: 900px) { .s2rv-builder { grid-template-columns: 1fr; } }\n\n/* Resultat */\n.s2rv-status { min-height: 18px; color: var(--s2rv-text-3); font-size: 12px; margin: 6px 0; }\n.s2rv-status-error { color: var(--s2rv-danger); }\n.s2rv-empty { padding: 24px; color: var(--s2rv-text-4); text-align: center; }\n.s2rv-viz { min-height: 40px; }\n.s2rv-chart { position: relative; height: 340px; }\n.s2rv-export-row { margin-top: 12px; border-top: 1px solid var(--s2rv-border-soft); padding-top: 10px; }\n\n/* Tabell */\n.s2rv-table-wrap { overflow: auto; max-height: 480px; border: 1px solid var(--s2rv-border-soft); border-radius: 8px; }\n.s2rv-table { border-collapse: collapse; width: 100%; font-size: 13px; }\n.s2rv-table th, .s2rv-table td { padding: 7px 10px; border-bottom: 1px solid var(--s2rv-border-soft); text-align: left; white-space: nowrap; color: var(--s2rv-text-1); }\n.s2rv-table thead th { position: sticky; top: 0; background: var(--s2rv-bg-2); font-weight: 600; z-index: 1; }\n.s2rv-table .s2rv-num { text-align: right; font-variant-numeric: tabular-nums; }\n.s2rv-total-row td { font-weight: 600; background: var(--s2rv-bg-2); }\n\n/* KPI-kort */\n.s2rv-kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }\n.s2rv-kpi-card { background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 16px; text-align: center; }\n.s2rv-kpi-value { font-size: 26px; font-weight: 700; color: var(--s2rv-primary); font-variant-numeric: tabular-nums; }\n.s2rv-kpi-label { margin-top: 4px; color: var(--s2rv-text-2); font-size: 12px; }\n\n/* Förlopp */\n.s2rv-progress { display: flex; align-items: center; gap: 12px; margin: 10px 0; flex-wrap: wrap; }\n.s2rv-progress-label { font-size: 12px; color: var(--s2rv-text-2); min-width: 180px; }\n.s2rv-progress-track { flex: 1 1 200px; height: 8px; background: var(--s2rv-bg-3); border-radius: 4px; overflow: hidden; position: relative; }\n.s2rv-progress-bar { height: 100%; background: var(--s2rv-primary); border-radius: 4px; transition: width 0.25s ease; }\n.s2rv-progress-indeterminate .s2rv-progress-bar { animation: s2rv-slide 1.2s ease-in-out infinite; }\n@keyframes s2rv-slide {\n  0% { margin-left: -30%; }\n  100% { margin-left: 100%; }\n}\n\n/* Dashboards */\n.s2rv-dash-header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }\n.s2rv-dash-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }\n.s2rv-tile { background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 12px 14px; min-height: 120px; }\n.s2rv-tile-wide { grid-column: span 2; }\n.s2rv-tile-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.s2rv-tile-head h3 { margin: 0; font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.s2rv-tile-actions { white-space: nowrap; }\n.s2rv-tile-content .s2rv-chart { height: 240px; }\n.s2rv-tile-content .s2rv-table-wrap { max-height: 280px; }\n@media (max-width: 900px) { .s2rv-dash-grid { grid-template-columns: 1fr; } .s2rv-tile-wide { grid-column: auto; } }\n\n/* Mallar */\n.s2rv-template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }\n.s2rv-template-card {\n  background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 14px 16px;\n  cursor: pointer; text-align: left; font: inherit; color: var(--s2rv-text-1); display: block; width: 100%;\n}\n.s2rv-template-card:hover { background: var(--s2rv-hover); border-color: rgba(var(--s2rv-primary-rgb), 0.4); }\n.s2rv-template-card h3 { margin: 0 0 6px; font-size: 15px; }\n.s2rv-template-card p { margin: 0 0 10px; color: var(--s2rv-text-2); font-size: 13px; }\n.s2rv-template-cta { color: var(--s2rv-primary); font-size: 13px; font-weight: 600; }\n\n/* Exporter */\n.s2rv-export-list { display: flex; flex-direction: column; gap: 8px; }\n.s2rv-export-item {\n  display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;\n  background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 10px 14px;\n}\n\n/* Tomtillstånd */\n.s2rv-empty-state {\n  text-align: center; padding: 36px 20px; border: 1px dashed var(--s2rv-border); border-radius: 10px;\n  color: var(--s2rv-text-2);\n}\n.s2rv-empty-state h3 { margin: 0 0 6px; font-size: 15px; color: var(--s2rv-text-1); }\n.s2rv-empty-state p { margin: 0 auto 14px; font-size: 13px; max-width: 420px; }\n.s2rv-empty-state-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }\n.s2rv-dash-grid .s2rv-empty-state { grid-column: 1 / -1; }\n\n.s2rv-muted { color: var(--s2rv-text-4); font-size: 13px; }\n\n</style>\n<script>\n\"use strict\";\n(() => {\n  // src/env.ts\n  var Ke = \"0.1.0\";\n  function ae() {\n    let t = document.cookie.match(/(?:^|;\\s*)s2_utoken=([^;]+)/);\n    return t ? decodeURIComponent(t[1]) : null;\n  }\n  function Qe() {\n    return window.__s2rvApiBase || \"\";\n  }\n\n  // src/api/http.ts\n  var N = class extends Error {\n    constructor(e, r) {\n      super(r), this.status = e;\n    }\n  };\n  function qt(t) {\n    if (!t) return \"\";\n    let e = [];\n    for (let r of Object.keys(t)) {\n      let o = t[r];\n      if (o == null || Array.isArray(o) && o.length === 0) continue;\n      let s = Array.isArray(o) ? o.join(\",\") : String(o);\n      e.push(encodeURIComponent(r) + \"=\" + encodeURIComponent(s));\n    }\n    return e.length ? \"?\" + e.join(\"&\") : \"\";\n  }\n  var Kt = /* @__PURE__ */ new Set([429, 502, 503, 504]), De = { until: 0 };\n  async function We(t, e, r, o, s) {\n    let a = ae();\n    if (!a) throw new N(401, \"no token\");\n    let l = Qe() + e + qt(r), i = null;\n    for (let u = 0; u < 4; u++) {\n      if (s != null && s.aborted) throw new DOMException(\"aborted\", \"AbortError\");\n      if (u > 0) {\n        De.until = Date.now() + 3e4;\n        let m = 1e3 * Math.pow(2, u - 1);\n        await new Promise((y, g) => {\n          let b = setTimeout(y, m);\n          s == null || s.addEventListener(\"abort\", () => {\n            clearTimeout(b), g(new DOMException(\"aborted\", \"AbortError\"));\n          }, { once: !0 });\n        });\n      }\n      let d;\n      try {\n        d = await fetch(l, {\n          method: t,\n          signal: s,\n          headers: {\n            authorization: \"Bearer \" + a,\n            ...o != null ? { \"content-type\": \"application/json\" } : {}\n          },\n          body: o != null ? JSON.stringify(o) : void 0\n        });\n      } catch (m) {\n        if ((m == null ? void 0 : m.name) === \"AbortError\") throw m;\n        i = m;\n        continue;\n      }\n      if (d.ok) {\n        let m = await d.text();\n        return m ? JSON.parse(m) : null;\n      }\n      if (!Kt.has(d.status)) {\n        let m = await d.text().catch(() => \"\");\n        throw new N(d.status, t + \" \" + e + \" -> \" + d.status + \" \" + m.slice(0, 300));\n      }\n      i = new N(d.status, t + \" \" + e + \" -> \" + d.status);\n    }\n    throw i instanceof Error ? i : new N(0, \"request failed\");\n  }\n  function D(t, e, r) {\n    return We(\"GET\", t, e, void 0, r);\n  }\n  function Je(t, e, r) {\n    return We(\"PUT\", t, void 0, e, r);\n  }\n\n  // src/i18n/sv.ts\n  var n = {\n    appTitle: \"Rapportverktyg\",\n    tabReports: \"Rapporter\",\n    tabDashboards: \"Dashboards\",\n    tabTemplates: \"Mallar\",\n    tabExports: \"Exporter\",\n    noToken: \"Kunde inte l\\xE4sa din inloggning. Ladda om sidan och f\\xF6rs\\xF6k igen.\",\n    loadError: \"N\\xE5got gick snett vid inl\\xE4sning. F\\xF6rs\\xF6k igen.\",\n    networkError: \"N\\xE4tverksfel. Kontrollera din anslutning och f\\xF6rs\\xF6k igen.\",\n    cdnError: \"Kunde inte l\\xE4sa in diagrambiblioteket. Kontrollera din anslutning och f\\xF6rs\\xF6k igen.\",\n    newReport: \"Ny rapport\",\n    editReport: \"Redigera\",\n    runReport: \"K\\xF6r rapport\",\n    saveReport: \"Spara rapport\",\n    deleteReport: \"Ta bort\",\n    duplicateReport: \"Duplicera\",\n    reportName: \"Rapportnamn\",\n    unnamedReport: \"Namnl\\xF6s rapport\",\n    savedReports: \"Sparade rapporter\",\n    noSavedReports: \"Inga sparade rapporter \\xE4nnu. Skapa en ny rapport eller utg\\xE5 fr\\xE5n en mall.\",\n    startFromTemplate: \"B\\xF6rja fr\\xE5n en mall \\u2192\",\n    reportInUse: \"Rapporten anv\\xE4nds i en dashboard. Ta bort den \\xE4nd\\xE5?\",\n    confirmDelete: \"Ta bort? Detta g\\xE5r inte att \\xE5ngra.\",\n    sectionData: \"Data\",\n    sectionGrouping: \"Gruppering\",\n    sectionPeriodFilters: \"Period & filter\",\n    sectionViz: \"Visning\",\n    source: \"Datak\\xE4lla\",\n    metrics: \"M\\xE4tv\\xE4rden\",\n    groupBy: \"Gruppera p\\xE5\",\n    groupBySecondary: \"Sekund\\xE4r gruppering\",\n    filters: \"Filter\",\n    addFilter: \"L\\xE4gg till filter\",\n    dateRange: \"Period\",\n    chartType: \"Typ av visning\",\n    timeGrain: \"Tidsintervall\",\n    none: \"Ingen\",\n    all: \"Alla\",\n    selected: \"valda\",\n    vizTable: \"Tabell\",\n    vizBar: \"Stapel\",\n    vizLine: \"Linje\",\n    vizPie: \"Cirkel\",\n    vizKpi: \"Nyckeltal\",\n    stacked: \"Staplad\",\n    grainHour: \"Timme\",\n    grainDay: \"Dag\",\n    grainWeek: \"Vecka\",\n    grainMonth: \"M\\xE5nad\",\n    rangeToday: \"Idag\",\n    rangeYesterday: \"Ig\\xE5r\",\n    rangeThisWeek: \"Denna vecka\",\n    rangeLastWeek: \"F\\xF6rra veckan\",\n    rangeThisMonth: \"Denna m\\xE5nad\",\n    rangeLastMonth: \"F\\xF6rra m\\xE5naden\",\n    rangeLast7: \"Senaste 7 dagarna\",\n    rangeLast30: \"Senaste 30 dagarna\",\n    rangeThisYear: \"Detta \\xE5r\",\n    rangeCustom: \"Anpassad\",\n    rangeFrom: \"Fr\\xE5n\",\n    rangeTo: \"Till\",\n    fetching: \"H\\xE4mtar data\\u2026\",\n    fetchingRows: \"H\\xE4mtar data\",\n    counting: \"R\\xE4knar rader\\u2026\",\n    cancel: \"Avbryt\",\n    cancelled: \"Avbruten.\",\n    modeStats: \"snabbstatistik\",\n    modeRows: \"radbaserad\",\n    rows: \"rader\",\n    total: \"Totalt\",\n    emptyResult: \"Inga data f\\xF6r vald period.\",\n    largeWarnTitle: \"Stor datam\\xE4ngd\",\n    largeWarn1: \"Perioden inneh\\xE5ller\",\n    largeWarn2: \"rader, vilket kr\\xE4ver ungef\\xE4r\",\n    largeWarn3: \"f\\xF6rfr\\xE5gningar och kan ta flera minuter. Forts\\xE4tta?\",\n    continueAnyway: \"Forts\\xE4tt\",\n    tooLargeChart: \"F\\xF6r m\\xE5nga rader f\\xF6r att visa som rapport. Begr\\xE4nsa perioden eller filtren, eller exportera radvis till Excel ist\\xE4llet.\",\n    tooLargeAbs: \"F\\xF6r stor datam\\xE4ngd (\\xF6ver 250 000 rader). Dela upp perioden i mindre delar.\",\n    export: \"Exportera\",\n    exportExcel: \"Exportera till Excel\",\n    exportCsv: \"Exportera till CSV\",\n    exportRaw: \"Exportera rader (r\\xE5data)\",\n    exportAggregated: \"Exportera resultat\",\n    savedExports: \"Sparade exporter\",\n    noSavedExports: \"Inga sparade exporter \\xE4nnu. K\\xF6r en rapport och exportera f\\xF6r att spara inst\\xE4llningarna.\",\n    saveExport: \"Spara exportinst\\xE4llning\",\n    exportName: \"Exportnamn\",\n    runAgain: \"K\\xF6r igen\",\n    exporting: \"Exporterar\\u2026\",\n    newDashboard: \"Ny dashboard\",\n    dashboardName: \"Dashboardnamn\",\n    unnamedDashboard: \"Namnl\\xF6s dashboard\",\n    noDashboards: \"Inga dashboards \\xE4nnu. Spara en rapport och l\\xE4gg till den som panel.\",\n    emptyDashboardsTitle: \"Inga dashboards \\xE4nnu\",\n    emptyDashboardsText: \"Kom ig\\xE5ng direkt med en f\\xE4rdig exempeldashboard, eller bygg en egen av dina sparade rapporter.\",\n    createExampleDashboard: \"Skapa exempeldashboard\",\n    exampleDashboardName: \"Exempeldashboard\",\n    addTile: \"L\\xE4gg till panel\",\n    pickReport: \"V\\xE4lj rapport\",\n    refreshAll: \"Uppdatera alla\",\n    refresh: \"Uppdatera\",\n    removeTile: \"Ta bort panel\",\n    deleteDashboard: \"Ta bort dashboard\",\n    useDashboardRange: \"F\\xF6lj dashboardens period\",\n    moveUp: \"Flytta upp\",\n    moveDown: \"Flytta ner\",\n    tileWide: \"Bred\",\n    tileNarrow: \"Smal\",\n    useTemplate: \"Anv\\xE4nd mall\",\n    templatesIntro: \"F\\xE4rdiga rapporter \\u2014 klicka f\\xF6r att \\xF6ppna i rapportbyggaren.\",\n    saveOk: \"Sparat.\",\n    saveError: \"Kunde inte spara. F\\xF6rs\\xF6k igen.\",\n    storageFull: \"Lagringsutrymmet f\\xF6r sparade rapporter \\xE4r fullt. Ta bort gamla rapporter eller dashboards.\",\n    user: \"Anv\\xE4ndare\",\n    team: \"Team\",\n    project: \"Projekt\",\n    dialGroup: \"Ringlista\",\n    tag: \"Tagg\",\n    status: \"Status\",\n    template: \"Avtalsmall\",\n    time: \"Tid\",\n    unknown: \"Ok\\xE4nd\",\n    noValue: \"(saknas)\",\n    others: \"\\xD6vriga\",\n    sourceCalls: \"Samtal\",\n    sourceOrders: \"Ordrar\",\n    sourceOffers: \"Avtal (e-sign)\",\n    sourceDialGroups: \"Ringlistor\",\n    sourceTime: \"Tid & aktivitet\",\n    statusPending: \"Utkast\",\n    statusDistributed: \"Skickat\",\n    statusRead: \"L\\xE4st\",\n    statusReviewPending: \"V\\xE4ntar granskning\",\n    statusSigned: \"Signerat\",\n    statusDeclined: \"Nekat\",\n    statusExpired: \"Utg\\xE5nget\",\n    statusCanceled: \"Avbrutet\"\n  };\n\n  // src/api/lookups.ts\n  var Te = [\n    { id: \"pending\", label: n.statusPending },\n    { id: \"distributed\", label: n.statusDistributed },\n    { id: \"read\", label: n.statusRead },\n    { id: \"reviewPending\", label: n.statusReviewPending },\n    { id: \"signed\", label: n.statusSigned },\n    { id: \"declined\", label: n.statusDeclined },\n    { id: \"expired\", label: n.statusExpired },\n    { id: \"canceled\", label: n.statusCanceled }\n  ];\n  async function H(t, e) {\n    try {\n      return await t;\n    } catch (r) {\n      if ((r == null ? void 0 : r.name) === \"AbortError\") throw r;\n      return console.error(\"s2rv lookup error\", r), e;\n    }\n  }\n  async function Xe(t) {\n    var y;\n    let [e, r, o, s, a, l, i] = await Promise.all([\n      H(D(\"/api/users/users-v1\", void 0, t), []),\n      H(D(\"/api/users/teams-v1\", void 0, t), []),\n      H(D(\"/api/users/project-information-v1/simple\", void 0, t), []),\n      H(Qt(t), []),\n      H(D(\"/api/dial/tags-v1\", void 0, t), []),\n      H(D(\"/api/orders/tags-v1\", void 0, t), []),\n      H(D(\"/api/offers/offer-templates-v2\", void 0, t), [])\n    ]), u = Array.isArray(a) ? a : (y = a == null ? void 0 : a.tags) != null ? y : [], d = {\n      users: e.map((g) => ({ id: g.id, label: g.fullName || g.alias || g.username || g.id })),\n      teams: r.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      projects: o.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      dialGroups: s.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      callTags: u.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      orderTags: l.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      offerTemplates: i.filter((g) => !g.isRemoved).map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.title) != null ? b : g.id };\n      }),\n      offerStatuses: Te,\n      userTeams: /* @__PURE__ */ new Map(),\n      names: /* @__PURE__ */ new Map()\n    };\n    for (let g of e) {\n      let b = [\n        ...g.teamId ? [g.teamId] : [],\n        ...Array.isArray(g.teams) ? g.teams.map((v) => typeof v == \"string\" ? v : v == null ? void 0 : v.id) : []\n      ].filter(Boolean);\n      d.userTeams.set(g.id, [...new Set(b)]);\n    }\n    let m = [\n      d.users,\n      d.teams,\n      d.projects,\n      d.dialGroups,\n      d.callTags,\n      d.orderTags,\n      d.offerTemplates,\n      d.offerStatuses\n    ];\n    for (let g of m)\n      for (let b of g) d.names.set(b.id, b.label);\n    return d;\n  }\n  async function Qt(t) {\n    let e = [];\n    for (let r = 0; ; r += 100) {\n      let o = await D(\"/api/dial/dial-groups-v2\", { offset: r, count: 100 }, t);\n      if (e.push(...o), o.length < 100 || r > 5e3) break;\n    }\n    return e;\n  }\n  function E(t, e) {\n    var r;\n    return e == null || e === \"\" ? n.noValue : (r = t.names.get(e)) != null ? r : n.unknown;\n  }\n\n  // src/schema.ts\n  function ie() {\n    return { version: 1, reports: {}, dashboards: {}, exports: {} };\n  }\n  function j(t) {\n    return t + \"_\" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n  }\n  function Ye(t) {\n    if (!t || typeof t != \"object\") return ie();\n    let e = t;\n    return e.version !== 1 ? ie() : {\n      version: 1,\n      reports: e.reports && typeof e.reports == \"object\" ? e.reports : {},\n      dashboards: e.dashboards && typeof e.dashboards == \"object\" ? e.dashboards : {},\n      exports: e.exports && typeof e.exports == \"object\" ? e.exports : {}\n    };\n  }\n\n  // src/storage.ts\n  var Ze = \"report-tool\", Wt = 8e4, Jt = 1500;\n  function $e() {\n    let t = null, e = null;\n    async function r() {\n      if (!e) return;\n      let { store: o, onStatus: s } = e;\n      if (e = null, JSON.stringify(o).length > Wt) {\n        s(\"error\", n.storageFull);\n        return;\n      }\n      s(\"saving\");\n      try {\n        await Je(\"/api/users/users-v1/me\", { [\"reference.\" + Ze]: o }), s(\"saved\");\n      } catch (l) {\n        console.error(\"s2rv save error\", l), s(\"error\", n.saveError);\n      }\n    }\n    return {\n      async load(o) {\n        var a;\n        let s = await D(\"/api/users/users-v1/me\", void 0, o);\n        return Ye((a = s == null ? void 0 : s.reference) == null ? void 0 : a[Ze]);\n      },\n      schedule(o, s) {\n        e = { store: o, onStatus: s }, t && clearTimeout(t), t = setTimeout(() => {\n          t = null, r();\n        }, Jt);\n      },\n      async flush() {\n        t && (clearTimeout(t), t = null), await r();\n      },\n      destroy() {\n        t && (clearTimeout(t), t = null), r();\n      }\n    };\n  }\n\n  // src/catalog/sources.ts\n  function rt(t) {\n    var o, s, a;\n    let e = Array.isArray(t.products) ? t.products : [], r = 0;\n    for (let l of e) {\n      let i = Number((s = (o = l.price) != null ? o : l.salaryBasis) != null ? s : 0), u = Number((a = l.quantity) != null ? a : 1);\n      Number.isFinite(i) && Number.isFinite(u) && (r += i * u);\n    }\n    return r;\n  }\n  function Xt(t, e) {\n    let r = e.userTeams.get(t.userId);\n    return r && r.length ? r : [null];\n  }\n  function et(t) {\n    let e = Array.isArray(t.tagIds) ? t.tagIds : [];\n    return e.length ? e : [null];\n  }\n  var le = (t) => ({\n    id: \"user\",\n    label: n.user,\n    statsSupported: t,\n    rowKeys: (e) => {\n      var r;\n      return [(r = e.userId) != null ? r : null];\n    }\n  }), ue = { id: \"team\", label: n.team, rowKeys: Xt }, de = {\n    id: \"project\",\n    label: n.project,\n    rowKeys: (t) => {\n      var e;\n      return [(e = t.projectId) != null ? e : null];\n    }\n  }, ce = (t) => ({\n    id: \"time\",\n    label: n.time,\n    statsSupported: t\n  });\n  function tt(t) {\n    return Math.round(Number(t != null ? t : 0) / 1e3);\n  }\n  var Yt = [\n    { id: \"date\", label: \"Datum\", value: (t) => {\n      var e;\n      return (e = t.connectingAt) != null ? e : \"\";\n    } },\n    { id: \"user\", label: n.user, value: (t, e) => E(e, t.userId) },\n    { id: \"dialGroup\", label: n.dialGroup, value: (t, e) => t.dialGroupId ? E(e, t.dialGroupId) : \"\" },\n    { id: \"project\", label: n.project, value: (t, e) => t.projectId ? E(e, t.projectId) : \"\" },\n    { id: \"recipient\", label: \"Nummer\", value: (t) => {\n      var e;\n      return (e = t.recipient) != null ? e : \"\";\n    } },\n    { id: \"answered\", label: \"Svarat\", value: (t) => t.isContactAnswer ? \"Ja\" : \"Nej\" },\n    { id: \"totalDuration\", label: \"Total tid (s)\", value: (t) => tt(t.totalDuration) },\n    { id: \"connectedDuration\", label: \"Samtalstid (s)\", value: (t) => tt(t.connectedDuration) },\n    { id: \"tags\", label: \"Taggar\", value: (t, e) => {\n      var r;\n      return ((r = t.tagIds) != null ? r : []).map((o) => E(e, o)).join(\", \");\n    } }\n  ], Zt = [\n    { id: \"date\", label: \"Datum\", value: (t) => {\n      var e, r;\n      return (r = (e = t.businessDate) != null ? e : t.date) != null ? r : \"\";\n    } },\n    { id: \"serialId\", label: \"Ordernr\", value: (t) => {\n      var e;\n      return (e = t.serialId) != null ? e : \"\";\n    } },\n    { id: \"user\", label: n.user, value: (t, e) => E(e, t.userId) },\n    { id: \"project\", label: n.project, value: (t, e) => t.projectId ? E(e, t.projectId) : \"\" },\n    { id: \"sum\", label: \"Orderv\\xE4rde\", value: (t) => rt(t) },\n    { id: \"products\", label: \"Produkter\", value: (t) => {\n      var e;\n      return ((e = t.products) != null ? e : []).map((r) => r.name).filter(Boolean).join(\", \");\n    } },\n    { id: \"tags\", label: \"Taggar\", value: (t, e) => {\n      var r;\n      return ((r = t.tagIds) != null ? r : []).map((o) => E(e, o)).join(\", \");\n    } }\n  ], $t = [\n    { id: \"date\", label: \"Datum\", value: (t) => {\n      var e;\n      return (e = t.date) != null ? e : \"\";\n    } },\n    { id: \"serialId\", label: \"Avtalsnr\", value: (t) => {\n      var e;\n      return (e = t.serialId) != null ? e : \"\";\n    } },\n    { id: \"user\", label: n.user, value: (t, e) => E(e, t.userId) },\n    { id: \"status\", label: n.status, value: (t, e) => E(e, t.status) },\n    { id: \"template\", label: n.template, value: (t, e) => t.templateId ? E(e, t.templateId) : \"\" },\n    { id: \"signedAt\", label: \"Signerat\", value: (t) => {\n      var e, r;\n      return (r = (e = t.signature) == null ? void 0 : e.signedAt) != null ? r : \"\";\n    } }\n  ], er = [\n    { id: \"name\", label: n.dialGroup, value: (t) => {\n      var e;\n      return (e = t.name) != null ? e : \"\";\n    } },\n    { id: \"project\", label: n.project, value: (t, e) => t.projectId ? E(e, t.projectId) : \"\" },\n    { id: \"contacts\", label: \"Kontakter\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.contactCount) != null ? r : 0;\n    } },\n    { id: \"reserved\", label: \"Reserverade\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.reservedContactCount) != null ? r : 0;\n    } },\n    { id: \"skipped\", label: \"\\xD6verhoppade\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.skippedContactCount) != null ? r : 0;\n    } },\n    { id: \"quarantined\", label: \"Karant\\xE4n\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.quarantinedContactCount) != null ? r : 0;\n    } }\n  ], Ae = [\n    {\n      id: \"calls\",\n      label: n.sourceCalls,\n      rowTime: (t) => {\n        var e;\n        return (e = t.connectingAt) != null ? e : null;\n      },\n      metrics: [\n        { id: \"count\", label: \"Antal samtal\", format: \"int\", agg: \"count\", statsField: \"count\" },\n        { id: \"connectedCount\", label: \"Svarade samtal\", format: \"int\", agg: \"count\", predicate: (t) => !!t.isContactAnswer, statsField: \"connectedCount\" },\n        { id: \"answerRate\", label: \"Svarsfrekvens\", format: \"percent\", rate: [\"connectedCount\", \"count\"] },\n        { id: \"totalDuration\", label: \"Total tid\", format: \"duration\", agg: \"sum\", value: (t) => {\n          var e;\n          return Number((e = t.totalDuration) != null ? e : 0);\n        }, statsField: \"totalDuration\" },\n        { id: \"connectedDuration\", label: \"Samtalstid\", format: \"duration\", agg: \"sum\", value: (t) => {\n          var e;\n          return Number((e = t.connectedDuration) != null ? e : 0);\n        }, statsField: \"connectedDuration\" },\n        { id: \"avgDuration\", label: \"Snitt samtalstid\", format: \"duration\", rate: [\"totalDuration\", \"count\"] }\n      ],\n      dimensions: [\n        le(!0),\n        ue,\n        de,\n        { id: \"dialGroup\", label: n.dialGroup, rowKeys: (t) => {\n          var e;\n          return [(e = t.dialGroupId) != null ? e : null];\n        } },\n        { id: \"tag\", label: n.tag, statsSupported: !0, rowKeys: et },\n        ce(!0)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" },\n        { id: \"dialGroup\", label: n.dialGroup, lookup: \"dialGroups\", target: \"dialGroupIds\" },\n        { id: \"tag\", label: n.tag, lookup: \"callTags\", target: \"tagIds\" }\n      ],\n      rowColumns: Yt\n    },\n    {\n      id: \"orders\",\n      label: n.sourceOrders,\n      rowTime: (t) => {\n        var e, r;\n        return (r = (e = t.businessDate) != null ? e : t.date) != null ? r : null;\n      },\n      metrics: [\n        { id: \"count\", label: \"Antal ordrar\", format: \"int\", agg: \"count\", statsField: \"count\" },\n        { id: \"sumAmount\", label: \"Orderv\\xE4rde\", format: \"currency\", agg: \"sum\", value: rt },\n        { id: \"avgAmount\", label: \"Snitt orderv\\xE4rde\", format: \"currency\", rate: [\"sumAmount\", \"count\"] }\n      ],\n      dimensions: [\n        le(!0),\n        ue,\n        de,\n        { id: \"tag\", label: n.tag, statsSupported: !0, rowKeys: et },\n        ce(!0)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" },\n        { id: \"tag\", label: n.tag, lookup: \"orderTags\", target: \"tagIds\" }\n      ],\n      rowColumns: Zt\n    },\n    {\n      id: \"offers\",\n      label: n.sourceOffers,\n      rowTime: (t) => {\n        var e;\n        return (e = t.date) != null ? e : null;\n      },\n      metrics: [\n        { id: \"count\", label: \"Antal avtal\", format: \"int\", agg: \"count\" },\n        { id: \"signedCount\", label: \"Signerade\", format: \"int\", agg: \"count\", predicate: (t) => t.status === \"signed\" },\n        { id: \"signRate\", label: \"Signeringsgrad\", format: \"percent\", rate: [\"signedCount\", \"count\"] }\n      ],\n      dimensions: [\n        le(!1),\n        ue,\n        de,\n        { id: \"status\", label: n.status, rowKeys: (t) => {\n          var e;\n          return [(e = t.status) != null ? e : null];\n        } },\n        { id: \"template\", label: n.template, rowKeys: (t) => {\n          var e;\n          return [(e = t.templateId) != null ? e : null];\n        } },\n        ce(!1)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" },\n        { id: \"status\", label: n.status, lookup: \"offerStatuses\", target: \"statuses\" },\n        { id: \"template\", label: n.template, lookup: \"offerTemplates\", target: \"templateIds\" }\n      ],\n      rowColumns: $t\n    },\n    {\n      id: \"dialGroups\",\n      label: n.sourceDialGroups,\n      metrics: [\n        { id: \"groupCount\", label: \"Antal ringlistor\", format: \"int\", agg: \"count\" },\n        { id: \"contactCount\", label: \"Kontakter\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.contactCount) != null ? r : 0);\n        } },\n        { id: \"reservedCount\", label: \"Reserverade\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.reservedContactCount) != null ? r : 0);\n        } },\n        { id: \"skippedCount\", label: \"\\xD6verhoppade\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.skippedContactCount) != null ? r : 0);\n        } },\n        { id: \"quarantinedCount\", label: \"Karant\\xE4n\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.quarantinedContactCount) != null ? r : 0);\n        } }\n      ],\n      dimensions: [\n        { id: \"dialGroup\", label: n.dialGroup, rowKeys: (t) => {\n          var e;\n          return [(e = t.id) != null ? e : null];\n        } },\n        de\n      ],\n      filters: [\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" }\n      ],\n      rowColumns: er\n    },\n    {\n      id: \"time\",\n      label: n.sourceTime,\n      statsOnly: !0,\n      metrics: [\n        { id: \"activeDuration\", label: \"Aktiv tid\", format: \"duration\", statsField: \"activeDuration\" }\n      ],\n      dimensions: [\n        le(!0),\n        ue,\n        ce(!0)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" }\n      ],\n      rowColumns: []\n    }\n  ];\n  function M(t) {\n    let e = Ae.find((r) => r.id === t);\n    if (!e) throw new Error(\"Ok\\xE4nd datak\\xE4lla: \" + t);\n    return e;\n  }\n\n  // src/engine/timeBuckets.ts\n  var tr = \"Europe/Stockholm\", Ee = null;\n  function rr() {\n    return Ee || (Ee = new Intl.DateTimeFormat(\"sv-SE\", {\n      timeZone: tr,\n      year: \"numeric\",\n      month: \"2-digit\",\n      day: \"2-digit\",\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n      hour12: !1\n    })), Ee;\n  }\n  function U(t) {\n    let e = {};\n    for (let r of rr().formatToParts(t)) e[r.type] = r.value;\n    return {\n      y: Number(e.year),\n      m: Number(e.month),\n      d: Number(e.day),\n      h: Number(e.hour) % 24,\n      //  'sv-SE' kan ge \"24\" vid midnatt.\n      min: Number(e.minute)\n    };\n  }\n  function ot(t) {\n    let e = U(t);\n    return Date.UTC(e.y, e.m - 1, e.d, e.h, e.min, t.getUTCSeconds()) - (t.getTime() - t.getTime() % 1e3);\n  }\n  function at(t, e, r, o = 0, s = 0) {\n    let a = Date.UTC(t, e - 1, r, o, s), l = ot(new Date(a)), i = a - l, u = ot(new Date(i));\n    return u !== l && (i = a - u), new Date(i);\n  }\n  function Ie(t, e, r) {\n    return new Date(Date.UTC(t, e - 1, r));\n  }\n  function L(t, e) {\n    let r = Ie(t.y, t.m, t.d);\n    return r.setUTCDate(r.getUTCDate() + e), { y: r.getUTCFullYear(), m: r.getUTCMonth() + 1, d: r.getUTCDate(), h: 0, min: 0 };\n  }\n  function me(t, e) {\n    let r = t.y * 12 + t.m - 1 + e;\n    return { y: Math.floor(r / 12), m: r % 12 + 1, d: 1, h: 0, min: 0 };\n  }\n  function Le(t) {\n    let e = Ie(t.y, t.m, t.d).getUTCDay();\n    return e === 0 ? 7 : e;\n  }\n  function it(t) {\n    let e = Ie(t.y, t.m, t.d);\n    e.setUTCDate(e.getUTCDate() + 4 - (e.getUTCDay() || 7));\n    let r = e.getUTCFullYear(), o = Date.UTC(r, 0, 1), s = Math.ceil(((e.getTime() - o) / 864e5 + 1) / 7);\n    return { year: r, week: s };\n  }\n  var nt = [\"jan\", \"feb\", \"mar\", \"apr\", \"maj\", \"jun\", \"jul\", \"aug\", \"sep\", \"okt\", \"nov\", \"dec\"];\n  function B(t) {\n    return t < 10 ? \"0\" + t : String(t);\n  }\n  function lt(t, e) {\n    switch (e) {\n      case \"hour\":\n        return t.y + \"-\" + B(t.m) + \"-\" + B(t.d) + \" \" + B(t.h);\n      case \"day\":\n        return t.y + \"-\" + B(t.m) + \"-\" + B(t.d);\n      case \"week\": {\n        let r = it(t);\n        return r.year + \"-V\" + B(r.week);\n      }\n      case \"month\":\n        return t.y + \"-\" + B(t.m);\n    }\n  }\n  function ut(t, e) {\n    return lt(U(new Date(t)), e);\n  }\n  function or(t, e) {\n    switch (e) {\n      case \"hour\":\n        return t.d + \"/\" + t.m + \" \" + B(t.h) + \":00\";\n      case \"day\":\n        return t.d + \" \" + nt[t.m - 1];\n      case \"week\":\n        return \"v. \" + it(t).week;\n      case \"month\":\n        return nt[t.m - 1] + \" \" + t.y;\n    }\n  }\n  function dt(t, e, r) {\n    let o = [], s = /* @__PURE__ */ new Set(), a = U(t);\n    (r === \"day\" || r === \"week\" || r === \"month\") && (a = { ...a, h: 0, min: 0 }), r === \"week\" && (a = L(a, 1 - Le(a))), r === \"month\" && (a = { ...a, d: 1 }), r === \"hour\" && (a = { ...a, min: 0 });\n    let l = 1e3;\n    for (; o.length < l; ) {\n      let i = at(a.y, a.m, a.d, a.h, a.min);\n      if (i >= e) break;\n      let u = lt(a, r);\n      switch (s.has(u) || (s.add(u), (i >= t || o.length === 0) && o.push({ key: u, label: or(a, r), startUtc: i })), r) {\n        case \"hour\": {\n          let d = new Date(i.getTime() + 36e5);\n          a = U(d), a = { ...a, min: 0 };\n          break;\n        }\n        case \"day\":\n          a = L(a, 1);\n          break;\n        case \"week\":\n          a = L(a, 7);\n          break;\n        case \"month\":\n          a = me(a, 1);\n          break;\n      }\n    }\n    return o;\n  }\n  var ct = [\n    { id: \"today\", label: n.rangeToday },\n    { id: \"yesterday\", label: n.rangeYesterday },\n    { id: \"thisWeek\", label: n.rangeThisWeek },\n    { id: \"lastWeek\", label: n.rangeLastWeek },\n    { id: \"thisMonth\", label: n.rangeThisMonth },\n    { id: \"lastMonth\", label: n.rangeLastMonth },\n    { id: \"last7\", label: n.rangeLast7 },\n    { id: \"last30\", label: n.rangeLast30 },\n    { id: \"thisYear\", label: n.rangeThisYear },\n    { id: \"custom\", label: n.rangeCustom }\n  ];\n  function st(t) {\n    let e = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(t);\n    if (!e) throw new Error(\"Ogiltigt datum: \" + t);\n    return { y: Number(e[1]), m: Number(e[2]), d: Number(e[3]), h: 0, min: 0 };\n  }\n  function pe(t, e = /* @__PURE__ */ new Date()) {\n    var s, a, l;\n    let r = { ...U(e), h: 0, min: 0 }, o = (i) => at(i.y, i.m, i.d);\n    switch (t.preset) {\n      case \"today\":\n        return { from: o(r), to: o(L(r, 1)) };\n      case \"yesterday\":\n        return { from: o(L(r, -1)), to: o(r) };\n      case \"thisWeek\": {\n        let i = L(r, 1 - Le(r));\n        return { from: o(i), to: o(L(i, 7)) };\n      }\n      case \"lastWeek\": {\n        let i = L(r, 1 - Le(r) - 7);\n        return { from: o(i), to: o(L(i, 7)) };\n      }\n      case \"thisMonth\": {\n        let i = { ...r, d: 1 };\n        return { from: o(i), to: o(me(i, 1)) };\n      }\n      case \"lastMonth\": {\n        let i = me({ ...r, d: 1 }, -1);\n        return { from: o(i), to: o(me(i, 1)) };\n      }\n      case \"last7\":\n        return { from: o(L(r, -6)), to: o(L(r, 1)) };\n      case \"last30\":\n        return { from: o(L(r, -29)), to: o(L(r, 1)) };\n      case \"thisYear\": {\n        let i = { y: r.y, m: 1, d: 1, h: 0, min: 0 };\n        return { from: o(i), to: o({ y: r.y + 1, m: 1, d: 1, h: 0, min: 0 }) };\n      }\n      case \"custom\": {\n        let i = st((s = t.from) != null ? s : \"\"), u = L(st((l = (a = t.to) != null ? a : t.from) != null ? l : \"\"), 1);\n        return { from: o(i), to: o(u) };\n      }\n      default:\n        throw new Error(\"Ok\\xE4nd period: \" + t.preset);\n    }\n  }\n  function mt(t, e) {\n    let r = (e.getTime() - t.getTime()) / 864e5;\n    return r <= 2 ? \"hour\" : r <= 62 ? \"day\" : r <= 200 ? \"week\" : \"month\";\n  }\n\n  // src/ui/dom.ts\n  function c(t, e, ...r) {\n    let o = document.createElement(t);\n    e && (e.class && (o.className = e.class), e.style && Object.assign(o.style, e.style), e.text != null && (o.textContent = e.text), e.title && (o.title = e.title), e.type && (o.type = e.type), e.value != null && (o.value = e.value), e.placeholder && (o.placeholder = e.placeholder), e.disabled && (o.disabled = !0), e.checked != null && (o.checked = e.checked), e.selected != null && (o.selected = e.selected), e.min != null && (o.min = e.min), e.max != null && (o.max = e.max), e.colspan != null && (o.colSpan = e.colspan), e.onClick && o.addEventListener(\"click\", e.onClick), e.onChange && o.addEventListener(\"change\", e.onChange), e.onInput && o.addEventListener(\"input\", e.onInput));\n    for (let s of r)\n      s != null && o.appendChild(typeof s == \"string\" ? document.createTextNode(s) : s);\n    return o;\n  }\n  function T(t) {\n    for (; t.firstChild; ) t.removeChild(t.firstChild);\n  }\n  function P(t, e, r) {\n    let o = c(\"option\", { text: e, selected: !!r });\n    return o.value = t, o;\n  }\n  function Pe(t) {\n    let e = new Set(t.selected), r = c(\"button\", { class: \"s2rv-msel-button\", type: \"button\" }), o = c(\"div\", { class: \"s2rv-msel-list\" }), s = c(\"div\", { class: \"s2rv-msel\" }, r, o);\n    function a() {\n      var i, u;\n      r.textContent = e.size === 0 ? t.allLabel : e.size === 1 ? (u = (i = t.items.find((d) => e.has(d.id))) == null ? void 0 : i.label) != null ? u : \"1 \" + t.selectedSuffix : e.size + \" \" + t.selectedSuffix;\n    }\n    for (let i of t.items) {\n      let u = c(\"input\", {\n        type: \"checkbox\",\n        checked: e.has(i.id),\n        onChange: () => {\n          u.checked ? e.add(i.id) : e.delete(i.id), a(), t.onChange([...e]);\n        }\n      });\n      o.appendChild(c(\"label\", { class: \"s2rv-msel-item\" }, u, c(\"span\", { text: i.label })));\n    }\n    r.addEventListener(\"click\", (i) => {\n      i.stopPropagation(), s.classList.toggle(\"s2rv-msel-open\");\n    });\n    let l = (i) => {\n      s.contains(i.target) || s.classList.remove(\"s2rv-msel-open\");\n    };\n    return document.addEventListener(\"click\", l), s.__s2rvDispose = () => document.removeEventListener(\"click\", l), a(), s;\n  }\n  function G(t) {\n    var e;\n    (e = t.__s2rvDispose) == null || e.call(t), t.querySelectorAll(\"*\").forEach((r) => {\n      var o;\n      return (o = r.__s2rvDispose) == null ? void 0 : o.call(r);\n    });\n  }\n  function pt(t, e) {\n    let r = URL.createObjectURL(e), o = c(\"a\");\n    o.href = r, o.download = t, document.body.appendChild(o), o.click(), o.remove(), setTimeout(() => URL.revokeObjectURL(r), 1e4);\n  }\n\n  // src/ui/dateRange.ts\n  function Me() {\n    let t = U(/* @__PURE__ */ new Date()), e = (r) => r < 10 ? \"0\" + r : String(r);\n    return t.y + \"-\" + e(t.m) + \"-\" + e(t.d);\n  }\n  function fe(t, e) {\n    var d, m;\n    let r = { ...t }, o = c(\"input\", { type: \"date\", value: (d = r.from) != null ? d : Me(), onChange: () => u() }), s = c(\"input\", { type: \"date\", value: (m = r.to) != null ? m : Me(), onChange: () => u() }), a = c(\n      \"span\",\n      { class: \"s2rv-range-custom\" },\n      c(\"label\", { text: n.rangeFrom + \" \" }, o),\n      c(\"label\", { text: \" \" + n.rangeTo + \" \" }, s)\n    ), l = c(\"select\", {\n      onChange: () => {\n        r.preset = l.value, i(), u();\n      }\n    }, ...ct.map((y) => P(y.id, y.label, y.id === r.preset)));\n    function i() {\n      a.style.display = r.preset === \"custom\" ? \"\" : \"none\";\n    }\n    function u() {\n      r.preset === \"custom\" ? (r.from = o.value || Me(), r.to = s.value || r.from) : (delete r.from, delete r.to), e({ ...r });\n    }\n    return i(), c(\"span\", { class: \"s2rv-range\" }, l, a);\n  }\n\n  // src/api/paginate.ts\n  var ge = 100, nr = 3, sr = 2e4, Oe = 1e5, W = 25e4;\n  async function be(t) {\n    var g, b;\n    let { signal: e } = t, r = await t.count(e), o = (g = t.maxRows) != null ? g : Oe;\n    if ((b = t.onProgress) == null || b.call(t, { total: r, fetched: 0 }), r > W || r > o)\n      return { status: \"tooLarge\", total: r, limit: Math.min(o, W) };\n    if (r > sr && t.confirmLarge && !await t.confirmLarge(r))\n      return { status: \"declined\" };\n    let s = [];\n    for (let v = 0; v < r; v += ge) s.push(v);\n    let a = !t.onPage, l = [], i = 0, u = 0, d = null;\n    async function m() {\n      var v;\n      for (; u < s.length && !e.aborted && !d; ) {\n        if (Date.now() < De.until && u > 0 && (await new Promise((p) => setTimeout(p, 500)), e.aborted || d))\n          return;\n        let S = s[u++];\n        try {\n          let p = await t.page(S, ge, e);\n          if (i += p.length, a ? l.push(...p) : t.onPage(p), (v = t.onProgress) == null || v.call(t, { total: r, fetched: i }), p.length < ge && S + ge < r) return;\n        } catch (p) {\n          if ((p == null ? void 0 : p.name) === \"AbortError\") return;\n          d = p;\n        }\n      }\n    }\n    let y = Math.min(nr, s.length) || 1;\n    if (await Promise.all(Array.from({ length: y }, () => m())), e.aborted) return { status: \"cancelled\" };\n    if (d) throw d;\n    return { status: \"ok\", rows: l, total: r };\n  }\n\n  // src/format.ts\n  var ft = new Intl.NumberFormat(\"sv-SE\", { maximumFractionDigits: 0 }), ar = new Intl.NumberFormat(\"sv-SE\", { style: \"percent\", maximumFractionDigits: 1 });\n  function ir(t) {\n    let e = Math.round(Number(t || 0) / 1e3), r = Math.floor(e / 3600), o = Math.floor(e % 3600 / 60), s = e % 60;\n    return r > 0 ? r + \" t \" + o + \" min\" : o > 0 ? o + \" min \" + s + \" s\" : s + \" s\";\n  }\n  function q(t, e) {\n    let r = Number.isFinite(t) ? t : 0;\n    switch (e) {\n      case \"int\":\n        return ft.format(Math.round(r));\n      case \"currency\":\n        return ft.format(Math.round(r)) + \" kr\";\n      case \"percent\":\n        return ar.format(r);\n      case \"duration\":\n        return ir(r);\n    }\n  }\n  function gt(t, e) {\n    let r = Number.isFinite(t) ? t : 0;\n    return e === \"duration\" ? Math.round(r / 1e3) : r;\n  }\n\n  // src/charts/loader.ts\n  var lr = \"https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js\", ur = \"https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js\";\n  function bt(t, e) {\n    let r = window;\n    if (r[e]) return Promise.resolve(r[e]);\n    let o = r.__s2rvLibLoads || (r.__s2rvLibLoads = {});\n    return o[t] || (o[t] = new Promise((s, a) => {\n      let l = document.createElement(\"script\");\n      l.src = t, l.async = !0, l.onload = () => s(), l.onerror = () => {\n        delete o[t], l.remove(), a(new Error(n.cdnError));\n      }, document.head.appendChild(l);\n    })), o[t].then(() => {\n      if (!r[e]) throw new Error(n.cdnError);\n      return r[e];\n    });\n  }\n  function vt() {\n    return bt(lr, \"Chart\");\n  }\n  function Fe() {\n    return bt(ur, \"XLSX\");\n  }\n\n  // src/charts/render.ts\n  var J = [\n    \"#4e79a7\",\n    \"#f28e2b\",\n    \"#59a14f\",\n    \"#e15759\",\n    \"#76b7b2\",\n    \"#edc948\",\n    \"#b07aa1\",\n    \"#ff9da7\",\n    \"#9c755f\",\n    \"#bab0ac\",\n    \"#86bcb6\",\n    \"#d37295\"\n  ], _e = 10;\n  function dr(t) {\n    let e = getComputedStyle(t), r = e.getPropertyValue(\"--s2rv-text-2\").trim() || \"#616161\", o = e.getPropertyValue(\"--s2rv-text-1-rgb\").trim() || \"33, 33, 33\";\n    return { text: r, grid: \"rgba(\" + o + \", 0.1)\" };\n  }\n  function ee(t, e, r, o) {\n    var s, a;\n    if (r == null) return n.noValue;\n    if (e === \"time\") {\n      let l = (s = t.buckets) == null ? void 0 : s.find((i) => i.key === r);\n      return (a = l == null ? void 0 : l.label) != null ? a : r;\n    }\n    return E(o, r);\n  }\n  function Ne(t) {\n    var r, o, s;\n    let e = {};\n    for (let a of t.rows)\n      for (let l of Object.keys(a.values)) e[l] = ((r = e[l]) != null ? r : 0) + a.values[l];\n    for (let a of t.output) {\n      if (!a.rate) continue;\n      let l = (o = e[a.rate[1]]) != null ? o : 0;\n      e[a.id] = l > 0 ? ((s = e[a.rate[0]]) != null ? s : 0) / l : 0;\n    }\n    return e;\n  }\n  function cr(t, e) {\n    var d, m, y, g, b;\n    let [r, o] = t.dimSpecs, s = t.output;\n    if (!r) {\n      let v = Ne(t);\n      return {\n        labels: [n.total],\n        series: s.map((S) => {\n          var p;\n          return { label: S.label, values: [(p = v[S.id]) != null ? p : 0], metric: S };\n        })\n      };\n    }\n    let a = [], l = /* @__PURE__ */ new Set();\n    for (let v of t.rows) {\n      let S = (d = v.dims[r.id]) != null ? d : null, p = JSON.stringify(S);\n      l.has(p) || (l.add(p), a.push(S));\n    }\n    let i = a.map((v) => ee(t, r.id, v, e));\n    if (o) {\n      let v = s[0], S = [], p = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Map();\n      for (let w of t.rows) {\n        let k = (m = w.dims[o.id]) != null ? m : null, x = JSON.stringify(k);\n        p.has(x) || (p.add(x), S.push(k)), h.set(JSON.stringify([(y = w.dims[r.id]) != null ? y : null, k]), (g = w.values[v.id]) != null ? g : 0);\n      }\n      return {\n        labels: i,\n        series: S.map((w) => ({\n          label: ee(t, o.id, w, e),\n          values: a.map((k) => {\n            var x;\n            return (x = h.get(JSON.stringify([k, w]))) != null ? x : 0;\n          }),\n          metric: v\n        }))\n      };\n    }\n    let u = /* @__PURE__ */ new Map();\n    for (let v of t.rows) u.set(JSON.stringify((b = v.dims[r.id]) != null ? b : null), v.values);\n    return {\n      labels: i,\n      series: s.map((v) => ({\n        label: v.label,\n        values: a.map((S) => {\n          var p, h;\n          return (h = (p = u.get(JSON.stringify(S))) == null ? void 0 : p[v.id]) != null ? h : 0;\n        }),\n        metric: v\n      }))\n    };\n  }\n  async function ve(t, e, r, o) {\n    if (T(t), !e.rows.length)\n      return t.appendChild(c(\"div\", { class: \"s2rv-empty\", text: n.emptyResult })), { destroy: () => T(t) };\n    switch (r.type) {\n      case \"table\":\n        return mr(t, e, o);\n      case \"kpi\":\n        return pr(t, e);\n      default:\n        return fr(t, e, r, o);\n    }\n  }\n  function mr(t, e, r) {\n    let o = {\n      user: n.user,\n      team: n.team,\n      project: n.project,\n      dialGroup: n.dialGroup,\n      tag: n.tag,\n      status: n.status,\n      template: n.template,\n      time: n.time\n    }, s = c(\n      \"tr\",\n      null,\n      ...e.dimSpecs.map((l) => {\n        var i;\n        return c(\"th\", { text: (i = o[l.id]) != null ? i : l.id });\n      }),\n      ...e.output.map((l) => c(\"th\", { class: \"s2rv-num\", text: l.label }))\n    ), a = c(\"tbody\", null, ...e.rows.map((l) => c(\n      \"tr\",\n      null,\n      ...e.dimSpecs.map((i) => {\n        var u;\n        return c(\"td\", { text: ee(e, i.id, (u = l.dims[i.id]) != null ? u : null, r) });\n      }),\n      ...e.output.map((i) => {\n        var u;\n        return c(\"td\", { class: \"s2rv-num\", text: q((u = l.values[i.id]) != null ? u : 0, i.format) });\n      })\n    )));\n    if (e.rows.length > 1 && e.dimSpecs.length > 0) {\n      let l = Ne(e);\n      a.appendChild(c(\n        \"tr\",\n        { class: \"s2rv-total-row\" },\n        c(\"td\", { text: n.total, colspan: e.dimSpecs.length }),\n        ...e.output.map((i) => {\n          var u;\n          return c(\"td\", { class: \"s2rv-num\", text: q((u = l[i.id]) != null ? u : 0, i.format) });\n        })\n      ));\n    }\n    return t.appendChild(c(\n      \"div\",\n      { class: \"s2rv-table-wrap\" },\n      c(\"table\", { class: \"s2rv-table\" }, c(\"thead\", null, s), a)\n    )), { destroy: () => T(t) };\n  }\n  function pr(t, e) {\n    let r = Ne(e);\n    return t.appendChild(c(\n      \"div\",\n      { class: \"s2rv-kpi-grid\" },\n      ...e.output.map((o) => {\n        var s;\n        return c(\n          \"div\",\n          { class: \"s2rv-kpi-card\" },\n          c(\"div\", { class: \"s2rv-kpi-value\", text: q((s = r[o.id]) != null ? s : 0, o.format) }),\n          c(\"div\", { class: \"s2rv-kpi-label\", text: o.label })\n        );\n      })\n    )), { destroy: () => T(t) };\n  }\n  async function fr(t, e, r, o) {\n    var y, g;\n    let s = await vt(), a = cr(e, o), l = (g = (y = a.series[0]) == null ? void 0 : y.metric.format) != null ? g : \"int\", i = c(\"canvas\");\n    t.appendChild(c(\"div\", { class: \"s2rv-chart\" }, i));\n    let u = dr(t), d;\n    if (r.type === \"pie\") {\n      let b = a.series[0], v = a.labels, S = b ? [...b.values] : [];\n      if (v.length > _e) {\n        let p = v.map((k, x) => ({ label: k, value: S[x] }));\n        p.sort((k, x) => x.value - k.value);\n        let h = p.slice(0, _e - 1), w = p.slice(_e - 1).reduce((k, x) => k + x.value, 0);\n        v = [...h.map((k) => k.label), n.others], S = [...h.map((k) => k.value), w];\n      }\n      d = {\n        type: \"pie\",\n        data: {\n          labels: v,\n          datasets: [{ data: S, backgroundColor: v.map((p, h) => J[h % J.length]) }]\n        },\n        options: {\n          responsive: !0,\n          maintainAspectRatio: !1,\n          plugins: {\n            legend: { position: \"right\", labels: { color: u.text } },\n            tooltip: { callbacks: { label: (p) => {\n              var h;\n              return ((h = p.label) != null ? h : \"\") + \": \" + q(p.parsed, l);\n            } } }\n          }\n        }\n      };\n    } else\n      d = {\n        type: r.type === \"line\" ? \"line\" : \"bar\",\n        data: {\n          labels: a.labels,\n          datasets: a.series.map((b, v) => ({\n            label: b.label,\n            data: b.values,\n            backgroundColor: J[v % J.length],\n            borderColor: J[v % J.length],\n            fill: !1,\n            tension: 0.2\n          }))\n        },\n        options: {\n          responsive: !0,\n          maintainAspectRatio: !1,\n          scales: {\n            x: { stacked: !!r.stacked, ticks: { color: u.text }, grid: { color: u.grid } },\n            y: {\n              stacked: !!r.stacked,\n              beginAtZero: !0,\n              ticks: {\n                color: u.text,\n                ...l === \"int\" ? { precision: 0 } : {},\n                callback: (b) => q(Number(b), l)\n              },\n              grid: { color: u.grid }\n            }\n          },\n          plugins: {\n            legend: { display: a.series.length > 1, labels: { color: u.text } },\n            tooltip: {\n              callbacks: {\n                label: (b) => {\n                  var v, S;\n                  return ((v = b.dataset.label) != null ? v : \"\") + \": \" + q(b.parsed.y, (S = b.dataset.__format) != null ? S : l);\n                }\n              }\n            }\n          }\n        }\n      }, d.data.datasets.forEach((b, v) => {\n        var S, p;\n        b.__format = (p = (S = a.series[v]) == null ? void 0 : S.metric.format) != null ? p : l;\n      });\n    let m = new s(i.getContext(\"2d\"), d);\n    return {\n      destroy: () => {\n        m.destroy(), T(t);\n      }\n    };\n  }\n\n  // src/api/endpoints.ts\n  function je(t) {\n    return {\n      userIds: t.userIds,\n      teamIds: t.teamIds,\n      projectIds: t.projectIds\n    };\n  }\n  function yt(t) {\n    return {\n      ...je(t),\n      after: t.from,\n      before: t.to,\n      dialGroupIds: t.dialGroupIds,\n      anyTagId: t.tagIds\n    };\n  }\n  function ht(t, e) {\n    return D(\"/api/dial/calls-v1/count\", yt(t), e).then((r) => r.count);\n  }\n  function xt(t, e, r, o) {\n    return D(\"/api/dial/calls-v1\", {\n      ...yt(t),\n      offset: e,\n      count: r,\n      sortBy: \"date\",\n      sortOrder: \"asc\"\n    }, o).then((s) => {\n      var a;\n      return Array.isArray(s) ? s : (a = s == null ? void 0 : s.calls) != null ? a : [];\n    });\n  }\n  var kt = 4e12;\n  function wt(t, e, r) {\n    return D(\"/api/dial/statistics-v1/own/issue_1238_2\", {\n      from: new Date(t).toISOString(),\n      to: new Date(e - 1).toISOString(),\n      fixedIntervalMillis: kt\n    }, r);\n  }\n  function Be(t) {\n    return {\n      ...je(t),\n      from: t.from,\n      to: t.to,\n      anyTagId: t.tagIds,\n      isTest: \"false\"\n    };\n  }\n  function Ge(t, e) {\n    return D(\"/api/orders/orders-v2/count\", Be(t), e).then((r) => r.count);\n  }\n  function St(t, e) {\n    return D(\"/api/orders/orders-v2/sum\", Be(t), e).then((r) => {\n      var o, s;\n      return (s = (o = r.sum) != null ? o : r.total) != null ? s : 0;\n    });\n  }\n  function Ct(t, e, r, o) {\n    return D(\"/api/orders/orders-v3\", {\n      ...Be(t),\n      offset: e,\n      count: r,\n      sortBy: \"businessDate\",\n      sortOrder: \"asc\"\n    }, o);\n  }\n  function Rt(t, e, r) {\n    return D(\"/api/orders/statistics-v1/own/issue_1238_user_tag\", {\n      from: new Date(t - 1).toISOString(),\n      to: new Date(e).toISOString(),\n      fixedIntervalMillis: kt\n    }, r);\n  }\n  function Dt(t) {\n    return {\n      ...je(t),\n      from: t.from,\n      to: t.to,\n      statuses: t.statuses,\n      templateIds: t.templateIds,\n      isTest: \"false\"\n    };\n  }\n  function te(t, e) {\n    return D(\"/api/offers/offers-v1/count\", Dt(t), e).then((r) => r.count);\n  }\n  function Tt(t, e, r, o) {\n    return D(\"/api/offers/offers-v2\", {\n      ...Dt(t),\n      offset: e,\n      count: r,\n      sortBy: \"date\",\n      sortOrder: \"asc\"\n    }, o);\n  }\n  function At(t, e, r) {\n    return D(\"/api/users/statistics-v1/own/issue_1295_active_user_time\", {\n      from: new Date(t - 1).toISOString(),\n      to: new Date(e).toISOString()\n    }, r);\n  }\n  function gr(t, e, r, o) {\n    return D(\"/api/dial/dial-groups-v2\", {\n      teamIds: t.teamIds,\n      projectIds: t.projectIds,\n      offset: e,\n      count: r\n    }, o);\n  }\n  async function ye(t, e) {\n    let r = [];\n    for (let o = 0; ; o += 100) {\n      let s = await gr(t, o, 100, e);\n      if (r.push(...s), s.length < 100 || o > 5e3) break;\n    }\n    return r;\n  }\n\n  // src/engine/aggregate.ts\n  function Et(t, e) {\n    let r = new Map(t.map((l) => [l.id, l])), o = [], s = /* @__PURE__ */ new Set();\n    for (let l of e) {\n      let i = r.get(l);\n      i && (o.push(i), i.rate ? (s.add(i.rate[0]), s.add(i.rate[1])) : s.add(i.id));\n    }\n    return { compute: [...s].map((l) => r.get(l)).filter((l) => !!(l && !l.rate)), output: o };\n  }\n  function ze(t) {\n    if (t.agg === \"sum\" && t.value) {\n      let { value: r } = t;\n      return { id: t.id, add: (o) => {\n        let s = r(o);\n        return Number.isFinite(s) ? s : 0;\n      } };\n    }\n    let e = t.predicate;\n    return { id: t.id, add: (r) => e ? e(r) ? 1 : 0 : 1 };\n  }\n  function he(t, e, r) {\n    let o = /* @__PURE__ */ new Map();\n    for (let s of t) {\n      let a = [[]];\n      for (let l of e) {\n        let i = l.keys(s), u = [];\n        for (let d of a)\n          for (let m of i) u.push([...d, m]);\n        a = u;\n      }\n      for (let l of a) {\n        let i = JSON.stringify(l), u = o.get(i);\n        if (!u) {\n          let d = {};\n          e.forEach((m, y) => {\n            d[m.id] = l[y];\n          }), u = { dims: d, values: {} };\n          for (let m of r) u.values[m.id] = 0;\n          o.set(i, u);\n        }\n        for (let d of r) u.values[d.id] += d.add(s);\n      }\n    }\n    return [...o.values()];\n  }\n  function Lt(t, e) {\n    var r, o;\n    for (let s of e) {\n      if (!s.rate) continue;\n      let [a, l] = s.rate;\n      for (let i of t) {\n        let u = (r = i.values[l]) != null ? r : 0;\n        i.values[s.id] = u > 0 ? ((o = i.values[a]) != null ? o : 0) / u : 0;\n      }\n    }\n  }\n  function It(t, e) {\n    if (!t) return [null];\n    let r = e.userTeams.get(t);\n    return r && r.length ? r : [null];\n  }\n\n  // src/engine/planner.ts\n  var br = /* @__PURE__ */ new Set([\"user\", \"team\", \"tag\"]), vr = /* @__PURE__ */ new Set([\"user\", \"team\", \"tag\", \"time\"]);\n  function yr(t, e) {\n    let r = new Map(t.metrics.map((s) => [s.id, s])), o = (s) => !!(s != null && s.statsField);\n    return e.every((s) => {\n      let a = r.get(s);\n      return a ? a.rate ? o(r.get(a.rate[0])) && o(r.get(a.rate[1])) : o(a) : !1;\n    });\n  }\n  function hr(t, e) {\n    return e.dimensions.every((r) => {\n      if (r.id === \"team\") return !0;\n      if (!vr.has(r.id)) return !1;\n      let o = t.dimensions.find((s) => s.id === r.id);\n      return !!(o != null && o.statsSupported);\n    });\n  }\n  function xr(t) {\n    return t.filters.every((e) => !e.values.length || br.has(e.dimension));\n  }\n  function Pt(t, e) {\n    if (e.statsOnly) return { mode: \"stats\", fast: !0 };\n    if (e.id === \"dialGroups\") return { mode: \"dialGroupList\", fast: !0 };\n    if (e.id === \"offers\") {\n      let r = t.metrics.every((s) => s === \"count\" || s === \"signedCount\" || s === \"signRate\"), o = t.dimensions.every((s) => s.id === \"status\");\n      return r && o ? { mode: \"offerStatusCounts\", fast: !0 } : { mode: \"rows\", fast: !1 };\n    }\n    return yr(e, t.metrics) && hr(e, t) && xr(t) ? { mode: \"stats\", fast: !0 } : e.id === \"orders\" && t.dimensions.length === 0 && t.metrics.every((r) => r === \"count\" || r === \"sumAmount\" || r === \"avgAmount\") ? { mode: \"orderAggregates\", fast: !0 } : { mode: \"rows\", fast: !1 };\n  }\n\n  // src/engine/rowCache.ts\n  var z = /* @__PURE__ */ new Map();\n  function Mt(t, e, r, o) {\n    return JSON.stringify([t, e, r, o]);\n  }\n  function Ot(t, e) {\n    let r = z.get(t);\n    return !r || e && Date.now() - r.fetchedAt > 6e4 ? null : (z.delete(t), z.set(t, r), r);\n  }\n  function Ft(t, e, r) {\n    for (z.delete(t), z.set(t, { rows: e, fetchedAt: Date.now(), total: r }); z.size > 3; ) {\n      let o = z.keys().next().value;\n      if (o == null) break;\n      z.delete(o);\n    }\n  }\n\n  // src/engine/execute.ts\n  async function X(t, e) {\n    try {\n      return await kr(t, e);\n    } catch (r) {\n      if ((r == null ? void 0 : r.name) === \"AbortError\" || e.signal.aborted) return { status: \"cancelled\" };\n      throw r;\n    }\n  }\n  async function kr(t, e) {\n    var p;\n    let r = M(t.source), o = Pt(t, r), { from: s, to: a } = pe(t.range), l = s.toISOString(), i = a.toISOString(), u = t.dimensions.find((h) => h.id === \"time\"), d = u ? (p = u.grain) != null ? p : mt(s, a) : null, m = d ? dt(s, a, d) : null, { compute: y, output: g } = Et(r.metrics, t.metrics), b = Ve(t, r, l, i), v, S = null;\n    switch (o.mode) {\n      case \"stats\":\n        v = await wr(t, r, y, b, l, i, m, e);\n        break;\n      case \"offerStatusCounts\":\n        v = await Rr(t, y, b, e);\n        break;\n      case \"orderAggregates\":\n        v = await Dr(y, b, e);\n        break;\n      case \"dialGroupList\": {\n        let h = await ye(b, e.signal);\n        v = he(h, _t(t, r, d, e.lookups), y.map(ze)), S = h.length;\n        break;\n      }\n      case \"rows\": {\n        let h = await Tr(t, r, b, l, i, e);\n        if (h.status !== \"ok\") return h;\n        v = he(h.rows, _t(t, r, d, e.lookups), y.map(ze)), S = h.total;\n        break;\n      }\n    }\n    return m && Ar(v, t.dimensions, m, y.map((h) => h.id)), Lt(v, g), Er(v, t.dimensions, m, g), {\n      status: \"ok\",\n      result: { query: t, plan: o, rows: v, output: g, dimSpecs: t.dimensions, buckets: m, grain: d, total: S }\n    };\n  }\n  function Ve(t, e, r, o) {\n    let s = { from: r, to: o };\n    for (let a of t.filters) {\n      if (!a.values.length) continue;\n      let l = e.filters.find((i) => i.id === a.dimension);\n      l && (s[l.target] = a.values);\n    }\n    return s;\n  }\n  async function wr(t, e, r, o, s, a, l, i) {\n    var k;\n    let u = Date.parse(s), d = Date.parse(a), m = l ? l.map((x, f) => ({\n      key: x.key,\n      fromMs: Math.max(x.startUtc.getTime(), u),\n      toMs: Math.min(f + 1 < l.length ? l[f + 1].startUtc.getTime() : d, d)\n    })) : [{ key: null, fromMs: u, toMs: d }], y = (x, f) => {\n      switch (e.id) {\n        case \"calls\":\n          return wt(x, f, i.signal);\n        case \"orders\":\n          return Rt(x, f, i.signal);\n        case \"time\":\n          return At(x, f, i.signal);\n        default:\n          throw new Error(\"Snabbstatistik saknas f\\xF6r: \" + e.id);\n      }\n    }, g = new Array(m.length), b = 0, v = 0;\n    m.length > 1 && ((k = i.onProgress) == null || k.call(i, { total: m.length, fetched: 0 })), await Promise.all(Array.from({ length: Math.min(3, m.length) }, async () => {\n      var x;\n      for (; b < m.length; ) {\n        let f = b++, C = m[f], A = await y(C.fromMs, C.toMs);\n        g[f] = C.key === null ? A : A.map((K) => ({ ...K, __bucketKey: C.key })), v += 1, m.length > 1 && ((x = i.onProgress) == null || x.call(i, { total: m.length, fetched: v }));\n      }\n    }));\n    let S = Sr(o, i.lookups), p = g.flat().filter(S), h = t.dimensions.map((x) => Cr(x, i.lookups)), w = r.map((x) => {\n      let f = x.statsField;\n      return { id: x.id, add: (C) => {\n        var A;\n        return Number((A = C[f]) != null ? A : 0);\n      } };\n    });\n    return he(p, h, w);\n  }\n  function Sr(t, e) {\n    var a, l, i;\n    let r = (a = t.userIds) != null && a.length ? new Set(t.userIds) : null, o = (l = t.teamIds) != null && l.length ? new Set(t.teamIds) : null, s = (i = t.tagIds) != null && i.length ? new Set(t.tagIds) : null;\n    return (u) => {\n      var d;\n      return !(r && !r.has(u.userId) || o && !((d = e.userTeams.get(u.userId)) != null ? d : []).some((y) => o.has(y)) || s && !(u.tagId && s.has(u.tagId)));\n    };\n  }\n  function Cr(t, e) {\n    switch (t.id) {\n      case \"user\":\n        return { id: \"user\", keys: (r) => {\n          var o;\n          return [(o = r.userId) != null ? o : null];\n        } };\n      case \"team\":\n        return { id: \"team\", keys: (r) => It(r.userId, e) };\n      case \"tag\":\n        return { id: \"tag\", keys: (r) => {\n          var o;\n          return [(o = r.tagId) != null ? o : null];\n        } };\n      case \"time\":\n        return { id: \"time\", keys: (r) => {\n          var o;\n          return [(o = r.__bucketKey) != null ? o : null];\n        } };\n      default:\n        throw new Error(\"Dimension st\\xF6ds ej i snabbstatistik: \" + t.id);\n    }\n  }\n  async function Rr(t, e, r, o) {\n    var m;\n    let s = new Set(e.map((y) => y.id)), a = (m = t.filters.find((y) => y.dimension === \"status\")) == null ? void 0 : m.values;\n    if (t.dimensions.some((y) => y.id === \"status\")) {\n      let y = a != null && a.length ? a : Te.map((b) => b.id), g = await Promise.all(y.map((b) => te({ ...r, statuses: [b] }, o.signal)));\n      return y.map((b, v) => ({\n        dims: { status: b },\n        values: { count: g[v], signedCount: b === \"signed\" ? g[v] : 0 }\n      }));\n    }\n    let i = !(a != null && a.length) || a.includes(\"signed\"), [u, d] = await Promise.all([\n      s.has(\"count\") ? te(r, o.signal) : Promise.resolve(0),\n      s.has(\"signedCount\") && i ? te({ ...r, statuses: [\"signed\"] }, o.signal) : Promise.resolve(0)\n    ]);\n    return [{ dims: {}, values: { count: u, signedCount: d } }];\n  }\n  async function Dr(t, e, r) {\n    let o = new Set(t.map((l) => l.id)), [s, a] = await Promise.all([\n      o.has(\"count\") ? Ge(e, r.signal) : Promise.resolve(0),\n      o.has(\"sumAmount\") ? St(e, r.signal) : Promise.resolve(0)\n    ]);\n    return [{ dims: {}, values: { count: s, sumAmount: a } }];\n  }\n  async function Tr(t, e, r, o, s, a) {\n    let l = Mt(e.id, t.filters, o, s), i = new Date(s).getTime() > Date.now(), u = Ot(l, i);\n    if (u) return { status: \"ok\", rows: u.rows, total: u.total };\n    let d = He(e.id, r), m = await be({\n      count: d.count,\n      page: d.page,\n      signal: a.signal,\n      onProgress: a.onProgress,\n      confirmLarge: a.confirmLarge,\n      maxRows: Oe\n    });\n    return m.status === \"ok\" && Ft(l, m.rows, m.total), m;\n  }\n  function He(t, e) {\n    switch (t) {\n      case \"calls\":\n        return {\n          count: (r) => ht(e, r),\n          page: (r, o, s) => xt(e, r, o, s)\n        };\n      case \"orders\":\n        return {\n          count: (r) => Ge(e, r),\n          page: (r, o, s) => Ct(e, r, o, s)\n        };\n      case \"offers\":\n        return {\n          count: (r) => te(e, r),\n          page: (r, o, s) => Tt(e, r, o, s)\n        };\n      default:\n        throw new Error(\"Radh\\xE4mtning saknas f\\xF6r: \" + t);\n    }\n  }\n  function _t(t, e, r, o) {\n    return t.dimensions.map((s) => {\n      if (s.id === \"time\") {\n        let i = e.rowTime, u = r != null ? r : \"day\";\n        return {\n          id: \"time\",\n          keys: (d) => {\n            let m = i ? i(d) : null;\n            return [m ? ut(m, u) : null];\n          }\n        };\n      }\n      let a = e.dimensions.find((i) => i.id === s.id), l = a == null ? void 0 : a.rowKeys;\n      if (!l) throw new Error(\"Dimension saknar radnycklar: \" + s.id);\n      return { id: s.id, keys: (i) => l(i, o) };\n    });\n  }\n  function Ar(t, e, r, o) {\n    var u;\n    let s = e.filter((d) => d.id !== \"time\").map((d) => d.id), a = /* @__PURE__ */ new Map();\n    s.length || a.set(\"[]\", {});\n    for (let d of t) {\n      if (!s.length) break;\n      let m = {};\n      for (let y of s) m[y] = (u = d.dims[y]) != null ? u : null;\n      a.set(JSON.stringify(s.map((y) => m[y])), m);\n    }\n    let l = (d) => JSON.stringify(e.map((m) => {\n      var y;\n      return (y = d[m.id]) != null ? y : null;\n    })), i = new Set(t.map((d) => l(d.dims)));\n    for (let d of r)\n      for (let m of a.values()) {\n        let y = { ...m, time: d.key }, g = l(y);\n        if (i.has(g)) continue;\n        i.add(g);\n        let b = {};\n        for (let v of o) b[v] = 0;\n        t.push({ dims: y, values: b });\n      }\n  }\n  function Er(t, e, r, o) {\n    var i;\n    let s = e.some((u) => u.id === \"time\"), a = /* @__PURE__ */ new Map();\n    r && r.forEach((u, d) => a.set(u.key, d));\n    let l = (i = o[0]) == null ? void 0 : i.id;\n    t.sort((u, d) => {\n      var m, y, g, b, v, S;\n      if (s) {\n        let p = (y = a.get((m = u.dims.time) != null ? m : \"\")) != null ? y : Number.MAX_SAFE_INTEGER, h = (b = a.get((g = d.dims.time) != null ? g : \"\")) != null ? b : Number.MAX_SAFE_INTEGER;\n        if (p !== h) return p - h;\n      }\n      if (l) {\n        let p = ((v = d.values[l]) != null ? v : 0) - ((S = u.values[l]) != null ? S : 0);\n        if (p !== 0) return p;\n      }\n      return 0;\n    });\n  }\n\n  // src/export/xlsx.ts\n  function Lr(t) {\n    let e = String(t != null ? t : \"\");\n    return /[\";\\n\\r]/.test(e) ? '\"' + e.replace(/\"/g, '\"\"') + '\"' : e;\n  }\n  function Ir(t) {\n    let e = \"\\uFEFF\" + t.map((r) => r.map(Lr).join(\";\")).join(`\\r\n`);\n    return new Blob([e], { type: \"text/csv;charset=utf-8\" });\n  }\n  async function Pr(t) {\n    let e = await Fe(), r = e.utils.aoa_to_sheet(t), o = e.utils.book_new();\n    e.utils.book_append_sheet(o, r, \"Rapport\");\n    let s = e.write(o, { bookType: \"xlsx\", type: \"array\" });\n    return new Blob([s], { type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\" });\n  }\n  function re(t, e) {\n    let r = (t || n.appTitle).replace(/[^a-zA-Z0-9åäöÅÄÖ _-]+/g, \"\").trim() || \"rapport\", o = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);\n    return r + \" \" + o + \".\" + e;\n  }\n  async function Nt(t, e, r) {\n    let o = e === \"csv\" ? Ir(t) : await Pr(t);\n    pt(r, o);\n  }\n  async function xe(t, e, r, o) {\n    let s = M(t.query.source), a = t.dimSpecs.map((u) => {\n      var d, m;\n      return u.id === \"time\" ? n.time : (m = (d = s.dimensions.find((y) => y.id === u.id)) == null ? void 0 : d.label) != null ? m : u.id;\n    }), l = t.output.map((u) => u.format === \"duration\" ? u.label + \" (s)\" : u.label), i = [[...a, ...l]];\n    for (let u of t.rows)\n      i.push([\n        ...t.dimSpecs.map((d) => {\n          var m;\n          return ee(t, d.id, (m = u.dims[d.id]) != null ? m : null, e);\n        }),\n        ...t.output.map((d) => {\n          var m;\n          return gt((m = u.values[d.id]) != null ? m : 0, d.format);\n        })\n      ]);\n    await Nt(i, r, o);\n  }\n  async function ke(t, e, r, o) {\n    let s = M(t.source);\n    if (!s.rowColumns.length) throw new Error(\"Radexport saknas f\\xF6r: \" + s.label);\n    e === \"xlsx\" && await Fe();\n    let { from: a, to: l } = pe(t.range), i = Ve(t, s, a.toISOString(), l.toISOString()), u = [[...s.rowColumns.map((m) => m.label)]], d = (m) => {\n      for (let y of m)\n        u.push(s.rowColumns.map((g) => g.value(y, o.lookups)));\n    };\n    if (s.id === \"dialGroups\") {\n      if (d(await ye(i, o.signal)), o.signal.aborted) return { status: \"cancelled\" };\n    } else {\n      let m = He(s.id, i), y = await be({\n        count: m.count,\n        page: m.page,\n        signal: o.signal,\n        onProgress: o.onProgress,\n        confirmLarge: o.confirmLarge,\n        maxRows: W,\n        onPage: d\n      });\n      if (y.status !== \"ok\") return y;\n    }\n    return await Nt(u, e, r), { status: \"ok\", count: u.length - 1 };\n  }\n\n  // src/ui/progress.ts\n  function we(t) {\n    let e = c(\"div\", { class: \"s2rv-progress-label\" }), r = c(\"div\", { class: \"s2rv-progress-bar\" }), o = c(\"div\", { class: \"s2rv-progress-track\" }, r), s = c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.cancel, onClick: () => t() }), a = c(\"div\", { class: \"s2rv-progress\", style: { display: \"none\" } }, e, o, s);\n    return {\n      root: a,\n      show(l) {\n        a.style.display = \"\", e.textContent = l, o.classList.add(\"s2rv-progress-indeterminate\"), r.style.width = \"30%\";\n      },\n      update(l) {\n        a.style.display = \"\", o.classList.remove(\"s2rv-progress-indeterminate\");\n        let i = Math.max(l.total, 1), u = Math.min(100, Math.round(l.fetched / i * 100));\n        r.style.width = u + \"%\", e.textContent = n.fetchingRows + \": \" + l.fetched.toLocaleString(\"sv-SE\") + \" / \" + l.total.toLocaleString(\"sv-SE\") + \" (\" + u + \" %)\";\n      },\n      hide() {\n        a.style.display = \"none\", o.classList.remove(\"s2rv-progress-indeterminate\"), r.style.width = \"0%\";\n      }\n    };\n  }\n  function V(t) {\n    let e = Math.ceil(t / 100), r = n.largeWarnTitle + `\n\n` + n.largeWarn1 + \" \" + t.toLocaleString(\"sv-SE\") + \" \" + n.largeWarn2 + \" \" + e.toLocaleString(\"sv-SE\") + \" \" + n.largeWarn3;\n    return Promise.resolve(window.confirm(r));\n  }\n\n  // src/ui/resultView.ts\n  function O(t) {\n    var r;\n    return console.error(\"s2rv\", t), t instanceof N && t.status === 401 ? n.noToken : ((r = t == null ? void 0 : t.message) != null ? r : \"\") === n.cdnError ? n.cdnError : t instanceof TypeError ? n.networkError : n.loadError;\n  }\n  function jt(t) {\n    let e = null, r = null, o = null, s = \"\", a = null, l = we(() => e == null ? void 0 : e.abort()), i = c(\"div\", { class: \"s2rv-status\" }), u = c(\"div\", { class: \"s2rv-viz\" }), d = c(\"div\", { class: \"s2rv-export-row\", style: { display: \"none\" } }), m = c(\"div\", { class: \"s2rv-result\" }, i, l.root, u, d);\n    function y() {\n      r && (r.destroy(), r = null), T(u), d.style.display = \"none\", i.textContent = \"\", i.classList.remove(\"s2rv-status-error\");\n    }\n    function g(h) {\n      i.textContent = h, i.classList.add(\"s2rv-status-error\");\n    }\n    function b() {\n      if (T(d), !a) return;\n      let h = a, w = M(h.source), k = [\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportAggregated + \" (Excel)\", onClick: () => void v(\"xlsx\") }),\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportAggregated + \" (CSV)\", onClick: () => void v(\"csv\") })\n      ];\n      w.rowColumns.length && k.push(\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportRaw + \" (Excel)\", onClick: () => void S(\"xlsx\") }),\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportRaw + \" (CSV)\", onClick: () => void S(\"csv\") })\n      ), t.onSaveExport && k.push(c(\"button\", {\n        class: \"s2rv-btn\",\n        type: \"button\",\n        text: n.saveExport,\n        onClick: () => {\n          let x = window.prompt(n.exportName, s || n.unnamedReport);\n          if (!x) return;\n          let f = w.rowColumns.length && window.confirm(n.exportRaw + \"? (OK = \" + n.exportRaw + \", Avbryt = \" + n.exportAggregated + \")\") ? \"raw\" : \"aggregated\";\n          t.onSaveExport({ name: x, kind: f, query: h, format: \"xlsx\" });\n        }\n      })), d.append(...k), d.style.display = \"\";\n    }\n    async function v(h) {\n      if (o)\n        try {\n          await xe(o, t.lookups, h, re(s, h));\n        } catch (w) {\n          g(O(w));\n        }\n    }\n    async function S(h) {\n      if (a) {\n        e == null || e.abort(), e = new AbortController(), l.show(n.exporting);\n        try {\n          let w = await ke(a, h, re(s, h), {\n            lookups: t.lookups,\n            signal: e.signal,\n            onProgress: (k) => l.update(k),\n            confirmLarge: V\n          });\n          l.hide(), w.status === \"cancelled\" ? i.textContent = n.cancelled : w.status === \"tooLarge\" && g(n.tooLargeAbs);\n        } catch (w) {\n          l.hide(), e.signal.aborted || g(O(w));\n        }\n      }\n    }\n    async function p(h, w, k) {\n      e == null || e.abort(), e = new AbortController();\n      let x = e.signal;\n      s = h, a = w, o = null, y(), l.show(n.fetching);\n      try {\n        let f = await X(w, {\n          lookups: t.lookups,\n          signal: x,\n          onProgress: (C) => l.update(C),\n          confirmLarge: V\n        });\n        if (x.aborted && f.status !== \"ok\") {\n          l.hide(), i.textContent = n.cancelled;\n          return;\n        }\n        switch (l.hide(), f.status) {\n          case \"cancelled\":\n            i.textContent = n.cancelled;\n            return;\n          case \"declined\":\n            return;\n          case \"tooLarge\":\n            g(f.total > W ? n.tooLargeAbs : n.tooLargeChart);\n            return;\n          case \"ok\": {\n            o = f.result;\n            let A = [f.result.plan.fast ? n.modeStats : n.modeRows];\n            f.result.total != null && A.push(f.result.total.toLocaleString(\"sv-SE\") + \" \" + n.rows), i.textContent = A.join(\" \\xB7 \"), r = await ve(u, f.result, k, t.lookups), b();\n            return;\n          }\n        }\n      } catch (f) {\n        l.hide(), x.aborted ? i.textContent = n.cancelled : g(O(f));\n      }\n    }\n    return {\n      root: m,\n      run: p,\n      cancel() {\n        e == null || e.abort();\n      },\n      destroy() {\n        e == null || e.abort(), r && (r.destroy(), r = null);\n      }\n    };\n  }\n\n  // src/ui/builder.ts\n  function Se(t) {\n    return JSON.parse(JSON.stringify(t));\n  }\n  function Ue() {\n    return {\n      id: null,\n      name: \"\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    };\n  }\n  var Mr = [\n    { id: \"table\", label: n.vizTable },\n    { id: \"bar\", label: n.vizBar },\n    { id: \"line\", label: n.vizLine },\n    { id: \"pie\", label: n.vizPie },\n    { id: \"kpi\", label: n.vizKpi }\n  ], Or = [\n    { id: \"hour\", label: n.grainHour },\n    { id: \"day\", label: n.grainDay },\n    { id: \"week\", label: n.grainWeek },\n    { id: \"month\", label: n.grainMonth }\n  ];\n  function Bt(t) {\n    let e = Ue(), r = c(\"div\", { class: \"s2rv-report-list\" }), o = c(\"div\", { class: \"s2rv-form\" }), s = jt({ lookups: t.lookups, onSaveExport: t.onSaveExport }), a = c(\n      \"div\",\n      { class: \"s2rv-sidebar\" },\n      c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.newReport, onClick: () => {\n        e = Ue(), g();\n      } }),\n      c(\"h3\", { text: n.savedReports }),\n      r\n    ), l = c(\"div\", { class: \"s2rv-main\" }, o, s.root), i = c(\"div\", { class: \"s2rv-builder\" }, a, l);\n    function u(w) {\n      var k;\n      return (k = t.lookups[w]) != null ? k : [];\n    }\n    function d(w, ...k) {\n      return c(\"div\", { class: \"s2rv-field\" }, c(\"label\", { class: \"s2rv-field-label\", text: w }), ...k);\n    }\n    function m(w) {\n      var k, x;\n      return (x = (k = e.query.dimensions[w]) == null ? void 0 : k.id) != null ? x : \"\";\n    }\n    function y(w, k) {\n      let x = e.query.dimensions.filter((C, A) => A !== w);\n      k && x.splice(w, 0, { id: k, ...k === \"time\" ? { grain: \"day\" } : {} });\n      let f = /* @__PURE__ */ new Set();\n      e.query.dimensions = x.filter((C) => f.has(C.id) ? !1 : (f.add(C.id), !0)).slice(0, 2), g();\n    }\n    function g() {\n      G(o), T(o);\n      let w = M(e.query.source), k = c(\"input\", {\n        type: \"text\",\n        value: e.name,\n        placeholder: n.reportName,\n        onInput: () => {\n          e.name = k.value;\n        }\n      }), x = c(\"select\", {\n        onChange: () => {\n          let R = M(x.value);\n          e.query.source = R.id, e.query.metrics = [R.metrics[0].id], e.query.dimensions = [], e.query.filters = [], g();\n        }\n      }, ...Ae.map((R) => P(R.id, R.label, R.id === w.id))), f = Pe({\n        items: w.metrics.map((R) => ({ id: R.id, label: R.label })),\n        selected: e.query.metrics,\n        placeholder: n.metrics,\n        allLabel: n.metrics,\n        selectedSuffix: n.selected,\n        onChange: (R) => {\n          e.query.metrics = R;\n        }\n      }), C = (R) => [\n        P(\"\", n.none, m(R) === \"\"),\n        ...w.dimensions.filter((I) => I.id !== m(R === 0 ? 1 : 0)).map((I) => P(I.id, I.label, I.id === m(R)))\n      ], A = c(\"select\", { onChange: () => y(0, A.value) }, ...C(0)), K = c(\"select\", { onChange: () => y(1, K.value) }, ...C(1)), F = e.query.dimensions.find((R) => R.id === \"time\"), Y = null;\n      if (F) {\n        let R = c(\"select\", {\n          onChange: () => {\n            F.grain = R.value;\n          }\n        }, ...Or.map((I) => {\n          var $;\n          return P(I.id, I.label, I.id === (($ = F.grain) != null ? $ : \"day\"));\n        }));\n        Y = d(n.timeGrain, R);\n      }\n      let oe = w.filters.map((R) => {\n        var $, qe;\n        let I = (qe = ($ = e.query.filters.find((se) => se.dimension === R.id)) == null ? void 0 : $.values) != null ? qe : [];\n        return d(R.label, Pe({\n          items: u(R.lookup),\n          selected: I,\n          placeholder: R.label,\n          allLabel: n.all,\n          selectedSuffix: n.selected,\n          onChange: (se) => {\n            e.query.filters = e.query.filters.filter((Ut) => Ut.dimension !== R.id), se.length && e.query.filters.push({ dimension: R.id, values: se });\n          }\n        }));\n      }), Z = c(\"select\", {\n        onChange: () => {\n          e.viz.type = Z.value, Q.style.display = e.viz.type === \"bar\" ? \"\" : \"none\";\n        }\n      }, ...Mr.map((R) => P(R.id, R.label, R.id === e.viz.type))), _ = c(\"input\", {\n        type: \"checkbox\",\n        checked: !!e.viz.stacked,\n        onChange: () => {\n          e.viz.stacked = _.checked;\n        }\n      }), Q = c(\"label\", { class: \"s2rv-inline\" }, _, c(\"span\", { text: \" \" + n.stacked }));\n      Q.style.display = e.viz.type === \"bar\" ? \"\" : \"none\";\n      let Re = c(\n        \"div\",\n        { class: \"s2rv-form-actions\" },\n        c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.runReport, onClick: () => void b() }),\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.saveReport, onClick: v }),\n        e.id ? c(\"button\", { class: \"s2rv-btn s2rv-btn-danger\", type: \"button\", text: n.deleteReport, onClick: S }) : null\n      ), ne = (R, ...I) => c(\n        \"div\",\n        { class: \"s2rv-form-section\" },\n        c(\"h4\", { class: \"s2rv-section-title\", text: R }),\n        c(\"div\", { class: \"s2rv-form-grid\" }, ...I)\n      );\n      o.append(\n        ne(\n          n.sectionData,\n          d(n.reportName, k),\n          d(n.source, x),\n          d(n.metrics, f)\n        ),\n        ne(\n          n.sectionGrouping,\n          d(n.groupBy, A),\n          d(n.groupBySecondary, K),\n          Y\n        ),\n        ne(\n          n.sectionPeriodFilters,\n          d(n.dateRange, fe(e.query.range, (R) => {\n            e.query.range = R;\n          })),\n          ...oe\n        ),\n        ne(\n          n.sectionViz,\n          d(n.chartType, c(\"span\", null, Z, Q))\n        ),\n        Re\n      );\n    }\n    async function b() {\n      e.query.metrics.length || (e.query.metrics = [M(e.query.source).metrics[0].id]), await s.run(e.name || n.unnamedReport, Se(e.query), { ...e.viz });\n    }\n    function v() {\n      var f, C;\n      let w = Date.now(), k = e.id ? t.getStore().reports[e.id] : null, x = {\n        id: (f = e.id) != null ? f : j(\"rep\"),\n        name: e.name || n.unnamedReport,\n        query: Se(e.query),\n        viz: { ...e.viz },\n        createdAt: (C = k == null ? void 0 : k.createdAt) != null ? C : w,\n        updatedAt: w\n      };\n      e.id = x.id, t.onSaveReport(x), p();\n    }\n    function S() {\n      if (!e.id) return;\n      let w = t.getStore(), k = Object.values(w.dashboards).some((x) => x.tiles.some((f) => f.reportId === e.id));\n      window.confirm(k ? n.reportInUse : n.confirmDelete) && (t.onDeleteReport(e.id), e = Ue(), g(), p());\n    }\n    function p() {\n      T(r);\n      let w = Object.values(t.getStore().reports).sort((k, x) => x.updatedAt - k.updatedAt);\n      if (!w.length) {\n        r.appendChild(c(\"div\", { class: \"s2rv-muted\", text: n.noSavedReports })), t.onOpenTemplates && r.appendChild(c(\"button\", {\n          class: \"s2rv-btn\",\n          type: \"button\",\n          text: n.startFromTemplate,\n          onClick: t.onOpenTemplates\n        }));\n        return;\n      }\n      for (let k of w)\n        r.appendChild(c(\"button\", {\n          class: \"s2rv-list-item\" + (k.id === e.id ? \" s2rv-list-item-active\" : \"\"),\n          type: \"button\",\n          text: k.name,\n          onClick: () => h(k)\n        }));\n    }\n    function h(w) {\n      e = {\n        id: w.id,\n        name: w.name,\n        query: Se(w.query),\n        viz: { ...w.viz }\n      }, g(), p(), b();\n    }\n    return g(), p(), {\n      root: i,\n      openReport: h,\n      openQuery(w, k, x) {\n        e = { id: null, name: w, query: Se(k), viz: { ...x } }, g(), p(), b();\n      },\n      refreshList: p,\n      destroy() {\n        s.destroy(), G(i);\n      }\n    };\n  }\n\n  // src/templates.ts\n  var Ce = [\n    {\n      id: \"agent-leaderboard\",\n      name: \"Agentleaderboard\",\n      description: \"Samtal, svarade samtal och svarsfrekvens per anv\\xE4ndare denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\", \"answerRate\", \"connectedDuration\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"dial-group-report\",\n      name: \"Ringlisterapport\",\n      description: \"Kontakter, reserverade, \\xF6verhoppade och karant\\xE4n per ringlista.\",\n      query: {\n        source: \"dialGroups\",\n        metrics: [\"contactCount\", \"reservedCount\", \"skippedCount\", \"quarantinedCount\"],\n        dimensions: [{ id: \"dialGroup\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"esign-funnel\",\n      name: \"E-sign-tratt\",\n      description: \"Avtal per status \\u2014 f\\xF6lj fl\\xF6det fr\\xE5n utkast till signerat.\",\n      query: {\n        source: \"offers\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"status\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"bar\" }\n    },\n    {\n      id: \"orders-per-day\",\n      name: \"Ordrar per dag\",\n      description: \"Antal ordrar per dag de senaste 30 dagarna.\",\n      query: {\n        source: \"orders\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"time\", grain: \"day\" }],\n        filters: [],\n        range: { preset: \"last30\" }\n      },\n      viz: { type: \"line\" }\n    },\n    {\n      id: \"time-activity\",\n      name: \"Tid & aktivitet\",\n      description: \"Aktiv tid per anv\\xE4ndare denna vecka.\",\n      query: {\n        source: \"time\",\n        metrics: [\"activeDuration\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisWeek\" }\n      },\n      viz: { type: \"bar\" }\n    },\n    {\n      id: \"orders-per-user\",\n      name: \"Ordrar per anv\\xE4ndare\",\n      description: \"Antal ordrar och orderv\\xE4rde per anv\\xE4ndare denna m\\xE5nad.\",\n      query: {\n        source: \"orders\",\n        metrics: [\"count\", \"sumAmount\", \"avgAmount\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"order-value-per-month\",\n      name: \"Orderv\\xE4rde per m\\xE5nad\",\n      description: \"Orderv\\xE4rde m\\xE5nad f\\xF6r m\\xE5nad i \\xE5r.\",\n      query: {\n        source: \"orders\",\n        metrics: [\"sumAmount\"],\n        dimensions: [{ id: \"time\", grain: \"month\" }],\n        filters: [],\n        range: { preset: \"thisYear\" }\n      },\n      viz: { type: \"bar\" }\n    },\n    {\n      id: \"calls-per-day\",\n      name: \"Samtal per dag\",\n      description: \"Samtal och svarade samtal per dag de senaste 30 dagarna.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\"],\n        dimensions: [{ id: \"time\", grain: \"day\" }],\n        filters: [],\n        range: { preset: \"last30\" }\n      },\n      viz: { type: \"line\" }\n    },\n    {\n      id: \"answer-rate-per-team\",\n      name: \"Svarsfrekvens per team\",\n      description: \"Samtal, svarade och svarsfrekvens per team denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\", \"answerRate\"],\n        dimensions: [{ id: \"team\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"calls-per-dial-group\",\n      name: \"Samtal per ringlista\",\n      description: \"Samtal och svarsfrekvens per ringlista denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\", \"answerRate\"],\n        dimensions: [{ id: \"dialGroup\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"offers-per-template\",\n      name: \"Avtal per mall\",\n      description: \"Antal avtal, signerade och signeringsgrad per avtalsmall denna m\\xE5nad.\",\n      query: {\n        source: \"offers\",\n        metrics: [\"count\", \"signedCount\", \"signRate\"],\n        dimensions: [{ id: \"template\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"call-tags\",\n      name: \"Samtalstaggar\",\n      description: \"F\\xF6rdelning av samtalstaggar denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"tag\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"pie\" }\n    }\n  ];\n\n  // src/ui/dashboard.ts\n  function Gt(t) {\n    let e = null, r = [], o = c(\"div\", { class: \"s2rv-dash-header\" }), s = c(\"div\", { class: \"s2rv-dash-grid\" }), a = c(\"div\", { class: \"s2rv-dashboards\" }, o, s);\n    function l() {\n      var p;\n      for (let h of r)\n        h.controller.abort(), (p = h.chart) == null || p.destroy();\n      r = [];\n    }\n    function i() {\n      var w;\n      let p = t.getStore();\n      if (e && p.dashboards[e]) return p.dashboards[e];\n      let h = Object.values(p.dashboards).sort((k, x) => x.updatedAt - k.updatedAt)[0];\n      return e = (w = h == null ? void 0 : h.id) != null ? w : null, h != null ? h : null;\n    }\n    function u(p) {\n      p.updatedAt = Date.now(), t.onSave(p);\n    }\n    function d() {\n      let p = window.prompt(n.dashboardName, n.unnamedDashboard);\n      if (!p) return;\n      let h = { id: j(\"dash\"), name: p, tiles: [], defaultRange: { preset: \"thisMonth\" }, updatedAt: Date.now() };\n      e = h.id, u(h), S();\n    }\n    function m() {\n      let p = t.getStore(), h = [\"agent-leaderboard\", \"calls-per-day\", \"orders-per-day\", \"esign-funnel\"], w = [];\n      for (let x of h) {\n        let f = Ce.find((A) => A.id === x);\n        if (!f) continue;\n        let C = Object.values(p.reports).find((A) => A.name === f.name);\n        C || (C = {\n          id: j(\"rep\"),\n          name: f.name,\n          query: JSON.parse(JSON.stringify(f.query)),\n          viz: { ...f.viz },\n          createdAt: Date.now(),\n          updatedAt: Date.now()\n        }, t.onSaveReport(C)), w.push({ reportId: C.id, w: x === \"calls-per-day\" ? 2 : 1 });\n      }\n      let k = { id: j(\"dash\"), name: n.exampleDashboardName, tiles: w, defaultRange: { preset: \"last30\" }, updatedAt: Date.now() };\n      e = k.id, u(k), S();\n    }\n    function y(p) {\n      let h = Object.values(t.getStore().reports).sort((x, f) => f.updatedAt - x.updatedAt), w = P(\"\", \"+ \" + n.addTile, !0);\n      w.disabled = !0;\n      let k = c(\"select\", {\n        onChange: () => {\n          let x = h.find((f) => f.id === k.value);\n          x && (p.tiles.push({ reportId: x.id, w: 1 }), u(p), S());\n        }\n      }, w, ...h.map((x) => P(x.id, x.name, !1)));\n      return k.value = \"\", h.length || (k.disabled = !0, k.title = n.noSavedReports), k;\n    }\n    function g(p) {\n      var x;\n      G(o), T(o);\n      let h = t.getStore(), w = Object.values(h.dashboards).sort((f, C) => C.updatedAt - f.updatedAt);\n      if (!w.length) return;\n      let k = c(\"select\", {\n        onChange: () => {\n          e = k.value, S();\n        }\n      }, ...w.map((f) => P(f.id, f.name, f.id === (p == null ? void 0 : p.id))));\n      o.append(\n        k,\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.newDashboard, onClick: d })\n      ), p && o.append(\n        fe((x = p.defaultRange) != null ? x : { preset: \"thisMonth\" }, (f) => {\n          p.defaultRange = f, u(p);\n        }),\n        c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.refreshAll, onClick: () => b(p) }),\n        y(p),\n        c(\"button\", {\n          class: \"s2rv-btn s2rv-btn-danger\",\n          type: \"button\",\n          text: n.deleteDashboard,\n          onClick: () => {\n            window.confirm(n.confirmDelete) && (t.onDelete(p.id), e = null, S());\n          }\n        })\n      );\n    }\n    function b(p) {\n      l(), T(s), p.tiles.forEach((h, w) => {\n        var Z;\n        let x = t.getStore().reports[h.reportId], f = c(\"div\", { class: \"s2rv-status\" }), C = c(\"div\", { class: \"s2rv-tile-content\" }), A = c(\n          \"div\",\n          { class: \"s2rv-tile-actions\" },\n          c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u25C0\\u25B6\", title: h.w === 2 ? n.tileNarrow : n.tileWide, onClick: () => {\n            h.w = h.w === 2 ? 1 : 2, u(p), S();\n          } }),\n          w > 0 ? c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u2191\", title: n.moveUp, onClick: () => {\n            p.tiles.splice(w - 1, 0, p.tiles.splice(w, 1)[0]), u(p), S();\n          } }) : null,\n          w < p.tiles.length - 1 ? c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u2193\", title: n.moveDown, onClick: () => {\n            p.tiles.splice(w + 1, 0, p.tiles.splice(w, 1)[0]), u(p), S();\n          } }) : null,\n          c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u2715\", title: n.removeTile, onClick: () => {\n            p.tiles.splice(w, 1), u(p), S();\n          } })\n        ), K = c(\n          \"div\",\n          { class: \"s2rv-tile\" + (h.w === 2 ? \" s2rv-tile-wide\" : \"\") },\n          c(\n            \"div\",\n            { class: \"s2rv-tile-head\" },\n            c(\"h3\", { text: (Z = x == null ? void 0 : x.name) != null ? Z : n.unknown }),\n            A\n          ),\n          f,\n          C\n        );\n        if (s.appendChild(K), !x) {\n          f.textContent = n.unknown;\n          return;\n        }\n        let F = new AbortController(), Y = { controller: F, chart: null };\n        r.push(Y);\n        let oe = JSON.parse(JSON.stringify(x.query));\n        p.defaultRange && (oe.range = { ...p.defaultRange }), f.textContent = n.fetching, (async () => {\n          try {\n            let _ = await X(oe, {\n              lookups: t.lookups,\n              signal: F.signal,\n              onProgress: (Q) => {\n                let Re = Math.max(Q.total, 1);\n                f.textContent = n.fetchingRows + \" \" + Math.min(100, Math.round(Q.fetched / Re * 100)) + \" %\";\n              },\n              confirmLarge: V\n            });\n            if (F.signal.aborted) return;\n            _.status === \"ok\" ? (f.textContent = \"\", Y.chart = await ve(C, _.result, x.viz, t.lookups)) : _.status === \"tooLarge\" ? f.textContent = n.tooLargeChart : f.textContent = n.cancelled;\n          } catch (_) {\n            F.signal.aborted || (f.textContent = O(_));\n          }\n        })();\n      });\n    }\n    function v() {\n      l(), T(s), s.appendChild(c(\n        \"div\",\n        { class: \"s2rv-empty-state\" },\n        c(\"h3\", { text: n.emptyDashboardsTitle }),\n        c(\"p\", { text: n.emptyDashboardsText }),\n        c(\n          \"div\",\n          { class: \"s2rv-empty-state-actions\" },\n          c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.createExampleDashboard, onClick: m }),\n          c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.newDashboard, onClick: d })\n        )\n      ));\n    }\n    function S() {\n      let p = i();\n      g(p), p ? b(p) : v();\n    }\n    return S(), {\n      root: a,\n      refresh: S,\n      destroy() {\n        l(), G(a);\n      }\n    };\n  }\n\n  // src/ui/exportPanel.ts\n  function zt(t) {\n    let e = null, r = we(() => e == null ? void 0 : e.abort()), o = c(\"div\", { class: \"s2rv-status\" }), s = c(\"div\", { class: \"s2rv-export-list\" }), a = c(\"div\", { class: \"s2rv-exports\" }, o, r.root, s);\n    async function l(u) {\n      e == null || e.abort(), e = new AbortController();\n      let d = e.signal;\n      o.textContent = \"\", r.show(n.exporting);\n      try {\n        let m = re(u.name, u.format);\n        if (u.kind === \"raw\") {\n          let y = await ke(u.query, u.format, m, {\n            lookups: t.lookups,\n            signal: d,\n            onProgress: (g) => r.update(g),\n            confirmLarge: V\n          });\n          r.hide(), y.status === \"cancelled\" ? o.textContent = n.cancelled : y.status === \"tooLarge\" && (o.textContent = n.tooLargeAbs);\n        } else {\n          let y = await X(u.query, {\n            lookups: t.lookups,\n            signal: d,\n            onProgress: (g) => r.update(g),\n            confirmLarge: V\n          });\n          r.hide(), y.status === \"ok\" ? await xe(y.result, t.lookups, u.format, m) : y.status === \"cancelled\" ? o.textContent = n.cancelled : y.status === \"tooLarge\" && (o.textContent = n.tooLargeChart);\n        }\n      } catch (m) {\n        r.hide(), d.aborted ? o.textContent = n.cancelled : o.textContent = O(m);\n      }\n    }\n    function i() {\n      T(s);\n      let u = Object.values(t.getStore().exports).sort((d, m) => m.updatedAt - d.updatedAt);\n      if (!u.length) {\n        s.appendChild(c(\"div\", { class: \"s2rv-muted\", text: n.noSavedExports }));\n        return;\n      }\n      for (let d of u)\n        s.appendChild(c(\n          \"div\",\n          { class: \"s2rv-export-item\" },\n          c(\n            \"div\",\n            { class: \"s2rv-export-item-name\" },\n            c(\"strong\", { text: d.name }),\n            c(\"span\", { class: \"s2rv-muted\", text: \" \\xB7 \" + (d.kind === \"raw\" ? n.exportRaw : n.exportAggregated) + \" \\xB7 \" + d.format.toUpperCase() })\n          ),\n          c(\n            \"div\",\n            null,\n            c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.runAgain, onClick: () => void l(d) }),\n            c(\"button\", {\n              class: \"s2rv-btn s2rv-btn-danger\",\n              type: \"button\",\n              text: n.deleteReport,\n              onClick: () => {\n                window.confirm(n.confirmDelete) && (t.onDelete(d.id), i());\n              }\n            })\n          )\n        ));\n    }\n    return i(), {\n      root: a,\n      refresh: i,\n      destroy() {\n        e == null || e.abort();\n      }\n    };\n  }\n\n  // src/ui/templateGallery.ts\n  function Vt(t) {\n    return c(\n      \"div\",\n      { class: \"s2rv-templates\" },\n      c(\"p\", { class: \"s2rv-muted\", text: n.templatesIntro }),\n      c(\n        \"div\",\n        { class: \"s2rv-template-grid\" },\n        ...Ce.map((e) => c(\n          \"button\",\n          { class: \"s2rv-template-card\", type: \"button\", onClick: () => t(e) },\n          c(\"h3\", { text: e.name }),\n          c(\"p\", { text: e.description }),\n          c(\"span\", { class: \"s2rv-template-cta\", text: n.useTemplate + \" \\u2192\" })\n        ))\n      )\n    );\n  }\n\n  // src/ui/app.ts\n  function Ht(t) {\n    let e = new AbortController(), r = $e(), o = ie(), s = !1, a = null, l = null, i = null;\n    T(t);\n    let u = c(\"div\", { class: \"s2rv-boot\", text: n.appTitle + \" l\\xE4ser in\\u2026\" });\n    t.appendChild(u);\n    let d = c(\"span\", { class: \"s2rv-save-indicator\" });\n    function m() {\n      r.schedule(o, (b, v) => {\n        s || (d.classList.remove(\"s2rv-save-error\"), b === \"saving\" ? d.textContent = \"\\u2026\" : b === \"saved\" ? d.textContent = n.saveOk : (d.textContent = v != null ? v : n.saveError, d.classList.add(\"s2rv-save-error\")));\n      });\n    }\n    function y(b) {\n      a = Bt({\n        lookups: b,\n        getStore: () => o,\n        onSaveReport: (f) => {\n          o.reports[f.id] = f, m();\n        },\n        onDeleteReport: (f) => {\n          delete o.reports[f];\n          for (let C of Object.values(o.dashboards))\n            C.tiles = C.tiles.filter((A) => A.reportId !== f);\n          m(), l == null || l.refresh();\n        },\n        onSaveExport: (f) => {\n          let C = { ...f, id: j(\"exp\"), updatedAt: Date.now() };\n          o.exports[C.id] = C, m(), i == null || i.refresh();\n        },\n        onOpenTemplates: () => k(\"templates\")\n      }), l = Gt({\n        lookups: b,\n        getStore: () => o,\n        onSave: (f) => {\n          o.dashboards[f.id] = f, m();\n        },\n        onSaveReport: (f) => {\n          o.reports[f.id] = f, m(), a == null || a.refreshList();\n        },\n        onDelete: (f) => {\n          delete o.dashboards[f], m();\n        }\n      }), i = zt({\n        lookups: b,\n        getStore: () => o,\n        onDelete: (f) => {\n          delete o.exports[f], m();\n        }\n      });\n      let v = Vt((f) => {\n        a.openQuery(f.name, f.query, f.viz), k(\"reports\");\n      }), S = {\n        reports: a.root,\n        dashboards: l.root,\n        templates: v,\n        exports: i.root\n      }, p = [\n        { id: \"reports\", label: n.tabReports },\n        { id: \"dashboards\", label: n.tabDashboards },\n        { id: \"templates\", label: n.tabTemplates },\n        { id: \"exports\", label: n.tabExports }\n      ], h = /* @__PURE__ */ new Map(), w = c(\"nav\", { class: \"s2rv-tabs\" }, ...p.map((f) => {\n        let C = c(\"button\", {\n          class: \"s2rv-tab\",\n          type: \"button\",\n          text: f.label,\n          onClick: () => k(f.id)\n        });\n        return h.set(f.id, C), C;\n      }));\n      function k(f) {\n        for (let C of p)\n          S[C.id].style.display = C.id === f ? \"\" : \"none\", h.get(C.id).classList.toggle(\"s2rv-tab-active\", C.id === f);\n        f === \"dashboards\" && l.refresh(), f === \"exports\" && i.refresh(), f === \"reports\" && a.refreshList();\n      }\n      T(t), t.appendChild(c(\n        \"div\",\n        { class: \"s2rv-app\" },\n        c(\n          \"header\",\n          { class: \"s2rv-header\" },\n          d,\n          c(\"span\", { class: \"s2rv-version\", text: \"v\" + Ke })\n        ),\n        w,\n        ...p.map((f) => S[f.id])\n      ));\n      let x = Object.keys(o.reports).length > 0 || Object.keys(o.dashboards).length > 0;\n      k(x ? \"reports\" : \"templates\");\n    }\n    async function g() {\n      if (!ae()) {\n        u.textContent = n.noToken;\n        return;\n      }\n      try {\n        let [b, v] = await Promise.all([\n          Xe(e.signal),\n          r.load(e.signal)\n        ]);\n        if (s) return;\n        o = v, y(b);\n      } catch (b) {\n        if (s || e.signal.aborted) return;\n        u.textContent = O(b);\n      }\n    }\n    return g(), {\n      destroy() {\n        s = !0, e.abort(), a == null || a.destroy(), l == null || l.destroy(), i == null || i.destroy(), r.destroy(), G(t), T(t);\n      }\n    };\n  }\n\n  // src/index.ts\n  (function() {\n    let e = window;\n    if (e.__s2rvInstance && typeof e.__s2rvInstance.destroy == \"function\")\n      try {\n        e.__s2rvInstance.destroy();\n      } catch (l) {\n        console.error(\"s2rv destroy\", l);\n      }\n    let r = null, o = null;\n    function s() {\n      let l = document.getElementById(\"s2rv-root\");\n      if (l !== o) {\n        if (r) {\n          try {\n            r.destroy();\n          } catch (i) {\n            console.error(\"s2rv destroy\", i);\n          }\n          r = null, o = null;\n        }\n        if (l)\n          try {\n            r = Ht(l), o = l;\n          } catch (i) {\n            console.error(\"s2rv boot\", i);\n          }\n      }\n    }\n    let a = window.setInterval(s, 200);\n    e.__s2rvInstance = {\n      destroy() {\n        if (window.clearInterval(a), r) {\n          try {\n            r.destroy();\n          } catch (l) {\n            console.error(\"s2rv destroy\", l);\n          }\n          r = null, o = null;\n        }\n      }\n    }, s();\n  })();\n})();\n\n</script>","iconName":"chart-bar","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a4943b9e58b9e7a9eb337f6","name":"Playbook","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a4cc9c8704935b4ffb551ca"],"locations":[{"type":"navigation","label":"Playbook"}],"html":"<!-- SaleSys Playbook — realtids-wallboard som egen vy (views-v1). Självständig HTML, inga externa beroenden. -->\n<!-- OBS: Ingen \"dubbel-måsvinge\" i denna fil — vyer processas av Handlebars. -->\n<meta charset=\"utf-8\">\n<div id=\"pb-root\">\n  <style>\n    #pb-root, #pb-root * { box-sizing: border-box; margin: 0; padding: 0; }\n    #pb-root {\n      --pb-card: rgba(18, 26, 30, 0.90);\n      --pb-card-2: rgba(28, 38, 43, 0.92);\n      --pb-line: rgba(255, 255, 255, 0.07);\n      --pb-teal: #2eb4c0;\n      --pb-green: #83c341;\n      --pb-yellow: #e8a33d;\n      --pb-red: #e05252;\n      --pb-purple: #a05cc2;\n      --pb-gray: #6d7a80;\n      --pb-text: #e8edef;\n      --pb-dim: #97a4aa;\n      font-family: 'Roboto Condensed', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n      color: var(--pb-text);\n      position: relative;\n      width: 100%;\n      height: 640px; /* justeras vid boot till tillgänglig höjd */\n      overflow: hidden;\n      border-radius: 4px;\n      background: linear-gradient(160deg, #22303a 0%, #2c4438 45%, #1c2b33 100%);\n      background-size: cover;\n      background-position: center;\n    }\n    #pb-root .pb-bgimg {\n      position: absolute; inset: 0;\n      background-size: cover; background-position: center;\n      filter: saturate(0.9);\n    }\n    #pb-root .pb-app { position: relative; height: 100%; display: flex; flex-direction: column; }\n\n    /* ---------- Header ---------- */\n    .pb-header {\n      display: flex; align-items: center; gap: 18px;\n      padding: 10px 18px;\n      background: rgba(10, 16, 19, 0.85);\n      backdrop-filter: blur(6px);\n      flex: 0 0 auto;\n    }\n    .pb-logo { font-size: 22px; letter-spacing: 1px; white-space: nowrap; }\n    .pb-logo b { font-weight: 700; color: #fff; }\n    .pb-logo span { font-weight: 300; color: var(--pb-teal); }\n    .pb-pills { display: flex; gap: 8px; }\n    .pb-pill {\n      border: 1px solid var(--pb-teal); color: var(--pb-teal);\n      background: transparent; border-radius: 14px;\n      font-size: 11px; font-weight: 700; letter-spacing: 1px;\n      padding: 4px 14px; cursor: pointer; text-transform: uppercase;\n      font-family: inherit;\n    }\n    .pb-pill.pb-on { background: var(--pb-teal); color: #06272b; }\n    .pb-head-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }\n    .pb-updated { font-size: 11px; color: var(--pb-dim); letter-spacing: 0.5px; }\n    .pb-me { display: flex; align-items: center; gap: 8px; }\n    .pb-me-avatar {\n      width: 34px; height: 34px; border-radius: 4px;\n      background: var(--pb-teal); color: #06272b;\n      display: flex; align-items: center; justify-content: center;\n      font-weight: 700; font-size: 13px;\n    }\n    .pb-me-name { font-size: 13px; font-weight: 700; color: #fff; line-height: 1.2; }\n    .pb-me-sub { font-size: 11px; color: var(--pb-teal); }\n\n    .pb-banner {\n      display: none; padding: 6px 18px; font-size: 12px;\n      background: rgba(224, 82, 82, 0.9); color: #fff; letter-spacing: 0.5px;\n    }\n    .pb-banner.pb-show { display: block; }\n    .pb-banner.pb-mock { background: rgba(232, 163, 61, 0.9); color: #201500; }\n\n    /* ---------- Layout ---------- */\n    .pb-main {\n      flex: 1 1 auto; min-height: 0;\n      display: grid; grid-template-columns: 330px minmax(380px, 1fr) 360px;\n      gap: 14px; padding: 14px 18px 18px;\n    }\n    .pb-col { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; }\n    .pb-col-mid { overflow: hidden; }\n    .pb-col::-webkit-scrollbar { width: 6px; }\n    .pb-col::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }\n    @media (max-width: 1100px) {\n      .pb-main { grid-template-columns: 1fr; overflow-y: auto; }\n      .pb-col { overflow: visible; }\n    }\n\n    .pb-card {\n      background: var(--pb-card); border-radius: 3px;\n      box-shadow: 0 4px 14px rgba(0,0,0,0.35);\n      flex: 0 0 auto;\n    }\n    .pb-card-head {\n      display: flex; align-items: flex-start; gap: 8px;\n      padding: 12px 14px 8px;\n    }\n    .pb-card-title { font-size: 13px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; color: #fff; }\n    .pb-card-sub { font-size: 11px; color: var(--pb-dim); margin-top: 2px; }\n    .pb-dots { margin-left: auto; color: var(--pb-dim); font-size: 14px; line-height: 1; cursor: default; user-select: none; }\n\n    /* ---------- Kort A: teamstatus ---------- */\n    .pb-segbar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; margin: 4px 14px 10px; background: rgba(255,255,255,0.08); }\n    .pb-seg { height: 100%; transition: width 0.6s ease; }\n    .pb-counts { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--pb-line); }\n    .pb-count { padding: 10px 6px 12px; text-align: center; border-right: 1px solid var(--pb-line); }\n    .pb-count:last-child { border-right: 0; }\n    .pb-count-num { font-size: 26px; font-weight: 300; line-height: 1.1; }\n    .pb-count-label { font-size: 9px; font-weight: 700; letter-spacing: 1px; color: var(--pb-dim); text-transform: uppercase; margin-top: 2px; }\n\n    /* ---------- Kort B: översikt/donut ---------- */\n    .pb-ov { display: flex; gap: 6px; padding: 6px 14px 14px; align-items: center; }\n    .pb-donut-wrap { position: relative; width: 128px; height: 128px; flex: 0 0 auto; }\n    .pb-donut-txt { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }\n    .pb-donut-val { font-size: 26px; font-weight: 300; color: #fff; }\n    .pb-donut-label { font-size: 8px; font-weight: 700; letter-spacing: 1px; color: var(--pb-dim); text-transform: uppercase; }\n    .pb-ov-rows { flex: 1 1 auto; min-width: 0; }\n    .pb-ov-row { display: flex; align-items: center; gap: 9px; padding: 5px 0; border-bottom: 1px solid var(--pb-line); }\n    .pb-ov-row:last-child { border-bottom: 0; }\n    .pb-ov-ico {\n      width: 22px; height: 22px; border-radius: 3px; flex: 0 0 auto;\n      display: flex; align-items: center; justify-content: center;\n    }\n    .pb-ov-ico svg { width: 12px; height: 12px; fill: #fff; }\n    .pb-ov-main { flex: 1 1 auto; min-width: 0; }\n    .pb-ov-name { font-size: 11px; font-weight: 700; letter-spacing: 0.7px; text-transform: uppercase; color: #dfe7ea; }\n    .pb-ov-desc { font-size: 10px; color: var(--pb-dim); }\n    .pb-ov-val { font-size: 19px; font-weight: 300; color: #fff; }\n\n    /* ---------- Mittkort: agentlista ---------- */\n    .pb-agents-card { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }\n    .pb-tabs { display: flex; border-bottom: 1px solid var(--pb-line); }\n    .pb-tab {\n      flex: 1; background: transparent; border: 0; color: var(--pb-dim);\n      font-family: inherit; cursor: pointer; padding: 8px 4px 10px; text-align: center;\n      border-bottom: 3px solid transparent;\n    }\n    .pb-tab.pb-on { background: rgba(255,255,255,0.05); color: #fff; border-bottom-color: var(--pb-teal); }\n    .pb-tab-num { display: block; font-size: 24px; font-weight: 300; line-height: 1.1; }\n    .pb-tab-label { display: block; font-size: 9px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; margin-top: 1px; }\n    .pb-tab.pb-warn .pb-tab-num { color: var(--pb-yellow); }\n\n    .pb-list-head {\n      display: flex; padding: 6px 14px; font-size: 9px; font-weight: 700;\n      letter-spacing: 1px; color: var(--pb-dim); text-transform: uppercase;\n      border-bottom: 1px solid var(--pb-line);\n    }\n    .pb-list-head .pb-lh-agent { flex: 1; margin-left: 36px; }\n    .pb-list-head .pb-lh-action { width: 92px; }\n    .pb-list-head .pb-lh-kpi { width: 122px; text-align: right; }\n    .pb-rows { overflow-y: auto; min-height: 0; flex: 1 1 auto; }\n    .pb-rows::-webkit-scrollbar { width: 6px; }\n    .pb-rows::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }\n\n    .pb-row { display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid var(--pb-line); }\n    .pb-row.pb-off { opacity: 0.55; }\n    .pb-ava {\n      width: 26px; height: 26px; border-radius: 3px; flex: 0 0 auto;\n      display: flex; align-items: center; justify-content: center;\n    }\n    .pb-ava svg { width: 13px; height: 13px; fill: #fff; }\n    .pb-row-main { flex: 1 1 auto; min-width: 0; }\n    .pb-row-name { font-size: 13px; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .pb-row-sub { font-size: 10px; color: var(--pb-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .pb-row-action { width: 92px; flex: 0 0 auto; }\n    .pb-row-status { font-size: 12px; font-weight: 700; }\n    .pb-row-timer { font-size: 10px; color: var(--pb-dim); font-variant-numeric: tabular-nums; }\n    .pb-chips { display: flex; gap: 3px; width: 122px; flex: 0 0 auto; justify-content: flex-end; }\n    .pb-chip {\n      min-width: 38px; text-align: center; padding: 3px 4px; border-radius: 2px;\n      font-size: 10px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums;\n    }\n\n    /* ---------- Högerkort: team-KPI ---------- */\n    .pb-kpi-body { display: flex; align-items: stretch; gap: 10px; padding: 4px 14px 14px; }\n    .pb-bars { flex: 1 1 auto; display: flex; align-items: flex-end; gap: 3px; height: 74px; min-width: 0; }\n    .pb-bar { flex: 1; background: rgba(255,255,255,0.25); border-radius: 1px 1px 0 0; min-height: 2px; transition: height 0.6s ease; }\n    .pb-bar.pb-now { background: #fff; }\n    .pb-kpi-stat {\n      flex: 0 0 96px; border-radius: 2px; color: #fff;\n      display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;\n    }\n    .pb-kpi-val { font-size: 26px; font-weight: 300; }\n    .pb-kpi-label { font-size: 8px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; opacity: 0.9; }\n\n    .pb-skel { position: relative; overflow: hidden; background: rgba(255,255,255,0.06); border-radius: 3px; }\n    .pb-skel::after {\n      content: \"\"; position: absolute; inset: 0;\n      background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);\n      animation: pbShimmer 1.4s infinite;\n    }\n    @keyframes pbShimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }\n    .pb-empty { padding: 24px 14px; text-align: center; font-size: 12px; color: var(--pb-dim); }\n  </style>\n\n  <div class=\"pb-bgimg\" id=\"pbBg\"></div>\n  <div class=\"pb-app\">\n    <div class=\"pb-header\">\n      <div class=\"pb-logo\"><b>SALESYS</b> <span>PLAYBOOK</span></div>\n      <div class=\"pb-pills\">\n        <button class=\"pb-pill pb-on\" id=\"pbPillToday\" type=\"button\">Idag</button>\n        <button class=\"pb-pill\" id=\"pbPillHour\" type=\"button\">Senaste timmen</button>\n      </div>\n      <div class=\"pb-head-right\">\n        <span class=\"pb-updated\" id=\"pbUpdated\"></span>\n        <div class=\"pb-me\" id=\"pbMe\"></div>\n      </div>\n    </div>\n    <div class=\"pb-banner\" id=\"pbBanner\"></div>\n    <div class=\"pb-main\">\n      <div class=\"pb-col\" id=\"pbColLeft\">\n        <div class=\"pb-skel\" style=\"height:150px\"></div>\n        <div class=\"pb-skel\" style=\"height:170px\"></div>\n      </div>\n      <div class=\"pb-col pb-col-mid\">\n        <div class=\"pb-card pb-agents-card\" id=\"pbAgentsCard\">\n          <div class=\"pb-skel\" style=\"height:100%;min-height:300px\"></div>\n        </div>\n      </div>\n      <div class=\"pb-col\" id=\"pbColRight\">\n        <div class=\"pb-skel\" style=\"height:130px\"></div>\n        <div class=\"pb-skel\" style=\"height:130px\"></div>\n        <div class=\"pb-skel\" style=\"height:130px\"></div>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script>\n(function () {\n  \"use strict\";\n\n  /* ================= Konfiguration ================= */\n  var CONFIG = {\n    backgroundUrl: \"\",             //  Valfri foto-bakgrund (URL). Tomt = gradient.\n    pollStatsMs: 5000,             //  call-statistics-v2\n    pollActiveMs: 30000,           //  aktiv tid (online-detektering)\n    pollSlowMs: 60000,             //  timstaplar + inloggningstider\n    onlineWindowMinutes: 10,       //  aktiv tid inom X min = online\n    warnMinutes: 30,               //  i kö/efterarbete längre än X min = varning\n    belGood: 35, belOk: 18,        //  trösklar (%) för beläggnings-chip\n    svarGood: 40, svarOk: 20,      //  trösklar (%) för svar-chip\n    callTagIdsSvar: []             //  taggar för SVAR% (callCountPercent). Tomt = alla samtal.\n  };\n\n  var API_BASE = (location.hostname === \"app.salesys.se\") ? \"\" : \"https://app.salesys.se\";\n  var STATUS_DEF = {\n    connected:  { label: \"I samtal\",     color: \"var(--pb-teal)\",   hex: \"#2eb4c0\", order: 0 },\n    cohearing:  { label: \"Medlyssning\",  color: \"var(--pb-purple)\", hex: \"#a05cc2\", order: 1 },\n    connecting: { label: \"Ringer\",       color: \"var(--pb-teal)\",   hex: \"#2eb4c0\", order: 2 },\n    evaluating: { label: \"Efterarbete\",  color: \"var(--pb-yellow)\", hex: \"#e8a33d\", order: 3 },\n    ready:      { label: \"Redo\",         color: \"var(--pb-green)\",  hex: \"#83c341\", order: 4 },\n    active:     { label: \"Aktiv\",        color: \"var(--pb-green)\",  hex: \"#83c341\", order: 5 },\n    offline:    { label: \"Offline\",      color: \"var(--pb-gray)\",   hex: \"#6d7a80\", order: 6 }\n  };\n  var TEAM_ACCENTS = [\"#2eb4c0\", \"#83c341\", \"#a05cc2\", \"#e8a33d\", \"#5c8ac2\", \"#e05252\"];\n\n  var ICONS = {\n    phone: '<svg viewBox=\"0 0 24 24\"><path d=\"M6.6 10.8c1.4 2.8 3.8 5.1 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.6 21 3 13.4 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.2.2 2.4.6 3.6.1.3 0 .7-.2 1l-2.3 2.2z\"/></svg>',\n    user: '<svg viewBox=\"0 0 24 24\"><path d=\"M12 12c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4z\"/></svg>',\n    users: '<svg viewBox=\"0 0 24 24\"><path d=\"M16 11c1.7 0 3-1.3 3-3s-1.3-3-3-3-3 1.3-3 3 1.3 3 3 3zm-8 0c1.7 0 3-1.3 3-3S9.7 5 8 5 5 6.3 5 8s1.3 3 3 3zm0 2c-2.3 0-7 1.2-7 3.5V19h14v-2.5C15 14.2 10.3 13 8 13zm8 0c-.3 0-.6 0-1 .1 1.2.8 2 1.9 2 3.4V19h6v-2.5c0-2.3-4.7-3.5-7-3.5z\"/></svg>',\n    clock: '<svg viewBox=\"0 0 24 24\"><path d=\"M12 2a10 10 0 100 20 10 10 0 000-20zm1 11h-6v-2h4V6h2v7z\"/></svg>',\n    headset: '<svg viewBox=\"0 0 24 24\"><path d=\"M12 2a8 8 0 00-8 8v6a3 3 0 003 3h2v-7H6v-2a6 6 0 1112 0v2h-3v7h2a3 3 0 003-3v-6a8 8 0 00-8-8z\"/></svg>',\n    bolt: '<svg viewBox=\"0 0 24 24\"><path d=\"M11 21l1-7H8l6-11-1 7h4l-6 11z\"/></svg>',\n    pause: '<svg viewBox=\"0 0 24 24\"><path d=\"M7 5h4v14H7zM13 5h4v14h-4z\"/></svg>'\n  };\n  var STATUS_ICON = {\n    connected: ICONS.phone, cohearing: ICONS.headset, connecting: ICONS.bolt,\n    evaluating: ICONS.clock, ready: ICONS.headset, active: ICONS.user, offline: ICONS.pause\n  };\n\n  /* ================= State ================= */\n  var state = {\n    period: \"today\",             //  today | hour\n    me: null,\n    users: [],                   //  [{id, fullName, teamId}]\n    teams: [],                   //  [{id, name, accent}]\n    statItems: {},               //  userId -> call-statistics-v2 item\n    activeNow: {},               //  userId -> true (aktiv tid inom onlineWindow)\n    activeDur: {},               //  userId -> aktiv tid (ms) inom perioden\n    hourly: {},                  //  teamId -> [24] samtal per timme (idag, lokal tid)\n    loggedInToday: {},           //  userId -> true\n    statsLoadedAt: null,\n    mock: false,\n    error: null\n  };\n  var timers = { stats: null, active: null, slow: null, tick: null };\n  var currentTab = \"now\";\n\n  /* ================= Hjälpare ================= */\n  function $(id) { return document.getElementById(id); }\n  function esc(s) {\n    return String(s == null ? \"\" : s).replace(/[&<>\"']/g, function (c) {\n      return { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" }[c];\n    });\n  }\n  function initials(name) {\n    var parts = String(name || \"?\").trim().split(/\\s+/);\n    var a = parts[0] ? parts[0][0] : \"?\";\n    var b = parts.length > 1 ? parts[parts.length - 1][0] : \"\";\n    return (a + b).toUpperCase();\n  }\n  function fmtDur(ms) {\n    if (ms == null || isNaN(ms) || ms < 0) { ms = 0; }\n    var s = Math.floor(ms / 1000);\n    var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;\n    function p(n) { return (n < 10 ? \"0\" : \"\") + n; }\n    return h > 0 ? h + \":\" + p(m) + \":\" + p(sec) : p(m) + \":\" + p(sec);\n  }\n  function fmtHours(ms) {\n    var min = Math.round((ms || 0) / 60000);\n    var h = Math.floor(min / 60), m = min % 60;\n    return h > 0 ? h + \" h \" + m + \" min\" : m + \" min\";\n  }\n  function periodStart() {\n    if (state.period === \"hour\") { return new Date(Date.now() - 3600000); }\n    var d = new Date(); d.setHours(0, 0, 0, 0); return d;\n  }\n  function chipColor(val, good, ok) {\n    if (val == null || isNaN(val)) { return \"rgba(255,255,255,0.14)\"; }\n    if (val >= good) { return STATUS_DEF.ready.hex; }\n    if (val >= ok) { return STATUS_DEF.evaluating.hex; }\n    return STATUS_DEF.offline.hex;\n  }\n  function getToken() {\n    var m = /(?:^|;\\s*)s2_utoken=([^;]+)/.exec(document.cookie || \"\");\n    return m ? decodeURIComponent(m[1]) : null;\n  }\n\n  function api(path, params) {\n    var qs = [];\n    Object.keys(params || {}).forEach(function (k) {\n      var v = params[k];\n      if (v == null || v === \"\") { return; }\n      if (Array.isArray(v)) {\n        v.forEach(function (x) { qs.push(encodeURIComponent(k) + \"=\" + encodeURIComponent(x)); });\n      } else {\n        qs.push(encodeURIComponent(k) + \"=\" + encodeURIComponent(v));\n      }\n    });\n    var url = API_BASE + path + (qs.length ? \"?\" + qs.join(\"&\") : \"\");\n    return fetch(url, { headers: { Authorization: \"Bearer \" + getToken() } }).then(function (res) {\n      if (!res.ok) { throw new Error(\"HTTP \" + res.status + \" \" + path); }\n      return res.json();\n    });\n  }\n\n  /* ================= Statusmodell ================= */\n  function agentStatus(userId) {\n    var it = state.statItems[userId];\n    var raw = it ? it.userStatus : \"inactive\";\n    var key;\n    if (raw === \"connected\") { key = \"connected\"; }\n    else if (raw === \"cohearing\") { key = \"cohearing\"; }\n    else if (raw === \"connecting\") { key = \"connecting\"; }\n    else if (raw === \"evaluatingCall\") { key = \"evaluating\"; }\n    else if (raw === \"predictiveAwaitingCall\" || raw === \"cohearingAwaitingCall\") { key = \"ready\"; }\n    else { key = state.activeNow[userId] ? \"active\" : \"offline\"; }\n    var since = it && it.userStatusSince ? new Date(it.userStatusSince).getTime() : null;\n    return { key: key, def: STATUS_DEF[key], since: key === \"offline\" ? null : since };\n  }\n  function agentKpis(userId) {\n    var it = state.statItems[userId] || {};\n    var talk = it.connectedDuration || 0;\n    var active = state.activeDur[userId] || 0;\n    var bel = active > 0 ? Math.min(100, Math.round((talk / active) * 100)) : null;\n    var svar = (it.callCountTotal > 0 && it.callCountPercentArgument != null) ? it.callCountPercentArgument : null;\n    return { bel: bel, calls: it.callCountTotal || 0, svar: svar, talk: talk };\n  }\n  function isWarning(userId) {\n    var st = agentStatus(userId);\n    if (st.key === \"offline\" || st.key === \"connected\" || st.key === \"cohearing\") { return false; }\n    if (st.since == null) { return false; }\n    return (Date.now() - st.since) > CONFIG.warnMinutes * 60000;\n  }\n  function teamAccent(i) { return TEAM_ACCENTS[i % TEAM_ACCENTS.length]; }\n  function usersInTeam(teamId) {\n    return state.users.filter(function (u) { return (u.teamId || \"none\") === teamId; });\n  }\n  function activeTeams() {\n    var byId = {};\n    state.users.forEach(function (u) { byId[u.teamId || \"none\"] = true; });\n    var teams = state.teams.filter(function (t) { return byId[t.id]; });\n    if (byId[\"none\"]) { teams.push({ id: \"none\", name: \"Utan team\" }); }\n    return teams;\n  }\n\n  /* ================= Datahämtning ================= */\n  function fetchAllPages(path, params, extract) {\n    var all = [];\n    function page(offset) {\n      var p = {}; Object.keys(params).forEach(function (k) { p[k] = params[k]; });\n      p.count = 100; p.offset = offset;\n      return api(path, p).then(function (data) {\n        var arr = extract(data) || [];\n        all = all.concat(arr);\n        return arr.length === 100 ? page(offset + 100) : all;\n      });\n    }\n    return page(0);\n  }\n\n  function loadBootstrap() {\n    return Promise.all([\n      fetchAllPages(\"/api/users/users-v1\", { active: true }, function (d) {\n        return Array.isArray(d) ? d : (d.users || d.items || []);\n      }),\n      api(\"/api/users/teams-v1\", {}).then(function (d) {\n        return Array.isArray(d) ? d : (d.teams || d.items || []);\n      }),\n      api(\"/api/users/users-v1/me\", {}).catch(function () { return null; })\n    ]).then(function (res) {\n      state.users = res[0].filter(function (u) {\n        return u.type !== \"system\" && u.type !== \"private\" && u.active !== false;\n      }).map(function (u) {\n        return { id: u.id, fullName: u.fullName || u.email || \"Okänd\", teamId: u.teamId || null };\n      });\n      state.teams = res[1].map(function (t) { return { id: t.id, name: t.name }; });\n      state.me = res[2];\n    });\n  }\n\n  function loadStats() {\n    var params = {\n      after: periodStart().toISOString(),\n      connectionState: \"disconnected\"\n    };\n    if (CONFIG.callTagIdsSvar.length) { params[\"callTagIds.callCountPercent\"] = CONFIG.callTagIdsSvar; }\n    return api(\"/api/dial/call-statistics-v2\", params).then(function (d) {\n      var items = {};\n      (d.items || []).forEach(function (it) { items[it.userId] = it; });\n      state.statItems = items;\n      state.statsLoadedAt = d.lastUpdatedAt ? new Date(d.lastUpdatedAt) : new Date();\n    });\n  }\n\n  function loadActiveNow() {\n    var to = new Date();\n    var from = new Date(to.getTime() - CONFIG.onlineWindowMinutes * 60000);\n    return api(\"/api/users/statistics-v1/own/user_tabs\", {\n      from: from.toISOString(), to: to.toISOString(), fixedIntervalType: \"minute\"\n    }).then(function (rows) {\n      var map = {};\n      (rows || []).forEach(function (r) {\n        if ((r.activeDuration || 0) > 0) { map[r.userId] = true; }\n      });\n      state.activeNow = map;\n    });\n  }\n\n  function loadActiveDurations() {\n    var from = periodStart();\n    return api(\"/api/users/statistics-v1/own/user_tabs\", {\n      from: from.toISOString(), to: new Date().toISOString(),\n      fixedIntervalType: state.period === \"hour\" ? \"minute\" : \"hour\"\n    }).then(function (rows) {\n      var map = {};\n      (rows || []).forEach(function (r) {\n        map[r.userId] = (map[r.userId] || 0) + (r.activeDuration || 0);\n      });\n      state.activeDur = map;\n    });\n  }\n\n  function loadLoggedIn() {\n    function d(x) {\n      return x.getFullYear() + \"-\" + (\"0\" + (x.getMonth() + 1)).slice(-2) + \"-\" + (\"0\" + x.getDate()).slice(-2);\n    }\n    var today = new Date();\n    var tomorrow = new Date(today.getTime() + 86400000);\n    return api(\"/api/users/statistics-v1/own/issue_1295_active_user_time\", {\n      from: d(today), to: d(tomorrow)\n    }).then(function (rows) {\n      var map = {};\n      (rows || []).forEach(function (r) {\n        if ((r.activeDuration || 0) > 0 || r.startAt) { map[r.userId] = true; }\n      });\n      state.loggedInToday = map;\n    });\n  }\n\n  function loadHourly() {\n    var from = new Date(); from.setHours(0, 0, 0, 0);\n    return api(\"/api/dial/statistics-v1/own/issue_1238_2\", {\n      from: from.toISOString(), to: new Date().toISOString(), fixedIntervalType: \"hour\"\n    }).then(function (rows) {\n      var teamOf = {};\n      state.users.forEach(function (u) { teamOf[u.id] = u.teamId || \"none\"; });\n      var hourly = {};\n      (rows || []).forEach(function (r) {\n        var start = r.intervalStart || r.from || r.startAt;\n        if (!start) { return; }\n        var h = new Date(start).getHours();\n        var teamId = teamOf[r.userId];\n        if (teamId == null) { return; }\n        if (!hourly[teamId]) { hourly[teamId] = new Array(24).fill(0); }\n        hourly[teamId][h] += (r.count || 0);\n      });\n      state.hourly = hourly;\n    });\n  }\n\n  /* ================= Mockdata (lokal utveckling) ================= */\n  function loadMock() {\n    state.mock = true;\n    var teams = [\n      { id: \"t1\", name: \"Team Special Forces\" },\n      { id: \"t2\", name: \"Team Nordost\" },\n      { id: \"t3\", name: \"Team Innesälj\" }\n    ];\n    var names = [\"Jesper Gran\", \"Mille Bessö\", \"Andreas Gordonsson\", \"Tobias Sjölander\", \"Tuomas Koli\",\n      \"Nicklas Svensson\", \"Daniel Sandgren\", \"Ola Bäckström\", \"Emil Nilsson\", \"Peter Svärd\",\n      \"Björn Bylander\", \"Martin Lund\", \"Anders Widell\", \"Richard Magnusson\", \"Håkan Smedby\",\n      \"Anna Lindqvist\", \"Sara Öberg\", \"Johan Ek\", \"Lisa Månsson\", \"Karl Fors\",\n      \"Eva Strand\", \"Oscar Palm\", \"Nina Berg\", \"Adam Hall\"];\n    var statuses = [\"connected\", \"connected\", \"connecting\", \"evaluatingCall\", \"predictiveAwaitingCall\",\n      \"predictiveAwaitingCall\", \"inactive\", \"inactive\", \"inactive\", \"cohearing\"];\n    state.teams = teams;\n    state.users = names.map(function (n, i) {\n      return { id: \"u\" + i, fullName: n, teamId: teams[i % 3].id };\n    });\n    var items = {}, activeNow = {}, activeDur = {}, logged = {};\n    state.users.forEach(function (u, i) {\n      var st = statuses[i % statuses.length];\n      var online = i % 4 !== 3;\n      items[u.id] = {\n        userId: u.id,\n        userStatus: st,\n        userStatusSince: new Date(Date.now() - (i * 47000 + 30000) % 3200000).toISOString(),\n        callCountTotal: online ? (5 + (i * 7) % 40) : 0,\n        connectedDuration: online ? ((i * 13) % 90 + 15) * 60000 : 0,\n        averageConnectedDuration: 180000,\n        callCountPercentArgument: online ? (20 + (i * 11) % 70) : 0\n      };\n      if (online) {\n        activeNow[u.id] = true;\n        activeDur[u.id] = (3 + (i % 5)) * 3600000 * (state.period === \"hour\" ? 0.15 : 1);\n        logged[u.id] = true;\n      }\n    });\n    state.statItems = items;\n    state.activeNow = activeNow;\n    state.activeDur = activeDur;\n    state.loggedInToday = logged;\n    var hourly = {};\n    teams.forEach(function (t, ti) {\n      hourly[t.id] = new Array(24).fill(0).map(function (_, h) {\n        return (h >= 8 && h <= 17) ? 10 + ((h * (ti + 3)) % 25) : 0;\n      });\n    });\n    state.hourly = hourly;\n    state.me = { fullName: \"Mock Användare\" };\n    state.statsLoadedAt = new Date();\n    return Promise.resolve();\n  }\n\n  /* ================= Rendering ================= */\n  function teamStatusCounts(teamId) {\n    var counts = { ready: 0, incall: 0, wrap: 0, offline: 0, online: 0, dialer: 0 };\n    usersInTeam(teamId).forEach(function (u) {\n      var st = agentStatus(u.id);\n      if (st.key === \"connected\" || st.key === \"cohearing\" || st.key === \"connecting\") { counts.incall += 1; }\n      else if (st.key === \"evaluating\") { counts.wrap += 1; }\n      else if (st.key === \"ready\" || st.key === \"active\") { counts.ready += 1; }\n      else { counts.offline += 1; }\n      if (st.key !== \"offline\") { counts.online += 1; }\n      if (st.key !== \"offline\" && st.key !== \"active\") { counts.dialer += 1; }\n    });\n    return counts;\n  }\n\n  function renderLeft() {\n    var html = \"\";\n\n    //  Översiktskort med donut (hela organisationen)\n    var totTalk = 0, totActive = 0, totCalls = 0, inCallNow = 0, inDialerNow = 0, loggedIn = 0;\n    state.users.forEach(function (u) {\n      var k = agentKpis(u.id), st = agentStatus(u.id);\n      totTalk += k.talk; totCalls += k.calls;\n      totActive += state.activeDur[u.id] || 0;\n      if (st.key === \"connected\" || st.key === \"cohearing\") { inCallNow += 1; }\n      if (st.key !== \"offline\" && st.key !== \"active\") { inDialerNow += 1; }\n      if (state.loggedInToday[u.id]) { loggedIn += 1; }\n    });\n    var bel = totActive > 0 ? Math.min(100, Math.round((totTalk / totActive) * 100)) : 0;\n    var C = 2 * Math.PI * 54;\n    var dash = (bel / 100) * C;\n\n    html += '<div class=\"pb-card\">'\n      + '<div class=\"pb-card-head\"><div><div class=\"pb-card-title\">' + esc((state.me && state.me.organizationName) || \"Översikt\") + '</div>'\n      + '<div class=\"pb-card-sub\">' + state.users.length + ' agenter totalt</div></div><div class=\"pb-dots\">&#8942;</div></div>'\n      + '<div class=\"pb-ov\">'\n      + '<div class=\"pb-donut-wrap\"><svg viewBox=\"0 0 128 128\" width=\"128\" height=\"128\">'\n      + '<circle cx=\"64\" cy=\"64\" r=\"54\" fill=\"none\" stroke=\"rgba(255,255,255,0.10)\" stroke-width=\"10\"/>'\n      + '<circle cx=\"64\" cy=\"64\" r=\"54\" fill=\"none\" stroke=\"' + STATUS_DEF.ready.hex + '\" stroke-width=\"10\" stroke-linecap=\"round\" '\n      + 'stroke-dasharray=\"' + dash.toFixed(1) + ' ' + C.toFixed(1) + '\" transform=\"rotate(-90 64 64)\"/>'\n      + '</svg><div class=\"pb-donut-txt\"><div class=\"pb-donut-val\">' + bel + '%</div><div class=\"pb-donut-label\">Beläggning</div></div></div>'\n      + '<div class=\"pb-ov-rows\">'\n      + ovRow(ICONS.users, STATUS_DEF.connected.hex, \"Inloggade\", \"idag\", loggedIn)\n      + ovRow(ICONS.headset, STATUS_DEF.ready.hex, \"I dialern\", \"just nu\", inDialerNow)\n      + ovRow(ICONS.phone, STATUS_DEF.evaluating.hex, \"I samtal\", \"just nu\", inCallNow)\n      + ovRow(ICONS.bolt, STATUS_DEF.cohearing.hex, \"Samtal\", state.period === \"hour\" ? \"senaste timmen\" : \"idag\", totCalls)\n      + ovRow(ICONS.clock, STATUS_DEF.offline.hex, \"Taltid\", state.period === \"hour\" ? \"senaste timmen\" : \"idag\", fmtHours(totTalk))\n      + '</div></div></div>';\n\n    //  Ett statuskort per team\n    activeTeams().forEach(function (t, i) {\n      var c = teamStatusCounts(t.id);\n      var team = usersInTeam(t.id);\n      var working = c.incall + c.wrap + c.ready;\n      var segTotal = Math.max(1, team.length);\n      function seg(n, color) {\n        return '<div class=\"pb-seg\" style=\"width:' + ((n / segTotal) * 100).toFixed(1) + '%;background:' + color + '\"></div>';\n      }\n      html += '<div class=\"pb-card\">'\n        + '<div class=\"pb-card-head\"><div><div class=\"pb-card-title\" style=\"color:' + teamAccent(i) + '\">' + esc(t.name) + '</div>'\n        + '<div class=\"pb-card-sub\">' + working + ' agenter arbetar (' + c.online + ' online)</div></div><div class=\"pb-dots\">&#8942;</div></div>'\n        + '<div class=\"pb-segbar\">'\n        + seg(c.ready, STATUS_DEF.ready.hex) + seg(c.incall, STATUS_DEF.connected.hex)\n        + seg(c.wrap, STATUS_DEF.evaluating.hex) + seg(c.offline, \"rgba(255,255,255,0.10)\")\n        + '</div>'\n        + '<div class=\"pb-counts\">'\n        + countCell(c.ready, \"Redo\", STATUS_DEF.ready.hex)\n        + countCell(c.incall, \"I samtal\", STATUS_DEF.connected.hex)\n        + countCell(c.wrap, \"Efterarbete\", STATUS_DEF.evaluating.hex)\n        + countCell(c.offline, \"Offline\", c.offline > 0 ? STATUS_DEF.offline.hex : \"rgba(255,255,255,0.25)\")\n        + '</div></div>';\n    });\n\n    $(\"pbColLeft\").innerHTML = html;\n  }\n\n  function ovRow(icon, color, name, desc, val) {\n    return '<div class=\"pb-ov-row\"><div class=\"pb-ov-ico\" style=\"background:' + color + '\">' + icon + '</div>'\n      + '<div class=\"pb-ov-main\"><div class=\"pb-ov-name\">' + esc(name) + '</div><div class=\"pb-ov-desc\">' + esc(desc) + '</div></div>'\n      + '<div class=\"pb-ov-val\">' + val + '</div></div>';\n  }\n  function countCell(n, label, color) {\n    return '<div class=\"pb-count\"><div class=\"pb-count-num\" style=\"color:' + color + '\">' + n + '</div>'\n      + '<div class=\"pb-count-label\">' + esc(label) + '</div></div>';\n  }\n\n  function filteredAgents() {\n    var teamName = {};\n    state.teams.forEach(function (t) { teamName[t.id] = t.name; });\n    var list = state.users.map(function (u) {\n      var st = agentStatus(u.id);\n      return {\n        user: u, st: st, kpi: agentKpis(u.id),\n        team: teamName[u.teamId] || \"Utan team\",\n        warn: isWarning(u.id)\n      };\n    });\n    var filtered;\n    if (currentTab === \"now\") {\n      filtered = list.filter(function (a) { return a.st.key !== \"offline\" && a.st.key !== \"active\"; });\n    } else if (currentTab === \"warning\") {\n      filtered = list.filter(function (a) { return a.warn; });\n    } else {\n      filtered = list.filter(function (a) { return state.loggedInToday[a.user.id] || a.st.key !== \"offline\"; });\n      if (!filtered.length) { filtered = list; }\n    }\n    filtered.sort(function (a, b) {\n      var d = a.st.def.order - b.st.def.order;\n      if (d !== 0) { return d; }\n      return a.user.fullName.localeCompare(b.user.fullName, \"sv\");\n    });\n    return { list: list, filtered: filtered };\n  }\n\n  function renderAgents() {\n    var data = filteredAgents();\n    var nowCount = data.list.filter(function (a) { return a.st.key !== \"offline\" && a.st.key !== \"active\"; }).length;\n    var todayCount = data.list.filter(function (a) { return state.loggedInToday[a.user.id] || a.st.key !== \"offline\"; }).length;\n    var warnCount = data.list.filter(function (a) { return a.warn; }).length;\n\n    var html = '<div class=\"pb-card-head\"><div><div class=\"pb-card-title\">Agenter</div>'\n      + '<div class=\"pb-card-sub\">' + (state.period === \"hour\" ? \"Senaste timmen\" : \"Idag\") + '</div></div><div class=\"pb-dots\">&#8942;</div></div>'\n      + '<div class=\"pb-tabs\">'\n      + tab(\"now\", nowCount, \"Nu\") + tab(\"today\", todayCount, \"Idag\") + tab(\"warning\", warnCount, \"Varning\")\n      + '</div>'\n      + '<div class=\"pb-list-head\"><div class=\"pb-lh-agent\">Agent</div><div class=\"pb-lh-action\">Status</div><div class=\"pb-lh-kpi\">Bel &middot; Samtal &middot; Svar</div></div>'\n      + '<div class=\"pb-rows\" id=\"pbRows\">';\n\n    if (!data.filtered.length) {\n      html += '<div class=\"pb-empty\">Inga agenter att visa</div>';\n    }\n    data.filtered.forEach(function (a) {\n      var st = a.st, k = a.kpi;\n      html += '<div class=\"pb-row' + (st.key === \"offline\" ? \" pb-off\" : \"\") + '\">'\n        + '<div class=\"pb-ava\" style=\"background:' + st.def.hex + '\" title=\"' + esc(st.def.label) + '\">' + STATUS_ICON[st.key] + '</div>'\n        + '<div class=\"pb-row-main\"><div class=\"pb-row-name\">' + esc(a.user.fullName) + '</div>'\n        + '<div class=\"pb-row-sub\">' + esc(a.team) + '</div></div>'\n        + '<div class=\"pb-row-action\"><div class=\"pb-row-status\" style=\"color:' + st.def.hex + '\">'\n        + (a.warn && currentTab !== \"warning\" ? \"&#9888; \" : \"\") + esc(st.def.label) + '</div>'\n        + (st.since ? '<div class=\"pb-row-timer\" data-since=\"' + st.since + '\">' + fmtDur(Date.now() - st.since) + '</div>' : '')\n        + '</div>'\n        + '<div class=\"pb-chips\">'\n        + chip(k.bel == null ? \"&ndash;\" : k.bel + \"%\", chipColor(k.bel, CONFIG.belGood, CONFIG.belOk), \"Beläggning\")\n        + chip(String(k.calls), \"rgba(255,255,255,0.14)\", \"Samtal\")\n        + chip(k.svar == null ? \"&ndash;\" : k.svar + \"%\", chipColor(k.svar, CONFIG.svarGood, CONFIG.svarOk), \"Svar\")\n        + '</div></div>';\n    });\n    html += \"</div>\";\n\n    var card = $(\"pbAgentsCard\");\n    var prevScroll = card.querySelector(\".pb-rows\");\n    var scrollTop = prevScroll ? prevScroll.scrollTop : 0;\n    card.innerHTML = html;\n    card.querySelector(\".pb-rows\").scrollTop = scrollTop;\n    Array.prototype.forEach.call(card.querySelectorAll(\".pb-tab\"), function (el) {\n      el.addEventListener(\"click\", function () {\n        currentTab = el.getAttribute(\"data-tab\");\n        renderAgents();\n      });\n    });\n  }\n\n  function tab(id, num, label) {\n    return '<button type=\"button\" class=\"pb-tab' + (currentTab === id ? \" pb-on\" : \"\") + (id === \"warning\" ? \" pb-warn\" : \"\")\n      + '\" data-tab=\"' + id + '\"><span class=\"pb-tab-num\">' + num + '</span><span class=\"pb-tab-label\">' + label + '</span></button>';\n  }\n  function chip(text, bg, title) {\n    return '<div class=\"pb-chip\" style=\"background:' + bg + '\" title=\"' + esc(title) + '\">' + text + '</div>';\n  }\n\n  function renderRight() {\n    var html = \"\";\n    var nowHour = new Date().getHours();\n    activeTeams().forEach(function (t, i) {\n      var accent = teamAccent(i);\n      var team = usersInTeam(t.id);\n      var talk = 0, active = 0, calls = 0;\n      team.forEach(function (u) {\n        var k = agentKpis(u.id);\n        talk += k.talk; calls += k.calls;\n        active += state.activeDur[u.id] || 0;\n      });\n      var bel = active > 0 ? Math.min(100, Math.round((talk / active) * 100)) : 0;\n      var hours = state.hourly[t.id] || new Array(24).fill(0);\n      var winStart = 7, winEnd = Math.max(17, Math.min(23, nowHour));\n      var win = hours.slice(winStart, winEnd + 1);\n      var max = Math.max(1, Math.max.apply(null, win));\n      var bars = win.map(function (v, j) {\n        var h = winStart + j;\n        var pct = Math.max(3, Math.round((v / max) * 100));\n        return '<div class=\"pb-bar' + (h === nowHour ? \" pb-now\" : \"\") + '\" style=\"height:' + pct + '%\" title=\"kl ' + h + ': ' + v + ' samtal\"></div>';\n      }).join(\"\");\n      html += '<div class=\"pb-card\">'\n        + '<div class=\"pb-card-head\"><div><div class=\"pb-card-title\">' + esc(t.name) + '</div>'\n        + '<div class=\"pb-card-sub\">' + calls + ' samtal &middot; ' + fmtHours(talk) + ' taltid</div></div><div class=\"pb-dots\">&#8942;</div></div>'\n        + '<div class=\"pb-kpi-body\"><div class=\"pb-bars\">' + bars + '</div>'\n        + '<div class=\"pb-kpi-stat\" style=\"background:' + accent + '\"><div class=\"pb-kpi-val\">' + bel + '%</div>'\n        + '<div class=\"pb-kpi-label\">Beläggning</div></div></div></div>';\n    });\n    $(\"pbColRight\").innerHTML = html;\n  }\n\n  function renderHeader() {\n    var me = state.me;\n    $(\"pbMe\").innerHTML = me\n      ? '<div class=\"pb-me-avatar\">' + esc(initials(me.fullName)) + '</div>'\n      + '<div><div class=\"pb-me-name\">' + esc(me.fullName || \"\") + '</div>'\n      + '<div class=\"pb-me-sub\">' + esc(me.email || \"\") + '</div></div>'\n      : \"\";\n    $(\"pbPillToday\").className = \"pb-pill\" + (state.period === \"today\" ? \" pb-on\" : \"\");\n    $(\"pbPillHour\").className = \"pb-pill\" + (state.period === \"hour\" ? \" pb-on\" : \"\");\n    if (CONFIG.backgroundUrl) {\n      $(\"pbBg\").style.backgroundImage = \"url('\" + CONFIG.backgroundUrl + \"')\";\n    }\n  }\n\n  function renderBanner() {\n    var b = $(\"pbBanner\");\n    if (state.error) {\n      b.textContent = \"Kunde inte hämta data: \" + state.error + \" — försöker igen…\";\n      b.className = \"pb-banner pb-show\";\n    } else if (state.mock) {\n      b.textContent = \"MOCK-LÄGE — visar testdata (öppna vyn i SaleSys för riktig data)\";\n      b.className = \"pb-banner pb-show pb-mock\";\n    } else {\n      b.className = \"pb-banner\";\n    }\n  }\n\n  function renderAll() {\n    renderHeader();\n    renderBanner();\n    renderLeft();\n    renderAgents();\n    renderRight();\n    if (state.statsLoadedAt) {\n      $(\"pbUpdated\").textContent = \"Uppdaterad \" + state.statsLoadedAt.toLocaleTimeString(\"sv-SE\");\n    }\n  }\n\n  function tickTimers() {\n    Array.prototype.forEach.call(document.querySelectorAll(\".pb-row-timer[data-since]\"), function (el) {\n      var since = parseInt(el.getAttribute(\"data-since\"), 10);\n      if (since) { el.textContent = fmtDur(Date.now() - since); }\n    });\n  }\n\n  /* ================= Polling ================= */\n  function safe(fn) {\n    return function () {\n      if (document.hidden || state.mock) { return Promise.resolve(); }\n      return fn().then(function () {\n        state.error = null;\n      }).catch(function (err) {\n        state.error = err && err.message ? err.message : String(err);\n      });\n    };\n  }\n\n  function startPolling() {\n    var pollStats = safe(loadStats);\n    var pollActive = safe(function () { return Promise.all([loadActiveNow(), loadActiveDurations()]); });\n    var pollSlow = safe(function () { return Promise.all([loadLoggedIn(), loadHourly()]); });\n\n    timers.stats = setInterval(function () { pollStats().then(renderAll); }, CONFIG.pollStatsMs);\n    timers.active = setInterval(function () { pollActive(); }, CONFIG.pollActiveMs);\n    timers.slow = setInterval(function () { pollSlow(); }, CONFIG.pollSlowMs);\n    timers.tick = setInterval(tickTimers, 1000);\n  }\n\n  function refreshPeriod() {\n    var pollAll = safe(function () {\n      return Promise.all([loadStats(), loadActiveDurations()]);\n    });\n    pollAll().then(renderAll);\n    if (state.mock) { loadMock().then(renderAll); }\n  }\n\n  /* ================= Init ================= */\n  function stopAllTimers() {\n    Object.keys(timers).forEach(function (k) {\n      if (timers[k]) { clearInterval(timers[k]); timers[k] = null; }\n    });\n  }\n\n  function init() {\n    //  Städa upp om vyn avmonterats (SPA-navigering).\n    var unmountWatch = setInterval(function () {\n      if (!document.getElementById(\"pb-root\")) {\n        stopAllTimers();\n        clearInterval(unmountWatch);\n      }\n    }, 5000);\n    $(\"pbPillToday\").addEventListener(\"click\", function () {\n      if (state.period !== \"today\") { state.period = \"today\"; renderHeader(); refreshPeriod(); }\n    });\n    $(\"pbPillHour\").addEventListener(\"click\", function () {\n      if (state.period !== \"hour\") { state.period = \"hour\"; renderHeader(); refreshPeriod(); }\n    });\n\n    var useMock = /[?&]mock=1/.test(location.search) || !getToken();\n    if (useMock) {\n      loadMock().then(function () {\n        renderAll();\n        timers.tick = setInterval(tickTimers, 1000);\n      });\n      return;\n    }\n\n    loadBootstrap().then(function () {\n      return Promise.all([\n        safe(loadStats)(), safe(loadActiveNow)(), safe(loadActiveDurations)(),\n        safe(loadLoggedIn)(), safe(loadHourly)()\n      ]);\n    }).then(function () {\n      renderAll();\n      startPolling();\n    }).catch(function (err) {\n      state.error = err && err.message ? err.message : String(err);\n      renderBanner();\n      //  Bootstrap misslyckades — försök igen om 10 s.\n      setTimeout(init, 10000);\n    });\n  }\n\n  //  Vyer monteras av SPA:n — skriptet kan köras innan HTML:en finns i DOM. Vänta in elementen.\n  //  Fyll tillgänglig höjd inom containern (utan att täcka appens navigation).\n  function fitHeight() {\n    var root = document.getElementById(\"pb-root\");\n    if (!root) { return; }\n    var top = root.getBoundingClientRect().top + (window.scrollY || 0);\n    var h = window.innerHeight - top - 16;\n    root.style.height = Math.max(420, Math.round(h)) + \"px\";\n  }\n\n  var bootTries = 0;\n  function boot() {\n    if (document.getElementById(\"pbPillToday\")) {\n      fitHeight();\n      window.addEventListener(\"resize\", fitHeight);\n      init();\n      return;\n    }\n    bootTries += 1;\n    if (bootTries < 200) { setTimeout(boot, 50); }\n  }\n  boot();\n})();\n</script>","iconName":"chart-line","iconColor":"#2eb4c0","settingsHtml":"<div>Playbook är en realtidstavla för teamledare: agentstatus, beläggning och samtal per team. Lägg till vyn i navigationen. Inga inställningar krävs.</div>","description":"","rights":[],"components":[]},{"id":"6a49498fe58b9e7a9eb33f84","name":"Temastudio","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","label":"Teman"}],"html":"<meta charset=\"utf-8\">\n<style>\n  #s2ts-root { max-width: 1020px; margin: 0 auto; padding: 8px 16px 60px; display: flex; flex-direction: column; gap: 24px; font-size: 14px; }\n  .s2ts-card { background: var(--color-background-2); border: 1px solid var(--color-background-3); border-radius: 12px; padding: 20px; }\n  .s2ts-h { font-size: 17px; font-weight: 700; color: var(--color-text-1); margin: 0 0 4px 0; }\n  .s2ts-sub { font-size: 13px; color: var(--color-text-3); margin: 0 0 14px 0; line-height: 1.5; }\n  .s2ts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 12px; }\n  .s2ts-tile { cursor: pointer; border: 2px solid var(--color-background-3); border-radius: 10px; overflow: hidden; transition: transform 120ms ease, border-color 120ms ease; background: var(--color-background-2); position: relative; }\n  .s2ts-tile:hover { transform: translateY(-2px); border-color: var(--color-background-4); }\n  .s2ts-tile.s2ts-active { border-color: var(--color-accent); }\n  .s2ts-swatch { height: 62px; display: flex; align-items: center; justify-content: center; gap: 7px; }\n  .s2ts-dot { width: 15px; height: 15px; border-radius: 50%; border: 1px solid rgba(128, 128, 128, 0.4); box-sizing: border-box; }\n  .s2ts-tile-name { padding: 8px 10px; font-size: 12.5px; font-weight: 600; color: var(--color-text-2); display: flex; justify-content: space-between; align-items: center; gap: 6px; min-height: 34px; box-sizing: border-box; }\n  .s2ts-tile-kind { display: block; font-size: 10.5px; font-weight: 500; color: var(--color-text-3); }\n  .s2ts-check { color: var(--color-accent); font-weight: 800; }\n  .s2ts-remove { border: none; background: transparent; color: var(--color-text-4); cursor: pointer; font-size: 14px; padding: 0 2px; line-height: 1; }\n  .s2ts-remove:hover { color: var(--color-danger); }\n  .s2ts-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }\n  .s2ts-input { flex: 1; min-width: 240px; padding: 10px 12px; border: 1px solid var(--color-background-4); border-radius: 8px; background: var(--color-input-background); color: var(--color-text-1); font-size: 14px; outline: none; font-family: inherit; }\n  .s2ts-input:focus { border-color: var(--color-accent); }\n  .s2ts-btn { padding: 9px 16px; border-radius: 8px; border: 1px solid var(--color-background-4); background: var(--color-background-3); color: var(--color-text-1); cursor: pointer; font-size: 13.5px; font-weight: 600; font-family: inherit; }\n  .s2ts-btn:hover { border-color: var(--color-accent); }\n  .s2ts-btn.s2ts-primary { background: var(--color-accent); border-color: var(--color-accent); color: #ffffff; }\n  .s2ts-btn:disabled { opacity: 0.5; cursor: default; }\n  .s2ts-status { font-size: 13px; color: var(--color-text-3); min-height: 18px; margin-top: 10px; }\n  .s2ts-status.s2ts-ok { color: var(--color-good); }\n  .s2ts-status.s2ts-err { color: var(--color-danger); }\n  .s2ts-result { display: flex; gap: 14px; align-items: center; margin-top: 14px; padding: 12px 14px; border: 1px dashed var(--color-background-4); border-radius: 10px; }\n  .s2ts-result-logo { width: 56px; height: 56px; object-fit: contain; flex-shrink: 0; background: rgba(128, 128, 128, 0.08); border-radius: 8px; padding: 4px; box-sizing: border-box; }\n  .s2ts-result-info { flex: 1; min-width: 0; }\n  .s2ts-result-name { font-weight: 700; color: var(--color-text-1); }\n  .s2ts-result-detail { font-size: 12.5px; color: var(--color-text-3); display: flex; align-items: center; gap: 6px; margin-top: 3px; }\n  .s2ts-chip { display: inline-block; width: 13px; height: 13px; border-radius: 4px; border: 1px solid rgba(128, 128, 128, 0.4); vertical-align: -2px; }\n  .s2ts-bar { position: fixed; left: 50%; transform: translateX(-50%); bottom: 20px; z-index: 999990; background: var(--color-background-2); border: 1px solid var(--color-background-4); border-radius: 12px; padding: 10px 14px; display: flex; gap: 12px; align-items: center; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35); }\n  .s2ts-bar-label { font-size: 13px; color: var(--color-text-2); font-weight: 600; }\n  .s2ts-active-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n  .s2ts-footer { font-size: 12px; color: var(--color-text-4); line-height: 1.6; }\n</style>\n\n<div id=\"s2ts-root\"></div>\n\n<script>\n  (function () {\n    'use strict';\n\n    var BF_KEY = 'co3tw+doPXXIlTVJCe5GwUAjSxzFybNNYDkmSuqnXqo=';\n    var CONFIG_BASE = 'https://salesys-config.s3.eu-north-1.amazonaws.com';\n    var LS_THEMES = 'testThemes';\n    var LS_STATE = 's2ThemeStudio';\n    var LOGO_MARKER = 's2-brand-logo';\n    var EMPTY_CSS_URI = 'data:text/css;charset=utf-8,' + encodeURIComponent('/* SaleSys Temastudio */');\n\n    /* ---------------- Color utilities ---------------- */\n\n    function parseHex(hex) {\n      var h = String(hex || '').trim().replace(/^#/, '');\n      if (/^[0-9a-f]{3}$/i.test(h)) {\n        h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);\n      }\n      if (!/^[0-9a-f]{6}$/i.test(h)) { return null; }\n      return {\n        r: parseInt(h.slice(0, 2), 16),\n        g: parseInt(h.slice(2, 4), 16),\n        b: parseInt(h.slice(4, 6), 16)\n      };\n    }\n\n    function tryParseColor(value) {\n      var str = String(value || '').trim();\n      var hex = parseHex(str);\n      if (hex) { return hex; }\n      var m = /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*(?:,\\s*([\\d.]+)\\s*)?\\)$/i.exec(str);\n      if (m) {\n        return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]), a: m[4] === undefined ? 1 : Number(m[4]) };\n      }\n      return null;\n    }\n\n    function rgbToHsl(c) {\n      var r = c.r / 255, g = c.g / 255, b = c.b / 255;\n      var max = Math.max(r, g, b), min = Math.min(r, g, b);\n      var h = 0, s = 0, l = (max + min) / 2;\n      if (max !== min) {\n        var d = max - min;\n        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n        if (max === r) { h = (g - b) / d + (g < b ? 6 : 0); }\n        else if (max === g) { h = (b - r) / d + 2; }\n        else { h = (r - g) / d + 4; }\n        h = h / 6;\n      }\n      return { h: h, s: s, l: l };\n    }\n\n    function hslToHex(h, s, l) {\n      function f(n) {\n        var k = (n + h * 12) % 12;\n        var a = s * Math.min(l, 1 - l);\n        var v = l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));\n        return Math.round(v * 255).toString(16).padStart(2, '0');\n      }\n      return '#' + f(0) + f(8) + f(4);\n    }\n\n    function vividness(hex) {\n      var rgb = parseHex(hex);\n      if (!rgb) { return 0; }\n      var hsl = rgbToHsl(rgb);\n      return hsl.s * (1 - Math.abs(hsl.l - 0.5) * 1.6);\n    }\n\n    /* ---------------- Theme construction ---------------- */\n\n    var DEFAULTS = {\n      colorBackground1: '#fff', colorBackground2: '#fafafa', colorBackground3: '#eee', colorBackground4: '#bdbdbd',\n      colorPrimary: '#1565C0', colorDanger: '#E53935',\n      colorButtonBackgroundNormal: 'transparent', colorButtonBackgroundDanger: 'transparent', colorButtonBackgroundDimmed: 'transparent',\n      colorInputBackground: 'transparent',\n      colorCplBorder: 'rgba(20, 20, 20, 0.035)', colorCplBackground: '#fafafa',\n      colorElementHover: 'rgba(20, 20, 20, 0.035)',\n      colorText1: '#212121', colorText2: '#616161', colorText3: '#757575', colorText4: '#9e9e9e',\n      colorGood: '#43A047', colorNeutral: '#F4511E',\n      fontFamilyDefault: 'Nunito, sans-serif', fontWeightDefault: 'normal', fontWeightSignWidget: '600',\n      borderRadiusButton: 8, paddingButtonNormalHorizontal: 10\n    };\n\n    //  Replicates the fallback logic in the app's Theme constructor, so that\n    //  instant (pre-refresh) application looks identical to the app's own.\n    function normalizeTheme(partial) {\n      var out = {};\n      var key;\n      for (key in DEFAULTS) { out[key] = DEFAULTS[key]; }\n      for (key in partial) {\n        if (partial[key] !== null && partial[key] !== undefined) { out[key] = partial[key]; }\n      }\n      out.colorPrimaryHover = out.colorPrimaryHover || out.colorPrimary;\n      out.colorButtonSimple = out.colorButtonSimple || out.colorPrimary;\n      out.colorButtonNormal = out.colorButtonNormal || out.colorButtonSimple;\n      out.colorButtonBackgroundDimmed = out.colorButtonBackgroundDimmed || out.colorButtonBackgroundNormal;\n      out.colorBackground2 = out.colorBackground2 || out.colorBackground1;\n      out.colorAccent = out.colorAccent || out.colorPrimary;\n      if (!out.colorElementActive) {\n        var c = tryParseColor(out.colorElementHover);\n        if (c) {\n          out.colorElementActive = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + Math.min(1, (c.a === undefined ? 1 : c.a) * 2) + ')';\n        }\n      }\n      return out;\n    }\n\n    //  Same regex the app uses: \"colorBackground1\" -> \"--color-background-1\"\n    function cssVarName(key) {\n      return '--' + key.replace(/([A-Z|\\d])(\\d+|\\w)?/g, ' $1$2').replace(/\\s/g, '-').toLowerCase();\n    }\n\n    function headElement(tagName, id) {\n      var element = document.getElementById(id) || document.createElement(tagName);\n      element.id = id;\n      element.remove();\n      document.head.appendChild(element);\n      return element;\n    }\n\n    //  Replicates the app's setTheme(): writes CSS variables to style#__theme-root\n    //  and stylesheet links for fontFamilyUrls.\n    function applyThemeToDom(theme) {\n      var lines = [];\n      Object.keys(theme).forEach(function (key) {\n        if (!/^color|padding|fontWeight|fontFamily|borderRadius/.test(key)) { return; }\n        var value = theme[key];\n        if (value === undefined || value === null) { return; }\n        if (key === 'borderRadiusButton' || key === 'paddingButtonNormalHorizontal') { value = value + 'px'; }\n        var name = cssVarName(key);\n        var rgb = tryParseColor(value);\n        if (rgb) { lines.push(name + '-rgb: ' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ';'); }\n        lines.push(name + ': ' + value + ';');\n      });\n      if (theme.isDark) { lines.push('color-scheme: dark;'); }\n\n      var style = headElement('style', '__theme-root');\n      style.innerHTML = ':root ' + '{\\n' + lines.join('\\n') + '\\ncolor: var(--color-text-2);\\nfont-weight: var(--font-weight-default);\\n}';\n\n      var urls = {};\n      var source = theme.fontFamilyUrls || {};\n      Object.keys(source).forEach(function (name) { urls[name] = source[name]; });\n      if (!urls.s2extras) { urls.s2extras = EMPTY_CSS_URI; }\n      Object.keys(urls).forEach(function (name) {\n        if (typeof urls[name] !== 'string') { return; }\n        var link = headElement('link', 'font-' + name);\n        link.setAttribute('rel', 'stylesheet');\n        link.setAttribute('href', urls[name]);\n      });\n    }\n\n    function isDarkPreferred() {\n      var pref = window.sessionStorage.getItem('isDarkModePreferred');\n      if (pref === 'true') { return true; }\n      if (pref === 'false') { return false; }\n      return Boolean(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);\n    }\n\n    function cssUri(css) {\n      return 'data:text/css;charset=utf-8,' + encodeURIComponent(css || '/* SaleSys Temastudio */');\n    }\n\n    /* ---------------- Theme generators ---------------- */\n\n    function makeLightFrom(hex, id, name) {\n      var hsl = rgbToHsl(parseHex(hex));\n      var l = hsl.l;\n      if (l > 0.62) { l = 0.45; }\n      if (l < 0.16) { l = 0.24; }\n      return {\n        id: id, name: name, isDark: false,\n        colorPrimary: hslToHex(hsl.h, hsl.s, l)\n      };\n    }\n\n    function makeDarkFrom(hex, id, name) {\n      var hsl = rgbToHsl(parseHex(hex));\n      var s = Math.max(0.22, Math.min(hsl.s, 0.74));\n      var accent = hex;\n      if (hsl.l < 0.32) { accent = hslToHex(hsl.h, Math.max(hsl.s, 0.4), 0.45); }\n      if (hsl.l > 0.72) { accent = hslToHex(hsl.h, Math.max(hsl.s, 0.4), 0.55); }\n      function bg(l) { return hslToHex(hsl.h, s, l); }\n      return {\n        id: id, name: name, isDark: true,\n        colorPrimary: '#CFD8DC',\n        colorBackground1: bg(0.075), colorBackground2: bg(0.125), colorBackground3: bg(0.195), colorBackground4: bg(0.285),\n        colorText1: '#EEEEEE', colorText2: '#E0E0E0', colorText3: '#90A4AE', colorText4: '#78909C',\n        colorAccent: accent,\n        colorElementHover: 'rgba(255, 255, 255, 0.1)',\n        colorDanger: '#E57373'\n      };\n    }\n\n    var MATRIX_CSS = [\n      'body { text-shadow: 0 0 6px rgba(0, 255, 65, 0.3); }',\n      /* Monospace labels are wider, so the navbar chevron ends up glued to the text. */\n      'li .fa-chevron-down { margin-left: 5px !important; }',\n      'body::after { content: \"\"; position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 2147483000; background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.14) 0px, rgba(0, 0, 0, 0.14) 1px, transparent 1px, transparent 3px); }',\n      '::selection { background: rgba(0, 230, 118, 0.35); color: #E8FFE8; }',\n      '::-webkit-scrollbar { width: 12px; height: 12px; background: #020B02; }',\n      '::-webkit-scrollbar-thumb { background: #0E3B0E; border: 1px solid #1C641C; }',\n      '::-webkit-scrollbar-thumb:hover { background: #145214; }'\n    ].join('\\n');\n\n    var XP_CSS = [\n      'button { border: 1px solid #003C74 !important; border-radius: 3px !important; background: linear-gradient(180deg, #FFFFFF 0%, #F4F3EE 42%, #E8E4D8 85%, #D8D0BF 100%) !important; box-shadow: none !important; }',\n      'button:hover { border-color: #E68B2C !important; box-shadow: inset 0 0 0 1px rgba(230, 139, 44, 0.45) !important; }',\n      'button:active { background: linear-gradient(180deg, #D8D0BF 0%, #E8E4D8 60%, #F4F3EE 100%) !important; }',\n      'input, textarea, select { background: #FFFFFF !important; border-color: #7F9DB9 !important; border-radius: 0 !important; }',\n      '::-webkit-scrollbar { width: 17px; height: 17px; background: #F0EFE7; }',\n      '::-webkit-scrollbar-thumb { background: linear-gradient(90deg, #CDDBF8 0%, #B5CBF3 50%, #8FB2EE 100%); border: 1px solid #7A96DF; border-radius: 3px; }',\n      '::-webkit-scrollbar-thumb:hover { background: linear-gradient(90deg, #DBE6FB 0%, #C4D6F7 50%, #A3C0F2 100%); }'\n    ].join('\\n');\n\n    function logoCss(url) {\n      if (!url) { return ''; }\n      return 'img[src*=\"' + LOGO_MARKER + '\"] ' +\n        '{ content: url(\"' + url + '\"); opacity: 1 !important; visibility: visible !important; animation: none !important; max-width: 180px; }';\n    }\n\n    /* ---------------- Library ---------------- */\n\n    var POPULAR = [\n      ['pop-red', 'Röd', '#C62828'],\n      ['pop-pink', 'Rosa', '#C2185B'],\n      ['pop-purple', 'Lila', '#7B1FA2'],\n      ['pop-indigo', 'Indigo', '#303F9F'],\n      ['pop-blue', 'Ljusblå', '#0277BD'],\n      ['pop-cyan', 'Cyan', '#00838F'],\n      ['pop-teal', 'Turkos', '#00695C'],\n      ['pop-green', 'Grön', '#2E7D32'],\n      ['pop-amber', 'Gul', '#F9A825'],\n      ['pop-orange', 'Orange', '#E64A19'],\n      ['pop-brown', 'Brun', '#5D4037'],\n      ['pop-bluegrey', 'Blågrå', '#455A64']\n    ];\n\n    var MATRIX_ITEM = {\n      id: 'matrix', name: 'Matrix', kind: 'special', hex: '#00E676', extrasCss: MATRIX_CSS,\n      dark: {\n        id: 'matrix', name: 'Matrix', isDark: true,\n        colorPrimary: '#AAFFAA',\n        colorBackground1: '#020B02', colorBackground2: '#041504', colorBackground3: '#0A2E0A', colorBackground4: '#124612',\n        colorText1: '#D6FFD6', colorText2: '#7CE87C', colorText3: '#4CAF50', colorText4: '#2E7D32',\n        colorAccent: '#00E676', colorGood: '#00E676', colorDanger: '#FF5252',\n        colorElementHover: 'rgba(0, 255, 100, 0.08)',\n        fontFamilyDefault: '\"Courier New\", \"Lucida Console\", monospace',\n        borderRadiusButton: 0\n      }\n    };\n\n    var XP_ITEM = {\n      id: 'xp', name: 'Windows XP', kind: 'special', hex: '#3A6EA5', extrasCss: XP_CSS,\n      light: {\n        id: 'xp', name: 'Windows XP', isDark: false,\n        colorPrimary: '#3A6EA5', colorAccent: '#316AC5',\n        colorBackground1: '#ECE9D8', colorBackground2: '#FFFFFF', colorBackground3: '#E3DFD0', colorBackground4: '#ACA899',\n        colorText1: '#000000', colorText2: '#333333', colorText3: '#555555', colorText4: '#9C9884',\n        colorGood: '#5EA832', colorDanger: '#D64545',\n        colorElementHover: 'rgba(49, 106, 197, 0.12)',\n        colorInputBackground: '#FFFFFF',\n        fontFamilyDefault: 'Tahoma, Verdana, \"Segoe UI\", sans-serif',\n        borderRadiusButton: 3\n      }\n    };\n\n    function popularItem(def) {\n      return {\n        id: def[0], name: def[1], kind: 'color', hex: def[2],\n        light: makeLightFrom(def[2], def[0], def[1]),\n        dark: makeDarkFrom(def[2], def[0], def[1] + ', mörk')\n      };\n    }\n\n    function savedToItem(saved) {\n      var item = {\n        id: saved.id, name: saved.name, kind: saved.kind, hex: saved.hex,\n        light: makeLightFrom(saved.hex, saved.id, saved.name),\n        dark: makeDarkFrom(saved.hex, saved.id, saved.name + ', mörk'),\n        logoForLight: saved.logoForLight || null,\n        logoForDark: saved.logoForDark || null,\n        domain: saved.domain || null\n      };\n      return item;\n    }\n\n    var LIBRARY = [XP_ITEM, MATRIX_ITEM].concat(POPULAR.map(popularItem));\n\n    /* ---------------- State ---------------- */\n\n    function readState() {\n      try {\n        var state = JSON.parse(localStorage.getItem(LS_STATE) || '0');\n        if (state && typeof state === 'object') {\n          state.saved = Array.isArray(state.saved) ? state.saved : [];\n          return state;\n        }\n      } catch (error) { /* corrupt state, start over */ }\n      return { appliedId: null, appliedName: null, saved: [] };\n    }\n\n    var state = readState();\n\n    function writeState() {\n      localStorage.setItem(LS_STATE, JSON.stringify(state));\n    }\n\n    /* ---------------- Variants & persistence ---------------- */\n\n    function extrasFor(item, wantDark) {\n      var css = item.extrasCss || '';\n      var logo = wantDark ? (item.logoForDark || item.logoForLight) : (item.logoForLight || item.logoForDark);\n      if (logo) { css = css + '\\n' + logoCss(logo); }\n      return css;\n    }\n\n    function variantOf(item, wantDark, id) {\n      var base = wantDark ? (item.dark || item.light) : (item.light || item.dark);\n      var out = {};\n      for (var key in base) { out[key] = base[key]; }\n      out.isDark = wantDark;\n      out.id = id || out.id;\n      var urls = {};\n      var source = out.fontFamilyUrls || {};\n      for (var name in source) { urls[name] = source[name]; }\n      urls.s2extras = cssUri(extrasFor(item, wantDark));\n      out.fontFamilyUrls = urls;\n      if (item.logoForLight || item.logoForDark) {\n        out.logo = LOGO_MARKER + '.png';\n      }\n      return out;\n    }\n\n    //  Library/saved entries listed under their own ids: no logo field, so the\n    //  header logo only appears for the applied (\"default\") brand theme.\n    function listEntriesOf(item) {\n      var entries = [];\n      if (item.light) {\n        var light = variantOf(item, false);\n        delete light.logo;\n        entries.push(light);\n      }\n      if (item.dark) {\n        var dark = variantOf(item, true);\n        delete dark.logo;\n        entries.push(dark);\n      }\n      return entries;\n    }\n\n    var commonThemesPromise = null;\n    function fetchCommonThemes() {\n      if (!commonThemesPromise) {\n        commonThemesPromise = fetch(new URL('/themes/common.json', CONFIG_BASE).href, { cache: 'no-cache' })\n          .then(function (response) { return response.json(); })\n          .catch(function () { return []; });\n      }\n      return commonThemesPromise;\n    }\n\n    //  Writes the full theme list to localStorage.testThemes. The app reads this\n    //  key natively on every load, so the applied theme survives refresh and the\n    //  next login. The applied theme is duplicated under id \"default\" (light +\n    //  dark) because that id is auto-selected by the app.\n    function persistApply(item) {\n      return fetchCommonThemes().then(function (common) {\n        var list = [];\n        list.push(variantOf(item, false, 'default'));\n        list.push(variantOf(item, true, 'default'));\n\n        LIBRARY.concat(state.saved.map(savedToItem)).forEach(function (libItem) {\n          listEntriesOf(libItem).forEach(function (entry) { list.push(entry); });\n        });\n\n        (common || []).forEach(function (theme) {\n          var copy = {};\n          for (var key in theme) { copy[key] = theme[key]; }\n          if (copy.id === 'default') { copy.id = 'standard'; copy.name = (copy.name || 'Standard'); }\n          var urls = {};\n          var source = copy.fontFamilyUrls || {};\n          for (var name in source) { urls[name] = source[name]; }\n          if (!urls.s2extras) { urls.s2extras = EMPTY_CSS_URI; }\n          copy.fontFamilyUrls = urls;\n          list.push(copy);\n        });\n\n        localStorage.setItem(LS_THEMES, JSON.stringify(list));\n\n        state.appliedId = item.id;\n        state.appliedName = item.name;\n        writeState();\n      });\n    }\n\n    function resetToStandard() {\n      localStorage.removeItem(LS_THEMES);\n      state.appliedId = null;\n      state.appliedName = null;\n      writeState();\n      removeSessionLogo();\n      return fetchCommonThemes().then(function (common) {\n        var theme = null;\n        if (isDarkPreferred()) {\n          theme = (common || []).filter(function (t) { return t.id === 'default' && t.isDark; })[0] || null;\n        }\n        applyThemeToDom(normalizeTheme(theme || { id: 'default' }));\n      });\n    }\n\n    /* ---------------- Session logo (instant preview before refresh) ---------------- */\n\n    function removeSessionLogo() {\n      var img = document.getElementById('s2ts-session-logo');\n      if (img) { img.remove(); }\n    }\n\n    function injectSessionLogo(item) {\n      removeSessionLogo();\n      var url = isDarkPreferred() ? (item.logoForDark || item.logoForLight) : (item.logoForLight || item.logoForDark);\n      if (!url) { return; }\n      if (document.querySelector('img[src*=\"/logos/\"]')) { return; }\n      var img = document.createElement('img');\n      img.id = 's2ts-session-logo';\n      img.src = url;\n      img.style.cssText = 'position: fixed; top: 8px; right: 16px; height: 34px; max-width: 170px; object-fit: contain; z-index: 999980; pointer-events: none;';\n      document.body.appendChild(img);\n    }\n\n    /* ---------------- Preview (with restore snapshot) ---------------- */\n\n    var snapshot = null;\n\n    function takeSnapshot() {\n      if (snapshot) { return; }\n      var style = document.getElementById('__theme-root');\n      var links = {};\n      document.querySelectorAll('link[id^=\"font-\"]').forEach(function (link) {\n        links[link.id] = link.getAttribute('href');\n      });\n      snapshot = { styleHtml: style ? style.innerHTML : null, links: links, hadLogo: Boolean(document.getElementById('s2ts-session-logo')) };\n    }\n\n    function restoreSnapshot() {\n      if (!snapshot) { return; }\n      if (snapshot.styleHtml !== null) {\n        var style = headElement('style', '__theme-root');\n        style.innerHTML = snapshot.styleHtml;\n      }\n      Object.keys(snapshot.links).forEach(function (id) {\n        var link = headElement('link', id);\n        link.setAttribute('rel', 'stylesheet');\n        link.setAttribute('href', snapshot.links[id]);\n      });\n      if (!snapshot.hadLogo) { removeSessionLogo(); }\n      snapshot = null;\n    }\n\n    function previewItem(item) {\n      takeSnapshot();\n      var wantDark = isDarkPreferred();\n      applyThemeToDom(normalizeTheme(variantOf(item, wantDark)));\n      if (item.logoForLight || item.logoForDark) { injectSessionLogo(item); }\n      else { removeSessionLogo(); }\n    }\n\n    /* ---------------- Brandfetch ---------------- */\n\n    function fetchBrand(domain) {\n      return fetch('https://api.brandfetch.io/v2/brands/' + encodeURIComponent(domain), {\n        headers: { 'Authorization': 'Bearer ' + BF_KEY }\n      }).then(function (response) {\n        if (!response.ok) { throw new Error('Hittade inget varumärke för \"' + domain + '\" (' + response.status + ')'); }\n        return response.json();\n      }).then(function (data) {\n        var colors = (data.colors || []).filter(function (c) { return parseHex(c.hex); });\n        var hex = null;\n        var branded = colors.filter(function (c) { return c.type === 'brand' || c.type === 'accent'; });\n        var pool = branded.length ? branded : colors;\n        pool.forEach(function (c) {\n          if (hex === null || vividness(c.hex) > vividness(hex)) { hex = c.hex; }\n        });\n        if (!hex && colors.length) { hex = colors[0].hex; }\n        if (!hex) { hex = '#1565C0'; }\n\n        function pickLogo(theme) {\n          var order = ['logo', 'symbol', 'icon', 'other'];\n          var logos = (data.logos || []).slice().sort(function (a, b) {\n            return order.indexOf(a.type) - order.indexOf(b.type);\n          });\n          var match = null;\n          logos.forEach(function (l) { if (!match && l.theme === theme) { match = l; } });\n          match = match || logos[0] || null;\n          if (!match) { return null; }\n          var formats = match.formats || [];\n          var best = null;\n          formats.forEach(function (f) {\n            if (!best) { best = f; }\n            else if (f.format === 'svg' && best.format !== 'svg') { best = f; }\n            else if (f.format === 'png' && best.format !== 'svg' && best.format !== 'png') { best = f; }\n          });\n          return best ? best.src : null;\n        }\n\n        return {\n          name: data.name || domain,\n          domain: (data.domain || domain).toLowerCase(),\n          hex: hex,\n          logoForLight: pickLogo('dark'),\n          logoForDark: pickLogo('light')\n        };\n      });\n    }\n\n    /* ---------------- UI helpers ---------------- */\n\n    function el(tag, className, text) {\n      var node = document.createElement(tag);\n      if (className) { node.className = className; }\n      if (text !== undefined && text !== null) { node.textContent = text; }\n      return node;\n    }\n\n    function dot(color) {\n      var d = el('span', 's2ts-dot');\n      d.style.background = color;\n      return d;\n    }\n\n    /* ---------------- UI ---------------- */\n\n    var refs = {};\n\n    function setStatus(node, message, type) {\n      node.textContent = message || '';\n      node.className = 's2ts-status' + (type === 'ok' ? ' s2ts-ok' : type === 'err' ? ' s2ts-err' : '');\n    }\n\n    function hideBar() {\n      if (refs.bar) { refs.bar.remove(); refs.bar = null; }\n    }\n\n    function showBar(item) {\n      hideBar();\n      var bar = el('div', 's2ts-bar');\n      bar.appendChild(el('span', 's2ts-bar-label', 'Förhandsvisar: ' + item.name));\n\n      var apply = el('button', 's2ts-btn s2ts-primary', 'Använd tema');\n      apply.onclick = function () {\n        apply.disabled = true;\n        applyItem(item).then(function () {\n          hideBar();\n          snapshot = null;\n        });\n      };\n      bar.appendChild(apply);\n\n      var cancel = el('button', 's2ts-btn', 'Avbryt');\n      cancel.onclick = function () {\n        restoreSnapshot();\n        hideBar();\n        renderGallery();\n      };\n      bar.appendChild(cancel);\n\n      document.body.appendChild(bar);\n      refs.bar = bar;\n    }\n\n    function applyItem(item) {\n      var wantDark = isDarkPreferred();\n      applyThemeToDom(normalizeTheme(variantOf(item, wantDark)));\n      if (item.logoForLight || item.logoForDark) { injectSessionLogo(item); }\n      else { removeSessionLogo(); }\n\n      if (item.kind === 'brand' || item.kind === 'hex') {\n        var exists = state.saved.filter(function (s) { return s.id === item.id; }).length > 0;\n        if (!exists) {\n          state.saved.unshift({\n            id: item.id, name: item.name, kind: item.kind, hex: item.hex,\n            domain: item.domain || null, logoForLight: item.logoForLight || null, logoForDark: item.logoForDark || null\n          });\n          state.saved = state.saved.slice(0, 24);\n        }\n      }\n\n      return persistApply(item).then(function () {\n        renderActive();\n        renderGallery();\n        setStatus(refs.libraryStatus, 'Temat \"' + item.name + '\" är aktivt och sparat. Det gäller även efter omladdning och nästa inloggning i den här webbläsaren.', 'ok');\n      });\n    }\n\n    function tileFor(item, removable) {\n      var tile = el('div', 's2ts-tile' + (state.appliedId === item.id ? ' s2ts-active' : ''));\n\n      var swatch = el('div', 's2ts-swatch');\n      var variant = item.dark || item.light || {};\n      var bg = variant.colorBackground1 || (variant.isDark ? '#111' : '#ffffff');\n      swatch.style.background = bg;\n      swatch.appendChild(dot(variant.colorBackground3 || 'rgba(128, 128, 128, 0.25)'));\n      swatch.appendChild(dot(variant.colorAccent || variant.colorPrimary || item.hex || '#1565C0'));\n      swatch.appendChild(dot(item.hex || variant.colorPrimary || '#1565C0'));\n      tile.appendChild(swatch);\n\n      var nameRow = el('div', 's2ts-tile-name');\n      var nameCol = el('div', null);\n      nameCol.appendChild(el('span', null, item.name));\n      nameCol.appendChild(el('span', 's2ts-tile-kind', item.kind === 'special' ? 'Komplett tema' : 'Accentfärg'));\n      nameRow.appendChild(nameCol);\n      if (state.appliedId === item.id) {\n        nameRow.appendChild(el('span', 's2ts-check', '✓'));\n      } else if (removable) {\n        var remove = el('button', 's2ts-remove', '×');\n        remove.title = 'Ta bort sparat tema';\n        remove.onclick = function (event) {\n          event.stopPropagation();\n          state.saved = state.saved.filter(function (s) { return s.id !== item.id; });\n          writeState();\n          renderGallery();\n        };\n        nameRow.appendChild(remove);\n      }\n      tile.appendChild(nameRow);\n\n      tile.onclick = function () {\n        previewItem(item);\n        showBar(item);\n        renderGallery();\n      };\n\n      return tile;\n    }\n\n    function renderActive() {\n      var line = refs.activeLine;\n      line.textContent = '';\n      var label = state.appliedName ? 'Aktivt tema: ' + state.appliedName : 'Inget eget tema aktivt – standardteman används.';\n      line.appendChild(el('span', 's2ts-sub', label)).style.margin = '0';\n\n      if (state.appliedId) {\n        var reset = el('button', 's2ts-btn', 'Återställ till standard');\n        reset.onclick = function () {\n          reset.disabled = true;\n          resetToStandard().then(function () {\n            reset.disabled = false;\n            renderActive();\n            renderGallery();\n            setStatus(refs.libraryStatus, 'Standardteman återställda.', 'ok');\n          });\n        };\n        line.appendChild(reset);\n      }\n    }\n\n    function renderGallery() {\n      var grid = refs.libraryGrid;\n      grid.textContent = '';\n      LIBRARY.forEach(function (item) { grid.appendChild(tileFor(item, false)); });\n\n      var savedGrid = refs.savedGrid;\n      savedGrid.textContent = '';\n      if (state.saved.length) {\n        refs.savedSection.style.display = '';\n        state.saved.map(savedToItem).forEach(function (item) { savedGrid.appendChild(tileFor(item, true)); });\n      } else {\n        refs.savedSection.style.display = 'none';\n      }\n    }\n\n    function renderBuilderResult(item) {\n      var box = refs.builderResult;\n      box.textContent = '';\n      box.style.display = 'flex';\n\n      var logo = item.logoForLight || item.logoForDark;\n      if (logo) {\n        var img = el('img', 's2ts-result-logo');\n        img.src = logo;\n        box.appendChild(img);\n      }\n\n      var info = el('div', 's2ts-result-info');\n      info.appendChild(el('div', 's2ts-result-name', item.name));\n      var detail = el('div', 's2ts-result-detail');\n      var chip = el('span', 's2ts-chip');\n      chip.style.background = item.hex;\n      detail.appendChild(chip);\n      detail.appendChild(el('span', null, item.hex.toUpperCase() + (item.domain ? ' · ' + item.domain : '')));\n      info.appendChild(detail);\n      box.appendChild(info);\n\n      var preview = el('button', 's2ts-btn', 'Förhandsvisa');\n      preview.onclick = function () {\n        previewItem(item);\n        showBar(item);\n      };\n      box.appendChild(preview);\n\n      var apply = el('button', 's2ts-btn s2ts-primary', 'Använd & spara');\n      apply.onclick = function () {\n        apply.disabled = true;\n        applyItem(item).then(function () {\n          apply.disabled = false;\n          hideBar();\n          snapshot = null;\n          setStatus(refs.builderStatus, 'Temat är aktivt och sparat i biblioteket nedan.', 'ok');\n        });\n      };\n      box.appendChild(apply);\n    }\n\n    function runBuilder() {\n      var raw = refs.builderInput.value.trim();\n      if (!raw) { return; }\n      setStatus(refs.builderStatus, '');\n      refs.builderResult.style.display = 'none';\n\n      var hexMatch = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(raw);\n      if (hexMatch) {\n        var hex = '#' + (hexMatch[1].length === 3 ?\n          hexMatch[1].split('').map(function (c) { return c + c; }).join('') :\n          hexMatch[1]).toUpperCase();\n        renderBuilderResult({\n          id: 'hex-' + hex.slice(1).toLowerCase(),\n          name: 'Egen – ' + hex.toUpperCase(),\n          kind: 'hex', hex: hex,\n          light: makeLightFrom(hex, 'hex-' + hex.slice(1).toLowerCase(), 'Egen – ' + hex.toUpperCase()),\n          dark: makeDarkFrom(hex, 'hex-' + hex.slice(1).toLowerCase(), 'Egen – ' + hex.toUpperCase() + ', mörk')\n        });\n        return;\n      }\n\n      var domain = raw.toLowerCase().replace(/^https?:\\/\\//, '').replace(/^www\\./, '').split('/')[0];\n      if (!/^[a-z0-9.-]+\\.[a-z]{2,}$/.test(domain)) {\n        setStatus(refs.builderStatus, 'Ange en giltig HEX-färg (t.ex. #E91E63) eller domän (t.ex. spotify.com).', 'err');\n        return;\n      }\n\n      setStatus(refs.builderStatus, 'Hämtar varumärke för ' + domain + '…');\n      refs.builderButton.disabled = true;\n\n      fetchBrand(domain).then(function (brand) {\n        refs.builderButton.disabled = false;\n        setStatus(refs.builderStatus, '');\n        var id = 'brand-' + brand.domain.replace(/[^a-z0-9]+/g, '-');\n        renderBuilderResult({\n          id: id, name: brand.name, kind: 'brand', hex: brand.hex, domain: brand.domain,\n          light: makeLightFrom(brand.hex, id, brand.name),\n          dark: makeDarkFrom(brand.hex, id, brand.name + ', mörk'),\n          logoForLight: brand.logoForLight,\n          logoForDark: brand.logoForDark\n        });\n      }).catch(function (error) {\n        refs.builderButton.disabled = false;\n        setStatus(refs.builderStatus, error && error.message ? error.message : 'Något gick fel.', 'err');\n      });\n    }\n\n    function render(root) {\n      root.textContent = '';\n\n      //  Active theme\n      var activeCard = el('div', 's2ts-card');\n      activeCard.appendChild(el('div', 's2ts-h', 'Temastudio'));\n      activeCard.appendChild(el('p', 's2ts-sub', 'Välj ett tema ur biblioteket eller bygg ett eget från en färg eller ett varumärke. Temat sparas i webbläsaren och gäller automatiskt vid omladdning och nästa inloggning.'));\n      var activeLine = el('div', 's2ts-active-line');\n      activeCard.appendChild(activeLine);\n      refs.activeLine = activeLine;\n      root.appendChild(activeCard);\n\n      //  Builder\n      var builderCard = el('div', 's2ts-card');\n      builderCard.appendChild(el('div', 's2ts-h', 'Temabyggare'));\n      builderCard.appendChild(el('p', 's2ts-sub', 'Ange en HEX-färg (t.ex. #E91E63) eller en domän (t.ex. spotify.com). Vid domän hämtas varumärkets färg och logotyp automatiskt, och logotypen visas uppe till höger precis som vanligt.'));\n      var row = el('div', 's2ts-row');\n      var input = el('input', 's2ts-input');\n      input.setAttribute('placeholder', '#HEX eller domän…');\n      input.onkeydown = function (event) { if (event.key === 'Enter') { runBuilder(); } };\n      row.appendChild(input);\n      refs.builderInput = input;\n      var go = el('button', 's2ts-btn s2ts-primary', 'Generera tema');\n      go.onclick = runBuilder;\n      refs.builderButton = go;\n      row.appendChild(go);\n      builderCard.appendChild(row);\n      var result = el('div', 's2ts-result');\n      result.style.display = 'none';\n      builderCard.appendChild(result);\n      refs.builderResult = result;\n      var builderStatus = el('div', 's2ts-status');\n      builderCard.appendChild(builderStatus);\n      refs.builderStatus = builderStatus;\n      root.appendChild(builderCard);\n\n      //  Library\n      var libraryCard = el('div', 's2ts-card');\n      libraryCard.appendChild(el('div', 's2ts-h', 'Temabibliotek'));\n      libraryCard.appendChild(el('p', 's2ts-sub', 'Klicka på ett tema för att förhandsvisa det. Accentfärgsteman byter appens färger; kompletta teman ändrar hela utseendet med typsnitt och detaljer.'));\n      var grid = el('div', 's2ts-grid');\n      libraryCard.appendChild(grid);\n      refs.libraryGrid = grid;\n      var libraryStatus = el('div', 's2ts-status');\n      libraryCard.appendChild(libraryStatus);\n      refs.libraryStatus = libraryStatus;\n      root.appendChild(libraryCard);\n\n      //  Saved themes\n      var savedCard = el('div', 's2ts-card');\n      savedCard.appendChild(el('div', 's2ts-h', 'Sparade teman'));\n      savedCard.appendChild(el('p', 's2ts-sub', 'Teman du byggt själv.'));\n      var savedGrid = el('div', 's2ts-grid');\n      savedCard.appendChild(savedGrid);\n      refs.savedGrid = savedGrid;\n      refs.savedSection = savedCard;\n      root.appendChild(savedCard);\n\n      //  Footer\n      var footer = el('div', 's2ts-footer',\n        'Temat sparas per webbläsare (localStorage). Alla teman ovan dyker också upp i den vanliga temaväljaren. ' +\n        '\"Återställ till standard\" tar bort allt och går tillbaka till standardteman.');\n      root.appendChild(footer);\n\n      renderActive();\n      renderGallery();\n    }\n\n    /* ---------------- Boot ---------------- */\n\n    function boot() {\n      var root = document.getElementById('s2ts-root');\n      if (!root) { setTimeout(boot, 120); return; }\n      if (root.getAttribute('data-s2ts-booted')) { return; }\n      root.setAttribute('data-s2ts-booted', '1');\n      render(root);\n    }\n\n    boot();\n\n    //  If the SPA re-renders the section, the container is recreated empty.\n    //  Watch for that and re-boot. Global handle so re-opening the view does\n    //  not stack intervals.\n    if (window.__s2tsWatch) { clearInterval(window.__s2tsWatch); }\n    window.__s2tsWatch = setInterval(function () {\n      var root = document.getElementById('s2ts-root');\n      if (root && !root.getAttribute('data-s2ts-booted')) {\n        root.setAttribute('data-s2ts-booted', '1');\n        render(root);\n      }\n      if (!root && refs.bar) {\n        //  User left the view mid-preview: undo the preview and remove the bar.\n        restoreSnapshot();\n        hideBar();\n      }\n    }, 600);\n  })();\n</script>","iconName":"palette","iconColor":"#AB47BC","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a49b49ae58b9e7a9eb3aa0a","name":"Rapportverktyg","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":["6a4cc9c8704935b4ffb551ca"],"locations":[{"type":"navigation","label":"Rapportverktyg"}],"html":"<div id=\"s2rv-root\"><div class=\"s2rv-boot\">Läser in rapportverktyget…</div></div>\n<style>\n/* Rapportverktyg — allt prefixat s2rv för att inte läcka in i SPA:n.\n   Färger mappas mot SPA:ns temavariabler (mörkt läge/orgteman följer med). */\n\n#s2rv-root {\n  --s2rv-bg-1: var(--color-background-1, #fff);\n  --s2rv-bg-2: var(--color-background-2, #fafafa);\n  --s2rv-bg-3: var(--color-background-3, #eee);\n  --s2rv-text-1: var(--color-text-1, #212121);\n  --s2rv-text-2: var(--color-text-2, #616161);\n  --s2rv-text-3: var(--color-text-3, #757575);\n  --s2rv-text-4: var(--color-text-4, #9e9e9e);\n  --s2rv-text-1-rgb: var(--color-text-1-rgb, 33, 33, 33);\n  --s2rv-primary: var(--color-primary, #1565c0);\n  --s2rv-primary-rgb: var(--color-primary-rgb, 21, 101, 192);\n  --s2rv-danger: var(--color-danger, #e53935);\n  --s2rv-danger-rgb: var(--color-danger-rgb, 229, 57, 53);\n  --s2rv-good: var(--color-good, #43a047);\n  --s2rv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n  --s2rv-active: var(--color-element-active, rgba(20, 20, 20, 0.07));\n  --s2rv-border: rgba(var(--s2rv-text-1-rgb), 0.14);\n  --s2rv-border-soft: rgba(var(--s2rv-text-1-rgb), 0.08);\n  --s2rv-radius: var(--border-radius-button, 8px);\n\n  font-family: var(--font-family-default, Nunito, -apple-system, sans-serif);\n  color: var(--s2rv-text-1);\n  font-size: 14px;\n  line-height: 1.45;\n}\n\n#s2rv-root *, #s2rv-root *::before, #s2rv-root *::after { box-sizing: border-box; }\n\n.s2rv-boot { padding: 32px; color: var(--s2rv-text-3); }\n\n.s2rv-app { padding: 8px 0 40px; max-width: 1280px; margin: 0 auto; }\n\n.s2rv-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 4px; min-height: 16px; }\n.s2rv-version { color: var(--s2rv-text-4); font-size: 11px; }\n.s2rv-save-indicator { color: var(--s2rv-text-3); font-size: 12px; }\n.s2rv-save-error { color: var(--s2rv-danger); }\n\n/* Flikar */\n.s2rv-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--s2rv-border-soft); margin-bottom: 16px; }\n.s2rv-tab {\n  border: none; background: none; padding: 8px 14px; cursor: pointer;\n  font: inherit; color: var(--s2rv-text-2); border-bottom: 2px solid transparent;\n}\n.s2rv-tab:hover { color: var(--s2rv-text-1); }\n.s2rv-tab-active { color: var(--s2rv-primary); border-bottom-color: var(--s2rv-primary); font-weight: 600; }\n\n/* Knappar */\n.s2rv-btn {\n  display: inline-block; border: 1px solid var(--s2rv-border); background: transparent; color: var(--s2rv-text-1);\n  border-radius: var(--s2rv-radius); padding: 6px 12px; font: inherit; font-size: 13px; cursor: pointer;\n  margin: 2px 4px 2px 0;\n}\n.s2rv-btn:hover { background: var(--s2rv-hover); }\n.s2rv-btn-primary { background: var(--s2rv-primary); border-color: var(--s2rv-primary); color: #fff; }\n.s2rv-btn-primary:hover { background: var(--s2rv-primary); filter: brightness(0.9); }\n.s2rv-btn-danger { color: var(--s2rv-danger); border-color: rgba(var(--s2rv-danger-rgb), 0.4); }\n.s2rv-btn-danger:hover { background: rgba(var(--s2rv-danger-rgb), 0.08); }\n.s2rv-btn-small { padding: 2px 7px; font-size: 12px; }\n\n/* Formulär */\n#s2rv-root select, #s2rv-root input[type='text'], #s2rv-root input[type='date'] {\n  font: inherit; font-size: 13px; padding: 6px 8px; border: 1px solid var(--s2rv-border);\n  border-radius: var(--s2rv-radius); background: var(--s2rv-bg-1); color: var(--s2rv-text-1); max-width: 100%;\n}\n.s2rv-form { background: var(--s2rv-bg-2); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 14px 16px; margin-bottom: 16px; }\n.s2rv-form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px 16px; }\n.s2rv-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n.s2rv-field-label { font-size: 11px; font-weight: 600; color: var(--s2rv-text-2); text-transform: uppercase; letter-spacing: 0.03em; }\n.s2rv-form-section { padding: 10px 0; border-bottom: 1px solid var(--s2rv-border-soft); }\n.s2rv-form-section:first-child { padding-top: 0; }\n.s2rv-form-section:last-of-type { border-bottom: none; }\n.s2rv-form-actions { margin-top: 8px; display: flex; gap: 8px; align-items: center; }\n.s2rv-inline { margin-left: 10px; white-space: nowrap; }\n.s2rv-range { display: inline-flex; gap: 8px; flex-wrap: wrap; align-items: center; }\n.s2rv-range-custom { display: inline-flex; gap: 8px; align-items: center; }\n.s2rv-section-title { margin: 0 0 10px; font-size: 13px; font-weight: 700; color: var(--s2rv-text-1); }\n\n/* Flervalsdropdown */\n.s2rv-msel { position: relative; display: inline-block; min-width: 160px; }\n.s2rv-msel-button {\n  width: 100%; text-align: left; border: 1px solid var(--s2rv-border); background: var(--s2rv-bg-1);\n  border-radius: var(--s2rv-radius); padding: 6px 8px; font: inherit; font-size: 13px; cursor: pointer;\n  color: var(--s2rv-text-1);\n  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.s2rv-msel-list {\n  display: none; position: absolute; z-index: 40; top: calc(100% + 2px); left: 0; min-width: 100%;\n  max-height: 260px; overflow: auto; background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border);\n  border-radius: var(--s2rv-radius); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); padding: 4px;\n}\n.s2rv-msel-open .s2rv-msel-list { display: block; }\n.s2rv-msel-item { display: flex; gap: 8px; align-items: center; padding: 4px 6px; border-radius: 4px; cursor: pointer; white-space: nowrap; color: var(--s2rv-text-1); }\n.s2rv-msel-item:hover { background: var(--s2rv-hover); }\n\n/* Byggare: sidopanel + huvudyta */\n.s2rv-builder { display: grid; grid-template-columns: 220px 1fr; gap: 20px; align-items: start; }\n.s2rv-sidebar h3 { margin: 16px 0 6px; font-size: 12px; text-transform: uppercase; color: var(--s2rv-text-2); letter-spacing: 0.03em; }\n.s2rv-report-list { display: flex; flex-direction: column; gap: 2px; }\n.s2rv-list-item {\n  text-align: left; border: none; background: none; padding: 6px 8px; border-radius: var(--s2rv-radius);\n  font: inherit; font-size: 13px; cursor: pointer; color: var(--s2rv-text-1);\n  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.s2rv-list-item:hover { background: var(--s2rv-hover); }\n.s2rv-list-item-active { background: rgba(var(--s2rv-primary-rgb), 0.12); color: var(--s2rv-primary); font-weight: 600; }\n@media (max-width: 900px) { .s2rv-builder { grid-template-columns: 1fr; } }\n\n/* Resultat */\n.s2rv-status { min-height: 18px; color: var(--s2rv-text-3); font-size: 12px; margin: 6px 0; }\n.s2rv-status-error { color: var(--s2rv-danger); }\n.s2rv-empty { padding: 24px; color: var(--s2rv-text-4); text-align: center; }\n.s2rv-viz { min-height: 40px; }\n.s2rv-chart { position: relative; height: 340px; }\n.s2rv-export-row { margin-top: 12px; border-top: 1px solid var(--s2rv-border-soft); padding-top: 10px; }\n\n/* Tabell */\n.s2rv-table-wrap { overflow: auto; max-height: 480px; border: 1px solid var(--s2rv-border-soft); border-radius: 8px; }\n.s2rv-table { border-collapse: collapse; width: 100%; font-size: 13px; }\n.s2rv-table th, .s2rv-table td { padding: 7px 10px; border-bottom: 1px solid var(--s2rv-border-soft); text-align: left; white-space: nowrap; color: var(--s2rv-text-1); }\n.s2rv-table thead th { position: sticky; top: 0; background: var(--s2rv-bg-2); font-weight: 600; z-index: 1; }\n.s2rv-table .s2rv-num { text-align: right; font-variant-numeric: tabular-nums; }\n.s2rv-total-row td { font-weight: 600; background: var(--s2rv-bg-2); }\n\n/* KPI-kort */\n.s2rv-kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }\n.s2rv-kpi-card { background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 16px; text-align: center; }\n.s2rv-kpi-value { font-size: 26px; font-weight: 700; color: var(--s2rv-primary); font-variant-numeric: tabular-nums; }\n.s2rv-kpi-label { margin-top: 4px; color: var(--s2rv-text-2); font-size: 12px; }\n\n/* Förlopp */\n.s2rv-progress { display: flex; align-items: center; gap: 12px; margin: 10px 0; flex-wrap: wrap; }\n.s2rv-progress-label { font-size: 12px; color: var(--s2rv-text-2); min-width: 180px; }\n.s2rv-progress-track { flex: 1 1 200px; height: 8px; background: var(--s2rv-bg-3); border-radius: 4px; overflow: hidden; position: relative; }\n.s2rv-progress-bar { height: 100%; background: var(--s2rv-primary); border-radius: 4px; transition: width 0.25s ease; }\n.s2rv-progress-indeterminate .s2rv-progress-bar { animation: s2rv-slide 1.2s ease-in-out infinite; }\n@keyframes s2rv-slide {\n  0% { margin-left: -30%; }\n  100% { margin-left: 100%; }\n}\n\n/* Dashboards */\n.s2rv-dash-header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }\n.s2rv-dash-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }\n.s2rv-tile { background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 12px 14px; min-height: 120px; }\n.s2rv-tile-wide { grid-column: span 2; }\n.s2rv-tile-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }\n.s2rv-tile-head h3 { margin: 0; font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.s2rv-tile-actions { white-space: nowrap; }\n.s2rv-tile-content .s2rv-chart { height: 240px; }\n.s2rv-tile-content .s2rv-table-wrap { max-height: 280px; }\n@media (max-width: 900px) { .s2rv-dash-grid { grid-template-columns: 1fr; } .s2rv-tile-wide { grid-column: auto; } }\n\n/* Mallar */\n.s2rv-template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }\n.s2rv-template-card {\n  background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 14px 16px;\n  cursor: pointer; text-align: left; font: inherit; color: var(--s2rv-text-1); display: block; width: 100%;\n}\n.s2rv-template-card:hover { background: var(--s2rv-hover); border-color: rgba(var(--s2rv-primary-rgb), 0.4); }\n.s2rv-template-card h3 { margin: 0 0 6px; font-size: 15px; }\n.s2rv-template-card p { margin: 0 0 10px; color: var(--s2rv-text-2); font-size: 13px; }\n.s2rv-template-cta { color: var(--s2rv-primary); font-size: 13px; font-weight: 600; }\n\n/* Exporter */\n.s2rv-export-list { display: flex; flex-direction: column; gap: 8px; }\n.s2rv-export-item {\n  display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;\n  background: var(--s2rv-bg-1); border: 1px solid var(--s2rv-border-soft); border-radius: 10px; padding: 10px 14px;\n}\n\n/* Tomtillstånd */\n.s2rv-empty-state {\n  text-align: center; padding: 36px 20px; border: 1px dashed var(--s2rv-border); border-radius: 10px;\n  color: var(--s2rv-text-2);\n}\n.s2rv-empty-state h3 { margin: 0 0 6px; font-size: 15px; color: var(--s2rv-text-1); }\n.s2rv-empty-state p { margin: 0 auto 14px; font-size: 13px; max-width: 420px; }\n.s2rv-empty-state-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }\n.s2rv-dash-grid .s2rv-empty-state { grid-column: 1 / -1; }\n\n.s2rv-muted { color: var(--s2rv-text-4); font-size: 13px; }\n\n</style>\n<script>\n\"use strict\";\n(() => {\n  // src/env.ts\n  var Ke = \"0.1.0\";\n  function ae() {\n    let t = document.cookie.match(/(?:^|;\\s*)s2_utoken=([^;]+)/);\n    return t ? decodeURIComponent(t[1]) : null;\n  }\n  function Qe() {\n    return window.__s2rvApiBase || \"\";\n  }\n\n  // src/api/http.ts\n  var N = class extends Error {\n    constructor(e, r) {\n      super(r), this.status = e;\n    }\n  };\n  function qt(t) {\n    if (!t) return \"\";\n    let e = [];\n    for (let r of Object.keys(t)) {\n      let o = t[r];\n      if (o == null || Array.isArray(o) && o.length === 0) continue;\n      let s = Array.isArray(o) ? o.join(\",\") : String(o);\n      e.push(encodeURIComponent(r) + \"=\" + encodeURIComponent(s));\n    }\n    return e.length ? \"?\" + e.join(\"&\") : \"\";\n  }\n  var Kt = /* @__PURE__ */ new Set([429, 502, 503, 504]), De = { until: 0 };\n  async function We(t, e, r, o, s) {\n    let a = ae();\n    if (!a) throw new N(401, \"no token\");\n    let l = Qe() + e + qt(r), i = null;\n    for (let u = 0; u < 4; u++) {\n      if (s != null && s.aborted) throw new DOMException(\"aborted\", \"AbortError\");\n      if (u > 0) {\n        De.until = Date.now() + 3e4;\n        let p = 1e3 * Math.pow(2, u - 1);\n        await new Promise((y, g) => {\n          let b = setTimeout(y, p);\n          s == null || s.addEventListener(\"abort\", () => {\n            clearTimeout(b), g(new DOMException(\"aborted\", \"AbortError\"));\n          }, { once: !0 });\n        });\n      }\n      let d;\n      try {\n        d = await fetch(l, {\n          method: t,\n          signal: s,\n          headers: {\n            authorization: \"Bearer \" + a,\n            ...o != null ? { \"content-type\": \"application/json\" } : {}\n          },\n          body: o != null ? JSON.stringify(o) : void 0\n        });\n      } catch (p) {\n        if ((p == null ? void 0 : p.name) === \"AbortError\") throw p;\n        i = p;\n        continue;\n      }\n      if (d.ok) {\n        let p = await d.text();\n        return p ? JSON.parse(p) : null;\n      }\n      if (!Kt.has(d.status)) {\n        let p = await d.text().catch(() => \"\");\n        throw new N(d.status, t + \" \" + e + \" -> \" + d.status + \" \" + p.slice(0, 300));\n      }\n      i = new N(d.status, t + \" \" + e + \" -> \" + d.status);\n    }\n    throw i instanceof Error ? i : new N(0, \"request failed\");\n  }\n  function D(t, e, r) {\n    return We(\"GET\", t, e, void 0, r);\n  }\n  function Je(t, e, r) {\n    return We(\"PUT\", t, void 0, e, r);\n  }\n\n  // src/i18n/sv.ts\n  var n = {\n    appTitle: \"Rapportverktyg\",\n    tabReports: \"Rapporter\",\n    tabDashboards: \"Dashboards\",\n    tabTemplates: \"Mallar\",\n    tabExports: \"Exporter\",\n    noToken: \"Kunde inte l\\xE4sa din inloggning. Ladda om sidan och f\\xF6rs\\xF6k igen.\",\n    loadError: \"N\\xE5got gick snett vid inl\\xE4sning. F\\xF6rs\\xF6k igen.\",\n    networkError: \"N\\xE4tverksfel. Kontrollera din anslutning och f\\xF6rs\\xF6k igen.\",\n    cdnError: \"Kunde inte l\\xE4sa in diagrambiblioteket. Kontrollera din anslutning och f\\xF6rs\\xF6k igen.\",\n    newReport: \"Ny rapport\",\n    editReport: \"Redigera\",\n    runReport: \"K\\xF6r rapport\",\n    saveReport: \"Spara rapport\",\n    deleteReport: \"Ta bort\",\n    duplicateReport: \"Duplicera\",\n    reportName: \"Rapportnamn\",\n    unnamedReport: \"Namnl\\xF6s rapport\",\n    savedReports: \"Sparade rapporter\",\n    noSavedReports: \"Inga sparade rapporter \\xE4nnu. Skapa en ny rapport eller utg\\xE5 fr\\xE5n en mall.\",\n    reportInUse: \"Rapporten anv\\xE4nds i en dashboard. Ta bort den \\xE4nd\\xE5?\",\n    confirmDelete: \"Ta bort? Detta g\\xE5r inte att \\xE5ngra.\",\n    sectionData: \"Data\",\n    sectionGrouping: \"Gruppering\",\n    sectionPeriodFilters: \"Period & filter\",\n    sectionViz: \"Visning\",\n    source: \"Datak\\xE4lla\",\n    metrics: \"M\\xE4tv\\xE4rden\",\n    groupBy: \"Gruppera p\\xE5\",\n    groupBySecondary: \"Sekund\\xE4r gruppering\",\n    filters: \"Filter\",\n    addFilter: \"L\\xE4gg till filter\",\n    dateRange: \"Period\",\n    chartType: \"Typ av visning\",\n    timeGrain: \"Tidsintervall\",\n    none: \"Ingen\",\n    all: \"Alla\",\n    selected: \"valda\",\n    vizTable: \"Tabell\",\n    vizBar: \"Stapel\",\n    vizLine: \"Linje\",\n    vizPie: \"Cirkel\",\n    vizKpi: \"Nyckeltal\",\n    stacked: \"Staplad\",\n    grainHour: \"Timme\",\n    grainDay: \"Dag\",\n    grainWeek: \"Vecka\",\n    grainMonth: \"M\\xE5nad\",\n    rangeToday: \"Idag\",\n    rangeYesterday: \"Ig\\xE5r\",\n    rangeThisWeek: \"Denna vecka\",\n    rangeLastWeek: \"F\\xF6rra veckan\",\n    rangeThisMonth: \"Denna m\\xE5nad\",\n    rangeLastMonth: \"F\\xF6rra m\\xE5naden\",\n    rangeLast7: \"Senaste 7 dagarna\",\n    rangeLast30: \"Senaste 30 dagarna\",\n    rangeThisYear: \"Detta \\xE5r\",\n    rangeCustom: \"Anpassad\",\n    rangeFrom: \"Fr\\xE5n\",\n    rangeTo: \"Till\",\n    fetching: \"H\\xE4mtar data\\u2026\",\n    fetchingRows: \"H\\xE4mtar data\",\n    counting: \"R\\xE4knar rader\\u2026\",\n    cancel: \"Avbryt\",\n    cancelled: \"Avbruten.\",\n    modeStats: \"snabbstatistik\",\n    modeRows: \"radbaserad\",\n    rows: \"rader\",\n    total: \"Totalt\",\n    emptyResult: \"Inga data f\\xF6r vald period.\",\n    largeWarnTitle: \"Stor datam\\xE4ngd\",\n    largeWarn1: \"Perioden inneh\\xE5ller\",\n    largeWarn2: \"rader, vilket kr\\xE4ver ungef\\xE4r\",\n    largeWarn3: \"f\\xF6rfr\\xE5gningar och kan ta flera minuter. Forts\\xE4tta?\",\n    continueAnyway: \"Forts\\xE4tt\",\n    tooLargeChart: \"F\\xF6r m\\xE5nga rader f\\xF6r att visa som rapport. Begr\\xE4nsa perioden eller filtren, eller exportera radvis till Excel ist\\xE4llet.\",\n    tooLargeAbs: \"F\\xF6r stor datam\\xE4ngd (\\xF6ver 250 000 rader). Dela upp perioden i mindre delar.\",\n    export: \"Exportera\",\n    exportExcel: \"Exportera till Excel\",\n    exportCsv: \"Exportera till CSV\",\n    exportRaw: \"Exportera rader (r\\xE5data)\",\n    exportAggregated: \"Exportera resultat\",\n    savedExports: \"Sparade exporter\",\n    noSavedExports: \"Inga sparade exporter \\xE4nnu. K\\xF6r en rapport och exportera f\\xF6r att spara inst\\xE4llningarna.\",\n    saveExport: \"Spara exportinst\\xE4llning\",\n    exportName: \"Exportnamn\",\n    runAgain: \"K\\xF6r igen\",\n    exporting: \"Exporterar\\u2026\",\n    newDashboard: \"Ny dashboard\",\n    dashboardName: \"Dashboardnamn\",\n    unnamedDashboard: \"Namnl\\xF6s dashboard\",\n    noDashboards: \"Inga dashboards \\xE4nnu. Spara en rapport och l\\xE4gg till den som panel.\",\n    emptyDashboardsTitle: \"Inga dashboards \\xE4nnu\",\n    emptyDashboardsText: \"Kom ig\\xE5ng direkt med en f\\xE4rdig exempeldashboard, eller bygg en egen av dina sparade rapporter.\",\n    createExampleDashboard: \"Skapa exempeldashboard\",\n    exampleDashboardName: \"Exempeldashboard\",\n    addTile: \"L\\xE4gg till panel\",\n    pickReport: \"V\\xE4lj rapport\",\n    refreshAll: \"Uppdatera alla\",\n    refresh: \"Uppdatera\",\n    removeTile: \"Ta bort panel\",\n    deleteDashboard: \"Ta bort dashboard\",\n    useDashboardRange: \"F\\xF6lj dashboardens period\",\n    moveUp: \"Flytta upp\",\n    moveDown: \"Flytta ner\",\n    tileWide: \"Bred\",\n    tileNarrow: \"Smal\",\n    useTemplate: \"Anv\\xE4nd mall\",\n    templatesIntro: \"F\\xE4rdiga rapporter \\u2014 klicka f\\xF6r att \\xF6ppna i rapportbyggaren.\",\n    saveOk: \"Sparat.\",\n    saveError: \"Kunde inte spara. F\\xF6rs\\xF6k igen.\",\n    storageFull: \"Lagringsutrymmet f\\xF6r sparade rapporter \\xE4r fullt. Ta bort gamla rapporter eller dashboards.\",\n    user: \"Anv\\xE4ndare\",\n    team: \"Team\",\n    project: \"Projekt\",\n    dialGroup: \"Ringlista\",\n    tag: \"Tagg\",\n    status: \"Status\",\n    template: \"Avtalsmall\",\n    time: \"Tid\",\n    unknown: \"Ok\\xE4nd\",\n    noValue: \"(saknas)\",\n    others: \"\\xD6vriga\",\n    sourceCalls: \"Samtal\",\n    sourceOrders: \"Ordrar\",\n    sourceOffers: \"Avtal (e-sign)\",\n    sourceDialGroups: \"Ringlistor\",\n    sourceTime: \"Tid & aktivitet\",\n    statusPending: \"Utkast\",\n    statusDistributed: \"Skickat\",\n    statusRead: \"L\\xE4st\",\n    statusReviewPending: \"V\\xE4ntar granskning\",\n    statusSigned: \"Signerat\",\n    statusDeclined: \"Nekat\",\n    statusExpired: \"Utg\\xE5nget\",\n    statusCanceled: \"Avbrutet\"\n  };\n\n  // src/api/lookups.ts\n  var Te = [\n    { id: \"pending\", label: n.statusPending },\n    { id: \"distributed\", label: n.statusDistributed },\n    { id: \"read\", label: n.statusRead },\n    { id: \"reviewPending\", label: n.statusReviewPending },\n    { id: \"signed\", label: n.statusSigned },\n    { id: \"declined\", label: n.statusDeclined },\n    { id: \"expired\", label: n.statusExpired },\n    { id: \"canceled\", label: n.statusCanceled }\n  ];\n  async function H(t, e) {\n    try {\n      return await t;\n    } catch (r) {\n      if ((r == null ? void 0 : r.name) === \"AbortError\") throw r;\n      return console.error(\"s2rv lookup error\", r), e;\n    }\n  }\n  async function Xe(t) {\n    var y;\n    let [e, r, o, s, a, l, i] = await Promise.all([\n      H(D(\"/api/users/users-v1\", void 0, t), []),\n      H(D(\"/api/users/teams-v1\", void 0, t), []),\n      H(D(\"/api/users/project-information-v1/simple\", void 0, t), []),\n      H(Qt(t), []),\n      H(D(\"/api/dial/tags-v1\", void 0, t), []),\n      H(D(\"/api/orders/tags-v1\", void 0, t), []),\n      H(D(\"/api/offers/offer-templates-v2\", void 0, t), [])\n    ]), u = Array.isArray(a) ? a : (y = a == null ? void 0 : a.tags) != null ? y : [], d = {\n      users: e.map((g) => ({ id: g.id, label: g.fullName || g.alias || g.username || g.id })),\n      teams: r.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      projects: o.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      dialGroups: s.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      callTags: u.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      orderTags: l.map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.name) != null ? b : g.id };\n      }),\n      offerTemplates: i.filter((g) => !g.isRemoved).map((g) => {\n        var b;\n        return { id: g.id, label: (b = g.title) != null ? b : g.id };\n      }),\n      offerStatuses: Te,\n      userTeams: /* @__PURE__ */ new Map(),\n      names: /* @__PURE__ */ new Map()\n    };\n    for (let g of e) {\n      let b = [\n        ...g.teamId ? [g.teamId] : [],\n        ...Array.isArray(g.teams) ? g.teams.map((v) => typeof v == \"string\" ? v : v == null ? void 0 : v.id) : []\n      ].filter(Boolean);\n      d.userTeams.set(g.id, [...new Set(b)]);\n    }\n    let p = [\n      d.users,\n      d.teams,\n      d.projects,\n      d.dialGroups,\n      d.callTags,\n      d.orderTags,\n      d.offerTemplates,\n      d.offerStatuses\n    ];\n    for (let g of p)\n      for (let b of g) d.names.set(b.id, b.label);\n    return d;\n  }\n  async function Qt(t) {\n    let e = [];\n    for (let r = 0; ; r += 100) {\n      let o = await D(\"/api/dial/dial-groups-v2\", { offset: r, count: 100 }, t);\n      if (e.push(...o), o.length < 100 || r > 5e3) break;\n    }\n    return e;\n  }\n  function E(t, e) {\n    var r;\n    return e == null || e === \"\" ? n.noValue : (r = t.names.get(e)) != null ? r : n.unknown;\n  }\n\n  // src/schema.ts\n  function ie() {\n    return { version: 1, reports: {}, dashboards: {}, exports: {} };\n  }\n  function j(t) {\n    return t + \"_\" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n  }\n  function Ye(t) {\n    if (!t || typeof t != \"object\") return ie();\n    let e = t;\n    return e.version !== 1 ? ie() : {\n      version: 1,\n      reports: e.reports && typeof e.reports == \"object\" ? e.reports : {},\n      dashboards: e.dashboards && typeof e.dashboards == \"object\" ? e.dashboards : {},\n      exports: e.exports && typeof e.exports == \"object\" ? e.exports : {}\n    };\n  }\n\n  // src/storage.ts\n  var Ze = \"report-tool\", Wt = 8e4, Jt = 1500;\n  function $e() {\n    let t = null, e = null;\n    async function r() {\n      if (!e) return;\n      let { store: o, onStatus: s } = e;\n      if (e = null, JSON.stringify(o).length > Wt) {\n        s(\"error\", n.storageFull);\n        return;\n      }\n      s(\"saving\");\n      try {\n        await Je(\"/api/users/users-v1/me\", { [\"reference.\" + Ze]: o }), s(\"saved\");\n      } catch (l) {\n        console.error(\"s2rv save error\", l), s(\"error\", n.saveError);\n      }\n    }\n    return {\n      async load(o) {\n        var a;\n        let s = await D(\"/api/users/users-v1/me\", void 0, o);\n        return Ye((a = s == null ? void 0 : s.reference) == null ? void 0 : a[Ze]);\n      },\n      schedule(o, s) {\n        e = { store: o, onStatus: s }, t && clearTimeout(t), t = setTimeout(() => {\n          t = null, r();\n        }, Jt);\n      },\n      async flush() {\n        t && (clearTimeout(t), t = null), await r();\n      },\n      destroy() {\n        t && (clearTimeout(t), t = null), r();\n      }\n    };\n  }\n\n  // src/catalog/sources.ts\n  function rt(t) {\n    var o, s, a;\n    let e = Array.isArray(t.products) ? t.products : [], r = 0;\n    for (let l of e) {\n      let i = Number((s = (o = l.price) != null ? o : l.salaryBasis) != null ? s : 0), u = Number((a = l.quantity) != null ? a : 1);\n      Number.isFinite(i) && Number.isFinite(u) && (r += i * u);\n    }\n    return r;\n  }\n  function Xt(t, e) {\n    let r = e.userTeams.get(t.userId);\n    return r && r.length ? r : [null];\n  }\n  function et(t) {\n    let e = Array.isArray(t.tagIds) ? t.tagIds : [];\n    return e.length ? e : [null];\n  }\n  var le = (t) => ({\n    id: \"user\",\n    label: n.user,\n    statsSupported: t,\n    rowKeys: (e) => {\n      var r;\n      return [(r = e.userId) != null ? r : null];\n    }\n  }), ue = { id: \"team\", label: n.team, rowKeys: Xt }, de = {\n    id: \"project\",\n    label: n.project,\n    rowKeys: (t) => {\n      var e;\n      return [(e = t.projectId) != null ? e : null];\n    }\n  }, ce = (t) => ({\n    id: \"time\",\n    label: n.time,\n    statsSupported: t\n  });\n  function tt(t) {\n    return Math.round(Number(t != null ? t : 0) / 1e3);\n  }\n  var Yt = [\n    { id: \"date\", label: \"Datum\", value: (t) => {\n      var e;\n      return (e = t.connectingAt) != null ? e : \"\";\n    } },\n    { id: \"user\", label: n.user, value: (t, e) => E(e, t.userId) },\n    { id: \"dialGroup\", label: n.dialGroup, value: (t, e) => t.dialGroupId ? E(e, t.dialGroupId) : \"\" },\n    { id: \"project\", label: n.project, value: (t, e) => t.projectId ? E(e, t.projectId) : \"\" },\n    { id: \"recipient\", label: \"Nummer\", value: (t) => {\n      var e;\n      return (e = t.recipient) != null ? e : \"\";\n    } },\n    { id: \"answered\", label: \"Svarat\", value: (t) => t.isContactAnswer ? \"Ja\" : \"Nej\" },\n    { id: \"totalDuration\", label: \"Total tid (s)\", value: (t) => tt(t.totalDuration) },\n    { id: \"connectedDuration\", label: \"Samtalstid (s)\", value: (t) => tt(t.connectedDuration) },\n    { id: \"tags\", label: \"Taggar\", value: (t, e) => {\n      var r;\n      return ((r = t.tagIds) != null ? r : []).map((o) => E(e, o)).join(\", \");\n    } }\n  ], Zt = [\n    { id: \"date\", label: \"Datum\", value: (t) => {\n      var e, r;\n      return (r = (e = t.businessDate) != null ? e : t.date) != null ? r : \"\";\n    } },\n    { id: \"serialId\", label: \"Ordernr\", value: (t) => {\n      var e;\n      return (e = t.serialId) != null ? e : \"\";\n    } },\n    { id: \"user\", label: n.user, value: (t, e) => E(e, t.userId) },\n    { id: \"project\", label: n.project, value: (t, e) => t.projectId ? E(e, t.projectId) : \"\" },\n    { id: \"sum\", label: \"Orderv\\xE4rde\", value: (t) => rt(t) },\n    { id: \"products\", label: \"Produkter\", value: (t) => {\n      var e;\n      return ((e = t.products) != null ? e : []).map((r) => r.name).filter(Boolean).join(\", \");\n    } },\n    { id: \"tags\", label: \"Taggar\", value: (t, e) => {\n      var r;\n      return ((r = t.tagIds) != null ? r : []).map((o) => E(e, o)).join(\", \");\n    } }\n  ], $t = [\n    { id: \"date\", label: \"Datum\", value: (t) => {\n      var e;\n      return (e = t.date) != null ? e : \"\";\n    } },\n    { id: \"serialId\", label: \"Avtalsnr\", value: (t) => {\n      var e;\n      return (e = t.serialId) != null ? e : \"\";\n    } },\n    { id: \"user\", label: n.user, value: (t, e) => E(e, t.userId) },\n    { id: \"status\", label: n.status, value: (t, e) => E(e, t.status) },\n    { id: \"template\", label: n.template, value: (t, e) => t.templateId ? E(e, t.templateId) : \"\" },\n    { id: \"signedAt\", label: \"Signerat\", value: (t) => {\n      var e, r;\n      return (r = (e = t.signature) == null ? void 0 : e.signedAt) != null ? r : \"\";\n    } }\n  ], er = [\n    { id: \"name\", label: n.dialGroup, value: (t) => {\n      var e;\n      return (e = t.name) != null ? e : \"\";\n    } },\n    { id: \"project\", label: n.project, value: (t, e) => t.projectId ? E(e, t.projectId) : \"\" },\n    { id: \"contacts\", label: \"Kontakter\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.contactCount) != null ? r : 0;\n    } },\n    { id: \"reserved\", label: \"Reserverade\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.reservedContactCount) != null ? r : 0;\n    } },\n    { id: \"skipped\", label: \"\\xD6verhoppade\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.skippedContactCount) != null ? r : 0;\n    } },\n    { id: \"quarantined\", label: \"Karant\\xE4n\", value: (t) => {\n      var e, r;\n      return (r = (e = t.contactSummaryCache) == null ? void 0 : e.quarantinedContactCount) != null ? r : 0;\n    } }\n  ], Ae = [\n    {\n      id: \"calls\",\n      label: n.sourceCalls,\n      rowTime: (t) => {\n        var e;\n        return (e = t.connectingAt) != null ? e : null;\n      },\n      metrics: [\n        { id: \"count\", label: \"Antal samtal\", format: \"int\", agg: \"count\", statsField: \"count\" },\n        { id: \"connectedCount\", label: \"Svarade samtal\", format: \"int\", agg: \"count\", predicate: (t) => !!t.isContactAnswer, statsField: \"connectedCount\" },\n        { id: \"answerRate\", label: \"Svarsfrekvens\", format: \"percent\", rate: [\"connectedCount\", \"count\"] },\n        { id: \"totalDuration\", label: \"Total tid\", format: \"duration\", agg: \"sum\", value: (t) => {\n          var e;\n          return Number((e = t.totalDuration) != null ? e : 0);\n        }, statsField: \"totalDuration\" },\n        { id: \"connectedDuration\", label: \"Samtalstid\", format: \"duration\", agg: \"sum\", value: (t) => {\n          var e;\n          return Number((e = t.connectedDuration) != null ? e : 0);\n        }, statsField: \"connectedDuration\" },\n        { id: \"avgDuration\", label: \"Snitt samtalstid\", format: \"duration\", rate: [\"totalDuration\", \"count\"] }\n      ],\n      dimensions: [\n        le(!0),\n        ue,\n        de,\n        { id: \"dialGroup\", label: n.dialGroup, rowKeys: (t) => {\n          var e;\n          return [(e = t.dialGroupId) != null ? e : null];\n        } },\n        { id: \"tag\", label: n.tag, statsSupported: !0, rowKeys: et },\n        ce(!0)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" },\n        { id: \"dialGroup\", label: n.dialGroup, lookup: \"dialGroups\", target: \"dialGroupIds\" },\n        { id: \"tag\", label: n.tag, lookup: \"callTags\", target: \"tagIds\" }\n      ],\n      rowColumns: Yt\n    },\n    {\n      id: \"orders\",\n      label: n.sourceOrders,\n      rowTime: (t) => {\n        var e, r;\n        return (r = (e = t.businessDate) != null ? e : t.date) != null ? r : null;\n      },\n      metrics: [\n        { id: \"count\", label: \"Antal ordrar\", format: \"int\", agg: \"count\", statsField: \"count\" },\n        { id: \"sumAmount\", label: \"Orderv\\xE4rde\", format: \"currency\", agg: \"sum\", value: rt },\n        { id: \"avgAmount\", label: \"Snitt orderv\\xE4rde\", format: \"currency\", rate: [\"sumAmount\", \"count\"] }\n      ],\n      dimensions: [\n        le(!0),\n        ue,\n        de,\n        { id: \"tag\", label: n.tag, statsSupported: !0, rowKeys: et },\n        ce(!0)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" },\n        { id: \"tag\", label: n.tag, lookup: \"orderTags\", target: \"tagIds\" }\n      ],\n      rowColumns: Zt\n    },\n    {\n      id: \"offers\",\n      label: n.sourceOffers,\n      rowTime: (t) => {\n        var e;\n        return (e = t.date) != null ? e : null;\n      },\n      metrics: [\n        { id: \"count\", label: \"Antal avtal\", format: \"int\", agg: \"count\" },\n        { id: \"signedCount\", label: \"Signerade\", format: \"int\", agg: \"count\", predicate: (t) => t.status === \"signed\" },\n        { id: \"signRate\", label: \"Signeringsgrad\", format: \"percent\", rate: [\"signedCount\", \"count\"] }\n      ],\n      dimensions: [\n        le(!1),\n        ue,\n        de,\n        { id: \"status\", label: n.status, rowKeys: (t) => {\n          var e;\n          return [(e = t.status) != null ? e : null];\n        } },\n        { id: \"template\", label: n.template, rowKeys: (t) => {\n          var e;\n          return [(e = t.templateId) != null ? e : null];\n        } },\n        ce(!1)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" },\n        { id: \"status\", label: n.status, lookup: \"offerStatuses\", target: \"statuses\" },\n        { id: \"template\", label: n.template, lookup: \"offerTemplates\", target: \"templateIds\" }\n      ],\n      rowColumns: $t\n    },\n    {\n      id: \"dialGroups\",\n      label: n.sourceDialGroups,\n      metrics: [\n        { id: \"groupCount\", label: \"Antal ringlistor\", format: \"int\", agg: \"count\" },\n        { id: \"contactCount\", label: \"Kontakter\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.contactCount) != null ? r : 0);\n        } },\n        { id: \"reservedCount\", label: \"Reserverade\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.reservedContactCount) != null ? r : 0);\n        } },\n        { id: \"skippedCount\", label: \"\\xD6verhoppade\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.skippedContactCount) != null ? r : 0);\n        } },\n        { id: \"quarantinedCount\", label: \"Karant\\xE4n\", format: \"int\", agg: \"sum\", value: (t) => {\n          var e, r;\n          return Number((r = (e = t.contactSummaryCache) == null ? void 0 : e.quarantinedContactCount) != null ? r : 0);\n        } }\n      ],\n      dimensions: [\n        { id: \"dialGroup\", label: n.dialGroup, rowKeys: (t) => {\n          var e;\n          return [(e = t.id) != null ? e : null];\n        } },\n        de\n      ],\n      filters: [\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" },\n        { id: \"project\", label: n.project, lookup: \"projects\", target: \"projectIds\" }\n      ],\n      rowColumns: er\n    },\n    {\n      id: \"time\",\n      label: n.sourceTime,\n      statsOnly: !0,\n      metrics: [\n        { id: \"activeDuration\", label: \"Aktiv tid\", format: \"duration\", statsField: \"activeDuration\" }\n      ],\n      dimensions: [\n        le(!0),\n        ue,\n        ce(!0)\n      ],\n      filters: [\n        { id: \"user\", label: n.user, lookup: \"users\", target: \"userIds\" },\n        { id: \"team\", label: n.team, lookup: \"teams\", target: \"teamIds\" }\n      ],\n      rowColumns: []\n    }\n  ];\n  function M(t) {\n    let e = Ae.find((r) => r.id === t);\n    if (!e) throw new Error(\"Ok\\xE4nd datak\\xE4lla: \" + t);\n    return e;\n  }\n\n  // src/engine/timeBuckets.ts\n  var tr = \"Europe/Stockholm\", Ee = null;\n  function rr() {\n    return Ee || (Ee = new Intl.DateTimeFormat(\"sv-SE\", {\n      timeZone: tr,\n      year: \"numeric\",\n      month: \"2-digit\",\n      day: \"2-digit\",\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n      hour12: !1\n    })), Ee;\n  }\n  function U(t) {\n    let e = {};\n    for (let r of rr().formatToParts(t)) e[r.type] = r.value;\n    return {\n      y: Number(e.year),\n      m: Number(e.month),\n      d: Number(e.day),\n      h: Number(e.hour) % 24,\n      //  'sv-SE' kan ge \"24\" vid midnatt.\n      min: Number(e.minute)\n    };\n  }\n  function ot(t) {\n    let e = U(t);\n    return Date.UTC(e.y, e.m - 1, e.d, e.h, e.min, t.getUTCSeconds()) - (t.getTime() - t.getTime() % 1e3);\n  }\n  function at(t, e, r, o = 0, s = 0) {\n    let a = Date.UTC(t, e - 1, r, o, s), l = ot(new Date(a)), i = a - l, u = ot(new Date(i));\n    return u !== l && (i = a - u), new Date(i);\n  }\n  function Ie(t, e, r) {\n    return new Date(Date.UTC(t, e - 1, r));\n  }\n  function L(t, e) {\n    let r = Ie(t.y, t.m, t.d);\n    return r.setUTCDate(r.getUTCDate() + e), { y: r.getUTCFullYear(), m: r.getUTCMonth() + 1, d: r.getUTCDate(), h: 0, min: 0 };\n  }\n  function me(t, e) {\n    let r = t.y * 12 + t.m - 1 + e;\n    return { y: Math.floor(r / 12), m: r % 12 + 1, d: 1, h: 0, min: 0 };\n  }\n  function Le(t) {\n    let e = Ie(t.y, t.m, t.d).getUTCDay();\n    return e === 0 ? 7 : e;\n  }\n  function it(t) {\n    let e = Ie(t.y, t.m, t.d);\n    e.setUTCDate(e.getUTCDate() + 4 - (e.getUTCDay() || 7));\n    let r = e.getUTCFullYear(), o = Date.UTC(r, 0, 1), s = Math.ceil(((e.getTime() - o) / 864e5 + 1) / 7);\n    return { year: r, week: s };\n  }\n  var nt = [\"jan\", \"feb\", \"mar\", \"apr\", \"maj\", \"jun\", \"jul\", \"aug\", \"sep\", \"okt\", \"nov\", \"dec\"];\n  function B(t) {\n    return t < 10 ? \"0\" + t : String(t);\n  }\n  function lt(t, e) {\n    switch (e) {\n      case \"hour\":\n        return t.y + \"-\" + B(t.m) + \"-\" + B(t.d) + \" \" + B(t.h);\n      case \"day\":\n        return t.y + \"-\" + B(t.m) + \"-\" + B(t.d);\n      case \"week\": {\n        let r = it(t);\n        return r.year + \"-V\" + B(r.week);\n      }\n      case \"month\":\n        return t.y + \"-\" + B(t.m);\n    }\n  }\n  function ut(t, e) {\n    return lt(U(new Date(t)), e);\n  }\n  function or(t, e) {\n    switch (e) {\n      case \"hour\":\n        return t.d + \"/\" + t.m + \" \" + B(t.h) + \":00\";\n      case \"day\":\n        return t.d + \" \" + nt[t.m - 1];\n      case \"week\":\n        return \"v. \" + it(t).week;\n      case \"month\":\n        return nt[t.m - 1] + \" \" + t.y;\n    }\n  }\n  function dt(t, e, r) {\n    let o = [], s = /* @__PURE__ */ new Set(), a = U(t);\n    (r === \"day\" || r === \"week\" || r === \"month\") && (a = { ...a, h: 0, min: 0 }), r === \"week\" && (a = L(a, 1 - Le(a))), r === \"month\" && (a = { ...a, d: 1 }), r === \"hour\" && (a = { ...a, min: 0 });\n    let l = 1e3;\n    for (; o.length < l; ) {\n      let i = at(a.y, a.m, a.d, a.h, a.min);\n      if (i >= e) break;\n      let u = lt(a, r);\n      switch (s.has(u) || (s.add(u), (i >= t || o.length === 0) && o.push({ key: u, label: or(a, r), startUtc: i })), r) {\n        case \"hour\": {\n          let d = new Date(i.getTime() + 36e5);\n          a = U(d), a = { ...a, min: 0 };\n          break;\n        }\n        case \"day\":\n          a = L(a, 1);\n          break;\n        case \"week\":\n          a = L(a, 7);\n          break;\n        case \"month\":\n          a = me(a, 1);\n          break;\n      }\n    }\n    return o;\n  }\n  var ct = [\n    { id: \"today\", label: n.rangeToday },\n    { id: \"yesterday\", label: n.rangeYesterday },\n    { id: \"thisWeek\", label: n.rangeThisWeek },\n    { id: \"lastWeek\", label: n.rangeLastWeek },\n    { id: \"thisMonth\", label: n.rangeThisMonth },\n    { id: \"lastMonth\", label: n.rangeLastMonth },\n    { id: \"last7\", label: n.rangeLast7 },\n    { id: \"last30\", label: n.rangeLast30 },\n    { id: \"thisYear\", label: n.rangeThisYear },\n    { id: \"custom\", label: n.rangeCustom }\n  ];\n  function st(t) {\n    let e = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(t);\n    if (!e) throw new Error(\"Ogiltigt datum: \" + t);\n    return { y: Number(e[1]), m: Number(e[2]), d: Number(e[3]), h: 0, min: 0 };\n  }\n  function pe(t, e = /* @__PURE__ */ new Date()) {\n    var s, a, l;\n    let r = { ...U(e), h: 0, min: 0 }, o = (i) => at(i.y, i.m, i.d);\n    switch (t.preset) {\n      case \"today\":\n        return { from: o(r), to: o(L(r, 1)) };\n      case \"yesterday\":\n        return { from: o(L(r, -1)), to: o(r) };\n      case \"thisWeek\": {\n        let i = L(r, 1 - Le(r));\n        return { from: o(i), to: o(L(i, 7)) };\n      }\n      case \"lastWeek\": {\n        let i = L(r, 1 - Le(r) - 7);\n        return { from: o(i), to: o(L(i, 7)) };\n      }\n      case \"thisMonth\": {\n        let i = { ...r, d: 1 };\n        return { from: o(i), to: o(me(i, 1)) };\n      }\n      case \"lastMonth\": {\n        let i = me({ ...r, d: 1 }, -1);\n        return { from: o(i), to: o(me(i, 1)) };\n      }\n      case \"last7\":\n        return { from: o(L(r, -6)), to: o(L(r, 1)) };\n      case \"last30\":\n        return { from: o(L(r, -29)), to: o(L(r, 1)) };\n      case \"thisYear\": {\n        let i = { y: r.y, m: 1, d: 1, h: 0, min: 0 };\n        return { from: o(i), to: o({ y: r.y + 1, m: 1, d: 1, h: 0, min: 0 }) };\n      }\n      case \"custom\": {\n        let i = st((s = t.from) != null ? s : \"\"), u = L(st((l = (a = t.to) != null ? a : t.from) != null ? l : \"\"), 1);\n        return { from: o(i), to: o(u) };\n      }\n      default:\n        throw new Error(\"Ok\\xE4nd period: \" + t.preset);\n    }\n  }\n  function mt(t, e) {\n    let r = (e.getTime() - t.getTime()) / 864e5;\n    return r <= 2 ? \"hour\" : r <= 62 ? \"day\" : r <= 200 ? \"week\" : \"month\";\n  }\n\n  // src/ui/dom.ts\n  function c(t, e, ...r) {\n    let o = document.createElement(t);\n    e && (e.class && (o.className = e.class), e.style && Object.assign(o.style, e.style), e.text != null && (o.textContent = e.text), e.title && (o.title = e.title), e.type && (o.type = e.type), e.value != null && (o.value = e.value), e.placeholder && (o.placeholder = e.placeholder), e.disabled && (o.disabled = !0), e.checked != null && (o.checked = e.checked), e.selected != null && (o.selected = e.selected), e.min != null && (o.min = e.min), e.max != null && (o.max = e.max), e.colspan != null && (o.colSpan = e.colspan), e.onClick && o.addEventListener(\"click\", e.onClick), e.onChange && o.addEventListener(\"change\", e.onChange), e.onInput && o.addEventListener(\"input\", e.onInput));\n    for (let s of r)\n      s != null && o.appendChild(typeof s == \"string\" ? document.createTextNode(s) : s);\n    return o;\n  }\n  function T(t) {\n    for (; t.firstChild; ) t.removeChild(t.firstChild);\n  }\n  function P(t, e, r) {\n    let o = c(\"option\", { text: e, selected: !!r });\n    return o.value = t, o;\n  }\n  function Pe(t) {\n    let e = new Set(t.selected), r = c(\"button\", { class: \"s2rv-msel-button\", type: \"button\" }), o = c(\"div\", { class: \"s2rv-msel-list\" }), s = c(\"div\", { class: \"s2rv-msel\" }, r, o);\n    function a() {\n      var i, u;\n      r.textContent = e.size === 0 ? t.allLabel : e.size === 1 ? (u = (i = t.items.find((d) => e.has(d.id))) == null ? void 0 : i.label) != null ? u : \"1 \" + t.selectedSuffix : e.size + \" \" + t.selectedSuffix;\n    }\n    for (let i of t.items) {\n      let u = c(\"input\", {\n        type: \"checkbox\",\n        checked: e.has(i.id),\n        onChange: () => {\n          u.checked ? e.add(i.id) : e.delete(i.id), a(), t.onChange([...e]);\n        }\n      });\n      o.appendChild(c(\"label\", { class: \"s2rv-msel-item\" }, u, c(\"span\", { text: i.label })));\n    }\n    r.addEventListener(\"click\", (i) => {\n      i.stopPropagation(), s.classList.toggle(\"s2rv-msel-open\");\n    });\n    let l = (i) => {\n      s.contains(i.target) || s.classList.remove(\"s2rv-msel-open\");\n    };\n    return document.addEventListener(\"click\", l), s.__s2rvDispose = () => document.removeEventListener(\"click\", l), a(), s;\n  }\n  function G(t) {\n    var e;\n    (e = t.__s2rvDispose) == null || e.call(t), t.querySelectorAll(\"*\").forEach((r) => {\n      var o;\n      return (o = r.__s2rvDispose) == null ? void 0 : o.call(r);\n    });\n  }\n  function pt(t, e) {\n    let r = URL.createObjectURL(e), o = c(\"a\");\n    o.href = r, o.download = t, document.body.appendChild(o), o.click(), o.remove(), setTimeout(() => URL.revokeObjectURL(r), 1e4);\n  }\n\n  // src/ui/dateRange.ts\n  function Me() {\n    let t = U(/* @__PURE__ */ new Date()), e = (r) => r < 10 ? \"0\" + r : String(r);\n    return t.y + \"-\" + e(t.m) + \"-\" + e(t.d);\n  }\n  function fe(t, e) {\n    var d, p;\n    let r = { ...t }, o = c(\"input\", { type: \"date\", value: (d = r.from) != null ? d : Me(), onChange: () => u() }), s = c(\"input\", { type: \"date\", value: (p = r.to) != null ? p : Me(), onChange: () => u() }), a = c(\n      \"span\",\n      { class: \"s2rv-range-custom\" },\n      c(\"label\", { text: n.rangeFrom + \" \" }, o),\n      c(\"label\", { text: \" \" + n.rangeTo + \" \" }, s)\n    ), l = c(\"select\", {\n      onChange: () => {\n        r.preset = l.value, i(), u();\n      }\n    }, ...ct.map((y) => P(y.id, y.label, y.id === r.preset)));\n    function i() {\n      a.style.display = r.preset === \"custom\" ? \"\" : \"none\";\n    }\n    function u() {\n      r.preset === \"custom\" ? (r.from = o.value || Me(), r.to = s.value || r.from) : (delete r.from, delete r.to), e({ ...r });\n    }\n    return i(), c(\"span\", { class: \"s2rv-range\" }, l, a);\n  }\n\n  // src/api/paginate.ts\n  var ge = 100, nr = 3, sr = 2e4, Oe = 1e5, W = 25e4;\n  async function be(t) {\n    var g, b;\n    let { signal: e } = t, r = await t.count(e), o = (g = t.maxRows) != null ? g : Oe;\n    if ((b = t.onProgress) == null || b.call(t, { total: r, fetched: 0 }), r > W || r > o)\n      return { status: \"tooLarge\", total: r, limit: Math.min(o, W) };\n    if (r > sr && t.confirmLarge && !await t.confirmLarge(r))\n      return { status: \"declined\" };\n    let s = [];\n    for (let v = 0; v < r; v += ge) s.push(v);\n    let a = !t.onPage, l = [], i = 0, u = 0, d = null;\n    async function p() {\n      var v;\n      for (; u < s.length && !e.aborted && !d; ) {\n        if (Date.now() < De.until && u > 0 && (await new Promise((f) => setTimeout(f, 500)), e.aborted || d))\n          return;\n        let S = s[u++];\n        try {\n          let f = await t.page(S, ge, e);\n          if (i += f.length, a ? l.push(...f) : t.onPage(f), (v = t.onProgress) == null || v.call(t, { total: r, fetched: i }), f.length < ge && S + ge < r) return;\n        } catch (f) {\n          if ((f == null ? void 0 : f.name) === \"AbortError\") return;\n          d = f;\n        }\n      }\n    }\n    let y = Math.min(nr, s.length) || 1;\n    if (await Promise.all(Array.from({ length: y }, () => p())), e.aborted) return { status: \"cancelled\" };\n    if (d) throw d;\n    return { status: \"ok\", rows: l, total: r };\n  }\n\n  // src/format.ts\n  var ft = new Intl.NumberFormat(\"sv-SE\", { maximumFractionDigits: 0 }), ar = new Intl.NumberFormat(\"sv-SE\", { style: \"percent\", maximumFractionDigits: 1 });\n  function ir(t) {\n    let e = Math.round(Number(t || 0) / 1e3), r = Math.floor(e / 3600), o = Math.floor(e % 3600 / 60), s = e % 60;\n    return r > 0 ? r + \" t \" + o + \" min\" : o > 0 ? o + \" min \" + s + \" s\" : s + \" s\";\n  }\n  function q(t, e) {\n    let r = Number.isFinite(t) ? t : 0;\n    switch (e) {\n      case \"int\":\n        return ft.format(Math.round(r));\n      case \"currency\":\n        return ft.format(Math.round(r)) + \" kr\";\n      case \"percent\":\n        return ar.format(r);\n      case \"duration\":\n        return ir(r);\n    }\n  }\n  function gt(t, e) {\n    let r = Number.isFinite(t) ? t : 0;\n    return e === \"duration\" ? Math.round(r / 1e3) : r;\n  }\n\n  // src/charts/loader.ts\n  var lr = \"https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js\", ur = \"https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js\";\n  function bt(t, e) {\n    let r = window;\n    if (r[e]) return Promise.resolve(r[e]);\n    let o = r.__s2rvLibLoads || (r.__s2rvLibLoads = {});\n    return o[t] || (o[t] = new Promise((s, a) => {\n      let l = document.createElement(\"script\");\n      l.src = t, l.async = !0, l.onload = () => s(), l.onerror = () => {\n        delete o[t], l.remove(), a(new Error(n.cdnError));\n      }, document.head.appendChild(l);\n    })), o[t].then(() => {\n      if (!r[e]) throw new Error(n.cdnError);\n      return r[e];\n    });\n  }\n  function vt() {\n    return bt(lr, \"Chart\");\n  }\n  function _e() {\n    return bt(ur, \"XLSX\");\n  }\n\n  // src/charts/render.ts\n  var J = [\n    \"#4e79a7\",\n    \"#f28e2b\",\n    \"#59a14f\",\n    \"#e15759\",\n    \"#76b7b2\",\n    \"#edc948\",\n    \"#b07aa1\",\n    \"#ff9da7\",\n    \"#9c755f\",\n    \"#bab0ac\",\n    \"#86bcb6\",\n    \"#d37295\"\n  ], Fe = 10;\n  function dr(t) {\n    let e = getComputedStyle(t), r = e.getPropertyValue(\"--s2rv-text-2\").trim() || \"#616161\", o = e.getPropertyValue(\"--s2rv-text-1-rgb\").trim() || \"33, 33, 33\";\n    return { text: r, grid: \"rgba(\" + o + \", 0.1)\" };\n  }\n  function ee(t, e, r, o) {\n    var s, a;\n    if (r == null) return n.noValue;\n    if (e === \"time\") {\n      let l = (s = t.buckets) == null ? void 0 : s.find((i) => i.key === r);\n      return (a = l == null ? void 0 : l.label) != null ? a : r;\n    }\n    return E(o, r);\n  }\n  function Ne(t) {\n    var r, o, s;\n    let e = {};\n    for (let a of t.rows)\n      for (let l of Object.keys(a.values)) e[l] = ((r = e[l]) != null ? r : 0) + a.values[l];\n    for (let a of t.output) {\n      if (!a.rate) continue;\n      let l = (o = e[a.rate[1]]) != null ? o : 0;\n      e[a.id] = l > 0 ? ((s = e[a.rate[0]]) != null ? s : 0) / l : 0;\n    }\n    return e;\n  }\n  function cr(t, e) {\n    var d, p, y, g, b;\n    let [r, o] = t.dimSpecs, s = t.output;\n    if (!r) {\n      let v = Ne(t);\n      return {\n        labels: [n.total],\n        series: s.map((S) => {\n          var f;\n          return { label: S.label, values: [(f = v[S.id]) != null ? f : 0], metric: S };\n        })\n      };\n    }\n    let a = [], l = /* @__PURE__ */ new Set();\n    for (let v of t.rows) {\n      let S = (d = v.dims[r.id]) != null ? d : null, f = JSON.stringify(S);\n      l.has(f) || (l.add(f), a.push(S));\n    }\n    let i = a.map((v) => ee(t, r.id, v, e));\n    if (o) {\n      let v = s[0], S = [], f = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Map();\n      for (let k of t.rows) {\n        let w = (p = k.dims[o.id]) != null ? p : null, m = JSON.stringify(w);\n        f.has(m) || (f.add(m), S.push(w)), h.set(JSON.stringify([(y = k.dims[r.id]) != null ? y : null, w]), (g = k.values[v.id]) != null ? g : 0);\n      }\n      return {\n        labels: i,\n        series: S.map((k) => ({\n          label: ee(t, o.id, k, e),\n          values: a.map((w) => {\n            var m;\n            return (m = h.get(JSON.stringify([w, k]))) != null ? m : 0;\n          }),\n          metric: v\n        }))\n      };\n    }\n    let u = /* @__PURE__ */ new Map();\n    for (let v of t.rows) u.set(JSON.stringify((b = v.dims[r.id]) != null ? b : null), v.values);\n    return {\n      labels: i,\n      series: s.map((v) => ({\n        label: v.label,\n        values: a.map((S) => {\n          var f, h;\n          return (h = (f = u.get(JSON.stringify(S))) == null ? void 0 : f[v.id]) != null ? h : 0;\n        }),\n        metric: v\n      }))\n    };\n  }\n  async function ve(t, e, r, o) {\n    if (T(t), !e.rows.length)\n      return t.appendChild(c(\"div\", { class: \"s2rv-empty\", text: n.emptyResult })), { destroy: () => T(t) };\n    switch (r.type) {\n      case \"table\":\n        return mr(t, e, o);\n      case \"kpi\":\n        return pr(t, e);\n      default:\n        return fr(t, e, r, o);\n    }\n  }\n  function mr(t, e, r) {\n    let o = {\n      user: n.user,\n      team: n.team,\n      project: n.project,\n      dialGroup: n.dialGroup,\n      tag: n.tag,\n      status: n.status,\n      template: n.template,\n      time: n.time\n    }, s = c(\n      \"tr\",\n      null,\n      ...e.dimSpecs.map((l) => {\n        var i;\n        return c(\"th\", { text: (i = o[l.id]) != null ? i : l.id });\n      }),\n      ...e.output.map((l) => c(\"th\", { class: \"s2rv-num\", text: l.label }))\n    ), a = c(\"tbody\", null, ...e.rows.map((l) => c(\n      \"tr\",\n      null,\n      ...e.dimSpecs.map((i) => {\n        var u;\n        return c(\"td\", { text: ee(e, i.id, (u = l.dims[i.id]) != null ? u : null, r) });\n      }),\n      ...e.output.map((i) => {\n        var u;\n        return c(\"td\", { class: \"s2rv-num\", text: q((u = l.values[i.id]) != null ? u : 0, i.format) });\n      })\n    )));\n    if (e.rows.length > 1 && e.dimSpecs.length > 0) {\n      let l = Ne(e);\n      a.appendChild(c(\n        \"tr\",\n        { class: \"s2rv-total-row\" },\n        c(\"td\", { text: n.total, colspan: e.dimSpecs.length }),\n        ...e.output.map((i) => {\n          var u;\n          return c(\"td\", { class: \"s2rv-num\", text: q((u = l[i.id]) != null ? u : 0, i.format) });\n        })\n      ));\n    }\n    return t.appendChild(c(\n      \"div\",\n      { class: \"s2rv-table-wrap\" },\n      c(\"table\", { class: \"s2rv-table\" }, c(\"thead\", null, s), a)\n    )), { destroy: () => T(t) };\n  }\n  function pr(t, e) {\n    let r = Ne(e);\n    return t.appendChild(c(\n      \"div\",\n      { class: \"s2rv-kpi-grid\" },\n      ...e.output.map((o) => {\n        var s;\n        return c(\n          \"div\",\n          { class: \"s2rv-kpi-card\" },\n          c(\"div\", { class: \"s2rv-kpi-value\", text: q((s = r[o.id]) != null ? s : 0, o.format) }),\n          c(\"div\", { class: \"s2rv-kpi-label\", text: o.label })\n        );\n      })\n    )), { destroy: () => T(t) };\n  }\n  async function fr(t, e, r, o) {\n    var y, g;\n    let s = await vt(), a = cr(e, o), l = (g = (y = a.series[0]) == null ? void 0 : y.metric.format) != null ? g : \"int\", i = c(\"canvas\");\n    t.appendChild(c(\"div\", { class: \"s2rv-chart\" }, i));\n    let u = dr(t), d;\n    if (r.type === \"pie\") {\n      let b = a.series[0], v = a.labels, S = b ? [...b.values] : [];\n      if (v.length > Fe) {\n        let f = v.map((w, m) => ({ label: w, value: S[m] }));\n        f.sort((w, m) => m.value - w.value);\n        let h = f.slice(0, Fe - 1), k = f.slice(Fe - 1).reduce((w, m) => w + m.value, 0);\n        v = [...h.map((w) => w.label), n.others], S = [...h.map((w) => w.value), k];\n      }\n      d = {\n        type: \"pie\",\n        data: {\n          labels: v,\n          datasets: [{ data: S, backgroundColor: v.map((f, h) => J[h % J.length]) }]\n        },\n        options: {\n          responsive: !0,\n          maintainAspectRatio: !1,\n          plugins: {\n            legend: { position: \"right\", labels: { color: u.text } },\n            tooltip: { callbacks: { label: (f) => {\n              var h;\n              return ((h = f.label) != null ? h : \"\") + \": \" + q(f.parsed, l);\n            } } }\n          }\n        }\n      };\n    } else\n      d = {\n        type: r.type === \"line\" ? \"line\" : \"bar\",\n        data: {\n          labels: a.labels,\n          datasets: a.series.map((b, v) => ({\n            label: b.label,\n            data: b.values,\n            backgroundColor: J[v % J.length],\n            borderColor: J[v % J.length],\n            fill: !1,\n            tension: 0.2\n          }))\n        },\n        options: {\n          responsive: !0,\n          maintainAspectRatio: !1,\n          scales: {\n            x: { stacked: !!r.stacked, ticks: { color: u.text }, grid: { color: u.grid } },\n            y: {\n              stacked: !!r.stacked,\n              beginAtZero: !0,\n              ticks: { color: u.text, callback: (b) => q(Number(b), l) },\n              grid: { color: u.grid }\n            }\n          },\n          plugins: {\n            legend: { display: a.series.length > 1, labels: { color: u.text } },\n            tooltip: {\n              callbacks: {\n                label: (b) => {\n                  var v, S;\n                  return ((v = b.dataset.label) != null ? v : \"\") + \": \" + q(b.parsed.y, (S = b.dataset.__format) != null ? S : l);\n                }\n              }\n            }\n          }\n        }\n      }, d.data.datasets.forEach((b, v) => {\n        var S, f;\n        b.__format = (f = (S = a.series[v]) == null ? void 0 : S.metric.format) != null ? f : l;\n      });\n    let p = new s(i.getContext(\"2d\"), d);\n    return {\n      destroy: () => {\n        p.destroy(), T(t);\n      }\n    };\n  }\n\n  // src/api/endpoints.ts\n  function je(t) {\n    return {\n      userIds: t.userIds,\n      teamIds: t.teamIds,\n      projectIds: t.projectIds\n    };\n  }\n  function yt(t) {\n    return {\n      ...je(t),\n      after: t.from,\n      before: t.to,\n      dialGroupIds: t.dialGroupIds,\n      anyTagId: t.tagIds\n    };\n  }\n  function ht(t, e) {\n    return D(\"/api/dial/calls-v1/count\", yt(t), e).then((r) => r.count);\n  }\n  function xt(t, e, r, o) {\n    return D(\"/api/dial/calls-v1\", {\n      ...yt(t),\n      offset: e,\n      count: r,\n      sortBy: \"date\",\n      sortOrder: \"asc\"\n    }, o).then((s) => {\n      var a;\n      return Array.isArray(s) ? s : (a = s == null ? void 0 : s.calls) != null ? a : [];\n    });\n  }\n  var kt = 4e12;\n  function wt(t, e, r) {\n    return D(\"/api/dial/statistics-v1/own/issue_1238_2\", {\n      from: new Date(t).toISOString(),\n      to: new Date(e - 1).toISOString(),\n      fixedIntervalMillis: kt\n    }, r);\n  }\n  function Be(t) {\n    return {\n      ...je(t),\n      from: t.from,\n      to: t.to,\n      anyTagId: t.tagIds,\n      isTest: \"false\"\n    };\n  }\n  function Ge(t, e) {\n    return D(\"/api/orders/orders-v2/count\", Be(t), e).then((r) => r.count);\n  }\n  function St(t, e) {\n    return D(\"/api/orders/orders-v2/sum\", Be(t), e).then((r) => {\n      var o, s;\n      return (s = (o = r.sum) != null ? o : r.total) != null ? s : 0;\n    });\n  }\n  function Ct(t, e, r, o) {\n    return D(\"/api/orders/orders-v3\", {\n      ...Be(t),\n      offset: e,\n      count: r,\n      sortBy: \"businessDate\",\n      sortOrder: \"asc\"\n    }, o);\n  }\n  function Rt(t, e, r) {\n    return D(\"/api/orders/statistics-v1/own/issue_1238_user_tag\", {\n      from: new Date(t - 1).toISOString(),\n      to: new Date(e).toISOString(),\n      fixedIntervalMillis: kt\n    }, r);\n  }\n  function Dt(t) {\n    return {\n      ...je(t),\n      from: t.from,\n      to: t.to,\n      statuses: t.statuses,\n      templateIds: t.templateIds,\n      isTest: \"false\"\n    };\n  }\n  function te(t, e) {\n    return D(\"/api/offers/offers-v1/count\", Dt(t), e).then((r) => r.count);\n  }\n  function Tt(t, e, r, o) {\n    return D(\"/api/offers/offers-v2\", {\n      ...Dt(t),\n      offset: e,\n      count: r,\n      sortBy: \"date\",\n      sortOrder: \"asc\"\n    }, o);\n  }\n  function At(t, e, r) {\n    return D(\"/api/users/statistics-v1/own/issue_1295_active_user_time\", {\n      from: new Date(t - 1).toISOString(),\n      to: new Date(e).toISOString()\n    }, r);\n  }\n  function gr(t, e, r, o) {\n    return D(\"/api/dial/dial-groups-v2\", {\n      teamIds: t.teamIds,\n      projectIds: t.projectIds,\n      offset: e,\n      count: r\n    }, o);\n  }\n  async function ye(t, e) {\n    let r = [];\n    for (let o = 0; ; o += 100) {\n      let s = await gr(t, o, 100, e);\n      if (r.push(...s), s.length < 100 || o > 5e3) break;\n    }\n    return r;\n  }\n\n  // src/engine/aggregate.ts\n  function Et(t, e) {\n    let r = new Map(t.map((l) => [l.id, l])), o = [], s = /* @__PURE__ */ new Set();\n    for (let l of e) {\n      let i = r.get(l);\n      i && (o.push(i), i.rate ? (s.add(i.rate[0]), s.add(i.rate[1])) : s.add(i.id));\n    }\n    return { compute: [...s].map((l) => r.get(l)).filter((l) => !!(l && !l.rate)), output: o };\n  }\n  function ze(t) {\n    if (t.agg === \"sum\" && t.value) {\n      let { value: r } = t;\n      return { id: t.id, add: (o) => {\n        let s = r(o);\n        return Number.isFinite(s) ? s : 0;\n      } };\n    }\n    let e = t.predicate;\n    return { id: t.id, add: (r) => e ? e(r) ? 1 : 0 : 1 };\n  }\n  function he(t, e, r) {\n    let o = /* @__PURE__ */ new Map();\n    for (let s of t) {\n      let a = [[]];\n      for (let l of e) {\n        let i = l.keys(s), u = [];\n        for (let d of a)\n          for (let p of i) u.push([...d, p]);\n        a = u;\n      }\n      for (let l of a) {\n        let i = JSON.stringify(l), u = o.get(i);\n        if (!u) {\n          let d = {};\n          e.forEach((p, y) => {\n            d[p.id] = l[y];\n          }), u = { dims: d, values: {} };\n          for (let p of r) u.values[p.id] = 0;\n          o.set(i, u);\n        }\n        for (let d of r) u.values[d.id] += d.add(s);\n      }\n    }\n    return [...o.values()];\n  }\n  function Lt(t, e) {\n    var r, o;\n    for (let s of e) {\n      if (!s.rate) continue;\n      let [a, l] = s.rate;\n      for (let i of t) {\n        let u = (r = i.values[l]) != null ? r : 0;\n        i.values[s.id] = u > 0 ? ((o = i.values[a]) != null ? o : 0) / u : 0;\n      }\n    }\n  }\n  function It(t, e) {\n    if (!t) return [null];\n    let r = e.userTeams.get(t);\n    return r && r.length ? r : [null];\n  }\n\n  // src/engine/planner.ts\n  var br = /* @__PURE__ */ new Set([\"user\", \"team\", \"tag\"]), vr = /* @__PURE__ */ new Set([\"user\", \"team\", \"tag\", \"time\"]);\n  function yr(t, e) {\n    let r = new Map(t.metrics.map((s) => [s.id, s])), o = (s) => !!(s != null && s.statsField);\n    return e.every((s) => {\n      let a = r.get(s);\n      return a ? a.rate ? o(r.get(a.rate[0])) && o(r.get(a.rate[1])) : o(a) : !1;\n    });\n  }\n  function hr(t, e) {\n    return e.dimensions.every((r) => {\n      if (r.id === \"team\") return !0;\n      if (!vr.has(r.id)) return !1;\n      let o = t.dimensions.find((s) => s.id === r.id);\n      return !!(o != null && o.statsSupported);\n    });\n  }\n  function xr(t) {\n    return t.filters.every((e) => !e.values.length || br.has(e.dimension));\n  }\n  function Pt(t, e) {\n    if (e.statsOnly) return { mode: \"stats\", fast: !0 };\n    if (e.id === \"dialGroups\") return { mode: \"dialGroupList\", fast: !0 };\n    if (e.id === \"offers\") {\n      let r = t.metrics.every((s) => s === \"count\" || s === \"signedCount\" || s === \"signRate\"), o = t.dimensions.every((s) => s.id === \"status\");\n      return r && o ? { mode: \"offerStatusCounts\", fast: !0 } : { mode: \"rows\", fast: !1 };\n    }\n    return yr(e, t.metrics) && hr(e, t) && xr(t) ? { mode: \"stats\", fast: !0 } : e.id === \"orders\" && t.dimensions.length === 0 && t.metrics.every((r) => r === \"count\" || r === \"sumAmount\" || r === \"avgAmount\") ? { mode: \"orderAggregates\", fast: !0 } : { mode: \"rows\", fast: !1 };\n  }\n\n  // src/engine/rowCache.ts\n  var z = /* @__PURE__ */ new Map();\n  function Mt(t, e, r, o) {\n    return JSON.stringify([t, e, r, o]);\n  }\n  function Ot(t, e) {\n    let r = z.get(t);\n    return !r || e && Date.now() - r.fetchedAt > 6e4 ? null : (z.delete(t), z.set(t, r), r);\n  }\n  function _t(t, e, r) {\n    for (z.delete(t), z.set(t, { rows: e, fetchedAt: Date.now(), total: r }); z.size > 3; ) {\n      let o = z.keys().next().value;\n      if (o == null) break;\n      z.delete(o);\n    }\n  }\n\n  // src/engine/execute.ts\n  async function X(t, e) {\n    try {\n      return await kr(t, e);\n    } catch (r) {\n      if ((r == null ? void 0 : r.name) === \"AbortError\" || e.signal.aborted) return { status: \"cancelled\" };\n      throw r;\n    }\n  }\n  async function kr(t, e) {\n    var f;\n    let r = M(t.source), o = Pt(t, r), { from: s, to: a } = pe(t.range), l = s.toISOString(), i = a.toISOString(), u = t.dimensions.find((h) => h.id === \"time\"), d = u ? (f = u.grain) != null ? f : mt(s, a) : null, p = d ? dt(s, a, d) : null, { compute: y, output: g } = Et(r.metrics, t.metrics), b = Ve(t, r, l, i), v, S = null;\n    switch (o.mode) {\n      case \"stats\":\n        v = await wr(t, r, y, b, l, i, p, e);\n        break;\n      case \"offerStatusCounts\":\n        v = await Rr(t, y, b, e);\n        break;\n      case \"orderAggregates\":\n        v = await Dr(y, b, e);\n        break;\n      case \"dialGroupList\": {\n        let h = await ye(b, e.signal);\n        v = he(h, Ft(t, r, d, e.lookups), y.map(ze)), S = h.length;\n        break;\n      }\n      case \"rows\": {\n        let h = await Tr(t, r, b, l, i, e);\n        if (h.status !== \"ok\") return h;\n        v = he(h.rows, Ft(t, r, d, e.lookups), y.map(ze)), S = h.total;\n        break;\n      }\n    }\n    return p && Ar(v, t.dimensions, p, y.map((h) => h.id)), Lt(v, g), Er(v, t.dimensions, p, g), {\n      status: \"ok\",\n      result: { query: t, plan: o, rows: v, output: g, dimSpecs: t.dimensions, buckets: p, grain: d, total: S }\n    };\n  }\n  function Ve(t, e, r, o) {\n    let s = { from: r, to: o };\n    for (let a of t.filters) {\n      if (!a.values.length) continue;\n      let l = e.filters.find((i) => i.id === a.dimension);\n      l && (s[l.target] = a.values);\n    }\n    return s;\n  }\n  async function wr(t, e, r, o, s, a, l, i) {\n    var w;\n    let u = Date.parse(s), d = Date.parse(a), p = l ? l.map((m, x) => ({\n      key: m.key,\n      fromMs: Math.max(m.startUtc.getTime(), u),\n      toMs: Math.min(x + 1 < l.length ? l[x + 1].startUtc.getTime() : d, d)\n    })) : [{ key: null, fromMs: u, toMs: d }], y = (m, x) => {\n      switch (e.id) {\n        case \"calls\":\n          return wt(m, x, i.signal);\n        case \"orders\":\n          return Rt(m, x, i.signal);\n        case \"time\":\n          return At(m, x, i.signal);\n        default:\n          throw new Error(\"Snabbstatistik saknas f\\xF6r: \" + e.id);\n      }\n    }, g = new Array(p.length), b = 0, v = 0;\n    p.length > 1 && ((w = i.onProgress) == null || w.call(i, { total: p.length, fetched: 0 })), await Promise.all(Array.from({ length: Math.min(3, p.length) }, async () => {\n      var m;\n      for (; b < p.length; ) {\n        let x = b++, R = p[x], A = await y(R.fromMs, R.toMs);\n        g[x] = R.key === null ? A : A.map((K) => ({ ...K, __bucketKey: R.key })), v += 1, p.length > 1 && ((m = i.onProgress) == null || m.call(i, { total: p.length, fetched: v }));\n      }\n    }));\n    let S = Sr(o, i.lookups), f = g.flat().filter(S), h = t.dimensions.map((m) => Cr(m, i.lookups)), k = r.map((m) => {\n      let x = m.statsField;\n      return { id: m.id, add: (R) => {\n        var A;\n        return Number((A = R[x]) != null ? A : 0);\n      } };\n    });\n    return he(f, h, k);\n  }\n  function Sr(t, e) {\n    var a, l, i;\n    let r = (a = t.userIds) != null && a.length ? new Set(t.userIds) : null, o = (l = t.teamIds) != null && l.length ? new Set(t.teamIds) : null, s = (i = t.tagIds) != null && i.length ? new Set(t.tagIds) : null;\n    return (u) => {\n      var d;\n      return !(r && !r.has(u.userId) || o && !((d = e.userTeams.get(u.userId)) != null ? d : []).some((y) => o.has(y)) || s && !(u.tagId && s.has(u.tagId)));\n    };\n  }\n  function Cr(t, e) {\n    switch (t.id) {\n      case \"user\":\n        return { id: \"user\", keys: (r) => {\n          var o;\n          return [(o = r.userId) != null ? o : null];\n        } };\n      case \"team\":\n        return { id: \"team\", keys: (r) => It(r.userId, e) };\n      case \"tag\":\n        return { id: \"tag\", keys: (r) => {\n          var o;\n          return [(o = r.tagId) != null ? o : null];\n        } };\n      case \"time\":\n        return { id: \"time\", keys: (r) => {\n          var o;\n          return [(o = r.__bucketKey) != null ? o : null];\n        } };\n      default:\n        throw new Error(\"Dimension st\\xF6ds ej i snabbstatistik: \" + t.id);\n    }\n  }\n  async function Rr(t, e, r, o) {\n    var p;\n    let s = new Set(e.map((y) => y.id)), a = (p = t.filters.find((y) => y.dimension === \"status\")) == null ? void 0 : p.values;\n    if (t.dimensions.some((y) => y.id === \"status\")) {\n      let y = a != null && a.length ? a : Te.map((b) => b.id), g = await Promise.all(y.map((b) => te({ ...r, statuses: [b] }, o.signal)));\n      return y.map((b, v) => ({\n        dims: { status: b },\n        values: { count: g[v], signedCount: b === \"signed\" ? g[v] : 0 }\n      }));\n    }\n    let i = !(a != null && a.length) || a.includes(\"signed\"), [u, d] = await Promise.all([\n      s.has(\"count\") ? te(r, o.signal) : Promise.resolve(0),\n      s.has(\"signedCount\") && i ? te({ ...r, statuses: [\"signed\"] }, o.signal) : Promise.resolve(0)\n    ]);\n    return [{ dims: {}, values: { count: u, signedCount: d } }];\n  }\n  async function Dr(t, e, r) {\n    let o = new Set(t.map((l) => l.id)), [s, a] = await Promise.all([\n      o.has(\"count\") ? Ge(e, r.signal) : Promise.resolve(0),\n      o.has(\"sumAmount\") ? St(e, r.signal) : Promise.resolve(0)\n    ]);\n    return [{ dims: {}, values: { count: s, sumAmount: a } }];\n  }\n  async function Tr(t, e, r, o, s, a) {\n    let l = Mt(e.id, t.filters, o, s), i = new Date(s).getTime() > Date.now(), u = Ot(l, i);\n    if (u) return { status: \"ok\", rows: u.rows, total: u.total };\n    let d = He(e.id, r), p = await be({\n      count: d.count,\n      page: d.page,\n      signal: a.signal,\n      onProgress: a.onProgress,\n      confirmLarge: a.confirmLarge,\n      maxRows: Oe\n    });\n    return p.status === \"ok\" && _t(l, p.rows, p.total), p;\n  }\n  function He(t, e) {\n    switch (t) {\n      case \"calls\":\n        return {\n          count: (r) => ht(e, r),\n          page: (r, o, s) => xt(e, r, o, s)\n        };\n      case \"orders\":\n        return {\n          count: (r) => Ge(e, r),\n          page: (r, o, s) => Ct(e, r, o, s)\n        };\n      case \"offers\":\n        return {\n          count: (r) => te(e, r),\n          page: (r, o, s) => Tt(e, r, o, s)\n        };\n      default:\n        throw new Error(\"Radh\\xE4mtning saknas f\\xF6r: \" + t);\n    }\n  }\n  function Ft(t, e, r, o) {\n    return t.dimensions.map((s) => {\n      if (s.id === \"time\") {\n        let i = e.rowTime, u = r != null ? r : \"day\";\n        return {\n          id: \"time\",\n          keys: (d) => {\n            let p = i ? i(d) : null;\n            return [p ? ut(p, u) : null];\n          }\n        };\n      }\n      let a = e.dimensions.find((i) => i.id === s.id), l = a == null ? void 0 : a.rowKeys;\n      if (!l) throw new Error(\"Dimension saknar radnycklar: \" + s.id);\n      return { id: s.id, keys: (i) => l(i, o) };\n    });\n  }\n  function Ar(t, e, r, o) {\n    var u;\n    let s = e.filter((d) => d.id !== \"time\").map((d) => d.id), a = /* @__PURE__ */ new Map();\n    s.length || a.set(\"[]\", {});\n    for (let d of t) {\n      if (!s.length) break;\n      let p = {};\n      for (let y of s) p[y] = (u = d.dims[y]) != null ? u : null;\n      a.set(JSON.stringify(s.map((y) => p[y])), p);\n    }\n    let l = (d) => JSON.stringify(e.map((p) => {\n      var y;\n      return (y = d[p.id]) != null ? y : null;\n    })), i = new Set(t.map((d) => l(d.dims)));\n    for (let d of r)\n      for (let p of a.values()) {\n        let y = { ...p, time: d.key }, g = l(y);\n        if (i.has(g)) continue;\n        i.add(g);\n        let b = {};\n        for (let v of o) b[v] = 0;\n        t.push({ dims: y, values: b });\n      }\n  }\n  function Er(t, e, r, o) {\n    var i;\n    let s = e.some((u) => u.id === \"time\"), a = /* @__PURE__ */ new Map();\n    r && r.forEach((u, d) => a.set(u.key, d));\n    let l = (i = o[0]) == null ? void 0 : i.id;\n    t.sort((u, d) => {\n      var p, y, g, b, v, S;\n      if (s) {\n        let f = (y = a.get((p = u.dims.time) != null ? p : \"\")) != null ? y : Number.MAX_SAFE_INTEGER, h = (b = a.get((g = d.dims.time) != null ? g : \"\")) != null ? b : Number.MAX_SAFE_INTEGER;\n        if (f !== h) return f - h;\n      }\n      if (l) {\n        let f = ((v = d.values[l]) != null ? v : 0) - ((S = u.values[l]) != null ? S : 0);\n        if (f !== 0) return f;\n      }\n      return 0;\n    });\n  }\n\n  // src/export/xlsx.ts\n  function Lr(t) {\n    let e = String(t != null ? t : \"\");\n    return /[\";\\n\\r]/.test(e) ? '\"' + e.replace(/\"/g, '\"\"') + '\"' : e;\n  }\n  function Ir(t) {\n    let e = \"\\uFEFF\" + t.map((r) => r.map(Lr).join(\";\")).join(`\\r\n`);\n    return new Blob([e], { type: \"text/csv;charset=utf-8\" });\n  }\n  async function Pr(t) {\n    let e = await _e(), r = e.utils.aoa_to_sheet(t), o = e.utils.book_new();\n    e.utils.book_append_sheet(o, r, \"Rapport\");\n    let s = e.write(o, { bookType: \"xlsx\", type: \"array\" });\n    return new Blob([s], { type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\" });\n  }\n  function re(t, e) {\n    let r = (t || n.appTitle).replace(/[^a-zA-Z0-9åäöÅÄÖ _-]+/g, \"\").trim() || \"rapport\", o = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);\n    return r + \" \" + o + \".\" + e;\n  }\n  async function Nt(t, e, r) {\n    let o = e === \"csv\" ? Ir(t) : await Pr(t);\n    pt(r, o);\n  }\n  async function xe(t, e, r, o) {\n    let s = M(t.query.source), a = t.dimSpecs.map((u) => {\n      var d, p;\n      return u.id === \"time\" ? n.time : (p = (d = s.dimensions.find((y) => y.id === u.id)) == null ? void 0 : d.label) != null ? p : u.id;\n    }), l = t.output.map((u) => u.format === \"duration\" ? u.label + \" (s)\" : u.label), i = [[...a, ...l]];\n    for (let u of t.rows)\n      i.push([\n        ...t.dimSpecs.map((d) => {\n          var p;\n          return ee(t, d.id, (p = u.dims[d.id]) != null ? p : null, e);\n        }),\n        ...t.output.map((d) => {\n          var p;\n          return gt((p = u.values[d.id]) != null ? p : 0, d.format);\n        })\n      ]);\n    await Nt(i, r, o);\n  }\n  async function ke(t, e, r, o) {\n    let s = M(t.source);\n    if (!s.rowColumns.length) throw new Error(\"Radexport saknas f\\xF6r: \" + s.label);\n    e === \"xlsx\" && await _e();\n    let { from: a, to: l } = pe(t.range), i = Ve(t, s, a.toISOString(), l.toISOString()), u = [[...s.rowColumns.map((p) => p.label)]], d = (p) => {\n      for (let y of p)\n        u.push(s.rowColumns.map((g) => g.value(y, o.lookups)));\n    };\n    if (s.id === \"dialGroups\") {\n      if (d(await ye(i, o.signal)), o.signal.aborted) return { status: \"cancelled\" };\n    } else {\n      let p = He(s.id, i), y = await be({\n        count: p.count,\n        page: p.page,\n        signal: o.signal,\n        onProgress: o.onProgress,\n        confirmLarge: o.confirmLarge,\n        maxRows: W,\n        onPage: d\n      });\n      if (y.status !== \"ok\") return y;\n    }\n    return await Nt(u, e, r), { status: \"ok\", count: u.length - 1 };\n  }\n\n  // src/ui/progress.ts\n  function we(t) {\n    let e = c(\"div\", { class: \"s2rv-progress-label\" }), r = c(\"div\", { class: \"s2rv-progress-bar\" }), o = c(\"div\", { class: \"s2rv-progress-track\" }, r), s = c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.cancel, onClick: () => t() }), a = c(\"div\", { class: \"s2rv-progress\", style: { display: \"none\" } }, e, o, s);\n    return {\n      root: a,\n      show(l) {\n        a.style.display = \"\", e.textContent = l, o.classList.add(\"s2rv-progress-indeterminate\"), r.style.width = \"30%\";\n      },\n      update(l) {\n        a.style.display = \"\", o.classList.remove(\"s2rv-progress-indeterminate\");\n        let i = Math.max(l.total, 1), u = Math.min(100, Math.round(l.fetched / i * 100));\n        r.style.width = u + \"%\", e.textContent = n.fetchingRows + \": \" + l.fetched.toLocaleString(\"sv-SE\") + \" / \" + l.total.toLocaleString(\"sv-SE\") + \" (\" + u + \" %)\";\n      },\n      hide() {\n        a.style.display = \"none\", o.classList.remove(\"s2rv-progress-indeterminate\"), r.style.width = \"0%\";\n      }\n    };\n  }\n  function V(t) {\n    let e = Math.ceil(t / 100), r = n.largeWarnTitle + `\n\n` + n.largeWarn1 + \" \" + t.toLocaleString(\"sv-SE\") + \" \" + n.largeWarn2 + \" \" + e.toLocaleString(\"sv-SE\") + \" \" + n.largeWarn3;\n    return Promise.resolve(window.confirm(r));\n  }\n\n  // src/ui/resultView.ts\n  function O(t) {\n    var r;\n    return console.error(\"s2rv\", t), t instanceof N && t.status === 401 ? n.noToken : ((r = t == null ? void 0 : t.message) != null ? r : \"\") === n.cdnError ? n.cdnError : t instanceof TypeError ? n.networkError : n.loadError;\n  }\n  function jt(t) {\n    let e = null, r = null, o = null, s = \"\", a = null, l = we(() => e == null ? void 0 : e.abort()), i = c(\"div\", { class: \"s2rv-status\" }), u = c(\"div\", { class: \"s2rv-viz\" }), d = c(\"div\", { class: \"s2rv-export-row\", style: { display: \"none\" } }), p = c(\"div\", { class: \"s2rv-result\" }, i, l.root, u, d);\n    function y() {\n      r && (r.destroy(), r = null), T(u), d.style.display = \"none\", i.textContent = \"\", i.classList.remove(\"s2rv-status-error\");\n    }\n    function g(h) {\n      i.textContent = h, i.classList.add(\"s2rv-status-error\");\n    }\n    function b() {\n      if (T(d), !a) return;\n      let h = a, k = M(h.source), w = [\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportAggregated + \" (Excel)\", onClick: () => void v(\"xlsx\") }),\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportAggregated + \" (CSV)\", onClick: () => void v(\"csv\") })\n      ];\n      k.rowColumns.length && w.push(\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportRaw + \" (Excel)\", onClick: () => void S(\"xlsx\") }),\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.exportRaw + \" (CSV)\", onClick: () => void S(\"csv\") })\n      ), t.onSaveExport && w.push(c(\"button\", {\n        class: \"s2rv-btn\",\n        type: \"button\",\n        text: n.saveExport,\n        onClick: () => {\n          let m = window.prompt(n.exportName, s || n.unnamedReport);\n          if (!m) return;\n          let x = k.rowColumns.length && window.confirm(n.exportRaw + \"? (OK = \" + n.exportRaw + \", Avbryt = \" + n.exportAggregated + \")\") ? \"raw\" : \"aggregated\";\n          t.onSaveExport({ name: m, kind: x, query: h, format: \"xlsx\" });\n        }\n      })), d.append(...w), d.style.display = \"\";\n    }\n    async function v(h) {\n      if (o)\n        try {\n          await xe(o, t.lookups, h, re(s, h));\n        } catch (k) {\n          g(O(k));\n        }\n    }\n    async function S(h) {\n      if (a) {\n        e == null || e.abort(), e = new AbortController(), l.show(n.exporting);\n        try {\n          let k = await ke(a, h, re(s, h), {\n            lookups: t.lookups,\n            signal: e.signal,\n            onProgress: (w) => l.update(w),\n            confirmLarge: V\n          });\n          l.hide(), k.status === \"cancelled\" ? i.textContent = n.cancelled : k.status === \"tooLarge\" && g(n.tooLargeAbs);\n        } catch (k) {\n          l.hide(), e.signal.aborted || g(O(k));\n        }\n      }\n    }\n    async function f(h, k, w) {\n      e == null || e.abort(), e = new AbortController();\n      let m = e.signal;\n      s = h, a = k, o = null, y(), l.show(n.fetching);\n      try {\n        let x = await X(k, {\n          lookups: t.lookups,\n          signal: m,\n          onProgress: (R) => l.update(R),\n          confirmLarge: V\n        });\n        if (m.aborted && x.status !== \"ok\") {\n          l.hide(), i.textContent = n.cancelled;\n          return;\n        }\n        switch (l.hide(), x.status) {\n          case \"cancelled\":\n            i.textContent = n.cancelled;\n            return;\n          case \"declined\":\n            return;\n          case \"tooLarge\":\n            g(x.total > W ? n.tooLargeAbs : n.tooLargeChart);\n            return;\n          case \"ok\": {\n            o = x.result;\n            let A = [x.result.plan.fast ? n.modeStats : n.modeRows];\n            x.result.total != null && A.push(x.result.total.toLocaleString(\"sv-SE\") + \" \" + n.rows), i.textContent = A.join(\" \\xB7 \"), r = await ve(u, x.result, w, t.lookups), b();\n            return;\n          }\n        }\n      } catch (x) {\n        l.hide(), m.aborted ? i.textContent = n.cancelled : g(O(x));\n      }\n    }\n    return {\n      root: p,\n      run: f,\n      cancel() {\n        e == null || e.abort();\n      },\n      destroy() {\n        e == null || e.abort(), r && (r.destroy(), r = null);\n      }\n    };\n  }\n\n  // src/ui/builder.ts\n  function Se(t) {\n    return JSON.parse(JSON.stringify(t));\n  }\n  function Ue() {\n    return {\n      id: null,\n      name: \"\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    };\n  }\n  var Mr = [\n    { id: \"table\", label: n.vizTable },\n    { id: \"bar\", label: n.vizBar },\n    { id: \"line\", label: n.vizLine },\n    { id: \"pie\", label: n.vizPie },\n    { id: \"kpi\", label: n.vizKpi }\n  ], Or = [\n    { id: \"hour\", label: n.grainHour },\n    { id: \"day\", label: n.grainDay },\n    { id: \"week\", label: n.grainWeek },\n    { id: \"month\", label: n.grainMonth }\n  ];\n  function Bt(t) {\n    let e = Ue(), r = c(\"div\", { class: \"s2rv-report-list\" }), o = c(\"div\", { class: \"s2rv-form\" }), s = jt({ lookups: t.lookups, onSaveExport: t.onSaveExport }), a = c(\n      \"div\",\n      { class: \"s2rv-sidebar\" },\n      c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.newReport, onClick: () => {\n        e = Ue(), g();\n      } }),\n      c(\"h3\", { text: n.savedReports }),\n      r\n    ), l = c(\"div\", { class: \"s2rv-main\" }, o, s.root), i = c(\"div\", { class: \"s2rv-builder\" }, a, l);\n    function u(k) {\n      var w;\n      return (w = t.lookups[k]) != null ? w : [];\n    }\n    function d(k, ...w) {\n      return c(\"div\", { class: \"s2rv-field\" }, c(\"label\", { class: \"s2rv-field-label\", text: k }), ...w);\n    }\n    function p(k) {\n      var w, m;\n      return (m = (w = e.query.dimensions[k]) == null ? void 0 : w.id) != null ? m : \"\";\n    }\n    function y(k, w) {\n      let m = e.query.dimensions.filter((R, A) => A !== k);\n      w && m.splice(k, 0, { id: w, ...w === \"time\" ? { grain: \"day\" } : {} });\n      let x = /* @__PURE__ */ new Set();\n      e.query.dimensions = m.filter((R) => x.has(R.id) ? !1 : (x.add(R.id), !0)).slice(0, 2), g();\n    }\n    function g() {\n      G(o), T(o);\n      let k = M(e.query.source), w = c(\"input\", {\n        type: \"text\",\n        value: e.name,\n        placeholder: n.reportName,\n        onInput: () => {\n          e.name = w.value;\n        }\n      }), m = c(\"select\", {\n        onChange: () => {\n          let C = M(m.value);\n          e.query.source = C.id, e.query.metrics = [C.metrics[0].id], e.query.dimensions = [], e.query.filters = [], g();\n        }\n      }, ...Ae.map((C) => P(C.id, C.label, C.id === k.id))), x = Pe({\n        items: k.metrics.map((C) => ({ id: C.id, label: C.label })),\n        selected: e.query.metrics,\n        placeholder: n.metrics,\n        allLabel: n.metrics,\n        selectedSuffix: n.selected,\n        onChange: (C) => {\n          e.query.metrics = C;\n        }\n      }), R = (C) => [\n        P(\"\", n.none, p(C) === \"\"),\n        ...k.dimensions.filter((I) => I.id !== p(C === 0 ? 1 : 0)).map((I) => P(I.id, I.label, I.id === p(C)))\n      ], A = c(\"select\", { onChange: () => y(0, A.value) }, ...R(0)), K = c(\"select\", { onChange: () => y(1, K.value) }, ...R(1)), _ = e.query.dimensions.find((C) => C.id === \"time\"), Y = null;\n      if (_) {\n        let C = c(\"select\", {\n          onChange: () => {\n            _.grain = C.value;\n          }\n        }, ...Or.map((I) => {\n          var $;\n          return P(I.id, I.label, I.id === (($ = _.grain) != null ? $ : \"day\"));\n        }));\n        Y = d(n.timeGrain, C);\n      }\n      let oe = k.filters.map((C) => {\n        var $, qe;\n        let I = (qe = ($ = e.query.filters.find((se) => se.dimension === C.id)) == null ? void 0 : $.values) != null ? qe : [];\n        return d(C.label, Pe({\n          items: u(C.lookup),\n          selected: I,\n          placeholder: C.label,\n          allLabel: n.all,\n          selectedSuffix: n.selected,\n          onChange: (se) => {\n            e.query.filters = e.query.filters.filter((Ut) => Ut.dimension !== C.id), se.length && e.query.filters.push({ dimension: C.id, values: se });\n          }\n        }));\n      }), Z = c(\"select\", {\n        onChange: () => {\n          e.viz.type = Z.value, Q.style.display = e.viz.type === \"bar\" ? \"\" : \"none\";\n        }\n      }, ...Mr.map((C) => P(C.id, C.label, C.id === e.viz.type))), F = c(\"input\", {\n        type: \"checkbox\",\n        checked: !!e.viz.stacked,\n        onChange: () => {\n          e.viz.stacked = F.checked;\n        }\n      }), Q = c(\"label\", { class: \"s2rv-inline\" }, F, c(\"span\", { text: \" \" + n.stacked }));\n      Q.style.display = e.viz.type === \"bar\" ? \"\" : \"none\";\n      let Re = c(\n        \"div\",\n        { class: \"s2rv-form-actions\" },\n        c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.runReport, onClick: () => void b() }),\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.saveReport, onClick: v }),\n        e.id ? c(\"button\", { class: \"s2rv-btn s2rv-btn-danger\", type: \"button\", text: n.deleteReport, onClick: S }) : null\n      ), ne = (C, ...I) => c(\n        \"div\",\n        { class: \"s2rv-form-section\" },\n        c(\"h4\", { class: \"s2rv-section-title\", text: C }),\n        c(\"div\", { class: \"s2rv-form-grid\" }, ...I)\n      );\n      o.append(\n        ne(\n          n.sectionData,\n          d(n.reportName, w),\n          d(n.source, m),\n          d(n.metrics, x)\n        ),\n        ne(\n          n.sectionGrouping,\n          d(n.groupBy, A),\n          d(n.groupBySecondary, K),\n          Y\n        ),\n        ne(\n          n.sectionPeriodFilters,\n          d(n.dateRange, fe(e.query.range, (C) => {\n            e.query.range = C;\n          })),\n          ...oe\n        ),\n        ne(\n          n.sectionViz,\n          d(n.chartType, c(\"span\", null, Z, Q))\n        ),\n        Re\n      );\n    }\n    async function b() {\n      e.query.metrics.length || (e.query.metrics = [M(e.query.source).metrics[0].id]), await s.run(e.name || n.unnamedReport, Se(e.query), { ...e.viz });\n    }\n    function v() {\n      var x, R;\n      let k = Date.now(), w = e.id ? t.getStore().reports[e.id] : null, m = {\n        id: (x = e.id) != null ? x : j(\"rep\"),\n        name: e.name || n.unnamedReport,\n        query: Se(e.query),\n        viz: { ...e.viz },\n        createdAt: (R = w == null ? void 0 : w.createdAt) != null ? R : k,\n        updatedAt: k\n      };\n      e.id = m.id, t.onSaveReport(m), f();\n    }\n    function S() {\n      if (!e.id) return;\n      let k = t.getStore(), w = Object.values(k.dashboards).some((m) => m.tiles.some((x) => x.reportId === e.id));\n      window.confirm(w ? n.reportInUse : n.confirmDelete) && (t.onDeleteReport(e.id), e = Ue(), g(), f());\n    }\n    function f() {\n      T(r);\n      let k = Object.values(t.getStore().reports).sort((w, m) => m.updatedAt - w.updatedAt);\n      if (!k.length) {\n        r.appendChild(c(\"div\", { class: \"s2rv-muted\", text: n.noSavedReports }));\n        return;\n      }\n      for (let w of k)\n        r.appendChild(c(\"button\", {\n          class: \"s2rv-list-item\" + (w.id === e.id ? \" s2rv-list-item-active\" : \"\"),\n          type: \"button\",\n          text: w.name,\n          onClick: () => h(w)\n        }));\n    }\n    function h(k) {\n      e = {\n        id: k.id,\n        name: k.name,\n        query: Se(k.query),\n        viz: { ...k.viz }\n      }, g(), f(), b();\n    }\n    return g(), f(), {\n      root: i,\n      openReport: h,\n      openQuery(k, w, m) {\n        e = { id: null, name: k, query: Se(w), viz: { ...m } }, g(), f(), b();\n      },\n      refreshList: f,\n      destroy() {\n        s.destroy(), G(i);\n      }\n    };\n  }\n\n  // src/templates.ts\n  var Ce = [\n    {\n      id: \"agent-leaderboard\",\n      name: \"Agentleaderboard\",\n      description: \"Samtal, svarade samtal och svarsfrekvens per anv\\xE4ndare denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\", \"answerRate\", \"connectedDuration\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"dial-group-report\",\n      name: \"Ringlisterapport\",\n      description: \"Kontakter, reserverade, \\xF6verhoppade och karant\\xE4n per ringlista.\",\n      query: {\n        source: \"dialGroups\",\n        metrics: [\"contactCount\", \"reservedCount\", \"skippedCount\", \"quarantinedCount\"],\n        dimensions: [{ id: \"dialGroup\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"esign-funnel\",\n      name: \"E-sign-tratt\",\n      description: \"Avtal per status \\u2014 f\\xF6lj fl\\xF6det fr\\xE5n utkast till signerat.\",\n      query: {\n        source: \"offers\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"status\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"bar\" }\n    },\n    {\n      id: \"orders-per-day\",\n      name: \"Ordrar per dag\",\n      description: \"Antal ordrar per dag de senaste 30 dagarna.\",\n      query: {\n        source: \"orders\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"time\", grain: \"day\" }],\n        filters: [],\n        range: { preset: \"last30\" }\n      },\n      viz: { type: \"line\" }\n    },\n    {\n      id: \"time-activity\",\n      name: \"Tid & aktivitet\",\n      description: \"Aktiv tid per anv\\xE4ndare denna vecka.\",\n      query: {\n        source: \"time\",\n        metrics: [\"activeDuration\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisWeek\" }\n      },\n      viz: { type: \"bar\" }\n    },\n    {\n      id: \"orders-per-user\",\n      name: \"Ordrar per anv\\xE4ndare\",\n      description: \"Antal ordrar och orderv\\xE4rde per anv\\xE4ndare denna m\\xE5nad.\",\n      query: {\n        source: \"orders\",\n        metrics: [\"count\", \"sumAmount\", \"avgAmount\"],\n        dimensions: [{ id: \"user\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"order-value-per-month\",\n      name: \"Orderv\\xE4rde per m\\xE5nad\",\n      description: \"Orderv\\xE4rde m\\xE5nad f\\xF6r m\\xE5nad i \\xE5r.\",\n      query: {\n        source: \"orders\",\n        metrics: [\"sumAmount\"],\n        dimensions: [{ id: \"time\", grain: \"month\" }],\n        filters: [],\n        range: { preset: \"thisYear\" }\n      },\n      viz: { type: \"bar\" }\n    },\n    {\n      id: \"calls-per-day\",\n      name: \"Samtal per dag\",\n      description: \"Samtal och svarade samtal per dag de senaste 30 dagarna.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\"],\n        dimensions: [{ id: \"time\", grain: \"day\" }],\n        filters: [],\n        range: { preset: \"last30\" }\n      },\n      viz: { type: \"line\" }\n    },\n    {\n      id: \"answer-rate-per-team\",\n      name: \"Svarsfrekvens per team\",\n      description: \"Samtal, svarade och svarsfrekvens per team denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\", \"answerRate\"],\n        dimensions: [{ id: \"team\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"calls-per-dial-group\",\n      name: \"Samtal per ringlista\",\n      description: \"Samtal och svarsfrekvens per ringlista denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\", \"connectedCount\", \"answerRate\"],\n        dimensions: [{ id: \"dialGroup\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"offers-per-template\",\n      name: \"Avtal per mall\",\n      description: \"Antal avtal, signerade och signeringsgrad per avtalsmall denna m\\xE5nad.\",\n      query: {\n        source: \"offers\",\n        metrics: [\"count\", \"signedCount\", \"signRate\"],\n        dimensions: [{ id: \"template\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"table\" }\n    },\n    {\n      id: \"call-tags\",\n      name: \"Samtalstaggar\",\n      description: \"F\\xF6rdelning av samtalstaggar denna m\\xE5nad.\",\n      query: {\n        source: \"calls\",\n        metrics: [\"count\"],\n        dimensions: [{ id: \"tag\" }],\n        filters: [],\n        range: { preset: \"thisMonth\" }\n      },\n      viz: { type: \"pie\" }\n    }\n  ];\n\n  // src/ui/dashboard.ts\n  function Gt(t) {\n    let e = null, r = [], o = c(\"div\", { class: \"s2rv-dash-header\" }), s = c(\"div\", { class: \"s2rv-dash-grid\" }), a = c(\"div\", { class: \"s2rv-dashboards\" }, o, s);\n    function l() {\n      var f;\n      for (let h of r)\n        h.controller.abort(), (f = h.chart) == null || f.destroy();\n      r = [];\n    }\n    function i() {\n      var k;\n      let f = t.getStore();\n      if (e && f.dashboards[e]) return f.dashboards[e];\n      let h = Object.values(f.dashboards).sort((w, m) => m.updatedAt - w.updatedAt)[0];\n      return e = (k = h == null ? void 0 : h.id) != null ? k : null, h != null ? h : null;\n    }\n    function u(f) {\n      f.updatedAt = Date.now(), t.onSave(f);\n    }\n    function d() {\n      let f = window.prompt(n.dashboardName, n.unnamedDashboard);\n      if (!f) return;\n      let h = { id: j(\"dash\"), name: f, tiles: [], defaultRange: { preset: \"thisMonth\" }, updatedAt: Date.now() };\n      e = h.id, u(h), S();\n    }\n    function p() {\n      let f = t.getStore(), h = [\"agent-leaderboard\", \"calls-per-day\", \"orders-per-day\", \"esign-funnel\"], k = [];\n      for (let m of h) {\n        let x = Ce.find((A) => A.id === m);\n        if (!x) continue;\n        let R = Object.values(f.reports).find((A) => A.name === x.name);\n        R || (R = {\n          id: j(\"rep\"),\n          name: x.name,\n          query: JSON.parse(JSON.stringify(x.query)),\n          viz: { ...x.viz },\n          createdAt: Date.now(),\n          updatedAt: Date.now()\n        }, t.onSaveReport(R)), k.push({ reportId: R.id, w: m === \"calls-per-day\" ? 2 : 1 });\n      }\n      let w = { id: j(\"dash\"), name: n.exampleDashboardName, tiles: k, defaultRange: { preset: \"last30\" }, updatedAt: Date.now() };\n      e = w.id, u(w), S();\n    }\n    function y(f) {\n      let h = Object.values(t.getStore().reports).sort((m, x) => x.updatedAt - m.updatedAt), k = P(\"\", \"+ \" + n.addTile, !0);\n      k.disabled = !0;\n      let w = c(\"select\", {\n        onChange: () => {\n          let m = h.find((x) => x.id === w.value);\n          m && (f.tiles.push({ reportId: m.id, w: 1 }), u(f), S());\n        }\n      }, k, ...h.map((m) => P(m.id, m.name, !1)));\n      return w.value = \"\", h.length || (w.disabled = !0, w.title = n.noSavedReports), w;\n    }\n    function g(f) {\n      var m;\n      G(o), T(o);\n      let h = t.getStore(), k = Object.values(h.dashboards).sort((x, R) => R.updatedAt - x.updatedAt);\n      if (!k.length) return;\n      let w = c(\"select\", {\n        onChange: () => {\n          e = w.value, S();\n        }\n      }, ...k.map((x) => P(x.id, x.name, x.id === (f == null ? void 0 : f.id))));\n      o.append(\n        w,\n        c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.newDashboard, onClick: d })\n      ), f && o.append(\n        fe((m = f.defaultRange) != null ? m : { preset: \"thisMonth\" }, (x) => {\n          f.defaultRange = x, u(f);\n        }),\n        c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.refreshAll, onClick: () => b(f) }),\n        y(f),\n        c(\"button\", {\n          class: \"s2rv-btn s2rv-btn-danger\",\n          type: \"button\",\n          text: n.deleteDashboard,\n          onClick: () => {\n            window.confirm(n.confirmDelete) && (t.onDelete(f.id), e = null, S());\n          }\n        })\n      );\n    }\n    function b(f) {\n      l(), T(s), f.tiles.forEach((h, k) => {\n        var Z;\n        let m = t.getStore().reports[h.reportId], x = c(\"div\", { class: \"s2rv-status\" }), R = c(\"div\", { class: \"s2rv-tile-content\" }), A = c(\n          \"div\",\n          { class: \"s2rv-tile-actions\" },\n          c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u25C0\\u25B6\", title: h.w === 2 ? n.tileNarrow : n.tileWide, onClick: () => {\n            h.w = h.w === 2 ? 1 : 2, u(f), S();\n          } }),\n          k > 0 ? c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u2191\", title: n.moveUp, onClick: () => {\n            f.tiles.splice(k - 1, 0, f.tiles.splice(k, 1)[0]), u(f), S();\n          } }) : null,\n          k < f.tiles.length - 1 ? c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u2193\", title: n.moveDown, onClick: () => {\n            f.tiles.splice(k + 1, 0, f.tiles.splice(k, 1)[0]), u(f), S();\n          } }) : null,\n          c(\"button\", { class: \"s2rv-btn s2rv-btn-small\", type: \"button\", text: \"\\u2715\", title: n.removeTile, onClick: () => {\n            f.tiles.splice(k, 1), u(f), S();\n          } })\n        ), K = c(\n          \"div\",\n          { class: \"s2rv-tile\" + (h.w === 2 ? \" s2rv-tile-wide\" : \"\") },\n          c(\n            \"div\",\n            { class: \"s2rv-tile-head\" },\n            c(\"h3\", { text: (Z = m == null ? void 0 : m.name) != null ? Z : n.unknown }),\n            A\n          ),\n          x,\n          R\n        );\n        if (s.appendChild(K), !m) {\n          x.textContent = n.unknown;\n          return;\n        }\n        let _ = new AbortController(), Y = { controller: _, chart: null };\n        r.push(Y);\n        let oe = JSON.parse(JSON.stringify(m.query));\n        f.defaultRange && (oe.range = { ...f.defaultRange }), x.textContent = n.fetching, (async () => {\n          try {\n            let F = await X(oe, {\n              lookups: t.lookups,\n              signal: _.signal,\n              onProgress: (Q) => {\n                let Re = Math.max(Q.total, 1);\n                x.textContent = n.fetchingRows + \" \" + Math.min(100, Math.round(Q.fetched / Re * 100)) + \" %\";\n              },\n              confirmLarge: V\n            });\n            if (_.signal.aborted) return;\n            F.status === \"ok\" ? (x.textContent = \"\", Y.chart = await ve(R, F.result, m.viz, t.lookups)) : F.status === \"tooLarge\" ? x.textContent = n.tooLargeChart : x.textContent = n.cancelled;\n          } catch (F) {\n            _.signal.aborted || (x.textContent = O(F));\n          }\n        })();\n      });\n    }\n    function v() {\n      l(), T(s), s.appendChild(c(\n        \"div\",\n        { class: \"s2rv-empty-state\" },\n        c(\"h3\", { text: n.emptyDashboardsTitle }),\n        c(\"p\", { text: n.emptyDashboardsText }),\n        c(\n          \"div\",\n          { class: \"s2rv-empty-state-actions\" },\n          c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.createExampleDashboard, onClick: p }),\n          c(\"button\", { class: \"s2rv-btn\", type: \"button\", text: n.newDashboard, onClick: d })\n        )\n      ));\n    }\n    function S() {\n      let f = i();\n      g(f), f ? b(f) : v();\n    }\n    return S(), {\n      root: a,\n      refresh: S,\n      destroy() {\n        l(), G(a);\n      }\n    };\n  }\n\n  // src/ui/exportPanel.ts\n  function zt(t) {\n    let e = null, r = we(() => e == null ? void 0 : e.abort()), o = c(\"div\", { class: \"s2rv-status\" }), s = c(\"div\", { class: \"s2rv-export-list\" }), a = c(\"div\", { class: \"s2rv-exports\" }, o, r.root, s);\n    async function l(u) {\n      e == null || e.abort(), e = new AbortController();\n      let d = e.signal;\n      o.textContent = \"\", r.show(n.exporting);\n      try {\n        let p = re(u.name, u.format);\n        if (u.kind === \"raw\") {\n          let y = await ke(u.query, u.format, p, {\n            lookups: t.lookups,\n            signal: d,\n            onProgress: (g) => r.update(g),\n            confirmLarge: V\n          });\n          r.hide(), y.status === \"cancelled\" ? o.textContent = n.cancelled : y.status === \"tooLarge\" && (o.textContent = n.tooLargeAbs);\n        } else {\n          let y = await X(u.query, {\n            lookups: t.lookups,\n            signal: d,\n            onProgress: (g) => r.update(g),\n            confirmLarge: V\n          });\n          r.hide(), y.status === \"ok\" ? await xe(y.result, t.lookups, u.format, p) : y.status === \"cancelled\" ? o.textContent = n.cancelled : y.status === \"tooLarge\" && (o.textContent = n.tooLargeChart);\n        }\n      } catch (p) {\n        r.hide(), d.aborted ? o.textContent = n.cancelled : o.textContent = O(p);\n      }\n    }\n    function i() {\n      T(s);\n      let u = Object.values(t.getStore().exports).sort((d, p) => p.updatedAt - d.updatedAt);\n      if (!u.length) {\n        s.appendChild(c(\"div\", { class: \"s2rv-muted\", text: n.noSavedExports }));\n        return;\n      }\n      for (let d of u)\n        s.appendChild(c(\n          \"div\",\n          { class: \"s2rv-export-item\" },\n          c(\n            \"div\",\n            { class: \"s2rv-export-item-name\" },\n            c(\"strong\", { text: d.name }),\n            c(\"span\", { class: \"s2rv-muted\", text: \" \\xB7 \" + (d.kind === \"raw\" ? n.exportRaw : n.exportAggregated) + \" \\xB7 \" + d.format.toUpperCase() })\n          ),\n          c(\n            \"div\",\n            null,\n            c(\"button\", { class: \"s2rv-btn s2rv-btn-primary\", type: \"button\", text: n.runAgain, onClick: () => void l(d) }),\n            c(\"button\", {\n              class: \"s2rv-btn s2rv-btn-danger\",\n              type: \"button\",\n              text: n.deleteReport,\n              onClick: () => {\n                window.confirm(n.confirmDelete) && (t.onDelete(d.id), i());\n              }\n            })\n          )\n        ));\n    }\n    return i(), {\n      root: a,\n      refresh: i,\n      destroy() {\n        e == null || e.abort();\n      }\n    };\n  }\n\n  // src/ui/templateGallery.ts\n  function Vt(t) {\n    return c(\n      \"div\",\n      { class: \"s2rv-templates\" },\n      c(\"p\", { class: \"s2rv-muted\", text: n.templatesIntro }),\n      c(\n        \"div\",\n        { class: \"s2rv-template-grid\" },\n        ...Ce.map((e) => c(\n          \"button\",\n          { class: \"s2rv-template-card\", type: \"button\", onClick: () => t(e) },\n          c(\"h3\", { text: e.name }),\n          c(\"p\", { text: e.description }),\n          c(\"span\", { class: \"s2rv-template-cta\", text: n.useTemplate + \" \\u2192\" })\n        ))\n      )\n    );\n  }\n\n  // src/ui/app.ts\n  function Ht(t) {\n    let e = new AbortController(), r = $e(), o = ie(), s = !1, a = null, l = null, i = null;\n    T(t);\n    let u = c(\"div\", { class: \"s2rv-boot\", text: n.appTitle + \" l\\xE4ser in\\u2026\" });\n    t.appendChild(u);\n    let d = c(\"span\", { class: \"s2rv-save-indicator\" });\n    function p() {\n      r.schedule(o, (b, v) => {\n        s || (d.classList.remove(\"s2rv-save-error\"), b === \"saving\" ? d.textContent = \"\\u2026\" : b === \"saved\" ? d.textContent = n.saveOk : (d.textContent = v != null ? v : n.saveError, d.classList.add(\"s2rv-save-error\")));\n      });\n    }\n    function y(b) {\n      a = Bt({\n        lookups: b,\n        getStore: () => o,\n        onSaveReport: (m) => {\n          o.reports[m.id] = m, p();\n        },\n        onDeleteReport: (m) => {\n          delete o.reports[m];\n          for (let x of Object.values(o.dashboards))\n            x.tiles = x.tiles.filter((R) => R.reportId !== m);\n          p(), l == null || l.refresh();\n        },\n        onSaveExport: (m) => {\n          let x = { ...m, id: j(\"exp\"), updatedAt: Date.now() };\n          o.exports[x.id] = x, p(), i == null || i.refresh();\n        }\n      }), l = Gt({\n        lookups: b,\n        getStore: () => o,\n        onSave: (m) => {\n          o.dashboards[m.id] = m, p();\n        },\n        onSaveReport: (m) => {\n          o.reports[m.id] = m, p(), a == null || a.refreshList();\n        },\n        onDelete: (m) => {\n          delete o.dashboards[m], p();\n        }\n      }), i = zt({\n        lookups: b,\n        getStore: () => o,\n        onDelete: (m) => {\n          delete o.exports[m], p();\n        }\n      });\n      let v = Vt((m) => {\n        a.openQuery(m.name, m.query, m.viz), w(\"reports\");\n      }), S = {\n        reports: a.root,\n        dashboards: l.root,\n        templates: v,\n        exports: i.root\n      }, f = [\n        { id: \"reports\", label: n.tabReports },\n        { id: \"dashboards\", label: n.tabDashboards },\n        { id: \"templates\", label: n.tabTemplates },\n        { id: \"exports\", label: n.tabExports }\n      ], h = /* @__PURE__ */ new Map(), k = c(\"nav\", { class: \"s2rv-tabs\" }, ...f.map((m) => {\n        let x = c(\"button\", {\n          class: \"s2rv-tab\",\n          type: \"button\",\n          text: m.label,\n          onClick: () => w(m.id)\n        });\n        return h.set(m.id, x), x;\n      }));\n      function w(m) {\n        for (let x of f)\n          S[x.id].style.display = x.id === m ? \"\" : \"none\", h.get(x.id).classList.toggle(\"s2rv-tab-active\", x.id === m);\n        m === \"dashboards\" && l.refresh(), m === \"exports\" && i.refresh(), m === \"reports\" && a.refreshList();\n      }\n      T(t), t.appendChild(c(\n        \"div\",\n        { class: \"s2rv-app\" },\n        c(\n          \"header\",\n          { class: \"s2rv-header\" },\n          d,\n          c(\"span\", { class: \"s2rv-version\", text: \"v\" + Ke })\n        ),\n        k,\n        ...f.map((m) => S[m.id])\n      )), w(\"reports\");\n    }\n    async function g() {\n      if (!ae()) {\n        u.textContent = n.noToken;\n        return;\n      }\n      try {\n        let [b, v] = await Promise.all([\n          Xe(e.signal),\n          r.load(e.signal)\n        ]);\n        if (s) return;\n        o = v, y(b);\n      } catch (b) {\n        if (s || e.signal.aborted) return;\n        u.textContent = O(b);\n      }\n    }\n    return g(), {\n      destroy() {\n        s = !0, e.abort(), a == null || a.destroy(), l == null || l.destroy(), i == null || i.destroy(), r.destroy(), G(t), T(t);\n      }\n    };\n  }\n\n  // src/index.ts\n  (function() {\n    let e = window;\n    if (e.__s2rvInstance && typeof e.__s2rvInstance.destroy == \"function\")\n      try {\n        e.__s2rvInstance.destroy();\n      } catch (l) {\n        console.error(\"s2rv destroy\", l);\n      }\n    let r = null, o = null;\n    function s() {\n      let l = document.getElementById(\"s2rv-root\");\n      if (l !== o) {\n        if (r) {\n          try {\n            r.destroy();\n          } catch (i) {\n            console.error(\"s2rv destroy\", i);\n          }\n          r = null, o = null;\n        }\n        if (l)\n          try {\n            r = Ht(l), o = l;\n          } catch (i) {\n            console.error(\"s2rv boot\", i);\n          }\n      }\n    }\n    let a = window.setInterval(s, 200);\n    e.__s2rvInstance = {\n      destroy() {\n        if (window.clearInterval(a), r) {\n          try {\n            r.destroy();\n          } catch (l) {\n            console.error(\"s2rv destroy\", l);\n          }\n          r = null, o = null;\n        }\n      }\n    }, s();\n  })();\n})();\n\n</script>","iconName":"chart-bar","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a49c77ee58b9e7a9eb3bbf4","name":"Länkar","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":1,"label":"Länkar"}],"html":"<!-- SaleSys Länkar — egen vy (views-v1). Självständig HTML, inga externa beroenden. -->\n<!-- OBS: Ingen \"dubbel-måsvinge\" i denna fil — vyer processas av Handlebars. -->\n<!--\n  Länktyper (alla byggda på webbformulär + kompletterande formulär):\n    - Samtalslänk:      kundnöjdhet på ett samtal — utfall sätts som TAGG på samtalet.\n    - Orderlänk:        formuläret skapar en order (t.ex. mötesbokning = order med datum).\n    - Uppföljningslänk: uppdaterar SAMMA order med ny tagg/kommentar.\n    - Avtalslänk:       vanlig e-signering (native-flödet).\n  Utskick döljs från vanliga offertvyn via isTest=true. Provenance via taggar:\n    s2links, s2type<call|order|orderupdate|offer>, s2form<formId>, s2order<orderId>, s2synced\n-->\n<meta charset=\"utf-8\">\n<div id=\"ln-root\">\n  <style>\n    #ln-root, #ln-root * { box-sizing: border-box; margin: 0; padding: 0; }\n    #ln-root {\n      --ln-primary: #1565C0;\n      --ln-danger: #E53935;\n      --ln-good: #43A047;\n      --ln-signed: #558B2F;\n      --ln-read: #F4511E;\n      --ln-declined: #E91E63;\n      --ln-text-1: #212121;\n      --ln-text-2: #616161;\n      --ln-text-3: #757575;\n      --ln-text-4: #9e9e9e;\n      --ln-bg-1: #fff;\n      --ln-bg-2: #fafafa;\n      --ln-bg-3: #eee;\n      --ln-hover: rgba(20, 20, 20, 0.035);\n      font-family: 'Nunito', sans-serif;\n      color: var(--ln-text-2);\n      font-size: 15px;\n      font-weight: normal;\n      line-height: 1.35em;\n      display: block;\n      background: var(--ln-bg-1);\n      min-height: 480px;\n      padding-bottom: 80px;\n    }\n    #ln-root h1, #ln-root h2, #ln-root h3 { font-weight: normal; color: var(--ln-text-1); }\n\n    /* ---------- Ribbon (header) ---------- */\n    #ln-root .ln-header {\n      position: relative;\n      display: flex; align-items: center; gap: 24px; flex-wrap: wrap;\n      margin-bottom: 24px; width: 100%;\n      min-height: 44px;\n    }\n    #ln-root .ln-title { font-size: 18px; font-weight: normal; color: var(--ln-text-1); }\n    #ln-root .ln-title small { display: block; font-size: 15px; color: var(--ln-text-3); font-weight: normal; margin-top: 2px; }\n    #ln-root .ln-tabs { display: flex; gap: 4px; }\n    #ln-root .ln-tab {\n      border: 0; background: transparent; color: var(--ln-primary);\n      font-size: 15px; font-weight: normal; font-family: inherit;\n      height: 42px; padding: 4px 10px; cursor: pointer; border-radius: 8px;\n      transition: background 250ms cubic-bezier(0.215, 0.61, 0.355, 1);\n    }\n    #ln-root .ln-tab:hover { background: var(--ln-hover); }\n    #ln-root .ln-tab.ln-on { background: rgba(21, 101, 192, 0.09); }\n    #ln-root .ln-head-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }\n    #ln-root .ln-updated { font-size: 13px; color: var(--ln-text-3); }\n\n    #ln-root .ln-banner {\n      display: none; margin: 0 0 18px; padding: 8px 14px; font-size: 13px;\n      border-radius: 8px; background: #fdecec; color: #8c2f2f; border: 1px solid #f0c9c9;\n    }\n    #ln-root .ln-banner.ln-show { display: block; }\n    #ln-root .ln-banner.ln-mock { background: #fdf4e3; color: #6d5215; border-color: #ecd9ad; }\n\n    #ln-root .ln-page { display: none; }\n    #ln-root .ln-page.ln-on { display: block; }\n\n    #ln-root .ln-card { background: var(--ln-bg-1); margin-bottom: 14px; min-height: 0; }\n    #ln-root .ln-card-head { display: flex; align-items: center; gap: 10px; padding: 0 0 12px; }\n    #ln-root .ln-card-title { font-size: 15px; color: var(--ln-text-1); }\n    #ln-root .ln-card-sub { font-size: 15px; color: var(--ln-text-3); }\n\n    /* ---------- Knappar / fält ---------- */\n    #ln-root .ln-btn {\n      border: 0; color: var(--ln-primary); background: transparent;\n      font-size: 15px; font-weight: normal; font-family: inherit;\n      height: 42px; padding: 4px 10px; border-radius: 8px; cursor: pointer; white-space: nowrap;\n      transition: background 250ms cubic-bezier(0.215, 0.61, 0.355, 1);\n    }\n    #ln-root .ln-btn:hover { background: var(--ln-hover); }\n    #ln-root .ln-btn.ln-primary { background: var(--ln-primary); color: #fff; }\n    #ln-root .ln-btn.ln-primary:hover { background: #1258a8; }\n    #ln-root .ln-btn.ln-ghost { color: var(--ln-text-3); }\n    #ln-root .ln-btn.ln-compact { height: 32px; border-radius: 16px; font-size: 14px; }\n    #ln-root .ln-btn:disabled { opacity: 0.5; cursor: default; }\n    #ln-root .ln-input, #ln-root .ln-select, #ln-root .ln-textarea {\n      width: 100%; font-family: inherit; font-size: 15px; color: var(--ln-text-2);\n      border: 1px solid var(--ln-bg-3); border-radius: 8px; padding: 9px 12px; background: var(--ln-bg-1);\n    }\n    #ln-root .ln-input:focus, #ln-root .ln-select:focus, #ln-root .ln-textarea:focus {\n      outline: none; border-color: rgba(21, 101, 192, 0.35);\n      box-shadow: 0 0 0 1px rgba(21, 101, 192, 0.35);\n    }\n    #ln-root .ln-label { display: block; font-size: 13px; color: var(--ln-text-3); margin: 14px 0 4px; }\n    #ln-root .ln-hint { font-size: 13px; color: var(--ln-text-4); margin-top: 4px; }\n\n    /* ---------- Chips / status ---------- */\n    #ln-root .ln-chips { display: flex; gap: 4px; flex-wrap: wrap; }\n    #ln-root .ln-chip {\n      border: 0; background: transparent; color: var(--ln-primary);\n      font-size: 14px; font-weight: normal; font-family: inherit;\n      height: 32px; padding: 4px 12px; border-radius: 16px; cursor: pointer;\n      transition: background 250ms cubic-bezier(0.215, 0.61, 0.355, 1);\n    }\n    #ln-root .ln-chip:hover { background: var(--ln-hover); }\n    #ln-root .ln-chip.ln-on { background: rgba(21, 101, 192, 0.09); }\n    #ln-root .ln-status {\n      display: inline-block; font-size: 14px; color: #fff;\n      padding: 3px 8px; border-radius: 6px; white-space: nowrap;\n    }\n    #ln-root .ln-status-plain { display: inline-block; font-size: 15px; color: var(--ln-text-3); }\n    #ln-root .ln-status-negate { display: inline-block; font-size: 15px; color: var(--ln-danger); }\n    #ln-root .ln-badge {\n      display: inline-block; font-size: 14px; font-weight: normal;\n      padding: 3px 8px; border-radius: 6px; color: #fff; white-space: nowrap;\n    }\n\n    /* ---------- Tabell ---------- */\n    #ln-root .ln-table { width: 100%; border-collapse: collapse; table-layout: auto; }\n    #ln-root .ln-table th {\n      text-align: left; font-size: 15px; font-weight: normal;\n      color: var(--ln-text-1);\n      padding: 12px 36px 12px 0; white-space: nowrap; user-select: none;\n    }\n    #ln-root .ln-table td {\n      padding: 8px 36px 8px 0; font-size: 15px; color: var(--ln-text-2);\n      vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n    }\n    #ln-root .ln-table tbody tr { transition: background 150ms ease-out; }\n    #ln-root .ln-table tbody tr:hover { background: var(--ln-hover); }\n    #ln-root .ln-table .ln-dim { color: var(--ln-text-3); font-size: 14px; }\n    #ln-root .ln-empty { padding: 48px 0; text-align: center; color: var(--ln-text-3); font-size: 15px; }\n\n    /* ---------- Länk-kort ---------- */\n    #ln-root .ln-links-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 18px; }\n    #ln-root .ln-link-card {\n      background: var(--ln-bg-1); border: 1px solid var(--ln-bg-3); border-radius: 8px;\n      padding: 16px 18px; display: flex; flex-direction: column; gap: 8px;\n    }\n    #ln-root .ln-link-name { font-size: 15px; color: var(--ln-text-1); }\n    #ln-root .ln-link-meta { font-size: 14px; color: var(--ln-text-3); }\n    #ln-root .ln-link-actions { display: flex; gap: 12px; margin-top: 6px; }\n\n    /* ---------- Wizard ---------- */\n    #ln-root .ln-wizard { max-width: 720px; }\n    #ln-root .ln-steps { display: flex; gap: 6px; margin-bottom: 18px; }\n    #ln-root .ln-step-ind { flex: 1; height: 4px; border-radius: 2px; background: var(--ln-bg-3); }\n    #ln-root .ln-step-ind.ln-on { background: var(--ln-primary); }\n    #ln-root .ln-typegrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; padding: 0 0 16px; }\n    #ln-root .ln-typecard {\n      border: 1px solid var(--ln-bg-3); border-radius: 8px; padding: 16px; cursor: pointer;\n      background: var(--ln-bg-1); text-align: left; font-family: inherit;\n      transition: background 250ms cubic-bezier(0.215, 0.61, 0.355, 1);\n    }\n    #ln-root .ln-typecard:hover { background: var(--ln-hover); }\n    #ln-root .ln-typecard.ln-on { border-color: var(--ln-primary); background: rgba(21, 101, 192, 0.05); }\n    #ln-root .ln-typecard h3 { font-size: 15px; margin-bottom: 4px; color: var(--ln-text-1); }\n    #ln-root .ln-typecard p { font-size: 14px; color: var(--ln-text-3); line-height: 1.5; }\n    #ln-root .ln-wizard-body { padding: 0 0 16px; }\n    #ln-root .ln-wizard-foot { display: flex; justify-content: space-between; padding: 12px 0; border-top: 1px solid var(--ln-bg-3); }\n    #ln-root .ln-summary { font-size: 15px; line-height: 1.8; }\n    #ln-root .ln-summary b { font-weight: normal; color: var(--ln-text-1); }\n    #ln-root .ln-result { padding: 24px 0; text-align: center; }\n    #ln-root .ln-result-icon { font-size: 36px; color: var(--ln-good); }\n\n    /* ---------- Modal ---------- */\n    #ln-root .ln-overlay {\n      display: none; position: fixed; inset: 0; z-index: 4000;\n      background: rgba(20, 30, 34, 0.45);\n      align-items: flex-start; justify-content: center; padding: 60px 16px;\n    }\n    #ln-root .ln-overlay.ln-show { display: flex; }\n    #ln-root .ln-modal {\n      background: var(--ln-bg-2); border-radius: 8px; width: 100%; max-width: 460px;\n      box-shadow: 0 2px 16px rgba(10, 10, 10, 0.175); overflow: hidden;\n      font-size: 15px;\n    }\n    #ln-root .ln-modal-head { display: flex; align-items: center; padding: 14px 18px; }\n    #ln-root .ln-modal-title { font-size: 18px; font-weight: normal; color: var(--ln-text-1); }\n    #ln-root .ln-modal-x { margin-left: auto; border: 0; background: none; font-size: 20px; color: var(--ln-text-3); cursor: pointer; border-radius: 50%; width: 32px; height: 32px; }\n    #ln-root .ln-modal-x:hover { background: var(--ln-hover); }\n    #ln-root .ln-modal-body { padding: 0 18px 16px; }\n    #ln-root .ln-modal-foot { display: flex; justify-content: flex-end; gap: 12px; padding: 12px 18px; }\n    #ln-root .ln-error-txt { color: var(--ln-danger); font-size: 13px; margin-top: 8px; }\n\n    #ln-root .ln-log { font-size: 13px; color: var(--ln-text-3); padding: 8px 0; max-height: 120px; overflow-y: auto; border-top: 1px solid var(--ln-bg-3); }\n    #ln-root .ln-log div { padding: 1px 0; }\n  </style>\n\n  <div class=\"ln-header\">\n    <div class=\"ln-title\">Länkar<small>Utskick av samtals-, order- och avtalslänkar</small></div>\n    <div class=\"ln-tabs\">\n      <button class=\"ln-tab ln-on\" id=\"lnTabSend\" type=\"button\">Utskick</button>\n      <button class=\"ln-tab\" id=\"lnTabLinks\" type=\"button\">Mina länkar</button>\n      <button class=\"ln-tab\" id=\"lnTabCreate\" type=\"button\">Skapa länk</button>\n    </div>\n    <div class=\"ln-head-right\">\n      <span class=\"ln-updated\" id=\"lnUpdated\"></span>\n      <button class=\"ln-btn ln-ghost\" id=\"lnRefresh\" type=\"button\">Uppdatera</button>\n      <button class=\"ln-btn ln-primary\" id=\"lnNewSendout\" type=\"button\">Nytt utskick</button>\n    </div>\n  </div>\n  <div class=\"ln-banner\" id=\"lnBanner\"></div>\n\n  <!-- ============ Flik: Utskick ============ -->\n  <div class=\"ln-page ln-on\" id=\"lnPageSend\">\n    <div class=\"ln-card\">\n      <div class=\"ln-card-head\">\n        <div>\n          <div class=\"ln-card-title\">Utskick</div>\n          <div class=\"ln-card-sub\" id=\"lnSendSub\"></div>\n        </div>\n        <div style=\"margin-left:auto\" class=\"ln-chips\" id=\"lnStatusChips\"></div>\n      </div>\n      <div id=\"lnSendTableWrap\"></div>\n      <div class=\"ln-log\" id=\"lnSyncLog\" style=\"display:none\"></div>\n    </div>\n  </div>\n\n  <!-- ============ Flik: Mina länkar ============ -->\n  <div class=\"ln-page\" id=\"lnPageLinks\">\n    <div class=\"ln-links-grid\" id=\"lnLinksGrid\"></div>\n  </div>\n\n  <!-- ============ Flik: Skapa länk ============ -->\n  <div class=\"ln-page\" id=\"lnPageCreate\">\n    <div class=\"ln-wizard\">\n      <div class=\"ln-steps\" id=\"lnStepInd\"></div>\n      <div class=\"ln-card\">\n        <div id=\"lnWizardBody\"></div>\n        <div class=\"ln-wizard-foot\" id=\"lnWizardFoot\">\n          <button class=\"ln-btn ln-ghost\" id=\"lnWizBack\" type=\"button\">Tillbaka</button>\n          <button class=\"ln-btn ln-primary\" id=\"lnWizNext\" type=\"button\">Nästa</button>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <!-- ============ Modal: nytt utskick ============ -->\n  <div class=\"ln-overlay\" id=\"lnSendOverlay\">\n    <div class=\"ln-modal\">\n      <div class=\"ln-modal-head\">\n        <div class=\"ln-modal-title\">Nytt utskick</div>\n        <button class=\"ln-modal-x\" id=\"lnSendClose\" type=\"button\">&#215;</button>\n      </div>\n      <div class=\"ln-modal-body\" id=\"lnSendBody\"></div>\n      <div class=\"ln-modal-foot\">\n        <button class=\"ln-btn ln-ghost\" id=\"lnSendCancel\" type=\"button\">Avbryt</button>\n        <button class=\"ln-btn ln-primary\" id=\"lnSendGo\" type=\"button\">Skicka</button>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script>\n(function () {\n  \"use strict\";\n\n  var API_BASE = \"https://app.salesys.se/api\";\n  var PUBLIC_STATUS = \"https://skaffa.salesys.se/api/offers/offer-information-v1/status\";\n\n  var TYPES = {\n    call: { label: \"Samtalslänk\", color: \"#7B1FA2\", desc: \"Kundnöjdhet på ett samtal. Svaret sätter en tagg på samtalet.\" },\n    order: { label: \"Orderlänk\", color: \"#1565C0\", desc: \"Formuläret skapar en order, t.ex. en mötesbokning med datum.\" },\n    orderupdate: { label: \"Uppföljningslänk\", color: \"#00838F\", desc: \"Uppdaterar en befintlig order med ny tagg och kommentar.\" },\n    offer: { label: \"Avtalslänk\", color: \"#558B2F\", desc: \"Vanlig e-signering av avtal (native-flödet).\" }\n  };\n  //  Samma statusfärger som native offertvyn (chip för färgade, röd text för negerade).\n  var STATUS_DEF = {\n    created: { label: \"Ej skickad\", plain: true },\n    distributed: { label: \"Skickad\", plain: true },\n    read: { label: \"Öppnad\", color: \"#F4511E\" },\n    signed: { label: \"Klar\", color: \"#558B2F\" },\n    expired: { label: \"Utgången\", negate: true },\n    declined: { label: \"Avböjd\", color: \"#E91E63\" }\n  };\n  function renderStatus(st) {\n    if (st.negate) { return '<span class=\"ln-status-negate\">' + esc(st.label) + \"</span>\"; }\n    if (st.color) { return '<span class=\"ln-status\" style=\"background:' + st.color + '\">' + esc(st.label) + \"</span>\"; }\n    return '<span class=\"ln-status-plain\">' + esc(st.label) + \"</span>\";\n  }\n\n  var state = {\n    mock: false,\n    error: null,\n    tab: \"send\",\n    templates: [],          //  offertmallar\n    forms: [],              //  webbformulär (rådata)\n    links: [],              //  länkdefinitioner (formulär med s2links-config i developerMessage)\n    offers: [],             //  utskick (isTest-offerter taggade s2links)\n    filterStatus: \"all\",\n    syncBusy: false,\n    syncLog: [],\n    wizard: null,\n    send: null              //  { link, ... } för utskicksmodal\n  };\n  var timers = { poll: null };\n\n  /* ================= Hjälpare ================= */\n  function $(id) { return document.getElementById(id); }\n  function esc(s) {\n    return String(s == null ? \"\" : s).replace(/[&<>\"']/g, function (c) {\n      return { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" }[c];\n    });\n  }\n  function getToken() {\n    var m = /(?:^|;\\s*)s2_utoken=([^;]+)/.exec(document.cookie || \"\");\n    return m ? decodeURIComponent(m[1]) : null;\n  }\n  function fmtTime(v) {\n    if (!v) { return \"–\"; }\n    var d = new Date(v);\n    if (isNaN(d.getTime())) { return \"–\"; }\n    var now = new Date();\n    var sameDay = d.toDateString() === now.toDateString();\n    function p(n) { return (n < 10 ? \"0\" : \"\") + n; }\n    var hm = p(d.getHours()) + \":\" + p(d.getMinutes());\n    if (sameDay) { return \"Idag \" + hm; }\n    return d.getFullYear() + \"-\" + p(d.getMonth() + 1) + \"-\" + p(d.getDate()) + \" \" + hm;\n  }\n  //  Taggar normaliseras av backend: gemener, icke-ordtecken tas bort.\n  function normTag(s) { return String(s || \"\").toLowerCase().replace(/[^a-z0-9åäö]/g, \"\"); }\n  function hasTag(offer, tag) { return (offer.tags || []).indexOf(tag) !== -1; }\n  function tagValue(offer, prefix) {\n    var t = (offer.tags || []).filter(function (x) { return x.indexOf(prefix) === 0 && x.length > prefix.length; })[0];\n    return t ? t.slice(prefix.length) : null;\n  }\n  function offerType(offer) {\n    var t = tagValue(offer, \"s2type\");\n    return t && TYPES[t] ? t : null;\n  }\n  function log(msg) {\n    state.syncLog.unshift(fmtTime(new Date()) + \" — \" + msg);\n    state.syncLog = state.syncLog.slice(0, 40);\n    renderSyncLog();\n  }\n\n  function api(method, path, params, body) {\n    var qs = [];\n    Object.keys(params || {}).forEach(function (k) {\n      var v = params[k];\n      if (v == null || v === \"\") { return; }\n      if (Array.isArray(v)) { v.forEach(function (x) { qs.push(encodeURIComponent(k) + \"=\" + encodeURIComponent(x)); }); }\n      else { qs.push(encodeURIComponent(k) + \"=\" + encodeURIComponent(v)); }\n    });\n    var url = API_BASE + path + (qs.length ? \"?\" + qs.join(\"&\") : \"\");\n    var opts = { method: method, headers: { Authorization: \"Bearer \" + getToken() } };\n    if (body != null) {\n      opts.headers[\"Content-Type\"] = \"application/json\";\n      opts.body = JSON.stringify(body);\n    }\n    return fetch(url, opts).then(function (res) {\n      return res.text().then(function (txt) {\n        var json = null;\n        try { json = txt ? JSON.parse(txt) : null; } catch (e) { json = null; }\n        if (!res.ok) {\n          var err = new Error(\"HTTP \" + res.status + \" \" + method + \" \" + path + (txt ? \" — \" + txt.slice(0, 180) : \"\"));\n          err.status = res.status;\n          throw err;\n        }\n        return json;\n      });\n    });\n  }\n  function items(res) {\n    if (Array.isArray(res)) { return res; }\n    if (res && Array.isArray(res.items)) { return res.items; }\n    if (res && Array.isArray(res.results)) { return res.results; }\n    return [];\n  }\n\n  /* ================= Länkdefinitioner (webbformulär + developerMessage) ================= */\n  function parseLinkConfig(form) {\n    if (!form || !form.developerMessage) { return null; }\n    var m = /\\bS2LINKS:(\\S+)/.exec(form.developerMessage);\n    var raw = m ? m[1] : form.developerMessage;\n    try {\n      var cfg = JSON.parse(raw);\n      if (cfg && cfg.s2links && cfg.s2links.v === 1) { return cfg.s2links; }\n    } catch (e) { /* inte en s2links-form */ }\n    return null;\n  }\n  function buildLinks() {\n    state.links = state.forms.map(function (f) {\n      var cfg = parseLinkConfig(f);\n      if (!cfg) { return null; }\n      return {\n        formId: f.id,\n        name: f.name,\n        type: cfg.type,\n        templateId: cfg.templateId,\n        scoreFieldId: cfg.scoreFieldId || null,\n        commentFieldId: cfg.commentFieldId || null,\n        callTagIds: cfg.callTagIds || [],\n        orderTagIds: cfg.orderTagIds || [],\n        fieldIds: cfg.fieldIds || [],\n        comment: cfg.comment || \"\",\n        scale: cfg.scale || 5,\n        createdAt: f.date || f.createdAt || null\n      };\n    }).filter(Boolean);\n  }\n  function linkForOffer(offer) {\n    var formId = tagValue(offer, \"s2form\");\n    if (!formId) { return null; }\n    return state.links.filter(function (l) { return normTag(l.formId) === formId; })[0] || null;\n  }\n\n  /* ================= Dataladdning ================= */\n  function loadTemplates() {\n    return api(\"GET\", \"/offers/offer-templates-v2\", { count: 100 }).then(function (res) {\n      state.templates = items(res).filter(function (t) { return !t.isRemoved; });\n    }).catch(function () { state.templates = []; });\n  }\n  function tplName(t) { return t ? (t.title || t.name || t.id) : \"\"; }\n  function loadForms() {\n    return api(\"GET\", \"/offers/web-forms-v1\", { count: 100 }).then(function (res) {\n      state.forms = items(res);\n      buildLinks();\n    });\n  }\n  function loadOffers() {\n    return api(\"GET\", \"/offers/offers-v2\", { isTest: \"true\", tags: [\"s2links\"], count: 100 }).then(function (res) {\n      state.offers = items(res).filter(function (o) { return hasTag(o, \"s2links\"); });\n      state.offers.sort(function (a, b) {\n        return new Date(b.date || b.createdAt || 0) - new Date(a.date || a.createdAt || 0);\n      });\n    });\n  }\n  function loadAll() {\n    return Promise.all([loadTemplates(), loadForms()]).then(loadOffers);\n  }\n\n  /* ================= Rendering ================= */\n  function renderBanner() {\n    var b = $(\"lnBanner\");\n    if (state.error) {\n      b.textContent = \"Fel: \" + state.error;\n      b.className = \"ln-banner ln-show\";\n    } else if (state.mock) {\n      b.textContent = \"Demoläge — visar exempeldata (ingen inloggning hittades).\";\n      b.className = \"ln-banner ln-show ln-mock\";\n    } else {\n      b.className = \"ln-banner\";\n    }\n  }\n  function renderUpdated() {\n    $(\"lnUpdated\").textContent = \"Uppdaterad \" + fmtTime(new Date());\n  }\n  function setTab(tab) {\n    state.tab = tab;\n    $(\"lnTabSend\").className = \"ln-tab\" + (tab === \"send\" ? \" ln-on\" : \"\");\n    $(\"lnTabLinks\").className = \"ln-tab\" + (tab === \"links\" ? \" ln-on\" : \"\");\n    $(\"lnTabCreate\").className = \"ln-tab\" + (tab === \"create\" ? \" ln-on\" : \"\");\n    $(\"lnPageSend\").className = \"ln-page\" + (tab === \"send\" ? \" ln-on\" : \"\");\n    $(\"lnPageLinks\").className = \"ln-page\" + (tab === \"links\" ? \" ln-on\" : \"\");\n    $(\"lnPageCreate\").className = \"ln-page\" + (tab === \"create\" ? \" ln-on\" : \"\");\n    if (tab === \"create\" && !state.wizard) { wizardReset(); }\n    if (tab === \"create\") { renderWizard(); }\n  }\n\n  function statusOf(offer) {\n    var s = offer.status || \"created\";\n    return STATUS_DEF[s] || { label: s, plain: true };\n  }\n  function recipientOf(offer) {\n    var r = offer.recipient || offer.contact || {};\n    var name = r.name || offer.contactName || offer.name || \"\";\n    var phone = r.sms || r.phone || offer.phone || \"\";\n    return { name: name || \"(namnlös)\", phone: phone };\n  }\n  function offerUrlOf(offer) {\n    if (offer.linkUrl) { return offer.linkUrl; }\n    var pid = offer.publicId || offer.publicID || null;\n    return pid ? \"https://avtal.salesys.se/\" + pid : null;\n  }\n\n  function renderStatusChips() {\n    var keys = [\"all\", \"distributed\", \"read\", \"signed\", \"expired\"];\n    var labels = { all: \"Alla\" };\n    var html = keys.map(function (k) {\n      var lbl = labels[k] || (STATUS_DEF[k] ? STATUS_DEF[k].label : k);\n      var n = k === \"all\" ? state.offers.length : state.offers.filter(function (o) { return (o.status || \"created\") === k; }).length;\n      return '<button type=\"button\" class=\"ln-chip' + (state.filterStatus === k ? \" ln-on\" : \"\") + '\" data-st=\"' + k + '\">' + esc(lbl) + \" (\" + n + \")</button>\";\n    }).join(\"\");\n    $(\"lnStatusChips\").innerHTML = html;\n    Array.prototype.forEach.call($(\"lnStatusChips\").querySelectorAll(\".ln-chip\"), function (el) {\n      el.addEventListener(\"click\", function () {\n        state.filterStatus = el.getAttribute(\"data-st\");\n        renderSendouts();\n      });\n    });\n  }\n\n  function renderSendouts() {\n    renderStatusChips();\n    var list = state.offers.filter(function (o) {\n      return state.filterStatus === \"all\" || (o.status || \"created\") === state.filterStatus;\n    });\n    $(\"lnSendSub\").textContent = state.offers.length + \" utskick totalt\";\n    if (!list.length) {\n      $(\"lnSendTableWrap\").innerHTML = '<div class=\"ln-empty\">Inga utskick ännu. Skapa en länk och gör ditt första utskick.</div>';\n      return;\n    }\n    var rows = list.map(function (o, i) {\n      var rec = recipientOf(o);\n      var t = offerType(o);\n      var typeDef = t ? TYPES[t] : null;\n      var st = statusOf(o);\n      var link = linkForOffer(o);\n      var synced = hasTag(o, \"s2synced\");\n      var syncTxt = t === \"offer\" ? \"–\" : (synced ? \"Synkad\" : ((o.status || \"\") === \"signed\" ? \"Väntar\" : \"–\"));\n      var url = offerUrlOf(o);\n      return \"<tr>\" +\n        \"<td><div>\" + esc(rec.name) + '</div><div class=\"ln-dim\">' + esc(rec.phone) + \"</div></td>\" +\n        \"<td>\" + (typeDef ? '<span class=\"ln-badge\" style=\"background:' + typeDef.color + '\">' + esc(typeDef.label) + \"</span>\" : '<span class=\"ln-dim\">–</span>') + \"</td>\" +\n        '<td class=\"ln-dim\">' + esc(link ? link.name : \"(okänd länk)\") + \"</td>\" +\n        \"<td>\" + renderStatus(st) + \"</td>\" +\n        '<td class=\"ln-dim\">' + esc(fmtTime(o.date || o.createdAt)) + \"</td>\" +\n        '<td class=\"ln-dim\">' + esc(syncTxt) + \"</td>\" +\n        \"<td>\" + (url ? '<button type=\"button\" class=\"ln-btn ln-ghost ln-compact\" data-copy=\"' + esc(url) + '\">Kopiera länk</button>' : \"\") + \"</td>\" +\n        \"</tr>\";\n    }).join(\"\");\n    $(\"lnSendTableWrap\").innerHTML =\n      '<table class=\"ln-table\"><thead><tr>' +\n      \"<th>Mottagare</th><th>Typ</th><th>Länk</th><th>Status</th><th>Skickad</th><th>Synk</th><th></th>\" +\n      \"</tr></thead><tbody>\" + rows + \"</tbody></table>\";\n    Array.prototype.forEach.call($(\"lnSendTableWrap\").querySelectorAll(\"[data-copy]\"), function (el) {\n      el.addEventListener(\"click\", function () {\n        var v = el.getAttribute(\"data-copy\");\n        if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(v); }\n        el.textContent = \"Kopierad!\";\n        setTimeout(function () { el.textContent = \"Kopiera länk\"; }, 1500);\n      });\n    });\n  }\n\n  function renderSyncLog() {\n    var el = $(\"lnSyncLog\");\n    if (!state.syncLog.length) { el.style.display = \"none\"; return; }\n    el.style.display = \"block\";\n    el.innerHTML = state.syncLog.map(function (l) { return \"<div>\" + esc(l) + \"</div>\"; }).join(\"\");\n  }\n\n  function renderLinks() {\n    var grid = $(\"lnLinksGrid\");\n    if (!state.links.length) {\n      grid.innerHTML = '<div class=\"ln-card\"><div class=\"ln-empty\">Inga länkar ännu. Gå till \"Skapa länk\" för att komma igång.</div></div>';\n      return;\n    }\n    grid.innerHTML = state.links.map(function (l, i) {\n      var typeDef = TYPES[l.type] || { label: l.type, color: \"#8a969c\" };\n      var tpl = state.templates.filter(function (t) { return t.id === l.templateId; })[0];\n      var n = state.offers.filter(function (o) { return tagValue(o, \"s2form\") === normTag(l.formId); }).length;\n      return '<div class=\"ln-link-card\">' +\n        '<span class=\"ln-badge\" style=\"background:' + typeDef.color + ';align-self:flex-start\">' + esc(typeDef.label) + \"</span>\" +\n        '<div class=\"ln-link-name\">' + esc(l.name) + \"</div>\" +\n        '<div class=\"ln-link-meta\">Mall: ' + esc(tpl ? tplName(tpl) : l.templateId) + \" · \" + n + \" utskick</div>\" +\n        '<div class=\"ln-link-actions\">' +\n        '<button type=\"button\" class=\"ln-btn ln-primary\" data-send=\"' + i + '\">Nytt utskick</button>' +\n        \"</div></div>\";\n    }).join(\"\");\n    Array.prototype.forEach.call(grid.querySelectorAll(\"[data-send]\"), function (el) {\n      el.addEventListener(\"click\", function () {\n        openSendModal(state.links[parseInt(el.getAttribute(\"data-send\"), 10)]);\n      });\n    });\n  }\n\n  function renderAll() {\n    renderBanner();\n    renderUpdated();\n    renderSendouts();\n    renderLinks();\n    if (state.tab === \"create\") { renderWizard(); }\n  }\n\n  /* ================= Utskicksmodal ================= */\n  function openSendModal(link) {\n    state.send = { link: link || null, busy: false, error: null };\n    renderSendModal();\n    $(\"lnSendOverlay\").className = \"ln-overlay ln-show\";\n  }\n  function closeSendModal() {\n    state.send = null;\n    $(\"lnSendOverlay\").className = \"ln-overlay\";\n  }\n  function renderSendModal() {\n    var s = state.send;\n    if (!s) { return; }\n    var linkOptions = state.links.map(function (l, i) {\n      var sel = s.link && s.link.formId === l.formId ? \" selected\" : \"\";\n      return '<option value=\"' + i + '\"' + sel + \">\" + esc(l.name) + \" (\" + esc(TYPES[l.type] ? TYPES[l.type].label : l.type) + \")</option>\";\n    }).join(\"\");\n    var html =\n      '<label class=\"ln-label\">Länk</label>' +\n      '<select class=\"ln-select\" id=\"lnSendLink\">' + (linkOptions || \"<option value=''>Inga länkar</option>\") + \"</select>\" +\n      '<label class=\"ln-label\">Mottagarens namn</label>' +\n      '<input class=\"ln-input\" id=\"lnSendName\" placeholder=\"Anna Andersson\">' +\n      '<label class=\"ln-label\">Mobilnummer</label>' +\n      '<input class=\"ln-input\" id=\"lnSendPhone\" placeholder=\"+46701234567\">' +\n      '<label class=\"ln-label\">SMS-text (valfri, max 200 tecken)</label>' +\n      '<textarea class=\"ln-textarea\" id=\"lnSendMsg\" rows=\"3\" placeholder=\"Hej! Här kommer din länk.\"></textarea>' +\n      '<div class=\"ln-hint\">Länken läggs automatiskt till i slutet av SMS:et.</div>' +\n      '<div id=\"lnSendExtra\"></div>' +\n      '<div class=\"ln-error-txt\" id=\"lnSendErr\">' + esc(s.error || \"\") + \"</div>\";\n    $(\"lnSendBody\").innerHTML = html;\n    $(\"lnSendLink\").addEventListener(\"change\", function () {\n      var idx = parseInt($(\"lnSendLink\").value, 10);\n      state.send.link = state.links[idx] || null;\n      renderSendExtra();\n    });\n    renderSendExtra();\n  }\n  function renderSendExtra() {\n    var s = state.send;\n    var el = $(\"lnSendExtra\");\n    if (!s || !s.link) { el.innerHTML = \"\"; return; }\n    if (s.link.type === \"call\") {\n      el.innerHTML =\n        '<label class=\"ln-label\">Koppla till samtal</label>' +\n        '<div class=\"ln-hint\">Senaste samtalet till mobilnumret kopplas automatiskt, så att betyget hamnar som tagg på rätt samtal.</div>';\n    } else if (s.link.type === \"orderupdate\") {\n      el.innerHTML =\n        '<label class=\"ln-label\">Order-ID att uppdatera</label>' +\n        '<input class=\"ln-input\" id=\"lnSendOrderId\" placeholder=\"t.ex. 69355775c5f1627738bf9bb4\">' +\n        '<div class=\"ln-hint\">Ordern som uppföljningen gäller. Hittas i ordervyn.</div>';\n    } else {\n      el.innerHTML = \"\";\n    }\n  }\n  function doSend() {\n    var s = state.send;\n    if (!s || s.busy) { return; }\n    var link = s.link;\n    var name = ($(\"lnSendName\").value || \"\").trim();\n    var phone = ($(\"lnSendPhone\").value || \"\").trim();\n    var msg = ($(\"lnSendMsg\").value || \"\").trim();\n    var orderIdEl = $(\"lnSendOrderId\");\n    var orderId = orderIdEl ? (orderIdEl.value || \"\").trim() : \"\";\n    if (!link) { s.error = \"Välj en länk.\"; return renderSendModal(); }\n    if (!name || !phone) { s.error = \"Ange namn och mobilnummer.\"; return renderSendModal(); }\n    if (msg.length > 200) { s.error = \"SMS-texten får vara max 200 tecken.\"; return renderSendModal(); }\n    if (link.type === \"orderupdate\" && !orderId) { s.error = \"Ange order-ID.\"; return renderSendModal(); }\n    s.busy = true; s.error = null;\n    $(\"lnSendGo\").disabled = true;\n    $(\"lnSendGo\").textContent = \"Skickar…\";\n\n    var pre = link.type === \"call\"\n      ? api(\"GET\", \"/dial/calls-v1\", { recipients: [phone], count: 1 }).then(function (res) {\n          var c = items(res)[0];\n          return c ? c.id : null;\n        }).catch(function () { return null; })\n      : Promise.resolve(null);\n\n    pre.then(function (callId) {\n      var body = {\n        templateId: link.templateId,\n        isTest: true,\n        autoFill: false,\n        inserts: [],\n        recipient: { name: name, sms: phone },\n        distributeNow: { sms: [{ recipient: phone, message: msg || \"Hej \" + name + \"! Här kommer din länk från oss.\" }] }\n      };\n      if (callId) { body.callId = callId; }\n      if (state.mock) { return mockSend(link, body, orderId); }\n      return api(\"POST\", \"/offers/offers-v2\", null, body).then(function (res) {\n        var offerId = (res && (res.offerId || res.id)) || null;\n        if (!offerId) { throw new Error(\"Inget offert-ID i svaret.\"); }\n        var tags = [\"s2links\", \"s2type\" + link.type, \"s2form\" + normTag(link.formId)];\n        if (orderId) { tags.push(\"s2order\" + normTag(orderId)); }\n        return api(\"PUT\", \"/offers/offers-v1/\" + offerId, null, { addTags: tags });\n      });\n    }).then(function () {\n      closeSendModal();\n      log(\"Utskick till \" + name + \" (\" + phone + \") skapat.\");\n      return refresh();\n    }).catch(function (err) {\n      s.busy = false;\n      s.error = err && err.message ? err.message : String(err);\n      renderSendModal();\n    }).then(function () {\n      var go = $(\"lnSendGo\");\n      if (go) { go.disabled = false; go.textContent = \"Skicka\"; }\n    });\n  }\n\n  /* ================= Synkmotor =================\n     Klara (\"signed\") utskick utan s2synced-tagg synkas till samtal/ordrar\n     med användarens egen token — inget backend-stöd krävs. */\n  function extractAnswer(offer, fieldId) {\n    if (!fieldId) { return null; }\n    var pools = [offer.fields, offer.fieldValues, offer.contactFields, offer.orderFields, offer.webFormFields];\n    for (var i = 0; i < pools.length; i++) {\n      var arr = pools[i];\n      if (!Array.isArray(arr)) { continue; }\n      for (var j = 0; j < arr.length; j++) {\n        if (arr[j] && arr[j].fieldId === fieldId && arr[j].value != null) { return String(arr[j].value); }\n      }\n    }\n    return null;\n  }\n  function fetchOfferDetail(offer) {\n    //  Listan kan sakna fältvärden — hämta detalj vid behov.\n    if (extractAnswer(offer, \"___probe___\") !== null) { return Promise.resolve(offer); }\n    return api(\"GET\", \"/offers/offers-v1/\" + offer.id, null, null)\n      .then(function (full) { return full || offer; })\n      .catch(function () { return offer; });\n  }\n  function syncOne(offer) {\n    var link = linkForOffer(offer);\n    var type = offerType(offer);\n    if (!link || !type || type === \"offer\") { return Promise.resolve(false); }\n\n    var work;\n    if (type === \"call\") {\n      var callId = offer.callId || null;\n      if (!callId) {\n        log(\"Utskick \" + offer.id + \": inget samtal kopplat — hoppar över.\");\n        work = Promise.resolve();\n      } else {\n        work = fetchOfferDetail(offer).then(function (full) {\n          var score = parseInt(extractAnswer(full, link.scoreFieldId), 10);\n          var tagId = (score >= 1 && score <= link.callTagIds.length) ? link.callTagIds[score - 1] : null;\n          if (!tagId) {\n            log(\"Utskick \" + offer.id + \": inget giltigt betyg (\" + score + \").\");\n            return null;\n          }\n          return api(\"PUT\", \"/dial/calls-v1/\" + callId, null, { addTagIds: [tagId] }).then(function () {\n            log(\"Samtal taggat med betyg \" + score + \".\");\n          });\n        });\n      }\n    } else if (type === \"order\") {\n      work = fetchOfferDetail(offer).then(function (full) {\n        var fields = link.fieldIds.map(function (fid) {\n          var v = extractAnswer(full, fid);\n          return v == null ? null : { fieldId: fid, value: v };\n        }).filter(Boolean);\n        var rec = recipientOf(offer);\n        var body = {\n          key: \"s2links-\" + offer.id,\n          offerId: offer.id,\n          tagIds: link.orderTagIds,\n          fields: fields\n        };\n        return api(\"POST\", \"/orders/orders-v2\", null, body).then(function () {\n          log(\"Order skapad för \" + rec.name + \".\");\n        });\n      });\n    } else if (type === \"orderupdate\") {\n      var orderId = tagValue(offer, \"s2order\");\n      if (!orderId) {\n        log(\"Utskick \" + offer.id + \": saknar order-referens.\");\n        work = Promise.resolve();\n      } else {\n        work = fetchOfferDetail(offer).then(function (full) {\n          var parts = link.fieldIds.map(function (fid) { return extractAnswer(full, fid); }).filter(Boolean);\n          var steps = [];\n          if (link.orderTagIds.length) {\n            steps.push(api(\"PUT\", \"/orders/orders-v1/\" + orderId, null, { addTagIds: link.orderTagIds })\n              .catch(function (err) { log(\"Kunde inte tagga order: \" + err.message); }));\n          }\n          var text = (link.comment ? link.comment + \" \" : \"Uppföljning via länk. \") + (parts.length ? \"Svar: \" + parts.join(\", \") : \"\");\n          steps.push(api(\"POST\", \"/orders/orders-v1/\" + orderId + \"/comments\", null, { text: text.trim() })\n            .catch(function (err) { log(\"Kunde inte kommentera order: \" + err.message); }));\n          return Promise.all(steps).then(function () { log(\"Order \" + orderId + \" uppdaterad.\"); });\n        });\n      }\n    } else {\n      work = Promise.resolve();\n    }\n\n    return work.then(function () {\n      return api(\"PUT\", \"/offers/offers-v1/\" + offer.id, null, { addTags: [\"s2synced\"] });\n    }).then(function () { return true; });\n  }\n  function runSync() {\n    if (state.mock || state.syncBusy) { return Promise.resolve(); }\n    var due = state.offers.filter(function (o) {\n      return (o.status || \"\") === \"signed\" && !hasTag(o, \"s2synced\") && offerType(o) && offerType(o) !== \"offer\";\n    });\n    if (!due.length) { return Promise.resolve(); }\n    state.syncBusy = true;\n    var chain = Promise.resolve();\n    due.forEach(function (o) {\n      chain = chain.then(function () {\n        return syncOne(o).catch(function (err) { log(\"Synkfel \" + o.id + \": \" + err.message); });\n      });\n    });\n    return chain.then(function () {\n      state.syncBusy = false;\n      return loadOffers().then(renderAll);\n    });\n  }\n\n  /* ================= Wizard: skapa länk ================= */\n  function wizardReset() {\n    state.wizard = {\n      step: 1,\n      type: null,\n      name: \"\",\n      templateId: \"\",\n      scale: 5,\n      tagPrefix: \"NKI\",\n      orderTagNames: \"\",\n      commentText: \"\",\n      wantScore: true,\n      wantComment: true,\n      busy: false,\n      error: null,\n      result: null\n    };\n  }\n  function renderWizard() {\n    var w = state.wizard;\n    if (!w) { wizardReset(); w = state.wizard; }\n    var total = 3;\n    $(\"lnStepInd\").innerHTML = [1, 2, 3].map(function (i) {\n      return '<div class=\"ln-step-ind' + (i <= w.step ? \" ln-on\" : \"\") + '\"></div>';\n    }).join(\"\");\n    var body = $(\"lnWizardBody\");\n    var foot = $(\"lnWizardFoot\");\n    foot.style.display = w.result ? \"none\" : \"flex\";\n    $(\"lnWizBack\").style.visibility = w.step > 1 ? \"visible\" : \"hidden\";\n    $(\"lnWizNext\").textContent = w.step === total ? (w.busy ? \"Skapar…\" : \"Skapa länk\") : \"Nästa\";\n    $(\"lnWizNext\").disabled = !!w.busy;\n\n    if (w.result) {\n      body.innerHTML =\n        '<div class=\"ln-result\">' +\n        '<div class=\"ln-result-icon\">&#10003;</div>' +\n        \"<h2>Länken är skapad!</h2>\" +\n        '<p style=\"color:var(--ln-dim);margin:8px 0 16px\">Gör ditt första utskick från fliken Utskick eller Mina länkar.</p>' +\n        '<button type=\"button\" class=\"ln-btn ln-primary\" id=\"lnWizDone\">Till mina länkar</button>' +\n        \"</div>\";\n      $(\"lnWizDone\").addEventListener(\"click\", function () {\n        wizardReset();\n        setTab(\"links\");\n      });\n      return;\n    }\n\n    if (w.step === 1) {\n      body.innerHTML = '<div class=\"ln-typegrid\">' + Object.keys(TYPES).map(function (k) {\n        var t = TYPES[k];\n        return '<button type=\"button\" class=\"ln-typecard' + (w.type === k ? \" ln-on\" : \"\") + '\" data-type=\"' + k + '\">' +\n          \"<h3>\" + esc(t.label) + \"</h3><p>\" + esc(t.desc) + \"</p></button>\";\n      }).join(\"\") + \"</div>\";\n      Array.prototype.forEach.call(body.querySelectorAll(\"[data-type]\"), function (el) {\n        el.addEventListener(\"click\", function () {\n          w.type = el.getAttribute(\"data-type\");\n          renderWizard();\n        });\n      });\n    } else if (w.step === 2) {\n      var tplOpts = state.templates.map(function (t) {\n        return '<option value=\"' + esc(t.id) + '\"' + (w.templateId === t.id ? \" selected\" : \"\") + \">\" + esc(tplName(t)) + \"</option>\";\n      }).join(\"\");\n      var typeHtml = \"\";\n      if (w.type === \"call\") {\n        typeHtml =\n          '<label class=\"ln-label\">Betygsskala</label>' +\n          '<select class=\"ln-select\" id=\"lnWScale\">' +\n          '<option value=\"5\"' + (w.scale === 5 ? \" selected\" : \"\") + \">1&#8211;5</option>\" +\n          '<option value=\"10\"' + (w.scale === 10 ? \" selected\" : \"\") + \">1&#8211;10</option>\" +\n          \"</select>\" +\n          '<label class=\"ln-label\">Taggprefix på samtalet</label>' +\n          '<input class=\"ln-input\" id=\"lnWPrefix\" value=\"' + esc(w.tagPrefix) + '\">' +\n          '<div class=\"ln-hint\">Taggar skapas automatiskt, t.ex. \"' + esc(w.tagPrefix) + ' 1\" … \"' + esc(w.tagPrefix) + \" \" + w.scale + '\".</div>';\n      } else if (w.type === \"order\" || w.type === \"orderupdate\") {\n        typeHtml =\n          '<label class=\"ln-label\">' + (w.type === \"order\" ? \"Taggar på den nya ordern\" : \"Taggar som läggs på ordern\") + \"</label>\" +\n          '<input class=\"ln-input\" id=\"lnWOrderTags\" value=\"' + esc(w.orderTagNames) + '\" placeholder=\"t.ex. Möte bokat, Länk\">' +\n          '<div class=\"ln-hint\">Kommaseparerade. Taggar som inte finns skapas automatiskt.</div>' +\n          (w.type === \"orderupdate\"\n            ? '<label class=\"ln-label\">Kommentarsprefix</label><input class=\"ln-input\" id=\"lnWComment\" value=\"' + esc(w.commentText) + '\" placeholder=\"Uppföljning efter möte.\">'\n            : \"\");\n      } else {\n        typeHtml = '<div class=\"ln-hint\" style=\"margin-top:12px\">Avtalslänken använder mallens vanliga e-signeringsflöde utan omväg.</div>';\n      }\n      var extraFields = \"\";\n      if (w.type !== \"offer\") {\n        extraFields =\n          '<label class=\"ln-label\">Frågor i formuläret</label>' +\n          '<label style=\"display:block;font-size:13px;margin:4px 0\"><input type=\"checkbox\" id=\"lnWScore\"' + (w.wantScore ? \" checked\" : \"\") + \"> Betyg (\" + (w.type === \"call\" ? \"sätter tagg på samtalet\" : \"sparas på ordern\") + \")</label>\" +\n          '<label style=\"display:block;font-size:13px;margin:4px 0\"><input type=\"checkbox\" id=\"lnWCmt\"' + (w.wantComment ? \" checked\" : \"\") + \"> Fritextkommentar</label>\";\n      }\n      body.innerHTML = '<div class=\"ln-wizard-body\">' +\n        '<label class=\"ln-label\">Namn på länken</label>' +\n        '<input class=\"ln-input\" id=\"lnWName\" value=\"' + esc(w.name) + '\" placeholder=\"t.ex. Kundnöjdhet efter samtal\">' +\n        '<label class=\"ln-label\">Offertmall (krävs av webbformulär)</label>' +\n        '<select class=\"ln-select\" id=\"lnWTpl\"><option value=\"\">Välj mall…</option>' + tplOpts + \"</select>\" +\n        '<div class=\"ln-hint\">Formulärlänkar kräver alltid en offertmall i bakgrunden, även när inget avtal ska signeras.</div>' +\n        typeHtml + extraFields +\n        (w.error ? '<div class=\"ln-error-txt\">' + esc(w.error) + \"</div>\" : \"\") +\n        \"</div>\";\n    } else if (w.step === 3) {\n      var typeDef = TYPES[w.type] || {};\n      var tpl = state.templates.filter(function (t) { return t.id === w.templateId; })[0];\n      var lines = [\n        \"<b>Typ:</b> \" + esc(typeDef.label || w.type),\n        \"<b>Namn:</b> \" + esc(w.name),\n        \"<b>Offertmall:</b> \" + esc(tpl ? tplName(tpl) : w.templateId)\n      ];\n      if (w.type === \"call\") { lines.push(\"<b>Betygsskala:</b> 1–\" + w.scale + ' (taggar \"' + esc(w.tagPrefix) + ' 1\"–\"' + esc(w.tagPrefix) + \" \" + w.scale + '\" skapas)'); }\n      if (w.type === \"order\" || w.type === \"orderupdate\") { lines.push(\"<b>Ordertaggar:</b> \" + esc(w.orderTagNames || \"(inga)\")); }\n      if (w.type !== \"offer\") {\n        lines.push(\"<b>Frågor:</b> \" + [w.wantScore ? \"betyg\" : null, w.wantComment ? \"kommentar\" : null].filter(Boolean).join(\", \"));\n        lines.push('<span class=\"ln-hint\">Kunden stannar kvar i formuläret — inget avtal visas. Utskicken döljs från vanliga offertvyn.</span>');\n      }\n      body.innerHTML = '<div class=\"ln-wizard-body\"><div class=\"ln-summary\">' + lines.join(\"<br>\") + \"</div>\" +\n        (w.error ? '<div class=\"ln-error-txt\">' + esc(w.error) + \"</div>\" : \"\") +\n        \"</div>\";\n    }\n  }\n  function wizardCollectStep2() {\n    var w = state.wizard;\n    w.name = ($(\"lnWName\").value || \"\").trim();\n    w.templateId = $(\"lnWTpl\").value || \"\";\n    if (w.type === \"call\") {\n      w.scale = parseInt($(\"lnWScale\").value, 10) || 5;\n      w.tagPrefix = ($(\"lnWPrefix\").value || \"NKI\").trim();\n    }\n    if (w.type === \"order\" || w.type === \"orderupdate\") {\n      w.orderTagNames = ($(\"lnWOrderTags\").value || \"\").trim();\n      var c = $(\"lnWComment\");\n      if (c) { w.commentText = (c.value || \"\").trim(); }\n    }\n    if (w.type !== \"offer\") {\n      w.wantScore = $(\"lnWScore\") ? $(\"lnWScore\").checked : false;\n      w.wantComment = $(\"lnWCmt\") ? $(\"lnWCmt\").checked : false;\n    }\n    if (!w.name) { return \"Ange ett namn på länken.\"; }\n    if (!w.templateId) { return \"Välj en offertmall.\"; }\n    if (w.type === \"call\" && !w.tagPrefix) { return \"Ange ett taggprefix.\"; }\n    return null;\n  }\n  function wizardNext() {\n    var w = state.wizard;\n    if (w.busy) { return; }\n    w.error = null;\n    if (w.step === 1) {\n      if (!w.type) { return; }\n      w.step = 2;\n    } else if (w.step === 2) {\n      var err = wizardCollectStep2();\n      if (err) { w.error = err; return renderWizard(); }\n      w.step = 3;\n    } else if (w.step === 3) {\n      return wizardCreate();\n    }\n    renderWizard();\n  }\n  function wizardBack() {\n    var w = state.wizard;\n    if (w.busy || w.step <= 1) { return; }\n    w.error = null;\n    w.step -= 1;\n    renderWizard();\n  }\n\n  /* ---- Provisionering: taggar, fält, webbformulär ---- */\n  function ensureNamed(listPath, createPath, names) {\n    //  Skapar poster med givna namn om de saknas; returnerar id:n i samma ordning.\n    if (!names.length) { return Promise.resolve([]); }\n    return api(\"GET\", listPath, { count: 100 }).then(function (res) {\n      var existing = items(res);\n      var byNorm = {};\n      existing.forEach(function (x) { byNorm[normTag(x.name || x.label || \"\")] = x.id; });\n      var chain = Promise.resolve();\n      var ids = new Array(names.length);\n      names.forEach(function (nm, i) {\n        chain = chain.then(function () {\n          var found = byNorm[normTag(nm)];\n          if (found) { ids[i] = found; return null; }\n          return api(\"POST\", createPath, null, { name: nm, label: nm, type: \"plain\" }).then(function (created) {\n            ids[i] = created && created.id;\n            byNorm[normTag(nm)] = ids[i];\n          });\n        });\n      });\n      return chain.then(function () { return ids; });\n    });\n  }\n\n  //  Skript som injiceras i det kompletterande formuläret (custom-sektion).\n  //  Avleder native-flödet: submit utan redirect + tack-läge vid återbesök.\n  function buildFormScript() {\n    var thanksStyle = \"display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:#fff;z-index:99999;padding:60px 24px;text-align:center;font-family:sans-serif;\";\n    var open = \"<scr\" + \"ipt>\";\n    var close = \"</scr\" + \"ipt>\";\n    var js = [\n      \"(function () {\",\n      \"  function showDone() {\",\n      \"    var t = document.getElementById('s2l-thanks');\",\n      \"    if (t) { t.style.display = 'block'; }\",\n      \"  }\",\n      \"  function checkExisting(offerId) {\",\n      \"    if (!offerId) { return; }\",\n      \"    fetch('\" + PUBLIC_STATUS + \"?offerIds=' + encodeURIComponent(offerId))\",\n      \"      .then(function (r) { return r.json(); })\",\n      \"      .then(function (list) {\",\n      \"        var it = (list && list[0]) || (list && list.items && list.items[0]) || null;\",\n      \"        var st = it && (it.status || it.state);\",\n      \"        if (st === 'signed') { showDone(); }\",\n      \"      }).catch(function () {});\",\n      \"  }\",\n      \"  var tries = 0;\",\n      \"  function boot() {\",\n      \"    var f = window.form;\",\n      \"    if (!f || !f.internal) {\",\n      \"      tries += 1;\",\n      \"      if (tries < 100) { setTimeout(boot, 120); }\",\n      \"      return;\",\n      \"    }\",\n      \"    checkExisting(f.internal.offerId);\",\n      \"    if (f.setSubmitHook) {\",\n      \"      f.setSubmitHook(function () {\",\n      \"        return f.internal.submit({ redirect: false }).then(function () {\",\n      \"          showDone();\",\n      \"          return { preventDefault: true };\",\n      \"        }).catch(function () {\",\n      \"          return { preventDefault: true };\",\n      \"        });\",\n      \"      });\",\n      \"    }\",\n      \"  }\",\n      \"  boot();\",\n      \"})();\"\n    ].join(\"\\n\");\n    return '<div id=\"s2l-thanks\" style=\"' + thanksStyle + '\">' +\n      '<div style=\"font-size:44px;color:#43A047;\">&#10003;</div>' +\n      \"<h2 style='margin:12px 0 8px;font-weight:500;'>Tack f&#246;r ditt svar!</h2>\" +\n      \"<p style='color:#757575;'>Ditt svar &#228;r registrerat. Du kan st&#228;nga den h&#228;r sidan.</p>\" +\n      \"</div>\" + open + js + close;\n  }\n\n  function wizardCreate() {\n    var w = state.wizard;\n    w.busy = true; w.error = null;\n    renderWizard();\n\n    if (state.mock) {\n      setTimeout(function () {\n        w.busy = false;\n        w.result = { formId: \"mockform\" + Date.now() };\n        state.links.push({\n          formId: w.result.formId, name: w.name, type: w.type, templateId: w.templateId,\n          scoreFieldId: \"mockscore\", commentFieldId: \"mockcmt\",\n          callTagIds: [], orderTagIds: [], fieldIds: [], comment: w.commentText, scale: w.scale\n        });\n        renderWizard();\n        renderLinks();\n      }, 600);\n      return;\n    }\n\n    var jobs = {};\n    var chain = Promise.resolve();\n\n    //  1. Samtalstaggar för betygsskalan (samtalslänk).\n    if (w.type === \"call\") {\n      var tagNames = [];\n      for (var i = 1; i <= w.scale; i++) { tagNames.push(w.tagPrefix + \" \" + i); }\n      chain = chain.then(function () {\n        return ensureNamed(\"/dial/tags-v1\", \"/dial/tags-v1\", tagNames).then(function (ids) { jobs.callTagIds = ids; });\n      });\n    }\n    //  2. Ordertaggar (order/uppföljning).\n    if ((w.type === \"order\" || w.type === \"orderupdate\") && w.orderTagNames) {\n      var otNames = w.orderTagNames.split(\",\").map(function (s) { return s.trim(); }).filter(Boolean);\n      chain = chain.then(function () {\n        return ensureNamed(\"/orders/tags-v1\", \"/orders/tags-v1\", otNames).then(function (ids) { jobs.orderTagIds = ids; });\n      });\n    }\n    //  3. Orderfält för betyg/kommentar (formulärets frågor).\n    if (w.type !== \"offer\") {\n      var fieldNames = [];\n      if (w.wantScore) { fieldNames.push(\"Länkbetyg\"); }\n      if (w.wantComment) { fieldNames.push(\"Länkkommentar\"); }\n      if (fieldNames.length) {\n        chain = chain.then(function () {\n          return ensureNamed(\"/orders/fields-v1\", \"/orders/fields-v1\", fieldNames).then(function (ids) {\n            var k = 0;\n            if (w.wantScore) { jobs.scoreFieldId = ids[k]; k += 1; }\n            if (w.wantComment) { jobs.commentFieldId = ids[k]; }\n          });\n        });\n      }\n    }\n\n    //  4. Webbformuläret.\n    chain.then(function () {\n      var fieldIds = [];\n      var sections = [{ type: \"text\", text: { content: w.name, type: \"header\" } }];\n      if (w.type !== \"offer\") {\n        var fields = [];\n        if (jobs.scoreFieldId) {\n          fields.push({ fieldId: jobs.scoreFieldId, text: \"Betyg 1–\" + (w.type === \"call\" ? w.scale : 5), required: true });\n          fieldIds.push(jobs.scoreFieldId);\n        }\n        if (jobs.commentFieldId) {\n          fields.push({ fieldId: jobs.commentFieldId, text: \"Kommentar\", required: false });\n          fieldIds.push(jobs.commentFieldId);\n        }\n        if (fields.length) { sections.push({ type: \"fieldList\", fieldList: { fields: fields } }); }\n        sections.push({ type: \"custom\", custom: { content: buildFormScript() } });\n      }\n\n      var cfg = {\n        s2links: {\n          v: 1,\n          type: w.type,\n          templateId: w.templateId,\n          scoreFieldId: jobs.scoreFieldId || null,\n          commentFieldId: jobs.commentFieldId || null,\n          callTagIds: jobs.callTagIds || [],\n          orderTagIds: jobs.orderTagIds || [],\n          fieldIds: fieldIds,\n          comment: w.commentText || \"\",\n          scale: w.scale\n        }\n      };\n      var body = {\n        name: w.name,\n        templateIds: [w.templateId],\n        supplementaryOfferTemplateIds: [w.templateId],\n        supplementaryMessageTemplateIds: [],\n        steps: [{ sections: sections }],\n        isSmsEnabled: false,\n        isEmailEnabled: false,\n        developerMessage: \"S2LINKS:\" + JSON.stringify(cfg)\n      };\n      if (w.type !== \"offer\") {\n        //  Tyst one-click-signering: flippar status till \"signed\" utan avtalsvy.\n        body.automaticSign = { isEnabled: true, isPreviewEnabled: false };\n      }\n      return api(\"POST\", \"/offers/web-forms-v1\", null, body);\n    }).then(function (created) {\n      w.busy = false;\n      w.result = { formId: created && created.id };\n      return loadForms().then(function () {\n        renderWizard();\n        renderLinks();\n      });\n    }).catch(function (err) {\n      w.busy = false;\n      w.error = err && err.message ? err.message : String(err);\n      renderWizard();\n    });\n  }\n\n  /* ================= Mockdata (demoläge) ================= */\n  function loadMock() {\n    state.mock = true;\n    state.templates = [\n      { id: \"tpl1\", name: \"Standardavtal\" },\n      { id: \"tpl2\", name: \"Mötesbokning (tom mall)\" }\n    ];\n    var mkForm = function (id, name, type) {\n      return {\n        id: id, name: name,\n        developerMessage: \"S2LINKS:\" + JSON.stringify({\n          s2links: { v: 1, type: type, templateId: type === \"offer\" ? \"tpl1\" : \"tpl2\", scoreFieldId: \"f1\", commentFieldId: \"f2\", callTagIds: [\"ct1\", \"ct2\", \"ct3\", \"ct4\", \"ct5\"], orderTagIds: [\"ot1\"], fieldIds: [\"f1\", \"f2\"], comment: \"\", scale: 5 }\n        })\n      };\n    };\n    state.forms = [\n      mkForm(\"form1\", \"Kundnöjdhet efter samtal\", \"call\"),\n      mkForm(\"form2\", \"Boka uppföljningsmöte\", \"order\"),\n      mkForm(\"form3\", \"Hur gick mötet?\", \"orderupdate\"),\n      mkForm(\"form4\", \"Signera avtal\", \"offer\")\n    ];\n    buildLinks();\n    var now = Date.now();\n    var mk = function (i, formId, type, status, extra) {\n      var names = [\"Anna Andersson\", \"Bertil Berg\", \"Cecilia Carlsson\", \"David Dahl\", \"Emma Ek\", \"Filip Falk\"];\n      var o = {\n        id: \"off\" + i,\n        status: status,\n        date: new Date(now - i * 3600000).toISOString(),\n        recipient: { name: names[i % names.length], sms: \"+4670123456\" + i },\n        tags: [\"s2links\", \"s2type\" + type, \"s2form\" + formId],\n        publicId: \"pub\" + i,\n        fields: [{ fieldId: \"f1\", value: String((i % 5) + 1) }, { fieldId: \"f2\", value: \"Mycket bra bemötande!\" }]\n      };\n      (extra || []).forEach(function (t) { o.tags.push(t); });\n      return o;\n    };\n    state.offers = [\n      mk(0, \"form1\", \"call\", \"signed\", [\"s2synced\"]),\n      mk(1, \"form1\", \"call\", \"read\"),\n      mk(2, \"form2\", \"order\", \"signed\", [\"s2synced\"]),\n      mk(3, \"form2\", \"order\", \"distributed\"),\n      mk(4, \"form3\", \"orderupdate\", \"signed\", [\"s2order69355775\", \"s2synced\"]),\n      mk(5, \"form4\", \"offer\", \"expired\")\n    ];\n    return Promise.resolve();\n  }\n  function mockSend(link, body, orderId) {\n    var o = {\n      id: \"off\" + Date.now(),\n      status: \"distributed\",\n      date: new Date().toISOString(),\n      recipient: body.recipient,\n      tags: [\"s2links\", \"s2type\" + link.type, \"s2form\" + normTag(link.formId)],\n      publicId: \"pubx\" + Date.now(),\n      fields: []\n    };\n    if (orderId) { o.tags.push(\"s2order\" + normTag(orderId)); }\n    state.offers.unshift(o);\n    return Promise.resolve();\n  }\n\n  /* ================= Init & boot ================= */\n  function refresh() {\n    if (state.mock) { renderAll(); return Promise.resolve(); }\n    return loadAll().then(function () {\n      state.error = null;\n      renderAll();\n      return runSync();\n    }).catch(function (err) {\n      state.error = err && err.message ? err.message : String(err);\n      renderBanner();\n    });\n  }\n\n  function init() {\n    $(\"lnTabSend\").addEventListener(\"click\", function () { setTab(\"send\"); });\n    $(\"lnTabLinks\").addEventListener(\"click\", function () { setTab(\"links\"); });\n    $(\"lnTabCreate\").addEventListener(\"click\", function () { setTab(\"create\"); });\n    $(\"lnRefresh\").addEventListener(\"click\", function () { refresh(); });\n    $(\"lnNewSendout\").addEventListener(\"click\", function () {\n      if (!state.links.length) { setTab(\"create\"); return; }\n      openSendModal(state.links[0]);\n    });\n    $(\"lnSendClose\").addEventListener(\"click\", closeSendModal);\n    $(\"lnSendCancel\").addEventListener(\"click\", closeSendModal);\n    $(\"lnSendGo\").addEventListener(\"click\", doSend);\n    $(\"lnSendOverlay\").addEventListener(\"click\", function (e) {\n      if (e.target === $(\"lnSendOverlay\")) { closeSendModal(); }\n    });\n    $(\"lnWizNext\").addEventListener(\"click\", wizardNext);\n    $(\"lnWizBack\").addEventListener(\"click\", wizardBack);\n\n    var useMock = /[?&]mock=1/.test(location.search) || !getToken();\n    if (useMock) {\n      loadMock().then(renderAll);\n      return;\n    }\n    refresh();\n    timers.poll = setInterval(function () {\n      if (document.hidden) { return; }\n      loadOffers().then(function () {\n        renderAll();\n        return runSync();\n      }).catch(function () {});\n    }, 20000);\n  }\n\n  //  Vyer monteras av SPA:n — skriptet kan köras innan HTML:en finns i DOM. Vänta in elementen.\n  var bootTries = 0;\n  function boot() {\n    if (document.getElementById(\"lnTabSend\")) { init(); return; }\n    bootTries += 1;\n    if (bootTries < 200) { setTimeout(boot, 50); }\n  }\n  boot();\n})();\n</script>","iconName":"link","iconColor":"#2eb4c0","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50b4db736573cf6586efd0","name":"Hemskärm","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":0,"label":"Hem"}],"html":"<div id=\"hsv-root\">\n  <style>\n    #hsv-root {\n      --hsv-bg: var(--color-background-2, #fafafa);\n      --hsv-card: var(--color-background-1, #ffffff);\n      --hsv-secondary: var(--color-background-3, #eeeeee);\n      --hsv-fg: var(--color-text-1, #212121);\n      --hsv-fg2: var(--color-text-2, #616161);\n      --hsv-muted-fg: var(--color-text-3, #757575);\n      --hsv-primary: var(--color-primary, #1565C0);\n      --hsv-primary-fg: #ffffff;\n      --hsv-danger: var(--color-danger, #E53935);\n      --hsv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --hsv-border: color-mix(in srgb, var(--hsv-fg) 13%, transparent);\n      --hsv-border-soft: color-mix(in srgb, var(--hsv-fg) 8%, transparent);\n      --hsv-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--hsv-fg);\n      display: block;\n      background: var(--hsv-bg);\n      border: 1px solid var(--hsv-border-soft);\n      border-radius: 12px;\n      padding: 22px;\n      /* Size strictly to content: the app wrapper can stretch the view\n         (flex stretch / set heights), which leaves a huge empty card. */\n      height: fit-content !important;\n      min-height: 0 !important;\n      max-height: none !important;\n      align-self: flex-start;\n      flex: 0 0 auto;\n    }\n    #hsv-root * { box-sizing: border-box; }\n    #hsv-root button { font-family: inherit; cursor: pointer; }\n\n    .hsv-header { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }\n    .hsv-greeting { font-size: 22px; font-weight: 800; }\n    .hsv-date { font-size: 13px; color: var(--hsv-muted-fg); text-transform: capitalize; }\n\n    .hsv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n    @media (max-width: 900px) { .hsv-grid { grid-template-columns: 1fr; } }\n\n    .hsv-card {\n      background: var(--hsv-card); border: 1px solid var(--hsv-border-soft);\n      border-radius: var(--hsv-radius); padding: 16px 18px;\n    }\n    .hsv-card-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }\n    .hsv-card-title .fas, .hsv-card-title .far { color: var(--hsv-primary); }\n    .hsv-messages-card { grid-column: 1 / -1; }\n\n    .hsv-msg {\n      display: flex; gap: 12px; align-items: flex-start;\n      padding: 12px 14px; border-radius: var(--hsv-radius);\n      border: 1px solid var(--hsv-border-soft);\n      background: color-mix(in srgb, var(--hsv-primary) 5%, var(--hsv-card));\n      margin-bottom: 8px;\n    }\n    .hsv-msg-icon {\n      width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;\n      background: color-mix(in srgb, var(--hsv-primary) 14%, transparent);\n      color: var(--hsv-primary);\n      display: flex; align-items: center; justify-content: center; font-size: 14px;\n    }\n    .hsv-msg-body { flex: 1; min-width: 0; }\n    .hsv-msg-text { font-size: 14px; white-space: pre-wrap; word-break: break-word; }\n    .hsv-msg-meta { font-size: 11.5px; color: var(--hsv-muted-fg); margin-top: 4px; }\n    .hsv-msg-dismiss {\n      background: transparent; border: none; color: var(--hsv-muted-fg);\n      width: 28px; height: 28px; border-radius: 6px; flex-shrink: 0;\n    }\n    .hsv-msg-dismiss:hover { background: var(--hsv-hover); color: var(--hsv-danger); }\n    .hsv-empty { font-size: 13px; color: var(--hsv-muted-fg); padding: 8px 2px; }\n\n    .hsv-kpis { display: flex; gap: 14px; margin-bottom: 14px; flex-wrap: wrap; }\n    .hsv-kpi {\n      flex: 1; min-width: 120px;\n      border: 1px solid var(--hsv-border-soft); border-radius: var(--hsv-radius);\n      padding: 10px 14px; background: var(--hsv-bg);\n    }\n    .hsv-kpi-num { font-size: 24px; font-weight: 800; line-height: 1.2; }\n    .hsv-kpi-label { font-size: 11.5px; color: var(--hsv-muted-fg); }\n\n    .hsv-chart { display: flex; align-items: flex-end; gap: 8px; height: 150px; padding-top: 18px; }\n    .hsv-bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; min-width: 0; }\n    .hsv-bar-value { font-size: 11px; font-weight: 700; color: var(--hsv-fg2); margin-bottom: 3px; }\n    .hsv-bar {\n      width: 70%; max-width: 44px; border-radius: 6px 6px 2px 2px;\n      background: linear-gradient(180deg, color-mix(in srgb, var(--hsv-primary) 85%, #ffffff), var(--hsv-primary));\n      min-height: 2px;\n      transition: height 600ms cubic-bezier(0.22, 1, 0.36, 1);\n    }\n    .hsv-bar.hsv-bar-alt {\n      background: linear-gradient(180deg, color-mix(in srgb, #2E7D32 70%, #ffffff), #2E7D32);\n    }\n    .hsv-bar-day { font-size: 11px; color: var(--hsv-muted-fg); margin-top: 6px; text-transform: capitalize; white-space: nowrap; }\n    .hsv-bar-col.hsv-today .hsv-bar-day { font-weight: 800; color: var(--hsv-fg); }\n\n    .hsv-loading { display: flex; align-items: center; gap: 10px; color: var(--hsv-muted-fg); font-size: 13px; padding: 30px 0; justify-content: center; }\n    .hsv-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--hsv-border); border-top-color: var(--hsv-primary);\n      animation: hsv-spin 0.8s linear infinite;\n    }\n    @keyframes hsv-spin { to { transform: rotate(360deg); } }\n    .hsv-error { color: var(--hsv-danger); font-size: 13px; padding: 10px 0; }\n  </style>\n\n  <div class=\"hsv-header\">\n    <div class=\"hsv-greeting\" id=\"hsv-greeting\">Hej!</div>\n    <div class=\"hsv-date\" id=\"hsv-date\"></div>\n  </div>\n\n  <div class=\"hsv-grid\">\n    <div class=\"hsv-card hsv-messages-card\">\n      <div class=\"hsv-card-title\"><i class=\"fas fa-bullhorn\"></i> Meddelanden från din teamleader</div>\n      <div id=\"hsv-messages\"><div class=\"hsv-loading\"><div class=\"hsv-spinner\"></div> Laddar…</div></div>\n    </div>\n\n    <div class=\"hsv-card\">\n      <div class=\"hsv-card-title\"><i class=\"fas fa-phone-alt\"></i> Samtal per dag <span style=\"font-weight:400;color:var(--hsv-muted-fg);\">(7 dagar)</span></div>\n      <div class=\"hsv-kpis\">\n        <div class=\"hsv-kpi\"><div class=\"hsv-kpi-num\" id=\"hsv-calls-total\">–</div><div class=\"hsv-kpi-label\">Samtal senaste 7 dagarna</div></div>\n        <div class=\"hsv-kpi\"><div class=\"hsv-kpi-num\" id=\"hsv-calls-today\">–</div><div class=\"hsv-kpi-label\">Samtal idag</div></div>\n      </div>\n      <div id=\"hsv-calls-chart\"><div class=\"hsv-loading\"><div class=\"hsv-spinner\"></div> Laddar…</div></div>\n    </div>\n\n    <div class=\"hsv-card\">\n      <div class=\"hsv-card-title\"><i class=\"fas fa-file-signature\"></i> Signerade avtal per dag <span style=\"font-weight:400;color:var(--hsv-muted-fg);\">(7 dagar)</span></div>\n      <div class=\"hsv-kpis\">\n        <div class=\"hsv-kpi\"><div class=\"hsv-kpi-num\" id=\"hsv-signed-total\">–</div><div class=\"hsv-kpi-label\">Signerade senaste 7 dagarna</div></div>\n        <div class=\"hsv-kpi\"><div class=\"hsv-kpi-num\" id=\"hsv-signed-today\">–</div><div class=\"hsv-kpi-label\">Signerade idag</div></div>\n      </div>\n      <div id=\"hsv-signed-chart\"><div class=\"hsv-loading\"><div class=\"hsv-spinner\"></div> Laddar…</div></div>\n    </div>\n  </div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var POLL_MS = 60000;\n\n      if (window.__hsvCleanup) { try { window.__hsvCleanup(); } catch (e) {} }\n\n      var state = {\n        userId: null,\n        userName: '',\n        messages: null,\n        calls: null,\n        signed: null,\n        error: null\n      };\n\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(path, params) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += (url.indexOf('?') === -1 ? '?' : '&') + qs;\n        }\n        return fetch(url, { headers: { 'Authorization': 'Bearer ' + token() } })\n          .then(function (r) {\n            if (!r.ok) throw new Error('HTTP ' + r.status);\n            return r.json();\n          });\n      }\n\n      function apiPut(path, body) {\n        return fetch(API + path, {\n          method: 'PUT',\n          headers: { 'Authorization': 'Bearer ' + token(), 'Content-Type': 'application/json' },\n          body: JSON.stringify(body)\n        }).then(function (r) {\n          if (!r.ok) throw new Error('HTTP ' + r.status);\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n\n      /* ---------- date helpers (local days) ---------- */\n      function startOfDay(d) { var x = new Date(d); x.setHours(0, 0, 0, 0); return x; }\n      function dayKey(d) {\n        var x = new Date(d);\n        return x.getFullYear() + '-' + (x.getMonth() + 1) + '-' + x.getDate();\n      }\n      function last7Days() {\n        var days = [];\n        var today = startOfDay(new Date());\n        for (var i = 6; i >= 0; i--) {\n          days.push(new Date(today.getTime() - i * 86400000));\n        }\n        return days;\n      }\n      var DAY_NAMES = ['sön', 'mån', 'tis', 'ons', 'tor', 'fre', 'lör'];\n\n      /* ---------- rendering ---------- */\n      function el(id) { return document.getElementById(id); }\n\n      function renderHeader() {\n        var g = el('hsv-greeting');\n        if (g) {\n          var hour = new Date().getHours();\n          var word = hour < 10 ? 'God morgon' : hour < 18 ? 'Hej' : 'God kväll';\n          g.textContent = word + (state.userName ? ', ' + state.userName.split(' ')[0] : '') + '!';\n        }\n        var d = el('hsv-date');\n        if (d) {\n          try {\n            d.textContent = new Date().toLocaleDateString('sv-SE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n          } catch (e) { d.textContent = new Date().toLocaleDateString(); }\n        }\n      }\n\n      function renderMessages() {\n        var container = el('hsv-messages');\n        if (!container) return;\n        if (state.error === 'messages') {\n          container.innerHTML = '<div class=\"hsv-error\">Kunde inte ladda meddelanden.</div>';\n          return;\n        }\n        if (!state.messages) return;\n        if (!state.messages.length) {\n          container.innerHTML = '<div class=\"hsv-empty\"><i class=\"far fa-check-circle\"></i> Inga meddelanden just nu. Kör hårt!</div>';\n          return;\n        }\n        var html = '';\n        var sorted = state.messages.slice().sort(function (a, b) {\n          return new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime();\n        });\n        sorted.forEach(function (m) {\n          var when = '';\n          try {\n            when = new Date(m.date).toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });\n          } catch (e) {}\n          html += '<div class=\"hsv-msg\">' +\n            '<div class=\"hsv-msg-icon\"><i class=\"fas fa-bullhorn\"></i></div>' +\n            '<div class=\"hsv-msg-body\">' +\n            '<div class=\"hsv-msg-text\">' + esc(m.text) + '</div>' +\n            '<div class=\"hsv-msg-meta\">' + esc(m.fromName || 'Teamleader') + (when ? ' · ' + esc(when) : '') + '</div>' +\n            '</div>' +\n            '<button class=\"hsv-msg-dismiss\" data-hsv-dismiss=\"' + esc(m.id) + '\" title=\"Ta bort meddelande\"><i class=\"fas fa-times\"></i></button>' +\n            '</div>';\n        });\n        container.innerHTML = html;\n      }\n\n      function renderChart(containerId, series, altColor) {\n        var container = el(containerId);\n        if (!container) return;\n        if (!series) return;\n        var days = last7Days();\n        var max = 0;\n        days.forEach(function (d) { max = Math.max(max, series[dayKey(d)] || 0); });\n        var todayKey = dayKey(new Date());\n        var html = '<div class=\"hsv-chart\">';\n        days.forEach(function (d) {\n          var v = series[dayKey(d)] || 0;\n          var pct = max ? Math.max(3, Math.round(v / max * 100)) : 3;\n          var label = DAY_NAMES[d.getDay()] + ' ' + d.getDate() + '/' + (d.getMonth() + 1);\n          html += '<div class=\"hsv-bar-col' + (dayKey(d) === todayKey ? ' hsv-today' : '') + '\">' +\n            '<div class=\"hsv-bar-value\">' + v + '</div>' +\n            '<div class=\"hsv-bar' + (altColor ? ' hsv-bar-alt' : '') + '\" style=\"height:' + pct + '%\"></div>' +\n            '<div class=\"hsv-bar-day\">' + esc(label) + '</div>' +\n            '</div>';\n        });\n        html += '</div>';\n        container.innerHTML = html;\n      }\n\n      function sum(series) {\n        var t = 0;\n        Object.keys(series || {}).forEach(function (k) { t += series[k]; });\n        return t;\n      }\n\n      function renderStats() {\n        if (state.calls) {\n          renderChart('hsv-calls-chart', state.calls, false);\n          var ct = el('hsv-calls-total'); if (ct) ct.textContent = sum(state.calls);\n          var cd = el('hsv-calls-today'); if (cd) cd.textContent = state.calls[dayKey(new Date())] || 0;\n        }\n        if (state.signed) {\n          renderChart('hsv-signed-chart', state.signed, true);\n          var st = el('hsv-signed-total'); if (st) st.textContent = sum(state.signed);\n          var sd = el('hsv-signed-today'); if (sd) sd.textContent = state.signed[dayKey(new Date())] || 0;\n        }\n      }\n\n      /* ---------- data loading ---------- */\n      function loadMe() {\n        return api('/users/users-v1/me').then(function (me) {\n          state.userId = me.id;\n          state.userName = me.fullName || me.username || '';\n          state.messages = (me.reference && me.reference.homeMessages) || [];\n          renderHeader();\n          renderMessages();\n        }).catch(function () {\n          state.error = 'messages';\n          renderMessages();\n        });\n      }\n\n      function statRange() {\n        var from = startOfDay(new Date(Date.now() - 6 * 86400000));\n        var to = new Date(Date.now() + 60000);\n        return { from: from.toISOString(), to: to.toISOString() };\n      }\n\n      /* Calls per local day for the current user.\n         The API groups on UTC fixed intervals — group locally on intervalStart instead. */\n      function loadCalls() {\n        var range = statRange();\n        return api('/dial/statistics-v1/own/issue_1238_2', {\n          from: range.from, to: range.to, fixedIntervalType: 'hour'\n        }).then(function (rows) {\n          var series = {};\n          (rows || []).forEach(function (row) {\n            if (row.userId !== state.userId) return;\n            var key = dayKey(new Date(row.intervalStart));\n            series[key] = (series[key] || 0) + (row.count || 0);\n          });\n          state.calls = series;\n          renderStats();\n        }).catch(function () {\n          var c = el('hsv-calls-chart');\n          if (c) c.innerHTML = '<div class=\"hsv-error\">Kunde inte ladda samtalsstatistik.</div>';\n        });\n      }\n\n      function loadSigned() {\n        var range = statRange();\n        return api('/offers/statistics-v1/own/issue_1238_status_user_fi', {\n          from: range.from, to: range.to, fixedIntervalType: 'hour'\n        }).then(function (rows) {\n          var series = {};\n          (rows || []).forEach(function (row) {\n            if (row.userId !== state.userId) return;\n            if (row.status !== 'signed') return;\n            var key = dayKey(new Date(row.date));\n            series[key] = (series[key] || 0) + (row.count || 0);\n          });\n          state.signed = series;\n          renderStats();\n        }).catch(function () {\n          var c = el('hsv-signed-chart');\n          if (c) c.innerHTML = '<div class=\"hsv-error\">Kunde inte ladda avtalsstatistik.</div>';\n        });\n      }\n\n      function refresh() {\n        loadMe();\n        loadCalls();\n        loadSigned();\n      }\n\n      /* ---------- actions ---------- */\n      function dismissMessage(id) {\n        var next = (state.messages || []).filter(function (m) { return String(m.id) !== String(id); });\n        state.messages = next;\n        renderMessages();\n        apiPut('/users/users-v1/me', {\n          reference: { homeMessages: next },\n          replaceReference: false\n        }).catch(function () {\n          loadMe();\n        });\n      }\n\n      /* ---------- events (document-level delegation; app re-renders the DOM) ---------- */\n      function onClick(event) {\n        var dismiss = event.target.closest ? event.target.closest('[data-hsv-dismiss]') : null;\n        if (dismiss && document.getElementById('hsv-root') && document.getElementById('hsv-root').contains(dismiss)) {\n          dismissMessage(dismiss.getAttribute('data-hsv-dismiss'));\n        }\n      }\n\n      document.addEventListener('click', onClick);\n      var pollTimer = setInterval(function () {\n        if (!document.hidden && document.getElementById('hsv-root')) refresh();\n      }, POLL_MS);\n\n      window.__hsvCleanup = function () {\n        document.removeEventListener('click', onClick);\n        clearInterval(pollTimer);\n      };\n\n      renderHeader();\n      refresh();\n    })();\n  </script>\n</div>","iconName":"home","iconColor":"#1565c0","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50b4dc2e35b2b674732424","name":"Teamledare","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":2,"label":"Teamledare"}],"html":"<div id=\"tlv-root\">\n  <style>\n    #tlv-root {\n      --tlv-bg: var(--color-background-2, #fafafa);\n      --tlv-card: var(--color-background-1, #ffffff);\n      --tlv-secondary: var(--color-background-3, #eeeeee);\n      --tlv-fg: var(--color-text-1, #212121);\n      --tlv-fg2: var(--color-text-2, #616161);\n      --tlv-muted-fg: var(--color-text-3, #757575);\n      --tlv-primary: var(--color-primary, #1565C0);\n      --tlv-primary-fg: #ffffff;\n      --tlv-danger: var(--color-danger, #E53935);\n      --tlv-success: #2E7D32;\n      --tlv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --tlv-border: color-mix(in srgb, var(--tlv-fg) 13%, transparent);\n      --tlv-border-soft: color-mix(in srgb, var(--tlv-fg) 8%, transparent);\n      --tlv-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--tlv-fg);\n      display: block;\n      background: var(--tlv-bg);\n      border: 1px solid var(--tlv-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #tlv-root * { box-sizing: border-box; }\n    #tlv-root button { font-family: inherit; cursor: pointer; }\n    #tlv-root textarea, #tlv-root input, #tlv-root select { font-family: inherit; font-size: 14px; color: var(--tlv-fg); }\n\n    .tlv-app { display: flex; min-height: 480px; }\n    @media (max-width: 800px) { .tlv-app { flex-direction: column; } }\n\n    /* ---------- left: users ---------- */\n    .tlv-users-panel {\n      width: 320px; min-width: 260px; flex-shrink: 0;\n      border-right: 1px solid var(--tlv-border-soft);\n      background: var(--tlv-card);\n      display: flex; flex-direction: column;\n    }\n    @media (max-width: 800px) { .tlv-users-panel { width: 100%; border-right: none; border-bottom: 1px solid var(--tlv-border-soft); } }\n    .tlv-users-header { padding: 14px 16px 10px; border-bottom: 1px solid var(--tlv-border-soft); }\n    .tlv-users-title { font-size: 14px; font-weight: 800; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }\n    .tlv-users-title .fas { color: var(--tlv-primary); }\n    .tlv-select-all {\n      font-size: 12.5px; color: var(--tlv-primary); background: transparent; border: none; padding: 2px 0;\n      font-weight: 700;\n    }\n    .tlv-users-list { flex: 1; overflow-y: auto; max-height: 560px; }\n    .tlv-team-label {\n      font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;\n      color: var(--tlv-muted-fg); padding: 12px 16px 4px;\n    }\n    .tlv-user {\n      display: flex; align-items: center; gap: 10px; padding: 8px 16px; cursor: pointer;\n      transition: background-color 100ms ease;\n    }\n    .tlv-user:hover { background: var(--tlv-hover); }\n    .tlv-user input { pointer-events: none; }\n    .tlv-user-name { font-size: 14px; font-weight: 600; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .tlv-user-badge {\n      font-size: 10.5px; padding: 1px 7px; border-radius: 999px;\n      background: color-mix(in srgb, var(--tlv-primary) 12%, transparent);\n      color: color-mix(in srgb, var(--tlv-primary) 65%, var(--tlv-fg));\n    }\n\n    /* ---------- right: compose + messages ---------- */\n    .tlv-main { flex: 1; padding: 18px 20px; display: flex; flex-direction: column; gap: 16px; min-width: 0; }\n    .tlv-card {\n      background: var(--tlv-card); border: 1px solid var(--tlv-border-soft);\n      border-radius: var(--tlv-radius); padding: 14px 16px;\n    }\n    .tlv-card-title { font-size: 14px; font-weight: 800; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }\n    .tlv-card-title .fas { color: var(--tlv-primary); }\n    .tlv-compose textarea {\n      width: 100%; min-height: 90px; resize: vertical;\n      padding: 10px 12px; border: 1px solid var(--tlv-border); border-radius: var(--tlv-radius);\n      outline: none; background: var(--tlv-bg);\n    }\n    .tlv-compose textarea:focus { border-color: var(--tlv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--tlv-primary) 18%, transparent); }\n    .tlv-compose-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 10px; flex-wrap: wrap; }\n    .tlv-selected-info { font-size: 12.5px; color: var(--tlv-muted-fg); }\n    .tlv-send-btn {\n      background: var(--tlv-primary); color: var(--tlv-primary-fg, #fff);\n      border: none; border-radius: var(--tlv-radius); padding: 9px 18px;\n      font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; gap: 8px;\n    }\n    .tlv-send-btn:disabled { opacity: 0.5; cursor: default; }\n    .tlv-status { font-size: 12.5px; margin-top: 8px; }\n    .tlv-status.tlv-ok { color: var(--tlv-success); }\n    .tlv-status.tlv-fail { color: var(--tlv-danger); }\n\n    .tlv-current-user-select { margin-bottom: 10px; }\n    .tlv-current-user-select select {\n      width: 100%; max-width: 320px; padding: 7px 10px;\n      border: 1px solid var(--tlv-border); border-radius: var(--tlv-radius);\n      background: var(--tlv-card); outline: none;\n    }\n    .tlv-msg {\n      display: flex; gap: 10px; align-items: flex-start;\n      padding: 10px 12px; border-radius: var(--tlv-radius);\n      border: 1px solid var(--tlv-border-soft); background: var(--tlv-bg);\n      margin-bottom: 8px;\n    }\n    .tlv-msg-body { flex: 1; min-width: 0; }\n    .tlv-msg-text { font-size: 13.5px; white-space: pre-wrap; word-break: break-word; }\n    .tlv-msg-meta { font-size: 11px; color: var(--tlv-muted-fg); margin-top: 3px; }\n    .tlv-msg-remove {\n      background: transparent; border: none; color: var(--tlv-muted-fg);\n      width: 26px; height: 26px; border-radius: 6px; flex-shrink: 0;\n    }\n    .tlv-msg-remove:hover { background: var(--tlv-hover); color: var(--tlv-danger); }\n    .tlv-empty { font-size: 13px; color: var(--tlv-muted-fg); padding: 4px 0; }\n    .tlv-loading { display: flex; align-items: center; gap: 10px; color: var(--tlv-muted-fg); font-size: 13px; padding: 24px 0; justify-content: center; }\n    .tlv-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--tlv-border); border-top-color: var(--tlv-primary);\n      animation: tlv-spin 0.8s linear infinite;\n    }\n    @keyframes tlv-spin { to { transform: rotate(360deg); } }\n    .tlv-error-banner {\n      background: color-mix(in srgb, var(--tlv-danger) 8%, var(--tlv-card));\n      border: 1px solid color-mix(in srgb, var(--tlv-danger) 30%, transparent);\n      color: var(--tlv-danger); font-size: 13px; padding: 10px 14px; border-radius: var(--tlv-radius);\n    }\n  </style>\n\n  <div class=\"tlv-app\">\n    <div class=\"tlv-users-panel\">\n      <div class=\"tlv-users-header\">\n        <div class=\"tlv-users-title\"><i class=\"fas fa-users\"></i> Mottagare</div>\n        <button class=\"tlv-select-all\" id=\"tlv-select-all\">Markera alla</button>\n      </div>\n      <div class=\"tlv-users-list\" id=\"tlv-users-list\">\n        <div class=\"tlv-loading\"><div class=\"tlv-spinner\"></div> Laddar användare…</div>\n      </div>\n    </div>\n\n    <div class=\"tlv-main\">\n      <div class=\"tlv-card tlv-compose\">\n        <div class=\"tlv-card-title\"><i class=\"fas fa-bullhorn\"></i> Nytt meddelande</div>\n        <textarea id=\"tlv-text\" placeholder=\"Skriv ett peppande meddelande till dina säljare…\"></textarea>\n        <div class=\"tlv-compose-actions\">\n          <div class=\"tlv-selected-info\" id=\"tlv-selected-info\">0 mottagare valda</div>\n          <button class=\"tlv-send-btn\" id=\"tlv-send\" disabled><i class=\"fas fa-paper-plane\"></i> Skicka</button>\n        </div>\n        <div class=\"tlv-status\" id=\"tlv-status\"></div>\n      </div>\n\n      <div class=\"tlv-card\">\n        <div class=\"tlv-card-title\"><i class=\"fas fa-inbox\"></i> Aktiva meddelanden per användare</div>\n        <div class=\"tlv-current-user-select\">\n          <select id=\"tlv-view-user\"><option value=\"\">Välj användare…</option></select>\n        </div>\n        <div id=\"tlv-user-messages\"><div class=\"tlv-empty\">Välj en användare för att se och ta bort dennes meddelanden.</div></div>\n      </div>\n    </div>\n  </div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n\n      if (window.__tlvCleanup) { try { window.__tlvCleanup(); } catch (e) {} }\n\n      var state = {\n        me: null,\n        users: [],          /* visible target users */\n        teams: {},          /* teamId -> name */\n        selected: {},       /* userId -> true */\n        viewUserId: '',\n        sending: false\n      };\n\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(method, path, body) {\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) {\n          opts.headers['Content-Type'] = 'application/json';\n          opts.body = JSON.stringify(body);\n        }\n        return fetch(API + path, opts).then(function (r) {\n          if (!r.ok) {\n            var err = new Error('HTTP ' + r.status);\n            err.status = r.status;\n            throw err;\n          }\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n      function newId() {\n        return 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n      }\n\n      /* ---------- data ---------- */\n      function load() {\n        return Promise.all([\n          api('GET', '/users/users-v1/me'),\n          api('GET', '/users/users-v1'),\n          api('GET', '/users/teams-v1')\n        ]).then(function (results) {\n          state.me = results[0];\n          var users = results[1] || [];\n          (results[2] || []).forEach(function (t) { state.teams[t.id] = t.name; });\n\n          var isAdmin = state.me.type === 'admin';\n          var supervised = state.me.supervisedTeamIds || [];\n\n          state.users = users.filter(function (u) {\n            if (u.type === 'systemUser') return false;\n            if (u.active === false) return false;\n            if (u.id === state.me.id) return false;\n            if (isAdmin) return true;\n            return u.teamId && supervised.indexOf(u.teamId) !== -1;\n          }).sort(function (a, b) {\n            var ta = state.teams[a.teamId] || 'Övriga';\n            var tb = state.teams[b.teamId] || 'Övriga';\n            if (ta !== tb) return ta < tb ? -1 : 1;\n            return (a.fullName || a.username || '') < (b.fullName || b.username || '') ? -1 : 1;\n          });\n\n          renderUsers();\n          renderViewUserSelect();\n          if (!isAdmin && !supervised.length) {\n            el('tlv-users-list').innerHTML =\n              '<div style=\"padding:14px 16px;\"><div class=\"tlv-error-banner\">Du är inte teamledare för något team. ' +\n              'Vyn kräver att du har team du ansvarar för (eller adminbehörighet).</div></div>';\n          }\n        }).catch(function () {\n          var list = el('tlv-users-list');\n          if (list) list.innerHTML = '<div style=\"padding:14px 16px;\"><div class=\"tlv-error-banner\">Kunde inte ladda användare.</div></div>';\n        });\n      }\n\n      /* ---------- render ---------- */\n      function renderUsers() {\n        var list = el('tlv-users-list');\n        if (!list) return;\n        if (!state.users.length) {\n          list.innerHTML = '<div class=\"tlv-empty\" style=\"padding:14px 16px;\">Inga användare att visa.</div>';\n          return;\n        }\n        var html = '';\n        var lastTeam = null;\n        state.users.forEach(function (u) {\n          var teamName = state.teams[u.teamId] || 'Övriga';\n          if (teamName !== lastTeam) {\n            html += '<div class=\"tlv-team-label\">' + esc(teamName) + '</div>';\n            lastTeam = teamName;\n          }\n          var checked = state.selected[u.id] ? ' checked' : '';\n          var name = u.fullName || u.username || u.id;\n          var count = ((u.reference || {}).homeMessages || []).length;\n          html += '<div class=\"tlv-user\" data-tlv-user=\"' + esc(u.id) + '\">' +\n            '<input type=\"checkbox\"' + checked + '>' +\n            '<span class=\"tlv-user-name\">' + esc(name) + '</span>' +\n            (count ? '<span class=\"tlv-user-badge\">' + count + ' st</span>' : '') +\n            '</div>';\n        });\n        list.innerHTML = html;\n        updateSelectedInfo();\n      }\n\n      function updateSelectedInfo() {\n        var n = Object.keys(state.selected).filter(function (k) { return state.selected[k]; }).length;\n        var info = el('tlv-selected-info');\n        if (info) info.textContent = n + ' mottagare ' + (n === 1 ? 'vald' : 'valda');\n        var btn = el('tlv-send');\n        var text = el('tlv-text');\n        if (btn) btn.disabled = state.sending || !n || !(text && text.value.trim());\n        var selectAll = el('tlv-select-all');\n        if (selectAll) selectAll.textContent = (n && n === state.users.length) ? 'Avmarkera alla' : 'Markera alla';\n      }\n\n      function renderViewUserSelect() {\n        var select = el('tlv-view-user');\n        if (!select) return;\n        var html = '<option value=\"\">Välj användare…</option>';\n        state.users.forEach(function (u) {\n          var sel = state.viewUserId === u.id ? ' selected' : '';\n          html += '<option value=\"' + esc(u.id) + '\"' + sel + '>' + esc(u.fullName || u.username || u.id) + '</option>';\n        });\n        select.innerHTML = html;\n      }\n\n      function renderUserMessages() {\n        var container = el('tlv-user-messages');\n        if (!container) return;\n        var user = null;\n        state.users.forEach(function (u) { if (u.id === state.viewUserId) user = u; });\n        if (!user) {\n          container.innerHTML = '<div class=\"tlv-empty\">Välj en användare för att se och ta bort dennes meddelanden.</div>';\n          return;\n        }\n        var messages = ((user.reference || {}).homeMessages || []).slice().sort(function (a, b) {\n          return new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime();\n        });\n        if (!messages.length) {\n          container.innerHTML = '<div class=\"tlv-empty\">' + esc(user.fullName || user.username) + ' har inga aktiva meddelanden.</div>';\n          return;\n        }\n        var html = '';\n        messages.forEach(function (m) {\n          var when = '';\n          try { when = new Date(m.date).toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); } catch (e) {}\n          html += '<div class=\"tlv-msg\">' +\n            '<div class=\"tlv-msg-body\">' +\n            '<div class=\"tlv-msg-text\">' + esc(m.text) + '</div>' +\n            '<div class=\"tlv-msg-meta\">' + esc(m.fromName || '') + (when ? ' · ' + esc(when) : '') + '</div>' +\n            '</div>' +\n            '<button class=\"tlv-msg-remove\" data-tlv-remove=\"' + esc(m.id) + '\" title=\"Ta bort\"><i class=\"fas fa-times\"></i></button>' +\n            '</div>';\n        });\n        container.innerHTML = html;\n      }\n\n      function setStatus(text, ok) {\n        var status = el('tlv-status');\n        if (!status) return;\n        status.textContent = text || '';\n        status.className = 'tlv-status ' + (ok ? 'tlv-ok' : 'tlv-fail');\n      }\n\n      /* ---------- actions ---------- */\n      function refreshUser(userId) {\n        /* Re-read a single user to get fresh reference before mutating. */\n        return api('GET', '/users/users-v1/' + userId);\n      }\n\n      function sendToUser(userId, message) {\n        return refreshUser(userId).then(function (fresh) {\n          var existing = ((fresh.reference || {}).homeMessages) || [];\n          var next = existing.concat([message]);\n          return api('PUT', '/users/users-v1/' + userId, {\n            reference: { homeMessages: next },\n            replaceReference: false\n          });\n        });\n      }\n\n      function send() {\n        if (state.sending) return;\n        var text = el('tlv-text');\n        var value = text ? text.value.trim() : '';\n        var targets = Object.keys(state.selected).filter(function (k) { return state.selected[k]; });\n        if (!value || !targets.length) return;\n\n        state.sending = true;\n        updateSelectedInfo();\n        setStatus('Skickar…', true);\n\n        var message = {\n          id: newId(),\n          text: value,\n          fromUserId: state.me.id,\n          fromName: state.me.fullName || state.me.username || '',\n          date: new Date().toISOString()\n        };\n\n        var ok = 0, failed = 0;\n        var queue = targets.slice();\n\n        function next() {\n          if (!queue.length) {\n            state.sending = false;\n            if (text && !failed) text.value = '';\n            setStatus(\n              failed\n                ? ok + ' skickade, ' + failed + ' misslyckades. Kontrollera att du har rättighet att uppdatera användare.'\n                : 'Meddelandet skickades till ' + ok + ' ' + (ok === 1 ? 'användare' : 'användare') + '.',\n              !failed\n            );\n            load().then(renderUserMessages);\n            return;\n          }\n          var userId = queue.shift();\n          sendToUser(userId, message).then(function () { ok++; next(); })\n            .catch(function () { failed++; next(); });\n        }\n        next();\n      }\n\n      function removeMessage(messageId) {\n        var user = null;\n        state.users.forEach(function (u) { if (u.id === state.viewUserId) user = u; });\n        if (!user) return;\n        refreshUser(user.id).then(function (fresh) {\n          var existing = ((fresh.reference || {}).homeMessages) || [];\n          var next = existing.filter(function (m) { return String(m.id) !== String(messageId); });\n          return api('PUT', '/users/users-v1/' + user.id, {\n            reference: { homeMessages: next },\n            replaceReference: false\n          });\n        }).then(function () {\n          return load();\n        }).then(function () {\n          renderUserMessages();\n        }).catch(function () {\n          setStatus('Kunde inte ta bort meddelandet.', false);\n        });\n      }\n\n      /* ---------- events ---------- */\n      function inRoot(node) {\n        var root = document.getElementById('tlv-root');\n        return root && node && root.contains(node);\n      }\n\n      function onClick(event) {\n        var target = event.target;\n        if (!target.closest) return;\n\n        var userRow = target.closest('[data-tlv-user]');\n        if (userRow && inRoot(userRow)) {\n          var id = userRow.getAttribute('data-tlv-user');\n          state.selected[id] = !state.selected[id];\n          var box = userRow.querySelector('input[type=checkbox]');\n          if (box) box.checked = state.selected[id];\n          updateSelectedInfo();\n          return;\n        }\n\n        var selectAll = target.closest('#tlv-select-all');\n        if (selectAll && inRoot(selectAll)) {\n          var selectedCount = Object.keys(state.selected).filter(function (k) { return state.selected[k]; }).length;\n          var all = selectedCount !== state.users.length;\n          state.selected = {};\n          if (all) state.users.forEach(function (u) { state.selected[u.id] = true; });\n          renderUsers();\n          return;\n        }\n\n        var sendBtn = target.closest('#tlv-send');\n        if (sendBtn && inRoot(sendBtn)) { send(); return; }\n\n        var removeBtn = target.closest('[data-tlv-remove]');\n        if (removeBtn && inRoot(removeBtn)) {\n          removeMessage(removeBtn.getAttribute('data-tlv-remove'));\n        }\n      }\n\n      function onChange(event) {\n        var select = event.target.closest ? event.target.closest('#tlv-view-user') : null;\n        if (select && inRoot(select)) {\n          state.viewUserId = select.value;\n          renderUserMessages();\n        }\n      }\n\n      function onInput(event) {\n        if (event.target && event.target.id === 'tlv-text') updateSelectedInfo();\n      }\n\n      /* Contrast probe -- dark theme can resolve --color-primary to a LIGHT color,\n         so white text on primary is not a safe assumption. Measure and adapt. */\n      function applyContrast() {\n        var root = document.getElementById('tlv-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--tlv-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var tlvThemeObserver = new MutationObserver(applyContrast);\n      tlvThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      /* The app switches theme by rewriting a <style> tag in <head> - html/body attributes never change. */\n      tlvThemeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) tlvThemeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('change', onChange);\n      document.addEventListener('input', onInput);\n\n      window.__tlvCleanup = function () {\n        tlvThemeObserver.disconnect();\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('change', onChange);\n        document.removeEventListener('input', onInput);\n      };\n\n      load();\n    })();\n  </script>\n</div>","iconName":"bullhorn","iconColor":"#EF6C00","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50b4dcd31b9cf4ab2e8be1","name":"Tävlingar","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":2,"label":"Tävlingar"}],"html":"<div id=\"gmv-root\">\n  <style>\n    #gmv-root {\n      --gmv-bg: var(--color-background-2, #fafafa);\n      --gmv-card: var(--color-background-1, #ffffff);\n      --gmv-secondary: var(--color-background-3, #eeeeee);\n      --gmv-fg: var(--color-text-1, #212121);\n      --gmv-fg2: var(--color-text-2, #616161);\n      --gmv-muted-fg: var(--color-text-3, #757575);\n      --gmv-primary: var(--color-primary, #1565C0);\n      --gmv-danger: var(--color-danger, #E53935);\n      --gmv-success: #2E7D32;\n      --gmv-gold: #F9A825;\n      --gmv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --gmv-border: color-mix(in srgb, var(--gmv-fg) 13%, transparent);\n      --gmv-border-soft: color-mix(in srgb, var(--gmv-fg) 8%, transparent);\n      --gmv-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--gmv-fg);\n      display: block;\n      background: var(--gmv-bg);\n      border: 1px solid var(--gmv-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #gmv-root * { box-sizing: border-box; }\n    #gmv-root button { font-family: inherit; cursor: pointer; }\n    #gmv-root input, #gmv-root select { font-family: inherit; font-size: 14px; color: var(--gmv-fg); }\n\n    .gmv-toolbar {\n      display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n      padding: 12px 16px; border-bottom: 1px solid var(--gmv-border-soft); background: var(--gmv-card);\n    }\n    .gmv-toolbar-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; }\n    .gmv-toolbar-title .fas { color: var(--gmv-gold); }\n    .gmv-toolbar-spacer { flex: 1; }\n    .gmv-btn {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--gmv-border); background: var(--gmv-card);\n      border-radius: var(--gmv-radius); padding: 7px 12px; font-size: 13px; font-weight: 600;\n    }\n    .gmv-btn:hover { background: var(--gmv-hover); }\n    .gmv-btn.gmv-primary-btn { background: var(--gmv-primary); border-color: var(--gmv-primary); color: var(--gmv-primary-fg, #fff); }\n    .gmv-btn.gmv-danger-btn { color: var(--gmv-danger); }\n    .gmv-btn:disabled { opacity: 0.5; cursor: default; }\n\n    .gmv-body { padding: 16px; display: flex; flex-direction: column; gap: 16px; }\n    .gmv-section-title { font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gmv-muted-fg); }\n    .gmv-columns { display: grid; grid-template-columns: 1fr 300px; gap: 16px; align-items: start; }\n    @media (max-width: 980px) { .gmv-columns { grid-template-columns: 1fr; } }\n    .gmv-main-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }\n    .gmv-side-col { display: flex; flex-direction: column; gap: 16px; }\n\n    .gmv-card {\n      background: var(--gmv-card); border: 1px solid var(--gmv-border-soft);\n      border-radius: var(--gmv-radius); padding: 14px 16px;\n    }\n    .gmv-empty { font-size: 13px; color: var(--gmv-muted-fg); }\n    .gmv-loading { display: flex; align-items: center; gap: 10px; color: var(--gmv-muted-fg); font-size: 13px; padding: 30px 0; justify-content: center; }\n    .gmv-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--gmv-border); border-top-color: var(--gmv-primary);\n      animation: gmv-spin 0.8s linear infinite;\n    }\n    @keyframes gmv-spin { to { transform: rotate(360deg); } }\n\n    /* ---------- invites ---------- */\n    .gmv-invite { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--gmv-border-soft); flex-wrap: wrap; }\n    .gmv-invite:last-child { border-bottom: none; }\n    .gmv-invite-icon { font-size: 22px; }\n    .gmv-invite-info { flex: 1; min-width: 160px; }\n    .gmv-invite-name { font-weight: 800; font-size: 14px; }\n    .gmv-invite-meta { font-size: 12px; color: var(--gmv-muted-fg); }\n\n    /* ---------- game card ---------- */\n    .gmv-game { padding: 0; overflow: hidden; }\n    .gmv-game-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; flex-wrap: wrap; }\n    .gmv-game-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; }\n    .gmv-game-meta { font-size: 12px; color: var(--gmv-muted-fg); }\n    .gmv-game-spacer { flex: 1; }\n    .gmv-chip {\n      font-size: 11px; font-weight: 700; padding: 2px 10px; border-radius: 999px;\n      background: color-mix(in srgb, var(--gmv-primary) 12%, transparent);\n      color: color-mix(in srgb, var(--gmv-primary) 65%, var(--gmv-fg));\n    }\n    .gmv-chip.gmv-chip-live { background: color-mix(in srgb, var(--gmv-success) 14%, transparent); color: var(--gmv-success); }\n    .gmv-chip.gmv-chip-done { background: color-mix(in srgb, var(--gmv-gold) 18%, transparent); color: #8a6d00; }\n    .gmv-winner-banner {\n      display: flex; align-items: center; gap: 10px; margin: 0 16px 10px;\n      background: color-mix(in srgb, var(--gmv-gold) 14%, var(--gmv-card));\n      border: 1px solid color-mix(in srgb, var(--gmv-gold) 45%, transparent);\n      border-radius: var(--gmv-radius); padding: 9px 14px; font-size: 14px; font-weight: 700;\n    }\n    .gmv-winner-banner .fas { color: var(--gmv-gold); font-size: 18px; }\n\n    /* ---------- race track ---------- */\n    .gmv-track { position: relative; margin: 4px 16px 14px; border-radius: var(--gmv-radius); overflow: hidden; }\n    .gmv-track.gmv-horses { background: linear-gradient(180deg, #a8763e, #96683a); }\n    .gmv-track.gmv-boats { background: linear-gradient(180deg, #1e88c9, #10639c); }\n    .gmv-lane { position: relative; height: 52px; }\n    .gmv-horses .gmv-lane { border-bottom: 2px dashed rgba(255, 255, 255, 0.35); }\n    .gmv-horses .gmv-lane:last-child { border-bottom: none; }\n    .gmv-boats .gmv-lane {\n      border-bottom: 1px solid rgba(255, 255, 255, 0.18);\n      background:\n        radial-gradient(14px 5px at 22% 78%, rgba(255,255,255,0.13), transparent 70%),\n        radial-gradient(18px 6px at 62% 66%, rgba(255,255,255,0.10), transparent 70%),\n        radial-gradient(12px 5px at 86% 82%, rgba(255,255,255,0.12), transparent 70%);\n    }\n    .gmv-finish {\n      position: absolute; top: 0; bottom: 0; right: 34px; width: 12px;\n      background:\n        repeating-conic-gradient(#ffffff 0% 25%, #1b1b1b 0% 50%);\n      background-size: 12px 12px;\n      opacity: 0.9;\n    }\n    .gmv-sprite-wrap {\n      position: absolute; top: 50%; left: 0;\n      transform: translateY(-50%);\n      transition: left 1200ms cubic-bezier(0.25, 0.6, 0.3, 1);\n      z-index: 2;\n    }\n    .gmv-sprite { display: block; animation: gmv-bob 0.9s ease-in-out infinite; }\n    .gmv-boats .gmv-sprite { animation: gmv-sail 2.2s ease-in-out infinite; }\n    @keyframes gmv-bob {\n      0%, 100% { transform: translateY(0) rotate(0deg); }\n      30% { transform: translateY(-2.5px) rotate(-2deg); }\n      60% { transform: translateY(1px) rotate(1.5deg); }\n    }\n    @keyframes gmv-sail {\n      0%, 100% { transform: translateY(0) rotate(-2deg); }\n      50% { transform: translateY(-3px) rotate(2.5deg); }\n    }\n    .gmv-lane-label {\n      position: absolute; left: 8px; top: 5px; z-index: 3;\n      font-size: 11px; font-weight: 800; color: #fff;\n      text-shadow: 0 1px 3px rgba(0,0,0,0.55);\n      display: flex; align-items: center; gap: 6px;\n      max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n    }\n    .gmv-lane-score {\n      position: absolute; right: 8px; top: 5px; z-index: 3;\n      font-size: 11px; font-weight: 800; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.55);\n    }\n    .gmv-lane-crown { color: var(--gmv-gold); }\n\n    .gmv-game-footer { display: flex; align-items: center; gap: 12px; padding: 0 16px 12px; font-size: 12px; color: var(--gmv-muted-fg); flex-wrap: wrap; }\n\n    /* ---------- side col ---------- */\n    .gmv-lb-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13.5px; border-bottom: 1px solid var(--gmv-border-soft); }\n    .gmv-lb-row:last-child { border-bottom: none; }\n    .gmv-lb-rank { width: 20px; font-weight: 800; color: var(--gmv-muted-fg); }\n    .gmv-lb-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }\n    .gmv-lb-count { font-weight: 800; display: flex; align-items: center; gap: 5px; }\n    .gmv-lb-count .fas { color: var(--gmv-gold); font-size: 12px; }\n    .gmv-card-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }\n    .gmv-card-title .fas { color: var(--gmv-gold); }\n    .gmv-done-row { padding: 8px 0; border-bottom: 1px solid var(--gmv-border-soft); font-size: 13px; }\n    .gmv-done-row:last-child { border-bottom: none; }\n    .gmv-done-name { font-weight: 700; }\n    .gmv-done-meta { font-size: 11.5px; color: var(--gmv-muted-fg); }\n\n    /* ---------- modal ---------- */\n    .gmv-modal-backdrop {\n      position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,0.35);\n      display: none; align-items: center; justify-content: center;\n    }\n    .gmv-modal-backdrop.gmv-open { display: flex; }\n    .gmv-modal {\n      background: var(--gmv-card); color: var(--gmv-fg);\n      border-radius: 12px; padding: 20px 22px; width: 430px; max-width: calc(100vw - 40px);\n      max-height: calc(100vh - 60px); overflow-y: auto;\n      box-shadow: 0 12px 48px rgba(0,0,0,0.25);\n    }\n    .gmv-modal-title { font-size: 16px; font-weight: 800; margin-bottom: 12px; }\n    .gmv-field-label { font-size: 12.5px; font-weight: 700; margin: 12px 0 4px; }\n    .gmv-modal input[type=text], .gmv-modal input[type=number], .gmv-modal input[type=datetime-local], .gmv-modal select {\n      width: 100%; padding: 8px 11px; border: 1px solid var(--gmv-border);\n      border-radius: 8px; outline: none; background: var(--gmv-bg);\n    }\n    .gmv-theme-row { display: flex; gap: 10px; }\n    .gmv-theme-opt {\n      flex: 1; border: 2px solid var(--gmv-border); border-radius: 10px; padding: 10px;\n      text-align: center; cursor: pointer; font-size: 13px; font-weight: 700;\n    }\n    .gmv-theme-opt .gmv-theme-icon { font-size: 26px; display: block; margin-bottom: 4px; }\n    .gmv-theme-opt.gmv-picked { border-color: var(--gmv-primary); background: color-mix(in srgb, var(--gmv-primary) 7%, transparent); }\n    .gmv-invitees { max-height: 180px; overflow-y: auto; border: 1px solid var(--gmv-border-soft); border-radius: 8px; padding: 4px; }\n    .gmv-invitee { display: flex; align-items: center; gap: 9px; padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 13.5px; }\n    .gmv-invitee:hover { background: var(--gmv-hover); }\n    .gmv-invitee input { pointer-events: none; }\n    .gmv-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }\n    .gmv-modal-error { color: var(--gmv-danger); font-size: 12.5px; margin-top: 8px; }\n  </style>\n\n  <div class=\"gmv-toolbar\">\n    <div class=\"gmv-toolbar-title\"><i class=\"fas fa-flag-checkered\"></i> Tävlingar</div>\n    <div class=\"gmv-toolbar-spacer\"></div>\n    <button class=\"gmv-btn\" id=\"gmv-reload\"><i class=\"fas fa-sync-alt\"></i> Uppdatera</button>\n    <button class=\"gmv-btn gmv-primary-btn\" id=\"gmv-new-game\"><i class=\"fas fa-plus\"></i> Ny tävling</button>\n  </div>\n\n  <div class=\"gmv-body\" id=\"gmv-body\">\n    <div class=\"gmv-loading\"><div class=\"gmv-spinner\"></div> Laddar tävlingar…</div>\n  </div>\n\n  <div class=\"gmv-modal-backdrop\" id=\"gmv-modal-backdrop\">\n    <div class=\"gmv-modal\">\n      <div class=\"gmv-modal-title\">Ny tävling</div>\n\n      <div class=\"gmv-field-label\">Namn</div>\n      <input type=\"text\" id=\"gmv-f-name\" placeholder=\"T.ex. Fredagsracet\">\n\n      <div class=\"gmv-field-label\">Typ av tävling</div>\n      <select id=\"gmv-f-type\">\n        <option value=\"first_to\">Först till X signerade avtal</option>\n        <option value=\"most_before\">Flest signerade avtal före en tidpunkt</option>\n      </select>\n\n      <div id=\"gmv-f-target-wrap\">\n        <div class=\"gmv-field-label\">Mål (antal signerade avtal)</div>\n        <input type=\"number\" id=\"gmv-f-target\" min=\"1\" value=\"5\">\n      </div>\n\n      <div id=\"gmv-f-ends-wrap\" style=\"display:none;\">\n        <div class=\"gmv-field-label\">Slut-tid</div>\n        <input type=\"datetime-local\" id=\"gmv-f-ends\">\n      </div>\n\n      <div class=\"gmv-field-label\">Tema</div>\n      <div class=\"gmv-theme-row\">\n        <div class=\"gmv-theme-opt gmv-picked\" data-gmv-theme=\"horses\"><span class=\"gmv-theme-icon\"><i class=\"fas fa-horse\"></i></span> Hästkapplöpning</div>\n        <div class=\"gmv-theme-opt\" data-gmv-theme=\"boats\"><span class=\"gmv-theme-icon\"><i class=\"fas fa-ship\"></i></span> Segelbåtsrace</div>\n      </div>\n\n      <div class=\"gmv-field-label\">Bjud in kollegor</div>\n      <div class=\"gmv-invitees\" id=\"gmv-invitees\"></div>\n\n      <div class=\"gmv-modal-error\" id=\"gmv-modal-error\"></div>\n      <div class=\"gmv-modal-actions\">\n        <button class=\"gmv-btn\" id=\"gmv-modal-cancel\">Avbryt</button>\n        <button class=\"gmv-btn gmv-primary-btn\" id=\"gmv-modal-save\"><i class=\"fas fa-flag-checkered\"></i> Starta tävling</button>\n      </div>\n    </div>\n  </div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var POLL_MS = 30000;\n      var LANE_COLORS = ['#FFD54F', '#4FC3F7', '#FF8A65', '#AED581', '#CE93D8', '#F48FB1', '#80CBC4', '#FFAB91'];\n\n      if (window.__gmvCleanup) { try { window.__gmvCleanup(); } catch (e) {} }\n\n      var state = {\n        me: null,\n        users: [],            /* all non-system users */\n        usersById: {},\n        games: [],            /* assembled game models */\n        scores: {},           /* gameId -> { userId: signedCount } */\n        loaded: false,\n        pickedTheme: 'horses',\n        savingWins: {}        /* gameId -> true while writing win */\n      };\n\n      /* ---------- utils ---------- */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(method, path, params, body) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += '?' + qs;\n        }\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) {\n          opts.headers['Content-Type'] = 'application/json';\n          opts.body = JSON.stringify(body);\n        }\n        return fetch(url, opts).then(function (r) {\n          if (!r.ok) {\n            var err = new Error('HTTP ' + r.status);\n            err.status = r.status;\n            throw err;\n          }\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n      function newId() { return 'g' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }\n      function firstName(user) {\n        var n = (user && (user.fullName || user.username)) || '?';\n        return n.split(' ')[0];\n      }\n      function fullName(user) { return (user && (user.fullName || user.username)) || 'Okänd'; }\n\n      /* ---------- SVG sprites ---------- */\n      function horseSvg(color) {\n        return '<svg class=\"gmv-sprite\" width=\"46\" height=\"34\" viewBox=\"0 0 46 34\">' +\n          '<g>' +\n          '<ellipse cx=\"22\" cy=\"17\" rx=\"12\" ry=\"7\" fill=\"' + color + '\"/>' +\n          '<path d=\"M32 13 L40 7 L42 11 L36 16 Z\" fill=\"' + color + '\"/>' +\n          '<circle cx=\"40\" cy=\"9\" r=\"3.4\" fill=\"' + color + '\"/>' +\n          '<path d=\"M43 8 L46 9 L43 10.5 Z\" fill=\"#3b2a1a\"/>' +\n          '<path d=\"M38 5 L39.5 2.2 L41 5.2 Z\" fill=\"#3b2a1a\"/>' +\n          '<path d=\"M12 15 C8 13, 6 15, 4 13 C5 17, 8 19, 11 19 Z\" fill=\"' + color + '\" opacity=\"0.85\"/>' +\n          '<path d=\"M14 23 L11 31 M20 24 L19 32 M26 24 L28 32 M31 21 L34 30\" stroke=\"' + color + '\" stroke-width=\"2.6\" stroke-linecap=\"round\" fill=\"none\"/>' +\n          '<circle cx=\"40.5\" cy=\"8\" r=\"0.8\" fill=\"#1b1b1b\"/>' +\n          '</g></svg>';\n      }\n      function boatSvg(color) {\n        return '<svg class=\"gmv-sprite\" width=\"44\" height=\"38\" viewBox=\"0 0 44 38\">' +\n          '<g>' +\n          '<path d=\"M4 27 L40 27 L33 34 L10 34 Z\" fill=\"' + color + '\"/>' +\n          '<rect x=\"21\" y=\"4\" width=\"2\" height=\"23\" fill=\"#5d4037\"/>' +\n          '<path d=\"M23 5 L37 24 L23 24 Z\" fill=\"#ffffff\" opacity=\"0.95\"/>' +\n          '<path d=\"M20 8 L9 24 L20 24 Z\" fill=\"' + color + '\" opacity=\"0.75\"/>' +\n          '<path d=\"M23 4 L30 6.5 L23 9 Z\" fill=\"' + color + '\"/>' +\n          '</g></svg>';\n      }\n\n      /* ---------- data assembly ---------- */\n      function load() {\n        return api('GET', '/users/users-v1').then(function (users) {\n          state.users = (users || []).filter(function (u) { return u.type !== 'systemUser'; });\n          state.usersById = {};\n          state.users.forEach(function (u) { state.usersById[u.id] = u; });\n          if (!state.me) {\n            var uid = cookies()['s2_uid'];\n            state.me = state.usersById[uid] || null;\n          } else {\n            state.me = state.usersById[state.me.id] || state.me;\n          }\n          if (!state.me) {\n            return api('GET', '/users/users-v1/me').then(function (me) {\n              state.me = me;\n              assembleGames();\n            });\n          }\n          assembleGames();\n        }).catch(function () {\n          var body = el('gmv-body');\n          if (body) body.innerHTML = '<div class=\"gmv-empty\">Kunde inte ladda tävlingar. Försök igen.</div>';\n        });\n      }\n\n      function assembleGames() {\n        var games = [];\n        state.users.forEach(function (u) {\n          var docs = (u.reference || {}).ssgGames || {};\n          Object.keys(docs).forEach(function (gameId) {\n            var doc = docs[gameId];\n            if (!doc || !doc.id) return;\n            var participants = [u.id];\n            state.users.forEach(function (candidate) {\n              if (candidate.id === u.id) return;\n              var joined = (candidate.reference || {}).ssgJoined || {};\n              if (joined[gameId]) participants.push(candidate.id);\n            });\n            games.push({\n              doc: doc,\n              creatorId: u.id,\n              participants: participants\n            });\n          });\n        });\n        games.sort(function (a, b) {\n          return new Date(b.doc.createdAt || 0).getTime() - new Date(a.doc.createdAt || 0).getTime();\n        });\n        state.games = games;\n        state.loaded = true;\n        loadScores().then(function () {\n          render();\n          recordWinsIfNeeded();\n        });\n      }\n\n      function isRelevantToMe(game) {\n        if (!state.me) return false;\n        if (game.creatorId === state.me.id) return true;\n        if (game.participants.indexOf(state.me.id) !== -1) return true;\n        if ((game.doc.invitedUserIds || []).indexOf(state.me.id) !== -1) return true;\n        return false;\n      }\n\n      function myInviteFor(game) {\n        if (!state.me) return false;\n        if (game.doc.canceledAt) return false;\n        if ((game.doc.invitedUserIds || []).indexOf(state.me.id) === -1) return false;\n        if (game.participants.indexOf(state.me.id) !== -1) return false;\n        var declined = ((state.me.reference || {}).ssgDeclined) || {};\n        if (declined[game.doc.id]) return false;\n        if (gameFinished(game)) return false;\n        return true;\n      }\n\n      function gameEndsAt(game) {\n        return game.doc.endsAt ? new Date(game.doc.endsAt) : null;\n      }\n\n      function gameFinished(game) {\n        if (game.doc.canceledAt) return true;\n        var scores = state.scores[game.doc.id] || {};\n        if (game.doc.type === 'first_to') {\n          var target = game.doc.target || 1;\n          for (var i = 0; i < game.participants.length; i++) {\n            if ((scores[game.participants[i]] || 0) >= target) return true;\n          }\n          var ends = gameEndsAt(game);\n          if (ends && Date.now() > ends.getTime()) return true;\n          return false;\n        }\n        var endsAt = gameEndsAt(game);\n        return endsAt ? Date.now() > endsAt.getTime() : false;\n      }\n\n      function gameWinners(game) {\n        var scores = state.scores[game.doc.id] || {};\n        if (!gameFinished(game) || game.doc.canceledAt) return [];\n        var best = 0;\n        game.participants.forEach(function (id) { best = Math.max(best, scores[id] || 0); });\n        if (game.doc.type === 'first_to' && best < (game.doc.target || 1)) return [];\n        if (best <= 0) return [];\n        return game.participants.filter(function (id) { return (scores[id] || 0) === best; });\n      }\n\n      /* ---------- scores from signed offers ---------- */\n      function loadScores() {\n        var active = state.games.filter(function (g) { return isRelevantToMe(g) && !g.doc.canceledAt; });\n        return Promise.all(active.map(function (game) {\n          var from = game.doc.startAt || game.doc.createdAt;\n          var ends = gameEndsAt(game);\n          var to = ends && ends.getTime() < Date.now() ? ends.toISOString() : new Date().toISOString();\n          return api('GET', '/offers/statistics-v1/own/issue_1238_status_user', {\n            from: from, to: to\n          }).then(function (rows) {\n            var byUser = {};\n            (rows || []).forEach(function (row) {\n              if (row.status !== 'signed') return;\n              byUser[row.userId] = (byUser[row.userId] || 0) + (row.count || 0);\n            });\n            state.scores[game.doc.id] = byUser;\n          }).catch(function () {\n            state.scores[game.doc.id] = state.scores[game.doc.id] || {};\n          });\n        }));\n      }\n\n      /* ---------- win recording (only own reference) ---------- */\n      function recordWinsIfNeeded() {\n        if (!state.me) return;\n        var myWins = ((state.me.reference || {}).ssgWins) || [];\n        var recorded = {};\n        myWins.forEach(function (w) { recorded[w.gameId] = true; });\n\n        state.games.forEach(function (game) {\n          if (!isRelevantToMe(game)) return;\n          if (recorded[game.doc.id] || state.savingWins[game.doc.id]) return;\n          var winners = gameWinners(game);\n          if (winners.indexOf(state.me.id) === -1) return;\n\n          state.savingWins[game.doc.id] = true;\n          var scores = state.scores[game.doc.id] || {};\n          var entry = {\n            gameId: game.doc.id,\n            gameName: game.doc.name,\n            type: game.doc.type,\n            theme: game.doc.theme,\n            count: scores[state.me.id] || 0,\n            wonAt: new Date().toISOString()\n          };\n          api('PUT', '/users/users-v1/me', null, {\n            reference: { ssgWins: myWins.concat([entry]) },\n            replaceReference: false\n          }).then(function () {\n            if (state.me.reference) {\n              state.me.reference.ssgWins = myWins.concat([entry]);\n            }\n            render();\n          }).catch(function () {\n            delete state.savingWins[game.doc.id];\n          });\n        });\n      }\n\n      /* ---------- rendering ---------- */\n      function render() {\n        var body = el('gmv-body');\n        if (!body) return;\n        if (!state.loaded) return;\n\n        var invites = state.games.filter(myInviteFor);\n        var mine = state.games.filter(function (g) {\n          return isRelevantToMe(g) && !myInviteFor(g) && !g.doc.canceledAt;\n        });\n        var running = mine.filter(function (g) { return !gameFinished(g); });\n        var done = mine.filter(function (g) { return gameFinished(g); }).slice(0, 8);\n\n        var html = '';\n\n        if (invites.length) {\n          html += '<div class=\"gmv-card\">' +\n            '<div class=\"gmv-card-title\"><i class=\"fas fa-envelope-open-text\"></i> Inbjudningar</div>';\n          invites.forEach(function (game) {\n            var creator = state.usersById[game.creatorId];\n            html += '<div class=\"gmv-invite\">' +\n              '<span class=\"gmv-invite-icon\"><i class=\"fas fa-' + (game.doc.theme === 'boats' ? 'ship' : 'horse') + '\"></i></span>' +\n              '<div class=\"gmv-invite-info\">' +\n              '<div class=\"gmv-invite-name\">' + esc(game.doc.name) + '</div>' +\n              '<div class=\"gmv-invite-meta\">' + esc(fullName(creator)) + ' bjuder in dig · ' + esc(gameRuleText(game)) + '</div>' +\n              '</div>' +\n              '<button class=\"gmv-btn gmv-primary-btn\" data-gmv-accept=\"' + esc(game.doc.id) + '\"><i class=\"fas fa-check\"></i> Delta</button>' +\n              '<button class=\"gmv-btn\" data-gmv-decline=\"' + esc(game.doc.id) + '\">Tacka nej</button>' +\n              '</div>';\n          });\n          html += '</div>';\n        }\n\n        html += '<div class=\"gmv-columns\"><div class=\"gmv-main-col\">';\n\n        html += '<div class=\"gmv-section-title\">Pågående tävlingar</div>';\n        if (!running.length) {\n          html += '<div class=\"gmv-card\"><div class=\"gmv-empty\">Inga pågående tävlingar. Starta en med \"Ny tävling\"!</div></div>';\n        }\n        running.forEach(function (game) { html += renderGame(game, false); });\n\n        if (done.length) {\n          html += '<div class=\"gmv-section-title\" style=\"margin-top:6px;\">Avslutade</div>';\n          done.forEach(function (game) { html += renderGame(game, true); });\n        }\n\n        html += '</div><div class=\"gmv-side-col\">';\n        html += renderWinsCard();\n        html += '</div></div>';\n\n        body.innerHTML = html;\n\n        /* Animate sprites to their positions on next frame (CSS transition). */\n        requestAnimationFrame(function () {\n          requestAnimationFrame(function () {\n            document.querySelectorAll('[data-gmv-target-left]').forEach(function (n) {\n              n.style.left = n.getAttribute('data-gmv-target-left');\n            });\n          });\n        });\n      }\n\n      function gameRuleText(game) {\n        if (game.doc.type === 'first_to') {\n          return 'Först till ' + (game.doc.target || '?') + ' signerade avtal';\n        }\n        var ends = gameEndsAt(game);\n        var when = '';\n        try { when = ends ? ends.toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : '?'; } catch (e) {}\n        return 'Flest signerade före ' + when;\n      }\n\n      function timeLeftText(game) {\n        var ends = gameEndsAt(game);\n        if (!ends) return '';\n        var ms = ends.getTime() - Date.now();\n        if (ms <= 0) return 'Avslutad';\n        var mins = Math.floor(ms / 60000);\n        var days = Math.floor(mins / 1440);\n        var hours = Math.floor((mins % 1440) / 60);\n        var rest = mins % 60;\n        if (days > 0) return days + 'd ' + hours + 'h kvar';\n        if (hours > 0) return hours + 'h ' + rest + 'min kvar';\n        return rest + ' min kvar';\n      }\n\n      function renderGame(game, finished) {\n        var scores = state.scores[game.doc.id] || {};\n        var theme = game.doc.theme === 'boats' ? 'boats' : 'horses';\n        var winners = gameWinners(game);\n        var target = game.doc.target || 0;\n\n        var maxScore = 0;\n        game.participants.forEach(function (id) { maxScore = Math.max(maxScore, scores[id] || 0); });\n\n        var html = '<div class=\"gmv-card gmv-game\">' +\n          '<div class=\"gmv-game-header\">' +\n          '<div class=\"gmv-game-title\"><i class=\"fas fa-' + (theme === 'boats' ? 'ship' : 'horse') + '\"></i> ' + esc(game.doc.name) + '</div>' +\n          '<span class=\"gmv-chip ' + (finished ? 'gmv-chip-done' : 'gmv-chip-live') + '\">' + (finished ? 'Avslutad' : 'Pågår') + '</span>' +\n          '<div class=\"gmv-game-spacer\"></div>' +\n          '<div class=\"gmv-game-meta\">' + esc(gameRuleText(game)) + '</div>' +\n          (!finished && state.me && game.creatorId === state.me.id\n            ? '<button class=\"gmv-btn gmv-danger-btn\" data-gmv-cancel=\"' + esc(game.doc.id) + '\" title=\"Avbryt tävlingen\"><i class=\"fas fa-times\"></i></button>'\n            : '') +\n          '</div>';\n\n        if (finished && winners.length) {\n          var names = winners.map(function (id) { return fullName(state.usersById[id]); }).join(' & ');\n          html += '<div class=\"gmv-winner-banner\"><i class=\"fas fa-trophy\"></i> ' +\n            esc(names) + ' vann med ' + maxScore + ' signerade avtal!</div>';\n        } else if (finished && !winners.length && !game.doc.canceledAt) {\n          html += '<div class=\"gmv-winner-banner\" style=\"opacity:0.8;\"><i class=\"fas fa-flag\"></i> Ingen nådde mål — inga signeringar registrerades.</div>';\n        }\n\n        html += '<div class=\"gmv-track gmv-' + theme + '\">' +\n          '<div class=\"gmv-finish\"></div>';\n\n        game.participants.forEach(function (userId, i) {\n          var user = state.usersById[userId];\n          var color = LANE_COLORS[i % LANE_COLORS.length];\n          var score = scores[userId] || 0;\n\n          var fraction;\n          if (game.doc.type === 'first_to' && target > 0) {\n            fraction = Math.min(1, score / target);\n          } else {\n            /* Most-before: leader leads visually; at finish the leader reaches the flag. */\n            var denominator = Math.max(maxScore, 1);\n            fraction = (score / denominator) * (finished ? 1 : 0.85);\n          }\n          if (winners.length && winners.indexOf(userId) !== -1) fraction = 1;\n\n          /* left: from 6% to 84% (finish line ~ right:34px+12px). */\n          var leftPct = (4 + fraction * 80).toFixed(2) + '%';\n          var isWinner = winners.indexOf(userId) !== -1;\n          var isMe = state.me && userId === state.me.id;\n\n          html += '<div class=\"gmv-lane\">' +\n            '<div class=\"gmv-lane-label\">' +\n            (isWinner ? '<i class=\"fas fa-crown gmv-lane-crown\"></i>' : '') +\n            esc(firstName(user)) + (isMe ? ' (du)' : '') +\n            '</div>' +\n            '<div class=\"gmv-lane-score\">' + score + (game.doc.type === 'first_to' ? ' / ' + target : '') + '</div>' +\n            '<div class=\"gmv-sprite-wrap\" style=\"left:4%\" data-gmv-target-left=\"' + leftPct + '\">' +\n            (theme === 'boats' ? boatSvg(color) : horseSvg(color)) +\n            '</div>' +\n            '</div>';\n        });\n\n        html += '</div>';\n\n        var invitedPending = (game.doc.invitedUserIds || []).filter(function (id) {\n          return game.participants.indexOf(id) === -1;\n        });\n        html += '<div class=\"gmv-game-footer\">' +\n          '<span><i class=\"fas fa-user-friends\"></i> ' + game.participants.length + ' deltagare</span>' +\n          (invitedPending.length && !finished ? '<span>' + invitedPending.length + ' inbjudna har inte svarat</span>' : '') +\n          (!finished && game.doc.type === 'most_before' ? '<span><i class=\"far fa-clock\"></i> ' + esc(timeLeftText(game)) + '</span>' : '') +\n          '<span>Startade ' + esc(shortDate(game.doc.startAt || game.doc.createdAt)) + '</span>' +\n          '</div>';\n\n        html += '</div>';\n        return html;\n      }\n\n      function shortDate(iso) {\n        try {\n          return new Date(iso).toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });\n        } catch (e) { return ''; }\n      }\n\n      function renderWinsCard() {\n        var counts = {};\n        state.users.forEach(function (u) {\n          var wins = ((u.reference || {}).ssgWins) || [];\n          if (wins.length) counts[u.id] = wins.length;\n        });\n        var rows = Object.keys(counts).map(function (id) {\n          return { id: id, count: counts[id], name: fullName(state.usersById[id]) };\n        }).sort(function (a, b) { return b.count - a.count; }).slice(0, 10);\n\n        var html = '<div class=\"gmv-card\">' +\n          '<div class=\"gmv-card-title\"><i class=\"fas fa-trophy\"></i> Flest vinster</div>';\n        if (!rows.length) {\n          html += '<div class=\"gmv-empty\">Inga vinster ännu. Första tävlingen kan bli din!</div>';\n        } else {\n          rows.forEach(function (row, i) {\n            html += '<div class=\"gmv-lb-row\">' +\n              '<span class=\"gmv-lb-rank\">' + (i + 1) + '</span>' +\n              '<span class=\"gmv-lb-name\">' + esc(row.name) + '</span>' +\n              '<span class=\"gmv-lb-count\"><i class=\"fas fa-trophy\"></i> ' + row.count + '</span>' +\n              '</div>';\n          });\n        }\n        html += '</div>';\n\n        var myWins = state.me ? (((state.me.reference || {}).ssgWins) || []) : [];\n        if (myWins.length) {\n          html += '<div class=\"gmv-card\"><div class=\"gmv-card-title\"><i class=\"fas fa-medal\"></i> Dina vinster</div>';\n          myWins.slice().reverse().slice(0, 6).forEach(function (w) {\n            html += '<div class=\"gmv-done-row\">' +\n              '<div class=\"gmv-done-name\"><i class=\"fas fa-' + (w.theme === 'boats' ? 'ship' : 'horse') + '\"></i> ' + esc(w.gameName) + '</div>' +\n              '<div class=\"gmv-done-meta\">' + esc(shortDate(w.wonAt)) + ' · ' + w.count + ' signerade</div>' +\n              '</div>';\n          });\n          html += '</div>';\n        }\n        return html;\n      }\n\n      /* ---------- create game modal ---------- */\n      function openModal() {\n        state.pickedTheme = 'horses';\n        var err = el('gmv-modal-error'); if (err) err.textContent = '';\n        var name = el('gmv-f-name'); if (name) name.value = '';\n        var target = el('gmv-f-target'); if (target) target.value = '5';\n        var type = el('gmv-f-type'); if (type) type.value = 'first_to';\n        var ends = el('gmv-f-ends');\n        if (ends) {\n          var d = new Date(Date.now() + 8 * 3600000);\n          d.setMinutes(0, 0, 0);\n          try {\n            var pad = function (n) { return (n < 10 ? '0' : '') + n; };\n            ends.value = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());\n          } catch (e) {}\n        }\n        syncTypeFields();\n        renderThemePick();\n        renderInvitees();\n        var backdrop = el('gmv-modal-backdrop');\n        if (backdrop) backdrop.className = 'gmv-modal-backdrop gmv-open';\n      }\n      function closeModal() {\n        var backdrop = el('gmv-modal-backdrop');\n        if (backdrop) backdrop.className = 'gmv-modal-backdrop';\n      }\n\n      function syncTypeFields() {\n        var type = el('gmv-f-type');\n        var isFirstTo = !type || type.value === 'first_to';\n        var targetWrap = el('gmv-f-target-wrap');\n        var endsWrap = el('gmv-f-ends-wrap');\n        if (targetWrap) targetWrap.style.display = isFirstTo ? '' : 'none';\n        if (endsWrap) endsWrap.style.display = isFirstTo ? 'none' : '';\n      }\n\n      function renderThemePick() {\n        document.querySelectorAll('[data-gmv-theme]').forEach(function (n) {\n          n.className = 'gmv-theme-opt' + (n.getAttribute('data-gmv-theme') === state.pickedTheme ? ' gmv-picked' : '');\n        });\n      }\n\n      function renderInvitees() {\n        var container = el('gmv-invitees');\n        if (!container) return;\n        var html = '';\n        state.users.filter(function (u) {\n          return u.active !== false && (!state.me || u.id !== state.me.id);\n        }).sort(function (a, b) {\n          return fullName(a) < fullName(b) ? -1 : 1;\n        }).forEach(function (u) {\n          html += '<div class=\"gmv-invitee\" data-gmv-invitee=\"' + esc(u.id) + '\">' +\n            '<input type=\"checkbox\">' +\n            '<span>' + esc(fullName(u)) + '</span>' +\n            '</div>';\n        });\n        if (!html) html = '<div class=\"gmv-empty\" style=\"padding:8px;\">Inga kollegor att bjuda in.</div>';\n        container.innerHTML = html;\n      }\n\n      function createGame() {\n        var err = el('gmv-modal-error');\n        var name = el('gmv-f-name') ? el('gmv-f-name').value.trim() : '';\n        var type = el('gmv-f-type') ? el('gmv-f-type').value : 'first_to';\n        var target = parseInt(el('gmv-f-target') ? el('gmv-f-target').value : '0', 10);\n        var endsRaw = el('gmv-f-ends') ? el('gmv-f-ends').value : '';\n\n        if (!name) { if (err) err.textContent = 'Ge tävlingen ett namn.'; return; }\n        if (type === 'first_to' && (!target || target < 1)) { if (err) err.textContent = 'Ange ett mål på minst 1.'; return; }\n        var endsAt = null;\n        if (type === 'most_before') {\n          endsAt = endsRaw ? new Date(endsRaw) : null;\n          if (!endsAt || isNaN(endsAt.getTime()) || endsAt.getTime() <= Date.now()) {\n            if (err) err.textContent = 'Ange en slut-tid i framtiden.';\n            return;\n          }\n        }\n\n        var invited = [];\n        document.querySelectorAll('[data-gmv-invitee]').forEach(function (n) {\n          var box = n.querySelector('input');\n          if (box && box.checked) invited.push(n.getAttribute('data-gmv-invitee'));\n        });\n        if (!invited.length) { if (err) err.textContent = 'Bjud in minst en kollega.'; return; }\n\n        var gameId = newId();\n        var doc = {\n          id: gameId,\n          name: name,\n          type: type,\n          target: type === 'first_to' ? target : null,\n          endsAt: endsAt ? endsAt.toISOString() : null,\n          theme: state.pickedTheme,\n          createdBy: state.me ? state.me.id : null,\n          createdByName: state.me ? fullName(state.me) : '',\n          invitedUserIds: invited,\n          startAt: new Date().toISOString(),\n          createdAt: new Date().toISOString(),\n          canceledAt: null\n        };\n\n        /* Read own fresh reference, merge the games object, write back (single top-level key). */\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var games = ((me.reference || {}).ssgGames) || {};\n          games[gameId] = doc;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssgGames: games },\n            replaceReference: false\n          });\n        }).then(function () {\n          closeModal();\n          return load();\n        }).catch(function () {\n          if (err) err.textContent = 'Kunde inte skapa tävlingen. Försök igen.';\n        });\n      }\n\n      function acceptInvite(gameId) {\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var joined = ((me.reference || {}).ssgJoined) || {};\n          joined[gameId] = { joinedAt: new Date().toISOString() };\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssgJoined: joined },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      function declineInvite(gameId) {\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var declined = ((me.reference || {}).ssgDeclined) || {};\n          declined[gameId] = true;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssgDeclined: declined },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      function cancelGame(gameId) {\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var games = ((me.reference || {}).ssgGames) || {};\n          if (games[gameId]) games[gameId].canceledAt = new Date().toISOString();\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssgGames: games },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      /* ---------- events ---------- */\n      function inRoot(node) {\n        var root = document.getElementById('gmv-root');\n        return root && node && root.contains(node);\n      }\n\n      function onClick(event) {\n        var target = event.target;\n        if (!target.closest) return;\n\n        var newGame = target.closest('#gmv-new-game');\n        if (newGame && inRoot(newGame)) { openModal(); return; }\n\n        var reload = target.closest('#gmv-reload');\n        if (reload && inRoot(reload)) { load(); return; }\n\n        var cancelBtn = target.closest('#gmv-modal-cancel');\n        if (cancelBtn) { closeModal(); return; }\n\n        var save = target.closest('#gmv-modal-save');\n        if (save) { createGame(); return; }\n\n        var theme = target.closest('[data-gmv-theme]');\n        if (theme) {\n          state.pickedTheme = theme.getAttribute('data-gmv-theme');\n          renderThemePick();\n          return;\n        }\n\n        var invitee = target.closest('[data-gmv-invitee]');\n        if (invitee) {\n          var box = invitee.querySelector('input');\n          if (box) box.checked = !box.checked;\n          return;\n        }\n\n        var accept = target.closest('[data-gmv-accept]');\n        if (accept && inRoot(accept)) { acceptInvite(accept.getAttribute('data-gmv-accept')); return; }\n\n        var decline = target.closest('[data-gmv-decline]');\n        if (decline && inRoot(decline)) { declineInvite(decline.getAttribute('data-gmv-decline')); return; }\n\n        var cancelGameBtn = target.closest('[data-gmv-cancel]');\n        if (cancelGameBtn && inRoot(cancelGameBtn)) { cancelGame(cancelGameBtn.getAttribute('data-gmv-cancel')); return; }\n\n        var backdrop = target.closest('#gmv-modal-backdrop');\n        if (backdrop && target === backdrop) { closeModal(); return; }\n      }\n\n      function onChange(event) {\n        if (event.target && event.target.id === 'gmv-f-type') syncTypeFields();\n      }\n\n      function onKeyDown(event) {\n        if (event.key === 'Escape') closeModal();\n      }\n\n      /* Contrast probe -- dark theme can resolve --color-primary to a LIGHT color,\n         so white text on primary is not a safe assumption. Measure and adapt. */\n      function applyContrast() {\n        var root = document.getElementById('gmv-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--gmv-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var gmvThemeObserver = new MutationObserver(applyContrast);\n      gmvThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      /* The app switches theme by rewriting a <style> tag in <head> - html/body attributes never change. */\n      gmvThemeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) gmvThemeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('change', onChange);\n      document.addEventListener('keydown', onKeyDown);\n\n      var pollTimer = setInterval(function () {\n        if (!document.hidden && document.getElementById('gmv-root')) load();\n      }, POLL_MS);\n\n      window.__gmvCleanup = function () {\n        gmvThemeObserver.disconnect();\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('change', onChange);\n        document.removeEventListener('keydown', onKeyDown);\n        clearInterval(pollTimer);\n      };\n\n      load();\n    })();\n  </script>\n</div>","iconName":"flag-checkered","iconColor":"#F9A825","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50b4dcfe660af530a7a99d","name":"Snake","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["6a59f2ac44f8f2cdb383d502"],"locations":[{"type":"navigation","position":2,"label":"Snake"}],"html":"<div id=\"snk-root\">\n  <style>\n    #snk-root {\n      --snk-bg: var(--color-background-2, #fafafa);\n      --snk-card: var(--color-background-1, #ffffff);\n      --snk-fg: var(--color-text-1, #212121);\n      --snk-fg2: var(--color-text-2, #616161);\n      --snk-muted-fg: var(--color-text-3, #757575);\n      --snk-primary: var(--color-primary, #1565C0);\n      --snk-danger: var(--color-danger, #E53935);\n      --snk-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --snk-border: color-mix(in srgb, var(--snk-fg) 13%, transparent);\n      --snk-border-soft: color-mix(in srgb, var(--snk-fg) 8%, transparent);\n      --snk-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--snk-fg);\n      display: block;\n      background: var(--snk-bg);\n      border: 1px solid var(--snk-border-soft);\n      border-radius: 12px;\n      padding: 20px;\n    }\n    #snk-root * { box-sizing: border-box; }\n    #snk-root button { font-family: inherit; cursor: pointer; }\n\n    .snk-layout { display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; }\n    .snk-game-col { display: flex; flex-direction: column; align-items: center; gap: 12px; }\n\n    .snk-hud { display: flex; gap: 12px; align-items: center; width: 100%; justify-content: space-between; }\n    .snk-hud-box {\n      background: var(--snk-card); border: 1px solid var(--snk-border-soft); border-radius: var(--snk-radius);\n      padding: 6px 14px; font-size: 13px; font-weight: 700; display: flex; align-items: center; gap: 8px;\n    }\n    .snk-hud-box .fas { color: var(--snk-primary); }\n    .snk-hud-box .snk-num { font-size: 17px; font-weight: 800; }\n\n    .snk-canvas-wrap {\n      position: relative; border-radius: var(--snk-radius); overflow: hidden;\n      border: 1px solid var(--snk-border); box-shadow: 0 6px 24px rgba(0,0,0,0.08);\n    }\n    #snk-canvas { display: block; background: #17241b; }\n    .snk-overlay {\n      position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;\n      background: rgba(10, 18, 12, 0.72); color: #fff; text-align: center; gap: 10px; padding: 20px;\n    }\n    .snk-overlay.snk-hidden { display: none; }\n    .snk-overlay-title { font-size: 26px; font-weight: 800; }\n    .snk-overlay-sub { font-size: 14px; opacity: 0.85; }\n    .snk-play-btn {\n      background: var(--snk-primary); color: var(--snk-primary-fg, #fff); border: none; border-radius: 999px;\n      padding: 10px 26px; font-size: 15px; font-weight: 800; display: inline-flex; align-items: center; gap: 9px;\n      margin-top: 6px;\n    }\n    .snk-play-btn:hover { filter: brightness(1.08); }\n    .snk-help { font-size: 12px; color: var(--snk-muted-fg); text-align: center; }\n\n    .snk-board-col { width: 260px; }\n    .snk-card {\n      background: var(--snk-card); border: 1px solid var(--snk-border-soft);\n      border-radius: var(--snk-radius); padding: 14px 16px;\n    }\n    .snk-card-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }\n    .snk-card-title .fas { color: #F9A825; }\n    .snk-lb-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13.5px; border-bottom: 1px solid var(--snk-border-soft); }\n    .snk-lb-row:last-child { border-bottom: none; }\n    .snk-lb-rank { width: 22px; font-weight: 800; color: var(--snk-muted-fg); flex-shrink: 0; }\n    .snk-lb-row.snk-top1 .snk-lb-rank { color: #F9A825; }\n    .snk-lb-row.snk-top2 .snk-lb-rank { color: #9E9E9E; }\n    .snk-lb-row.snk-top3 .snk-lb-rank { color: #8D6E63; }\n    .snk-lb-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }\n    .snk-lb-row.snk-me .snk-lb-name { color: var(--snk-primary); font-weight: 800; }\n    .snk-lb-score { font-weight: 800; }\n    .snk-empty { font-size: 12.5px; color: var(--snk-muted-fg); }\n  </style>\n\n  <div class=\"snk-layout\">\n    <div class=\"snk-game-col\">\n      <div class=\"snk-hud\">\n        <div class=\"snk-hud-box\"><i class=\"fas fa-apple-alt\"></i> Poäng: <span class=\"snk-num\" id=\"snk-score\">0</span></div>\n        <div class=\"snk-hud-box\"><i class=\"fas fa-crown\"></i> Rekord: <span class=\"snk-num\" id=\"snk-best\">0</span></div>\n      </div>\n      <div class=\"snk-canvas-wrap\">\n        <canvas id=\"snk-canvas\" width=\"440\" height=\"440\"></canvas>\n        <div class=\"snk-overlay\" id=\"snk-overlay\">\n          <div class=\"snk-overlay-title\" id=\"snk-overlay-title\">Snake</div>\n          <div class=\"snk-overlay-sub\" id=\"snk-overlay-sub\">Samla äpplen — krocka inte!</div>\n          <button class=\"snk-play-btn\" id=\"snk-play\"><i class=\"fas fa-play\"></i> Spela</button>\n        </div>\n      </div>\n      <div class=\"snk-help\">Styr med piltangenterna eller WASD · Paus med mellanslag</div>\n    </div>\n\n    <div class=\"snk-board-col\">\n      <div class=\"snk-card\">\n        <div class=\"snk-card-title\"><i class=\"fas fa-trophy\"></i> Topplista</div>\n        <div id=\"snk-leaderboard\"><div class=\"snk-empty\">Laddar…</div></div>\n      </div>\n    </div>\n  </div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var GRID = 22;          /* cells per side */\n      var CELL = 20;          /* px per cell */\n      var TICK_START = 140;   /* ms */\n      var TICK_MIN = 60;\n\n      if (window.__snkCleanup) { try { window.__snkCleanup(); } catch (e) {} }\n\n      var state = {\n        me: null,\n        best: 0,\n        leaderboard: [],\n        running: false,\n        paused: false,\n        snake: [],\n        dir: { x: 1, y: 0 },\n        nextDir: { x: 1, y: 0 },\n        food: null,\n        score: 0,\n        tickMs: TICK_START,\n        timer: null\n      };\n\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(method, path, body) {\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) {\n          opts.headers['Content-Type'] = 'application/json';\n          opts.body = JSON.stringify(body);\n        }\n        return fetch(API + path, opts).then(function (r) {\n          if (!r.ok) throw new Error('HTTP ' + r.status);\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n\n      /* ---------- leaderboard ---------- */\n      function loadUsers() {\n        return api('GET', '/users/users-v1').then(function (users) {\n          var rows = [];\n          (users || []).forEach(function (u) {\n            if (u.type === 'systemUser') return;\n            var hs = (u.reference || {}).snakeHighScore;\n            if (hs && hs.score > 0) {\n              rows.push({ userId: u.id, name: u.fullName || u.username || '?', score: hs.score });\n            }\n          });\n          rows.sort(function (a, b) { return b.score - a.score; });\n          state.leaderboard = rows.slice(0, 10);\n          renderLeaderboard();\n        }).catch(function () {\n          var lb = el('snk-leaderboard');\n          if (lb) lb.innerHTML = '<div class=\"snk-empty\">Kunde inte ladda topplistan.</div>';\n        });\n      }\n\n      function loadMe() {\n        return api('GET', '/users/users-v1/me').then(function (me) {\n          state.me = me;\n          var hs = (me.reference || {}).snakeHighScore;\n          state.best = (hs && hs.score) || 0;\n          renderHud();\n        }).catch(function () {});\n      }\n\n      function renderLeaderboard() {\n        var lb = el('snk-leaderboard');\n        if (!lb) return;\n        if (!state.leaderboard.length) {\n          lb.innerHTML = '<div class=\"snk-empty\">Inga resultat ännu — bli först på listan!</div>';\n          return;\n        }\n        var html = '';\n        state.leaderboard.forEach(function (row, i) {\n          var cls = 'snk-lb-row' + (i === 0 ? ' snk-top1' : i === 1 ? ' snk-top2' : i === 2 ? ' snk-top3' : '') +\n            (state.me && row.userId === state.me.id ? ' snk-me' : '');\n          html += '<div class=\"' + cls + '\">' +\n            '<span class=\"snk-lb-rank\">' + (i + 1) + '</span>' +\n            '<span class=\"snk-lb-name\">' + esc(row.name) + '</span>' +\n            '<span class=\"snk-lb-score\">' + row.score + '</span>' +\n            '</div>';\n        });\n        lb.innerHTML = html;\n      }\n\n      function renderHud() {\n        var s = el('snk-score'); if (s) s.textContent = state.score;\n        var b = el('snk-best'); if (b) b.textContent = state.best;\n      }\n\n      function saveHighScore() {\n        if (!state.me || state.score <= state.best) return Promise.resolve();\n        state.best = state.score;\n        renderHud();\n        return api('PUT', '/users/users-v1/me', {\n          reference: { snakeHighScore: { score: state.score, date: new Date().toISOString() } },\n          replaceReference: false\n        }).then(function () {\n          return loadUsers();\n        }).catch(function () {});\n      }\n\n      /* ---------- game ---------- */\n      function resetGame() {\n        var mid = Math.floor(GRID / 2);\n        state.snake = [{ x: mid - 1, y: mid }, { x: mid - 2, y: mid }, { x: mid - 3, y: mid }];\n        state.dir = { x: 1, y: 0 };\n        state.nextDir = { x: 1, y: 0 };\n        state.score = 0;\n        state.tickMs = TICK_START;\n        placeFood();\n        renderHud();\n      }\n\n      function placeFood() {\n        var free = [];\n        for (var y = 0; y < GRID; y++) {\n          for (var x = 0; x < GRID; x++) {\n            var taken = false;\n            for (var i = 0; i < state.snake.length; i++) {\n              if (state.snake[i].x === x && state.snake[i].y === y) { taken = true; break; }\n            }\n            if (!taken) free.push({ x: x, y: y });\n          }\n        }\n        state.food = free[Math.floor(Math.random() * free.length)] || null;\n      }\n\n      function scheduleTick() {\n        clearTimeout(state.timer);\n        state.timer = setTimeout(tick, state.tickMs);\n      }\n\n      function tick() {\n        if (!state.running || state.paused) return;\n        if (!document.getElementById('snk-root')) { stopGame(); return; }\n\n        state.dir = state.nextDir;\n        var head = { x: state.snake[0].x + state.dir.x, y: state.snake[0].y + state.dir.y };\n\n        /* Wall or self collision */\n        var crash = head.x < 0 || head.y < 0 || head.x >= GRID || head.y >= GRID;\n        if (!crash) {\n          for (var i = 0; i < state.snake.length - 1; i++) {\n            if (state.snake[i].x === head.x && state.snake[i].y === head.y) { crash = true; break; }\n          }\n        }\n        if (crash) { gameOver(); return; }\n\n        state.snake.unshift(head);\n        if (state.food && head.x === state.food.x && head.y === state.food.y) {\n          state.score++;\n          state.tickMs = Math.max(TICK_MIN, state.tickMs - 3);\n          renderHud();\n          placeFood();\n        } else {\n          state.snake.pop();\n        }\n\n        draw();\n        scheduleTick();\n      }\n\n      function draw() {\n        var canvas = el('snk-canvas');\n        if (!canvas) return;\n        var ctx = canvas.getContext('2d');\n        ctx.fillStyle = '#17241b';\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n        /* Subtle grid */\n        ctx.strokeStyle = 'rgba(255,255,255,0.035)';\n        ctx.lineWidth = 1;\n        for (var g = 1; g < GRID; g++) {\n          ctx.beginPath(); ctx.moveTo(g * CELL, 0); ctx.lineTo(g * CELL, GRID * CELL); ctx.stroke();\n          ctx.beginPath(); ctx.moveTo(0, g * CELL); ctx.lineTo(GRID * CELL, g * CELL); ctx.stroke();\n        }\n\n        /* Food (apple) */\n        if (state.food) {\n          var fx = state.food.x * CELL + CELL / 2;\n          var fy = state.food.y * CELL + CELL / 2;\n          ctx.fillStyle = '#E53935';\n          ctx.beginPath();\n          ctx.arc(fx, fy + 1, CELL * 0.34, 0, Math.PI * 2);\n          ctx.fill();\n          ctx.strokeStyle = '#6D4C41';\n          ctx.lineWidth = 2;\n          ctx.beginPath(); ctx.moveTo(fx, fy - CELL * 0.22); ctx.lineTo(fx + 3, fy - CELL * 0.44); ctx.stroke();\n          ctx.fillStyle = '#43A047';\n          ctx.beginPath();\n          ctx.ellipse(fx + 5, fy - CELL * 0.4, 4, 2.2, -0.5, 0, Math.PI * 2);\n          ctx.fill();\n        }\n\n        /* Snake */\n        for (var i = state.snake.length - 1; i >= 0; i--) {\n          var seg = state.snake[i];\n          var t = i / Math.max(1, state.snake.length - 1);\n          var green = Math.round(200 - t * 90);\n          ctx.fillStyle = i === 0 ? '#AEEA00' : 'rgb(' + Math.round(90 - t * 40) + ',' + green + ',80)';\n          var pad = i === 0 ? 1 : 2;\n          roundRect(ctx, seg.x * CELL + pad, seg.y * CELL + pad, CELL - pad * 2, CELL - pad * 2, 6);\n          ctx.fill();\n        }\n\n        /* Eyes on head */\n        if (state.snake.length) {\n          var head = state.snake[0];\n          ctx.fillStyle = '#17241b';\n          var cx = head.x * CELL + CELL / 2;\n          var cy = head.y * CELL + CELL / 2;\n          var ox = state.dir.y !== 0 ? 5 : 0;\n          var oy = state.dir.x !== 0 ? 5 : 0;\n          var fx2 = state.dir.x * 4;\n          var fy2 = state.dir.y * 4;\n          ctx.beginPath(); ctx.arc(cx + fx2 - ox, cy + fy2 - oy, 2.1, 0, Math.PI * 2); ctx.fill();\n          ctx.beginPath(); ctx.arc(cx + fx2 + ox, cy + fy2 + oy, 2.1, 0, Math.PI * 2); ctx.fill();\n        }\n      }\n\n      function roundRect(ctx, x, y, w, h, r) {\n        ctx.beginPath();\n        ctx.moveTo(x + r, y);\n        ctx.arcTo(x + w, y, x + w, y + h, r);\n        ctx.arcTo(x + w, y + h, x, y + h, r);\n        ctx.arcTo(x, y + h, x, y, r);\n        ctx.arcTo(x, y, x + w, y, r);\n        ctx.closePath();\n      }\n\n      function startGame() {\n        resetGame();\n        state.running = true;\n        state.paused = false;\n        setOverlay(false);\n        draw();\n        scheduleTick();\n      }\n\n      function stopGame() {\n        state.running = false;\n        clearTimeout(state.timer);\n      }\n\n      function gameOver() {\n        stopGame();\n        saveHighScore();\n        setOverlay(true, 'Game over!', 'Du fick ' + state.score + ' poäng' +\n          (state.score >= state.best && state.score > 0 ? ' — nytt rekord!' : '') , 'Spela igen');\n      }\n\n      function togglePause() {\n        if (!state.running) return;\n        state.paused = !state.paused;\n        if (state.paused) {\n          setOverlay(true, 'Paus', 'Tryck på mellanslag för att fortsätta', 'Fortsätt');\n        } else {\n          setOverlay(false);\n          scheduleTick();\n        }\n      }\n\n      function setOverlay(show, title, sub, btnText) {\n        var overlay = el('snk-overlay');\n        if (!overlay) return;\n        overlay.className = 'snk-overlay' + (show ? '' : ' snk-hidden');\n        if (title) { var t = el('snk-overlay-title'); if (t) t.textContent = title; }\n        if (sub) { var s = el('snk-overlay-sub'); if (s) s.textContent = sub; }\n        if (btnText) {\n          var b = el('snk-play');\n          if (b) b.innerHTML = '<i class=\"fas fa-play\"></i> ' + esc(btnText);\n        }\n      }\n\n      /* ---------- events ---------- */\n      function inRoot(node) {\n        var root = document.getElementById('snk-root');\n        return root && node && root.contains(node);\n      }\n\n      function onClick(event) {\n        var play = event.target.closest ? event.target.closest('#snk-play') : null;\n        if (play && inRoot(play)) {\n          if (state.running && state.paused) { togglePause(); return; }\n          startGame();\n        }\n      }\n\n      var KEY_DIRS = {\n        ArrowUp: { x: 0, y: -1 }, ArrowDown: { x: 0, y: 1 }, ArrowLeft: { x: -1, y: 0 }, ArrowRight: { x: 1, y: 0 },\n        w: { x: 0, y: -1 }, s: { x: 0, y: 1 }, a: { x: -1, y: 0 }, d: { x: 1, y: 0 },\n        W: { x: 0, y: -1 }, S: { x: 0, y: 1 }, A: { x: -1, y: 0 }, D: { x: 1, y: 0 }\n      };\n\n      function onKeyDown(event) {\n        if (!document.getElementById('snk-root')) return;\n        var tag = (event.target && event.target.tagName) || '';\n        if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;\n\n        if (event.key === ' ' || event.code === 'Space') {\n          if (state.running) {\n            event.preventDefault();\n            togglePause();\n          }\n          return;\n        }\n        var dir = KEY_DIRS[event.key];\n        if (dir && state.running && !state.paused) {\n          event.preventDefault();\n          /* No 180° turns */\n          if (dir.x !== -state.dir.x || dir.y !== -state.dir.y) {\n            state.nextDir = dir;\n          }\n        }\n      }\n\n      /* Contrast probe -- dark theme can resolve --color-primary to a LIGHT color,\n         so white text on primary is not a safe assumption. Measure and adapt. */\n      function applyContrast() {\n        var root = document.getElementById('snk-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--snk-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var snkThemeObserver = new MutationObserver(applyContrast);\n      snkThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      /* The app switches theme by rewriting a <style> tag in <head> - html/body attributes never change. */\n      snkThemeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) snkThemeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('keydown', onKeyDown);\n\n      window.__snkCleanup = function () {\n        snkThemeObserver.disconnect();\n        stopGame();\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('keydown', onKeyDown);\n      };\n\n      draw();\n      loadMe().then(loadUsers);\n    })();\n  </script>\n</div>","iconName":"gamepad","iconColor":"#2E7D32","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50b4dc41f4334349fb4755","name":"Kontakt-kanban","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":0,"label":"Kanban"}],"html":"<div id=\"kbv-root\">\n  <style>\n    #kbv-root {\n      --kbv-bg: var(--color-background-2, #fafafa);\n      --kbv-card: var(--color-background-1, #ffffff);\n      --kbv-secondary: var(--color-background-3, #eeeeee);\n      --kbv-fg: var(--color-text-1, #212121);\n      --kbv-fg2: var(--color-text-2, #616161);\n      --kbv-muted-fg: var(--color-text-3, #757575);\n      --kbv-primary: var(--color-primary, #1565C0);\n      --kbv-danger: var(--color-danger, #E53935);\n      --kbv-success: #2E7D32;\n      --kbv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --kbv-border: color-mix(in srgb, var(--kbv-fg) 13%, transparent);\n      --kbv-border-soft: color-mix(in srgb, var(--kbv-fg) 8%, transparent);\n      --kbv-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--kbv-fg);\n      display: block;\n      background: var(--kbv-bg);\n      border: 1px solid var(--kbv-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #kbv-root * { box-sizing: border-box; }\n    #kbv-root button { font-family: inherit; cursor: pointer; }\n    #kbv-root input, #kbv-root select { font-family: inherit; font-size: 14px; color: var(--kbv-fg); }\n\n    /* ---------- toolbar ---------- */\n    .kbv-toolbar {\n      display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n      padding: 12px 16px; border-bottom: 1px solid var(--kbv-border-soft);\n      background: var(--kbv-card);\n    }\n    .kbv-toolbar-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-right: 6px; }\n    .kbv-toolbar-title .fas { color: var(--kbv-primary); }\n    .kbv-tool-btn {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--kbv-border); background: var(--kbv-card);\n      border-radius: var(--kbv-radius); padding: 7px 12px; font-size: 13px; font-weight: 600;\n    }\n    .kbv-tool-btn:hover { background: var(--kbv-hover); }\n    .kbv-tool-btn.kbv-primary-btn { background: var(--kbv-primary); border-color: var(--kbv-primary); color: var(--kbv-primary-fg, #fff); }\n    .kbv-toolbar select {\n      padding: 7px 10px; border: 1px solid var(--kbv-border); border-radius: var(--kbv-radius);\n      background: var(--kbv-card); outline: none; font-size: 13px;\n    }\n    .kbv-toolbar-spacer { flex: 1; }\n    .kbv-count-info { font-size: 12px; color: var(--kbv-muted-fg); }\n\n    /* ---------- tag picker dropdown ---------- */\n    .kbv-tagpick-wrap { position: relative; }\n    .kbv-tagpick-menu {\n      position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;\n      background: var(--kbv-card); border: 1px solid var(--kbv-border);\n      border-radius: var(--kbv-radius); box-shadow: 0 8px 28px rgba(0,0,0,0.14);\n      min-width: 240px; max-height: 320px; overflow-y: auto; padding: 6px;\n      display: none;\n    }\n    .kbv-tagpick-menu.kbv-open { display: block; }\n    .kbv-tagpick-item {\n      display: flex; align-items: center; gap: 9px; padding: 7px 9px;\n      border-radius: 7px; cursor: pointer; font-size: 13.5px;\n    }\n    .kbv-tagpick-item:hover { background: var(--kbv-hover); }\n    .kbv-tagpick-item input { pointer-events: none; }\n    .kbv-tag-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }\n\n    /* ---------- board ---------- */\n    .kbv-board {\n      display: flex; gap: 12px; align-items: flex-start;\n      padding: 14px 16px; overflow-x: auto; min-height: 420px;\n    }\n    .kbv-col {\n      width: 270px; min-width: 270px; border-radius: var(--kbv-radius);\n      background: var(--kbv-card); border: 1px solid var(--kbv-border-soft);\n      display: flex; flex-direction: column; max-height: 640px;\n    }\n    .kbv-col-header {\n      display: flex; align-items: center; gap: 8px; padding: 10px 12px;\n      border-bottom: 2px solid var(--kbv-col-color, var(--kbv-border-soft));\n      font-size: 13.5px; font-weight: 800;\n    }\n    .kbv-col-count {\n      margin-left: auto; font-size: 11.5px; font-weight: 700; color: var(--kbv-muted-fg);\n      background: var(--kbv-secondary); border-radius: 999px; padding: 1px 8px;\n    }\n    .kbv-col-cards { flex: 1; overflow-y: auto; padding: 10px; min-height: 60px; }\n    .kbv-col.kbv-dragover { outline: 2px dashed var(--kbv-primary); outline-offset: -4px; }\n\n    .kbv-card-item {\n      background: var(--kbv-bg); border: 1px solid var(--kbv-border-soft);\n      border-radius: 8px; padding: 9px 11px; margin-bottom: 8px;\n      cursor: grab; transition: box-shadow 120ms ease, opacity 120ms ease;\n    }\n    .kbv-card-item:hover { box-shadow: 0 3px 10px rgba(0,0,0,0.09); }\n    .kbv-card-item.kbv-dragging { opacity: 0.4; }\n    .kbv-card-item.kbv-saving { opacity: 0.55; pointer-events: none; }\n    .kbv-card-name { font-size: 13.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .kbv-card-sub { font-size: 12px; color: var(--kbv-muted-fg); margin-top: 2px; display: flex; gap: 8px; flex-wrap: wrap; }\n    .kbv-card-tags { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }\n    .kbv-mini-tag {\n      font-size: 10px; padding: 1px 7px; border-radius: 999px; font-weight: 700;\n      background: var(--kbv-secondary); color: var(--kbv-fg2);\n    }\n    .kbv-col-empty { font-size: 12px; color: var(--kbv-muted-fg); text-align: center; padding: 18px 6px; }\n\n    /* ---------- modal ---------- */\n    .kbv-modal-backdrop {\n      position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,0.35);\n      display: none; align-items: center; justify-content: center;\n    }\n    .kbv-modal-backdrop.kbv-open { display: flex; }\n    .kbv-modal {\n      background: var(--kbv-card); color: var(--kbv-fg);\n      border-radius: 12px; padding: 20px 22px; width: 360px; max-width: calc(100vw - 40px);\n      box-shadow: 0 12px 48px rgba(0,0,0,0.25);\n    }\n    .kbv-modal-title { font-size: 16px; font-weight: 800; margin-bottom: 14px; }\n    .kbv-field-label { font-size: 12.5px; font-weight: 700; margin: 10px 0 4px; }\n    .kbv-modal input[type=text] {\n      width: 100%; padding: 8px 11px; border: 1px solid var(--kbv-border);\n      border-radius: 8px; outline: none; background: var(--kbv-bg);\n    }\n    .kbv-modal input[type=text]:focus { border-color: var(--kbv-primary); }\n    .kbv-color-row { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 4px; }\n    .kbv-color-swatch {\n      width: 26px; height: 26px; border-radius: 50%; cursor: pointer; border: 2px solid transparent;\n    }\n    .kbv-color-swatch.kbv-picked { border-color: var(--kbv-fg); }\n    .kbv-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }\n    .kbv-modal-error { color: var(--kbv-danger); font-size: 12.5px; margin-top: 8px; }\n\n    /* ---------- toast ---------- */\n    .kbv-toast {\n      position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 80;\n      background: var(--kbv-fg); color: var(--kbv-card); font-size: 13.5px; font-weight: 600;\n      padding: 10px 18px; border-radius: 999px; box-shadow: 0 6px 24px rgba(0,0,0,0.25);\n      opacity: 0; pointer-events: none; transition: opacity 200ms ease;\n    }\n    .kbv-toast.kbv-show { opacity: 1; }\n\n    .kbv-loading { display: flex; align-items: center; gap: 10px; color: var(--kbv-muted-fg); font-size: 13px; padding: 40px 0; justify-content: center; width: 100%; }\n    .kbv-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--kbv-border); border-top-color: var(--kbv-primary);\n      animation: kbv-spin 0.8s linear infinite;\n    }\n    @keyframes kbv-spin { to { transform: rotate(360deg); } }\n    .kbv-error-banner {\n      margin: 16px; padding: 12px 16px; border-radius: var(--kbv-radius);\n      background: color-mix(in srgb, var(--kbv-danger) 8%, var(--kbv-card));\n      border: 1px solid color-mix(in srgb, var(--kbv-danger) 30%, transparent);\n      color: var(--kbv-danger); font-size: 13.5px;\n    }\n  </style>\n\n  <div class=\"kbv-toolbar\">\n    <div class=\"kbv-toolbar-title\"><i class=\"fas fa-columns\"></i> Kanban</div>\n\n    <div class=\"kbv-tagpick-wrap\">\n      <button class=\"kbv-tool-btn\" id=\"kbv-tagpick-btn\"><i class=\"fas fa-tags\"></i> <span id=\"kbv-tagpick-label\">Taggar</span> <i class=\"fas fa-caret-down\"></i></button>\n      <div class=\"kbv-tagpick-menu\" id=\"kbv-tagpick-menu\"></div>\n    </div>\n\n    <select id=\"kbv-count\" title=\"Antal kontakter att ladda per tagg\">\n      <option value=\"25\">25 per tagg</option>\n      <option value=\"50\" selected>50 per tagg</option>\n      <option value=\"100\">100 per tagg</option>\n      <option value=\"250\">250 per tagg</option>\n      <option value=\"500\">500 per tagg</option>\n    </select>\n\n    <button class=\"kbv-tool-btn\" id=\"kbv-reload\"><i class=\"fas fa-sync-alt\"></i> Uppdatera</button>\n    <button class=\"kbv-tool-btn kbv-primary-btn\" id=\"kbv-new-tag\" style=\"display:none;\"><i class=\"fas fa-plus\"></i> Ny tagg</button>\n\n    <div class=\"kbv-toolbar-spacer\"></div>\n    <div class=\"kbv-count-info\" id=\"kbv-count-info\"></div>\n  </div>\n\n  <div id=\"kbv-board-wrap\">\n    <div class=\"kbv-loading\"><div class=\"kbv-spinner\"></div> Laddar…</div>\n  </div>\n\n  <div class=\"kbv-modal-backdrop\" id=\"kbv-modal-backdrop\">\n    <div class=\"kbv-modal\">\n      <div class=\"kbv-modal-title\">Ny kontakttagg</div>\n      <div class=\"kbv-field-label\">Namn</div>\n      <input type=\"text\" id=\"kbv-tag-name\" placeholder=\"T.ex. Uppföljning\">\n      <div class=\"kbv-field-label\">Färg</div>\n      <div class=\"kbv-color-row\" id=\"kbv-color-row\"></div>\n      <div class=\"kbv-modal-error\" id=\"kbv-modal-error\"></div>\n      <div class=\"kbv-modal-actions\">\n        <button class=\"kbv-tool-btn\" id=\"kbv-modal-cancel\">Avbryt</button>\n        <button class=\"kbv-tool-btn kbv-primary-btn\" id=\"kbv-modal-save\"><i class=\"fas fa-plus\"></i> Skapa</button>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"kbv-toast\" id=\"kbv-toast\"></div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var SETTINGS_KEY = 'kbv-settings';\n      var NO_TAG = '__none__';\n      var COLORS = ['#1565C0', '#0288D1', '#00897B', '#2E7D32', '#F9A825', '#EF6C00', '#E53935', '#8E24AA', '#5E35B1', '#546E7A'];\n\n      if (window.__kbvCleanup) { try { window.__kbvCleanup(); } catch (e) {} }\n\n      var state = {\n        me: null,\n        tags: [],\n        selectedTagIds: null,   /* null until settings/tags loaded */\n        count: 50,\n        contacts: [],\n        nameFields: null,\n        phoneFieldIds: [],\n        loading: true,\n        fatalError: null,\n        pickedColor: COLORS[0],\n        dragContactId: null\n      };\n\n      /* ---------- utils ---------- */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(method, path, params, body) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null && params[k] !== ''; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += '?' + qs;\n        }\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) {\n          opts.headers['Content-Type'] = 'application/json';\n          opts.body = JSON.stringify(body);\n        }\n        return fetch(url, opts).then(function (r) {\n          if (!r.ok) {\n            var err = new Error('HTTP ' + r.status);\n            err.status = r.status;\n            throw err;\n          }\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n\n      function loadSettings() {\n        try {\n          var raw = localStorage.getItem(SETTINGS_KEY);\n          if (raw) {\n            var s = JSON.parse(raw);\n            if (s && typeof s === 'object') {\n              if (Array.isArray(s.tagIds)) state.selectedTagIds = s.tagIds;\n              if (s.count) state.count = s.count;\n            }\n          }\n        } catch (e) {}\n      }\n      function saveSettings() {\n        try {\n          localStorage.setItem(SETTINGS_KEY, JSON.stringify({ tagIds: state.selectedTagIds, count: state.count }));\n        } catch (e) {}\n      }\n\n      var toastTimer = null;\n      function toast(text) {\n        var t = el('kbv-toast');\n        if (!t) return;\n        t.textContent = text;\n        t.className = 'kbv-toast kbv-show';\n        clearTimeout(toastTimer);\n        toastTimer = setTimeout(function () { t.className = 'kbv-toast'; }, 3200);\n      }\n\n      /* ---------- contact display name (label-matched fields) ---------- */\n      var FIRST_LABELS = ['förnamn', 'first name', 'firstname'];\n      var LAST_LABELS = ['efternamn', 'last name', 'lastname', 'surname'];\n      var FULL_LABELS = ['namn', 'fullständigt namn', 'full name', 'fullname', 'name'];\n      var PHONE_LABELS = ['telefonnummer', 'telefon', 'mobil', 'mobilnummer', 'phone', 'phone number', 'mobile'];\n      var COMPANY_LABELS = ['företag', 'företagsnamn', 'company', 'bolag'];\n\n      function loadFields() {\n        return api('GET', '/contacts/fields-v1').then(function (result) {\n          var ids = { first: [], last: [], full: [], phone: [], company: [] };\n          ((result && result.fields) || []).forEach(function (f) {\n            var label = String(f.label || '').toLowerCase().trim();\n            if (FIRST_LABELS.indexOf(label) !== -1) ids.first.push(f.id);\n            else if (LAST_LABELS.indexOf(label) !== -1) ids.last.push(f.id);\n            else if (FULL_LABELS.indexOf(label) !== -1) ids.full.push(f.id);\n            else if (PHONE_LABELS.indexOf(label) !== -1) ids.phone.push(f.id);\n            else if (COMPANY_LABELS.indexOf(label) !== -1) ids.company.push(f.id);\n          });\n          state.nameFields = ids;\n        });\n      }\n\n      function fieldValue(contact, fieldIds) {\n        for (var i = 0; i < fieldIds.length; i++) {\n          for (var j = 0; j < (contact.fields || []).length; j++) {\n            var f = contact.fields[j];\n            if (f.fieldId === fieldIds[i] && f.value) return f.value;\n          }\n        }\n        return '';\n      }\n\n      function contactName(contact) {\n        var ids = state.nameFields || { first: [], last: [], full: [], phone: [], company: [] };\n        var full = fieldValue(contact, ids.full);\n        if (full) return full;\n        var first = fieldValue(contact, ids.first);\n        var last = fieldValue(contact, ids.last);\n        var name = (first + ' ' + last).trim();\n        if (name) return name;\n        var company = fieldValue(contact, ids.company);\n        if (company) return company;\n        return 'Kontakt #' + (contact.serialId != null ? contact.serialId : '?');\n      }\n\n      function contactPhone(contact) {\n        var ids = state.nameFields || { phone: [] };\n        return fieldValue(contact, ids.phone || []);\n      }\n\n      /* ---------- data ---------- */\n      function load() {\n        state.loading = true;\n        state.fatalError = null;\n        renderBoard();\n        var mePromise = state.me ? Promise.resolve(state.me) : api('GET', '/users/users-v1/me');\n        return Promise.all([\n          mePromise,\n          api('GET', '/contacts/tags-v1'),\n          state.nameFields ? Promise.resolve() : loadFields()\n        ]).then(function (results) {\n          state.me = results[0];\n          state.tags = ((results[1] || {}).tags) || [];\n          var newTagBtn = el('kbv-new-tag');\n          if (newTagBtn) newTagBtn.style.display = state.me.type === 'admin' ? '' : 'none';\n          if (state.selectedTagIds === null) {\n            /* Default: first 4 tags. */\n            state.selectedTagIds = state.tags.slice(0, 4).map(function (t) { return t.id; });\n          }\n          /* Drop selections for tags that no longer exist. */\n          var known = {};\n          state.tags.forEach(function (t) { known[t.id] = true; });\n          state.selectedTagIds = state.selectedTagIds.filter(function (id) { return known[id]; });\n          return loadContacts();\n        }).catch(function (err) {\n          state.loading = false;\n          state.fatalError = err && err.status === 403\n            ? 'Du saknar rättigheten att se alla kontakter (contacts.getAll) som krävs för denna vy.'\n            : 'Kunde inte ladda data. Försök igen.';\n          renderBoard();\n        });\n      }\n\n      function loadContacts() {\n        /* One request per selected tag, so each column shows the LATEST\n           `count` contacts for its own tag. A single shared fetch\n           (tagIds=a,b,c) only returns the overall-latest window, which\n           starves columns whose tag is rare. */\n        var selected = state.selectedTagIds || [];\n        var requests;\n        if (selected.length) {\n          requests = selected.map(function (tagId) {\n            return api('GET', '/contacts/contacts-v1', {\n              offset: 0,\n              count: state.count,\n              sortBy: 'serialId',\n              sortOrder: 'desc',\n              tagIds: tagId\n            });\n          });\n        } else {\n          requests = [api('GET', '/contacts/contacts-v1', {\n            offset: 0,\n            count: state.count,\n            sortBy: 'serialId',\n            sortOrder: 'desc'\n          })];\n        }\n        return Promise.all(requests).then(function (lists) {\n          var seen = {};\n          var merged = [];\n          lists.forEach(function (list) {\n            (list || []).forEach(function (c) {\n              if (!seen[c.id]) {\n                seen[c.id] = true;\n                merged.push(c);\n              }\n            });\n          });\n          merged.sort(function (a, b) { return (b.serialId || 0) - (a.serialId || 0); });\n          state.contacts = merged;\n          state.loading = false;\n          renderAll();\n        }).catch(function (err) {\n          state.loading = false;\n          state.fatalError = err && err.status === 403\n            ? 'Du saknar rättigheten att se alla kontakter (contacts.getAll) som krävs för denna vy.'\n            : 'Kunde inte ladda kontakter.';\n          renderBoard();\n        });\n      }\n\n      /* ---------- render ---------- */\n      function tagById(id) {\n        for (var i = 0; i < state.tags.length; i++) if (state.tags[i].id === id) return state.tags[i];\n        return null;\n      }\n\n      function renderAll() {\n        renderTagPicker();\n        renderBoard();\n        var count = el('kbv-count');\n        if (count) count.value = String(state.count);\n      }\n\n      function renderTagPicker() {\n        var menu = el('kbv-tagpick-menu');\n        var label = el('kbv-tagpick-label');\n        if (label) {\n          var n = (state.selectedTagIds || []).length;\n          label.textContent = n ? n + ' taggar' : 'Alla taggar';\n        }\n        if (!menu) return;\n        var html = '';\n        state.tags.forEach(function (t) {\n          var checked = state.selectedTagIds && state.selectedTagIds.indexOf(t.id) !== -1 ? ' checked' : '';\n          html += '<div class=\"kbv-tagpick-item\" data-kbv-tagpick=\"' + esc(t.id) + '\">' +\n            '<input type=\"checkbox\"' + checked + '>' +\n            '<span class=\"kbv-tag-dot\" style=\"background:' + esc(t.color || '#9E9E9E') + '\"></span>' +\n            '<span>' + esc(t.name) + '</span>' +\n            '</div>';\n        });\n        if (!state.tags.length) html = '<div class=\"kbv-col-empty\">Inga taggar finns ännu.</div>';\n        menu.innerHTML = html;\n      }\n\n      function renderBoard() {\n        var wrap = el('kbv-board-wrap');\n        if (!wrap) return;\n        if (state.loading) {\n          wrap.innerHTML = '<div class=\"kbv-loading\"><div class=\"kbv-spinner\"></div> Laddar…</div>';\n          return;\n        }\n        if (state.fatalError) {\n          wrap.innerHTML = '<div class=\"kbv-error-banner\"><i class=\"fas fa-exclamation-triangle\"></i> ' + esc(state.fatalError) + '</div>';\n          return;\n        }\n\n        var columns = [];\n        (state.selectedTagIds || []).forEach(function (id) {\n          var t = tagById(id);\n          if (t) columns.push({ id: t.id, name: t.name, color: t.color || '#9E9E9E', contacts: [] });\n        });\n        var noTagCol = { id: NO_TAG, name: 'Utan vald tagg', color: '#9E9E9E', contacts: [] };\n\n        state.contacts.forEach(function (c) {\n          var placed = false;\n          for (var i = 0; i < columns.length; i++) {\n            if ((c.tagIds || []).indexOf(columns[i].id) !== -1) {\n              columns[i].contacts.push(c);\n              placed = true;\n              break;\n            }\n          }\n          if (!placed) noTagCol.contacts.push(c);\n        });\n\n        /* Only show the \"no tag\" column when it is meaningful:\n           when nothing is filtered server-side or contacts slipped through. */\n        var showNoTag = !(state.selectedTagIds && state.selectedTagIds.length) || noTagCol.contacts.length;\n        var allColumns = showNoTag ? columns.concat([noTagCol]) : columns;\n\n        var html = '<div class=\"kbv-board\">';\n        allColumns.forEach(function (col) {\n          html += '<div class=\"kbv-col\" data-kbv-col=\"' + esc(col.id) + '\" style=\"--kbv-col-color:' + esc(col.color) + '\">' +\n            '<div class=\"kbv-col-header\">' +\n            '<span class=\"kbv-tag-dot\" style=\"background:' + esc(col.color) + '\"></span>' +\n            '<span>' + esc(col.name) + '</span>' +\n            '<span class=\"kbv-col-count\">' + col.contacts.length + '</span>' +\n            '</div>' +\n            '<div class=\"kbv-col-cards\">';\n          if (!col.contacts.length) {\n            html += '<div class=\"kbv-col-empty\">Släpp kontakter här</div>';\n          }\n          col.contacts.forEach(function (c) {\n            var phone = contactPhone(c);\n            var extraTags = '';\n            (c.tagIds || []).forEach(function (tid) {\n              if (tid === col.id) return;\n              var t = tagById(tid);\n              if (t) extraTags += '<span class=\"kbv-mini-tag\" style=\"background:color-mix(in srgb, ' + esc(t.color || '#9E9E9E') + ' 18%, transparent);color:' + esc(t.color || 'inherit') + ';\">' + esc(t.name) + '</span>';\n            });\n            html += '<div class=\"kbv-card-item\" draggable=\"true\" data-kbv-contact=\"' + esc(c.id) + '\">' +\n              '<div class=\"kbv-card-name\">' + esc(contactName(c)) + '</div>' +\n              '<div class=\"kbv-card-sub\">' +\n              (phone ? '<span><i class=\"fas fa-phone-alt\" style=\"font-size:10px;\"></i> ' + esc(phone) + '</span>' : '') +\n              '<span>#' + esc(c.serialId) + '</span>' +\n              '</div>' +\n              (extraTags ? '<div class=\"kbv-card-tags\">' + extraTags + '</div>' : '') +\n              '</div>';\n          });\n          html += '</div></div>';\n        });\n        html += '</div>';\n        wrap.innerHTML = html;\n\n        var info = el('kbv-count-info');\n        if (info) info.textContent = state.contacts.length + ' kontakter laddade';\n      }\n\n      /* ---------- tag creation modal ---------- */\n      function renderColorRow() {\n        var row = el('kbv-color-row');\n        if (!row) return;\n        var html = '';\n        COLORS.forEach(function (c) {\n          html += '<div class=\"kbv-color-swatch' + (state.pickedColor === c ? ' kbv-picked' : '') + '\" data-kbv-color=\"' + c + '\" style=\"background:' + c + '\"></div>';\n        });\n        row.innerHTML = html;\n      }\n\n      function openModal() {\n        state.pickedColor = COLORS[0];\n        renderColorRow();\n        var e = el('kbv-modal-error'); if (e) e.textContent = '';\n        var n = el('kbv-tag-name'); if (n) n.value = '';\n        var b = el('kbv-modal-backdrop'); if (b) b.className = 'kbv-modal-backdrop kbv-open';\n        if (n) setTimeout(function () { n.focus(); }, 50);\n      }\n      function closeModal() {\n        var b = el('kbv-modal-backdrop'); if (b) b.className = 'kbv-modal-backdrop';\n      }\n\n      function createTag() {\n        var nameInput = el('kbv-tag-name');\n        var name = nameInput ? nameInput.value.trim() : '';\n        var errEl = el('kbv-modal-error');\n        if (!name) {\n          if (errEl) errEl.textContent = 'Ange ett namn på taggen.';\n          return;\n        }\n        api('POST', '/contacts/tags-v1', null, {\n          name: name,\n          color: state.pickedColor,\n          isDefault: false,\n          isLocking: false,\n          position: Date.now()\n        }).then(function (result) {\n          closeModal();\n          toast('Taggen \"' + name + '\" skapades');\n          if (result && result.tagId) {\n            state.selectedTagIds = (state.selectedTagIds || []).concat([result.tagId]);\n            saveSettings();\n          }\n          return load();\n        }).catch(function (err) {\n          if (errEl) {\n            errEl.textContent = err && err.status === 403\n              ? 'Endast administratörer kan skapa taggar.'\n              : 'Kunde inte skapa taggen.';\n          }\n        });\n      }\n\n      /* ---------- drag & drop ---------- */\n      function contactById(id) {\n        for (var i = 0; i < state.contacts.length; i++) if (state.contacts[i].id === id) return state.contacts[i];\n        return null;\n      }\n\n      function moveContact(contactId, targetColId) {\n        var contact = contactById(contactId);\n        if (!contact) return;\n\n        var selected = state.selectedTagIds || [];\n        var oldTagIds = (contact.tagIds || []).slice();\n\n        /* Remove all *column* tags, keep unrelated tags, then add the target tag. */\n        var next = oldTagIds.filter(function (id) { return selected.indexOf(id) === -1; });\n        if (targetColId !== NO_TAG) {\n          if (next.indexOf(targetColId) === -1) next.push(targetColId);\n        }\n\n        if (String(next.slice().sort()) === String(oldTagIds.slice().sort())) return;\n\n        contact.tagIds = next;\n        renderBoard();\n        var cardEl = document.querySelector('[data-kbv-contact=\"' + contactId + '\"]');\n        if (cardEl) cardEl.className += ' kbv-saving';\n\n        api('PUT', '/contacts/contacts-v1/' + contactId, null, { tagIds: next }).then(function () {\n          renderBoard();\n        }).catch(function (err) {\n          contact.tagIds = oldTagIds;\n          renderBoard();\n          if (err && err.status === 403) {\n            toast('Kunde inte flytta: kontakten är låst eller rättighet saknas');\n          } else {\n            toast('Kunde inte flytta kontakten');\n          }\n        });\n      }\n\n      /* ---------- events ---------- */\n      function inRoot(node) {\n        var root = document.getElementById('kbv-root');\n        return root && node && root.contains(node);\n      }\n\n      function onClick(event) {\n        var target = event.target;\n        if (!target.closest) return;\n\n        var tagpickBtn = target.closest('#kbv-tagpick-btn');\n        if (tagpickBtn && inRoot(tagpickBtn)) {\n          var menu = el('kbv-tagpick-menu');\n          if (menu) menu.className = menu.className.indexOf('kbv-open') !== -1 ? 'kbv-tagpick-menu' : 'kbv-tagpick-menu kbv-open';\n          return;\n        }\n\n        var tagpickItem = target.closest('[data-kbv-tagpick]');\n        if (tagpickItem && inRoot(tagpickItem)) {\n          var id = tagpickItem.getAttribute('data-kbv-tagpick');\n          var idx = (state.selectedTagIds || []).indexOf(id);\n          if (idx === -1) state.selectedTagIds.push(id);\n          else state.selectedTagIds.splice(idx, 1);\n          saveSettings();\n          renderTagPicker();\n          loadContacts();\n          return;\n        }\n\n        /* Click outside the tag menu closes it. */\n        var openMenu = el('kbv-tagpick-menu');\n        if (openMenu && openMenu.className.indexOf('kbv-open') !== -1 && !target.closest('.kbv-tagpick-wrap')) {\n          openMenu.className = 'kbv-tagpick-menu';\n        }\n\n        var reload = target.closest('#kbv-reload');\n        if (reload && inRoot(reload)) { load(); return; }\n\n        var newTag = target.closest('#kbv-new-tag');\n        if (newTag && inRoot(newTag)) { openModal(); return; }\n\n        var cancel = target.closest('#kbv-modal-cancel');\n        if (cancel) { closeModal(); return; }\n\n        var save = target.closest('#kbv-modal-save');\n        if (save) { createTag(); return; }\n\n        var swatch = target.closest('[data-kbv-color]');\n        if (swatch) {\n          state.pickedColor = swatch.getAttribute('data-kbv-color');\n          renderColorRow();\n          return;\n        }\n\n        var backdrop = target.closest('#kbv-modal-backdrop');\n        if (backdrop && target === backdrop) { closeModal(); return; }\n      }\n\n      function onChange(event) {\n        var count = event.target.closest ? event.target.closest('#kbv-count') : null;\n        if (count && inRoot(count)) {\n          state.count = parseInt(count.value, 10) || 50;\n          saveSettings();\n          loadContacts();\n        }\n      }\n\n      function onDragStart(event) {\n        var card = event.target.closest ? event.target.closest('[data-kbv-contact]') : null;\n        if (card && inRoot(card)) {\n          state.dragContactId = card.getAttribute('data-kbv-contact');\n          card.className += ' kbv-dragging';\n          try { event.dataTransfer.setData('text/plain', state.dragContactId); } catch (e) {}\n          event.dataTransfer.effectAllowed = 'move';\n        }\n      }\n\n      function onDragEnd(event) {\n        state.dragContactId = null;\n        document.querySelectorAll('.kbv-dragging').forEach(function (n) {\n          n.className = n.className.replace(' kbv-dragging', '');\n        });\n        document.querySelectorAll('.kbv-col.kbv-dragover').forEach(function (n) {\n          n.className = n.className.replace(' kbv-dragover', '');\n        });\n      }\n\n      function onDragOver(event) {\n        var col = event.target.closest ? event.target.closest('[data-kbv-col]') : null;\n        if (col && inRoot(col) && state.dragContactId) {\n          event.preventDefault();\n          event.dataTransfer.dropEffect = 'move';\n          if (col.className.indexOf('kbv-dragover') === -1) col.className += ' kbv-dragover';\n        }\n      }\n\n      function onDragLeave(event) {\n        var col = event.target.closest ? event.target.closest('[data-kbv-col]') : null;\n        if (col && !col.contains(event.relatedTarget)) {\n          col.className = col.className.replace(' kbv-dragover', '');\n        }\n      }\n\n      function onDrop(event) {\n        var col = event.target.closest ? event.target.closest('[data-kbv-col]') : null;\n        if (col && inRoot(col)) {\n          event.preventDefault();\n          var contactId = state.dragContactId;\n          try { contactId = event.dataTransfer.getData('text/plain') || contactId; } catch (e) {}\n          col.className = col.className.replace(' kbv-dragover', '');\n          if (contactId) moveContact(contactId, col.getAttribute('data-kbv-col'));\n          state.dragContactId = null;\n        }\n      }\n\n      function onKeyDown(event) {\n        if (event.key === 'Escape') closeModal();\n        if (event.key === 'Enter' && event.target && event.target.id === 'kbv-tag-name') createTag();\n      }\n\n      /* Contrast probe -- dark theme can resolve --color-primary to a LIGHT color,\n         so white text on primary is not a safe assumption. Measure and adapt. */\n      function applyContrast() {\n        var root = document.getElementById('kbv-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--kbv-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var kbvThemeObserver = new MutationObserver(applyContrast);\n      kbvThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      /* The app switches theme by rewriting a <style> tag in <head> - html/body attributes never change. */\n      kbvThemeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) kbvThemeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('change', onChange);\n      document.addEventListener('dragstart', onDragStart);\n      document.addEventListener('dragend', onDragEnd);\n      document.addEventListener('dragover', onDragOver);\n      document.addEventListener('dragleave', onDragLeave);\n      document.addEventListener('drop', onDrop);\n      document.addEventListener('keydown', onKeyDown);\n\n      window.__kbvCleanup = function () {\n        kbvThemeObserver.disconnect();\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('change', onChange);\n        document.removeEventListener('dragstart', onDragStart);\n        document.removeEventListener('dragend', onDragEnd);\n        document.removeEventListener('dragover', onDragOver);\n        document.removeEventListener('dragleave', onDragLeave);\n        document.removeEventListener('drop', onDrop);\n        document.removeEventListener('keydown', onKeyDown);\n      };\n\n      loadSettings();\n      load();\n    })();\n  </script>\n</div>","iconName":"columns","iconColor":"#5E35B1","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50b874fe660af530a7b318","name":"Kollegor","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["69a3068e4e630d4dc2855cd3"],"locations":[{"type":"navigation","position":0,"label":"Meddelanden"}],"html":"<div id=\"msv-root\">\n  <style>\n    #msv-root {\n      --msv-bg: var(--color-background-2, #fafafa);\n      --msv-card: var(--color-background-1, #ffffff);\n      --msv-secondary: var(--color-background-3, #eeeeee);\n      --msv-fg: var(--color-text-1, #212121);\n      --msv-muted-fg: var(--color-text-3, #757575);\n      --msv-primary: var(--color-primary, #1565C0);\n      --msv-primary-fg: #ffffff;\n      --msv-danger: var(--color-danger, #E53935);\n      --msv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --msv-border: color-mix(in srgb, var(--msv-fg) 13%, transparent);\n      --msv-border-soft: color-mix(in srgb, var(--msv-fg) 8%, transparent);\n      --msv-unread-tint: color-mix(in srgb, var(--msv-primary) 7%, transparent);\n      --msv-unread: #1EAEDB;\n      --msv-radius: 8px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--msv-fg);\n      display: block;\n      background: var(--msv-bg);\n      border: 1px solid var(--msv-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #msv-root * { box-sizing: border-box; }\n    #msv-root button { font-family: inherit; cursor: pointer; }\n    #msv-root input, #msv-root textarea { font-family: inherit; font-size: 14px; color: var(--msv-fg); }\n\n    .msv-app { display: flex; height: 100%; }\n\n    /* ---------- Left panel ---------- */\n    .msv-list-panel {\n      width: 320px; min-width: 260px; flex-shrink: 0;\n      display: flex; flex-direction: column;\n      border-right: 1px solid var(--msv-border-soft);\n      background: var(--msv-card);\n    }\n    .msv-list-header { padding: 12px 14px; border-bottom: 1px solid var(--msv-border-soft); }\n    .msv-icon-btn {\n      background: transparent; border: none; border-radius: 6px;\n      width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;\n      color: var(--msv-muted-fg);\n    }\n    .msv-icon-btn:hover { background: var(--msv-hover); color: var(--msv-fg); }\n    .msv-icon-btn:disabled { opacity: 0.5; cursor: default; }\n    .msv-search-wrap { position: relative; }\n    .msv-search-wrap svg.msv-search-icon {\n      position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n      width: 14px; height: 14px; color: var(--msv-muted-fg); pointer-events: none;\n    }\n    .msv-search-input {\n      width: 100%; padding: 8px 12px 8px 32px;\n      border: 1px solid var(--msv-border); border-radius: var(--msv-radius);\n      outline: none; background: var(--msv-bg);\n    }\n    .msv-search-input::placeholder { color: var(--msv-muted-fg); }\n    .msv-search-input:focus { border-color: var(--msv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--msv-primary) 18%, transparent); }\n    .msv-settings { padding: 12px 14px; border-bottom: 1px solid var(--msv-border-soft); background: var(--msv-bg); display: none; }\n    .msv-settings.msv-open { display: block; }\n    .msv-settings label { display: flex; align-items: center; gap: 7px; font-size: 13px; user-select: none; cursor: pointer; }\n    .msv-settings-hint { font-size: 12px; color: var(--msv-muted-fg); margin: 6px 0 0 23px; }\n\n    .msv-people { flex: 1; overflow-y: auto; }\n    .msv-section {\n      padding: 10px 14px 5px; font-size: 11px; font-weight: 700; text-transform: uppercase;\n      letter-spacing: 0.06em; color: var(--msv-muted-fg);\n    }\n    .msv-person {\n      padding: 10px 14px; border-bottom: 1px solid var(--msv-border-soft);\n      cursor: pointer; transition: background-color 120ms ease;\n      display: flex; align-items: center; gap: 11px;\n    }\n    .msv-person:hover { background: var(--msv-hover); }\n    .msv-person.msv-active { background: var(--msv-secondary); }\n    .msv-person.msv-unread { background: var(--msv-unread-tint); }\n    .msv-person.msv-unread:hover { background: color-mix(in srgb, var(--msv-primary) 11%, transparent); }\n    .msv-avatar {\n      width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0;\n      display: inline-flex; align-items: center; justify-content: center;\n      font-size: 14px; font-weight: 700; color: var(--msv-primary-fg);\n      background: var(--msv-primary);\n    }\n    .msv-person-body { min-width: 0; flex: 1; }\n    .msv-person-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }\n    .msv-person-name { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .msv-person.msv-unread .msv-person-name { font-weight: 800; }\n    .msv-person-time { font-size: 11px; color: var(--msv-muted-fg); white-space: nowrap; flex-shrink: 0; }\n    .msv-person-bottom { display: flex; align-items: center; gap: 6px; margin-top: 1px; }\n    .msv-person-preview {\n      font-size: 13px; color: var(--color-text-2, #616161); flex: 1;\n      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n    }\n    .msv-person.msv-unread .msv-person-preview { color: var(--msv-fg); font-weight: 600; }\n    .msv-dot {\n      width: 8px; height: 8px; border-radius: 50%; background: var(--msv-unread);\n      flex-shrink: 0; animation: msv-pulse 1.6s ease-in-out infinite;\n    }\n    @keyframes msv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }\n    .msv-checkcheck { width: 14px; height: 14px; flex-shrink: 0; color: var(--msv-muted-fg); }\n    .msv-checkcheck.msv-seen { color: var(--msv-unread); }\n    .msv-empty-list { padding: 42px 20px; text-align: center; color: var(--msv-muted-fg); }\n    .msv-empty-list h3 { margin: 0 0 4px; font-size: 15px; color: var(--msv-fg); }\n    .msv-empty-list p { margin: 0; font-size: 13px; }\n    .msv-loading { padding: 20px 16px; color: var(--msv-muted-fg); font-size: 13px; display: flex; align-items: center; gap: 8px; }\n    .msv-error-box { padding: 14px 16px; color: var(--msv-danger); font-size: 13px; word-break: break-word; }\n\n    /* ---------- Thread panel ---------- */\n    .msv-thread-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: var(--msv-bg); }\n    .msv-thread-header {\n      padding: 11px 16px; border-bottom: 1px solid var(--msv-border-soft);\n      display: flex; align-items: center; justify-content: space-between; gap: 10px;\n      background: var(--msv-card);\n    }\n    .msv-thread-header-left { display: flex; align-items: center; gap: 11px; min-width: 0; }\n    .msv-thread-title { min-width: 0; }\n    .msv-thread-title h2 { margin: 0; font-size: 15px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .msv-thread-title p { margin: 0; font-size: 12px; color: var(--msv-muted-fg); min-height: 15px; }\n    .msv-thread-title p.msv-typing { color: var(--msv-primary); font-weight: 600; }\n\n    .msv-messages { flex: 1; overflow-y: auto; padding: 16px; }\n    .msv-date-sep { display: flex; justify-content: center; margin: 14px 0 10px; }\n    .msv-date-sep span {\n      font-size: 11px; color: var(--msv-muted-fg); background: var(--msv-card);\n      border: 1px solid var(--msv-border-soft); padding: 2px 10px; border-radius: 999px;\n    }\n    .msv-msg-row { display: flex; margin-bottom: 10px; }\n    .msv-msg-row.msv-out { justify-content: flex-end; }\n    .msv-msg { max-width: 74%; }\n    .msv-bubble {\n      padding: 9px 13px; border-radius: 16px; font-size: 14px; line-height: 1.45;\n      white-space: pre-wrap; word-break: break-word;\n    }\n    .msv-msg-row.msv-in .msv-bubble {\n      background: var(--msv-card); border: 1px solid var(--msv-border-soft);\n      border-bottom-left-radius: 5px; color: var(--msv-fg);\n    }\n    .msv-msg-row.msv-out .msv-bubble {\n      background: var(--msv-primary); color: var(--msv-primary-fg);\n      border-bottom-right-radius: 5px;\n    }\n    .msv-msg-meta { font-size: 11px; color: var(--msv-muted-fg); margin-top: 3px; display: flex; gap: 5px; align-items: center; }\n    .msv-msg-row.msv-out .msv-msg-meta { justify-content: flex-end; }\n    .msv-msg-meta .msv-error { color: var(--msv-danger); font-weight: 600; }\n\n    .msv-composer { padding: 12px 16px; border-top: 1px solid var(--msv-border-soft); background: var(--msv-card); }\n    .msv-composer-row { display: flex; gap: 9px; align-items: flex-end; }\n    .msv-composer textarea {\n      flex: 1; resize: none; min-height: 40px; max-height: 130px;\n      border: 1px solid var(--msv-border); border-radius: var(--msv-radius);\n      padding: 9px 12px; outline: none; background: var(--msv-bg); line-height: 1.4;\n    }\n    .msv-composer textarea::placeholder { color: var(--msv-muted-fg); }\n    .msv-composer textarea:focus { border-color: var(--msv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--msv-primary) 18%, transparent); }\n    .msv-send-btn {\n      background: var(--msv-primary); color: var(--msv-primary-fg); border: none;\n      border-radius: var(--msv-radius); width: 40px; height: 40px; flex-shrink: 0;\n      display: inline-flex; align-items: center; justify-content: center;\n    }\n    .msv-send-btn i { font-size: 15px; }\n    .msv-send-btn:hover { background: color-mix(in srgb, var(--msv-primary) 88%, #000); }\n    .msv-send-btn:disabled { opacity: 0.5; cursor: default; }\n    .msv-composer-hint { font-size: 11px; color: var(--msv-muted-fg); margin-top: 5px; }\n\n    .msv-thread-empty {\n      flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;\n      color: var(--msv-muted-fg); text-align: center; padding: 24px;\n    }\n    .msv-thread-empty .msv-empty-circle {\n      background: var(--msv-secondary); border-radius: 50%; padding: 22px; margin-bottom: 14px;\n    }\n    .msv-thread-empty svg { width: 40px; height: 40px; color: var(--msv-primary); display: block; }\n    .msv-thread-empty h3 { margin: 0 0 4px; font-size: 16px; color: var(--msv-fg); }\n    .msv-thread-empty p { margin: 0; font-size: 13px; max-width: 340px; }\n\n    .msv-live {\n      display: inline-flex; align-items: center; gap: 5px; font-size: 11px;\n      color: var(--msv-muted-fg); white-space: nowrap;\n    }\n    .msv-live .msv-live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--msv-muted-fg); }\n    .msv-live.msv-on .msv-live-dot { background: #43A047; }\n\n    .msv-spin { animation: msv-rotate 0.9s linear infinite; }\n    @keyframes msv-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n\n    .msv-people::-webkit-scrollbar, .msv-messages::-webkit-scrollbar { width: 8px; }\n    .msv-people::-webkit-scrollbar-thumb, .msv-messages::-webkit-scrollbar-thumb {\n      background: color-mix(in srgb, var(--msv-fg) 20%, transparent); border-radius: 4px;\n    }\n    .msv-people::-webkit-scrollbar-track, .msv-messages::-webkit-scrollbar-track { background: transparent; }\n\n    @media (max-width: 760px) {\n      .msv-list-panel { width: 100%; }\n      .msv-app.msv-mobile-thread .msv-list-panel { display: none; }\n      .msv-app:not(.msv-mobile-thread) .msv-thread-panel { display: none; }\n      .msv-back-btn { display: inline-flex !important; }\n    }\n    .msv-back-btn { display: none; }\n  </style>\n\n  <div class=\"msv-app\" id=\"msv-app\">\n    <aside class=\"msv-list-panel\">\n      <div class=\"msv-list-header\">\n        <div style=\"display:flex;align-items:center;gap:4px;\">\n          <div class=\"msv-search-wrap\" style=\"flex:1;\">\n            <svg class=\"msv-search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line></svg>\n            <input type=\"search\" class=\"msv-search-input\" id=\"msv-search\" placeholder=\"S&#246;k kollegor...\" autocomplete=\"off\">\n          </div>\n          <button class=\"msv-icon-btn\" id=\"msv-settings-btn\" title=\"Inst&#228;llningar\">\n            <i class=\"fas fa-cog\" style=\"font-size:14px;\"></i>\n          </button>\n          <button class=\"msv-icon-btn\" id=\"msv-refresh-btn\" title=\"Uppdatera\">\n            <svg id=\"msv-refresh-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-2.64-6.36\"></path><polyline points=\"21 3 21 9 15 9\"></polyline></svg>\n          </button>\n        </div>\n      </div>\n      <div class=\"msv-settings\" id=\"msv-settings-panel\">\n        <label>\n          <input type=\"checkbox\" id=\"msv-ping-toggle\"> Notifiera mottagaren i SaleSys\n        </label>\n        <div class=\"msv-settings-hint\">Skickar en SaleSys-notis till kollegan n&#228;r du skriver (h&#246;gst en per kollega var tredje minut).</div>\n      </div>\n      <div class=\"msv-people\" id=\"msv-people\">\n        <div class=\"msv-loading\">\n          <svg class=\"msv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg>\n          H&#228;mtar kollegor...\n        </div>\n      </div>\n      <div style=\"padding:8px 14px;border-top:1px solid var(--msv-border-soft);\">\n        <span class=\"msv-live\" id=\"msv-live\"><span class=\"msv-live-dot\"></span><span id=\"msv-live-text\">Ansluter...</span></span>\n      </div>\n    </aside>\n\n    <main class=\"msv-thread-panel\">\n      <div class=\"msv-thread-empty\" id=\"msv-thread-empty\">\n        <div class=\"msv-empty-circle\">\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"></path></svg>\n        </div>\n        <h3>Ingen chatt vald</h3>\n        <p>V&#228;lj en kollega i listan f&#246;r att b&#246;rja chatta</p>\n      </div>\n\n      <div id=\"msv-thread\" style=\"display:none; flex:1; flex-direction:column; min-height:0;\">\n        <div class=\"msv-thread-header\">\n          <div class=\"msv-thread-header-left\">\n            <button class=\"msv-icon-btn msv-back-btn\" id=\"msv-back-btn\" title=\"Tillbaka\">\n              <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 18 9 12 15 6\"></polyline></svg>\n            </button>\n            <span class=\"msv-avatar\" id=\"msv-thread-avatar\"></span>\n            <div class=\"msv-thread-title\">\n              <h2 id=\"msv-thread-name\"></h2>\n              <p id=\"msv-thread-status\"></p>\n            </div>\n          </div>\n          <button class=\"msv-icon-btn\" id=\"msv-thread-refresh\" title=\"Uppdatera chatt\">\n            <svg id=\"msv-thread-refresh-icon\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-2.64-6.36\"></path><polyline points=\"21 3 21 9 15 9\"></polyline></svg>\n          </button>\n        </div>\n        <div class=\"msv-messages\" id=\"msv-messages\"></div>\n        <div class=\"msv-composer\" id=\"msv-composer\">\n          <div class=\"msv-composer-row\">\n            <textarea id=\"msv-input\" rows=\"1\" placeholder=\"Skriv ett meddelande...\"></textarea>\n            <button class=\"msv-send-btn\" id=\"msv-send-btn\" title=\"Skicka\">\n              <i class=\"fas fa-paper-plane\"></i>\n            </button>\n          </div>\n          <div class=\"msv-composer-hint\">Enter f&#246;r att skicka, Shift+Enter f&#246;r ny rad</div>\n        </div>\n      </div>\n    </main>\n  </div>\n\n  <script>\n  (function () {\n    var API = 'https://app.salesys.se/api';\n    var WS_URL = 'wss://app.salesys.se/api/realtime/realtime-v1';\n    var POLL_MS = 15000;\n    var TYPING_SEND_MS = 2500;\n    var TYPING_SHOW_MS = 3000;\n    var PING_THROTTLE_MS = 180000;\n\n    function el(id) { return document.getElementById(id); }\n\n    function esc(str) {\n      return String(str == null ? '' : str)\n        .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n        .replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n    }\n\n    function showFatal(message) {\n      var container = el('msv-people');\n      if (container) {\n        container.innerHTML = '<div class=\"msv-error-box\">Vyn kunde inte starta: ' + esc(message) + '</div>';\n      }\n    }\n\n    /* Clean up any previous mount of this view (the app can re-render sections). */\n    if (window.__msvCleanup) {\n      try { window.__msvCleanup(); } catch (cleanupError) { /* ignore */ }\n      window.__msvCleanup = null;\n    }\n\n    /* ---------- Auth ---------- */\n    function getToken() {\n      var cookies = document.cookie.split(';').reduce(function (object, cookie) {\n        var parts = /(.+?)=(.+)/.exec(cookie.trim());\n        if (parts) object[parts[1]] = parts[2];\n        return object;\n      }, {});\n      var raw = cookies['s2_utoken'];\n      if (!raw) return null;\n      try { return decodeURIComponent(raw); } catch (decodeError) { return raw; }\n    }\n\n    function api(path, params) {\n      var qs = '';\n      if (params) {\n        var pairs = [];\n        Object.keys(params).forEach(function (key) {\n          if (params[key] === undefined || params[key] === null) return;\n          pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));\n        });\n        if (pairs.length) qs = '?' + pairs.join('&');\n      }\n      return fetch(API + path + qs, {\n        headers: { 'Authorization': 'Bearer ' + getToken() }\n      }).then(function (res) {\n        if (!res.ok) throw new Error('API ' + res.status + ' ' + path);\n        return res.json();\n      });\n    }\n\n    function apiSend(method, path, body) {\n      return fetch(API + path, {\n        method: method,\n        headers: {\n          'Authorization': 'Bearer ' + getToken(),\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(body)\n      }).then(function (res) {\n        if (!res.ok) throw new Error('API ' + res.status + ' ' + path);\n        return res.text().then(function (t) { try { return JSON.parse(t); } catch (parseError) { return null; } });\n      });\n    }\n\n    /* ---------- Helpers ---------- */\n    function sameDay(a, b) {\n      return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n    }\n\n    function fmtListDate(value) {\n      var d = new Date(value);\n      var now = new Date();\n      if (sameDay(d, now)) return d.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });\n      var yesterday = new Date(now.getTime() - 86400000);\n      if (sameDay(d, yesterday)) return 'Igår';\n      if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' });\n      return d.toLocaleDateString('sv-SE');\n    }\n\n    function fmtTime(value) {\n      return new Date(value).toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });\n    }\n\n    function fmtDateHeading(dateStr) {\n      return new Date(dateStr).toLocaleDateString('sv-SE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n    }\n\n    function initials(name) {\n      var parts = String(name || '').trim().split(/\\s+/);\n      if (!parts[0]) return '?';\n      var result = parts[0].charAt(0);\n      if (parts.length > 1) result += parts[parts.length - 1].charAt(0);\n      return result.toUpperCase();\n    }\n\n    function when(m) { return m.sentAt || m.createdAt; }\n\n    /* ---------- Settings ---------- */\n    function loadSettings() {\n      var s = { ping: true };\n      try {\n        var saved = JSON.parse(localStorage.getItem('msv-view-settings') || 'null') || {};\n        if (typeof saved.ping === 'boolean') s.ping = saved.ping;\n      } catch (e) { /* ignore */ }\n      return s;\n    }\n\n    function saveSettings() {\n      try { localStorage.setItem('msv-view-settings', JSON.stringify(state.settings)); } catch (e) { /* ignore */ }\n    }\n\n    /* ---------- State ---------- */\n    var state = {\n      me: null,               /* { id, name } */\n      users: {},              /* userId -> { id, name, active, type } */\n      sessions: {},           /* sessionId -> { id, partnerId, iAmLocal, isPartnerTyping } */\n      latest: {},             /* sessionId -> latest message */\n      active: null,           /* partnerId of open thread */\n      messages: [],\n      typingUntil: {},        /* partnerId -> timestamp until which \"skriver...\" shows */\n      typingTimer: null,\n      lastTypingSentAt: 0,\n      settings: loadSettings(),\n      sending: false,\n      pollTimer: null,\n      ws: null,\n      wsConnected: false,\n      wsReconnectTimer: null,\n      wsRetryMs: 2000,\n      destroyed: false\n    };\n\n    /* Direction is decided by who created the message, which works no matter\n       which side of the session we happen to be on. */\n    function isMine(m) { return !!(state.me && m.userId === state.me.id); }\n\n    /* Seen-by-me: list/thread responses carry requester-perspective\n       currentSeenAt; websocket payloads only have local/remote fields. */\n    function seenByMe(m, session) {\n      if (m.currentSeenAt !== undefined) return m.currentSeenAt;\n      if (!session) return m.localSeenAt || m.remoteSeenAt;\n      return session.iAmLocal ? m.localSeenAt : m.remoteSeenAt;\n    }\n\n    function seenByPartner(m, session) {\n      if (m.otherSeenAt !== undefined) return m.otherSeenAt;\n      if (!session) return null;\n      return session.iAmLocal ? m.remoteSeenAt : m.localSeenAt;\n    }\n\n    function isUnread(m, session) {\n      return !isMine(m) && !seenByMe(m, session);\n    }\n\n    function registerSession(raw) {\n      if (!raw || !raw.id || !state.me) return null;\n      var localId = raw.local && raw.local.userId;\n      var remoteId = raw.remote && raw.remote.userId;\n      if (!localId || !remoteId) return null;\n      var iAmLocal;\n      if (localId === state.me.id) iAmLocal = true;\n      else if (remoteId === state.me.id) iAmLocal = false;\n      else return null;\n      var partnerId = iAmLocal ? remoteId : localId;\n      if (partnerId === state.me.id) return null;\n      var existing = state.sessions[raw.id];\n      var entry = existing || { id: raw.id };\n      entry.partnerId = partnerId;\n      entry.iAmLocal = iAmLocal;\n      entry.isPartnerTyping = iAmLocal ? !!raw.isRemoteTyping : !!raw.isLocalTyping;\n      entry.partnerName = (iAmLocal ? (raw.remote && raw.remote.name) : (raw.local && raw.local.name)) || '';\n      state.sessions[raw.id] = entry;\n      return entry;\n    }\n\n    function sessionIdsOf(partnerId) {\n      return Object.keys(state.sessions).filter(function (sid) {\n        return state.sessions[sid].partnerId === partnerId;\n      });\n    }\n\n    /* Most recently used session for a partner (for sending / typing). */\n    function currentSessionOf(partnerId) {\n      var ids = sessionIdsOf(partnerId);\n      if (!ids.length) return null;\n      ids.sort(function (a, b) {\n        var la = state.latest[a] ? new Date(when(state.latest[a])).getTime() : 0;\n        var lb = state.latest[b] ? new Date(when(state.latest[b])).getTime() : 0;\n        return lb - la;\n      });\n      return state.sessions[ids[0]];\n    }\n\n    function partnerName(partnerId) {\n      var user = state.users[partnerId];\n      if (user && user.name) return user.name;\n      var ids = sessionIdsOf(partnerId);\n      for (var i = 0; i < ids.length; i++) {\n        var name = state.sessions[ids[i]].partnerName;\n        if (name) return name;\n      }\n      return 'Okänd användare';\n    }\n\n    /* ---------- Data loading ---------- */\n    function loadMe() {\n      return api('/users/users-v1/me').then(function (me) {\n        state.me = { id: me.id, name: me.alias || me.fullName || me.username || 'Jag' };\n      });\n    }\n\n    function loadUsers() {\n      return api('/users/users-v1').then(function (users) {\n        (users || []).forEach(function (u) {\n          state.users[u.id] = {\n            id: u.id,\n            name: u.alias || u.fullName || u.username || u.email || 'Okänd',\n            active: u.active !== false,\n            type: u.type || 'standard'\n          };\n        });\n      });\n    }\n\n    function loadSessions() {\n      return api('/users/chat-sessions-v1').then(function (sessions) {\n        (sessions || []).forEach(registerSession);\n      });\n    }\n\n    /* Latest message per session, batched to keep URLs short. */\n    function loadLatest() {\n      var ids = Object.keys(state.sessions);\n      if (!ids.length) return Promise.resolve();\n      var batches = [];\n      for (var i = 0; i < ids.length; i += 25) batches.push(ids.slice(i, i + 25));\n      return Promise.all(batches.map(function (batch) {\n        return api('/users/chat-messages-v1', {\n          sessionId: batch.join(','),\n          isLatest: true,\n          sortOrder: 'desc'\n        }).then(function (messages) {\n          (messages || []).forEach(function (m) {\n            if (m.sessionId) state.latest[m.sessionId] = m;\n          });\n        });\n      }));\n    }\n\n    function loadAll() {\n      return loadSessions().then(loadLatest).then(function () {\n        renderList();\n      });\n    }\n\n    /* ---------- Rendering: list ---------- */\n    function conversations() {\n      var byPartner = {};\n      Object.keys(state.sessions).forEach(function (sid) {\n        var session = state.sessions[sid];\n        var latest = state.latest[sid];\n        if (!latest) return;\n        var existing = byPartner[session.partnerId];\n        var ts = new Date(when(latest)).getTime();\n        var unread = isUnread(latest, session);\n        if (!existing || ts > existing.ts) {\n          byPartner[session.partnerId] = {\n            partnerId: session.partnerId,\n            latest: latest,\n            ts: ts,\n            unread: unread || (existing ? existing.unread : false)\n          };\n        } else if (unread) {\n          existing.unread = true;\n        }\n      });\n      return Object.keys(byPartner).map(function (partnerId) { return byPartner[partnerId]; })\n        .sort(function (a, b) { return b.ts - a.ts; });\n    }\n\n    function colleagues(excludeIds) {\n      return Object.keys(state.users).map(function (id) { return state.users[id]; })\n        .filter(function (u) {\n          if (!state.me || u.id === state.me.id) return false;\n          if (u.type === 'systemUser') return false;\n          if (!u.active) return false;\n          return excludeIds.indexOf(u.id) === -1;\n        })\n        .sort(function (a, b) { return a.name.localeCompare(b.name, 'sv'); });\n    }\n\n    function personRow(partnerId, conv) {\n      var name = partnerName(partnerId);\n      var classes = 'msv-person';\n      if (state.active === partnerId) classes += ' msv-active';\n      if (conv && conv.unread) classes += ' msv-unread';\n\n      var html = '<div class=\"' + classes + '\" data-partner=\"' + esc(partnerId) + '\">';\n      html += '<span class=\"msv-avatar\">' + esc(initials(name)) + '</span>';\n      html += '<div class=\"msv-person-body\"><div class=\"msv-person-top\">';\n      html += '<span class=\"msv-person-name\">' + esc(name) + '</span>';\n      if (conv) html += '<span class=\"msv-person-time\">' + esc(fmtListDate(when(conv.latest))) + '</span>';\n      html += '</div>';\n\n      if (conv) {\n        var preview = (conv.latest.text || '').replace(/\\s+/g, ' ');\n        if (preview.length > 60) preview = preview.substring(0, 60) + '...';\n        var mine = isMine(conv.latest);\n        html += '<div class=\"msv-person-bottom\">';\n        html += '<span class=\"msv-person-preview\">' + (mine ? 'Du: ' : '') + esc(preview) + '</span>';\n        if (conv.unread) {\n          html += '<span class=\"msv-dot\" title=\"Nytt meddelande\"></span>';\n        } else if (mine) {\n          var session = state.sessions[conv.latest.sessionId];\n          var seen = seenByPartner(conv.latest, session);\n          html += '<svg class=\"msv-checkcheck' + (seen ? ' msv-seen' : '') + '\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 7 17l-5-5\"></path><path d=\"m22 10-7.5 7.5L13 16\"></path></svg>';\n        }\n        html += '</div>';\n      } else {\n        html += '<div class=\"msv-person-bottom\"><span class=\"msv-person-preview\" style=\"font-style:italic;\">Starta en ny chatt</span></div>';\n      }\n\n      html += '</div></div>';\n      return html;\n    }\n\n    function renderList() {\n      var container = el('msv-people');\n      if (!container) return;\n      var query = (el('msv-search') ? el('msv-search').value : '').trim().toLowerCase();\n\n      var convs = conversations().filter(function (c) {\n        return !query || partnerName(c.partnerId).toLowerCase().indexOf(query) !== -1;\n      });\n      var convPartnerIds = conversations().map(function (c) { return c.partnerId; });\n      var others = colleagues(convPartnerIds).filter(function (u) {\n        return !query || u.name.toLowerCase().indexOf(query) !== -1;\n      });\n\n      if (!convs.length && !others.length) {\n        container.innerHTML =\n          '<div class=\"msv-empty-list\">' +\n          '<h3>Inga kollegor hittades</h3>' +\n          '<p>Försök med en annan sökning</p>' +\n          '</div>';\n        return;\n      }\n\n      var html = '';\n      if (convs.length) {\n        html += '<div class=\"msv-section\">Chattar</div>';\n        convs.forEach(function (c) { html += personRow(c.partnerId, c); });\n      }\n      if (others.length) {\n        html += '<div class=\"msv-section\">Kollegor</div>';\n        others.forEach(function (u) { html += personRow(u.id, null); });\n      }\n      container.innerHTML = html;\n    }\n\n    function renderLive() {\n      var wrap = el('msv-live');\n      var text = el('msv-live-text');\n      if (!wrap || !text) return;\n      if (state.wsConnected) {\n        wrap.classList.add('msv-on');\n        text.textContent = 'Realtid ansluten';\n      } else {\n        wrap.classList.remove('msv-on');\n        text.textContent = 'Realtid frånkopplad — uppdaterar var 15:e sekund';\n      }\n    }\n\n    /* ---------- Rendering: thread ---------- */\n    function renderThreadHeader() {\n      if (!state.active) return;\n      var name = partnerName(state.active);\n      el('msv-thread-name').textContent = name;\n      var avatar = el('msv-thread-avatar');\n      if (avatar) avatar.textContent = initials(name);\n      renderTypingStatus();\n    }\n\n    function renderTypingStatus() {\n      var status = el('msv-thread-status');\n      if (!status || !state.active) return;\n      var until = state.typingUntil[state.active] || 0;\n      if (until > Date.now()) {\n        status.textContent = 'Skriver...';\n        status.className = 'msv-typing';\n      } else {\n        status.textContent = 'Kollega';\n        status.className = '';\n      }\n    }\n\n    function renderMessages(keepScroll) {\n      var container = el('msv-messages');\n      if (!container) return;\n      var wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 60;\n\n      var sorted = state.messages.slice().sort(function (a, b) {\n        return new Date(when(a)) - new Date(when(b));\n      });\n\n      var html = '';\n      var lastDate = '';\n      sorted.forEach(function (m) {\n        var ts = when(m);\n        var dateStr = new Date(ts).toLocaleDateString('sv-SE');\n        if (dateStr !== lastDate) {\n          html += '<div class=\"msv-date-sep\"><span>' + esc(fmtDateHeading(ts)) + '</span></div>';\n          lastDate = dateStr;\n        }\n\n        var out = isMine(m) || m.pending || m.error;\n        html += '<div class=\"msv-msg-row ' + (out ? 'msv-out' : 'msv-in') + '\"><div class=\"msv-msg\">';\n        html += '<div class=\"msv-bubble\">' + esc(m.text || '') + '</div>';\n        html += '<div class=\"msv-msg-meta\">';\n        if (m.pending) {\n          html += '<span>Skickar...</span>';\n        } else if (m.error) {\n          html += '<span class=\"msv-error\">Kunde inte skickas</span>';\n        } else {\n          html += '<span>' + esc(fmtTime(ts)) + '</span>';\n          if (out) {\n            var session = state.sessions[m.sessionId];\n            var seen = seenByPartner(m, session);\n            html += '<svg class=\"msv-checkcheck' + (seen ? ' msv-seen' : '') + '\" style=\"width:13px;height:13px;\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 7 17l-5-5\"></path><path d=\"m22 10-7.5 7.5L13 16\"></path></svg>';\n          }\n        }\n        html += '</div></div></div>';\n      });\n\n      if (!html) {\n        html = '<div class=\"msv-loading\">Inga meddelanden ännu — skriv det första!</div>';\n      }\n\n      container.innerHTML = html;\n      if (!keepScroll || wasAtBottom) container.scrollTop = container.scrollHeight;\n    }\n\n    function openPartner(partnerId) {\n      state.active = partnerId;\n      state.messages = [];\n      el('msv-thread-empty').style.display = 'none';\n      el('msv-thread').style.display = 'flex';\n      el('msv-app').classList.add('msv-mobile-thread');\n      renderThreadHeader();\n      el('msv-messages').innerHTML = '<div class=\"msv-loading\"><svg class=\"msv-spin\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Hämtar meddelanden...</div>';\n      renderList();\n      refreshThread().then(markThreadSeen);\n      var input = el('msv-input');\n      if (input) input.focus();\n    }\n\n    function refreshThread() {\n      var partnerId = state.active;\n      if (!partnerId) return Promise.resolve();\n      var ids = sessionIdsOf(partnerId);\n      if (!ids.length) {\n        state.messages = [];\n        renderMessages(false);\n        return Promise.resolve();\n      }\n      return api('/users/chat-messages-v1', {\n        sessionId: ids.join(','),\n        sortOrder: 'asc'\n      }).then(function (messages) {\n        if (state.active !== partnerId) return;\n        state.messages = messages || [];\n        renderThreadHeader();\n        renderMessages(true);\n      }).catch(function () {\n        var container = el('msv-messages');\n        if (container) container.innerHTML = '<div class=\"msv-loading\">Kunde inte hämta meddelanden.</div>';\n      });\n    }\n\n    function markThreadSeen() {\n      var partnerId = state.active;\n      if (!partnerId || document.hidden) return;\n      var unseenIds = state.messages.filter(function (m) {\n        return m.id && isUnread(m, state.sessions[m.sessionId]);\n      }).map(function (m) { return m.id; });\n      if (!unseenIds.length) return;\n      apiSend('POST', '/users/chat-messages-v1/see', {\n        ids: unseenIds,\n        seen: true,\n        notified: true\n      }).then(function () {\n        var now = new Date().toISOString();\n        state.messages.forEach(function (m) {\n          if (unseenIds.indexOf(m.id) !== -1) {\n            m.currentSeenAt = now;\n            m.localSeenAt = m.localSeenAt || now;\n            m.remoteSeenAt = m.remoteSeenAt || now;\n          }\n        });\n        Object.keys(state.latest).forEach(function (sid) {\n          var latest = state.latest[sid];\n          if (latest && unseenIds.indexOf(latest.id) !== -1) {\n            latest.currentSeenAt = now;\n          }\n        });\n        renderList();\n      }).catch(function () { /* non-critical */ });\n    }\n\n    /* ---------- Sending ---------- */\n    function ensureSession(partnerId) {\n      var session = currentSessionOf(partnerId);\n      if (session) return Promise.resolve(session);\n      return apiSend('POST', '/users/chat-sessions-v1', {\n        local: { userId: state.me.id },\n        remote: { userId: partnerId }\n      }).then(function (result) {\n        var entry = registerSession(result && result.session);\n        if (!entry) throw new Error('Kunde inte skapa chattsession');\n        return entry;\n      });\n    }\n\n    /* Best-effort SaleSys notification so the colleague notices the message\n       even without this view open. Throttled per recipient. */\n    function pingPartner(partnerId, text) {\n      if (!state.settings.ping) return;\n      var timestamps = {};\n      try { timestamps = JSON.parse(localStorage.getItem('msv-ping-at') || 'null') || {}; } catch (e) { /* ignore */ }\n      var last = timestamps[partnerId] || 0;\n      if (Date.now() - last < PING_THROTTLE_MS) return;\n      timestamps[partnerId] = Date.now();\n      try { localStorage.setItem('msv-ping-at', JSON.stringify(timestamps)); } catch (e) { /* ignore */ }\n\n      var preview = text.replace(/\\s+/g, ' ');\n      if (preview.length > 90) preview = preview.substring(0, 90) + '...';\n      apiSend('POST', '/users/user-messages-v1', {\n        target: { userIds: [partnerId] },\n        notification: {\n          title: 'Nytt meddelande från ' + (state.me ? state.me.name : 'kollega'),\n          text: preview,\n          iconName: 'comment-dots'\n        },\n        expireAfterSeconds: 600\n      }).catch(function () { /* the account may lack the right to send these */ });\n    }\n\n    function sendMessage() {\n      var partnerId = state.active;\n      var input = el('msv-input');\n      if (!partnerId || !input || state.sending) return;\n      var text = input.value.trim();\n      if (!text) return;\n\n      state.sending = true;\n      var sendBtn = el('msv-send-btn');\n      if (sendBtn) sendBtn.disabled = true;\n\n      var optimistic = {\n        id: null,\n        text: text,\n        sentAt: new Date().toISOString(),\n        pending: true\n      };\n      state.messages.push(optimistic);\n      renderMessages(false);\n      input.value = '';\n      input.style.height = 'auto';\n\n      ensureSession(partnerId).then(function (session) {\n        return apiSend('POST', '/users/chat-messages-v1', {\n          sessionId: session.id,\n          message: { text: text }\n        }).then(function (created) {\n          optimistic.pending = false;\n          if (created && created.id) {\n            optimistic.id = created.id;\n            optimistic.sessionId = created.sessionId;\n            optimistic.userId = created.userId;\n            optimistic.sentAt = created.sentAt || optimistic.sentAt;\n            state.latest[session.id] = created;\n          }\n          renderMessages(true);\n          renderList();\n          pingPartner(partnerId, text);\n        });\n      }).catch(function () {\n        optimistic.pending = false;\n        optimistic.error = true;\n        renderMessages(true);\n      }).then(function () {\n        state.sending = false;\n        if (sendBtn) sendBtn.disabled = false;\n        if (input) input.focus();\n      });\n    }\n\n    /* ---------- Typing ---------- */\n    function sendTyping() {\n      if (!state.active) return;\n      var now = Date.now();\n      if (now - state.lastTypingSentAt < TYPING_SEND_MS) return;\n      var session = currentSessionOf(state.active);\n      if (!session) return;\n      state.lastTypingSentAt = now;\n      apiSend('PUT', '/users/chat-sessions-v1/' + session.id, { isTyping: true })\n        .catch(function () { /* best effort */ });\n    }\n\n    function notePartnerTyping(partnerId) {\n      state.typingUntil[partnerId] = Date.now() + TYPING_SHOW_MS;\n      renderTypingStatus();\n      if (state.typingTimer) clearTimeout(state.typingTimer);\n      state.typingTimer = setTimeout(renderTypingStatus, TYPING_SHOW_MS + 100);\n    }\n\n    /* ---------- Realtime (websocket) ---------- */\n    function handleWsMessage(data) {\n      if (!data || !data.type) return;\n\n      if (data.type === 'chatMessageCreated' && data.message) {\n        var m = data.message;\n        if (!m.sessionId) return;\n        var session = state.sessions[m.sessionId];\n        if (!session) {\n          /* A colleague may have just started a brand new chat with us. */\n          session = registerSession({\n            id: m.sessionId,\n            local: m.local,\n            remote: m.remote\n          });\n          if (!session) return;\n        }\n        var existing = state.latest[m.sessionId];\n        if (!existing || new Date(when(m)) >= new Date(when(existing))) {\n          state.latest[m.sessionId] = m;\n        }\n        if (state.active === session.partnerId) {\n          var already = state.messages.some(function (message) { return message.id === m.id; });\n          if (!already && !isMine(m)) {\n            state.messages.push(m);\n            renderMessages(true);\n            markThreadSeen();\n          }\n        }\n        renderList();\n        return;\n      }\n\n      if (data.type === 'chatSessionUpdated' && data.session) {\n        var entry = registerSession(data.session);\n        if (entry && entry.isPartnerTyping) notePartnerTyping(entry.partnerId);\n      }\n    }\n\n    function connectWs() {\n      if (state.destroyed || !state.me) return;\n      var token = getToken();\n      if (!token || !window.WebSocket) return;\n      try {\n        var url = WS_URL +\n          '?roomId=' + encodeURIComponent('auth:' + state.me.id) +\n          '&authorization=' + encodeURIComponent(token);\n        var ws = new WebSocket(url);\n        state.ws = ws;\n\n        ws.onopen = function () {\n          if (state.ws !== ws) return;\n          state.wsConnected = true;\n          state.wsRetryMs = 2000;\n          renderLive();\n        };\n        ws.onmessage = function (event) {\n          if (state.ws !== ws) return;\n          try { handleWsMessage(JSON.parse(event.data)); } catch (parseError) { /* ignore */ }\n        };\n        ws.onclose = function () {\n          if (state.ws !== ws) return;\n          state.wsConnected = false;\n          renderLive();\n          if (state.destroyed) return;\n          if (state.wsReconnectTimer) clearTimeout(state.wsReconnectTimer);\n          state.wsReconnectTimer = setTimeout(connectWs, state.wsRetryMs);\n          state.wsRetryMs = Math.min(state.wsRetryMs * 2, 30000);\n        };\n        ws.onerror = function () { try { ws.close(); } catch (closeError) { /* ignore */ } };\n      } catch (wsError) {\n        state.wsConnected = false;\n        renderLive();\n      }\n    }\n\n    /* ---------- Polling (fallback + reconciliation) ---------- */\n    function poll() {\n      if (document.hidden) return;\n      if (!el('msv-root')) return; /* view unmounted */\n      applyContrast();\n      loadAll().catch(function () { /* retry next tick */ });\n      if (state.active) refreshThread().then(markThreadSeen);\n    }\n\n    /* ---------- Layout ---------- */\n    /* The app's dark theme can resolve --color-primary to a light color, so\n       white-on-primary text is not a safe assumption. Probe the resolved color\n       and pick a readable foreground. */\n    function applyContrast() {\n      var root = el('msv-root');\n      if (!root) return;\n      try {\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;background-color:var(--msv-primary);';\n        root.appendChild(probe);\n        var rgb = /rgba?\\((\\d+)[,\\s]+(\\d+)[,\\s]+(\\d+)/.exec(getComputedStyle(probe).backgroundColor);\n        root.removeChild(probe);\n        if (!rgb) return;\n        var luminance = (0.299 * rgb[1] + 0.587 * rgb[2] + 0.114 * rgb[3]) / 255;\n        root.style.setProperty('--msv-primary-fg', luminance > 0.6 ? '#1b2430' : '#ffffff');\n      } catch (e) { /* keep default */ }\n    }\n\n    function fitHeight() {\n      var root = el('msv-root');\n      if (!root) return;\n      var top = root.getBoundingClientRect().top;\n      var height = window.innerHeight - top - 24;\n      if (height < 460) height = 460;\n      root.style.height = height + 'px';\n    }\n\n    /* ---------- Events ----------\n       The app can re-render the view's DOM after this script has run, which\n       detaches any listeners bound directly to elements. All wiring therefore\n       uses delegated listeners on `document`, matched by id at event time. */\n    function within(target, id) {\n      var node = target;\n      while (node && node !== document) {\n        if (node.id === id) return node;\n        node = node.parentNode;\n      }\n      return null;\n    }\n\n    function onDocClick(event) {\n      if (!el('msv-root')) return;\n      var target = event.target;\n      if (!target) return;\n\n      if (within(target, 'msv-settings-btn')) {\n        var panel = el('msv-settings-panel');\n        if (panel) {\n          var toggle = el('msv-ping-toggle');\n          if (toggle) toggle.checked = state.settings.ping;\n          panel.classList.toggle('msv-open');\n        }\n        return;\n      }\n\n      if (within(target, 'msv-refresh-btn')) {\n        var icon = el('msv-refresh-icon');\n        if (icon) icon.classList.add('msv-spin');\n        loadAll().then(function () { if (icon) icon.classList.remove('msv-spin'); },\n          function () { if (icon) icon.classList.remove('msv-spin'); });\n        return;\n      }\n\n      if (within(target, 'msv-thread-refresh')) {\n        var tIcon = el('msv-thread-refresh-icon');\n        if (tIcon) tIcon.classList.add('msv-spin');\n        refreshThread().then(function () { if (tIcon) tIcon.classList.remove('msv-spin'); markThreadSeen(); },\n          function () { if (tIcon) tIcon.classList.remove('msv-spin'); });\n        return;\n      }\n\n      if (within(target, 'msv-back-btn')) {\n        var app = el('msv-app');\n        if (app) app.classList.remove('msv-mobile-thread');\n        return;\n      }\n\n      if (within(target, 'msv-send-btn')) { sendMessage(); return; }\n\n      var container = el('msv-people');\n      if (container && container.contains(target)) {\n        var node = target;\n        while (node && node !== container) {\n          if (node.classList && node.classList.contains('msv-person')) {\n            var partnerId = node.getAttribute('data-partner');\n            if (partnerId) openPartner(partnerId);\n            return;\n          }\n          node = node.parentNode;\n        }\n      }\n    }\n\n    function onDocChange(event) {\n      var target = event.target;\n      if (!target || !target.id) return;\n      if (target.id === 'msv-ping-toggle') {\n        state.settings.ping = !!target.checked;\n        saveSettings();\n      }\n    }\n\n    function onDocInput(event) {\n      var target = event.target;\n      if (!target || !target.id) return;\n      if (target.id === 'msv-search') {\n        renderList();\n      } else if (target.id === 'msv-input') {\n        target.style.height = 'auto';\n        target.style.height = Math.min(target.scrollHeight, 130) + 'px';\n        sendTyping();\n      }\n    }\n\n    function onDocKeydown(event) {\n      var target = event.target;\n      if (target && target.id === 'msv-input' && event.key === 'Enter' && !event.shiftKey) {\n        event.preventDefault();\n        sendMessage();\n      }\n    }\n\n    function init() {\n      document.addEventListener('click', onDocClick);\n      document.addEventListener('change', onDocChange);\n      document.addEventListener('input', onDocInput);\n      document.addEventListener('keydown', onDocKeydown);\n      window.addEventListener('resize', fitHeight);\n\n      fitHeight();\n      applyContrast();\n      renderLive();\n      setTimeout(function () { fitHeight(); applyContrast(); }, 300);\n\n      /* The theme can switch at runtime (light/dark) which changes the resolved\n         --color-primary — re-probe contrast when html/body attributes change. */\n      var themeObserver = null;\n      if (window.MutationObserver) {\n        themeObserver = new MutationObserver(function () { applyContrast(); });\n        themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n        if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      }\n\n      if (!getToken()) {\n        showFatal('inloggnings-token saknas (s2_utoken).');\n        return;\n      }\n\n      loadMe().then(loadUsers).then(loadAll).then(function () {\n        connectWs();\n      }).catch(function (error) {\n        showFatal(error && error.message ? error.message : 'okänt fel');\n      });\n\n      state.pollTimer = setInterval(poll, POLL_MS);\n\n      window.__msvCleanup = function () {\n        state.destroyed = true;\n        if (themeObserver) themeObserver.disconnect();\n        clearInterval(state.pollTimer);\n        if (state.typingTimer) clearTimeout(state.typingTimer);\n        if (state.wsReconnectTimer) clearTimeout(state.wsReconnectTimer);\n        if (state.ws) { try { state.ws.close(); } catch (closeError) { /* ignore */ } }\n        window.removeEventListener('resize', fitHeight);\n        document.removeEventListener('click', onDocClick);\n        document.removeEventListener('change', onDocChange);\n        document.removeEventListener('input', onDocInput);\n        document.removeEventListener('keydown', onDocKeydown);\n      };\n    }\n\n    try {\n      init();\n    } catch (error) {\n      showFatal(error && error.message ? error.message : String(error));\n      if (window.console) console.error('MSV init error', error);\n    }\n  })();\n  </script>\n</div>","iconName":"comments","iconColor":"#00897B","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50c91f26468211bb255a1f","name":"Tävlingar v2","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":2,"label":"Tävlingar v2"}],"html":"<div id=\"g2-root\">\n  <style>\n    #g2-root {\n      --g2-bg: var(--color-background-2, #fafafa);\n      --g2-card: var(--color-background-1, #ffffff);\n      --g2-secondary: var(--color-background-3, #eeeeee);\n      --g2-fg: var(--color-text-1, #212121);\n      --g2-fg2: var(--color-text-2, #616161);\n      --g2-muted-fg: var(--color-text-3, #757575);\n      --g2-primary: var(--color-primary, #1565C0);\n      --g2-danger: var(--color-danger, #E53935);\n      --g2-success: #2E7D32;\n      --g2-gold: #F9A825;\n      --g2-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --g2-border: color-mix(in srgb, var(--g2-fg) 13%, transparent);\n      --g2-border-soft: color-mix(in srgb, var(--g2-fg) 8%, transparent);\n      --g2-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--g2-fg);\n      display: block;\n      background: var(--g2-bg);\n      border: 1px solid var(--g2-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #g2-root * { box-sizing: border-box; }\n    #g2-root button { font-family: inherit; cursor: pointer; }\n    #g2-root input, #g2-root select { font-family: inherit; font-size: 14px; color: var(--g2-fg); }\n\n    .g2-toolbar {\n      display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n      padding: 12px 16px; border-bottom: 1px solid var(--g2-border-soft); background: var(--g2-card);\n    }\n    .g2-toolbar-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; }\n    .g2-toolbar-title .fas { color: var(--g2-gold); }\n    .g2-toolbar-spacer { flex: 1; }\n    .g2-btn {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--g2-border); background: var(--g2-card);\n      border-radius: var(--g2-radius); padding: 7px 12px; font-size: 13px; font-weight: 600;\n    }\n    .g2-btn:hover { background: var(--g2-hover); }\n    .g2-btn.g2-primary-btn { background: var(--g2-primary); border-color: var(--g2-primary); color: var(--g2-primary-fg, #fff); }\n    .g2-btn.g2-danger-btn { color: var(--g2-danger); }\n    .g2-btn:disabled { opacity: 0.5; cursor: default; }\n\n    .g2-body { padding: 16px; display: flex; flex-direction: column; gap: 16px; }\n    .g2-section-title { font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; color: var(--g2-muted-fg); }\n    .g2-columns { display: grid; grid-template-columns: 1fr 300px; gap: 16px; align-items: start; }\n    @media (max-width: 1020px) { .g2-columns { grid-template-columns: 1fr; } }\n    .g2-main-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }\n    .g2-side-col { display: flex; flex-direction: column; gap: 16px; }\n\n    .g2-card { background: var(--g2-card); border: 1px solid var(--g2-border-soft); border-radius: var(--g2-radius); padding: 14px 16px; }\n    .g2-empty { font-size: 13px; color: var(--g2-muted-fg); }\n    .g2-loading { display: flex; align-items: center; gap: 10px; color: var(--g2-muted-fg); font-size: 13px; padding: 30px 0; justify-content: center; }\n    .g2-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--g2-border); border-top-color: var(--g2-primary);\n      animation: g2-spin 0.8s linear infinite;\n    }\n    @keyframes g2-spin { to { transform: rotate(360deg); } }\n\n    .g2-invite { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--g2-border-soft); flex-wrap: wrap; }\n    .g2-invite:last-child { border-bottom: none; }\n    .g2-invite-icon { font-size: 19px; width: 30px; text-align: center; color: var(--g2-primary); }\n    .g2-invite-info { flex: 1; min-width: 160px; }\n    .g2-invite-name { font-weight: 800; font-size: 14px; }\n    .g2-invite-meta { font-size: 12px; color: var(--g2-muted-fg); }\n\n    .g2-game { padding: 0; overflow: hidden; }\n    .g2-game-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; flex-wrap: wrap; }\n    .g2-game-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; }\n    .g2-game-title .fas { color: var(--g2-primary); font-size: 14px; }\n    .g2-game-meta { font-size: 12px; color: var(--g2-muted-fg); }\n    .g2-game-spacer { flex: 1; }\n    .g2-chip {\n      font-size: 11px; font-weight: 700; padding: 2px 10px; border-radius: 999px;\n      background: color-mix(in srgb, var(--g2-primary) 12%, transparent);\n      color: color-mix(in srgb, var(--g2-primary) 65%, var(--g2-fg));\n    }\n    .g2-chip.g2-chip-live { background: color-mix(in srgb, var(--g2-success) 14%, transparent); color: var(--g2-success); }\n    .g2-chip.g2-chip-done { background: color-mix(in srgb, var(--g2-gold) 18%, transparent); color: #8a6d00; }\n    .g2-winner-banner {\n      display: flex; align-items: center; gap: 10px; margin: 0 16px 10px;\n      background: color-mix(in srgb, var(--g2-gold) 14%, var(--g2-card));\n      border: 1px solid color-mix(in srgb, var(--g2-gold) 45%, transparent);\n      border-radius: var(--g2-radius); padding: 9px 14px; font-size: 14px; font-weight: 700;\n    }\n    .g2-winner-banner .fas { color: var(--g2-gold); font-size: 18px; }\n    .g2-game-footer { display: flex; align-items: center; gap: 12px; padding: 0 16px 12px; font-size: 12px; color: var(--g2-muted-fg); flex-wrap: wrap; }\n\n    /* ---------- lane race tracks ---------- */\n    .g2-track { position: relative; margin: 4px 16px 14px; border-radius: var(--g2-radius); overflow: hidden; }\n    .g2-lane { position: relative; height: 52px; }\n    .g2-track.g2-horses { background: linear-gradient(180deg, #a8763e, #96683a); }\n    .g2-horses .g2-lane { border-bottom: 2px dashed rgba(255,255,255,0.35); }\n    .g2-track.g2-boats { background: linear-gradient(180deg, #1e88c9, #10639c); }\n    .g2-boats .g2-lane {\n      border-bottom: 1px solid rgba(255,255,255,0.18);\n      background: radial-gradient(14px 5px at 22% 78%, rgba(255,255,255,0.13), transparent 70%),\n        radial-gradient(18px 6px at 62% 66%, rgba(255,255,255,0.10), transparent 70%);\n    }\n    .g2-track.g2-cars { background: linear-gradient(180deg, #55595f, #43474d); }\n    .g2-cars .g2-lane { border-bottom: 3px dashed rgba(255,255,255,0.55); }\n    .g2-cars .g2-lane:last-child { border-bottom: none; }\n    .g2-track.g2-rockets { background: linear-gradient(180deg, #101430, #1b1f45); }\n    .g2-rockets .g2-lane {\n      border-bottom: 1px solid rgba(255,255,255,0.10);\n      background: radial-gradient(1.5px 1.5px at 15% 30%, #fff, transparent),\n        radial-gradient(1.2px 1.2px at 45% 70%, rgba(255,255,255,0.8), transparent),\n        radial-gradient(1.5px 1.5px at 72% 25%, rgba(255,255,255,0.9), transparent),\n        radial-gradient(1px 1px at 88% 60%, rgba(255,255,255,0.7), transparent);\n    }\n    .g2-track.g2-skis { background: linear-gradient(180deg, #eef5fb, #d8e8f5); }\n    .g2-skis .g2-lane { border-bottom: 2px dotted rgba(50,90,140,0.25); }\n    .g2-skis .g2-lane-label, .g2-skis .g2-lane-score { color: #23527c !important; text-shadow: none !important; }\n    .g2-track.g2-cycles { background: linear-gradient(180deg, #4d5b4f, #3d4a40); }\n    .g2-cycles .g2-lane { border-bottom: 2px dashed rgba(255,255,255,0.4); }\n    .g2-track.g2-balloons { background: linear-gradient(180deg, #7ec3ef, #b7ddf6); }\n    .g2-balloons .g2-lane {\n      border-bottom: 1px solid rgba(255,255,255,0.5);\n      background: radial-gradient(26px 9px at 70% 30%, rgba(255,255,255,0.75), transparent 70%),\n        radial-gradient(20px 7px at 25% 60%, rgba(255,255,255,0.6), transparent 70%);\n    }\n    .g2-balloons .g2-lane-label, .g2-balloons .g2-lane-score { color: #1d4a6e !important; text-shadow: none !important; }\n    .g2-finish {\n      position: absolute; top: 0; bottom: 0; right: 34px; width: 12px;\n      background: repeating-conic-gradient(#ffffff 0% 25%, #1b1b1b 0% 50%);\n      background-size: 12px 12px; opacity: 0.9; z-index: 1;\n    }\n    .g2-sprite-wrap {\n      position: absolute; top: 50%; left: 0; transform: translateY(-50%);\n      transition: left 1200ms cubic-bezier(0.25, 0.6, 0.3, 1); z-index: 2;\n    }\n    .g2-sprite { display: block; animation: g2-bob 0.9s ease-in-out infinite; }\n    .g2-boats .g2-sprite, .g2-balloons .g2-sprite { animation: g2-sail 2.2s ease-in-out infinite; }\n    .g2-rockets .g2-sprite { animation: g2-thrust 0.5s ease-in-out infinite; }\n    @keyframes g2-bob {\n      0%, 100% { transform: translateY(0) rotate(0deg); }\n      30% { transform: translateY(-2.5px) rotate(-2deg); }\n      60% { transform: translateY(1px) rotate(1.5deg); }\n    }\n    @keyframes g2-sail {\n      0%, 100% { transform: translateY(0) rotate(-2deg); }\n      50% { transform: translateY(-3px) rotate(2.5deg); }\n    }\n    @keyframes g2-thrust {\n      0%, 100% { transform: translateX(0); }\n      50% { transform: translateX(1.5px); }\n    }\n    .g2-lane-label {\n      position: absolute; left: 8px; top: 5px; z-index: 3;\n      font-size: 11px; font-weight: 800; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.55);\n      display: flex; align-items: center; gap: 6px;\n      max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n    }\n    .g2-lane-score {\n      position: absolute; right: 52px; top: 5px; z-index: 3;\n      font-size: 11px; font-weight: 800; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.55);\n    }\n    .g2-lane-crown { color: var(--g2-gold); }\n    .g2-lane.g2-ghost-lane { opacity: 0.75; }\n    .g2-hcap {\n      font-size: 9.5px; font-weight: 800; padding: 0 5px; border-radius: 999px;\n      background: rgba(0,0,0,0.3); color: #fff;\n    }\n\n    /* ---------- tug of war ---------- */\n    .g2-tug { margin: 4px 16px 14px; }\n    .g2-tug-teams { display: flex; justify-content: space-between; font-size: 13px; font-weight: 800; margin-bottom: 6px; }\n    .g2-tug-score { font-size: 12px; color: var(--g2-muted-fg); font-weight: 600; }\n    .g2-tug-bar { position: relative; height: 38px; border-radius: 999px; overflow: hidden; background: var(--g2-secondary); border: 1px solid var(--g2-border-soft); }\n    .g2-tug-left { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, #1565C0, #4FC3F7); transition: width 1200ms cubic-bezier(0.25, 0.6, 0.3, 1); }\n    .g2-tug-knot {\n      position: absolute; top: 50%; transform: translate(-50%, -50%); z-index: 2;\n      width: 26px; height: 26px; border-radius: 50%; background: #fff;\n      border: 3px solid var(--g2-fg); transition: left 1200ms cubic-bezier(0.25, 0.6, 0.3, 1);\n      box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n    }\n    .g2-tug-rope { position: absolute; left: 3%; right: 3%; top: 50%; height: 4px; transform: translateY(-50%); background: repeating-linear-gradient(90deg, #8d6e63 0 8px, #6d4c41 8px 16px); border-radius: 2px; }\n    .g2-tug-members { display: flex; justify-content: space-between; gap: 10px; margin-top: 8px; font-size: 11.5px; color: var(--g2-fg2); flex-wrap: wrap; }\n    .g2-tug-side { display: flex; gap: 5px; flex-wrap: wrap; max-width: 48%; }\n    .g2-mini-chip { background: var(--g2-secondary); border-radius: 999px; padding: 1px 8px; font-weight: 700; }\n\n    /* ---------- relay ---------- */\n    .g2-relay { margin: 4px 16px 14px; display: flex; flex-direction: column; gap: 12px; }\n    .g2-relay-team-name { font-size: 13px; font-weight: 800; margin-bottom: 5px; display: flex; justify-content: space-between; }\n    .g2-relay-bar { display: flex; height: 26px; border-radius: 8px; overflow: hidden; border: 1px solid var(--g2-border-soft); background: var(--g2-secondary); }\n    .g2-relay-seg { position: relative; border-right: 2px solid var(--g2-card); overflow: hidden; }\n    .g2-relay-seg:last-child { border-right: none; }\n    .g2-relay-fill { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, #2E7D32, #AED581); transition: width 1000ms ease; }\n    .g2-relay-seg-label { position: relative; z-index: 2; font-size: 10px; font-weight: 800; padding: 5px 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }\n\n    /* ---------- streak ---------- */\n    .g2-streaks { margin: 4px 16px 14px; display: flex; flex-direction: column; gap: 8px; }\n    .g2-streak-row { display: flex; align-items: center; gap: 10px; }\n    .g2-streak-name { width: 110px; font-size: 12.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; }\n    .g2-streak-days { display: flex; gap: 4px; flex-wrap: wrap; flex: 1; }\n    .g2-streak-day {\n      width: 18px; height: 18px; border-radius: 5px; background: var(--g2-secondary);\n      border: 1px solid var(--g2-border-soft);\n    }\n    .g2-streak-day.g2-hit { background: linear-gradient(180deg, #FF8F00, #EF6C00); border-color: #EF6C00; }\n    .g2-streak-day.g2-today { outline: 2px solid var(--g2-primary); outline-offset: 1px; }\n    .g2-streak-count { font-size: 12.5px; font-weight: 800; display: flex; align-items: center; gap: 5px; width: 58px; justify-content: flex-end; }\n    .g2-streak-count .fas { color: #EF6C00; font-size: 12px; }\n\n    /* ---------- bingo ---------- */\n    .g2-bingo { margin: 4px 16px 14px; }\n    .g2-bingo-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }\n    .g2-bingo-top select { padding: 6px 10px; border: 1px solid var(--g2-border); border-radius: 8px; background: var(--g2-card); font-size: 13px; }\n    .g2-bingo-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; max-width: 560px; }\n    .g2-bingo-cell {\n      border: 1px solid var(--g2-border-soft); border-radius: 8px; padding: 8px 9px;\n      font-size: 11px; font-weight: 700; background: var(--g2-bg); min-height: 62px;\n      display: flex; flex-direction: column; justify-content: space-between; gap: 4px;\n    }\n    .g2-bingo-cell.g2-done { background: color-mix(in srgb, var(--g2-success) 16%, var(--g2-card)); border-color: color-mix(in srgb, var(--g2-success) 45%, transparent); }\n    .g2-bingo-cell .g2-bingo-progress { font-size: 10px; color: var(--g2-muted-fg); font-weight: 600; }\n    .g2-bingo-cell.g2-done .g2-bingo-progress { color: var(--g2-success); }\n    .g2-bingo-lb { margin-top: 10px; font-size: 12.5px; color: var(--g2-fg2); display: flex; gap: 12px; flex-wrap: wrap; }\n\n    /* ---------- lottery ---------- */\n    .g2-lottery { margin: 4px 16px 14px; }\n    .g2-lottery-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }\n    .g2-lottery-name { width: 120px; font-size: 12.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .g2-lottery-bar-wrap { flex: 1; height: 20px; background: var(--g2-secondary); border-radius: 999px; overflow: hidden; }\n    .g2-lottery-bar { height: 100%; background: linear-gradient(90deg, #8E24AA, #CE93D8); border-radius: 999px; transition: width 1000ms ease; min-width: 2px; }\n    .g2-lottery-tickets { width: 66px; font-size: 12px; font-weight: 800; text-align: right; }\n    .g2-lottery-note { font-size: 11.5px; color: var(--g2-muted-fg); margin-top: 6px; }\n\n    /* ---------- tournament ---------- */\n    .g2-bracket { margin: 4px 16px 14px; display: flex; gap: 14px; overflow-x: auto; padding-bottom: 6px; }\n    .g2-round { min-width: 190px; display: flex; flex-direction: column; gap: 10px; justify-content: space-around; }\n    .g2-round-title { font-size: 11px; font-weight: 800; text-transform: uppercase; color: var(--g2-muted-fg); text-align: center; }\n    .g2-round.g2-live .g2-round-title { color: var(--g2-success); }\n    .g2-match { border: 1px solid var(--g2-border-soft); border-radius: 8px; overflow: hidden; background: var(--g2-bg); }\n    .g2-match.g2-live-match { border-color: color-mix(in srgb, var(--g2-success) 50%, transparent); }\n    .g2-match-side { display: flex; justify-content: space-between; gap: 8px; padding: 6px 10px; font-size: 12.5px; }\n    .g2-match-side + .g2-match-side { border-top: 1px solid var(--g2-border-soft); }\n    .g2-match-side.g2-won { font-weight: 800; background: color-mix(in srgb, var(--g2-gold) 12%, transparent); }\n    .g2-match-side.g2-tbd { color: var(--g2-muted-fg); font-style: italic; }\n\n    /* ---------- tanks ---------- */\n    .g2-tankfield { margin: 4px 16px 10px; border-radius: var(--g2-radius); overflow: hidden; border: 1px solid var(--g2-border-soft); }\n    .g2-tankfield svg { display: block; width: 100%; height: auto; }\n    .g2-tank-controls { display: flex; align-items: center; gap: 10px; padding: 0 16px 10px; flex-wrap: wrap; }\n    .g2-tank-ctl { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 700; }\n    .g2-tank-ctl input[type=range] { width: 110px; }\n    .g2-tank-ctl b { min-width: 36px; text-align: right; }\n    .g2-tank-status { font-size: 12px; color: var(--g2-muted-fg); padding: 0 16px 10px; }\n\n    /* ---------- side col ---------- */\n    .g2-lb-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13.5px; border-bottom: 1px solid var(--g2-border-soft); }\n    .g2-lb-row:last-child { border-bottom: none; }\n    .g2-lb-rank { width: 20px; font-weight: 800; color: var(--g2-muted-fg); }\n    .g2-lb-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }\n    .g2-lb-count { font-weight: 800; display: flex; align-items: center; gap: 5px; }\n    .g2-lb-count .fas { color: var(--g2-gold); font-size: 12px; }\n    .g2-card-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }\n    .g2-card-title .fas { color: var(--g2-gold); }\n    .g2-done-row { padding: 8px 0; border-bottom: 1px solid var(--g2-border-soft); font-size: 13px; }\n    .g2-done-row:last-child { border-bottom: none; }\n    .g2-done-name { font-weight: 700; }\n    .g2-done-meta { font-size: 11.5px; color: var(--g2-muted-fg); }\n\n    /* ---------- modal ---------- */\n    .g2-modal-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,0.35); display: none; align-items: center; justify-content: center; }\n    .g2-modal-backdrop.g2-open { display: flex; }\n    .g2-modal {\n      /* Native SaleSys modal: background-2, 4px radius, 24px padding (core/modal).\n         Fixed size — the form area scrolls internally so the modal never jumps\n         around when a format shows/hides fields. */\n      background: var(--g2-bg); color: var(--g2-fg2); border-radius: 4px; padding: 24px;\n      width: 900px; max-width: calc(100vw - 40px);\n      height: min(680px, calc(100vh - 60px));\n      display: flex; flex-direction: column;\n      box-shadow: 0 12px 48px rgba(0,0,0,0.25);\n    }\n    .g2-modal input[type=text], .g2-modal input[type=number], .g2-modal input[type=datetime-local] {\n      /* Native SaleSys input: underline style, transparent bg (styles/form.scss). */\n      border: none !important; border-bottom: 2px solid var(--color-background-4, #e0e0e0) !important;\n      border-radius: 0 !important; background: transparent !important;\n      padding: 8px 12px; font-size: 16px; color: var(--g2-fg2);\n      transition: border-color 250ms cubic-bezier(0.215, 0.61, 0.355, 1);\n      box-shadow: none !important;\n    }\n    .g2-modal input[type=text]:focus, .g2-modal input[type=number]:focus, .g2-modal input[type=datetime-local]:focus {\n      border-bottom-color: var(--g2-primary) !important;\n    }\n    .g2-modal .g2-btn {\n      /* Native SaleSys button: 42px, 15px font, normal weight, 8px radius (core/Button). */\n      height: 42px; padding: 8px 16px; font-size: 15px; font-weight: var(--font-weight-default, 400);\n      border-radius: var(--border-radius-button, 8px);\n    }\n    .g2-modal-grid {\n      display: grid; grid-template-columns: 1fr 280px; gap: 22px; align-items: start;\n      flex: 1; overflow-y: auto; min-height: 0;\n      margin: 0 -6px; padding: 0 6px; /* keep card focus rings from clipping */\n    }\n    .g2-info-panel {\n      position: sticky; top: 0;\n      border: 1px solid var(--g2-border-soft); border-radius: 4px;\n      background: var(--g2-card); padding: 16px 16px 14px;\n    }\n    .g2-info-icon-row { display: flex; align-items: center; gap: 10px; }\n    .g2-info-badge {\n      width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0;\n      background: color-mix(in srgb, var(--g2-primary) 12%, transparent); color: var(--g2-primary);\n      display: inline-flex; align-items: center; justify-content: center; font-size: 15px;\n    }\n    .g2-info-title { font-size: 15px; font-weight: 800; }\n    .g2-info-illustration { margin: 12px 0 10px; border-radius: 9px; overflow: hidden; }\n    .g2-info-desc { font-size: 12.5px; color: var(--g2-fg2); line-height: 1.55; }\n    .g2-info-fact { font-size: 11.5px; color: var(--g2-muted-fg); font-weight: 600; margin-top: 9px; display: flex; gap: 7px; align-items: center; }\n    .g2-info-fact .fas { width: 14px; text-align: center; }\n    .g2-format-hint { display: none; }\n    @media (max-width: 780px) {\n      .g2-modal-grid { grid-template-columns: 1fr; }\n      .g2-info-panel { display: none; }\n      .g2-format-hint { display: block; }\n    }\n    .g2-step {\n      display: flex; align-items: center; gap: 8px;\n      font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em;\n      color: var(--g2-muted-fg); margin: 18px 0 8px;\n    }\n    .g2-step-num {\n      width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0;\n      background: color-mix(in srgb, var(--g2-primary) 14%, transparent);\n      color: var(--g2-primary); font-size: 10.5px; font-weight: 800;\n      display: inline-flex; align-items: center; justify-content: center;\n    }\n    .g2-fmt-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px; }\n    @media (max-width: 620px) { .g2-fmt-grid { grid-template-columns: repeat(3, 1fr); } }\n    .g2-fmt-opt {\n      border: 1px solid var(--g2-border); border-radius: 4px; padding: 10px 4px 8px;\n      display: flex; flex-direction: column; align-items: center; gap: 6px;\n      cursor: pointer; font-size: 10.5px; font-weight: 600; color: var(--g2-fg2);\n      text-align: center; background: var(--g2-card);\n      transition: border-color 150ms ease, background-color 150ms ease;\n    }\n    .g2-fmt-opt .fas { font-size: 17px; color: var(--g2-muted-fg); }\n    .g2-fmt-opt:hover { border-color: color-mix(in srgb, var(--g2-primary) 45%, var(--g2-border)); }\n    .g2-fmt-opt.g2-picked { border-color: var(--g2-primary); background: color-mix(in srgb, var(--g2-primary) 7%, transparent); color: var(--g2-fg); }\n    .g2-fmt-opt.g2-picked .fas { color: var(--g2-primary); }\n    .g2-format-hint { min-height: 16px; }\n    .g2-metric-row { display: flex; flex-wrap: wrap; gap: 6px; }\n    .g2-metric-chip {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--g2-border); border-radius: 999px; padding: 5px 13px;\n      font-size: 13px; font-weight: 600; cursor: pointer; color: var(--g2-fg2);\n      background: var(--g2-card); transition: border-color 150ms ease;\n    }\n    .g2-metric-chip .fas { font-size: 11px; color: var(--g2-muted-fg); }\n    .g2-metric-chip:hover { border-color: color-mix(in srgb, var(--g2-primary) 45%, var(--g2-border)); }\n    .g2-metric-chip.g2-picked { border-color: var(--g2-primary); background: color-mix(in srgb, var(--g2-primary) 7%, transparent); color: var(--g2-fg); }\n    .g2-metric-chip.g2-picked .fas { color: var(--g2-primary); }\n    .g2-map-row { display: flex; gap: 9px; flex-wrap: wrap; }\n    .g2-map-card {\n      flex: 1; min-width: 150px; border: 1px solid var(--g2-border); border-radius: 4px;\n      overflow: hidden; cursor: pointer; background: var(--g2-card);\n      transition: border-color 150ms ease;\n    }\n    .g2-map-card:hover { border-color: color-mix(in srgb, var(--g2-primary) 45%, var(--g2-border)); }\n    .g2-map-card.g2-picked { border-color: var(--g2-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--g2-primary) 20%, transparent); }\n    .g2-map-card svg { display: block; width: 100%; height: auto; }\n    .g2-map-card-name { font-size: 12px; font-weight: 800; padding: 6px 10px 2px; }\n    .g2-map-card-desc { font-size: 10.5px; color: var(--g2-muted-fg); padding: 0 10px 8px; }\n    .g2-modal-title { font-size: 24px; font-weight: var(--font-weight-default, 400); color: var(--g2-fg2); margin-bottom: 14px; }\n    .g2-quick-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 4px; }\n    .g2-quick { font-size: 11.5px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--g2-border); background: var(--g2-bg); font-weight: 700; }\n    .g2-quick:hover { border-color: var(--g2-primary); color: var(--g2-primary); }\n    .g2-field-label { font-size: 12.5px; font-weight: 700; margin: 12px 0 4px; }\n    .g2-modal input[type=text], .g2-modal input[type=number], .g2-modal input[type=datetime-local], .g2-modal select {\n      width: 100%; padding: 8px 11px; border: 1px solid var(--g2-border); border-radius: 8px; outline: none; background: var(--g2-bg);\n    }\n    .g2-two-col { display: flex; gap: 10px; }\n    .g2-two-col > div { flex: 1; }\n    .g2-theme-row { display: flex; gap: 8px; flex-wrap: wrap; }\n    .g2-theme-opt {\n      border: 2px solid var(--g2-border); border-radius: 10px; padding: 7px 10px;\n      text-align: center; cursor: pointer; font-size: 12px; font-weight: 700;\n      display: flex; align-items: center; gap: 7px;\n    }\n    .g2-theme-opt.g2-picked { border-color: var(--g2-primary); background: color-mix(in srgb, var(--g2-primary) 7%, transparent); }\n    .g2-check-row { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; margin-top: 12px; cursor: pointer; user-select: none; }\n    .g2-invitees { max-height: 160px; overflow-y: auto; border: 1px solid var(--g2-border-soft); border-radius: 8px; padding: 4px; }\n    .g2-invitee { display: flex; align-items: center; gap: 9px; padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 13.5px; }\n    .g2-invitee:hover { background: var(--g2-hover); }\n    .g2-invitee input { pointer-events: none; }\n    .g2-modal-actions { display: flex; flex-direction: row-reverse; justify-content: flex-start; gap: 6px; margin-top: 18px; }\n    .g2-modal-error { color: var(--g2-danger); font-size: 12.5px; margin-top: 8px; }\n    .g2-format-hint { font-size: 11.5px; color: var(--g2-muted-fg); margin-top: 4px; }\n  </style>\n\n  <div class=\"g2-toolbar\">\n    <div class=\"g2-toolbar-title\"><i class=\"fas fa-trophy\"></i> Tävlingar v2</div>\n    <div class=\"g2-toolbar-spacer\"></div>\n    <button class=\"g2-btn\" id=\"g2-reload\"><i class=\"fas fa-sync-alt\"></i> Uppdatera</button>\n    <button class=\"g2-btn g2-primary-btn\" id=\"g2-new-game\"><i class=\"fas fa-plus\"></i> Ny tävling</button>\n  </div>\n\n  <div class=\"g2-body\" id=\"g2-body\">\n    <div class=\"g2-loading\"><div class=\"g2-spinner\"></div> Laddar tävlingar…</div>\n  </div>\n\n  <div class=\"g2-modal-backdrop\" id=\"g2-modal-backdrop\">\n    <div class=\"g2-modal\">\n      <div class=\"g2-modal-title\">Ny tävling</div>\n\n      <div class=\"g2-quick-row\">\n        <button class=\"g2-quick\" data-g2-quick=\"powerhour\">Power hour (1h samtal)</button>\n        <button class=\"g2-quick\" data-g2-quick=\"dagsrace\">Dagens race (signerade)</button>\n        <button class=\"g2-quick\" data-g2-quick=\"veckostreak\">Veckostreak</button>\n        <button class=\"g2-quick\" data-g2-quick=\"fredagslotto\">Fredagslotto</button>\n      </div>\n\n      <div class=\"g2-modal-grid\">\n      <div class=\"g2-modal-main\">\n\n      <div class=\"g2-step\"><span class=\"g2-step-num\">1</span> Namn</div>\n      <input type=\"text\" id=\"g2-f-name\" placeholder=\"T.ex. Fredagsracet\">\n\n      <div class=\"g2-step\"><span class=\"g2-step-num\">2</span> Tävlingsform</div>\n      <div class=\"g2-fmt-grid\" id=\"g2-fmt-grid\"></div>\n      <div class=\"g2-format-hint\" id=\"g2-format-hint\"></div>\n\n      <div id=\"g2-f-metric-wrap\">\n        <div class=\"g2-step\"><span class=\"g2-step-num\">3</span> Vad räknas?</div>\n        <div class=\"g2-metric-row\" id=\"g2-metric-row\"></div>\n      </div>\n\n      <div id=\"g2-f-ordertag-wrap\" style=\"display:none;\">\n        <div class=\"g2-field-label\">Ordertagg</div>\n        <div class=\"g2-metric-row\" id=\"g2-ordertag-row\"></div>\n      </div>\n\n      <div class=\"g2-two-col\">\n        <div id=\"g2-f-target-wrap\">\n          <div class=\"g2-field-label\" id=\"g2-f-target-label\">Mål</div>\n          <input type=\"number\" id=\"g2-f-target\" min=\"1\" value=\"5\">\n        </div>\n        <div id=\"g2-f-ends-wrap\">\n          <div class=\"g2-field-label\">Slut-tid</div>\n          <input type=\"datetime-local\" id=\"g2-f-ends\">\n        </div>\n        <div id=\"g2-f-round-wrap\" style=\"display:none;\">\n          <div class=\"g2-field-label\">Rondlängd</div>\n          <div class=\"g2-metric-row\" id=\"g2-round-row\"></div>\n        </div>\n        <div id=\"g2-f-difficulty-wrap\" style=\"display:none;\">\n          <div class=\"g2-field-label\">Svårighetsgrad</div>\n          <div class=\"g2-metric-row\" id=\"g2-difficulty-row\"></div>\n        </div>\n      </div>\n\n      <div id=\"g2-f-map-wrap\" style=\"display:none;\">\n        <div class=\"g2-field-label\">Karta</div>\n        <div class=\"g2-map-row\" id=\"g2-map-row\"></div>\n      </div>\n\n      <div id=\"g2-f-teams-wrap\" style=\"display:none;\">\n        <div class=\"g2-field-label\">Lag — välj två</div>\n        <div class=\"g2-metric-row\" id=\"g2-team-row\"></div>\n      </div>\n\n      <div id=\"g2-f-theme-wrap\">\n        <div class=\"g2-field-label\">Tema</div>\n        <div class=\"g2-theme-row\" id=\"g2-theme-row\"></div>\n      </div>\n\n      <label class=\"g2-check-row\" id=\"g2-f-handicap-wrap\">\n        <input type=\"checkbox\" id=\"g2-f-handicap\">\n        Handikapp — utjämna mot senaste 30 dagarnas snitt\n      </label>\n\n      <div id=\"g2-f-invitees-wrap\">\n        <div class=\"g2-step\"><span class=\"g2-step-num\">4</span> <span id=\"g2-invitees-label\">Bjud in kollegor</span></div>\n        <div class=\"g2-invitees\" id=\"g2-invitees\"></div>\n      </div>\n\n      </div>\n      <div class=\"g2-info-panel\" id=\"g2-info-panel\"></div>\n      </div>\n\n      <div class=\"g2-modal-error\" id=\"g2-modal-error\"></div>\n      <div class=\"g2-modal-actions\">\n        <button class=\"g2-btn g2-primary-btn\" id=\"g2-modal-save\"><i class=\"fas fa-flag-checkered\"></i> Starta tävling</button>\n        <button class=\"g2-btn\" id=\"g2-modal-cancel\">Avbryt</button>\n      </div>\n    </div>\n  </div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var POLL_MS = 45000;\n      var LANE_COLORS = ['#FFD54F', '#4FC3F7', '#FF8A65', '#AED581', '#CE93D8', '#F48FB1', '#80CBC4', '#FFAB91'];\n      var DAY_MS = 86400000;\n\n      if (window.__g2Cleanup) { try { window.__g2Cleanup(); } catch (e) {} }\n\n      var METRICS = {\n        signed: { label: 'Signerade avtal', unit: 'st', short: 'signerade', icon: 'file-signature' },\n        distributed: { label: 'Skickade avtal', unit: 'st', short: 'skickade', icon: 'paper-plane' },\n        calls: { label: 'Samtal', unit: 'st', short: 'samtal', icon: 'phone-alt' },\n        connected: { label: 'Kopplade samtal', unit: 'st', short: 'kopplade', icon: 'headset' },\n        talktime: { label: 'Samtalstid', unit: 'min', short: 'min samtalstid', icon: 'stopwatch' },\n        ordertag: { label: 'Ordrar (tagg)', unit: 'st', short: 'ordrar', icon: 'tag' },\n        activetime: { label: 'Aktiv tid', unit: 'min', short: 'min aktiv', icon: 'user-clock' }\n      };\n\n      var FORMATS = {\n        first_to: { label: 'Först till X', short: 'Först till X', icon: 'flag-checkered', hint: 'Den som först når målet vinner.' },\n        most_before: { label: 'Mest före en tidpunkt', short: 'Mest före tid', icon: 'stopwatch', hint: 'Flest när tiden går ut vinner.' },\n        ghost: { label: 'Spöklopp', short: 'Spöklopp', icon: 'ghost', hint: 'Du mot din bästa 7-dagarsperiod de senaste 12 veckorna. Ingen inbjudan behövs.' },\n        tournament: { label: 'Turnering', short: 'Turnering', icon: 'sitemap', hint: 'Utslagning 1 mot 1 i ronder. Alla inbjudna deltar automatiskt.' },\n        tug: { label: 'Dragkamp', short: 'Dragkamp', icon: 'arrows-alt-h', hint: 'Två lag drar i repet — lagens summor avgör. Alla i lagen deltar automatiskt.' },\n        relay: { label: 'Stafett', short: 'Stafett', icon: 'exchange-alt', hint: 'Första lag där ALLA medlemmar nått målet vinner.' },\n        streak: { label: 'Streak', short: 'Streak', icon: 'fire', hint: 'Flest dagar i rad med minst målet per dag.' },\n        bingo: { label: 'Bingo', short: 'Bingo', icon: 'th-large', hint: 'Samma bricka för alla — först till en rad (vågrätt, lodrätt eller diagonal) vinner.' },\n        lottery: { label: 'Lotteri', short: 'Lotteri', icon: 'ticket-alt', hint: 'Varje enhet är en lott. Vinnaren lottas när tiden går ut — mer jobb, större chans.' },\n        tanks: { label: 'Stridsvagnsduell', short: 'Duell', icon: 'crosshairs', hint: 'Duell 1 mot 1. Varje resultat ger en granat — sikta, skjut och flytta i sidled. Först till 0 HP förlorar.' }\n      };\n      var FORMAT_ORDER = ['first_to', 'most_before', 'tug', 'relay', 'tournament', 'streak', 'bingo', 'lottery', 'tanks', 'ghost'];\n      var FORMAT_DESC = {\n        first_to: 'Ett race där varje resultat flyttar din pjäs framåt på banan. Den som först når målet vinner direkt — inget slutdatum behövs.',\n        most_before: 'Racet pågår till en bestämd tidpunkt. När tiden går ut vinner den som samlat mest. Bra som dagstävling eller power hour.',\n        tug: 'Två lag drar i samma rep. Varje resultat drar repet mot ditt lags sida, och laget med störst summa när tiden går ut vinner. Alla i lagen är med automatiskt.',\n        relay: 'Lagkamp där ALLA i laget måste nå sitt personliga mål. Första laget där varje medlem är i mål vinner — de snabba hjälper de långsamma över linjen.',\n        tournament: 'Utslagning 1 mot 1. Varje rond är ett tidsfönster — den med flest resultat under ronden går vidare. Vinnaren av finalen tar hela turneringen.',\n        streak: 'Håll lågan vid liv! Nå minst målet varje dag — längsta obrutna sviten av dagar vinner när tiden går ut.',\n        bingo: 'Alla får samma bricka med 16 delmål (samtal, signeringar, samtalstid med mera). Först till en full rad — vågrätt, lodrätt eller diagonalt — vinner.',\n        lottery: 'Varje resultat ger en lott. När tiden går ut dras en vinnare slumpmässigt — ju mer du jobbat, desto större chans, men alla med minst en lott kan vinna.',\n        tanks: 'Duell 1 mot 1: varje resultat laddar en granat. Sikta med vinkel och kraft — och flytta smart: max två steg per granat, och utan granat står du helt still. Fyra träffar avgör duellen.',\n        ghost: 'Solo-utmaning: ett spöke rör sig i takten från din bästa 7-dagarsperiod de senaste 12 veckorna. Hinner du före spöket har du satt nytt personligt rekord.'\n      };\n      var MAP_DESCRIPTIONS = { desert: 'Platt och öppen — raka skott', valley: 'Kulle i mitten — loba över', city: 'Hus i vägen — svåra vinklar' };\n\n      var THEMES = [\n        { id: 'horses', label: 'Hästar', icon: 'horse' },\n        { id: 'boats', label: 'Segelbåtar', icon: 'ship' },\n        { id: 'cars', label: 'Racerbilar', icon: 'car-side' },\n        { id: 'rockets', label: 'Raketer', icon: 'rocket' },\n        { id: 'skis', label: 'Skidor', icon: 'skiing-nordic' },\n        { id: 'cycles', label: 'Cyklar', icon: 'bicycle' },\n        { id: 'balloons', label: 'Ballonger', icon: 'cloud' }\n      ];\n\n      var state = {\n        me: null,\n        users: [],\n        usersById: {},\n        teams: [],\n        teamsById: {},\n        orderTags: [],\n        games: [],\n        results: {},          /* gameId -> computed result */\n        loaded: false,\n        pickedTheme: 'horses',\n        handicapCache: {},    /* gameId -> {userId: factor} */\n        ghostCache: {},       /* gameId -> {record} */\n        savingWins: {},\n        bingoViewUser: {},    /* gameId -> userId whose board is shown */\n        tankAim: {},          /* gameId -> {angle, power} */\n        animatedShots: {},    /* shotId -> true once replayed */\n        pickedFormat: 'first_to',\n        pickedMetric: 'signed',\n        pickedMap: 'desert',\n        pickedRound: 24,\n        pickedDifficulty: '1',\n        pickedOrderTag: null,\n        pickedTeams: []\n      };\n\n      /* ================= utils ================= */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(method, path, params, body) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += '?' + qs;\n        }\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) {\n          opts.headers['Content-Type'] = 'application/json';\n          opts.body = JSON.stringify(body);\n        }\n        return fetch(url, opts).then(function (r) {\n          if (!r.ok) { var err = new Error('HTTP ' + r.status); err.status = r.status; throw err; }\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n      function newId() { return 'g' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }\n      function firstName(user) { return (((user && (user.fullName || user.username)) || '?').split(' '))[0]; }\n      function fullName(user) { return (user && (user.fullName || user.username)) || 'Okänd'; }\n      function dayKey(d) { var x = new Date(d); return x.getFullYear() + '-' + (x.getMonth() + 1) + '-' + x.getDate(); }\n      function startOfDay(d) { var x = new Date(d); x.setHours(0, 0, 0, 0); return x; }\n      function shortDate(iso) {\n        try { return new Date(iso).toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); } catch (e) { return ''; }\n      }\n      function formatValue(metric, v) {\n        v = Math.round(v * 10) / 10;\n        if (metric === 'talktime' || metric === 'activetime') return v + ' min';\n        return String(Math.round(v));\n      }\n\n      /* Deterministic PRNG so every client computes the same result (lottery, bingo). */\n      function strSeed(str) {\n        var h = 1779033703 ^ str.length;\n        for (var i = 0; i < str.length; i++) {\n          h = Math.imul(h ^ str.charCodeAt(i), 3432918353);\n          h = (h << 13) | (h >>> 19);\n        }\n        return (h >>> 0);\n      }\n      function mulberry32(a) {\n        return function () {\n          a |= 0; a = (a + 1831565813) | 0;\n          var t = Math.imul(a ^ (a >>> 15), 1 | a);\n          t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n          return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n        };\n      }\n\n      /* ================= sprites ================= */\n      function sprite(theme, color) {\n        if (theme === 'boats') {\n          return '<svg class=\"g2-sprite\" width=\"44\" height=\"38\" viewBox=\"0 0 44 38\"><g>' +\n            '<path d=\"M4 27 L40 27 L33 34 L10 34 Z\" fill=\"' + color + '\"/>' +\n            '<rect x=\"21\" y=\"4\" width=\"2\" height=\"23\" fill=\"#5d4037\"/>' +\n            '<path d=\"M23 5 L37 24 L23 24 Z\" fill=\"#ffffff\" opacity=\"0.95\"/>' +\n            '<path d=\"M20 8 L9 24 L20 24 Z\" fill=\"' + color + '\" opacity=\"0.75\"/>' +\n            '</g></svg>';\n        }\n        if (theme === 'cars') {\n          return '<svg class=\"g2-sprite\" width=\"48\" height=\"26\" viewBox=\"0 0 48 26\"><g>' +\n            '<path d=\"M4 16 L8 9 C9 7, 11 6, 14 6 L26 6 C30 6, 33 8, 36 11 L44 13 C46 13.5, 46 16, 44 17 L4 17 Z\" fill=\"' + color + '\"/>' +\n            '<path d=\"M15 8 L24 8 L27 12 L14 12 Z\" fill=\"#cfe8ff\"/>' +\n            '<circle cx=\"13\" cy=\"18\" r=\"5\" fill=\"#1b1b1b\"/><circle cx=\"13\" cy=\"18\" r=\"2\" fill=\"#9e9e9e\"/>' +\n            '<circle cx=\"35\" cy=\"18\" r=\"5\" fill=\"#1b1b1b\"/><circle cx=\"35\" cy=\"18\" r=\"2\" fill=\"#9e9e9e\"/>' +\n            '</g></svg>';\n        }\n        if (theme === 'rockets') {\n          return '<svg class=\"g2-sprite\" width=\"50\" height=\"24\" viewBox=\"0 0 50 24\"><g>' +\n            '<path d=\"M2 12 L10 8 L10 16 Z\" fill=\"#FF8F00\"/>' +\n            '<path d=\"M6 12 L12 10 L12 14 Z\" fill=\"#FFD54F\"/>' +\n            '<path d=\"M10 6 L34 6 C42 6, 48 12, 48 12 C48 12, 42 18, 34 18 L10 18 C12 14, 12 10, 10 6 Z\" fill=\"' + color + '\"/>' +\n            '<circle cx=\"34\" cy=\"12\" r=\"3.4\" fill=\"#cfe8ff\" stroke=\"#ffffff\"/>' +\n            '<path d=\"M12 4 L20 6 L12 8 Z M12 16 L20 18 L12 20 Z\" fill=\"' + color + '\" opacity=\"0.7\"/>' +\n            '</g></svg>';\n        }\n        if (theme === 'skis') {\n          return '<svg class=\"g2-sprite\" width=\"40\" height=\"36\" viewBox=\"0 0 40 36\"><g>' +\n            '<circle cx=\"24\" cy=\"7\" r=\"4\" fill=\"' + color + '\"/>' +\n            '<path d=\"M22 11 L15 20 L26 24\" stroke=\"' + color + '\" stroke-width=\"3.4\" fill=\"none\" stroke-linecap=\"round\"/>' +\n            '<path d=\"M17 14 L28 17\" stroke=\"' + color + '\" stroke-width=\"2.4\" stroke-linecap=\"round\"/>' +\n            '<path d=\"M28 17 L33 27\" stroke=\"#8d6e63\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>' +\n            '<path d=\"M6 30 L38 28\" stroke=\"#37474f\" stroke-width=\"2.4\" stroke-linecap=\"round\"/>' +\n            '<path d=\"M4 33 L36 31\" stroke=\"#37474f\" stroke-width=\"2.4\" stroke-linecap=\"round\"/>' +\n            '</g></svg>';\n        }\n        if (theme === 'cycles') {\n          return '<svg class=\"g2-sprite\" width=\"46\" height=\"32\" viewBox=\"0 0 46 32\"><g>' +\n            '<circle cx=\"11\" cy=\"23\" r=\"7\" fill=\"none\" stroke=\"#263238\" stroke-width=\"2\"/>' +\n            '<circle cx=\"35\" cy=\"23\" r=\"7\" fill=\"none\" stroke=\"#263238\" stroke-width=\"2\"/>' +\n            '<path d=\"M11 23 L20 12 L30 12 L35 23 M20 12 L23 23 L11 23\" stroke=\"' + color + '\" stroke-width=\"2.4\" fill=\"none\" stroke-linecap=\"round\"/>' +\n            '<circle cx=\"26\" cy=\"6\" r=\"3.4\" fill=\"' + color + '\"/>' +\n            '<path d=\"M26 9 L23 15 L29 17\" stroke=\"' + color + '\" stroke-width=\"3\" fill=\"none\" stroke-linecap=\"round\"/>' +\n            '</g></svg>';\n        }\n        if (theme === 'balloons') {\n          return '<svg class=\"g2-sprite\" width=\"34\" height=\"42\" viewBox=\"0 0 34 42\"><g>' +\n            '<ellipse cx=\"17\" cy=\"14\" rx=\"13\" ry=\"14\" fill=\"' + color + '\"/>' +\n            '<path d=\"M17 0 C11 8, 11 20, 17 28 M17 0 C23 8, 23 20, 17 28\" stroke=\"rgba(255,255,255,0.5)\" stroke-width=\"1.6\" fill=\"none\"/>' +\n            '<path d=\"M11 27 L13 33 L21 33 L23 27\" stroke=\"#5d4037\" stroke-width=\"1.4\" fill=\"none\"/>' +\n            '<rect x=\"12.5\" y=\"33\" width=\"9\" height=\"7\" rx=\"1.6\" fill=\"#8d6e63\"/>' +\n            '</g></svg>';\n        }\n        /* horses (default) */\n        return '<svg class=\"g2-sprite\" width=\"46\" height=\"34\" viewBox=\"0 0 46 34\"><g>' +\n          '<ellipse cx=\"22\" cy=\"17\" rx=\"12\" ry=\"7\" fill=\"' + color + '\"/>' +\n          '<path d=\"M32 13 L40 7 L42 11 L36 16 Z\" fill=\"' + color + '\"/>' +\n          '<circle cx=\"40\" cy=\"9\" r=\"3.4\" fill=\"' + color + '\"/>' +\n          '<path d=\"M43 8 L46 9 L43 10.5 Z\" fill=\"#3b2a1a\"/>' +\n          '<path d=\"M38 5 L39.5 2.2 L41 5.2 Z\" fill=\"#3b2a1a\"/>' +\n          '<path d=\"M12 15 C8 13, 6 15, 4 13 C5 17, 8 19, 11 19 Z\" fill=\"' + color + '\" opacity=\"0.85\"/>' +\n          '<path d=\"M14 23 L11 31 M20 24 L19 32 M26 24 L28 32 M31 21 L34 30\" stroke=\"' + color + '\" stroke-width=\"2.6\" stroke-linecap=\"round\" fill=\"none\"/>' +\n          '<circle cx=\"40.5\" cy=\"8\" r=\"0.8\" fill=\"#1b1b1b\"/>' +\n          '</g></svg>';\n      }\n\n      /* ================= metric data ================= */\n      function metricEndpoint(metric) {\n        if (metric === 'signed' || metric === 'distributed') return '/offers/statistics-v1/own/issue_1238_status_user_fi';\n        if (metric === 'activetime') return '/users/statistics-v1/own/issue_1238_active_user_time';\n        if (metric === 'ordertag') return '/orders/statistics-v1/own/issue_1238_user_tag';\n        return '/dial/statistics-v1/own/issue_1238_2';\n      }\n\n      function rowsToEvents(metric, rows, orderTagId) {\n        var events = [];\n        (rows || []).forEach(function (row) {\n          var v = 0;\n          if (metric === 'signed' || metric === 'distributed') {\n            if (row.status !== metric) return;\n            v = row.count || 0;\n          } else if (metric === 'calls') v = row.count || 0;\n          else if (metric === 'connected') v = row.connectedCount || 0;\n          else if (metric === 'talktime') v = (row.connectedDuration || 0) / 60000;\n          else if (metric === 'activetime') v = (row.activeDuration || 0) / 60000;\n          else if (metric === 'ordertag') {\n            if (orderTagId && String(row.tagId) !== String(orderTagId)) return;\n            v = row.count || 0;\n          }\n          if (!v) return;\n          events.push({ userId: row.userId, t: new Date(row.date || row.intervalStart), v: v });\n        });\n        return events;\n      }\n\n      /* Fetch hourly metric events between two times. */\n      function fetchEvents(metric, from, to, orderTagId) {\n        return api('GET', metricEndpoint(metric), {\n          from: new Date(from).toISOString(),\n          to: new Date(to).toISOString(),\n          fixedIntervalType: 'hour'\n        }).then(function (rows) {\n          return rowsToEvents(metric, rows, orderTagId);\n        }).catch(function () { return []; });\n      }\n\n      function aggregate(events, userIds) {\n        var totals = {}, perDay = {};\n        userIds.forEach(function (id) { totals[id] = 0; perDay[id] = {}; });\n        events.forEach(function (ev) {\n          if (!(ev.userId in totals)) return;\n          totals[ev.userId] += ev.v;\n          var dk = dayKey(ev.t);\n          perDay[ev.userId][dk] = (perDay[ev.userId][dk] || 0) + ev.v;\n        });\n        return { totals: totals, perDay: perDay };\n      }\n\n      /* ================= data assembly ================= */\n      function load() {\n        return Promise.all([\n          api('GET', '/users/users-v1'),\n          api('GET', '/users/teams-v1'),\n          state.orderTags.length ? Promise.resolve(null) : api('GET', '/orders/tags-v1').catch(function () { return null; })\n        ]).then(function (results) {\n          state.users = (results[0] || []).filter(function (u) { return u.type !== 'systemUser'; });\n          state.usersById = {};\n          state.users.forEach(function (u) { state.usersById[u.id] = u; });\n          state.teams = results[1] || [];\n          state.teamsById = {};\n          state.teams.forEach(function (t) { state.teamsById[t.id] = t; });\n          if (results[2]) {\n            state.orderTags = Array.isArray(results[2]) ? results[2] : (results[2].tags || []);\n          }\n          var uid = cookies()['s2_uid'];\n          state.me = state.usersById[uid] || state.me;\n          if (!state.me) {\n            return api('GET', '/users/users-v1/me').then(function (me) { state.me = me; });\n          }\n        }).then(function () {\n          assembleGames();\n          return computeAll();\n        }).then(function () {\n          render();\n          recordWinsIfNeeded();\n          gcOwnReference();\n        }).catch(function () {\n          var body = el('g2-body');\n          if (body) body.innerHTML = '<div class=\"g2-empty\">Kunde inte ladda tävlingar. Försök igen.</div>';\n        });\n      }\n\n      function assembleGames() {\n        var games = [];\n        state.users.forEach(function (u) {\n          var docs = (u.reference || {}).ssg2Games || {};\n          Object.keys(docs).forEach(function (gameId) {\n            var doc = docs[gameId];\n            if (!doc || !doc.id) return;\n            games.push({ doc: doc, creatorId: u.id, participants: participantsFor(doc, u.id) });\n          });\n        });\n        games.sort(function (a, b) {\n          return new Date(b.doc.createdAt || 0).getTime() - new Date(a.doc.createdAt || 0).getTime();\n        });\n        state.games = games;\n        state.loaded = true;\n      }\n\n      function teamMembers(teamId) {\n        return state.users.filter(function (u) {\n          return u.active !== false && u.teamId === teamId;\n        }).map(function (u) { return u.id; });\n      }\n\n      function participantsFor(doc, creatorId) {\n        if (doc.format === 'ghost') return [creatorId];\n        if (doc.format === 'tug' || doc.format === 'relay') {\n          var ids = [];\n          (doc.teams || []).forEach(function (t) { ids = ids.concat(teamMembers(t.teamId)); });\n          return ids;\n        }\n        if (doc.format === 'tournament') {\n          /* Everyone invited participates automatically. */\n          var all = [creatorId].concat(doc.invitedUserIds || []);\n          return all.filter(function (id, i) { return all.indexOf(id) === i && state.usersById[id]; });\n        }\n        var participants = [creatorId];\n        state.users.forEach(function (u) {\n          if (u.id === creatorId) return;\n          var joined = (u.reference || {}).ssg2Joined || {};\n          if (joined[doc.id]) participants.push(u.id);\n        });\n        return participants;\n      }\n\n      function needsInvite(format) {\n        return format === 'first_to' || format === 'most_before' || format === 'streak' ||\n          format === 'bingo' || format === 'lottery' || format === 'tanks';\n      }\n\n      function myInviteFor(game) {\n        if (!state.me || game.doc.canceledAt) return false;\n        if (!needsInvite(game.doc.format)) return false;\n        if ((game.doc.invitedUserIds || []).indexOf(state.me.id) === -1) return false;\n        if (game.participants.indexOf(state.me.id) !== -1) return false;\n        var declined = ((state.me.reference || {}).ssg2Declined) || {};\n        if (declined[game.doc.id]) return false;\n        var result = state.results[game.doc.id];\n        if (result && result.finished) return false;\n        return true;\n      }\n\n      function isRelevantToMe(game) {\n        if (!state.me) return false;\n        if (game.creatorId === state.me.id) return true;\n        if (game.participants.indexOf(state.me.id) !== -1) return true;\n        if ((game.doc.invitedUserIds || []).indexOf(state.me.id) !== -1) return true;\n        return false;\n      }\n\n      /* ================= per-format computation ================= */\n      function computeAll() {\n        var relevant = state.games.filter(function (g) { return isRelevantToMe(g) && !g.doc.canceledAt; });\n        return Promise.all(relevant.map(function (game) {\n          return computeGame(game).then(function (result) {\n            state.results[game.doc.id] = result;\n          }).catch(function () {\n            state.results[game.doc.id] = state.results[game.doc.id] || { finished: false, winners: [], scores: {} };\n          });\n        }));\n      }\n\n      function gameWindow(doc) {\n        var from = new Date(doc.startAt || doc.createdAt);\n        var hardEnd = doc.endsAt ? new Date(doc.endsAt) : null;\n        var to = hardEnd && hardEnd.getTime() < Date.now() ? hardEnd : new Date();\n        return { from: from, to: to, hardEnd: hardEnd };\n      }\n\n      function handicapFactors(game) {\n        var doc = game.doc;\n        if (!doc.handicap) return Promise.resolve(null);\n        if (state.handicapCache[doc.id]) return Promise.resolve(state.handicapCache[doc.id]);\n        var start = new Date(doc.startAt || doc.createdAt);\n        var from = new Date(start.getTime() - 30 * DAY_MS);\n        return fetchEvents(doc.metric, from, start, doc.orderTagId).then(function (events) {\n          var agg = aggregate(events, game.participants);\n          var avgs = game.participants.map(function (id) { return agg.totals[id] / 30; });\n          var mean = avgs.reduce(function (s, v) { return s + v; }, 0) / Math.max(1, avgs.length);\n          var factors = {};\n          game.participants.forEach(function (id, i) {\n            var f = avgs[i] > 0.001 ? mean / avgs[i] : 3;\n            factors[id] = Math.round(Math.min(3, Math.max(0.5, f)) * 100) / 100;\n          });\n          state.handicapCache[doc.id] = factors;\n          return factors;\n        });\n      }\n\n      function computeGame(game) {\n        var doc = game.doc;\n        var win = gameWindow(doc);\n        if (doc.format === 'bingo') return computeBingo(game, win);\n        if (doc.format === 'ghost') return computeGhost(game, win);\n        if (doc.format === 'tanks') return computeTanks(game, win);\n        return Promise.all([\n          fetchEvents(doc.metric, win.from, win.to, doc.orderTagId),\n          handicapFactors(game)\n        ]).then(function (results) {\n          var agg = aggregate(results[0], game.participants);\n          var factors = results[1];\n          var scores = {}, raw = {};\n          game.participants.forEach(function (id) {\n            raw[id] = agg.totals[id];\n            scores[id] = factors ? agg.totals[id] * factors[id] : agg.totals[id];\n          });\n          var base = { scores: scores, raw: raw, factors: factors, perDay: agg.perDay, finished: false, winners: [] };\n          if (doc.format === 'first_to') return computeFirstTo(game, win, base);\n          if (doc.format === 'most_before') return computeMostBefore(game, win, base);\n          if (doc.format === 'tug') return computeTug(game, win, base);\n          if (doc.format === 'relay') return computeRelay(game, win, base);\n          if (doc.format === 'streak') return computeStreak(game, win, base);\n          if (doc.format === 'lottery') return computeLottery(game, win, base);\n          if (doc.format === 'tournament') return computeTournament(game, win, results[0], base);\n          return base;\n        });\n      }\n\n      function maxBy(ids, scores) {\n        var best = -1, winners = [];\n        ids.forEach(function (id) {\n          var v = scores[id] || 0;\n          if (v > best + 1e-9) { best = v; winners = [id]; }\n          else if (Math.abs(v - best) <= 1e-9) winners.push(id);\n        });\n        return { best: best, winners: winners };\n      }\n\n      function computeFirstTo(game, win, base) {\n        var target = game.doc.target || 1;\n        var reached = game.participants.filter(function (id) { return base.scores[id] >= target; });\n        var expired = win.hardEnd && Date.now() > win.hardEnd.getTime();\n        if (reached.length || expired) {\n          base.finished = true;\n          if (reached.length) {\n            var m = maxBy(reached, base.scores);\n            base.winners = m.winners;\n          }\n        }\n        base.target = target;\n        return base;\n      }\n\n      function computeMostBefore(game, win, base) {\n        if (win.hardEnd && Date.now() > win.hardEnd.getTime()) {\n          base.finished = true;\n          var m = maxBy(game.participants, base.scores);\n          if (m.best > 0) base.winners = m.winners;\n        }\n        return base;\n      }\n\n      function computeTug(game, win, base) {\n        var teams = (game.doc.teams || []).map(function (t) {\n          var members = teamMembers(t.teamId);\n          var sum = 0;\n          members.forEach(function (id) { sum += base.scores[id] || 0; });\n          return { teamId: t.teamId, name: t.name, members: members, sum: sum };\n        });\n        base.teams = teams;\n        if (win.hardEnd && Date.now() > win.hardEnd.getTime()) {\n          base.finished = true;\n          if (teams.length === 2 && teams[0].sum !== teams[1].sum) {\n            var winner = teams[0].sum > teams[1].sum ? teams[0] : teams[1];\n            base.winners = winner.members;\n            base.winnerTeam = winner;\n          }\n        }\n        return base;\n      }\n\n      function computeRelay(game, win, base) {\n        var target = game.doc.target || 1;\n        var teams = (game.doc.teams || []).map(function (t) {\n          var members = teamMembers(t.teamId);\n          var doneCount = 0, fractionSum = 0;\n          var memberStates = members.map(function (id) {\n            var v = base.scores[id] || 0;\n            var frac = Math.min(1, v / target);\n            if (v >= target) doneCount++;\n            fractionSum += frac;\n            return { userId: id, value: v, fraction: frac };\n          });\n          return {\n            teamId: t.teamId, name: t.name, members: memberStates,\n            complete: members.length > 0 && doneCount === members.length,\n            progress: members.length ? fractionSum / members.length : 0\n          };\n        });\n        base.teams = teams;\n        base.target = target;\n        var completeTeams = teams.filter(function (t) { return t.complete; });\n        var expired = win.hardEnd && Date.now() > win.hardEnd.getTime();\n        if (completeTeams.length || expired) {\n          base.finished = true;\n          var winner = null;\n          if (completeTeams.length === 1) winner = completeTeams[0];\n          else if (completeTeams.length > 1 || expired) {\n            teams.forEach(function (t) { if (!winner || t.progress > winner.progress) winner = t; });\n            if (winner && winner.progress <= 0) winner = null;\n          }\n          if (winner) {\n            base.winnerTeam = winner;\n            base.winners = winner.members.map(function (m) { return m.userId; });\n          }\n        }\n        return base;\n      }\n\n      function computeStreak(game, win, base) {\n        var perDayTarget = game.doc.target || 1;\n        var streaks = {};\n        var from = startOfDay(win.from);\n        var toDay = startOfDay(win.to);\n        game.participants.forEach(function (id) {\n          var best = 0, current = 0, run = 0;\n          for (var t = from.getTime(); t <= toDay.getTime(); t += DAY_MS) {\n            var v = (base.perDay[id] || {})[dayKey(new Date(t))] || 0;\n            if (v >= perDayTarget) { run++; if (run > best) best = run; }\n            else run = 0;\n          }\n          current = run;\n          streaks[id] = { best: best, current: current };\n          base.scores[id] = best;\n        });\n        base.streaks = streaks;\n        base.dayRange = { from: from, to: toDay };\n        if (win.hardEnd && Date.now() > win.hardEnd.getTime()) {\n          base.finished = true;\n          var m = maxBy(game.participants, base.scores);\n          if (m.best > 0) base.winners = m.winners;\n        }\n        return base;\n      }\n\n      function computeLottery(game, win, base) {\n        var tickets = {};\n        var total = 0;\n        var isTime = game.doc.metric === 'talktime' || game.doc.metric === 'activetime';\n        game.participants.forEach(function (id) {\n          var t = Math.floor(isTime ? (base.raw[id] || 0) / 10 : (base.raw[id] || 0));\n          tickets[id] = t;\n          total += t;\n        });\n        base.tickets = tickets;\n        base.totalTickets = total;\n        if (win.hardEnd && Date.now() > win.hardEnd.getTime()) {\n          base.finished = true;\n          if (total > 0) {\n            /* Deterministic draw: same seed on every client. */\n            var seedStr = game.doc.id + '|' + game.participants.slice().sort().map(function (id) {\n              return id + ':' + tickets[id];\n            }).join(',');\n            var rand = mulberry32(strSeed(seedStr))();\n            var pick = rand * total;\n            var acc = 0;\n            for (var i = 0; i < game.participants.length; i++) {\n              var id = game.participants[i];\n              acc += tickets[id];\n              if (pick < acc) { base.winners = [id]; break; }\n            }\n          }\n        }\n        return base;\n      }\n\n      function computeGhost(game, win, base0) {\n        var doc = game.doc;\n        var recordPromise = state.ghostCache[doc.id]\n          ? Promise.resolve(state.ghostCache[doc.id])\n          : fetchEvents(doc.metric, new Date(new Date(doc.startAt).getTime() - 84 * DAY_MS), new Date(doc.startAt), doc.orderTagId)\n            .then(function (events) {\n              var per = {};\n              events.forEach(function (ev) {\n                if (ev.userId !== game.creatorId) return;\n                var dk = dayKey(ev.t);\n                per[dk] = (per[dk] || 0) + ev.v;\n              });\n              var start = startOfDay(new Date(new Date(doc.startAt).getTime() - 84 * DAY_MS));\n              var best = 0;\n              for (var t = start.getTime(); t <= Date.now(); t += DAY_MS) {\n                var sum = 0;\n                for (var d = 0; d < 7; d++) sum += per[dayKey(new Date(t + d * DAY_MS))] || 0;\n                if (sum > best) best = sum;\n              }\n              var cache = { record: Math.round(best * 10) / 10 };\n              state.ghostCache[doc.id] = cache;\n              return cache;\n            });\n\n        return Promise.all([\n          fetchEvents(doc.metric, gameWindow(doc).from, gameWindow(doc).to, doc.orderTagId),\n          recordPromise\n        ]).then(function (results) {\n          var agg = aggregate(results[0], [game.creatorId]);\n          var record = Math.max(1, results[1].record);\n          var start = new Date(doc.startAt).getTime();\n          var duration = 7 * DAY_MS;\n          var elapsed = Math.min(1, (Date.now() - start) / duration);\n          var base = {\n            scores: {}, raw: {}, perDay: agg.perDay, finished: false, winners: [],\n            ghost: { record: record, progress: record * elapsed, elapsed: elapsed, hadRecord: results[1].record > 0 }\n          };\n          base.scores[game.creatorId] = agg.totals[game.creatorId];\n          base.raw[game.creatorId] = agg.totals[game.creatorId];\n          base.target = record;\n          if (agg.totals[game.creatorId] >= record) {\n            base.finished = true;\n            base.winners = [game.creatorId];\n          } else if (elapsed >= 1) {\n            base.finished = true; /* ghost won — no winners recorded */\n          }\n          return base;\n        });\n      }\n\n      function computeTournament(game, win, events, base) {\n        var doc = game.doc;\n        var roundMs = (doc.roundHours || 24) * 3600000;\n        var seeded = game.participants.slice().sort();\n        /* Bracket size: next power of two, byes auto-advance. */\n        var size = 1;\n        while (size < seeded.length) size *= 2;\n        var slots = seeded.concat([]);\n        while (slots.length < size) slots.push(null);\n        var totalRounds = Math.round(Math.log(size) / Math.log(2));\n        var start = new Date(doc.startAt).getTime();\n\n        /* Score per user per round window. */\n        function roundScore(userId, round) {\n          if (!userId) return -1;\n          var from = start + round * roundMs;\n          var to = from + roundMs;\n          var sum = 0;\n          events.forEach(function (ev) {\n            if (ev.userId !== userId) return;\n            var t = ev.t.getTime();\n            if (t >= from && t < to) sum += ev.v;\n          });\n          return sum;\n        }\n\n        var rounds = [];\n        var current = slots;\n        for (var r = 0; r < totalRounds; r++) {\n          var roundEndsAt = start + (r + 1) * roundMs;\n          var decided = Date.now() >= roundEndsAt;\n          var live = !decided && Date.now() >= start + r * roundMs;\n          var matches = [];\n          var next = [];\n          for (var i = 0; i < current.length; i += 2) {\n            var a = current[i], b = current[i + 1];\n            var scoreA = a ? roundScore(a, r) : -1;\n            var scoreB = b ? roundScore(b, r) : -1;\n            var winner = null;\n            if (!a && !b) winner = null;\n            else if (a && !b) winner = a;\n            else if (b && !a) winner = b;\n            else if (decided) {\n              if (scoreA > scoreB) winner = a;\n              else if (scoreB > scoreA) winner = b;\n              else winner = (a < b ? a : b); /* deterministic tie-break */\n            }\n            matches.push({ a: a, b: b, scoreA: scoreA, scoreB: scoreB, winner: decided || (!a || !b) ? winner : null, live: live });\n            next.push(decided || (!a || !b) ? winner : null);\n          }\n          rounds.push({ matches: matches, decided: decided, live: live, endsAt: new Date(roundEndsAt) });\n          current = next;\n        }\n        base.rounds = rounds;\n        base.champion = current[0] || null;\n        base.endsAtComputed = new Date(start + totalRounds * roundMs);\n        if (base.champion) {\n          base.finished = true;\n          base.winners = [base.champion];\n        } else if (Date.now() >= start + totalRounds * roundMs) {\n          base.finished = true;\n        }\n        return base;\n      }\n\n      /* ================= bingo ================= */\n      var BINGO_TEMPLATES = [\n        { m: 'calls', s: 'day', a: [10, 20, 35], label: 'samtal på en dag' },\n        { m: 'calls', s: 'total', a: [40, 80, 150], label: 'samtal totalt' },\n        { m: 'connected', s: 'day', a: [5, 10, 18], label: 'kopplade på en dag' },\n        { m: 'connected', s: 'total', a: [20, 40, 80], label: 'kopplade totalt' },\n        { m: 'talktime', s: 'day', a: [20, 45, 90], label: 'min samtalstid på en dag' },\n        { m: 'talktime', s: 'total', a: [60, 150, 300], label: 'min samtalstid totalt' },\n        { m: 'signed', s: 'day', a: [1, 2, 3], label: 'signerade på en dag' },\n        { m: 'signed', s: 'total', a: [2, 5, 9], label: 'signerade totalt' },\n        { m: 'distributed', s: 'day', a: [2, 3, 5], label: 'skickade avtal på en dag' },\n        { m: 'distributed', s: 'total', a: [4, 8, 15], label: 'skickade avtal totalt' }\n      ];\n\n      function bingoBoard(doc) {\n        var rand = mulberry32(strSeed('bingo|' + doc.id));\n        var difficulty = parseInt(doc.difficulty || 1, 10);\n        var pool = [];\n        BINGO_TEMPLATES.forEach(function (t) {\n          pool.push({ m: t.m, s: t.s, amount: t.a[difficulty], label: t.a[difficulty] + ' ' + t.label });\n          /* A second, tougher variant of each template gives 20 candidates. */\n          var harder = Math.round(t.a[difficulty] * 1.5);\n          pool.push({ m: t.m, s: t.s, amount: harder, label: harder + ' ' + t.label });\n        });\n        /* Deterministic shuffle, take 16. */\n        for (var i = pool.length - 1; i > 0; i--) {\n          var j = Math.floor(rand() * (i + 1));\n          var tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp;\n        }\n        return pool.slice(0, 16);\n      }\n\n      var BINGO_LINES = (function () {\n        var lines = [];\n        for (var r = 0; r < 4; r++) lines.push([r * 4, r * 4 + 1, r * 4 + 2, r * 4 + 3]);\n        for (var c = 0; c < 4; c++) lines.push([c, c + 4, c + 8, c + 12]);\n        lines.push([0, 5, 10, 15]);\n        lines.push([3, 6, 9, 12]);\n        return lines;\n      })();\n\n      function computeBingo(game, win) {\n        var doc = game.doc;\n        var board = bingoBoard(doc);\n        var metrics = {};\n        board.forEach(function (cell) { metrics[cell.m] = true; });\n        var metricList = Object.keys(metrics);\n        return Promise.all(metricList.map(function (m) {\n          return fetchEvents(m, win.from, win.to, null).then(function (events) {\n            return { metric: m, agg: aggregate(events, game.participants) };\n          });\n        })).then(function (fetched) {\n          var byMetric = {};\n          fetched.forEach(function (f) { byMetric[f.metric] = f.agg; });\n\n          function cellProgress(cell, userId) {\n            var agg = byMetric[cell.m];\n            if (!agg) return 0;\n            if (cell.s === 'total') return agg.totals[userId] || 0;\n            var best = 0;\n            var days = agg.perDay[userId] || {};\n            Object.keys(days).forEach(function (k) { if (days[k] > best) best = days[k]; });\n            return best;\n          }\n\n          var boards = {}, lineCounts = {}, doneCounts = {};\n          game.participants.forEach(function (id) {\n            var cells = board.map(function (cell) {\n              var progress = cellProgress(cell, id);\n              return { label: cell.label, amount: cell.amount, progress: progress, done: progress >= cell.amount, metric: cell.m };\n            });\n            var lines = 0;\n            BINGO_LINES.forEach(function (line) {\n              if (line.every(function (idx) { return cells[idx].done; })) lines++;\n            });\n            boards[id] = cells;\n            lineCounts[id] = lines;\n            doneCounts[id] = cells.filter(function (c) { return c.done; }).length;\n          });\n\n          var base = { scores: doneCounts, boards: boards, lineCounts: lineCounts, finished: false, winners: [], board: board };\n          var withLine = game.participants.filter(function (id) { return lineCounts[id] > 0; });\n          var expired = win.hardEnd && Date.now() > win.hardEnd.getTime();\n          if (withLine.length || expired) {\n            base.finished = true;\n            if (withLine.length) {\n              var m = maxBy(withLine, lineCounts);\n              base.winners = m.winners;\n            }\n          }\n          return base;\n        });\n      }\n\n      /* ================= tanks ================= */\n      var TANK_MAPS = {\n        desert: {\n          name: 'Öknen', sky: ['#f7d9a8', '#f2b26b'], ground: '#c98d4b',\n          points: [[0, 205], [140, 199], [320, 209], [500, 204], [680, 211], [860, 202], [1000, 207]]\n        },\n        valley: {\n          name: 'Dalen', sky: ['#bfe3ff', '#8fc7f2'], ground: '#5d8a4a',\n          points: [[0, 172], [150, 186], [300, 214], [420, 152], [500, 122], [580, 152], [700, 214], [850, 186], [1000, 172]]\n        },\n        city: {\n          name: 'Staden', sky: ['#dfe7ef', '#b8c6d4'], ground: '#6d6d74',\n          points: [[0, 210], [240, 210], [243, 152], [317, 152], [320, 210], [460, 210], [463, 128], [537, 128], [540, 210], [680, 210], [683, 166], [757, 166], [760, 210], [1000, 210]]\n        }\n      };\n      var TANK_START = { left: 180, right: 820 };\n      var TANK_RANGES = { left: [60, 460], right: [540, 940] };\n      var TANK_HIT_RADIUS = 28;\n      var TANK_MOVES_PER_SHOT = 2;   /* max steps between shots; firing resets the allowance */\n      var TANK_DAMAGE = 25;\n\n      function terrainY(map, x) {\n        var pts = map.points;\n        if (x <= pts[0][0]) return pts[0][1];\n        for (var i = 1; i < pts.length; i++) {\n          if (x <= pts[i][0]) {\n            var a = pts[i - 1], b = pts[i];\n            var f = b[0] === a[0] ? 1 : (x - a[0]) / (b[0] - a[0]);\n            return a[1] + (b[1] - a[1]) * f;\n          }\n        }\n        return pts[pts.length - 1][1];\n      }\n\n      /* Deterministic ballistics — every client computes the same trajectory. */\n      function simulateShot(map, fromX, dir, angle, power) {\n        var K = 0.165, G = 0.25;\n        var rad = angle * Math.PI / 180;\n        var vx = Math.cos(rad) * power * K * dir;\n        var vy = -Math.sin(rad) * power * K;\n        var x = fromX + dir * 20;\n        var y = terrainY(map, fromX) - 16;\n        var points = [[x, y]];\n        for (var i = 0; i < 2400; i++) {\n          x += vx; vy += G; y += vy;\n          points.push([x, y]);\n          if (x < -40 || x > 1040) break;\n          if (y >= terrainY(map, x)) break;\n        }\n        return { points: points, impactX: x, impactY: Math.min(y, terrainY(map, x)) };\n      }\n\n      function computeTanks(game, win) {\n        var doc = game.doc;\n        return fetchEvents(doc.metric, win.from, win.to, doc.orderTagId).then(function (events) {\n          if (game.participants.length < 2) {\n            return { tanksWaiting: true, finished: false, winners: [], scores: {}, raw: {} };\n          }\n          var left = game.creatorId;\n          var right = game.participants.filter(function (id) { return id !== left; })[0];\n          var agg = aggregate(events, [left, right]);\n          var map = TANK_MAPS[doc.map] || TANK_MAPS.desert;\n\n          var tstates = {};\n          [left, right].forEach(function (id) {\n            var u = state.usersById[id];\n            tstates[id] = ((((u || {}).reference || {}).ssg2Tanks) || {})[doc.id] || { pos: [], shots: [] };\n          });\n\n          function startX(id) { return id === left ? TANK_START.left : TANK_START.right; }\n\n          /* Movement economy: a move only counts if (a) you have used fewer than\n             TANK_MOVES_PER_SHOT steps since your last shot AND (b) you were holding\n             at least one grenade at the time of the move — firing your last shell\n             freezes you until the next result earns a new one. Invalid writes are\n             ignored by EVERY client's resolution, so this cannot be bypassed. */\n          function ammoAt(id, isoT) {\n            var cutoff = new Date(isoT).getTime();\n            var earned = 0;\n            events.forEach(function (ev) {\n              if (ev.userId === id && ev.t.getTime() <= cutoff) earned += ev.v;\n            });\n            var fired = 0;\n            (tstates[id].shots || []).forEach(function (s) { if (s.t <= isoT) fired++; });\n            return Math.floor(earned) - fired;\n          }\n\n          var effective = {};\n          [left, right].forEach(function (id) {\n            var timeline = [];\n            (tstates[id].pos || []).forEach(function (p) { timeline.push({ t: p.t, type: 'pos', x: p.x }); });\n            (tstates[id].shots || []).forEach(function (s) { timeline.push({ t: s.t, type: 'shot' }); });\n            timeline.sort(function (a, b) { return a.t < b.t ? -1 : a.t > b.t ? 1 : 0; });\n            var kept = [], used = 0;\n            timeline.forEach(function (ev) {\n              if (ev.type === 'shot') { used = 0; return; }\n              if (used < TANK_MOVES_PER_SHOT && ammoAt(id, ev.t) >= 1) {\n                kept.push({ x: ev.x, t: ev.t });\n                used++;\n              }\n            });\n            effective[id] = { kept: kept, used: used };\n          });\n\n          function posAt(id, t) {\n            var best = startX(id);\n            effective[id].kept.forEach(function (p) { if (p.t <= t) best = p.x; });\n            return best;\n          }\n          function curPos(id) {\n            var kept = effective[id].kept;\n            return kept.length ? kept[kept.length - 1].x : startX(id);\n          }\n\n          var shots = [];\n          [left, right].forEach(function (id) {\n            (tstates[id].shots || []).forEach(function (s) {\n              shots.push({ id: s.id, owner: id, t: s.t, angle: s.angle, power: s.power });\n            });\n          });\n          shots.sort(function (a, b) { return a.t < b.t ? -1 : a.t > b.t ? 1 : 0; });\n\n          var hp = {}; hp[left] = 100; hp[right] = 100;\n          var resolved = [];\n          var over = false;\n          shots.forEach(function (shot) {\n            if (over) return;\n            var target = shot.owner === left ? right : left;\n            var dir = shot.owner === left ? 1 : -1;\n            var sim = simulateShot(map, posAt(shot.owner, shot.t), dir, shot.angle, shot.power);\n            var hit = Math.abs(sim.impactX - posAt(target, shot.t)) <= TANK_HIT_RADIUS;\n            if (hit) hp[target] = Math.max(0, hp[target] - TANK_DAMAGE);\n            resolved.push({ id: shot.id, owner: shot.owner, t: shot.t, hit: hit, points: sim.points, impactX: sim.impactX, impactY: sim.impactY });\n            if (hp[target] <= 0) over = true;\n          });\n\n          var ammo = {};\n          [left, right].forEach(function (id) {\n            ammo[id] = Math.max(0, Math.floor(agg.totals[id] || 0) - (tstates[id].shots || []).length);\n          });\n\n          var result = {\n            tanks: true, left: left, right: right, hp: hp, ammo: ammo, raw: agg.totals,\n            mapId: doc.map || 'desert', resolved: resolved,\n            lastShot: resolved.length ? resolved[resolved.length - 1] : null,\n            positions: {}, scores: hp, finished: false, winners: []\n          };\n          result.positions[left] = curPos(left);\n          result.positions[right] = curPos(right);\n          result.movesLeft = {};\n          [left, right].forEach(function (id) {\n            result.movesLeft[id] = ammo[id] >= 1 ? Math.max(0, TANK_MOVES_PER_SHOT - effective[id].used) : 0;\n          });\n          if (hp[left] <= 0 || hp[right] <= 0) {\n            result.finished = true;\n            result.winners = [hp[left] <= 0 ? right : left];\n          } else if (win.hardEnd && Date.now() > win.hardEnd.getTime()) {\n            result.finished = true;\n            if (hp[left] !== hp[right]) result.winners = [hp[left] > hp[right] ? left : right];\n          }\n          return result;\n        });\n      }\n\n      function barrelPath(angleDeg, dir) {\n        var rad = (angleDeg == null ? 22 : angleDeg) * Math.PI / 180;\n        return 'M0 -10 L' + (Math.cos(rad) * 22 * dir).toFixed(1) + ' ' + (-10 - Math.sin(rad) * 22).toFixed(1);\n      }\n\n      /* Aim arrow from the muzzle: direction = vinkel, length + color = kraft (green->red). */\n      function aimColor(power) {\n        var f = Math.min(1, Math.max(0, (power - 30) / 70));\n        return 'rgb(' + Math.round(46 + 152 * f) + ',' + Math.round(125 - 85 * f) + ',' + Math.round(50 - 10 * f) + ')';\n      }\n      function aimArrowParts(angle, power, dir) {\n        var rad = angle * Math.PI / 180;\n        var vx = Math.cos(rad) * dir, vy = -Math.sin(rad);\n        var sx = vx * 24, sy = -10 + vy * 24;\n        var len = 16 + (power - 30) * 0.6;\n        var ex = sx + vx * len, ey = sy + vy * len;\n        var px = -vy, py = vx;\n        return {\n          line: { x1: sx, y1: sy, x2: ex, y2: ey },\n          head: (ex + vx * 10).toFixed(1) + ',' + (ey + vy * 10).toFixed(1) + ' ' +\n            (ex + px * 5).toFixed(1) + ',' + (ey + py * 5).toFixed(1) + ' ' +\n            (ex - px * 5).toFixed(1) + ',' + (ey - py * 5).toFixed(1),\n          color: aimColor(power)\n        };\n      }\n\n      function tankSvg(x, mapId, dir, color, dead, angleDeg, aim, gid) {\n        var map = TANK_MAPS[mapId] || TANK_MAPS.desert;\n        var y = terrainY(map, x);\n        var arrow = '';\n        if (aim) {\n          var parts = aimArrowParts(aim.angle, aim.power, dir);\n          arrow = '<line id=\"g2-aimline-' + gid + '\" x1=\"' + parts.line.x1.toFixed(1) + '\" y1=\"' + parts.line.y1.toFixed(1) +\n            '\" x2=\"' + parts.line.x2.toFixed(1) + '\" y2=\"' + parts.line.y2.toFixed(1) +\n            '\" stroke=\"' + parts.color + '\" stroke-width=\"4\" stroke-linecap=\"round\" opacity=\"0.9\"/>' +\n            '<polygon id=\"g2-aimhead-' + gid + '\" points=\"' + parts.head + '\" fill=\"' + parts.color + '\" opacity=\"0.9\"/>';\n        }\n        return '<g transform=\"translate(' + x.toFixed(1) + ',' + y.toFixed(1) + ')\"' + (dead ? ' opacity=\"0.45\"' : '') + '>' +\n          arrow +\n          '<path ' + (aim ? 'id=\"g2-turret-' + gid + '\" ' : '') + 'd=\"' + barrelPath(angleDeg, dir) + '\" stroke=\"' + color + '\" stroke-width=\"4\" stroke-linecap=\"round\"/>' +\n          '<rect x=\"-16\" y=\"-14\" width=\"32\" height=\"9\" rx=\"4\" fill=\"' + color + '\"/>' +\n          '<rect x=\"-20\" y=\"-7\" width=\"40\" height=\"9\" rx=\"4.5\" fill=\"' + color + '\"/>' +\n          '<circle cx=\"-12\" cy=\"-2\" r=\"3\" fill=\"rgba(0,0,0,0.35)\"/><circle cx=\"-4\" cy=\"-2\" r=\"3\" fill=\"rgba(0,0,0,0.35)\"/>' +\n          '<circle cx=\"4\" cy=\"-2\" r=\"3\" fill=\"rgba(0,0,0,0.35)\"/><circle cx=\"12\" cy=\"-2\" r=\"3\" fill=\"rgba(0,0,0,0.35)\"/>' +\n          (dead ? '<path d=\"M-10 -26 L10 -8 M10 -26 L-10 -8\" stroke=\"#b71c1c\" stroke-width=\"3\" stroke-linecap=\"round\"/>' : '') +\n          '</g>';\n      }\n\n      function renderTanks(game, result, finished) {\n        var doc = game.doc;\n        if (result.tanksWaiting) {\n          return '<div class=\"g2-tank-status\" style=\"padding:6px 16px 14px;\">Väntar på att motståndaren accepterar inbjudan…</div>';\n        }\n        var map = TANK_MAPS[result.mapId] || TANK_MAPS.desert;\n        var gid = doc.id;\n        var meIsPlayer = state.me && (state.me.id === result.left || state.me.id === result.right);\n        var mySide = meIsPlayer ? (state.me.id === result.left ? 'left' : 'right') : null;\n\n        var terr = map.points.map(function (p) { return p[0].toFixed(0) + ',' + p[1].toFixed(0); }).join(' ') + ' 1000,300 0,300';\n        var svg = '<svg viewBox=\"0 0 1000 300\" id=\"g2-tanksvg-' + esc(gid) + '\">' +\n          '<defs><linearGradient id=\"g2-sky-' + esc(gid) + '\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\n          '<stop offset=\"0\" stop-color=\"' + map.sky[0] + '\"/><stop offset=\"1\" stop-color=\"' + map.sky[1] + '\"/></linearGradient></defs>' +\n          '<rect width=\"1000\" height=\"300\" fill=\"url(#g2-sky-' + esc(gid) + ')\"/>' +\n          '<polygon points=\"' + terr + '\" fill=\"' + map.ground + '\"/>';\n\n        var myAim = state.tankAim[gid] || { angle: 45, power: 70 };\n        [result.left, result.right].forEach(function (id) {\n          var isLeft = id === result.left;\n          var color = isLeft ? '#1565C0' : '#C62828';\n          var isMe = meIsPlayer && state.me.id === id;\n          /* Opponent's barrel rests at the angle of their latest shot. */\n          var oppShots = (result.resolved || []).filter(function (s) { return s.owner === id; });\n          var restAngle = oppShots.length ? oppShots[oppShots.length - 1].angle : 40;\n          svg += tankSvg(result.positions[id], result.mapId, isLeft ? 1 : -1, color, result.hp[id] <= 0,\n            isMe && !finished ? myAim.angle : restAngle,\n            isMe && !finished ? myAim : null, gid);\n          /* HP + name plate */\n          var px = isLeft ? 20 : 700;\n          svg += '<g transform=\"translate(' + px + ',18)\">' +\n            '<text x=\"0\" y=\"0\" font-size=\"15\" font-weight=\"800\" fill=\"#1b2430\">' + esc(firstName(state.usersById[id])) +\n            (state.me && id === state.me.id ? ' (du)' : '') + '</text>' +\n            '<rect x=\"0\" y=\"8\" width=\"280\" height=\"11\" rx=\"5.5\" fill=\"rgba(0,0,0,0.18)\"/>' +\n            '<rect x=\"0\" y=\"8\" width=\"' + (result.hp[id] / 100 * 280).toFixed(0) + '\" height=\"11\" rx=\"5.5\" fill=\"' + (result.hp[id] > 50 ? '#2E7D32' : result.hp[id] > 25 ? '#F9A825' : '#C62828') + '\"/>' +\n            '<text x=\"290\" y=\"18\" font-size=\"12\" font-weight=\"800\" fill=\"#1b2430\">' + result.hp[id] + ' HP</text>' +\n            '</g>';\n        });\n\n        svg += '</svg>';\n\n        var html = '<div class=\"g2-tankfield\">' + svg + '</div>';\n\n        if (meIsPlayer && !finished) {\n          var aim = state.tankAim[gid] || { angle: 45, power: 70 };\n          var myAmmo = result.ammo[state.me.id] || 0;\n          var movesLeft = (result.movesLeft || {})[state.me.id] || 0;\n          html += '<div class=\"g2-tank-controls\">' +\n            '<button class=\"g2-btn\" data-g2-move=\"' + esc(gid) + '|-1\"' + (movesLeft < 1 ? ' disabled' : '') + ' title=\"Flytta vänster\"><i class=\"fas fa-arrow-left\"></i></button>' +\n            '<button class=\"g2-btn\" data-g2-move=\"' + esc(gid) + '|1\"' + (movesLeft < 1 ? ' disabled' : '') + ' title=\"Flytta höger\"><i class=\"fas fa-arrow-right\"></i></button>' +\n            '<span style=\"font-size:11.5px;font-weight:700;color:var(--g2-muted-fg);\">' + movesLeft + '/' + TANK_MOVES_PER_SHOT + ' steg</span>' +\n            '<div class=\"g2-tank-ctl\"><span>Vinkel</span><input type=\"range\" min=\"10\" max=\"80\" value=\"' + aim.angle + '\" data-g2-aim-angle=\"' + esc(gid) + '\"><b id=\"g2-angle-val-' + esc(gid) + '\">' + aim.angle + '&deg;</b></div>' +\n            '<div class=\"g2-tank-ctl\"><span>Kraft</span><input type=\"range\" min=\"30\" max=\"100\" value=\"' + aim.power + '\" data-g2-aim-power=\"' + esc(gid) + '\"><b id=\"g2-power-val-' + esc(gid) + '\">' + aim.power + '</b></div>' +\n            '<button class=\"g2-btn g2-primary-btn\" data-g2-fire=\"' + esc(gid) + '\"' + (myAmmo < 1 ? ' disabled' : '') + '><i class=\"fas fa-crosshairs\"></i> Eld! (' + myAmmo + ' granater)</button>' +\n            '</div>' +\n            '<div class=\"g2-tank-status\">' +\n            (myAmmo < 1 ? 'Ingen granat — du kan varken flytta eller skjuta förrän nästa ' + esc((METRICS[doc.metric] || {}).short || 'resultat') + ' laddar om. ' :\n              movesLeft < 1 ? 'Inga steg kvar — skjut för att kunna flytta igen. ' : '') +\n            'Flytt kräver minst en granat, max ' + TANK_MOVES_PER_SHOT + ' steg mellan skotten. Skott löses mot positionen motståndaren hade när du sköt.' +\n            '</div>';\n        }\n        return html;\n      }\n\n      function animateTankShots() {\n        state.games.forEach(function (game) {\n          if (game.doc.format !== 'tanks') return;\n          var result = state.results[game.doc.id];\n          if (!result || !result.lastShot) return;\n          var shot = result.lastShot;\n          if (state.animatedShots[shot.id]) return;\n          state.animatedShots[shot.id] = true;\n\n          var svg = document.getElementById('g2-tanksvg-' + game.doc.id);\n          if (!svg) return;\n          var NS = 'http://www.w3.org/2000/svg';\n          var shell = document.createElementNS(NS, 'circle');\n          shell.setAttribute('r', '5');\n          shell.setAttribute('fill', '#1b1b1b');\n          svg.appendChild(shell);\n          /* Time-based slow flight (2.2-3.8 s) — refresh-rate independent, and slow\n             enough to build anticipation about whether it will hit. */\n          var duration = Math.min(3800, 1400 + shot.points.length * 10);\n          var t0 = null;\n          function frame(ts) {\n            var live = document.getElementById('g2-tanksvg-' + game.doc.id);\n            if (!live) return; /* view re-rendered — stop quietly */\n            if (t0 == null) t0 = ts;\n            var progress = Math.min(1, (ts - t0) / duration);\n            var idx = Math.floor(progress * (shot.points.length - 1));\n            if (progress >= 1) {\n              shell.remove();\n              var boom = document.createElementNS(NS, 'circle');\n              boom.setAttribute('cx', shot.impactX);\n              boom.setAttribute('cy', shot.impactY);\n              boom.setAttribute('r', '5');\n              boom.setAttribute('fill', shot.hit ? '#E53935' : '#8d6e63');\n              boom.setAttribute('opacity', '0.9');\n              live.appendChild(boom);\n              var r = 5;\n              function explode() {\n                if (!document.getElementById('g2-tanksvg-' + game.doc.id)) return;\n                r += 2.4;\n                boom.setAttribute('r', String(r));\n                boom.setAttribute('opacity', String(Math.max(0, 0.9 - (r - 5) / 30)));\n                if (r < 34) requestAnimationFrame(explode);\n                else boom.remove();\n              }\n              requestAnimationFrame(explode);\n              return;\n            }\n            shell.setAttribute('cx', shot.points[idx][0]);\n            shell.setAttribute('cy', shot.points[idx][1]);\n            requestAnimationFrame(frame);\n          }\n          requestAnimationFrame(frame);\n        });\n      }\n\n      function fireTankShot(gid) {\n        var result = state.results[gid];\n        if (!result || !result.tanks || !state.me) return;\n        if ((result.ammo[state.me.id] || 0) < 1) return;\n        var aim = state.tankAim[gid] || { angle: 45, power: 70 };\n        var shot = { id: newId(), t: new Date().toISOString(), angle: aim.angle, power: aim.power };\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var tanks = ((me.reference || {}).ssg2Tanks) || {};\n          var entry = tanks[gid] || { pos: [], shots: [] };\n          entry.shots = (entry.shots || []).concat([shot]);\n          tanks[gid] = entry;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Tanks: tanks },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      function moveTank(gid, dirSign) {\n        var result = state.results[gid];\n        if (!result || !result.tanks || !state.me) return;\n        if (((result.movesLeft || {})[state.me.id] || 0) < 1) return;\n        var side = state.me.id === result.left ? 'left' : state.me.id === result.right ? 'right' : null;\n        if (!side) return;\n        var range = TANK_RANGES[side];\n        var newX = Math.min(range[1], Math.max(range[0], result.positions[state.me.id] + dirSign * 34));\n        if (newX === result.positions[state.me.id]) return;\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var tanks = ((me.reference || {}).ssg2Tanks) || {};\n          var entry = tanks[gid] || { pos: [], shots: [] };\n          entry.pos = (entry.pos || []).concat([{ x: newX, t: new Date().toISOString() }]).slice(-30);\n          tanks[gid] = entry;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Tanks: tanks },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      /* ================= reference housekeeping (GC) =================\n         References are loaded on every view load for every user, so they must\n         stay small. Each user prunes ONLY their own reference:\n         - finished/canceled games get a finishedAt stamp, and are deleted\n           GC_RETENTION_MS after they ended (wins live on in ssg2Wins)\n         - ssg2Joined/ssg2Declined/ssg2Tanks entries for games that no longer\n           exist anywhere are dropped\n         - ssg2Wins is capped to the most recent entries\n         - ssg2Meta.gcAt throttles this to once per GC_INTERVAL_MS per user */\n      var GC_INTERVAL_MS = 6 * 3600000;\n      var GC_RETENTION_MS = 7 * DAY_MS;\n      var WINS_CAP = 40;\n\n      function gcOwnReference() {\n        if (!state.me) return;\n        var meta = ((state.me.reference || {}).ssg2Meta) || {};\n        if (meta.gcAt && Date.now() - new Date(meta.gcAt).getTime() < GC_INTERVAL_MS) return;\n\n        var known = {};\n        state.games.forEach(function (g) { known[g.doc.id] = true; });\n\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var ref = me.reference || {};\n          var update = {};\n          var changed = false;\n\n          var ownGames = ref.ssg2Games || {};\n          var keptGames = {};\n          Object.keys(ownGames).forEach(function (id) {\n            var doc = ownGames[id];\n            if (!doc || !doc.id) { changed = true; return; }\n            var result = state.results[id];\n            if (result && result.finished && !doc.finishedAt && !doc.canceledAt) {\n              var copy = {};\n              for (var k in doc) copy[k] = doc[k];\n              copy.finishedAt = new Date().toISOString();\n              doc = copy;\n              changed = true;\n            }\n            var endedAt = doc.canceledAt || doc.finishedAt;\n            if (endedAt && Date.now() - new Date(endedAt).getTime() > GC_RETENTION_MS) {\n              changed = true;\n              return;\n            }\n            keptGames[id] = doc;\n          });\n          if (changed) update.ssg2Games = keptGames;\n\n          ['ssg2Joined', 'ssg2Declined', 'ssg2Tanks'].forEach(function (key) {\n            var obj = ref[key];\n            if (!obj) return;\n            var cleaned = {};\n            var removed = false;\n            Object.keys(obj).forEach(function (id) {\n              if (known[id]) cleaned[id] = obj[id];\n              else removed = true;\n            });\n            if (removed) { update[key] = cleaned; changed = true; }\n          });\n\n          var wins = ref.ssg2Wins || [];\n          if (wins.length > WINS_CAP) {\n            update.ssg2Wins = wins.slice(wins.length - WINS_CAP);\n            changed = true;\n          }\n\n          update.ssg2Meta = { v: 2, gcAt: new Date().toISOString() };\n          if (state.me.reference) state.me.reference.ssg2Meta = update.ssg2Meta;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: update,\n            replaceReference: false\n          });\n        }).catch(function () {});\n      }\n\n      /* ================= win recording (own reference only) ================= */\n      function recordWinsIfNeeded() {\n        if (!state.me) return;\n        var myWins = ((state.me.reference || {}).ssg2Wins) || [];\n        var recorded = {};\n        myWins.forEach(function (w) { recorded[w.gameId] = true; });\n\n        state.games.forEach(function (game) {\n          if (!isRelevantToMe(game) || game.doc.canceledAt) return;\n          if (recorded[game.doc.id] || state.savingWins[game.doc.id]) return;\n          var result = state.results[game.doc.id];\n          if (!result || !result.finished || result.winners.indexOf(state.me.id) === -1) return;\n\n          state.savingWins[game.doc.id] = true;\n          var entry = {\n            gameId: game.doc.id,\n            gameName: game.doc.name,\n            format: game.doc.format,\n            metric: game.doc.metric,\n            count: Math.round((result.raw ? result.raw[state.me.id] : result.scores[state.me.id]) || 0),\n            wonAt: new Date().toISOString()\n          };\n          api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Wins: myWins.concat([entry]) },\n            replaceReference: false\n          }).then(function () {\n            if (state.me.reference) state.me.reference.ssg2Wins = myWins.concat([entry]);\n            render();\n          }).catch(function () {\n            delete state.savingWins[game.doc.id];\n          });\n        });\n      }\n\n      /* ================= rendering ================= */\n      function render() {\n        var body = el('g2-body');\n        if (!body || !state.loaded) return;\n\n        var invites = state.games.filter(myInviteFor);\n        var mine = state.games.filter(function (g) {\n          return isRelevantToMe(g) && !myInviteFor(g) && !g.doc.canceledAt;\n        });\n        var running = mine.filter(function (g) { return !(state.results[g.doc.id] || {}).finished; });\n        var done = mine.filter(function (g) { return (state.results[g.doc.id] || {}).finished; }).slice(0, 8);\n\n        var html = '';\n        if (invites.length) {\n          html += '<div class=\"g2-card\"><div class=\"g2-card-title\"><i class=\"fas fa-envelope-open-text\"></i> Inbjudningar</div>';\n          invites.forEach(function (game) {\n            var creator = state.usersById[game.creatorId];\n            var fmt = FORMATS[game.doc.format] || {};\n            html += '<div class=\"g2-invite\">' +\n              '<span class=\"g2-invite-icon\"><i class=\"fas fa-' + esc(fmt.icon || 'flag') + '\"></i></span>' +\n              '<div class=\"g2-invite-info\">' +\n              '<div class=\"g2-invite-name\">' + esc(game.doc.name) + '</div>' +\n              '<div class=\"g2-invite-meta\">' + esc(fullName(creator)) + ' bjuder in dig · ' + esc(ruleText(game)) + '</div>' +\n              '</div>' +\n              '<button class=\"g2-btn g2-primary-btn\" data-g2-accept=\"' + esc(game.doc.id) + '\"><i class=\"fas fa-check\"></i> Delta</button>' +\n              '<button class=\"g2-btn\" data-g2-decline=\"' + esc(game.doc.id) + '\">Tacka nej</button>' +\n              '</div>';\n          });\n          html += '</div>';\n        }\n\n        html += '<div class=\"g2-columns\"><div class=\"g2-main-col\">';\n        html += '<div class=\"g2-section-title\">Pågående tävlingar</div>';\n        if (!running.length) {\n          html += '<div class=\"g2-card\"><div class=\"g2-empty\">Inga pågående tävlingar. Starta en med \"Ny tävling\"!</div></div>';\n        }\n        running.forEach(function (game) { html += renderGame(game, false); });\n        if (done.length) {\n          html += '<div class=\"g2-section-title\" style=\"margin-top:6px;\">Avslutade</div>';\n          done.forEach(function (game) { html += renderGame(game, true); });\n        }\n        html += '</div><div class=\"g2-side-col\">' + renderWinsCard() + '</div></div>';\n\n        /* Keep already-animated positions across re-renders: without this every\n           reload (e.g. after a tank move) restarts ALL race animations from the gate. */\n        var prevAnim = {};\n        document.querySelectorAll('[data-g2-key]').forEach(function (n) {\n          prevAnim[n.getAttribute('data-g2-key')] = { left: n.style.left, width: n.style.width };\n        });\n\n        body.innerHTML = html;\n\n        document.querySelectorAll('[data-g2-key]').forEach(function (n) {\n          var prev = prevAnim[n.getAttribute('data-g2-key')];\n          if (!prev) return;\n          if (prev.left) n.style.left = prev.left;\n          if (prev.width) n.style.width = prev.width;\n        });\n\n        requestAnimationFrame(function () {\n          requestAnimationFrame(function () {\n            document.querySelectorAll('[data-g2-target-left]').forEach(function (n) {\n              n.style.left = n.getAttribute('data-g2-target-left');\n            });\n            document.querySelectorAll('[data-g2-target-width]').forEach(function (n) {\n              n.style.width = n.getAttribute('data-g2-target-width');\n            });\n            applyContrast();\n            animateTankShots();\n          });\n        });\n      }\n\n      function ruleText(game) {\n        var doc = game.doc;\n        var metric = METRICS[doc.metric] || { short: '' };\n        if (doc.format === 'first_to') return 'Först till ' + doc.target + ' ' + metric.short;\n        if (doc.format === 'most_before') return 'Mest ' + metric.short + ' före ' + shortDate(doc.endsAt);\n        if (doc.format === 'ghost') return 'Slå ditt rekord: ' + metric.label.toLowerCase() + ' på 7 dagar';\n        if (doc.format === 'tournament') return 'Turnering i ' + metric.label.toLowerCase() + ', ' + (doc.roundHours || 24) + 'h-ronder';\n        if (doc.format === 'tug') return 'Dragkamp i ' + metric.label.toLowerCase() + ' till ' + shortDate(doc.endsAt);\n        if (doc.format === 'relay') return 'Stafett: alla till ' + doc.target + ' ' + metric.short;\n        if (doc.format === 'streak') return 'Flest dagar i rad med minst ' + doc.target + ' ' + metric.short;\n        if (doc.format === 'bingo') return 'Bingo — först till en rad';\n        if (doc.format === 'lottery') return 'Lotteri: 1 ' + (metric.unit === 'min' ? '10 min' : metric.short.replace(/s$/, '')) + ' = 1 lott, dras ' + shortDate(doc.endsAt);\n        if (doc.format === 'tanks') return 'Duell på ' + ((TANK_MAPS[doc.map] || {}).name || 'Öknen') + ' — 1 ' + metric.short + ' = 1 granat';\n        return '';\n      }\n\n      function timeLeftText(endsAtIso) {\n        if (!endsAtIso) return '';\n        var ms = new Date(endsAtIso).getTime() - Date.now();\n        if (ms <= 0) return 'Avslutad';\n        var mins = Math.floor(ms / 60000);\n        var days = Math.floor(mins / 1440);\n        var hours = Math.floor((mins % 1440) / 60);\n        var rest = mins % 60;\n        if (days > 0) return days + 'd ' + hours + 'h kvar';\n        if (hours > 0) return hours + 'h ' + rest + 'min kvar';\n        return rest + ' min kvar';\n      }\n\n      function renderGame(game, finished) {\n        var doc = game.doc;\n        var result = state.results[doc.id] || { scores: {}, winners: [], finished: false };\n        var fmt = FORMATS[doc.format] || { icon: 'flag' };\n\n        var html = '<div class=\"g2-card g2-game\">' +\n          '<div class=\"g2-game-header\">' +\n          '<div class=\"g2-game-title\"><i class=\"fas fa-' + esc(fmt.icon) + '\"></i> ' + esc(doc.name) + '</div>' +\n          '<span class=\"g2-chip ' + (finished ? 'g2-chip-done' : 'g2-chip-live') + '\">' + (finished ? 'Avslutad' : 'Pågår') + '</span>' +\n          (doc.handicap ? '<span class=\"g2-chip\">Handikapp</span>' : '') +\n          '<div class=\"g2-game-spacer\"></div>' +\n          '<div class=\"g2-game-meta\">' + esc(ruleText(game)) + '</div>' +\n          (!finished && state.me && game.creatorId === state.me.id\n            ? '<button class=\"g2-btn g2-danger-btn\" data-g2-cancel=\"' + esc(doc.id) + '\" title=\"Avbryt tävlingen\"><i class=\"fas fa-times\"></i></button>' : '') +\n          '</div>';\n\n        if (finished) html += renderWinnerBanner(game, result);\n\n        if (doc.format === 'first_to' || doc.format === 'most_before' || doc.format === 'ghost') html += renderRace(game, result, finished);\n        else if (doc.format === 'tug') html += renderTug(game, result);\n        else if (doc.format === 'relay') html += renderRelay(game, result);\n        else if (doc.format === 'streak') html += renderStreak(game, result);\n        else if (doc.format === 'bingo') html += renderBingo(game, result);\n        else if (doc.format === 'lottery') html += renderLottery(game, result, finished);\n        else if (doc.format === 'tournament') html += renderBracket(game, result);\n        else if (doc.format === 'tanks') html += renderTanks(game, result, finished);\n\n        var pending = needsInvite(doc.format) ? (doc.invitedUserIds || []).filter(function (id) {\n          return game.participants.indexOf(id) === -1;\n        }) : [];\n        html += '<div class=\"g2-game-footer\">' +\n          '<span><i class=\"fas fa-user-friends\"></i> ' + game.participants.length + ' deltagare</span>' +\n          (pending.length && !finished ? '<span>' + pending.length + ' inbjudna har inte svarat</span>' : '') +\n          (!finished && doc.endsAt ? '<span><i class=\"far fa-clock\"></i> ' + esc(timeLeftText(doc.endsAt)) + '</span>' : '') +\n          (!finished && doc.format === 'tournament' && result.endsAtComputed ? '<span><i class=\"far fa-clock\"></i> final ' + esc(shortDate(result.endsAtComputed)) + '</span>' : '') +\n          '<span>Startade ' + esc(shortDate(doc.startAt || doc.createdAt)) + '</span>' +\n          '</div></div>';\n        return html;\n      }\n\n      function renderWinnerBanner(game, result) {\n        var doc = game.doc;\n        if (result.winnerTeam) {\n          return '<div class=\"g2-winner-banner\"><i class=\"fas fa-trophy\"></i> Lag ' + esc(result.winnerTeam.name) + ' vann!</div>';\n        }\n        if (result.winners && result.winners.length) {\n          var names = result.winners.map(function (id) { return fullName(state.usersById[id]); }).join(' & ');\n          var score = result.scores[result.winners[0]] || 0;\n          var suffix = doc.format === 'tanks' ? ' duellen med ' + Math.round(score) + ' HP kvar'\n            : doc.format === 'lottery' ? ' i lottdragningen'\n            : doc.format === 'bingo' ? ' med bingo'\n            : doc.format === 'streak' ? ' med ' + Math.round(score) + ' dagar i rad'\n            : doc.format === 'tournament' ? ' hela turneringen'\n            : ' med ' + formatValue(doc.metric, score) + ' ' + (METRICS[doc.metric] || {}).short;\n          return '<div class=\"g2-winner-banner\"><i class=\"fas fa-trophy\"></i> ' + esc(names) + ' vann' + esc(suffix) + '!</div>';\n        }\n        if (doc.format === 'ghost') {\n          return '<div class=\"g2-winner-banner\" style=\"opacity:0.85;\"><i class=\"fas fa-ghost\"></i> Spöket vann den här gången — nytt försök?</div>';\n        }\n        return '<div class=\"g2-winner-banner\" style=\"opacity:0.8;\"><i class=\"fas fa-flag\"></i> Ingen vinnare — inga resultat registrerades.</div>';\n      }\n\n      function renderRace(game, result, finished) {\n        var doc = game.doc;\n        var theme = doc.theme || 'horses';\n        var isGhost = doc.format === 'ghost';\n        var target = result.target || doc.target || 0;\n        var maxScore = 0;\n        game.participants.forEach(function (id) { maxScore = Math.max(maxScore, result.scores[id] || 0); });\n        if (isGhost && result.ghost) maxScore = Math.max(maxScore, result.ghost.progress);\n\n        var html = '<div class=\"g2-track g2-' + esc(theme) + '\"><div class=\"g2-finish\"></div>';\n        var lanes = game.participants.map(function (id) { return { id: id, ghost: false }; });\n        if (isGhost) lanes.push({ id: null, ghost: true });\n\n        lanes.forEach(function (lane, i) {\n          var color = lane.ghost ? '#e0e0e0' : LANE_COLORS[i % LANE_COLORS.length];\n          var score = lane.ghost ? (result.ghost ? result.ghost.progress : 0) : (result.scores[lane.id] || 0);\n          var fraction;\n          if (target > 0) fraction = Math.min(1, score / target);\n          else fraction = (score / Math.max(maxScore, 1)) * (finished ? 1 : 0.85);\n          var isWinner = !lane.ghost && result.winners.indexOf(lane.id) !== -1;\n          if (isWinner) fraction = 1;\n          var leftPct = (4 + fraction * 80).toFixed(2) + '%';\n          var user = lane.ghost ? null : state.usersById[lane.id];\n          var label = lane.ghost ? 'Spöket (rekord)' : firstName(user) + (state.me && lane.id === state.me.id ? ' (du)' : '');\n          var factor = result.factors && !lane.ghost ? result.factors[lane.id] : null;\n\n          html += '<div class=\"g2-lane' + (lane.ghost ? ' g2-ghost-lane' : '') + '\">' +\n            '<div class=\"g2-lane-label\">' +\n            (isWinner ? '<i class=\"fas fa-crown g2-lane-crown\"></i>' : '') +\n            (lane.ghost ? '<i class=\"fas fa-ghost\"></i> ' : '') +\n            esc(label) +\n            (factor && factor !== 1 ? ' <span class=\"g2-hcap\">x' + factor + '</span>' : '') +\n            '</div>' +\n            '<div class=\"g2-lane-score\">' + formatValue(doc.metric, score) + (target ? ' / ' + formatValue(doc.metric, target) : '') + '</div>' +\n            '<div class=\"g2-sprite-wrap\" style=\"left:4%\" data-g2-key=\"' + esc(doc.id) + ':' + esc(lane.ghost ? 'ghost' : lane.id) + '\" data-g2-target-left=\"' + leftPct + '\">' + sprite(theme, color) + '</div>' +\n            '</div>';\n        });\n        html += '</div>';\n        if (isGhost && result.ghost && !result.ghost.hadRecord) {\n          html += '<div class=\"g2-game-footer\"><span>Inget tidigare rekord hittades — målet är satt till 1.</span></div>';\n        }\n        return html;\n      }\n\n      function renderTug(game, result) {\n        var teams = result.teams || [];\n        if (teams.length < 2) return '<div class=\"g2-tug\"><div class=\"g2-empty\">Lagen kunde inte laddas.</div></div>';\n        var a = teams[0], b = teams[1];\n        var total = a.sum + b.sum;\n        var fracA = total > 0 ? a.sum / total : 0.5;\n        var knotPct = (8 + fracA * 84).toFixed(2) + '%';\n        var html = '<div class=\"g2-tug\">' +\n          '<div class=\"g2-tug-teams\">' +\n          '<span>' + esc(a.name) + ' <span class=\"g2-tug-score\">' + formatValue(game.doc.metric, a.sum) + '</span></span>' +\n          '<span><span class=\"g2-tug-score\">' + formatValue(game.doc.metric, b.sum) + '</span> ' + esc(b.name) + '</span>' +\n          '</div>' +\n          '<div class=\"g2-tug-bar\">' +\n          '<div class=\"g2-tug-left\" style=\"width:50%\" data-g2-key=\"' + esc(game.doc.id) + ':tugleft\" data-g2-target-width=\"' + (fracA * 100).toFixed(2) + '%\"></div>' +\n          '<div class=\"g2-tug-rope\"></div>' +\n          '<div class=\"g2-tug-knot\" style=\"left:50%\" data-g2-key=\"' + esc(game.doc.id) + ':knot\" data-g2-target-left=\"' + knotPct + '\"></div>' +\n          '</div>' +\n          '<div class=\"g2-tug-members\">' +\n          '<div class=\"g2-tug-side\">' + a.members.map(function (id) {\n            return '<span class=\"g2-mini-chip\">' + esc(firstName(state.usersById[id])) + ' ' + formatValue(game.doc.metric, result.scores[id] || 0) + '</span>';\n          }).join('') + '</div>' +\n          '<div class=\"g2-tug-side\" style=\"justify-content:flex-end;\">' + b.members.map(function (id) {\n            return '<span class=\"g2-mini-chip\">' + esc(firstName(state.usersById[id])) + ' ' + formatValue(game.doc.metric, result.scores[id] || 0) + '</span>';\n          }).join('') + '</div>' +\n          '</div></div>';\n        return html;\n      }\n\n      function renderRelay(game, result) {\n        var html = '<div class=\"g2-relay\">';\n        (result.teams || []).forEach(function (team) {\n          html += '<div>' +\n            '<div class=\"g2-relay-team-name\"><span>' + esc(team.name) + (team.complete ? ' — i mål!' : '') + '</span>' +\n            '<span>' + Math.round(team.progress * 100) + '%</span></div>' +\n            '<div class=\"g2-relay-bar\">';\n          var segWidth = team.members.length ? (100 / team.members.length) : 100;\n          team.members.forEach(function (m) {\n            html += '<div class=\"g2-relay-seg\" style=\"width:' + segWidth + '%\">' +\n              '<div class=\"g2-relay-fill\" style=\"width:0%\" data-g2-key=\"' + esc(game.doc.id) + ':' + esc(m.userId) + '\" data-g2-target-width=\"' + (m.fraction * 100).toFixed(1) + '%\"></div>' +\n              '<span class=\"g2-relay-seg-label\">' + esc(firstName(state.usersById[m.userId])) + ' ' + formatValue(game.doc.metric, m.value) + '/' + result.target + '</span>' +\n              '</div>';\n          });\n          html += '</div></div>';\n        });\n        html += '</div>';\n        return html;\n      }\n\n      function renderStreak(game, result) {\n        var range = result.dayRange;\n        if (!range) return '';\n        var days = [];\n        for (var t = range.from.getTime(); t <= range.to.getTime(); t += DAY_MS) days.push(new Date(t));\n        if (days.length > 21) days = days.slice(days.length - 21);\n        var todayK = dayKey(new Date());\n        var ranked = game.participants.slice().sort(function (x, y) {\n          return (result.scores[y] || 0) - (result.scores[x] || 0);\n        });\n        var html = '<div class=\"g2-streaks\">';\n        ranked.forEach(function (id) {\n          var streak = (result.streaks || {})[id] || { best: 0, current: 0 };\n          html += '<div class=\"g2-streak-row\">' +\n            '<div class=\"g2-streak-name\">' + esc(firstName(state.usersById[id])) + (state.me && id === state.me.id ? ' (du)' : '') + '</div>' +\n            '<div class=\"g2-streak-days\">' + days.map(function (d) {\n              var v = ((result.perDay || {})[id] || {})[dayKey(d)] || 0;\n              var hit = v >= (game.doc.target || 1);\n              return '<div class=\"g2-streak-day' + (hit ? ' g2-hit' : '') + (dayKey(d) === todayK ? ' g2-today' : '') + '\" title=\"' + esc(d.toLocaleDateString('sv-SE')) + ': ' + formatValue(game.doc.metric, v) + '\"></div>';\n            }).join('') + '</div>' +\n            '<div class=\"g2-streak-count\"><i class=\"fas fa-fire\"></i> ' + streak.best + (streak.current === streak.best && streak.best > 0 ? ' *' : '') + '</div>' +\n            '</div>';\n        });\n        html += '<div class=\"g2-lottery-note\">* = streaken lever fortfarande. Kräver minst ' + game.doc.target + ' ' + (METRICS[game.doc.metric] || {}).short + ' per dag.</div>';\n        html += '</div>';\n        return html;\n      }\n\n      function renderBingo(game, result) {\n        var viewId = state.bingoViewUser[game.doc.id] || (state.me ? state.me.id : game.participants[0]);\n        if (game.participants.indexOf(viewId) === -1) viewId = game.participants[0];\n        var cells = (result.boards || {})[viewId] || [];\n        var html = '<div class=\"g2-bingo\">' +\n          '<div class=\"g2-bingo-top\"><span style=\"font-size:12.5px;font-weight:700;\">Visa bricka för:</span>' +\n          '<select data-g2-bingo-user=\"' + esc(game.doc.id) + '\">' +\n          game.participants.map(function (id) {\n            return '<option value=\"' + esc(id) + '\"' + (id === viewId ? ' selected' : '') + '>' + esc(fullName(state.usersById[id])) + '</option>';\n          }).join('') + '</select></div>' +\n          '<div class=\"g2-bingo-grid\">' +\n          cells.map(function (cell) {\n            return '<div class=\"g2-bingo-cell' + (cell.done ? ' g2-done' : '') + '\">' +\n              '<span>' + esc(cell.label) + '</span>' +\n              '<span class=\"g2-bingo-progress\">' + (cell.done ? 'Klar' : formatValue(cell.metric, cell.progress) + ' / ' + cell.amount) + '</span>' +\n              '</div>';\n          }).join('') +\n          '</div>' +\n          '<div class=\"g2-bingo-lb\">' +\n          game.participants.slice().sort(function (x, y) { return (result.scores[y] || 0) - (result.scores[x] || 0); })\n            .map(function (id) {\n              return '<span><b>' + esc(firstName(state.usersById[id])) + '</b>: ' + (result.scores[id] || 0) + '/16 rutor' +\n                ((result.lineCounts || {})[id] ? ', ' + result.lineCounts[id] + ' rader' : '') + '</span>';\n            }).join('') +\n          '</div></div>';\n        return html;\n      }\n\n      function renderLottery(game, result, finished) {\n        var maxTickets = 1;\n        game.participants.forEach(function (id) { maxTickets = Math.max(maxTickets, (result.tickets || {})[id] || 0); });\n        var html = '<div class=\"g2-lottery\">';\n        game.participants.slice().sort(function (x, y) {\n          return ((result.tickets || {})[y] || 0) - ((result.tickets || {})[x] || 0);\n        }).forEach(function (id) {\n          var t = (result.tickets || {})[id] || 0;\n          var pct = result.totalTickets ? Math.round(t / result.totalTickets * 100) : 0;\n          html += '<div class=\"g2-lottery-row\">' +\n            '<div class=\"g2-lottery-name\">' + (result.winners.indexOf(id) !== -1 ? '<i class=\"fas fa-crown g2-lane-crown\"></i> ' : '') +\n            esc(firstName(state.usersById[id])) + (state.me && id === state.me.id ? ' (du)' : '') + '</div>' +\n            '<div class=\"g2-lottery-bar-wrap\"><div class=\"g2-lottery-bar\" style=\"width:0%\" data-g2-key=\"' + esc(game.doc.id) + ':lot:' + esc(id) + '\" data-g2-target-width=\"' + (t / maxTickets * 100).toFixed(1) + '%\"></div></div>' +\n            '<div class=\"g2-lottery-tickets\">' + t + ' (' + pct + '%)</div>' +\n            '</div>';\n        });\n        html += '<div class=\"g2-lottery-note\">' +\n          (finished ? 'Dragningen är gjord — samma resultat på alla skärmar (deterministiskt frö).'\n            : 'Totalt ' + (result.totalTickets || 0) + ' lotter hittills. Dragning ' + esc(shortDate(game.doc.endsAt)) + '.') +\n          '</div></div>';\n        return html;\n      }\n\n      function renderBracket(game, result) {\n        var rounds = result.rounds || [];\n        var names = ['Åttondelar', 'Kvartsfinal', 'Semifinal', 'Final'];\n        var html = '<div class=\"g2-bracket\">';\n        rounds.forEach(function (round, r) {\n          var title = rounds.length - r <= 4 ? names[4 - (rounds.length - r)] || ('Rond ' + (r + 1)) : 'Rond ' + (r + 1);\n          html += '<div class=\"g2-round' + (round.live ? ' g2-live' : '') + '\">' +\n            '<div class=\"g2-round-title\">' + esc(title) + (round.live ? ' (pågår)' : '') + '</div>';\n          round.matches.forEach(function (match) {\n            html += '<div class=\"g2-match' + (match.live ? ' g2-live-match' : '') + '\">' +\n              bracketSide(game, match.a, match.scoreA, match.winner) +\n              bracketSide(game, match.b, match.scoreB, match.winner) +\n              '</div>';\n          });\n          html += '</div>';\n        });\n        html += '</div>';\n        return html;\n      }\n\n      function bracketSide(game, userId, score, winner) {\n        if (!userId) return '<div class=\"g2-match-side g2-tbd\">—</div>';\n        var won = winner && winner === userId;\n        return '<div class=\"g2-match-side' + (won ? ' g2-won' : '') + '\">' +\n          '<span>' + (won ? '<i class=\"fas fa-crown g2-lane-crown\"></i> ' : '') + esc(firstName(state.usersById[userId])) + '</span>' +\n          '<span>' + (score >= 0 ? formatValue(game.doc.metric, score) : '') + '</span>' +\n          '</div>';\n      }\n\n      function renderWinsCard() {\n        var counts = {};\n        state.users.forEach(function (u) {\n          var wins = (((u.reference || {}).ssgWins) || []).length + (((u.reference || {}).ssg2Wins) || []).length;\n          if (wins) counts[u.id] = wins;\n        });\n        var rows = Object.keys(counts).map(function (id) {\n          return { id: id, count: counts[id], name: fullName(state.usersById[id]) };\n        }).sort(function (a, b) { return b.count - a.count; }).slice(0, 10);\n\n        var html = '<div class=\"g2-card\"><div class=\"g2-card-title\"><i class=\"fas fa-trophy\"></i> Flest vinster</div>';\n        if (!rows.length) html += '<div class=\"g2-empty\">Inga vinster ännu. Första tävlingen kan bli din!</div>';\n        else rows.forEach(function (row, i) {\n          html += '<div class=\"g2-lb-row\"><span class=\"g2-lb-rank\">' + (i + 1) + '</span>' +\n            '<span class=\"g2-lb-name\">' + esc(row.name) + '</span>' +\n            '<span class=\"g2-lb-count\"><i class=\"fas fa-trophy\"></i> ' + row.count + '</span></div>';\n        });\n        html += '</div>';\n\n        var myWins = state.me ? ((((state.me.reference || {}).ssg2Wins) || []).concat(((state.me.reference || {}).ssgWins) || [])) : [];\n        if (myWins.length) {\n          html += '<div class=\"g2-card\"><div class=\"g2-card-title\"><i class=\"fas fa-medal\"></i> Dina vinster</div>';\n          myWins.slice().sort(function (a, b) { return new Date(b.wonAt) - new Date(a.wonAt); }).slice(0, 6).forEach(function (w) {\n            var fmt = FORMATS[w.format] || { icon: 'flag-checkered' };\n            html += '<div class=\"g2-done-row\">' +\n              '<div class=\"g2-done-name\"><i class=\"fas fa-' + esc(fmt.icon) + '\"></i> ' + esc(w.gameName) + '</div>' +\n              '<div class=\"g2-done-meta\">' + esc(shortDate(w.wonAt)) + (w.count ? ' · ' + w.count : '') + '</div>' +\n              '</div>';\n          });\n          html += '</div>';\n        }\n        return html;\n      }\n\n      /* ================= modal ================= */\n      function openModal() {\n        state.pickedTheme = 'horses';\n        state.pickedFormat = 'first_to';\n        state.pickedMetric = 'signed';\n        state.pickedMap = 'desert';\n        state.pickedRound = 24;\n        state.pickedDifficulty = '1';\n        state.pickedOrderTag = null;\n        state.pickedTeams = state.teams.slice(0, 2).map(function (t) { return t.id; });\n        var err = el('g2-modal-error'); if (err) err.textContent = '';\n        var name = el('g2-f-name'); if (name) name.value = '';\n        var target = el('g2-f-target'); if (target) target.value = '5';\n        setDefaultEnds(8);\n        renderFormatGrid();\n        renderMetricRow();\n        renderMapCards();\n        renderThemeRow();\n        renderInvitees();\n        renderTeamRow();\n        renderOrderTagRow();\n        renderRoundRow();\n        renderDifficultyRow();\n        syncFields();\n        var backdrop = el('g2-modal-backdrop');\n        if (backdrop) backdrop.className = 'g2-modal-backdrop g2-open';\n      }\n      function closeModal() {\n        var backdrop = el('g2-modal-backdrop');\n        if (backdrop) backdrop.className = 'g2-modal-backdrop';\n      }\n\n      function setDefaultEnds(hours) {\n        var ends = el('g2-f-ends');\n        if (!ends) return;\n        var d = new Date(Date.now() + hours * 3600000);\n        if (hours >= 8) d.setMinutes(0, 0, 0);\n        var pad = function (n) { return (n < 10 ? '0' : '') + n; };\n        ends.value = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());\n      }\n\n      function syncFields() {\n        var format = state.pickedFormat;\n        var metric = state.pickedMetric;\n        function show(id, visible) { var n = el(id); if (n) n.style.display = visible ? '' : 'none'; }\n\n        var hint = el('g2-format-hint');\n        if (hint) hint.textContent = (FORMATS[format] || {}).hint || '';\n\n        show('g2-f-metric-wrap', format !== 'bingo');\n        show('g2-f-ordertag-wrap', format !== 'bingo' && metric === 'ordertag');\n        show('g2-f-target-wrap', format === 'first_to' || format === 'relay' || format === 'streak');\n        var targetLabel = el('g2-f-target-label');\n        if (targetLabel) {\n          targetLabel.textContent = format === 'streak' ? 'Minst per dag' : format === 'relay' ? 'Mål per person' : 'Mål';\n        }\n        show('g2-f-ends-wrap', format === 'most_before' || format === 'tug' || format === 'relay' ||\n          format === 'streak' || format === 'bingo' || format === 'lottery' || format === 'first_to' || format === 'tanks');\n        show('g2-f-round-wrap', format === 'tournament');\n        show('g2-f-difficulty-wrap', format === 'bingo');\n        show('g2-f-teams-wrap', format === 'tug' || format === 'relay');\n        show('g2-f-map-wrap', format === 'tanks');\n        show('g2-f-theme-wrap', format === 'first_to' || format === 'most_before' || format === 'ghost');\n        show('g2-f-handicap-wrap', format === 'first_to' || format === 'most_before' || format === 'lottery');\n        show('g2-f-invitees-wrap', needsInvite(format) || format === 'tournament');\n        var invLabel = el('g2-invitees-label');\n        if (invLabel) {\n          invLabel.textContent = format === 'tournament'\n            ? 'Deltagare (deltar automatiskt, ingen inbjudan krävs)'\n            : format === 'tanks' ? 'Välj EN motståndare'\n            : 'Bjud in kollegor';\n        }\n        renderInfoPanel();\n      }\n\n      function renderFormatGrid() {\n        var grid = el('g2-fmt-grid');\n        if (grid) {\n          grid.innerHTML = FORMAT_ORDER.map(function (f) {\n            var fmt = FORMATS[f];\n            return '<div class=\"g2-fmt-opt' + (state.pickedFormat === f ? ' g2-picked' : '') + '\" data-g2-format=\"' + f + '\">' +\n              '<i class=\"fas fa-' + fmt.icon + '\"></i><span>' + esc(fmt.short) + '</span></div>';\n          }).join('');\n        }\n        var hint = el('g2-format-hint');\n        if (hint) hint.textContent = (FORMATS[state.pickedFormat] || {}).hint || '';\n      }\n\n      function renderMetricRow() {\n        var row = el('g2-metric-row');\n        if (!row) return;\n        row.innerHTML = Object.keys(METRICS).map(function (m) {\n          var met = METRICS[m];\n          return '<div class=\"g2-metric-chip' + (state.pickedMetric === m ? ' g2-picked' : '') + '\" data-g2-metric=\"' + m + '\">' +\n            '<i class=\"fas fa-' + met.icon + '\"></i>' + esc(met.label) + '</div>';\n        }).join('');\n      }\n\n      /* Map cards show a miniature of the ACTUAL terrain the duel is fought on. */\n      function renderMapCards() {\n        var row = el('g2-map-row');\n        if (!row) return;\n        row.innerHTML = Object.keys(TANK_MAPS).map(function (id) {\n          var map = TANK_MAPS[id];\n          var terr = map.points.map(function (p) { return p[0] + ',' + p[1]; }).join(' ') + ' 1000,300 0,300';\n          return '<div class=\"g2-map-card' + (state.pickedMap === id ? ' g2-picked' : '') + '\" data-g2-map=\"' + id + '\">' +\n            '<svg viewBox=\"0 0 1000 300\" preserveAspectRatio=\"none\" style=\"height:52px;\">' +\n            '<defs><linearGradient id=\"g2-minisky-' + id + '\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\n            '<stop offset=\"0\" stop-color=\"' + map.sky[0] + '\"/><stop offset=\"1\" stop-color=\"' + map.sky[1] + '\"/></linearGradient></defs>' +\n            '<rect width=\"1000\" height=\"300\" fill=\"url(#g2-minisky-' + id + ')\"/>' +\n            '<polygon points=\"' + terr + '\" fill=\"' + map.ground + '\"/>' +\n            '<rect x=\"160\" y=\"' + (terrainY(map, 180) - 22).toFixed(0) + '\" width=\"40\" height=\"18\" rx=\"7\" fill=\"#1565C0\"/>' +\n            '<rect x=\"800\" y=\"' + (terrainY(map, 820) - 22).toFixed(0) + '\" width=\"40\" height=\"18\" rx=\"7\" fill=\"#C62828\"/>' +\n            '</svg>' +\n            '<div class=\"g2-map-card-name\">' + esc(map.name) + '</div>' +\n            '<div class=\"g2-map-card-desc\">' + esc(MAP_DESCRIPTIONS[id] || '') + '</div>' +\n            '</div>';\n        }).join('');\n      }\n\n      function renderThemeRow() {\n        var row = el('g2-theme-row');\n        if (!row) return;\n        row.innerHTML = THEMES.map(function (t) {\n          return '<div class=\"g2-theme-opt' + (state.pickedTheme === t.id ? ' g2-picked' : '') + '\" data-g2-theme=\"' + t.id + '\">' +\n            '<i class=\"fas fa-' + t.icon + '\"></i> ' + esc(t.label) + '</div>';\n        }).join('');\n      }\n\n      function renderInvitees() {\n        var container = el('g2-invitees');\n        if (!container) return;\n        var html = '';\n        state.users.filter(function (u) {\n          return u.active !== false && (!state.me || u.id !== state.me.id);\n        }).sort(function (a, b) { return fullName(a) < fullName(b) ? -1 : 1; }).forEach(function (u) {\n          html += '<div class=\"g2-invitee\" data-g2-invitee=\"' + esc(u.id) + '\"><input type=\"checkbox\"><span>' + esc(fullName(u)) + '</span></div>';\n        });\n        container.innerHTML = html || '<div class=\"g2-empty\" style=\"padding:8px;\">Inga kollegor att bjuda in.</div>';\n      }\n\n      function renderTeamRow() {\n        var row = el('g2-team-row');\n        if (!row) return;\n        row.innerHTML = state.teams.map(function (t) {\n          var members = teamMembers(t.id).length;\n          var picked = state.pickedTeams.indexOf(t.id) !== -1;\n          return '<div class=\"g2-metric-chip' + (picked ? ' g2-picked' : '') + '\" data-g2-team=\"' + esc(t.id) + '\">' +\n            '<i class=\"fas fa-users\"></i>' + esc(t.name) + ' <span style=\"opacity:0.6;\">(' + members + ')</span></div>';\n        }).join('') || '<div class=\"g2-empty\">Inga team hittades.</div>';\n      }\n\n      function renderOrderTagRow() {\n        var row = el('g2-ordertag-row');\n        if (!row) return;\n        if (!state.pickedOrderTag && state.orderTags.length) state.pickedOrderTag = state.orderTags[0].id;\n        row.innerHTML = state.orderTags.map(function (t) {\n          return '<div class=\"g2-metric-chip' + (state.pickedOrderTag === t.id ? ' g2-picked' : '') + '\" data-g2-ordertag=\"' + esc(t.id) + '\">' +\n            '<i class=\"fas fa-tag\"></i>' + esc(t.name) + '</div>';\n        }).join('') || '<div class=\"g2-empty\">Inga ordertaggar finns.</div>';\n      }\n\n      function renderRoundRow() {\n        var row = el('g2-round-row');\n        if (!row) return;\n        row.innerHTML = [[4, '4 timmar'], [8, '8 timmar'], [24, '1 dag']].map(function (opt) {\n          return '<div class=\"g2-metric-chip' + (state.pickedRound === opt[0] ? ' g2-picked' : '') + '\" data-g2-round=\"' + opt[0] + '\">' +\n            '<i class=\"far fa-clock\"></i>' + opt[1] + '</div>';\n        }).join('');\n      }\n\n      function renderDifficultyRow() {\n        var row = el('g2-difficulty-row');\n        if (!row) return;\n        row.innerHTML = [['0', 'Lätt'], ['1', 'Medel'], ['2', 'Svår']].map(function (opt) {\n          return '<div class=\"g2-metric-chip' + (state.pickedDifficulty === opt[0] ? ' g2-picked' : '') + '\" data-g2-difficulty=\"' + opt[0] + '\">' + opt[1] + '</div>';\n        }).join('');\n      }\n\n      /* ---------- info panel (right column): explains the picked format ---------- */\n      function formatIllustration(format) {\n        function miniTrack(bg, sprites) {\n          return '<div style=\"background:' + bg + ';position:relative;height:88px;\">' +\n            '<div style=\"position:absolute;top:0;bottom:0;right:14px;width:9px;background:repeating-conic-gradient(#ffffff 0% 25%, #1b1b1b 0% 50%);background-size:9px 9px;opacity:0.9;\"></div>' +\n            sprites + '</div>';\n        }\n        function spriteAt(theme, color, left, top) {\n          return '<div style=\"position:absolute;left:' + left + '%;top:' + top + 'px;\">' + sprite(theme, color) + '</div>';\n        }\n        if (format === 'first_to') {\n          return miniTrack('linear-gradient(180deg,#a8763e,#96683a)',\n            spriteAt('horses', '#FFD54F', 48, 6) + spriteAt('horses', '#4FC3F7', 26, 46) +\n            '<div style=\"position:absolute;left:0;right:0;top:41px;border-top:2px dashed rgba(255,255,255,0.35);\"></div>');\n        }\n        if (format === 'most_before') {\n          return miniTrack('linear-gradient(180deg,#1e88c9,#10639c)',\n            spriteAt('boats', '#FFD54F', 44, 4) + spriteAt('boats', '#FF8A65', 30, 44) +\n            '<div style=\"position:absolute;left:0;right:0;top:44px;border-top:1px solid rgba(255,255,255,0.25);\"></div>');\n        }\n        if (format === 'ghost') {\n          return miniTrack('linear-gradient(180deg,#eef5fb,#d8e8f5)',\n            spriteAt('skis', '#4FC3F7', 40, 6) +\n            '<div style=\"position:absolute;left:26%;top:50px;opacity:0.55;color:#23527c;font-size:22px;\"><i class=\"fas fa-ghost\"></i></div>');\n        }\n        if (format === 'tanks') {\n          var map = TANK_MAPS.desert;\n          var terr = map.points.map(function (p) { return p[0] + ',' + p[1]; }).join(' ') + ' 1000,300 0,300';\n          var sim = simulateShot(map, 180, 1, 48, 88);\n          var pts = [];\n          for (var i = 0; i < sim.points.length; i += 12) pts.push(sim.points[i][0].toFixed(0) + ',' + sim.points[i][1].toFixed(0));\n          return '<svg viewBox=\"0 0 1000 300\" preserveAspectRatio=\"none\" style=\"display:block;width:100%;height:88px;\">' +\n            '<defs><linearGradient id=\"g2-illsky\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"' + map.sky[0] + '\"/><stop offset=\"1\" stop-color=\"' + map.sky[1] + '\"/></linearGradient></defs>' +\n            '<rect width=\"1000\" height=\"300\" fill=\"url(#g2-illsky)\"/>' +\n            '<polygon points=\"' + terr + '\" fill=\"' + map.ground + '\"/>' +\n            '<polyline points=\"' + pts.join(' ') + '\" fill=\"none\" stroke=\"rgba(0,0,0,0.4)\" stroke-width=\"4\" stroke-dasharray=\"4 14\"/>' +\n            tankSvg(180, 'desert', 1, '#1565C0', false) + tankSvg(820, 'desert', -1, '#C62828', false) +\n            '</svg>';\n        }\n        if (format === 'tug') {\n          return '<div style=\"background:var(--g2-secondary);position:relative;height:88px;display:flex;align-items:center;padding:0 12px;\">' +\n            '<div style=\"position:relative;height:26px;border-radius:999px;overflow:hidden;background:rgba(198,40,40,0.35);flex:1;\">' +\n            '<div style=\"position:absolute;left:0;top:0;bottom:0;width:58%;background:linear-gradient(90deg,#1565C0,#4FC3F7);\"></div>' +\n            '<div style=\"position:absolute;left:6%;right:6%;top:50%;height:3px;transform:translateY(-50%);background:repeating-linear-gradient(90deg,#8d6e63 0 7px,#6d4c41 7px 14px);\"></div>' +\n            '<div style=\"position:absolute;left:58%;top:50%;transform:translate(-50%,-50%);width:18px;height:18px;border-radius:50%;background:#fff;border:3px solid #1b2430;\"></div>' +\n            '</div></div>';\n        }\n        if (format === 'relay') {\n          var segs = [100, 100, 62, 15].map(function (fill, i) {\n            return '<div style=\"flex:1;position:relative;border-right:2px solid var(--g2-card);overflow:hidden;\">' +\n              '<div style=\"position:absolute;inset:0;width:' + fill + '%;background:linear-gradient(90deg,#2E7D32,#AED581);\"></div></div>';\n          }).join('');\n          return '<div style=\"background:var(--g2-secondary);height:88px;display:flex;align-items:center;padding:0 12px;\">' +\n            '<div style=\"display:flex;height:24px;border-radius:8px;overflow:hidden;flex:1;border:1px solid var(--g2-border-soft);background:var(--g2-card);\">' + segs + '</div></div>';\n        }\n        if (format === 'streak') {\n          var days = [1, 1, 0, 1, 1, 1, 1, 1, 0, 0].map(function (hit, i) {\n            return '<div style=\"width:17px;height:17px;border-radius:5px;background:' + (hit ? 'linear-gradient(180deg,#FF8F00,#EF6C00)' : 'var(--g2-card)') + ';border:1px solid var(--g2-border-soft);\"></div>';\n          }).join('');\n          return '<div style=\"background:var(--g2-secondary);height:88px;display:flex;align-items:center;justify-content:center;gap:5px;padding:0 10px;\">' + days +\n            '<span style=\"margin-left:8px;font-weight:800;color:#EF6C00;font-size:14px;\"><i class=\"fas fa-fire\"></i> 5</span></div>';\n        }\n        if (format === 'bingo') {\n          var cells = [1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1].map(function (done) {\n            return '<div style=\"border-radius:4px;background:' + (done ? 'color-mix(in srgb, #2E7D32 25%, var(--g2-card))' : 'var(--g2-card)') + ';border:1px solid var(--g2-border-soft);\"></div>';\n          }).join('');\n          return '<div style=\"background:var(--g2-secondary);height:88px;display:flex;align-items:center;justify-content:center;\">' +\n            '<div style=\"display:grid;grid-template-columns:repeat(4,17px);grid-auto-rows:17px;gap:4px;\">' + cells + '</div></div>';\n        }\n        if (format === 'lottery') {\n          return '<div style=\"background:var(--g2-secondary);height:88px;display:flex;align-items:center;justify-content:center;gap:8px;\">' +\n            ['-8', '0', '7'].map(function (rot, i) {\n              return '<div style=\"transform:rotate(' + rot + 'deg);background:' + (i === 1 ? 'linear-gradient(135deg,#8E24AA,#CE93D8)' : 'var(--g2-card)') + ';border:2px dashed ' + (i === 1 ? 'transparent' : 'var(--g2-border)') + ';border-radius:8px;padding:9px 14px;font-weight:800;font-size:12px;color:' + (i === 1 ? '#fff' : 'var(--g2-muted-fg)') + ';\"><i class=\"fas fa-ticket-alt\"></i></div>';\n            }).join('') + '</div>';\n        }\n        if (format === 'tournament') {\n          return '<svg viewBox=\"0 0 280 110\" style=\"display:block;width:100%;height:88px;background:var(--g2-secondary);\">' +\n            '<g fill=\"none\" stroke=\"var(--g2-muted-fg)\" stroke-width=\"2\">' +\n            '<path d=\"M85 25 L110 25 L110 45 M85 75 L110 75 L110 55 M110 50 L135 50\"/>' +\n            '</g>' +\n            '<rect x=\"15\" y=\"12\" width=\"70\" height=\"24\" rx=\"6\" fill=\"var(--g2-card)\" stroke=\"var(--g2-border)\"/>' +\n            '<rect x=\"15\" y=\"62\" width=\"70\" height=\"24\" rx=\"6\" fill=\"var(--g2-card)\" stroke=\"var(--g2-border)\"/>' +\n            '<rect x=\"135\" y=\"38\" width=\"70\" height=\"24\" rx=\"6\" fill=\"color-mix(in srgb, #F9A825 25%, var(--g2-card))\" stroke=\"#F9A825\"/>' +\n            '<text x=\"222\" y=\"57\" font-size=\"20\" fill=\"#F9A825\" font-family=\"inherit\">&#x2605;</text>' +\n            '</svg>';\n        }\n        return '';\n      }\n\n      function renderInfoPanel() {\n        var panel = el('g2-info-panel');\n        if (!panel) return;\n        var format = state.pickedFormat;\n        var fmt = FORMATS[format] || {};\n        var met = METRICS[state.pickedMetric] || {};\n        var metricLine = format === 'bingo' ? 'Blandade mätvärden på brickan' : 'Mäter: ' + (met.label || '');\n        var who = format === 'ghost' ? 'Solo — bara du'\n          : (format === 'tug' || format === 'relay') ? 'Två hela team, ingen inbjudan'\n          : format === 'tanks' ? '1 mot 1 — bjud in en motståndare'\n          : format === 'tournament' ? 'Alla valda deltar automatiskt'\n          : 'Du + inbjudna kollegor';\n        panel.innerHTML = '<div class=\"g2-info-icon-row\">' +\n          '<span class=\"g2-info-badge\"><i class=\"fas fa-' + esc(fmt.icon || 'flag') + '\"></i></span>' +\n          '<span class=\"g2-info-title\">' + esc(fmt.label || '') + '</span></div>' +\n          '<div class=\"g2-info-illustration\">' + formatIllustration(format) + '</div>' +\n          '<div class=\"g2-info-desc\">' + esc(FORMAT_DESC[format] || fmt.hint || '') + '</div>' +\n          '<div class=\"g2-info-fact\"><i class=\"fas fa-' + esc(met.icon || 'chart-bar') + '\"></i> ' + esc(metricLine) + '</div>' +\n          '<div class=\"g2-info-fact\"><i class=\"fas fa-user-friends\"></i> ' + esc(who) + '</div>';\n      }\n\n      function applyQuick(kind) {\n        var name = el('g2-f-name'), target = el('g2-f-target');\n        if (kind === 'powerhour') {\n          if (name) name.value = 'Power hour';\n          state.pickedFormat = 'most_before';\n          state.pickedMetric = 'calls';\n          setDefaultEnds(1);\n        } else if (kind === 'dagsrace') {\n          if (name) name.value = 'Dagens race';\n          state.pickedFormat = 'most_before';\n          state.pickedMetric = 'signed';\n          var d = new Date(); d.setHours(17, 0, 0, 0);\n          if (d.getTime() < Date.now()) d = new Date(Date.now() + 3600000);\n          var pad = function (n) { return (n < 10 ? '0' : '') + n; };\n          var ends = el('g2-f-ends');\n          if (ends) ends.value = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());\n        } else if (kind === 'veckostreak') {\n          if (name) name.value = 'Veckostreak';\n          state.pickedFormat = 'streak';\n          state.pickedMetric = 'signed';\n          if (target) target.value = '1';\n          setDefaultEnds(7 * 24);\n        } else if (kind === 'fredagslotto') {\n          if (name) name.value = 'Fredagslotto';\n          state.pickedFormat = 'lottery';\n          state.pickedMetric = 'signed';\n          setDefaultEnds(8);\n        }\n        renderFormatGrid();\n        renderMetricRow();\n        syncFields();\n      }\n\n      function createGame() {\n        var err = el('g2-modal-error');\n        function fail(msg) { if (err) err.textContent = msg; }\n\n        var name = el('g2-f-name') ? el('g2-f-name').value.trim() : '';\n        var format = state.pickedFormat;\n        var metric = format === 'bingo' ? 'mixed' : state.pickedMetric;\n        var target = parseInt(el('g2-f-target') ? el('g2-f-target').value : '0', 10);\n        var endsRaw = el('g2-f-ends') ? el('g2-f-ends').value : '';\n        var handicap = el('g2-f-handicap') ? el('g2-f-handicap').checked : false;\n\n        if (!name) return fail('Ge tävlingen ett namn.');\n        if (metric === 'ordertag' && !state.pickedOrderTag) return fail('Välj en ordertagg.');\n\n        var needsTarget = format === 'first_to' || format === 'relay' || format === 'streak';\n        if (needsTarget && (!target || target < 1)) return fail('Ange ett mål på minst 1.');\n\n        var needsEnds = format === 'most_before' || format === 'tug' || format === 'relay' ||\n          format === 'streak' || format === 'bingo' || format === 'lottery';\n        var endsAt = null;\n        if (endsRaw && (needsEnds || format === 'first_to' || format === 'tanks')) {\n          endsAt = new Date(endsRaw);\n          if (isNaN(endsAt.getTime())) endsAt = null;\n        }\n        if (needsEnds && (!endsAt || endsAt.getTime() <= Date.now())) return fail('Ange en slut-tid i framtiden.');\n\n        var teams = null;\n        if (format === 'tug' || format === 'relay') {\n          if (state.pickedTeams.length !== 2) return fail('Välj exakt två lag.');\n          var t1 = state.pickedTeams[0], t2 = state.pickedTeams[1];\n          teams = [\n            { teamId: t1, name: (state.teamsById[t1] || {}).name || 'Lag 1' },\n            { teamId: t2, name: (state.teamsById[t2] || {}).name || 'Lag 2' }\n          ];\n          if (!teamMembers(t1).length || !teamMembers(t2).length) return fail('Båda lagen måste ha aktiva medlemmar.');\n        }\n\n        var invited = [];\n        if (needsInvite(format) || format === 'tournament') {\n          document.querySelectorAll('[data-g2-invitee]').forEach(function (n) {\n            var box = n.querySelector('input');\n            if (box && box.checked) invited.push(n.getAttribute('data-g2-invitee'));\n          });\n          if (!invited.length) return fail(format === 'tournament' ? 'Välj minst en motståndare.' : 'Bjud in minst en kollega.');\n          if (format === 'tanks' && invited.length !== 1) return fail('En stridsvagnsduell är 1 mot 1 — välj exakt en motståndare.');\n        }\n\n        var gameId = newId();\n        var doc = {\n          id: gameId,\n          v: 2,\n          name: name,\n          format: format,\n          metric: metric,\n          orderTagId: metric === 'ordertag' ? state.pickedOrderTag : null,\n          orderTagName: metric === 'ordertag' ? (state.orderTags.filter(function (t) { return t.id === state.pickedOrderTag; })[0] || {}).name : null,\n          target: needsTarget ? target : null,\n          endsAt: endsAt ? endsAt.toISOString() : null,\n          roundHours: format === 'tournament' ? state.pickedRound : null,\n          difficulty: format === 'bingo' ? state.pickedDifficulty : null,\n          map: format === 'tanks' ? state.pickedMap : null,\n          theme: state.pickedTheme,\n          handicap: handicap,\n          teams: teams,\n          createdBy: state.me ? state.me.id : null,\n          createdByName: state.me ? fullName(state.me) : '',\n          invitedUserIds: invited,\n          startAt: new Date().toISOString(),\n          createdAt: new Date().toISOString(),\n          canceledAt: null\n        };\n\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var games = ((me.reference || {}).ssg2Games) || {};\n          games[gameId] = doc;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Games: games },\n            replaceReference: false\n          });\n        }).then(function () {\n          closeModal();\n          return load();\n        }).catch(function () {\n          fail('Kunde inte skapa tävlingen. Försök igen.');\n        });\n      }\n\n      function acceptInvite(gameId) {\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var joined = ((me.reference || {}).ssg2Joined) || {};\n          joined[gameId] = { joinedAt: new Date().toISOString() };\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Joined: joined },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      function declineInvite(gameId) {\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var declined = ((me.reference || {}).ssg2Declined) || {};\n          declined[gameId] = true;\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Declined: declined },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      function cancelGame(gameId) {\n        api('GET', '/users/users-v1/me').then(function (me) {\n          var games = ((me.reference || {}).ssg2Games) || {};\n          if (games[gameId]) games[gameId].canceledAt = new Date().toISOString();\n          return api('PUT', '/users/users-v1/me', null, {\n            reference: { ssg2Games: games },\n            replaceReference: false\n          });\n        }).then(load);\n      }\n\n      /* ================= events ================= */\n      function inRoot(node) {\n        var root = document.getElementById('g2-root');\n        return root && node && root.contains(node);\n      }\n\n      function onClick(event) {\n        var target = event.target;\n        if (!target.closest) return;\n\n        var newGame = target.closest('#g2-new-game');\n        if (newGame && inRoot(newGame)) { openModal(); return; }\n        var reload = target.closest('#g2-reload');\n        if (reload && inRoot(reload)) { load(); return; }\n        var cancelBtn = target.closest('#g2-modal-cancel');\n        if (cancelBtn) { closeModal(); return; }\n        var save = target.closest('#g2-modal-save');\n        if (save) { createGame(); return; }\n\n        var quick = target.closest('[data-g2-quick]');\n        if (quick) { applyQuick(quick.getAttribute('data-g2-quick')); return; }\n\n        var theme = target.closest('[data-g2-theme]');\n        if (theme) {\n          state.pickedTheme = theme.getAttribute('data-g2-theme');\n          renderThemeRow();\n          return;\n        }\n        var fmtOpt = target.closest('[data-g2-format]');\n        if (fmtOpt) {\n          state.pickedFormat = fmtOpt.getAttribute('data-g2-format');\n          renderFormatGrid();\n          syncFields();\n          return;\n        }\n        var metricChip = target.closest('[data-g2-metric]');\n        if (metricChip) {\n          state.pickedMetric = metricChip.getAttribute('data-g2-metric');\n          renderMetricRow();\n          syncFields();\n          return;\n        }\n        var mapCard = target.closest('[data-g2-map]');\n        if (mapCard) {\n          state.pickedMap = mapCard.getAttribute('data-g2-map');\n          renderMapCards();\n          return;\n        }\n        var teamChip = target.closest('[data-g2-team]');\n        if (teamChip) {\n          var tid = teamChip.getAttribute('data-g2-team');\n          var at = state.pickedTeams.indexOf(tid);\n          if (at !== -1) state.pickedTeams.splice(at, 1);\n          else {\n            state.pickedTeams.push(tid);\n            if (state.pickedTeams.length > 2) state.pickedTeams.shift();\n          }\n          renderTeamRow();\n          return;\n        }\n        var orderTagChip = target.closest('[data-g2-ordertag]');\n        if (orderTagChip) {\n          state.pickedOrderTag = orderTagChip.getAttribute('data-g2-ordertag');\n          renderOrderTagRow();\n          return;\n        }\n        var roundChip = target.closest('[data-g2-round]');\n        if (roundChip) {\n          state.pickedRound = parseInt(roundChip.getAttribute('data-g2-round'), 10);\n          renderRoundRow();\n          return;\n        }\n        var diffChip = target.closest('[data-g2-difficulty]');\n        if (diffChip) {\n          state.pickedDifficulty = diffChip.getAttribute('data-g2-difficulty');\n          renderDifficultyRow();\n          return;\n        }\n        var invitee = target.closest('[data-g2-invitee]');\n        if (invitee) {\n          var box = invitee.querySelector('input');\n          if (box) box.checked = !box.checked;\n          return;\n        }\n        var accept = target.closest('[data-g2-accept]');\n        if (accept && inRoot(accept)) { acceptInvite(accept.getAttribute('data-g2-accept')); return; }\n        var decline = target.closest('[data-g2-decline]');\n        if (decline && inRoot(decline)) { declineInvite(decline.getAttribute('data-g2-decline')); return; }\n        var cancelGameBtn = target.closest('[data-g2-cancel]');\n        if (cancelGameBtn && inRoot(cancelGameBtn)) { cancelGame(cancelGameBtn.getAttribute('data-g2-cancel')); return; }\n\n        var fire = target.closest('[data-g2-fire]');\n        if (fire && inRoot(fire)) { fireTankShot(fire.getAttribute('data-g2-fire')); return; }\n        var move = target.closest('[data-g2-move]');\n        if (move && inRoot(move)) {\n          var parts = move.getAttribute('data-g2-move').split('|');\n          moveTank(parts[0], parseInt(parts[1], 10));\n          return;\n        }\n\n        var backdrop = target.closest('#g2-modal-backdrop');\n        if (backdrop && target === backdrop) { closeModal(); return; }\n      }\n\n      function onChange(event) {\n        var t = event.target;\n        if (!t) return;\n\n        if (t.closest) {\n          var bingoUser = t.closest('[data-g2-bingo-user]');\n          if (bingoUser && inRoot(bingoUser)) {\n            state.bingoViewUser[bingoUser.getAttribute('data-g2-bingo-user')] = bingoUser.value;\n            render();\n          }\n        }\n      }\n\n      function onKeyDown(event) {\n        if (event.key === 'Escape') closeModal();\n      }\n\n      function onInput(event) {\n        var t = event.target;\n        if (!t || !t.getAttribute) return;\n        var gidAngle = t.getAttribute('data-g2-aim-angle');\n        var gidPower = t.getAttribute('data-g2-aim-power');\n        var gid = gidAngle || gidPower;\n        if (!gid) return;\n        var aim = state.tankAim[gid] || { angle: 45, power: 70 };\n        if (gidAngle) aim.angle = parseInt(t.value, 10);\n        if (gidPower) aim.power = parseInt(t.value, 10);\n        state.tankAim[gid] = aim;\n        var angleVal = el('g2-angle-val-' + gid);\n        if (angleVal) angleVal.innerHTML = aim.angle + '&deg;';\n        var powerVal = el('g2-power-val-' + gid);\n        if (powerVal) powerVal.textContent = aim.power;\n        /* Live-update barrel rotation + power arrow on my tank. */\n        var result = state.results[gid];\n        if (result && result.tanks && state.me) {\n          var dir = state.me.id === result.left ? 1 : -1;\n          var barrel = el('g2-turret-' + gid);\n          if (barrel) barrel.setAttribute('d', barrelPath(aim.angle, dir));\n          var parts = aimArrowParts(aim.angle, aim.power, dir);\n          var line = el('g2-aimline-' + gid);\n          if (line) {\n            line.setAttribute('x1', parts.line.x1.toFixed(1));\n            line.setAttribute('y1', parts.line.y1.toFixed(1));\n            line.setAttribute('x2', parts.line.x2.toFixed(1));\n            line.setAttribute('y2', parts.line.y2.toFixed(1));\n            line.setAttribute('stroke', parts.color);\n          }\n          var head = el('g2-aimhead-' + gid);\n          if (head) {\n            head.setAttribute('points', parts.head);\n            head.setAttribute('fill', parts.color);\n          }\n        }\n      }\n\n      /* Contrast probe — the app's dark theme can resolve --color-primary to a LIGHT\n         color, so white-on-primary is not a safe assumption. Measure and adapt. */\n      function applyContrast() {\n        var root = document.getElementById('g2-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--g2-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var themeObserver = new MutationObserver(applyContrast);\n      themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      /* The app switches theme by rewriting a <style> tag in <head> - html/body attributes never change. */\n      themeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('change', onChange);\n      document.addEventListener('keydown', onKeyDown);\n      document.addEventListener('input', onInput);\n\n      var pollTimer = setInterval(function () {\n        if (!document.hidden && document.getElementById('g2-root')) load();\n      }, POLL_MS);\n\n      window.__g2Cleanup = function () {\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('change', onChange);\n        document.removeEventListener('keydown', onKeyDown);\n        document.removeEventListener('input', onInput);\n        themeObserver.disconnect();\n        clearInterval(pollTimer);\n      };\n\n      applyContrast();\n      load();\n    })();\n  </script>\n</div>","iconName":"trophy","iconColor":"#F9A825","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50f1649b6db16592494e76","name":"Flöden","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":4,"label":"Flöden"}],"html":"<div id=\"flv-root\">\n  <style>\n    #flv-root {\n      --flv-bg: var(--color-background-2, #fafafa);\n      --flv-card: var(--color-background-1, #ffffff);\n      --flv-secondary: var(--color-background-3, #eeeeee);\n      --flv-fg: var(--color-text-1, #212121);\n      --flv-fg2: var(--color-text-2, #616161);\n      --flv-muted-fg: var(--color-text-3, #757575);\n      --flv-primary: var(--color-primary, #1565C0);\n      --flv-danger: var(--color-danger, #E53935);\n      --flv-success: #2E7D32;\n      --flv-warn: #F9A825;\n      --flv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --flv-border: color-mix(in srgb, var(--flv-fg) 13%, transparent);\n      --flv-border-soft: color-mix(in srgb, var(--flv-fg) 8%, transparent);\n      --flv-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--flv-fg);\n      display: block;\n      background: var(--flv-bg);\n      border: 1px solid var(--flv-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #flv-root * { box-sizing: border-box; }\n    #flv-root button { font-family: inherit; cursor: pointer; }\n    #flv-root input { font-family: inherit; font-size: 13.5px; color: var(--flv-fg); }\n\n    /* ---------- toolbar ---------- */\n    .flv-toolbar {\n      display: flex; align-items: center; gap: 10px; flex-wrap: wrap;\n      padding: 12px 16px; border-bottom: 1px solid var(--flv-border-soft);\n      background: var(--flv-card);\n    }\n    .flv-toolbar-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-right: 6px; }\n    .flv-toolbar-title .fas { color: var(--flv-primary); }\n    .flv-chip-row { display: inline-flex; gap: 4px; background: var(--flv-secondary); border-radius: 999px; padding: 3px; }\n    .flv-chip {\n      border: none; background: transparent; border-radius: 999px;\n      padding: 5px 13px; font-size: 12.5px; font-weight: 700; color: var(--flv-fg2);\n    }\n    .flv-chip:hover { background: var(--flv-hover); }\n    .flv-chip.flv-active { background: var(--flv-primary); color: var(--flv-primary-fg, #fff); }\n    .flv-search {\n      padding: 7px 11px; border: 1px solid var(--flv-border); border-radius: var(--flv-radius);\n      background: var(--flv-card); outline: none; width: 190px;\n    }\n    .flv-search:focus { border-color: var(--flv-primary); }\n    .flv-tool-btn {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--flv-border); background: var(--flv-card);\n      border-radius: var(--flv-radius); padding: 7px 12px; font-size: 13px; font-weight: 600;\n      color: var(--flv-fg);\n    }\n    .flv-tool-btn:hover { background: var(--flv-hover); }\n    .flv-toolbar-spacer { flex: 1; }\n    .flv-legend { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: var(--flv-muted-fg); }\n    .flv-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; vertical-align: baseline; }\n    .flv-count-info { font-size: 12px; color: var(--flv-muted-fg); }\n\n    /* ---------- layout: board + inspector ---------- */\n    .flv-body { display: flex; align-items: stretch; min-height: 460px; }\n    .flv-board-outer { flex: 1; min-width: 0; overflow-x: auto; }\n    .flv-lanes {\n      display: flex; gap: 0; padding: 10px 16px 2px; min-width: max-content;\n      font-size: 10.5px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;\n      color: var(--flv-muted-fg);\n    }\n    .flv-lane-label { flex-shrink: 0; }\n    .flv-board { padding: 4px 16px 18px; min-width: max-content; }\n\n    .flv-row {\n      display: flex; align-items: center; min-width: max-content;\n      padding: 9px 0; border-radius: var(--flv-radius);\n    }\n    .flv-row.flv-row-disabled .flv-node { opacity: 0.55; }\n    .flv-row.flv-row-disabled .flv-node:hover { opacity: 0.85; }\n\n    /* ---------- nodes ---------- */\n    .flv-node {\n      flex-shrink: 0; border-radius: var(--flv-radius); border: 1px solid var(--flv-border);\n      background: var(--flv-card); padding: 8px 11px; cursor: pointer; position: relative;\n      transition: box-shadow 120ms ease, border-color 120ms ease;\n      min-height: 58px; display: flex; flex-direction: column; justify-content: center;\n    }\n    .flv-node:hover { box-shadow: 0 3px 12px rgba(0,0,0,0.10); }\n    .flv-node.flv-selected { border-color: var(--flv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--flv-primary) 30%, transparent); }\n    .flv-node-err { border-color: color-mix(in srgb, var(--flv-danger) 55%, var(--flv-border)); }\n\n    .flv-node-title { font-size: 12.5px; font-weight: 800; display: flex; align-items: center; gap: 7px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .flv-node-title .fas, .flv-node-title .far { width: 14px; text-align: center; color: var(--flv-primary); font-size: 12px; }\n    .flv-node-sub { font-size: 11px; color: var(--flv-muted-fg); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n\n    .flv-node-trigger { width: 148px; border-left: 3px solid var(--flv-primary); }\n    .flv-node-cond { width: 186px; }\n    .flv-node-time { width: 128px; }\n    .flv-node-action { width: 228px; }\n\n    .flv-node-pass {\n      border-style: dashed; background: transparent; cursor: pointer;\n      color: var(--flv-muted-fg); align-items: flex-start;\n    }\n    .flv-node-pass .flv-node-title { color: var(--flv-muted-fg); font-weight: 600; }\n    .flv-node-pass .flv-node-title .fas { color: var(--flv-muted-fg); }\n\n    .flv-cond-chips { display: flex; gap: 3px; margin-top: 4px; flex-wrap: nowrap; overflow: hidden; }\n    .flv-tag-chip {\n      font-size: 10px; padding: 1px 7px; border-radius: 999px; font-weight: 700;\n      background: var(--flv-secondary); color: var(--flv-fg2); white-space: nowrap;\n    }\n\n    /* health + status */\n    .flv-health {\n      position: absolute; top: -7px; right: -7px; min-width: 18px; height: 18px;\n      border-radius: 999px; font-size: 10px; font-weight: 800; padding: 0 4px;\n      display: flex; align-items: center; justify-content: center; color: #fff;\n      border: 2px solid var(--flv-bg);\n    }\n    .flv-health-ok { background: var(--flv-success); }\n    .flv-health-err { background: var(--flv-danger); }\n    .flv-health-warn { background: var(--flv-warn); color: #4a3b00; }\n    .flv-health-none { background: var(--flv-secondary); color: var(--flv-muted-fg); }\n    .flv-status-pill {\n      display: inline-flex; align-items: center; gap: 4px; font-size: 9.5px; font-weight: 800;\n      text-transform: uppercase; letter-spacing: 0.05em; border-radius: 999px; padding: 1px 7px;\n      background: var(--flv-secondary); color: var(--flv-muted-fg);\n    }\n    .flv-pill-manual { background: color-mix(in srgb, var(--flv-primary) 14%, transparent); color: var(--flv-primary); }\n    .flv-pill-off { background: color-mix(in srgb, var(--flv-danger) 12%, transparent); color: var(--flv-danger); }\n\n    /* connectors */\n    .flv-link { flex-shrink: 0; width: 26px; height: 2px; background: var(--flv-border); position: relative; }\n    .flv-link::after {\n      content: ''; position: absolute; right: -1px; top: -3px;\n      border-left: 6px solid var(--flv-border); border-top: 4px solid transparent; border-bottom: 4px solid transparent;\n    }\n    .flv-gate { flex-shrink: 0; width: 46px; display: flex; align-items: center; justify-content: center; position: relative; }\n    .flv-gate::before, .flv-gate::after { content: ''; position: absolute; top: 50%; height: 2px; background: var(--flv-border); width: 12px; }\n    .flv-gate::before { left: 0; }\n    .flv-gate::after { right: 0; }\n    .flv-gate-diamond {\n      width: 13px; height: 13px; transform: rotate(45deg); border: 2px solid var(--flv-warn);\n      background: var(--flv-card); border-radius: 2px; z-index: 1;\n    }\n    .flv-gate-diamond.flv-gate-open { border-color: var(--flv-border); }\n\n    /* tag-chain connector: A ──[tagg]──▶ B */\n    .flv-taglink { flex-shrink: 0; display: flex; align-items: center; position: relative; padding: 0 14px; }\n    .flv-taglink::before, .flv-taglink::after { content: ''; position: absolute; top: 50%; height: 2px; background: var(--flv-border); width: 12px; }\n    .flv-taglink::before { left: 0; }\n    .flv-taglink::after { right: 2px; }\n    .flv-taglink .flv-taglink-arrow {\n      position: absolute; right: -1px; top: calc(50% - 4px);\n      border-left: 6px solid var(--flv-border); border-top: 4px solid transparent; border-bottom: 4px solid transparent;\n    }\n    .flv-taglink-pill {\n      font-size: 9.5px; font-weight: 800; white-space: nowrap; z-index: 1;\n      border: 1.5px solid var(--flv-warn); color: var(--flv-warn); background: var(--flv-card);\n      border-radius: 999px; padding: 2px 8px; max-width: 110px; overflow: hidden; text-overflow: ellipsis;\n    }\n    .flv-taglink-pill .fas { font-size: 8px; margin-right: 3px; }\n\n    .flv-row-serie-band {\n      font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em;\n      color: var(--flv-muted-fg); padding: 12px 16px 0; display: flex; align-items: center; gap: 7px;\n    }\n    .flv-row-serie-band .fas { color: var(--flv-warn); }\n\n    /* ---------- inspector ---------- */\n    .flv-inspector {\n      width: 400px; flex-shrink: 0; border-left: 1px solid var(--flv-border-soft);\n      background: var(--flv-card); display: none; flex-direction: column; max-height: 760px;\n    }\n    .flv-inspector.flv-open { display: flex; }\n    .flv-insp-head { padding: 14px 16px 0; }\n    .flv-insp-title { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 9px; }\n    .flv-insp-title .fas { color: var(--flv-primary); }\n    .flv-insp-close {\n      margin-left: auto; border: none; background: transparent; color: var(--flv-muted-fg);\n      font-size: 15px; padding: 4px 6px; border-radius: 6px;\n    }\n    .flv-insp-close:hover { background: var(--flv-hover); }\n    .flv-insp-sub { font-size: 12px; color: var(--flv-muted-fg); margin-top: 3px; }\n    .flv-insp-tabs { display: flex; gap: 2px; margin-top: 12px; border-bottom: 1px solid var(--flv-border-soft); }\n    .flv-insp-tab {\n      border: none; background: transparent; padding: 8px 11px; font-size: 12.5px; font-weight: 700;\n      color: var(--flv-muted-fg); border-bottom: 2px solid transparent; margin-bottom: -1px;\n    }\n    .flv-insp-tab:hover { color: var(--flv-fg); }\n    .flv-insp-tab.flv-active { color: var(--flv-primary); border-bottom-color: var(--flv-primary); }\n    .flv-insp-body { flex: 1; overflow-y: auto; padding: 14px 16px 18px; }\n\n    .flv-def { margin-bottom: 13px; }\n    .flv-def-label { font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em; color: var(--flv-muted-fg); margin-bottom: 3px; }\n    .flv-def-value { font-size: 13px; line-height: 1.45; word-break: break-word; }\n    .flv-def-value .flv-tag-chip { font-size: 11px; }\n\n    .flv-run-item { border: 1px solid var(--flv-border-soft); border-radius: 9px; padding: 9px 11px; margin-bottom: 8px; }\n    .flv-run-head { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 700; cursor: pointer; }\n    .flv-run-head .fas { font-size: 12px; }\n    .flv-run-ok { color: var(--flv-success); }\n    .flv-run-err { color: var(--flv-danger); }\n    .flv-run-warn { color: var(--flv-warn); }\n    .flv-run-meta { margin-left: auto; font-size: 11px; color: var(--flv-muted-fg); font-weight: 600; white-space: nowrap; }\n    .flv-run-detail { display: none; margin-top: 8px; }\n    .flv-run-item.flv-expanded .flv-run-detail { display: block; }\n    .flv-run-kv { font-size: 11.5px; color: var(--flv-fg2); margin-bottom: 3px; word-break: break-all; }\n    .flv-run-kv b { color: var(--flv-fg); }\n    .flv-pre {\n      background: var(--flv-bg); border: 1px solid var(--flv-border-soft); border-radius: 8px;\n      padding: 9px 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n      font-size: 11px; line-height: 1.5; overflow-x: auto; white-space: pre; margin: 5px 0 0;\n      max-height: 300px; overflow-y: auto;\n    }\n    .flv-json-pre { max-height: none; }\n    .flv-empty-note { font-size: 12.5px; color: var(--flv-muted-fg); padding: 8px 0; }\n    .flv-hist-item { border-bottom: 1px solid var(--flv-border-soft); padding: 8px 2px; font-size: 12.5px; }\n    .flv-hist-when { color: var(--flv-muted-fg); font-size: 11px; }\n\n    .flv-loading { display: flex; align-items: center; gap: 10px; color: var(--flv-muted-fg); font-size: 13px; padding: 48px 0; justify-content: center; width: 100%; }\n    .flv-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--flv-border); border-top-color: var(--flv-primary);\n      animation: flv-spin 0.8s linear infinite;\n    }\n    @keyframes flv-spin { to { transform: rotate(360deg); } }\n    .flv-error-banner {\n      margin: 16px; padding: 12px 16px; border-radius: var(--flv-radius);\n      background: color-mix(in srgb, var(--flv-danger) 8%, var(--flv-card));\n      border: 1px solid color-mix(in srgb, var(--flv-danger) 30%, transparent);\n      color: var(--flv-danger); font-size: 13.5px;\n    }\n    .flv-empty-board { padding: 60px 20px; text-align: center; color: var(--flv-muted-fg); font-size: 13.5px; }\n    .flv-empty-board .fas { font-size: 26px; margin-bottom: 10px; opacity: 0.6; display: block; }\n\n    .flv-toast {\n      position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 80;\n      background: var(--flv-fg); color: var(--flv-card); font-size: 13.5px; font-weight: 600;\n      padding: 10px 18px; border-radius: 999px; box-shadow: 0 6px 24px rgba(0,0,0,0.25);\n      opacity: 0; pointer-events: none; transition: opacity 200ms ease;\n    }\n    .flv-toast.flv-show { opacity: 1; }\n\n    /* ---------- phase 2: quick actions ---------- */\n    .flv-insp-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }\n    .flv-mini-btn {\n      display: inline-flex; align-items: center; gap: 6px;\n      border: 1px solid var(--flv-border); background: var(--flv-card); color: var(--flv-fg);\n      border-radius: 8px; padding: 6px 10px; font-size: 12px; font-weight: 700;\n    }\n    .flv-mini-btn:hover { background: var(--flv-hover); }\n    .flv-mini-btn.flv-danger-btn { color: var(--flv-danger); border-color: color-mix(in srgb, var(--flv-danger) 40%, transparent); }\n    .flv-mini-btn.flv-confirming { background: var(--flv-danger); border-color: var(--flv-danger); color: #fff; }\n    .flv-switch { position: relative; width: 34px; height: 19px; border-radius: 999px; background: var(--flv-secondary); transition: background 150ms ease; flex-shrink: 0; }\n    .flv-switch::after {\n      content: ''; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%;\n      background: var(--flv-card); box-shadow: 0 1px 3px rgba(0,0,0,0.3); transition: left 150ms ease;\n    }\n    .flv-switch.flv-on { background: var(--flv-success); }\n    .flv-switch.flv-on::after { left: 17px; }\n    .flv-dev-btn.flv-dev-active { background: var(--flv-primary); border-color: var(--flv-primary); color: var(--flv-primary-fg, #fff); }\n    .flv-node-add {\n      flex-shrink: 0; width: 44px; min-height: 58px; border-radius: var(--flv-radius);\n      border: 1px dashed var(--flv-border); background: transparent; cursor: pointer;\n      display: flex; align-items: center; justify-content: center; color: var(--flv-muted-fg);\n      transition: border-color 120ms ease, color 120ms ease; margin-left: 26px; position: relative;\n    }\n    .flv-node-add::before { content: ''; position: absolute; left: -26px; top: 50%; width: 26px; height: 2px; background: var(--flv-border); }\n    .flv-node-add:hover { border-color: var(--flv-primary); color: var(--flv-primary); }\n\n    /* ---------- phase 2: modal (native SaleSys chrome) ---------- */\n    .flv-modal-backdrop {\n      position: fixed; inset: 0; z-index: 90; background: rgba(0,0,0,0.45);\n      display: none; align-items: center; justify-content: center; padding: 20px;\n    }\n    .flv-modal-backdrop.flv-open { display: flex; }\n    .flv-modal {\n      background: var(--flv-bg); color: var(--flv-fg);\n      border-radius: 4px; width: 880px; max-width: calc(100vw - 40px);\n      max-height: calc(100vh - 60px); display: flex; flex-direction: column;\n      box-shadow: 0 12px 48px rgba(0,0,0,0.3);\n      font-family: var(--font-family-default, 'Nunito', sans-serif);\n    }\n    .flv-modal-sm { width: 640px; }\n    .flv-modal-title { font-size: 24px; color: var(--flv-fg2); padding: 22px 24px 6px; font-weight: 400; }\n    .flv-modal-body { flex: 1; overflow-y: auto; padding: 8px 24px 18px; display: flex; gap: 22px; min-height: 0; }\n    .flv-modal-main { flex: 1; min-width: 0; }\n    .flv-modal-side {\n      width: 230px; flex-shrink: 0; border-left: 1px solid var(--flv-border-soft);\n      padding-left: 18px; font-size: 12.5px; color: var(--flv-fg2); line-height: 1.55;\n    }\n    .flv-modal-side .fas { display: block; font-size: 30px; color: var(--flv-primary); margin: 8px 0 12px; }\n    .flv-modal-side-title { font-weight: 800; color: var(--flv-fg); font-size: 13px; margin-bottom: 4px; }\n    .flv-modal-footer {\n      display: flex; flex-direction: row-reverse; gap: 10px; padding: 14px 24px 18px;\n      border-top: 1px solid var(--flv-border-soft); align-items: center;\n    }\n    .flv-btn {\n      height: 42px; padding: 0 22px; font-size: 15px; font-weight: 700; font-family: inherit;\n      border-radius: var(--border-radius-button, 8px); border: 1px solid var(--flv-border);\n      background: var(--flv-card); color: var(--flv-fg); display: inline-flex; align-items: center; gap: 8px;\n    }\n    .flv-btn:hover { background: var(--flv-hover); }\n    .flv-btn-primary { background: var(--flv-primary); border-color: var(--flv-primary); color: var(--flv-primary-fg, #fff); }\n    .flv-btn-primary:hover { background: var(--flv-primary); filter: brightness(1.08); }\n    .flv-btn[disabled] { opacity: 0.5; pointer-events: none; }\n    .flv-modal-error { color: var(--flv-danger); font-size: 12.5px; margin-right: auto; }\n\n    .flv-step { margin-bottom: 22px; }\n    .flv-step-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }\n    .flv-step-num {\n      width: 24px; height: 24px; border-radius: 50%; background: var(--flv-primary);\n      color: var(--flv-primary-fg, #fff); font-size: 12.5px; font-weight: 800;\n      display: flex; align-items: center; justify-content: center; flex-shrink: 0;\n    }\n    .flv-step-title { font-size: 15px; font-weight: 800; }\n    .flv-step-body { padding-left: 34px; }\n\n    .flv-input-label { font-size: 11.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; color: var(--flv-muted-fg); margin: 12px 0 2px; }\n    .flv-input-label:first-child { margin-top: 0; }\n    .flv-underline-input {\n      width: 100%; border: none; border-bottom: 2px solid var(--flv-border); background: transparent;\n      padding: 7px 2px; font-size: 14px; color: var(--flv-fg); outline: none; font-family: inherit;\n      border-radius: 0;\n    }\n    .flv-underline-input:focus { border-bottom-color: var(--flv-primary); }\n    textarea.flv-underline-input { resize: vertical; min-height: 60px; line-height: 1.5; }\n    .flv-textarea-code {\n      width: 100%; border: 1px solid var(--flv-border); border-radius: 8px; background: var(--flv-card);\n      padding: 10px 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;\n      color: var(--flv-fg); outline: none; resize: vertical; min-height: 120px; line-height: 1.5;\n    }\n    .flv-textarea-code:focus { border-color: var(--flv-primary); }\n\n    .flv-card-grid { display: flex; gap: 8px; flex-wrap: wrap; }\n    .flv-opt-card {\n      border: 1px solid var(--flv-border); border-radius: 9px; background: var(--flv-card);\n      padding: 10px 8px 9px; width: 104px; cursor: pointer; text-align: center;\n      transition: border-color 120ms ease, box-shadow 120ms ease;\n    }\n    .flv-opt-card:hover { box-shadow: 0 3px 10px rgba(0,0,0,0.09); }\n    .flv-opt-card.flv-picked { border-color: var(--flv-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--flv-primary) 30%, transparent); }\n    .flv-opt-card .fas { font-size: 19px; color: var(--flv-primary); display: block; margin-bottom: 6px; }\n    .flv-opt-card-label { font-size: 11.5px; font-weight: 800; line-height: 1.3; }\n    .flv-opt-card-desc { font-size: 10px; color: var(--flv-muted-fg); margin-top: 2px; line-height: 1.35; }\n\n    .flv-tagpick { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 6px; }\n    .flv-picktag {\n      font-size: 12px; font-weight: 700; padding: 4px 12px; border-radius: 999px;\n      border: 1px solid var(--flv-border); background: var(--flv-card); color: var(--flv-fg2); cursor: pointer;\n    }\n    .flv-picktag:hover { background: var(--flv-hover); }\n    .flv-picktag.flv-picked { background: var(--flv-primary); border-color: var(--flv-primary); color: var(--flv-primary-fg, #fff); }\n    .flv-inline-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }\n    .flv-inline-row > div { flex: 1; min-width: 90px; }\n    .flv-num-input { width: 74px; }\n    .flv-kv-row { display: flex; gap: 8px; margin-bottom: 6px; align-items: center; }\n    .flv-kv-row .flv-underline-input { flex: 1; }\n    .flv-kv-del { border: none; background: transparent; color: var(--flv-muted-fg); cursor: pointer; padding: 4px 6px; border-radius: 6px; }\n    .flv-kv-del:hover { color: var(--flv-danger); background: var(--flv-hover); }\n    .flv-add-row-btn {\n      border: 1px dashed var(--flv-border); background: transparent; color: var(--flv-muted-fg);\n      border-radius: 8px; padding: 5px 12px; font-size: 12px; font-weight: 700; margin-top: 2px;\n    }\n    .flv-add-row-btn:hover { color: var(--flv-primary); border-color: var(--flv-primary); }\n    .flv-form-note { font-size: 11.5px; color: var(--flv-muted-fg); margin-top: 6px; line-height: 1.45; }\n    .flv-form-note .fas { font-size: 10px; margin-right: 4px; }\n    .flv-validation { color: var(--flv-danger); font-size: 12.5px; margin: 4px 0; }\n    .flv-diff-list { font-size: 12px; margin: 6px 0; }\n    .flv-diff-list li { margin-bottom: 2px; }\n\n    .flv-serie-step-row {\n      display: flex; align-items: center; gap: 9px; border: 1px solid var(--flv-border-soft);\n      border-radius: 9px; padding: 8px 11px; margin-bottom: 7px; background: var(--flv-card);\n    }\n    .flv-serie-step-name { flex: 1; font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .flv-serie-step-row .flv-mini-btn { padding: 4px 8px; }\n    .flv-worklist { border: 1px solid var(--flv-border); border-radius: 9px; max-height: 220px; overflow-y: auto; margin-top: 6px; background: var(--flv-card); }\n    .flv-worklist-item { padding: 8px 12px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; }\n    .flv-worklist-item:hover { background: var(--flv-hover); }\n    .flv-worklist-item .fas { color: var(--flv-primary); width: 15px; text-align: center; font-size: 12px; }\n  </style>\n\n  <div class=\"flv-toolbar\">\n    <div class=\"flv-toolbar-title\"><i class=\"fas fa-project-diagram\"></i> Flöden</div>\n    <div class=\"flv-chip-row\" id=\"flv-filter-chips\">\n      <button class=\"flv-chip flv-active\" data-flv-filter=\"all\">Alla</button>\n      <button class=\"flv-chip\" data-flv-filter=\"order\">Ordrar</button>\n      <button class=\"flv-chip\" data-flv-filter=\"call\">Samtal</button>\n      <button class=\"flv-chip\" data-flv-filter=\"webhook\" id=\"flv-webhook-chip\" style=\"display:none;\">Händelser</button>\n    </div>\n    <input type=\"text\" class=\"flv-search\" id=\"flv-search\" placeholder=\"Sök arbete eller serie...\">\n    <button class=\"flv-tool-btn\" id=\"flv-reload\"><i class=\"fas fa-sync-alt\"></i> Uppdatera</button>\n    <button class=\"flv-tool-btn\" id=\"flv-new-work\" style=\"border-color: var(--flv-primary); color: var(--flv-primary);\"><i class=\"fas fa-plus\"></i> Nytt arbete</button>\n    <button class=\"flv-tool-btn\" id=\"flv-new-serie\"><i class=\"fas fa-layer-group\"></i> Ny serie</button>\n    <button class=\"flv-tool-btn\" id=\"flv-new-automation\" style=\"display:none; border-color: var(--flv-warn); color: var(--flv-warn);\"><i class=\"fas fa-bolt\"></i> Ny automation</button>\n    <button class=\"flv-tool-btn\" id=\"flv-new-webhook\" style=\"display:none;\"><i class=\"fas fa-satellite-dish\"></i> Rå webhook</button>\n    <button class=\"flv-tool-btn\" id=\"flv-new-json\" style=\"display:none;\"><i class=\"fas fa-file-code\"></i> Ny från JSON</button>\n    <div class=\"flv-toolbar-spacer\"></div>\n    <button class=\"flv-tool-btn flv-dev-btn\" id=\"flv-dev-toggle\" title=\"Developer mode: curl-import, JSON-redigering, export\"><i class=\"fas fa-code\"></i></button>\n    <div class=\"flv-legend\">\n      <span><span class=\"flv-legend-dot\" style=\"background: var(--flv-success)\"></span>Lyckade</span>\n      <span><span class=\"flv-legend-dot\" style=\"background: var(--flv-danger)\"></span>Fel</span>\n      <span><span class=\"flv-legend-dot\" style=\"background: var(--flv-warn)\"></span>Väntar/hoppade</span>\n    </div>\n    <div class=\"flv-count-info\" id=\"flv-count-info\"></div>\n  </div>\n\n  <div class=\"flv-body\">\n    <div class=\"flv-board-outer\">\n      <div id=\"flv-board-wrap\">\n        <div class=\"flv-loading\"><div class=\"flv-spinner\"></div> Laddar flöden…</div>\n      </div>\n    </div>\n\n    <aside class=\"flv-inspector\" id=\"flv-inspector\">\n      <div class=\"flv-insp-head\">\n        <div class=\"flv-insp-title\">\n          <i class=\"fas\" id=\"flv-insp-icon\"></i>\n          <span id=\"flv-insp-name\"></span>\n          <button class=\"flv-insp-close\" id=\"flv-insp-close\" title=\"Stäng\"><i class=\"fas fa-times\"></i></button>\n        </div>\n        <div class=\"flv-insp-sub\" id=\"flv-insp-sub\"></div>\n        <div class=\"flv-insp-actions\" id=\"flv-insp-actions\"></div>\n        <div class=\"flv-insp-tabs\" id=\"flv-insp-tabs\">\n          <button class=\"flv-insp-tab flv-active\" data-flv-tab=\"overview\">Översikt</button>\n          <button class=\"flv-insp-tab\" data-flv-tab=\"runs\">Körningar</button>\n          <button class=\"flv-insp-tab\" data-flv-tab=\"history\">Historik</button>\n          <button class=\"flv-insp-tab\" data-flv-tab=\"json\">JSON</button>\n        </div>\n      </div>\n      <div class=\"flv-insp-body\" id=\"flv-insp-body\"></div>\n    </aside>\n  </div>\n\n  <div class=\"flv-modal-backdrop\" id=\"flv-work-modal\"></div>\n  <div class=\"flv-modal-backdrop\" id=\"flv-serie-modal\"></div>\n  <div class=\"flv-modal-backdrop\" id=\"flv-webhook-modal\"></div>\n  <div class=\"flv-modal-backdrop\" id=\"flv-recipe-modal\"></div>\n\n  <div class=\"flv-toast\" id=\"flv-toast\"></div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var EXEC_WINDOW = 100; /* how many recent orders/calls to scan for executions */\n\n      if (window.__flvCleanup) { try { window.__flvCleanup(); } catch (e) {} }\n\n      var state = {\n        me: null,\n        users: [],\n        works: { order: [], call: [] },\n        series: { order: [], call: [] },\n        presets: { order: [], call: [] },\n        tags: { order: [], call: [] },\n        execs: { order: {}, call: {} },   /* actionId -> [executions] */\n        execsStatus: { order: 'pending', call: 'pending' }, /* pending|loading|done|failed */\n        rows: [],\n        filter: 'all',\n        search: '',\n        selected: null,       /* { type: 'action'|'serie', kind, id } */\n        tab: 'overview',\n        history: {},          /* kind:actionId -> { status, items } */\n        loading: true,\n        fatalError: null,\n        renderStamp: 0,\n        orderFields: [],\n        devMode: false,\n        jsonEdit: false,      /* inspector JSON tab in edit mode */\n        confirmDelete: null,  /* id awaiting delete confirmation */\n        modal: null,          /* work modal: { mode:'create'|'edit'|'json', kind, base, draft, error } */\n        serieModal: null,     /* serie modal: { mode, kind, serieId, draft, error } */\n        webhooks: null,       /* null = unavailable (not admin / api off), [] = none */\n        allowedEvents: [],\n        webhookModal: null,   /* { mode, base, draft, error } */\n        recipeModal: null     /* { events, workKind, workId, authRights, setManual, … } */\n      };\n\n      /* The hook-runner relay that turns a SaleSys event into a work run.\n         (Netlify serverless function — see vyer/floden/README relay notes.) */\n      var RELAY_BASE = 'https://salesys-hook-runner.netlify.app/hook-runner';\n      /* Events that carry a state field worth branching on. */\n      var ROUTABLE = {\n        'offer-status-updated-v1': {\n          field: 'newStatus', entity: 'call', idPath: 'callId',\n          title: 'avtalets status',\n          values: ['signed', 'read', 'distributed', 'reviewPending', 'pending', 'declined', 'canceled', 'expired'],\n          labels: { signed: 'Signerat', read: 'Öppnat / läst', distributed: 'Skickat', reviewPending: 'Väntar granskning', pending: 'Väntande', declined: 'Nekat', canceled: 'Avbrutet', expired: 'Utgånget' }\n        }\n      };\n      function parseAutomation(wh) {\n        if (!wh || !wh.url || wh.url.indexOf('/hook-runner') === -1) return null;\n        try {\n          var u = new URL(wh.url);\n          var dop = u.searchParams.get('do');\n          if (dop === 'route') {\n            var routes = JSON.parse(u.searchParams.get('routes') || '{}');\n            return { routed: true, field: routes.field, entity: routes.entity || 'call', cases: routes.cases || {}, def: routes.default || null };\n          }\n          var actionId = u.searchParams.get('actionId');\n          if (!actionId) return null;\n          return { actionId: actionId, entity: u.searchParams.get('entity') || 'order' };\n        } catch (e) { return null; }\n      }\n\n      /* ---------------- utils ---------------- */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(method, path, params, body) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null && params[k] !== ''; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += (url.indexOf('?') === -1 ? '?' : '&') + qs;\n        }\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) {\n          opts.headers['Content-Type'] = 'application/json';\n          opts.body = JSON.stringify(body);\n        }\n        return fetch(url, opts).then(function (r) {\n          if (!r.ok) {\n            var err = new Error('HTTP ' + r.status);\n            err.status = r.status;\n            throw err;\n          }\n          return r.json().catch(function () { return null; });\n        });\n      }\n      function tryApi(method, path, params) {\n        return api(method, path, params).catch(function () { return null; });\n      }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n\n      var toastTimer = null;\n      function toast(text) {\n        var t = el('flv-toast');\n        if (!t) return;\n        t.textContent = text;\n        t.className = 'flv-toast flv-show';\n        clearTimeout(toastTimer);\n        toastTimer = setTimeout(function () { t.className = 'flv-toast'; }, 3000);\n      }\n\n      function fmtDate(d) {\n        if (!d) return '';\n        var date = new Date(d);\n        if (isNaN(date.getTime())) return String(d);\n        return date.toLocaleString('sv-SE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });\n      }\n      function objectIdDate(id) {\n        if (!id || String(id).length < 8) return null;\n        var seconds = parseInt(String(id).slice(0, 8), 16);\n        return isNaN(seconds) ? null : new Date(seconds * 1000);\n      }\n      function chunk(list, size) {\n        var out = [];\n        for (var i = 0; i < list.length; i += size) out.push(list.slice(i, i + size));\n        return out;\n      }\n      function userName(id) {\n        if (!id || id === '5f4d72e00000000000000000') return 'Automatiskt (system)';\n        for (var i = 0; i < state.users.length; i++) {\n          if (state.users[i].id === id) return state.users[i].fullName || state.users[i].alias || state.users[i].username || id;\n        }\n        return String(id).slice(-6);\n      }\n\n      /* ---------------- prefs (user reference, whole top-level key) ---------------- */\n      var prefsTimer = null;\n      function savePrefs() {\n        clearTimeout(prefsTimer);\n        prefsTimer = setTimeout(function () {\n          var prefs = { devMode: state.devMode, ui: { filter: state.filter } };\n          api('PUT', '/users/users-v1/me', null, { reference: { ksvFlows: prefs }, replaceReference: false })\n            .catch(function () {});\n        }, 900);\n      }\n      function applyPrefs() {\n        var prefs = state.me && state.me.reference && state.me.reference.ksvFlows;\n        if (prefs && prefs.ui && prefs.ui.filter) state.filter = prefs.ui.filter;\n        if (prefs && prefs.devMode) state.devMode = true;\n      }\n\n      /* ---------------- lookup helpers ---------------- */\n      function tagName(kind, id) {\n        var list = state.tags[kind] || [];\n        for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i].name;\n        return String(id).slice(-5);\n      }\n      function presetName(kind, id) {\n        var list = state.presets[kind] || [];\n        for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i].name;\n        return null;\n      }\n      function findWork(kind, id) {\n        var list = state.works[kind] || [];\n        for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];\n        return null;\n      }\n      function findSerie(kind, id) {\n        var list = state.series[kind] || [];\n        for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];\n        return null;\n      }\n\n      /* ---------------- humanizers ---------------- */\n      function opPreview(value, depth) {\n        depth = depth || 0;\n        if (value == null) return '';\n        if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);\n        if (depth > 3) return '…';\n        if (Array.isArray(value)) {\n          return value.map(function (v) { return opPreview(v, depth + 1); }).join(', ');\n        }\n        if (typeof value === 'object') {\n          if (value._value != null) return opPreview(value._value, depth + 1);\n          if (value._text != null) return String(value._text);\n          if (value._path != null) return String(value._path);\n          if (value._pathFirst != null) return String(value._pathFirst);\n          if (value._field != null) return 'fält ' + String(value._field).slice(-5);\n          if (value._firstNonNull) return opPreview(value._firstNonNull[0], depth + 1);\n          if (value._concat) return value._concat.map(function (v) { return opPreview(v, depth + 1); }).join('');\n          var json = JSON.stringify(value);\n          return json.length > 60 ? json.slice(0, 57) + '…' : json;\n        }\n        return String(value);\n      }\n\n      function relTimeText(t) {\n        if (!t) return null;\n        var parts = [];\n        if (t.weeks) parts.push(t.weeks + (t.weeks === 1 ? ' vecka' : ' veckor'));\n        if (t.days) parts.push(t.days + (t.days === 1 ? ' dag' : ' dagar'));\n        if (t.hours) parts.push(t.hours + ' tim');\n        if (t.minutes) parts.push(t.minutes + ' min');\n        return parts.length ? parts.join(' ') : null;\n      }\n\n      function afterText(action) {\n        var a = action.after;\n        var bits = [];\n        if (a) {\n          var rel = relTimeText(a);\n          if (a.tagIdsAdded && a.tagIdsAdded.length) {\n            bits.push((rel ? rel + ' efter tagg: ' : 'När tagg läggs till: ') + a.tagIdsAdded.map(function (id) { return tagName(actionKind(action), id); }).join(', '));\n          } else if (a.tagIdsRemoved && a.tagIdsRemoved.length) {\n            bits.push((rel ? rel + ' efter borttagen tagg: ' : 'När tagg tas bort: ') + a.tagIdsRemoved.map(function (id) { return tagName(actionKind(action), id); }).join(', '));\n          } else if (rel) {\n            bits.push('Efter ' + rel);\n          }\n          if (a.localClockHour != null) bits.push('kl ' + String(a.localClockHour).padStart(2, '0') + ':00');\n          if (a.date) bits.push(fmtDate(a.date));\n        }\n        var b = action.before;\n        if (b) {\n          var relB = relTimeText(b);\n          if (relB) bits.push('senast ' + relB);\n          if (b.localClockHour != null) bits.push('före kl ' + String(b.localClockHour).padStart(2, '0') + ':00');\n        }\n        return bits.join(' · ');\n      }\n\n      /* Every action gets a private marker for which domain it belongs to when rows are built. */\n      function actionKind(action) { return action.__flvKind || 'order'; }\n\n      function condParts(action) {\n        var c = action.conditions || {};\n        var kind = actionKind(action);\n        var parts = [];\n        function tags(ids, prefix) {\n          if (ids && ids.length) {\n            parts.push({ label: prefix, chips: ids.map(function (id) { return tagName(kind, id); }) });\n          }\n        }\n        tags(c.anyTagId, 'Någon av taggarna');\n        tags(c.allTagIds, 'Alla taggarna');\n        tags(c.notAnyTagId, 'Ej tagg');\n        tags(c.notAllTagIds, 'Ej alla taggarna');\n        if (c.fields && c.fields.length) parts.push({ label: c.fields.length + ' fältvillkor', chips: [] });\n        if (c.allFieldIds && c.allFieldIds.length) parts.push({ label: 'Kräver ' + c.allFieldIds.length + ' fält', chips: [] });\n        if (c.anyProductId && c.anyProductId.length) parts.push({ label: 'Någon av ' + c.anyProductId.length + ' produkter', chips: [] });\n        if (c.allProductIds && c.allProductIds.length) parts.push({ label: 'Alla ' + c.allProductIds.length + ' produkter', chips: [] });\n        if (c.anyProductName && c.anyProductName.length) parts.push({ label: 'Produktnamn', chips: c.anyProductName });\n        if (c.anyFileName && c.anyFileName.length) parts.push({ label: 'Fil finns', chips: c.anyFileName });\n        if (c.allFileNames && c.allFileNames.length) parts.push({ label: 'Alla filer', chips: c.allFileNames });\n        if (c.userIds && c.userIds.length) parts.push({ label: c.userIds.length + ' användare', chips: [] });\n        if (c.teamIds && c.teamIds.length) parts.push({ label: c.teamIds.length + ' team', chips: [] });\n        if (c.projectIds && c.projectIds.length) parts.push({ label: c.projectIds.length + (c.projectIds.length === 1 ? ' projekt' : ' projekt'), chips: [] });\n        if (c.isConnected === true) parts.push({ label: 'Uppkopplat samtal', chips: [] });\n        if (c.isDisconnected === true) parts.push({ label: 'Avslutat samtal', chips: [] });\n        /* Generic fallback so unknown/technical condition keys never render as \"Inga villkor\". */\n        var known = ['anyTagId', 'allTagIds', 'notAnyTagId', 'notAllTagIds', 'fields', 'allFieldIds', 'anyProductId',\n          'allProductIds', 'anyProductName', 'allProductNames', 'anyProductCategoryId', 'anyFileName', 'allFileNames',\n          'userIds', 'excludeUserIds', 'teamIds', 'projectIds', 'isConnected', 'isDisconnected', 'noTag', 'anyNegatedTag'];\n        var other = Object.keys(c).filter(function (k) {\n          return known.indexOf(k) === -1 && c[k] != null && !(Array.isArray(c[k]) && !c[k].length);\n        });\n        if (other.length) parts.push({ label: other.length + ' tekniska villkor (' + other.join(', ') + ')', chips: [] });\n        return parts;\n      }\n\n      function toDoType(action) {\n        var t = action.toDo || {};\n        if (t.http) return 'http';\n        if (t.email) return 'email';\n        if (t.sms) return 'sms';\n        if (t.preset) return 'preset';\n        if (t.document) return 'document';\n        if (t.hangUp) return 'hangUp';\n        if (t.endPredictiveMode) return 'endPredictiveMode';\n        return 'unknown';\n      }\n      var TODO_ICONS = {\n        http: 'fa-globe', email: 'fa-envelope', sms: 'fa-mobile-alt', preset: 'fa-cube',\n        document: 'fa-file-alt', hangUp: 'fa-phone-slash', endPredictiveMode: 'fa-stop-circle', unknown: 'fa-question'\n      };\n      var TODO_LABELS = {\n        http: 'HTTP-anrop', email: 'Epost', sms: 'SMS', preset: 'Arbetsmall',\n        document: 'Dokument', hangUp: 'Lägg på', endPredictiveMode: 'Avsluta prediktivt', unknown: 'Okänd åtgärd'\n      };\n\n      function toDoSummary(action) {\n        var t = action.toDo || {};\n        var type = toDoType(action);\n        if (type === 'http') {\n          var url = opPreview(t.http.url);\n          var method = t.http.method || 'GET';\n          return method.toUpperCase() + ' ' + url.replace(/^https?:\\/\\//, '');\n        }\n        if (type === 'email') return 'Till: ' + (opPreview(t.email.to) || '?');\n        if (type === 'sms') return 'Till: ' + (opPreview(t.sms.to) || '?');\n        if (type === 'preset') {\n          var name = presetName(actionKind(action), t.preset.presetId);\n          return name || ('Mall ' + String(t.preset.presetId).slice(-5));\n        }\n        if (type === 'document') return 'Dokumentbearbetning';\n        return '';\n      }\n\n      /* ---------------- data loading ---------------- */\n      function load() {\n        state.loading = true;\n        state.fatalError = null;\n        renderBoard();\n        return Promise.all([\n          api('GET', '/users/users-v1/me'),\n          tryApi('GET', '/orders/after-order-work-v1'),\n          tryApi('GET', '/dial/call-actions-v1'),\n          tryApi('GET', '/orders/after-order-work-series-v1'),\n          tryApi('GET', '/dial/call-action-series-v1'),\n          tryApi('GET', '/orders/tags-v1'),\n          tryApi('GET', '/dial/tags-v1'),\n          tryApi('GET', '/orders/after-order-work-presets-v1'),\n          tryApi('GET', '/dial/call-action-presets-v1'),\n          tryApi('GET', '/users/users-v1'),\n          tryApi('GET', '/orders/fields-v1'),\n          tryApi('GET', '/events/webhooks-v1'),\n          tryApi('GET', '/events/webhooks-v1/events')\n        ]).then(function (r) {\n          state.me = r[0];\n          applyPrefs();\n          state.works.order = normalizeList(r[1]);\n          state.works.call = normalizeList(r[2]);\n          state.series.order = normalizeList(r[3]);\n          state.series.call = normalizeList(r[4]);\n          state.tags.order = normalizeList(r[5], 'tags');\n          state.tags.call = normalizeList(r[6], 'tags');\n          state.presets.order = normalizeList(r[7], 'presets');\n          state.presets.call = normalizeList(r[8], 'presets');\n          state.users = normalizeList(r[9]);\n          state.orderFields = normalizeList(r[10], 'fields');\n          state.webhooks = r[11] ? normalizeList(r[11]) : null;\n          state.allowedEvents = (r[12] && r[12].allowedEvents) || [];\n          state.works.order.forEach(function (w) { w.__flvKind = 'order'; });\n          state.works.call.forEach(function (w) { w.__flvKind = 'call'; });\n          buildRows();\n          state.loading = false;\n          renderAll();\n          loadExecutions();\n        }).catch(function (err) {\n          state.loading = false;\n          state.fatalError = err && err.status === 403\n            ? 'Du saknar rättigheter för att se arbeten. Vyn kräver administratörsåtkomst.'\n            : 'Kunde inte ladda flöden. Försök igen.';\n          renderBoard();\n        });\n      }\n\n      function normalizeList(value, key) {\n        if (!value) return [];\n        if (Array.isArray(value)) return value;\n        if (key && Array.isArray(value[key])) return value[key];\n        var candidates = ['items', 'list', 'actions', 'series', 'presets', 'tags'];\n        for (var i = 0; i < candidates.length; i++) {\n          if (Array.isArray(value[candidates[i]])) return value[candidates[i]];\n        }\n        return [];\n      }\n\n      /* ---------------- tag-chain detection (order works) ----------------\n         Work A whose http.set adds a tag + work B with after.tagIdsAdded on the\n         same tag = an implicit chain A -> B. We surface those edges. */\n      function workSetTags(action) {\n        var out = [];\n        var http = action.toDo && action.toDo.http;\n        if (http && Array.isArray(http.set)) {\n          http.set.forEach(function (s) {\n            (s.tags || []).forEach(function (t) { if (t && t.tagId) out.push(String(t.tagId)); });\n          });\n        }\n        return out;\n      }\n      function workTriggerTags(action) {\n        var a = action.after || {};\n        return {\n          added: (a.tagIdsAdded || []).map(String),\n          removed: (a.tagIdsRemoved || []).map(String)\n        };\n      }\n      function computeTagEdges() {\n        var edges = { out: {}, inn: {} };\n        var works = state.works.order || [];\n        var settersByTag = {};\n        works.forEach(function (a) {\n          workSetTags(a).forEach(function (tagId) {\n            if (!settersByTag[tagId]) settersByTag[tagId] = [];\n            settersByTag[tagId].push(a.id);\n          });\n        });\n        works.forEach(function (b) {\n          var trig = workTriggerTags(b);\n          ['added', 'removed'].forEach(function (mode) {\n            trig[mode].forEach(function (tagId) {\n              (settersByTag[tagId] || []).forEach(function (fromId) {\n                if (fromId === b.id) return;\n                (edges.out[fromId] = edges.out[fromId] || []).push({ to: b.id, tagId: tagId, mode: mode });\n                (edges.inn[b.id] = edges.inn[b.id] || []).push({ from: fromId, tagId: tagId, mode: mode });\n              });\n            });\n          });\n        });\n        state.tagEdges = edges;\n      }\n\n      function buildRows() {\n        var rows = [];\n        computeTagEdges();\n        ['order', 'call'].forEach(function (kind) {\n          var inSerie = {};\n          (state.series[kind] || []).forEach(function (serie) {\n            var steps = [];\n            (serie.actionIds || []).forEach(function (actionId) {\n              var action = findWork(kind, actionId);\n              if (!action) return;\n              inSerie[actionId] = true;\n              var block = null;\n              if (kind === 'order') {\n                block = serie.defaultOptions && serie.defaultOptions.blockOnUnmatchedConditions === false ? false : true;\n                (serie.options || []).forEach(function (o) {\n                  if (o.actionId === actionId && o.blockOnUnmatchedConditions != null) block = o.blockOnUnmatchedConditions;\n                });\n              }\n              steps.push({ action: action, block: block });\n            });\n            if (steps.length) {\n              rows.push({ id: 'serie:' + kind + ':' + serie.id, kind: kind, serie: serie, steps: steps });\n            }\n          });\n          var singles = (state.works[kind] || []).filter(function (a) { return !inSerie[a.id]; });\n          if (kind === 'order') {\n            /* Merge unambiguous linear tag-chains (A -> B) into one row.\n               Only when B's ONLY incoming edge is from A, and A's ONLY outgoing\n               edge is to B, and neither is in a serie. Branches stay separate rows. */\n            var singleIds = {};\n            singles.forEach(function (a) { singleIds[a.id] = true; });\n            function soleSuccessor(aId) {\n              var outs = state.tagEdges.out[aId] || [];\n              if (outs.length !== 1) return null;\n              var edge = outs[0];\n              if (!singleIds[edge.to]) return null;\n              var inns = state.tagEdges.inn[edge.to] || [];\n              if (inns.length !== 1) return null;\n              return edge;\n            }\n            var isChainTarget = {};\n            singles.forEach(function (a) {\n              var edge = soleSuccessor(a.id);\n              if (edge) isChainTarget[edge.to] = true;\n            });\n            var visited = {};\n            singles.forEach(function (head) {\n              if (isChainTarget[head.id] || visited[head.id]) return;\n              var steps = [{ action: head, block: null }];\n              visited[head.id] = true;\n              var current = head.id;\n              var guard = 0;\n              while (guard++ < 30) {\n                var edge = soleSuccessor(current);\n                if (!edge || visited[edge.to]) break;\n                var next = findWork('order', edge.to);\n                if (!next) break;\n                steps.push({ action: next, block: null, viaTag: { tagId: edge.tagId, mode: edge.mode } });\n                visited[edge.to] = true;\n                current = edge.to;\n              }\n              rows.push({\n                id: (steps.length > 1 ? 'chain:' : 'work:') + kind + ':' + head.id,\n                kind: kind, serie: null, chain: steps.length > 1, steps: steps\n              });\n            });\n            /* cycles (A->B->A): nothing was a head — emit leftovers as plain rows */\n            singles.forEach(function (a) {\n              if (visited[a.id]) return;\n              rows.push({ id: 'work:' + kind + ':' + a.id, kind: kind, serie: null, steps: [{ action: a, block: null }] });\n            });\n          } else {\n            singles.forEach(function (action) {\n              rows.push({ id: 'work:' + kind + ':' + action.id, kind: kind, serie: null, steps: [{ action: action, block: null }] });\n            });\n          }\n        });\n        (state.webhooks || []).forEach(function (wh) {\n          var whId = wh.id || wh._id;\n          rows.push({ id: 'webhook:' + whId, kind: 'webhook', webhook: wh, webhookId: whId, steps: [] });\n        });\n        state.rows = rows;\n      }\n\n      /* ---------------- executions ---------------- */\n      function health(kind, actionId) {\n        var list = state.execs[kind][actionId];\n        if (!list || !list.length) return { level: state.execsStatus[kind] === 'done' ? 'none' : 'unknown', ok: 0, err: 0, warn: 0 };\n        var ok = 0, err = 0, warn = 0;\n        list.forEach(function (e) {\n          if (e.error) err++;\n          else if ((e.result && e.result.isSkipped) || e.retryAfter) warn++;\n          else ok++;\n        });\n        var level = err ? 'err' : (warn && !ok ? 'warn' : (ok ? 'ok' : 'none'));\n        return { level: level, ok: ok, err: err, warn: warn };\n      }\n\n      function loadExecutions() {\n        loadOrderExecutions();\n        loadCallExecutions();\n      }\n\n      function registerExec(kind, exec) {\n        var actionId = exec && exec.action && exec.action.id;\n        if (!actionId) return;\n        if (!state.execs[kind][actionId]) state.execs[kind][actionId] = [];\n        state.execs[kind][actionId].push(exec);\n      }\n      function sortExecs(kind) {\n        Object.keys(state.execs[kind]).forEach(function (actionId) {\n          state.execs[kind][actionId].sort(function (a, b) {\n            return new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime();\n          });\n        });\n      }\n\n      function loadOrderExecutions() {\n        if (!state.works.order.length) { state.execsStatus.order = 'done'; return; }\n        state.execsStatus.order = 'loading';\n        api('GET', '/orders/orders-v3', { sortBy: 'serialId', sortOrder: 'desc', count: EXEC_WINDOW, offset: 0 })\n          .then(function (orders) {\n            var list = normalizeList(orders, 'orders');\n            var ids = list.map(function (o) { return o.id; }).filter(Boolean);\n            if (!ids.length) { state.execsStatus.order = 'done'; renderBoard(); return; }\n            var batches = chunk(ids, 30).map(function (part) {\n              return tryApi('GET', '/orders/after-order-work-executions-v1', { orderIds: part.join(','), includeHttp: true })\n                .then(function (execs) {\n                  normalizeList(execs, 'executions').forEach(function (e) { registerExec('order', e); });\n                });\n            });\n            return Promise.all(batches).then(function () {\n              sortExecs('order');\n              state.execsStatus.order = 'done';\n              renderBoard();\n              renderInspector();\n            });\n          })\n          .catch(function () {\n            state.execsStatus.order = 'failed';\n            renderBoard();\n          });\n      }\n\n      function loadCallExecutions() {\n        if (!state.works.call.length) { state.execsStatus.call = 'done'; return; }\n        state.execsStatus.call = 'loading';\n        api('GET', '/dial/calls-v1', { sortBy: 'date', sortOrder: 'desc', count: EXEC_WINDOW, offset: 0 })\n          .then(function (result) {\n            var list = normalizeList(result, 'calls');\n            var ids = list.map(function (c) { return c.id; }).filter(Boolean);\n            if (!ids.length) { state.execsStatus.call = 'done'; renderBoard(); return; }\n            var batches = chunk(ids, 30).map(function (part) {\n              return tryApi('GET', '/dial/call-action-executions-v1', { ids: part.join(',') })\n                .then(function (execs) {\n                  normalizeList(execs, 'executions').forEach(function (e) { registerExec('call', e); });\n                });\n            });\n            return Promise.all(batches).then(function () {\n              sortExecs('call');\n              state.execsStatus.call = 'done';\n              renderBoard();\n              renderInspector();\n            });\n          })\n          .catch(function () {\n            state.execsStatus.call = 'failed';\n            renderBoard();\n          });\n      }\n\n      /* ---------------- history (audit) ---------------- */\n      function loadHistory(kind, actionId) {\n        var key = kind + ':' + actionId;\n        if (state.history[key]) return;\n        state.history[key] = { status: 'loading', items: [] };\n        renderInspector();\n        var path = kind === 'order'\n          ? '/orders/after-order-work-action-updates-v1/' + actionId\n          : '/dial/call-action-updates-v1/' + actionId;\n        api('GET', path, { sortOrder: 'desc', count: 25 })\n          .then(function (items) {\n            state.history[key] = { status: 'done', items: normalizeList(items, 'updates') };\n            renderInspector();\n          })\n          .catch(function () {\n            state.history[key] = { status: 'failed', items: [] };\n            renderInspector();\n          });\n      }\n\n      /* ---------------- rendering ---------------- */\n      function visibleRows() {\n        var query = state.search.trim().toLowerCase();\n        return state.rows.filter(function (row) {\n          if (state.filter !== 'all' && row.kind !== state.filter) return false;\n          if (!query) return true;\n          if (row.webhook) {\n            if (String(row.webhook.url || '').toLowerCase().indexOf(query) !== -1) return true;\n            return (row.webhook.events || []).some(function (e) { return String(e.name || '').toLowerCase().indexOf(query) !== -1; });\n          }\n          if (row.serie && String(row.serie.name || '').toLowerCase().indexOf(query) !== -1) return true;\n          return row.steps.some(function (s) { return String(s.action.name || '').toLowerCase().indexOf(query) !== -1; });\n        }).sort(function (a, b) {\n          var scoreA = rowScore(a), scoreB = rowScore(b);\n          if (scoreA !== scoreB) return scoreB - scoreA;\n          return rowName(a).localeCompare(rowName(b), 'sv');\n        });\n      }\n      function rowName(row) {\n        if (row.webhook) return String(row.webhook.url || '');\n        return row.serie ? (row.serie.name || '') : (row.steps[0] ? row.steps[0].action.name || '' : '');\n      }\n      function rowScore(row) {\n        if (row.webhook) return row.webhook.isEnabled !== false ? 0.5 : -1; /* group webhooks after works */\n        var hasErr = row.steps.some(function (s) { return health(row.kind, s.action.id).err > 0; });\n        var enabled = row.steps.some(function (s) { return s.action.isEnabled; });\n        return (hasErr ? 4 : 0) + (enabled ? 1 : 0);\n      }\n\n      function renderAll() {\n        renderChips();\n        renderBoard();\n        renderInspector();\n        renderWorkModal();\n        renderSerieModal();\n        renderWebhookModal();\n        renderRecipeModal();\n      }\n\n      function renderChips() {\n        var wrap = el('flv-filter-chips');\n        if (!wrap) return;\n        Array.prototype.forEach.call(wrap.querySelectorAll('[data-flv-filter]'), function (btn) {\n          var isActive = btn.getAttribute('data-flv-filter') === state.filter;\n          btn.className = 'flv-chip' + (isActive ? ' flv-active' : '');\n        });\n        var search = el('flv-search');\n        if (search && search.value !== state.search) search.value = state.search;\n        var dev = el('flv-dev-toggle');\n        if (dev) dev.className = 'flv-tool-btn flv-dev-btn' + (state.devMode ? ' flv-dev-active' : '');\n        var newJson = el('flv-new-json');\n        if (newJson) newJson.style.display = state.devMode ? '' : 'none';\n        var whAvailable = state.webhooks !== null;\n        var whChip = el('flv-webhook-chip');\n        if (whChip) whChip.style.display = whAvailable ? '' : 'none';\n        var newAuto = el('flv-new-automation');\n        if (newAuto) newAuto.style.display = whAvailable ? '' : 'none';\n        var newWh = el('flv-new-webhook');\n        if (newWh) newWh.style.display = (whAvailable && state.devMode) ? '' : 'none';\n      }\n\n      function healthBadge(kind, actionId) {\n        var h = health(kind, actionId);\n        if (h.level === 'unknown') return '';\n        if (h.level === 'none') return '<span class=\"flv-health flv-health-none\" title=\"Inga körningar hittades bland de senaste ' + EXEC_WINDOW + '\">–</span>';\n        if (h.level === 'err') return '<span class=\"flv-health flv-health-err\" title=\"' + h.err + ' fel av ' + (h.ok + h.err + h.warn) + ' körningar\">' + h.err + '</span>';\n        if (h.level === 'warn') return '<span class=\"flv-health flv-health-warn\" title=\"' + h.warn + ' hoppade/väntande körningar\">' + h.warn + '</span>';\n        return '<span class=\"flv-health flv-health-ok\" title=\"' + h.ok + ' lyckade körningar\">' + h.ok + '</span>';\n      }\n\n      function nodeHtml(row, step, index) {\n        var action = step.action;\n        var kind = row.kind;\n        var h = health(kind, action.id);\n        var selected = state.selected && state.selected.type === 'action' && state.selected.id === action.id;\n        var parts = condParts(action);\n        var timeText = afterText(action);\n        var html = '';\n\n        /* Villkor segment */\n        html += '<div class=\"flv-node flv-node-cond' + (parts.length ? '' : ' flv-node-pass') + (selected ? ' flv-selected' : '') + '\" data-flv-action=\"' + kind + ':' + esc(action.id) + '\">';\n        if (parts.length) {\n          var first = parts[0];\n          html += '<div class=\"flv-node-title\"><i class=\"fas fa-filter\"></i> ' + esc(first.label) + '</div>';\n          if (first.chips.length) {\n            html += '<div class=\"flv-cond-chips\">' + first.chips.slice(0, 2).map(function (c) { return '<span class=\"flv-tag-chip\">' + esc(c) + '</span>'; }).join('') +\n              (first.chips.length > 2 ? '<span class=\"flv-tag-chip\">+' + (first.chips.length - 2) + '</span>' : '') + '</div>';\n          }\n          if (parts.length > 1) html += '<div class=\"flv-node-sub\">+ ' + (parts.length - 1) + ' villkor till</div>';\n        } else {\n          html += '<div class=\"flv-node-title\"><i class=\"fas fa-filter\"></i> Inga villkor</div>';\n        }\n        html += '</div>';\n\n        html += '<div class=\"flv-link\"></div>';\n\n        /* Tid segment */\n        html += '<div class=\"flv-node flv-node-time' + (timeText ? '' : ' flv-node-pass') + (selected ? ' flv-selected' : '') + '\" data-flv-action=\"' + kind + ':' + esc(action.id) + '\">';\n        html += '<div class=\"flv-node-title\"><i class=\"far fa-clock\"></i> ' + (timeText ? esc(timeText) : 'Direkt') + '</div>';\n        html += '</div>';\n\n        html += '<div class=\"flv-link\"></div>';\n\n        /* Åtgärd segment */\n        var type = toDoType(action);\n        html += '<div class=\"flv-node flv-node-action' + (selected ? ' flv-selected' : '') + (h.err ? ' flv-node-err' : '') + '\" data-flv-action=\"' + kind + ':' + esc(action.id) + '\">';\n        html += healthBadge(kind, action.id);\n        html += '<div class=\"flv-node-title\"><i class=\"fas ' + TODO_ICONS[type] + '\"></i> ' + esc(action.name || TODO_LABELS[type]) + '</div>';\n        html += '<div class=\"flv-node-sub\">' + esc(toDoSummary(action) || TODO_LABELS[type]) + '</div>';\n        var pills = [];\n        if (!action.isEnabled) pills.push('<span class=\"flv-status-pill flv-pill-off\">Inaktiv</span>');\n        if (action.isAutomatic === false) pills.push('<span class=\"flv-status-pill flv-pill-manual\">Manuell</span>');\n        /* unmerged outgoing tag-edges (branching or serie-involved): show a chain badge */\n        if (kind === 'order' && state.tagEdges) {\n          var outs = state.tagEdges.out[action.id] || [];\n          var nextStep = row.steps[index + 1];\n          var mergedOut = nextStep && nextStep.viaTag ? 1 : 0;\n          if (outs.length > mergedOut) {\n            var targets = outs.map(function (e) {\n              var t = findWork('order', e.to);\n              return (t ? t.name : e.to) + ' (tagg ' + tagName('order', e.tagId) + ')';\n            }).join(', ');\n            pills.push('<span class=\"flv-status-pill\" style=\"background: color-mix(in srgb, var(--flv-warn) 16%, transparent); color: var(--flv-warn);\" title=\"Sätter tagg som triggar: ' + esc(targets) + '\"><i class=\"fas fa-link\" style=\"font-size:8px;\"></i> kedjar</span>');\n          }\n        }\n        if (pills.length) html += '<div style=\"margin-top:4px; display:flex; gap:4px;\">' + pills.join('') + '</div>';\n        html += '</div>';\n\n        return html;\n      }\n\n      function webhookRowHtml(row) {\n        var wh = row.webhook;\n        var enabled = wh.isEnabled !== false;\n        var selected = state.selected && state.selected.type === 'webhook' && state.selected.id === row.webhookId;\n        var events = wh.events || [];\n        var firstEvent = events.length ? (events[0].name || '?') : 'inga händelser';\n        var auto = parseAutomation(wh);\n        var html = '<div class=\"flv-row' + (enabled ? '' : ' flv-row-disabled') + '\" data-flv-row=\"' + esc(row.id) + '\">';\n        /* trigger */\n        html += '<div class=\"flv-node flv-node-trigger' + (selected ? ' flv-selected' : '') + '\" data-flv-webhook=\"' + esc(row.webhookId) + '\" style=\"border-left-color: var(--flv-warn);\">';\n        html += '<div class=\"flv-node-title\"><i class=\"fas fa-bolt\" style=\"color: var(--flv-warn);\"></i> Händelse</div>';\n        html += '<div class=\"flv-node-sub\" title=\"' + esc(events.map(function (e) { return e.name; }).join(', ')) + '\">' +\n          esc(firstEvent) + (events.length > 1 ? ' +' + (events.length - 1) : '') + '</div>';\n        html += '</div>';\n        html += '<div class=\"flv-link\"></div>';\n        /* passthrough villkor + tid to keep lane alignment */\n        html += '<div class=\"flv-node flv-node-cond flv-node-pass\" data-flv-webhook=\"' + esc(row.webhookId) + '\">' +\n          '<div class=\"flv-node-title\"><i class=\"fas fa-filter\"></i> Inga villkor</div></div>';\n        html += '<div class=\"flv-link\"></div>';\n        html += '<div class=\"flv-node flv-node-time flv-node-pass\" data-flv-webhook=\"' + esc(row.webhookId) + '\">' +\n          '<div class=\"flv-node-title\"><i class=\"far fa-clock\"></i> Direkt</div></div>';\n\n        if (auto && auto.routed) {\n          /* conditional automation: event -> relay reads a field -> branches to a work per value */\n          var cat = ROUTABLE[firstEvent] || null;\n          var caseKeys = Object.keys(auto.cases);\n          html += '<div class=\"flv-taglink\" title=\"Relayn väljer arbete utifrån ' + esc(auto.field) + '\"><span class=\"flv-taglink-pill\"><i class=\"fas fa-code-branch\"></i>' + esc(auto.field) + '</span><span class=\"flv-taglink-arrow\"></span></div>';\n          html += '<div style=\"display:flex; flex-direction:column; gap:6px;\">';\n          if (!caseKeys.length) html += '<div class=\"flv-node flv-node-action flv-node-pass\"><div class=\"flv-node-title\">Inga grenar</div></div>';\n          caseKeys.forEach(function (val) {\n            var work = findWork(auto.entity, auto.cases[val]);\n            var type = work ? toDoType(work) : 'unknown';\n            var label = cat && cat.labels[val] ? cat.labels[val] : val;\n            html += '<div style=\"display:flex; align-items:center; gap:0;\">' +\n              '<span class=\"flv-tag-chip\" style=\"min-width:78px; border:1px solid var(--flv-border); background:var(--flv-card);\">' + esc(label) + '</span>' +\n              '<span style=\"width:14px; height:2px; background:var(--flv-border); flex-shrink:0;\"></span>' +\n              '<div class=\"flv-node flv-node-action' + (selected ? ' flv-selected' : '') + '\" data-flv-webhook=\"' + esc(row.webhookId) + '\" style=\"min-width:180px;\">' +\n              '<div class=\"flv-node-title\"><i class=\"fas ' + (TODO_ICONS[type] || 'fa-cog') + '\"></i> ' + esc(work ? work.name : 'Arbete …' + String(auto.cases[val]).slice(-5)) + '</div>' +\n              (work ? '' : '<div class=\"flv-node-sub\" style=\"color:var(--flv-danger);\">Saknat arbete</div>') +\n              '</div></div>';\n          });\n          html += '</div>';\n        } else if (auto) {\n          /* automation: event -> relay -> a real work. Draw the work as the action. */\n          var work = findWork(auto.entity, auto.actionId);\n          var type = work ? toDoType(work) : 'unknown';\n          html += '<div class=\"flv-taglink\" title=\"Kör arbetet via hook-runner-relayn\"><span class=\"flv-taglink-pill\" style=\"border-color: var(--flv-primary); color: var(--flv-primary);\"><i class=\"fas fa-play\"></i>kör</span><span class=\"flv-taglink-arrow\"></span></div>';\n          html += '<div class=\"flv-node flv-node-action' + (selected ? ' flv-selected' : '') + '\" data-flv-webhook=\"' + esc(row.webhookId) + '\">';\n          html += '<div class=\"flv-node-title\"><i class=\"fas ' + (TODO_ICONS[type] || 'fa-cog') + '\"></i> ' + esc(work ? work.name : 'Arbete …' + String(auto.actionId).slice(-5)) + '</div>';\n          html += '<div class=\"flv-node-sub\">' + (auto.entity === 'call' ? 'Samtalsarbete' : 'Orderarbete') + (work ? ' · ' + esc(toDoSummary(work) || TODO_LABELS[type]) : '') + '</div>';\n          var apills = [];\n          if (!enabled) apills.push('<span class=\"flv-status-pill flv-pill-off\">Inaktiv</span>');\n          if (!work) apills.push('<span class=\"flv-status-pill flv-pill-off\">Saknat arbete</span>');\n          if (apills.length) html += '<div style=\"margin-top:4px; display:flex; gap:4px;\">' + apills.join('') + '</div>';\n          html += '</div>';\n        } else {\n          html += '<div class=\"flv-link\"></div>';\n          /* raw webhook: outbound POST */\n          var host = String(wh.url || '').replace(/^https?:\\/\\//, '');\n          html += '<div class=\"flv-node flv-node-action' + (selected ? ' flv-selected' : '') + '\" data-flv-webhook=\"' + esc(row.webhookId) + '\">';\n          html += '<div class=\"flv-node-title\"><i class=\"fas fa-satellite-dish\"></i> Webhook</div>';\n          html += '<div class=\"flv-node-sub\" title=\"' + esc(wh.url || '') + '\">POST ' + esc(host) + '</div>';\n          var pills = [];\n          if (!enabled) pills.push('<span class=\"flv-status-pill flv-pill-off\">Inaktiv</span>');\n          if (wh.auth) pills.push('<span class=\"flv-status-pill flv-pill-manual\">Auth</span>');\n          if (wh.retry) pills.push('<span class=\"flv-status-pill\">Retry</span>');\n          if (pills.length) html += '<div style=\"margin-top:4px; display:flex; gap:4px;\">' + pills.join('') + '</div>';\n          html += '</div>';\n        }\n        html += '</div>';\n        return html;\n      }\n\n      function renderBoard() {\n        var wrap = el('flv-board-wrap');\n        if (!wrap) return;\n        state.renderStamp++;\n\n        if (state.loading) {\n          wrap.innerHTML = '<div class=\"flv-loading\"><div class=\"flv-spinner\"></div> Laddar flöden…</div>';\n          return;\n        }\n        if (state.fatalError) {\n          wrap.innerHTML = '<div class=\"flv-error-banner\"><i class=\"fas fa-exclamation-triangle\"></i> ' + esc(state.fatalError) + '</div>';\n          return;\n        }\n\n        var rows = visibleRows();\n        var html = '';\n\n        if (!rows.length) {\n          html = '<div class=\"flv-empty-board\"><i class=\"fas fa-project-diagram\"></i>' +\n            (state.rows.length ? 'Inga flöden matchar filtret.' : 'Inga orderarbeten eller samtalsarbeten finns ännu.') +\n            '</div>';\n          wrap.innerHTML = html;\n          wrap.setAttribute('data-flv-stamp', String(state.renderStamp));\n          updateCountInfo();\n          return;\n        }\n\n        html += '<div class=\"flv-lanes\">' +\n          '<span class=\"flv-lane-label\" style=\"width:148px;\">Trigger</span><span style=\"width:26px;\"></span>' +\n          '<span class=\"flv-lane-label\" style=\"width:186px;\">Villkor</span><span style=\"width:26px;\"></span>' +\n          '<span class=\"flv-lane-label\" style=\"width:128px;\">Tid</span><span style=\"width:26px;\"></span>' +\n          '<span class=\"flv-lane-label\" style=\"width:228px;\">Åtgärd</span>' +\n          '</div>';\n\n        html += '<div class=\"flv-board\">';\n        rows.forEach(function (row) {\n          if (row.webhook) {\n            html += webhookRowHtml(row);\n            return;\n          }\n          var enabled = row.steps.some(function (s) { return s.action.isEnabled; });\n          var isSerieSelected = state.selected && state.selected.type === 'serie' && row.serie && state.selected.id === row.serie.id;\n\n          html += '<div class=\"flv-row' + (enabled ? '' : ' flv-row-disabled') + '\" data-flv-row=\"' + esc(row.id) + '\">';\n\n          /* Trigger node */\n          var triggerIcon = row.kind === 'order' ? 'fa-shopping-cart' : 'fa-phone-alt';\n          var triggerLabel = row.kind === 'order' ? 'Order' : 'Samtal';\n          var triggerSub;\n          if (row.serie) triggerSub = '<i class=\"fas fa-layer-group\" style=\"font-size:9px;\"></i> Serie: ' + esc(row.serie.name || '');\n          else if (row.chain) triggerSub = '<i class=\"fas fa-tags\" style=\"font-size:9px;\"></i> Taggkedja (' + row.steps.length + ' steg)';\n          else triggerSub = row.steps[0].action.isAutomatic === false ? 'Manuell via plussknappen' : 'Automatisk';\n          html += '<div class=\"flv-node flv-node-trigger' + (isSerieSelected ? ' flv-selected' : '') + '\" data-flv-trigger=\"' + esc(row.id) + '\">';\n          html += '<div class=\"flv-node-title\"><i class=\"fas ' + triggerIcon + '\"></i> ' + triggerLabel + '</div>';\n          html += '<div class=\"flv-node-sub\">' + triggerSub + '</div>';\n          html += '</div>';\n          html += '<div class=\"flv-link\"></div>';\n\n          row.steps.forEach(function (step, index) {\n            if (index > 0) {\n              if (step.viaTag) {\n                var linkTagName = tagName('order', step.viaTag.tagId);\n                var removedMode = step.viaTag.mode === 'removed';\n                html += '<div class=\"flv-taglink\" title=\"Taggkedja: föregående arbete sätter taggen &quot;' + esc(linkTagName) + '&quot; — detta arbete körs när den ' + (removedMode ? 'tas bort' : 'läggs till') + '\">' +\n                  '<span class=\"flv-taglink-pill\"><i class=\"fas fa-tag\"></i>' + (removedMode ? '−' : '') + esc(linkTagName) + '</span>' +\n                  '<span class=\"flv-taglink-arrow\"></span></div>';\n              } else if (row.kind === 'order') {\n                var open = step.block === false;\n                html += '<div class=\"flv-gate\" title=\"' + (open ? 'Icke-blockerande: nästa steg körs även om villkoren inte matchar' : 'Blockerande: serien stannar här om villkoren inte matchar') + '\">' +\n                  '<div class=\"flv-gate-diamond' + (open ? ' flv-gate-open' : '') + '\"></div></div>';\n              } else {\n                html += '<div class=\"flv-link\"></div>';\n              }\n            }\n            html += nodeHtml(row, step, index);\n          });\n\n          html += '<div class=\"flv-node-add\" data-flv-addstep=\"' + esc(row.id) + '\" title=\"' +\n            (row.serie ? 'Lägg till/ändra steg i serien' : 'Kedja fler arbeten efter detta (skapar en serie)') +\n            '\"><i class=\"fas fa-plus\"></i></div>';\n\n          html += '</div>';\n        });\n        html += '</div>';\n\n        wrap.innerHTML = html;\n        wrap.setAttribute('data-flv-stamp', String(state.renderStamp));\n        updateCountInfo();\n      }\n\n      function updateCountInfo() {\n        var info = el('flv-count-info');\n        if (!info) return;\n        var works = state.works.order.length + state.works.call.length;\n        var series = state.series.order.length + state.series.call.length;\n        var execNote = '';\n        if (state.execsStatus.order === 'failed' && state.execsStatus.call === 'failed') execNote = ' · körningar otillgängliga';\n        else if (state.execsStatus.order === 'loading' || state.execsStatus.call === 'loading') execNote = ' · hämtar körningar…';\n        var whNote = state.webhooks !== null ? ' · ' + state.webhooks.length + ' webhooks' : '';\n        info.textContent = works + ' arbeten · ' + series + ' serier' + whNote + execNote;\n      }\n\n      /* ---------------- inspector ---------------- */\n      function selectedAction() {\n        if (!state.selected || state.selected.type !== 'action') return null;\n        return findWork(state.selected.kind, state.selected.id);\n      }\n      function selectedSerie() {\n        if (!state.selected || state.selected.type !== 'serie') return null;\n        return findSerie(state.selected.kind, state.selected.id);\n      }\n      function selectedWebhook() {\n        if (!state.selected || state.selected.type !== 'webhook') return null;\n        var list = state.webhooks || [];\n        for (var i = 0; i < list.length; i++) {\n          if ((list[i].id || list[i]._id) === state.selected.id) return list[i];\n        }\n        return null;\n      }\n\n      function renderInspector() {\n        var panel = el('flv-inspector');\n        if (!panel) return;\n        var action = selectedAction();\n        var serie = selectedSerie();\n        var webhook = selectedWebhook();\n        if (!action && !serie && !webhook) {\n          panel.className = 'flv-inspector';\n          return;\n        }\n        panel.className = 'flv-inspector flv-open';\n\n        var icon = el('flv-insp-icon');\n        var name = el('flv-insp-name');\n        var sub = el('flv-insp-sub');\n        var kind = state.selected.kind;\n\n        if (action) {\n          var type = toDoType(action);\n          if (icon) icon.className = 'fas ' + TODO_ICONS[type];\n          if (name) name.textContent = action.name || TODO_LABELS[type];\n          if (sub) sub.textContent = (kind === 'order' ? 'Orderarbete' : 'Samtalsarbete') + ' · ' + TODO_LABELS[type];\n        } else if (serie) {\n          if (icon) icon.className = 'fas fa-layer-group';\n          if (name) name.textContent = serie.name || 'Serie';\n          if (sub) sub.textContent = (kind === 'order' ? 'Orderarbetsserie' : 'Samtalsarbetsserie') + ' · ' + (serie.actionIds || []).length + ' steg';\n        } else {\n          if (icon) icon.className = 'fas fa-bolt';\n          if (name) name.textContent = String(webhook.url || 'Webhook').replace(/^https?:\\/\\//, '').split('/')[0];\n          if (sub) sub.textContent = 'Webhook · ' + (webhook.events || []).length + ' händelser';\n        }\n\n        var tabsWrap = el('flv-insp-tabs');\n        if (tabsWrap) {\n          Array.prototype.forEach.call(tabsWrap.querySelectorAll('[data-flv-tab]'), function (btn) {\n            var tab = btn.getAttribute('data-flv-tab');\n            var available = action ? true : (tab === 'overview' || tab === 'json');\n            btn.style.display = available ? '' : 'none';\n            btn.className = 'flv-insp-tab' + (tab === state.tab ? ' flv-active' : '');\n          });\n        }\n\n        /* quick actions */\n        var actionsWrap = el('flv-insp-actions');\n        if (actionsWrap) {\n          var confirming = state.confirmDelete === (action ? action.id : serie && serie.id);\n          if (action) {\n            actionsWrap.innerHTML =\n              '<span class=\"flv-mini-btn\" data-flv-toggle-enabled=\"1\" style=\"cursor:pointer;\"><span class=\"flv-switch' + (action.isEnabled ? ' flv-on' : '') + '\" style=\"width:28px; height:16px;\"></span> ' + (action.isEnabled ? 'Aktivt' : 'Inaktivt') + '</span>' +\n              '<button class=\"flv-mini-btn\" data-flv-edit-action=\"1\"><i class=\"fas fa-pen\"></i> Redigera</button>' +\n              '<button class=\"flv-mini-btn\" data-flv-duplicate=\"1\"><i class=\"far fa-copy\"></i> Duplicera</button>' +\n              '<button class=\"flv-mini-btn flv-danger-btn' + (confirming ? ' flv-confirming' : '') + '\" data-flv-delete=\"1\"><i class=\"fas fa-trash\"></i> ' + (confirming ? 'Säker? Klicka igen' : 'Ta bort') + '</button>';\n          } else if (serie) {\n            actionsWrap.innerHTML =\n              '<button class=\"flv-mini-btn\" data-flv-edit-serie=\"1\"><i class=\"fas fa-pen\"></i> Redigera serie</button>' +\n              '<button class=\"flv-mini-btn flv-danger-btn' + (confirming ? ' flv-confirming' : '') + '\" data-flv-delete-serie=\"1\"><i class=\"fas fa-trash\"></i> ' + (confirming ? 'Säker? Klicka igen' : 'Ta bort serie') + '</button>';\n          } else if (webhook) {\n            var whEnabled = webhook.isEnabled !== false;\n            actionsWrap.innerHTML =\n              '<span class=\"flv-mini-btn\" data-flv-toggle-webhook=\"1\" style=\"cursor:pointer;\"><span class=\"flv-switch' + (whEnabled ? ' flv-on' : '') + '\" style=\"width:28px; height:16px;\"></span> ' + (whEnabled ? 'Aktiv' : 'Inaktiv') + '</span>' +\n              '<button class=\"flv-mini-btn\" data-flv-edit-webhook=\"1\"><i class=\"fas fa-pen\"></i> Redigera</button>';\n          } else actionsWrap.innerHTML = '';\n        }\n\n        var body = el('flv-insp-body');\n        if (!body) return;\n        if (webhook) {\n          body.innerHTML = state.tab === 'json' ? jsonHtml(webhook, 'webhook') : webhookOverviewHtml(webhook);\n          return;\n        }\n        if (state.tab === 'overview') body.innerHTML = action ? overviewHtml(kind, action) : serieOverviewHtml(kind, serie);\n        else if (state.tab === 'runs' && action) body.innerHTML = runsHtml(kind, action);\n        else if (state.tab === 'history' && action) body.innerHTML = historyHtml(kind, action);\n        else body.innerHTML = jsonHtml(action || serie, kind);\n      }\n\n      function webhookOverviewHtml(wh) {\n        var html = '';\n        var auto = parseAutomation(wh);\n        html += def('Status', wh.isEnabled !== false\n          ? '<span style=\"color: var(--flv-success); font-weight:700;\">Aktiv</span>'\n          : '<span style=\"color: var(--flv-danger); font-weight:700;\">Inaktiv</span>');\n        if (auto && auto.routed) {\n          var rcat = ROUTABLE[(wh.events && wh.events[0] && wh.events[0].name) || ''] || null;\n          html += def('Typ', '<i class=\"fas fa-code-branch\" style=\"color:var(--flv-warn);\"></i> Villkorad automation — väljer arbete utifrån <b>' + esc(auto.field) + '</b>');\n          var rows = Object.keys(auto.cases).map(function (val) {\n            var w = findWork(auto.entity, auto.cases[val]);\n            var label = rcat && rcat.labels[val] ? rcat.labels[val] : val;\n            return '<div style=\"margin-bottom:4px;\"><span class=\"flv-tag-chip\">' + esc(label) + '</span> → ' +\n              (w ? esc(w.name) : '<span style=\"color:var(--flv-danger);\">saknat arbete</span>') + '</div>';\n          }).join('');\n          html += def('Grenar', rows || '<span class=\"flv-empty-note\">Inga.</span>');\n          html += def('Via relä', '<span style=\"font-size:11px; color:var(--flv-muted-fg);\">' + esc(RELAY_BASE) + '</span>');\n        } else if (auto) {\n          var work = findWork(auto.entity, auto.actionId);\n          html += def('Typ', '<i class=\"fas fa-bolt\" style=\"color:var(--flv-warn);\"></i> Automation — händelse kör ett arbete');\n          html += def('Kör arbete', work\n            ? '<span class=\"flv-tag-chip\"><i class=\"fas ' + (TODO_ICONS[toDoType(work)] || 'fa-cog') + '\"></i> ' + esc(work.name) + '</span> <span style=\"color:var(--flv-muted-fg); font-size:11px;\">(' + (auto.entity === 'call' ? 'samtalsarbete' : 'orderarbete') + ')</span>'\n            : '<span style=\"color:var(--flv-danger);\">Saknat arbete (…' + esc(String(auto.actionId).slice(-5)) + ')</span>');\n          html += def('Via relä', '<span style=\"font-size:11px; color:var(--flv-muted-fg);\">' + esc(RELAY_BASE) + '</span>');\n        }\n        html += def('URL', esc(wh.url || ''));\n        var events = wh.events || [];\n        html += def('Händelser (' + events.length + ')', events.map(function (e) {\n          return '<span class=\"flv-tag-chip\">' + esc(e.name || '?') + (e.topic ? ' · ' + esc(e.topic) : '') + '</span>';\n        }).join(' ') || '<span class=\"flv-empty-note\">Inga.</span>');\n        html += def('Autentisering', wh.auth\n          ? 'Skickar Bearer-token' + (wh.authRights && wh.authRights.length ? ' med rättigheter: ' + esc(wh.authRights.join(', ')) : ' (inga rättigheter)')\n          : 'Ingen token skickas');\n        html += def('Retry', wh.retry ? 'Ja — nytt försök efter ca 20 min vid fel' : 'Nej');\n        var created = objectIdDate(wh.id || wh._id);\n        if (created) html += def('Skapad', esc(fmtDate(created)));\n        html += '<div class=\"flv-form-note\" style=\"margin-top:14px;\"><i class=\"fas fa-info-circle\"></i>' +\n          'Begränsningar i SaleSys idag: webhooken POST:ar endast utåt (payload = id:n, inte hela objekt), ' +\n          'leveransförsök loggas inte queryable (ingen körningshistorik kan visas här), och webhooks kan ' +\n          'inte tas bort via API — inaktivera istället.</div>';\n        return html;\n      }\n\n      function def(label, valueHtml) {\n        if (!valueHtml) return '';\n        return '<div class=\"flv-def\"><div class=\"flv-def-label\">' + esc(label) + '</div><div class=\"flv-def-value\">' + valueHtml + '</div></div>';\n      }\n\n      function overviewHtml(kind, action) {\n        var html = '';\n        var statusBits = [];\n        statusBits.push(action.isEnabled ? '<span style=\"color: var(--flv-success); font-weight:700;\">Aktivt</span>' : '<span style=\"color: var(--flv-danger); font-weight:700;\">Inaktivt</span>');\n        statusBits.push(action.isAutomatic === false ? 'Manuellt (plussknappen)' : 'Automatiskt');\n        if (action.isConditionsTest) statusBits.push('Villkorstest (utför inget)');\n        html += def('Status', statusBits.join(' · '));\n\n        var parts = condParts(action);\n        if (parts.length) {\n          var condHtml = parts.map(function (p) {\n            return '<div style=\"margin-bottom:4px;\">' + esc(p.label) +\n              (p.chips.length ? ' ' + p.chips.map(function (c) { return '<span class=\"flv-tag-chip\">' + esc(c) + '</span>'; }).join(' ') : '') + '</div>';\n          }).join('');\n          html += def('Villkor', condHtml);\n        } else {\n          html += def('Villkor', '<span style=\"color: var(--flv-muted-fg);\">Inga — gäller alla ' + (kind === 'order' ? 'ordrar' : 'samtal') + '</span>');\n        }\n\n        html += def('Tid', esc(afterText(action) || 'Körs direkt'));\n\n        var type = toDoType(action);\n        var actionHtml = '<b>' + esc(TODO_LABELS[type]) + '</b>';\n        var summary = toDoSummary(action);\n        if (summary) actionHtml += '<div style=\"margin-top:3px;\">' + esc(summary) + '</div>';\n        if (type === 'email' && action.toDo.email) {\n          actionHtml += '<div style=\"margin-top:3px; color: var(--flv-fg2);\">Ämne: ' + esc(opPreview(action.toDo.email.subject)) + '</div>';\n        }\n        if (type === 'sms' && action.toDo.sms) {\n          var msg = opPreview(action.toDo.sms.message);\n          actionHtml += '<div style=\"margin-top:3px; color: var(--flv-fg2);\">' + esc(msg.length > 140 ? msg.slice(0, 137) + '…' : msg) + '</div>';\n        }\n        if (type === 'http' && action.toDo.http) {\n          var extras = [];\n          if (action.toDo.http.pre) extras.push('pre-request');\n          if (action.toDo.http.set && action.toDo.http.set.length) extras.push(action.toDo.http.set.length + ' sättare');\n          if (action.toDo.http.setPath) extras.push('setPath');\n          if (action.toDo.http.attachFiles || action.toDo.http.attachFilesPath) extras.push('bifogar filer');\n          if (extras.length) actionHtml += '<div style=\"margin-top:3px; color: var(--flv-fg2);\">' + esc(extras.join(' · ')) + '</div>';\n        }\n        html += def('Åtgärd', actionHtml);\n\n        if (action.maxExecutions != null && action.maxExecutions !== 1) {\n          html += def('Max körningar', esc(String(action.maxExecutions)) + (action.executeAgainConditions && action.executeAgainConditions.length ? ' (med omkörningsvillkor)' : ''));\n        }\n\n        /* Serie membership */\n        var series = (state.series[kind] || []).filter(function (s) { return (s.actionIds || []).indexOf(action.id) !== -1; });\n        if (series.length) {\n          html += def('Ingår i serie', series.map(function (s) { return '<span class=\"flv-tag-chip\">' + esc(s.name) + '</span>'; }).join(' '));\n        }\n\n        /* Tag-chain relations */\n        if (kind === 'order' && state.tagEdges) {\n          var setTags = workSetTags(action);\n          if (setTags.length) {\n            html += def('Sätter taggar vid svar', setTags.map(function (id) {\n              return '<span class=\"flv-tag-chip\">' + esc(tagName('order', id)) + '</span>';\n            }).join(' '));\n          }\n          var chainOuts = state.tagEdges.out[action.id] || [];\n          if (chainOuts.length) {\n            html += def('Kedjar vidare till', chainOuts.map(function (e) {\n              var t = findWork('order', e.to);\n              return '<div style=\"margin-bottom:3px;\"><i class=\"fas fa-link\" style=\"font-size:10px; color:var(--flv-warn);\"></i> ' +\n                esc(t ? t.name : e.to) + ' <span style=\"color:var(--flv-muted-fg); font-size:11px;\">(tagg ' + esc(tagName('order', e.tagId)) + (e.mode === 'removed' ? ' tas bort' : '') + ')</span></div>';\n            }).join(''));\n          }\n          var chainIns = state.tagEdges.inn[action.id] || [];\n          if (chainIns.length) {\n            html += def('Kedjas från', chainIns.map(function (e) {\n              var f = findWork('order', e.from);\n              return '<div style=\"margin-bottom:3px;\"><i class=\"fas fa-link\" style=\"font-size:10px; color:var(--flv-warn);\"></i> ' +\n                esc(f ? f.name : e.from) + ' <span style=\"color:var(--flv-muted-fg); font-size:11px;\">(sätter tagg ' + esc(tagName('order', e.tagId)) + ')</span></div>';\n            }).join(''));\n          }\n        }\n\n        var h = health(kind, action.id);\n        if (h.level !== 'unknown' && h.level !== 'none') {\n          html += def('Körningar (senaste ' + EXEC_WINDOW + ' ' + (kind === 'order' ? 'ordrarna' : 'samtalen') + ')',\n            '<span style=\"color: var(--flv-success); font-weight:700;\">' + h.ok + ' ok</span> · ' +\n            '<span style=\"color: var(--flv-danger); font-weight:700;\">' + h.err + ' fel</span> · ' +\n            '<span style=\"color: var(--flv-warn); font-weight:700;\">' + h.warn + ' väntar/hoppade</span>');\n        } else if (state.execsStatus[kind] === 'failed') {\n          html += def('Körningar', '<span style=\"color: var(--flv-muted-fg);\">Kunde inte hämtas (saknar rättighet att lista ' + (kind === 'order' ? 'ordrar' : 'samtal') + ')</span>');\n        }\n\n        var created = objectIdDate(action.id);\n        if (created) html += def('Skapat', esc(fmtDate(created)));\n\n        return html || '<div class=\"flv-empty-note\">Ingen information.</div>';\n      }\n\n      function serieOverviewHtml(kind, serie) {\n        var html = '';\n        if (serie.description) html += def('Beskrivning', esc(serie.description));\n        var stepsHtml = (serie.actionIds || []).map(function (id, i) {\n          var action = findWork(kind, id);\n          var blockNote = '';\n          if (kind === 'order' && i > 0) {\n            var block = serie.defaultOptions && serie.defaultOptions.blockOnUnmatchedConditions === false ? false : true;\n            (serie.options || []).forEach(function (o) { if (o.actionId === id && o.blockOnUnmatchedConditions != null) block = o.blockOnUnmatchedConditions; });\n            blockNote = block ? ' · blockerande' : ' · icke-blockerande';\n          }\n          return '<div style=\"margin-bottom:5px;\">' + (i + 1) + '. ' + esc(action ? action.name : 'Saknat arbete ' + String(id).slice(-5)) +\n            '<span style=\"color: var(--flv-muted-fg); font-size: 11px;\">' + blockNote + '</span></div>';\n        }).join('');\n        html += def('Steg', stepsHtml || '<span class=\"flv-empty-note\">Inga steg.</span>');\n        if (serie.maxExecutions != null) html += def('Max körningar totalt', esc(String(serie.maxExecutions)));\n        var created = objectIdDate(serie.id);\n        if (created) html += def('Skapad', esc(fmtDate(created)));\n        return html;\n      }\n\n      function runsHtml(kind, action) {\n        var status = state.execsStatus[kind];\n        if (status === 'loading' || status === 'pending') {\n          return '<div class=\"flv-loading\" style=\"padding: 24px 0;\"><div class=\"flv-spinner\"></div> Hämtar körningar…</div>';\n        }\n        if (status === 'failed') {\n          return '<div class=\"flv-empty-note\">Kunde inte hämta körningar — vyn behöver kunna lista ' + (kind === 'order' ? 'ordrar' : 'samtal') + '.</div>';\n        }\n        var list = state.execs[kind][action.id] || [];\n        if (!list.length) {\n          return '<div class=\"flv-empty-note\">Inga körningar hittades bland de senaste ' + EXEC_WINDOW + ' ' + (kind === 'order' ? 'ordrarna' : 'samtalen') + '.</div>';\n        }\n        var html = '';\n        list.slice(0, 40).forEach(function (e, i) {\n          var isErr = !!e.error;\n          var isSkip = (e.result && e.result.isSkipped) || false;\n          var iconClass = isErr ? 'fa-times-circle flv-run-err' : (isSkip || e.retryAfter ? 'fa-exclamation-circle flv-run-warn' : 'fa-check-circle flv-run-ok');\n          var label = isErr ? 'Fel' : (isSkip ? 'Hoppades över' : (e.retryAfter ? 'Väntar på nytt försök' : 'Lyckades'));\n          var entity = e.orderSerialId != null ? 'Order #' + e.orderSerialId : (e.orderId ? 'Order …' + String(e.orderId).slice(-5) : (e.callId ? 'Samtal …' + String(e.callId).slice(-5) : ''));\n          html += '<div class=\"flv-run-item\" data-flv-run=\"' + i + '\">';\n          html += '<div class=\"flv-run-head\" data-flv-run-toggle=\"' + i + '\">' +\n            '<i class=\"fas ' + iconClass + '\"></i> ' + esc(label) +\n            '<span class=\"flv-run-meta\">' + esc(entity) + (entity ? ' · ' : '') + esc(fmtDate(e.executedAt)) + '</span></div>';\n          html += '<div class=\"flv-run-detail\">';\n          if (e.userId) html += '<div class=\"flv-run-kv\"><b>Utförd av:</b> ' + esc(userName(e.userId)) + '</div>';\n          if (e.executionTime != null) html += '<div class=\"flv-run-kv\"><b>Tid:</b> ' + esc(String(e.executionTime)) + ' ms</div>';\n          if (e.retryAfter) html += '<div class=\"flv-run-kv\"><b>Nytt försök efter:</b> ' + esc(fmtDate(e.retryAfter)) + '</div>';\n          if (e.previousTotal != null && e.previousTotal > 0) {\n            html += '<div class=\"flv-run-kv\"><b>Tidigare körningar på samma order:</b> ' + esc(String(e.previousOk || 0)) + ' ok, ' + esc(String(e.previousError || 0)) + ' fel</div>';\n          }\n          if (e.result && e.result.http) {\n            if (e.result.http.url) html += '<div class=\"flv-run-kv\"><b>URL:</b> ' + esc(e.result.http.url) + '</div>';\n            if (e.result.http.link) html += '<div class=\"flv-run-kv\"><b>Länk:</b> ' + esc(e.result.http.link) + '</div>';\n            if (e.result.http.requestBody != null) html += '<div class=\"flv-run-kv\"><b>Request:</b></div><pre class=\"flv-pre\">' + esc(pretty(e.result.http.requestBody)) + '</pre>';\n            if (e.result.http.responseBody != null) html += '<div class=\"flv-run-kv\"><b>Response:</b></div><pre class=\"flv-pre\">' + esc(pretty(e.result.http.responseBody)) + '</pre>';\n            else if (e.result.http.isResponseBodyAlwaysAvailable === false) html += '<div class=\"flv-run-kv\" style=\"color: var(--flv-muted-fg);\">HTTP-kroppar ej sparade för detta arbete.</div>';\n          }\n          if (e.error) html += '<div class=\"flv-run-kv\"><b>Fel:</b></div><pre class=\"flv-pre\">' + esc(pretty(e.error)) + '</pre>';\n          html += '</div></div>';\n        });\n        if (list.length > 40) html += '<div class=\"flv-empty-note\">Visar 40 av ' + list.length + ' körningar.</div>';\n        return html;\n      }\n\n      function historyHtml(kind, action) {\n        var key = kind + ':' + action.id;\n        var entry = state.history[key];\n        if (!entry) { loadHistory(kind, action.id); return '<div class=\"flv-loading\" style=\"padding: 24px 0;\"><div class=\"flv-spinner\"></div></div>'; }\n        if (entry.status === 'loading') return '<div class=\"flv-loading\" style=\"padding: 24px 0;\"><div class=\"flv-spinner\"></div> Hämtar historik…</div>';\n        if (entry.status === 'failed') return '<div class=\"flv-empty-note\">Kunde inte hämta ändringshistorik.</div>';\n        if (!entry.items.length) return '<div class=\"flv-empty-note\">Inga ändringar registrerade.</div>';\n        return entry.items.map(function (u) {\n          var when = objectIdDate(u.id);\n          var changed = u.beforeUpdate ? Object.keys(u.beforeUpdate).join(', ') : '';\n          return '<div class=\"flv-hist-item\">' +\n            '<div><b>' + esc(userName(u.userId)) + '</b>' + (changed ? ' ändrade: ' + esc(changed) : ' gjorde en ändring') + '</div>' +\n            '<div class=\"flv-hist-when\">' + esc(when ? fmtDate(when) : '') + (u.ip ? ' · ' + esc(u.ip) : '') + '</div>' +\n            '</div>';\n        }).join('');\n      }\n\n      function pretty(value) {\n        if (value == null) return '';\n        if (typeof value === 'string') {\n          try { return JSON.stringify(JSON.parse(value), null, 2); } catch (e) { return value; }\n        }\n        try { return JSON.stringify(value, null, 2); } catch (e) { return String(value); }\n      }\n\n      function jsonHtml(entity, kind) {\n        if (!entity) return '';\n        var clean = {};\n        Object.keys(entity).forEach(function (k) { if (k.indexOf('__flv') !== 0) clean[k] = entity[k]; });\n        var isAction = state.selected && state.selected.type === 'action';\n        var html = '<div style=\"display:flex; gap:6px; margin-bottom:10px; flex-wrap:wrap;\">' +\n          '<button class=\"flv-mini-btn\" id=\"flv-copy-json\"><i class=\"far fa-copy\"></i> Kopiera JSON</button>';\n        if (isAction && state.devMode) {\n          html += '<button class=\"flv-mini-btn\" id=\"flv-copy-curl\"><i class=\"fas fa-terminal\"></i> Kopiera som curl</button>';\n          html += state.jsonEdit\n            ? '<button class=\"flv-mini-btn\" id=\"flv-json-save\" style=\"color: var(--flv-success); border-color: color-mix(in srgb, var(--flv-success) 40%, transparent);\"><i class=\"fas fa-check\"></i> Validera &amp; spara</button>' +\n              '<button class=\"flv-mini-btn\" id=\"flv-json-cancel\"><i class=\"fas fa-times\"></i> Avbryt</button>'\n            : '<button class=\"flv-mini-btn\" id=\"flv-json-edit\"><i class=\"fas fa-pen\"></i> Redigera JSON</button>';\n        }\n        html += '</div>';\n        if (state.jsonEdit && isAction && state.devMode) {\n          html += '<div class=\"flv-validation\" id=\"flv-json-errors\" style=\"display:none;\"></div>';\n          html += '<textarea class=\"flv-textarea-code\" id=\"flv-json-editarea\" style=\"min-height:380px;\">' + esc(pretty(clean)) + '</textarea>';\n          html += '<div class=\"flv-form-note\"><i class=\"fas fa-info-circle\"></i>Sparas som PUT med hela objektet. Interna fält (id, organizationId, …) tas bort automatiskt.</div>';\n        } else {\n          html += '<pre class=\"flv-pre flv-json-pre\" id=\"flv-json-src\">' + esc(pretty(clean)) + '</pre>';\n        }\n        return html;\n      }\n\n      function submitJsonEdit() {\n        var action = selectedAction();\n        var textarea = el('flv-json-editarea');\n        var errBox = el('flv-json-errors');\n        if (!action || !textarea) return;\n        var obj;\n        try { obj = JSON.parse(textarea.value); }\n        catch (e) {\n          if (errBox) { errBox.style.display = ''; errBox.textContent = 'Ogiltig JSON: ' + e.message; }\n          return;\n        }\n        var errors = validateWorkJson(obj);\n        if (errors.length) {\n          if (errBox) { errBox.style.display = ''; errBox.textContent = errors.join(' '); }\n          return;\n        }\n        var kind = state.selected.kind;\n        var body = stripInternal(obj);\n        var changed = [];\n        Object.keys(body).forEach(function (k) {\n          if (JSON.stringify(body[k]) !== JSON.stringify(action[k])) changed.push(k);\n        });\n        Object.keys(action).forEach(function (k) {\n          if (k.indexOf('__flv') === 0 || ['id', 'organizationId', 'automaticExecutionAt', 'isAutomaticallyExecutingNow', 'deletedAt'].indexOf(k) !== -1) return;\n          if (!(k in body) && action[k] != null) changed.push(k + ' (borttagen — skickas ej, behålls)');\n        });\n        api('PUT', workPath(kind) + '/' + action.id, null, body).then(function () {\n          toast(changed.length ? 'Sparat. Ändrade: ' + changed.slice(0, 5).join(', ') : 'Sparat (inga ändringar)');\n          state.jsonEdit = false;\n          return refresh();\n        }).catch(function (err) {\n          if (errBox) { errBox.style.display = ''; errBox.textContent = 'Kunde inte spara (' + (err && err.status) + ').'; }\n        });\n      }\n\n      /* ================= PHASE 2: editing ================= */\n\n      function fieldLabel(id) {\n        for (var i = 0; i < state.orderFields.length; i++) {\n          if (state.orderFields[i].id === id) return state.orderFields[i].label || id;\n        }\n        return null;\n      }\n      function deepClone(v) { return v == null ? v : JSON.parse(JSON.stringify(v)); }\n\n      /* Operator wrapper handling: read an editable string out of common wrappers and\n         write it back in the SAME shape, so prod values like { _firstNonNull: [str] }\n         round-trip losslessly. */\n      function opEditable(v) {\n        if (v == null) return { kind: 'plain', str: '' };\n        if (typeof v === 'string') return { kind: 'plain', str: v };\n        if (typeof v === 'object' && !Array.isArray(v)) {\n          var keys = Object.keys(v);\n          if (keys.length === 1 && typeof v._text === 'string') return { kind: 'text', str: v._text };\n          if (keys.length === 1 && typeof v._value === 'string') return { kind: 'value', str: v._value };\n          if (keys.length === 1 && Array.isArray(v._firstNonNull) && v._firstNonNull.length === 1 && typeof v._firstNonNull[0] === 'string') {\n            return { kind: 'firstNonNull', str: v._firstNonNull[0] };\n          }\n          if (typeof v._path === 'string' && keys.length === 1) return { kind: 'path', str: v._path };\n          if (v._field != null) return { kind: 'field', str: '', fieldId: String(v._field) };\n        }\n        return { kind: 'complex', str: '', original: v };\n      }\n      function opWrite(desc, str) {\n        if (desc.kind === 'text') return { _text: str };\n        if (desc.kind === 'value') return { _value: str };\n        if (desc.kind === 'firstNonNull') return { _firstNonNull: [str] };\n        return str;\n      }\n\n      /* ---------------- draft model ---------------- */\n      function emptyDraft(kind) {\n        return {\n          name: '', isEnabled: true, isAutomatic: true,\n          tags: { any: [], all: [], not: [] },\n          time: { unit: 'direct', amount: 1, clockHour: '', trigger: 'none', trigTags: [] },\n          todoType: kind === 'call' ? 'sms' : 'http',\n          wraps: {},\n          sms: { toMode: kind === 'call' ? 'phonePath' : 'field', toField: '', toText: '', message: '', senderId: '' },\n          email: { toMode: kind === 'call' ? 'valueText' : 'field', toField: '', toText: '', subject: '', body: '', senderName: '', senderAddress: '' },\n          http: { method: 'POST', url: '', headers: [], query: [], bodyText: '' },\n          preset: { presetId: '', stateText: '' },\n          fieldSearch: ''\n        };\n      }\n\n      function draftFromWork(kind, work) {\n        var d = emptyDraft(kind);\n        d.name = work.name || '';\n        d.isEnabled = !!work.isEnabled;\n        d.isAutomatic = work.isAutomatic !== false;\n        var c = work.conditions || {};\n        d.tags.any = (c.anyTagId || []).slice();\n        d.tags.all = (c.allTagIds || []).slice();\n        d.tags.not = (c.notAnyTagId || []).slice();\n        var a = work.after || {};\n        var units = ['minutes', 'hours', 'days', 'weeks'];\n        for (var i = 0; i < units.length; i++) {\n          if (a[units[i]]) { d.time.unit = units[i]; d.time.amount = a[units[i]]; break; }\n        }\n        if (a.localClockHour != null) d.time.clockHour = String(a.localClockHour);\n        if (a.tagIdsAdded && a.tagIdsAdded.length) { d.time.trigger = 'added'; d.time.trigTags = a.tagIdsAdded.slice(); }\n        else if (a.tagIdsRemoved && a.tagIdsRemoved.length) { d.time.trigger = 'removed'; d.time.trigTags = a.tagIdsRemoved.slice(); }\n        d.todoType = toDoType(work);\n        var t = work.toDo || {};\n        if (t.sms) {\n          var toD = opEditable(t.sms.to), msgD = opEditable(t.sms.message);\n          d.wraps.smsTo = toD; d.wraps.smsMessage = msgD;\n          if (toD.kind === 'field') { d.sms.toMode = 'field'; d.sms.toField = toD.fieldId; }\n          else if (toD.kind === 'path') { d.sms.toMode = toD.str === '$.phoneNumber' ? 'phonePath' : 'path'; d.sms.toText = toD.str; }\n          else if (toD.kind !== 'complex') { d.sms.toMode = 'valueText'; d.sms.toText = toD.str; }\n          else d.sms.toMode = 'complex';\n          d.sms.message = msgD.str;\n          d.sms.senderId = t.sms.senderId || '';\n        }\n        if (t.email) {\n          var eToD = opEditable(t.email.to), subD = opEditable(t.email.subject), bodyD = opEditable(t.email.body);\n          d.wraps.emailTo = eToD; d.wraps.emailSubject = subD; d.wraps.emailBody = bodyD;\n          if (eToD.kind === 'field') { d.email.toMode = 'field'; d.email.toField = eToD.fieldId; }\n          else if (eToD.kind === 'path') { d.email.toMode = 'path'; d.email.toText = eToD.str; }\n          else if (eToD.kind !== 'complex') { d.email.toMode = 'valueText'; d.email.toText = eToD.str; }\n          else d.email.toMode = 'complex';\n          d.email.subject = subD.str;\n          d.email.body = bodyD.str;\n          d.email.senderName = t.email.senderName || '';\n          d.email.senderAddress = t.email.senderAddress || '';\n        }\n        if (t.http) {\n          var urlD = opEditable(t.http.url);\n          d.wraps.httpUrl = urlD;\n          d.http.method = (t.http.method || 'POST').toUpperCase();\n          d.http.url = urlD.kind === 'complex' ? '' : urlD.str;\n          if (urlD.kind === 'complex') d.http.urlComplex = true;\n          d.http.headers = Object.keys(t.http.headers || {}).map(function (k) { return { k: k, v: typeof t.http.headers[k] === 'string' ? t.http.headers[k] : JSON.stringify(t.http.headers[k]) }; });\n          d.http.query = Object.keys(t.http.query || {}).map(function (k) { return { k: k, v: typeof t.http.query[k] === 'string' ? t.http.query[k] : JSON.stringify(t.http.query[k]) }; });\n          d.http.bodyText = t.http.body != null ? pretty(t.http.body) : '';\n        }\n        if (t.preset) {\n          d.preset.presetId = String(t.preset.presetId || '');\n          d.preset.stateText = t.preset.state && Object.keys(t.preset.state).length ? pretty(t.preset.state) : '';\n        }\n        return d;\n      }\n\n      function draftToWork(modal) {\n        var d = modal.draft, kind = modal.kind;\n        var errors = [];\n        var work = deepClone(modal.base) || {};\n        ['id', 'organizationId', 'automaticExecutionAt', 'isAutomaticallyExecutingNow', 'deletedAt', '__flvKind'].forEach(function (k) { delete work[k]; });\n\n        if (!d.name.trim()) errors.push('Ange ett namn (steg 1).');\n        work.name = d.name.trim();\n        work.isEnabled = !!d.isEnabled;\n        work.isAutomatic = !!d.isAutomatic;\n\n        /* conditions: preserve unknown keys, replace tag keys */\n        var cond = deepClone((modal.base && modal.base.conditions) || {});\n        delete cond.anyTagId; delete cond.allTagIds; delete cond.notAnyTagId;\n        if (d.tags.any.length) cond.anyTagId = d.tags.any;\n        if (d.tags.all.length) cond.allTagIds = d.tags.all;\n        if (d.tags.not.length) cond.notAnyTagId = d.tags.not;\n        if (Object.keys(cond).length) work.conditions = cond;\n        else if (modal.base && modal.base.conditions) work.conditions = {};\n        else delete work.conditions;\n\n        /* after: preserve unknown keys (e.g. date), replace known */\n        var after = deepClone((modal.base && modal.base.after) || {});\n        ['minutes', 'hours', 'days', 'weeks', 'localClockHour', 'tagIdsAdded', 'tagIdsRemoved'].forEach(function (k) { delete after[k]; });\n        if (d.time.unit !== 'direct') {\n          var amount = parseInt(d.time.amount, 10);\n          if (!amount || amount < 1) errors.push('Ange ett giltigt antal för fördröjningen (steg 3).');\n          else after[d.time.unit] = amount;\n        }\n        if (d.time.clockHour !== '' && d.time.clockHour != null) {\n          var hour = parseInt(d.time.clockHour, 10);\n          if (isNaN(hour) || hour < 0 || hour > 24) errors.push('Klockslag måste vara 0-24 (steg 3).');\n          else after.localClockHour = hour;\n        }\n        if (kind === 'order' && d.time.trigger !== 'none') {\n          if (!d.time.trigTags.length) errors.push('Välj minst en tagg för taggutlösaren (steg 3).');\n          after[d.time.trigger === 'added' ? 'tagIdsAdded' : 'tagIdsRemoved'] = d.time.trigTags;\n        }\n        if (Object.keys(after).length) work.after = after;\n        else if (modal.base && modal.base.after) work.after = null;\n        else delete work.after;\n\n        /* toDo: exactly one type; preserve extra keys when type unchanged */\n        var baseToDo = (modal.base && modal.base.toDo) || {};\n        var sameType = toDoType({ toDo: baseToDo }) === d.todoType;\n        var inner = sameType ? deepClone(baseToDo[d.todoType]) || {} : {};\n\n        if (d.todoType === 'sms') {\n          if (d.sms.toMode === 'field') {\n            if (!d.sms.toField) errors.push('Välj vilket fält som innehåller mottagarens nummer (steg 4).');\n            inner.to = { _field: d.sms.toField };\n          } else if (d.sms.toMode === 'phonePath') inner.to = { _path: '$.phoneNumber' };\n          else if (d.sms.toMode === 'path') {\n            if (!d.sms.toText.trim()) errors.push('Ange JSONPath för mottagaren (steg 4).');\n            inner.to = { _path: d.sms.toText.trim() };\n          } else if (d.sms.toMode === 'valueText') {\n            if (!d.sms.toText.trim()) errors.push('Ange mottagare (steg 4).');\n            inner.to = d.sms.toText.trim();\n          } /* complex: keep inner.to from base */\n          if (d.sms.toMode !== 'complex' || !sameType) { if (d.sms.toMode === 'complex') errors.push('Mottagaren är ett komplext uttryck — redigera via JSON.'); }\n          if (!d.sms.message.trim() && !(d.wraps.smsMessage && d.wraps.smsMessage.kind === 'complex')) errors.push('Skriv ett SMS-meddelande (steg 4).');\n          if (!(d.wraps.smsMessage && d.wraps.smsMessage.kind === 'complex')) {\n            inner.message = opWrite(d.wraps.smsMessage || { kind: 'plain' }, d.sms.message);\n          }\n          if (d.sms.senderId.trim()) inner.senderId = d.sms.senderId.trim();\n          else delete inner.senderId;\n        } else if (d.todoType === 'email') {\n          if (d.email.toMode === 'field') {\n            if (!d.email.toField) errors.push('Välj vilket fält som innehåller mottagarens epost (steg 4).');\n            inner.to = { _field: d.email.toField };\n          } else if (d.email.toMode === 'path') {\n            if (!d.email.toText.trim()) errors.push('Ange JSONPath för mottagaren (steg 4).');\n            inner.to = { _path: d.email.toText.trim() };\n          } else if (d.email.toMode === 'valueText') {\n            if (!d.email.toText.trim()) errors.push('Ange mottagarens epostadress (steg 4).');\n            inner.to = d.email.toText.trim();\n          } else if (d.email.toMode === 'complex') errors.push('Mottagaren är ett komplext uttryck — redigera via JSON.');\n          if (!(d.wraps.emailSubject && d.wraps.emailSubject.kind === 'complex')) {\n            if (!d.email.subject.trim()) errors.push('Ange ett ämne (steg 4).');\n            inner.subject = opWrite(d.wraps.emailSubject || { kind: 'plain' }, d.email.subject);\n          }\n          if (!(d.wraps.emailBody && d.wraps.emailBody.kind === 'complex')) {\n            if (!d.email.body.trim()) errors.push('Skriv ett meddelande (steg 4).');\n            inner.body = opWrite(d.wraps.emailBody || { kind: 'plain' }, d.email.body);\n          }\n          if (d.email.senderName.trim()) inner.senderName = d.email.senderName.trim(); else delete inner.senderName;\n          if (d.email.senderAddress.trim()) inner.senderAddress = d.email.senderAddress.trim(); else delete inner.senderAddress;\n        } else if (d.todoType === 'http') {\n          inner.method = d.http.method;\n          if (d.http.urlComplex && sameType) { /* keep inner.url from base */ }\n          else {\n            if (!d.http.url.trim()) errors.push('Ange en URL (steg 4).');\n            inner.url = opWrite(d.wraps.httpUrl && d.wraps.httpUrl.kind !== 'complex' ? d.wraps.httpUrl : { kind: 'plain' }, d.http.url.trim());\n          }\n          var headers = {};\n          d.http.headers.forEach(function (row) { if (row.k.trim()) headers[row.k.trim().toLowerCase()] = row.v; });\n          inner.headers = headers;\n          var query = {};\n          d.http.query.forEach(function (row) { if (row.k.trim()) query[row.k.trim()] = row.v; });\n          inner.query = query;\n          if (d.http.bodyText.trim()) {\n            try { inner.body = JSON.parse(d.http.bodyText); }\n            catch (e) { errors.push('HTTP-body är inte giltig JSON (steg 4).'); }\n          } else if (!sameType) delete inner.body;\n          else if (inner.body != null && !d.http.bodyText.trim()) delete inner.body;\n        } else if (d.todoType === 'preset') {\n          if (!d.preset.presetId) errors.push('Välj en arbetsmall (steg 4).');\n          inner.presetId = d.preset.presetId;\n          if (d.preset.stateText.trim()) {\n            try { inner.state = JSON.parse(d.preset.stateText); }\n            catch (e) { errors.push('Mall-state är inte giltig JSON (steg 4).'); }\n          }\n        } else if (d.todoType === 'hangUp' || d.todoType === 'endPredictiveMode') {\n          inner = {};\n        }\n\n        work.toDo = {};\n        work.toDo[d.todoType] = inner;\n        return { work: work, errors: errors };\n      }\n\n      function validateWorkJson(obj) {\n        var errors = [];\n        if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return ['JSON måste vara ett objekt.']; }\n        if (!obj.name || !String(obj.name).trim()) errors.push('\"name\" krävs.');\n        var t = obj.toDo || {};\n        var types = ['sms', 'email', 'http', 'preset', 'document', 'hangUp', 'endPredictiveMode'].filter(function (k) { return t[k] != null; });\n        if (types.length !== 1) errors.push('\"toDo\" måste innehålla exakt EN åtgärd (sms/email/http/preset/…), hittade ' + types.length + '.');\n        return errors;\n      }\n      function stripInternal(obj) {\n        var out = deepClone(obj);\n        ['id', 'organizationId', 'automaticExecutionAt', 'isAutomaticallyExecutingNow', 'deletedAt', '__flvKind'].forEach(function (k) { delete out[k]; });\n        return out;\n      }\n\n      /* ---------------- mutations ---------------- */\n      function workPath(kind) { return kind === 'order' ? '/orders/after-order-work-v1' : '/dial/call-actions-v1'; }\n      function seriePath(kind) { return kind === 'order' ? '/orders/after-order-work-series-v1' : '/dial/call-action-series-v1'; }\n\n      function refresh() {\n        return Promise.all([\n          tryApi('GET', workPath('order')),\n          tryApi('GET', workPath('call')),\n          tryApi('GET', seriePath('order')),\n          tryApi('GET', seriePath('call'))\n        ]).then(function (r) {\n          state.works.order = normalizeList(r[0]);\n          state.works.call = normalizeList(r[1]);\n          state.series.order = normalizeList(r[2]);\n          state.series.call = normalizeList(r[3]);\n          state.works.order.forEach(function (w) { w.__flvKind = 'order'; });\n          state.works.call.forEach(function (w) { w.__flvKind = 'call'; });\n          buildRows();\n          renderAll();\n        });\n      }\n\n      function saveWork(kind, id, body) {\n        if (id) return api('PUT', workPath(kind) + '/' + id, null, body).then(function () { return id; });\n        return api('POST', workPath(kind), null, body).then(function (result) {\n          return (result && (result.actionId || result.id)) || null;\n        });\n      }\n      function toggleEnabled(kind, action) {\n        var next = !action.isEnabled;\n        action.isEnabled = next; /* optimistic */\n        renderAll();\n        api('PUT', workPath(kind) + '/' + action.id, null, { isEnabled: next }).then(function () {\n          toast(next ? 'Arbetet aktiverades' : 'Arbetet inaktiverades');\n        }).catch(function () {\n          action.isEnabled = !next;\n          renderAll();\n          toast('Kunde inte ändra status');\n        });\n      }\n      function duplicateWork(kind, action) {\n        var copy = stripInternal(action);\n        copy.name = (action.name || 'Arbete') + ' (kopia)';\n        copy.isEnabled = false;\n        api('POST', workPath(kind), null, copy).then(function () {\n          toast('Kopia skapad (inaktiv)');\n          return refresh();\n        }).catch(function (err) {\n          toast('Kunde inte duplicera (' + (err && err.status) + ')');\n        });\n      }\n      function deleteWork(kind, id) {\n        api('DELETE', workPath(kind) + '/' + id).then(function () {\n          toast('Arbetet togs bort');\n          state.selected = null;\n          return refresh();\n        }).catch(function (err) {\n          toast(err && err.status === 403 ? 'Endast admin kan ta bort arbeten' : 'Kunde inte ta bort');\n        });\n      }\n      function deleteSerie(kind, id) {\n        api('DELETE', seriePath(kind) + '/' + id).then(function () {\n          toast('Serien togs bort (arbetena finns kvar)');\n          state.selected = null;\n          return refresh();\n        }).catch(function (err) {\n          toast(err && err.status === 403 ? 'Endast admin kan ta bort serier' : 'Kunde inte ta bort serien');\n        });\n      }\n\n      /* ---------------- curl import/export ---------------- */\n      function tokenizeCurl(text) {\n        var cleaned = text.replace(/\\\\\\r?\\n/g, ' ');\n        var tokens = [];\n        var current = '';\n        var quote = null;\n        for (var i = 0; i < cleaned.length; i++) {\n          var ch = cleaned[i];\n          if (quote) {\n            if (ch === '\\\\' && quote === '\"' && i + 1 < cleaned.length) { current += cleaned[++i]; continue; }\n            if (ch === quote) { quote = null; continue; }\n            current += ch;\n          } else if (ch === '\"' || ch === \"'\") {\n            quote = ch;\n          } else if (/\\s/.test(ch)) {\n            if (current) { tokens.push(current); current = ''; }\n          } else current += ch;\n        }\n        if (current) tokens.push(current);\n        return tokens;\n      }\n      function parseCurl(text) {\n        var tokens = tokenizeCurl(text);\n        if (!tokens.length) return { error: 'Tomt kommando.' };\n        if (tokens[0] === 'curl') tokens.shift();\n        var result = { method: null, url: null, headers: [], body: null, notes: [] };\n        var bodyParts = [];\n        for (var i = 0; i < tokens.length; i++) {\n          var tok = tokens[i];\n          if (tok === '-X' || tok === '--request') result.method = (tokens[++i] || '').toUpperCase();\n          else if (tok === '-H' || tok === '--header') {\n            var header = tokens[++i] || '';\n            var colon = header.indexOf(':');\n            if (colon > 0) {\n              var hk = header.slice(0, colon).trim().toLowerCase();\n              var hv = header.slice(colon + 1).trim();\n              if (hk === 'cookie') result.notes.push('Cookie-header hoppades över.');\n              else if (hk === 'authorization') result.notes.push('Authorization-header importerades — kontrollera att den hör hemma här.') || result.headers.push({ k: hk, v: hv });\n              else result.headers.push({ k: hk, v: hv });\n            }\n          }\n          else if (tok === '-d' || tok === '--data' || tok === '--data-raw' || tok === '--data-binary' || tok === '--data-urlencode') bodyParts.push(tokens[++i] || '');\n          else if (tok === '-u' || tok === '--user') {\n            var creds = tokens[++i] || '';\n            try { result.headers.push({ k: 'authorization', v: 'Basic ' + btoa(creds) }); } catch (e) {}\n          }\n          else if (tok === '--url') result.url = tokens[++i];\n          else if (tok === '-b' || tok === '--cookie') { ++i; result.notes.push('Cookies hoppades över.'); }\n          else if (tok === '-F' || tok === '--form') { ++i; result.notes.push('Formdata (-F) stöds ej — hoppades över.'); }\n          else if (tok === '--compressed' || tok === '-s' || tok === '-L' || tok === '-k' || tok === '-i' || tok === '-v') { /* ignore */ }\n          else if (tok === '-o' || tok === '--output' || tok === '-A' || tok === '--user-agent' || tok === '-e' || tok === '--referer') ++i;\n          else if (tok[0] !== '-' && !result.url) result.url = tok;\n        }\n        if (!result.url) return { error: 'Hittade ingen URL i kommandot.' };\n        if (bodyParts.length) result.body = bodyParts.join('&');\n        if (!result.method) result.method = result.body ? 'POST' : 'GET';\n        /* split query from url */\n        var qIndex = result.url.indexOf('?');\n        result.query = [];\n        if (qIndex !== -1) {\n          var qs = result.url.slice(qIndex + 1);\n          result.url = result.url.slice(0, qIndex);\n          qs.split('&').forEach(function (pair) {\n            var eq = pair.indexOf('=');\n            if (eq > 0) result.query.push({ k: decodeURIComponent(pair.slice(0, eq)), v: decodeURIComponent(pair.slice(eq + 1)) });\n          });\n        }\n        return result;\n      }\n      function applyCurlToDraft(modal, parsed) {\n        var d = modal.draft;\n        d.todoType = 'http';\n        d.http.method = parsed.method;\n        d.http.url = parsed.url;\n        d.http.urlComplex = false;\n        d.wraps.httpUrl = { kind: 'plain' };\n        d.http.headers = parsed.headers;\n        d.http.query = parsed.query || [];\n        if (parsed.body) {\n          try { d.http.bodyText = JSON.stringify(JSON.parse(parsed.body), null, 2); }\n          catch (e) { d.http.bodyText = parsed.body; }\n        } else d.http.bodyText = '';\n      }\n      function workToCurl(kind, work) {\n        var isNew = !work.id;\n        var url = 'https://app.salesys.se/api' + workPath(kind) + (isNew ? '' : '/' + work.id);\n        var body = pretty(stripInternal(work));\n        return 'curl -X ' + (isNew ? 'POST' : 'PUT') + \" '\" + url + \"' \\\\\\n\" +\n          '  -H \"Authorization: Bearer $TOKEN\" \\\\\\n' +\n          \"  -H 'Content-Type: application/json' \\\\\\n\" +\n          \"  -d '\" + body.replace(/'/g, \"'\\\\''\") + \"'\";\n      }\n\n      /* ---------------- work modal ---------------- */\n      function openWorkModal(mode, kind, base) {\n        state.modal = {\n          mode: mode, kind: kind || 'order',\n          base: base ? deepClone(base) : null,\n          draft: base ? draftFromWork(kind, base) : emptyDraft(kind || 'order'),\n          jsonText: mode === 'json' ? '' : null,\n          curlOpen: false, curlText: '', curlNotes: [],\n          error: '', saving: false\n        };\n        renderWorkModal();\n      }\n      function closeWorkModal() { state.modal = null; renderWorkModal(); }\n\n      function cardHtml(attr, value, picked, icon, label, desc) {\n        return '<div class=\"flv-opt-card' + (picked ? ' flv-picked' : '') + '\" data-' + attr + '=\"' + esc(value) + '\">' +\n          '<i class=\"fas ' + icon + '\"></i><div class=\"flv-opt-card-label\">' + esc(label) + '</div>' +\n          (desc ? '<div class=\"flv-opt-card-desc\">' + esc(desc) + '</div>' : '') + '</div>';\n      }\n      function tagPickHtml(kind, group, selected) {\n        var list = state.tags[kind] || [];\n        if (!list.length) return '<div class=\"flv-form-note\">Inga taggar finns.</div>';\n        return '<div class=\"flv-tagpick\">' + list.map(function (t) {\n          var picked = selected.indexOf(t.id) !== -1;\n          return '<span class=\"flv-picktag' + (picked ? ' flv-picked' : '') + '\" data-flv-tagtoggle=\"' + esc(group + ':' + t.id) + '\">' + esc(t.name) + '</span>';\n        }).join('') + '</div>';\n      }\n      function kvRowsHtml(rows, groupName, keyPlaceholder, valPlaceholder) {\n        var html = rows.map(function (row, i) {\n          return '<div class=\"flv-kv-row\">' +\n            '<input class=\"flv-underline-input\" placeholder=\"' + keyPlaceholder + '\" data-flv-kv=\"' + groupName + ':' + i + ':k\" value=\"' + esc(row.k) + '\">' +\n            '<input class=\"flv-underline-input\" placeholder=\"' + valPlaceholder + '\" data-flv-kv=\"' + groupName + ':' + i + ':v\" value=\"' + esc(row.v) + '\">' +\n            '<button class=\"flv-kv-del\" data-flv-kv-del=\"' + groupName + ':' + i + '\" title=\"Ta bort\"><i class=\"fas fa-times\"></i></button>' +\n            '</div>';\n        }).join('');\n        html += '<button class=\"flv-add-row-btn\" data-flv-kv-add=\"' + groupName + '\"><i class=\"fas fa-plus\"></i> Lägg till rad</button>';\n        return html;\n      }\n\n      var SIDE_INFO = {\n        sms: { icon: 'fa-mobile-alt', title: 'SMS', text: 'Skickar ett SMS när arbetet körs. Mottagaren kan hämtas ur ett fält på ordern, ur samtalet, eller anges fast. Avsändar-ID är valfritt (annars används organisationens).' },\n        email: { icon: 'fa-envelope', title: 'Epost', text: 'Skickar ett mejl. Meddelandet skrivs i Markdown. Avsändare är valfri — annars används SaleSys standardavsändare för organisationen.' },\n        http: { icon: 'fa-globe', title: 'HTTP-anrop', text: 'Anropar ett externt API med data från ordern eller samtalet. Använd operators som _path i bodyn för att skicka med värden, t.ex. { \"_path\": \"$\" } för hela objektet. I developer mode kan du klistra in en curl direkt.' },\n        preset: { icon: 'fa-cube', title: 'Arbetsmall', text: 'Kör en färdig arbetsmall (action preset) som er organisation eller SaleSys har satt upp. Mallens beteende styrs centralt.' },\n        document: { icon: 'fa-file-alt', title: 'Dokument', text: 'Dokumentbearbetning (t.ex. bryta ut sidor ur PDF). Konfigureras i sin helhet via JSON — använd JSON-fliken i developer mode.' },\n        hangUp: { icon: 'fa-phone-slash', title: 'Lägg på', text: 'Lägger på det pågående samtalet när villkoren matchar.' },\n        endPredictiveMode: { icon: 'fa-stop-circle', title: 'Avsluta prediktivt', text: 'Avslutar prediktivt uppringningsläge för användaren.' }\n      };\n\n      function renderWorkModal() {\n        var wrap = el('flv-work-modal');\n        if (!wrap) return;\n        var modal = state.modal;\n        if (!modal) { wrap.className = 'flv-modal-backdrop'; wrap.innerHTML = ''; return; }\n        wrap.className = 'flv-modal-backdrop flv-open';\n\n        if (modal.mode === 'json') { renderJsonModal(wrap, modal); return; }\n\n        var d = modal.draft, kind = modal.kind;\n        var isEdit = modal.mode === 'edit';\n        var html = '<div class=\"flv-modal\">';\n        html += '<div class=\"flv-modal-title\">' + (isEdit ? 'Redigera arbete' : 'Nytt arbete') + '</div>';\n        html += '<div class=\"flv-modal-body\"><div class=\"flv-modal-main\">';\n\n        /* Step 1: grund */\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">1</div><div class=\"flv-step-title\">Grund</div></div><div class=\"flv-step-body\">';\n        if (!isEdit) {\n          html += '<div class=\"flv-card-grid\" style=\"margin-bottom:12px;\">' +\n            cardHtml('flv-kindpick', 'order', kind === 'order', 'fa-shopping-cart', 'Orderarbete', 'Körs på ordrar') +\n            cardHtml('flv-kindpick', 'call', kind === 'call', 'fa-phone-alt', 'Samtalsarbete', 'Körs på samtal') +\n            '</div>';\n        }\n        html += '<div class=\"flv-input-label\">Namn</div>' +\n          '<input class=\"flv-underline-input\" data-flv-bind=\"name\" placeholder=\"T.ex. Skicka till CRM\" value=\"' + esc(d.name) + '\">';\n        html += '<div style=\"display:flex; gap:22px; margin-top:14px;\">' +\n          '<div style=\"display:flex; align-items:center; gap:8px; cursor:pointer;\" data-flv-drafttoggle=\"isEnabled\"><span class=\"flv-switch' + (d.isEnabled ? ' flv-on' : '') + '\"></span><span style=\"font-size:13px; font-weight:700;\">Aktivt</span></div>' +\n          '<div style=\"display:flex; align-items:center; gap:8px; cursor:pointer;\" data-flv-drafttoggle=\"isAutomatic\"><span class=\"flv-switch' + (d.isAutomatic ? ' flv-on' : '') + '\"></span><span style=\"font-size:13px; font-weight:700;\">Automatiskt</span></div>' +\n          '</div>';\n        if (!d.isAutomatic) html += '<div class=\"flv-form-note\"><i class=\"fas fa-info-circle\"></i>Manuella arbeten körs via plussknappen i ' + (kind === 'order' ? 'ordervyn' : 'samtalsvyn') + '.</div>';\n        html += '</div></div>';\n\n        /* Step 2: villkor */\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">2</div><div class=\"flv-step-title\">Villkor</div></div><div class=\"flv-step-body\">';\n        html += '<div class=\"flv-input-label\">Har någon av taggarna</div>' + tagPickHtml(kind, 'any', d.tags.any);\n        html += '<div class=\"flv-input-label\">Har alla taggarna</div>' + tagPickHtml(kind, 'all', d.tags.all);\n        html += '<div class=\"flv-input-label\">Har inte någon av taggarna</div>' + tagPickHtml(kind, 'not', d.tags.not);\n        var extraCond = modal.base && modal.base.conditions ? Object.keys(modal.base.conditions).filter(function (k) {\n          return ['anyTagId', 'allTagIds', 'notAnyTagId'].indexOf(k) === -1 && modal.base.conditions[k] != null;\n        }) : [];\n        if (extraCond.length) html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Övriga villkor bevaras oförändrade: ' + esc(extraCond.join(', ')) + '. Redigera dem via JSON-fliken.</div>';\n        html += '</div></div>';\n\n        /* Step 3: tid */\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">3</div><div class=\"flv-step-title\">Tid</div></div><div class=\"flv-step-body\">';\n        html += '<div class=\"flv-card-grid\">' +\n          cardHtml('flv-timeunit', 'direct', d.time.unit === 'direct', 'fa-bolt', 'Direkt', '') +\n          cardHtml('flv-timeunit', 'minutes', d.time.unit === 'minutes', 'fa-stopwatch', 'Minuter', '') +\n          cardHtml('flv-timeunit', 'hours', d.time.unit === 'hours', 'fa-hourglass-half', 'Timmar', '') +\n          cardHtml('flv-timeunit', 'days', d.time.unit === 'days', 'fa-calendar-day', 'Dagar', '') +\n          cardHtml('flv-timeunit', 'weeks', d.time.unit === 'weeks', 'fa-calendar-week', 'Veckor', '') +\n          '</div>';\n        if (d.time.unit !== 'direct') {\n          html += '<div class=\"flv-inline-row\" style=\"margin-top:10px;\"><div style=\"flex:0;\">' +\n            '<div class=\"flv-input-label\">Antal</div>' +\n            '<input class=\"flv-underline-input flv-num-input\" type=\"number\" min=\"1\" data-flv-bind=\"time.amount\" value=\"' + esc(d.time.amount) + '\"></div>' +\n            '<div style=\"flex:0;\"><div class=\"flv-input-label\">Tidigast kl (0-24, valfritt)</div>' +\n            '<input class=\"flv-underline-input flv-num-input\" type=\"number\" min=\"0\" max=\"24\" data-flv-bind=\"time.clockHour\" value=\"' + esc(d.time.clockHour) + '\"></div></div>';\n        }\n        if (kind === 'order') {\n          html += '<div class=\"flv-input-label\" style=\"margin-top:14px;\">Räkna från</div>';\n          html += '<div class=\"flv-card-grid\">' +\n            cardHtml('flv-timetrigger', 'none', d.time.trigger === 'none', 'fa-shopping-cart', 'Orderns datum', '') +\n            cardHtml('flv-timetrigger', 'added', d.time.trigger === 'added', 'fa-tag', 'Tagg läggs till', '') +\n            cardHtml('flv-timetrigger', 'removed', d.time.trigger === 'removed', 'fa-eraser', 'Tagg tas bort', '') +\n            '</div>';\n          if (d.time.trigger !== 'none') {\n            html += '<div class=\"flv-input-label\">Vilka taggar</div>' + '<div class=\"flv-tagpick\">' + (state.tags.order || []).map(function (t) {\n              var picked = d.time.trigTags.indexOf(t.id) !== -1;\n              return '<span class=\"flv-picktag' + (picked ? ' flv-picked' : '') + '\" data-flv-trigtag=\"' + esc(t.id) + '\">' + esc(t.name) + '</span>';\n            }).join('') + '</div>';\n          }\n        }\n        html += '</div></div>';\n\n        /* Step 4: åtgärd */\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">4</div><div class=\"flv-step-title\">Åtgärd</div></div><div class=\"flv-step-body\">';\n        var types = kind === 'order' ? ['http', 'sms', 'email', 'preset'] : ['http', 'sms', 'email', 'preset', 'hangUp', 'endPredictiveMode'];\n        if (isEdit && types.indexOf(d.todoType) === -1) types.push(d.todoType);\n        html += '<div class=\"flv-card-grid\">' + types.map(function (t) {\n          return cardHtml('flv-todotype', t, d.todoType === t, TODO_ICONS[t] || 'fa-question', TODO_LABELS[t] || t, '');\n        }).join('') + '</div>';\n        html += '<div style=\"margin-top:12px;\">' + todoFormHtml(modal) + '</div>';\n        html += '</div></div>';\n\n        /* Step 5: granska */\n        var built = draftToWork(modal);\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">5</div><div class=\"flv-step-title\">Granska</div></div><div class=\"flv-step-body\" id=\"flv-preview-wrap\">';\n        html += previewHtml(built);\n        html += '</div></div>';\n\n        html += '</div>'; /* main */\n\n        /* side info panel */\n        var info = SIDE_INFO[d.todoType] || SIDE_INFO.http;\n        html += '<div class=\"flv-modal-side\"><i class=\"fas ' + info.icon + '\"></i>' +\n          '<div class=\"flv-modal-side-title\">' + esc(info.title) + '</div>' + esc(info.text) +\n          (d.todoType === 'sms' || d.todoType === 'email' ?\n            '<div style=\"margin-top:12px; font-size:11.5px;\">Tips: i texter kan du använda variabler som skrivs inom dubbla hakparenteser, t.ex. fältnamn.</div>' : '') +\n          '</div>';\n\n        html += '</div>'; /* body */\n        html += '<div class=\"flv-modal-footer\">' +\n          '<button class=\"flv-btn flv-btn-primary\" id=\"flv-modal-save\"' + (modal.saving ? ' disabled' : '') + '>' + (isEdit ? 'Spara' : 'Skapa') + '</button>' +\n          '<button class=\"flv-btn\" id=\"flv-modal-cancel\">Avbryt</button>' +\n          (modal.error ? '<div class=\"flv-modal-error\">' + esc(modal.error) + '</div>' : '') +\n          '</div>';\n        html += '</div>';\n        wrap.innerHTML = html;\n      }\n\n      function previewHtml(built) {\n        var html = '';\n        if (built.errors.length) {\n          html += built.errors.map(function (e) { return '<div class=\"flv-validation\"><i class=\"fas fa-exclamation-circle\"></i> ' + esc(e) + '</div>'; }).join('');\n        } else {\n          html += '<div class=\"flv-form-note\" style=\"color: var(--flv-success);\"><i class=\"fas fa-check-circle\"></i>Allt ser bra ut.</div>';\n        }\n        html += '<pre class=\"flv-pre\" style=\"max-height:200px;\" id=\"flv-preview-json\">' + esc(pretty(built.work)) + '</pre>';\n        return html;\n      }\n      function updatePreview() {\n        if (!state.modal || state.modal.mode === 'json') return;\n        var wrap = el('flv-preview-wrap');\n        if (wrap) wrap.innerHTML = previewHtml(draftToWork(state.modal));\n      }\n\n      function fieldListItemsHtml(d, sub) {\n        var q = d.fieldSearch.trim().toLowerCase();\n        var fields = state.orderFields.filter(function (f) { return !q || String(f.label || '').toLowerCase().indexOf(q) !== -1; }).slice(0, 40);\n        return fields.length ? fields.map(function (f) {\n          return '<div class=\"flv-worklist-item\" data-flv-fieldpick=\"' + esc(f.id) + '\"><i class=\"fas ' + (sub.toField === f.id ? 'fa-check-circle' : 'fa-columns') + '\"></i>' + esc(f.label || f.id) + '</div>';\n        }).join('') : '<div class=\"flv-worklist-item\" style=\"color:var(--flv-muted-fg); cursor:default;\">Inga fält matchar.</div>';\n      }\n\n      function seriePickItemsHtml(sd, kind) {\n        var q = sd.pickerSearch.trim().toLowerCase();\n        var used = {};\n        sd.steps.forEach(function (s) { used[s.actionId] = true; });\n        var candidates = (state.works[kind] || []).filter(function (w) {\n          return !used[w.id] && (!q || String(w.name || '').toLowerCase().indexOf(q) !== -1);\n        });\n        return candidates.length ? candidates.map(function (w) {\n          return '<div class=\"flv-worklist-item\" data-flv-stepadd=\"' + esc(w.id) + '\"><i class=\"fas ' + TODO_ICONS[toDoType(w)] + '\"></i>' + esc(w.name || '?') + '</div>';\n        }).join('') : '<div class=\"flv-worklist-item\" style=\"color:var(--flv-muted-fg); cursor:default;\">Inga fler arbeten att lägga till.</div>';\n      }\n\n      function todoFormHtml(modal) {\n        var d = modal.draft, kind = modal.kind, type = d.todoType;\n        var html = '';\n        if (type === 'sms' || type === 'email') {\n          var sub = type === 'sms' ? d.sms : d.email;\n          var complexTo = sub.toMode === 'complex';\n          html += '<div class=\"flv-input-label\">Mottagare</div>';\n          if (complexTo) {\n            html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Mottagaren är ett komplext uttryck och bevaras. Redigera via JSON-fliken (dev mode).</div>';\n          } else {\n            var modes = [];\n            if (kind === 'order') modes.push(['field', 'fa-columns', 'Fält på ordern']);\n            if (type === 'sms' && kind === 'call') modes.push(['phonePath', 'fa-phone-alt', 'Samtalets nummer']);\n            modes.push(['valueText', 'fa-keyboard', 'Fast värde']);\n            modes.push(['path', 'fa-code', 'JSONPath']);\n            html += '<div class=\"flv-card-grid\">' + modes.map(function (m) {\n              return cardHtml('flv-tomode', m[0], sub.toMode === m[0], m[1], m[2], '');\n            }).join('') + '</div>';\n            if (sub.toMode === 'field') {\n              var selectedLabel = sub.toField ? (fieldLabel(sub.toField) || sub.toField) : '';\n              html += '<div class=\"flv-input-label\">Välj fält' + (selectedLabel ? ' — valt: ' + esc(selectedLabel) : '') + '</div>' +\n                '<input class=\"flv-underline-input\" placeholder=\"Sök fält...\" data-flv-bind=\"fieldSearch\" value=\"' + esc(d.fieldSearch) + '\">';\n              html += '<div class=\"flv-worklist\" id=\"flv-field-list\">' + fieldListItemsHtml(d, sub) + '</div>';\n            } else if (sub.toMode === 'valueText') {\n              html += '<input class=\"flv-underline-input\" placeholder=\"' + (type === 'sms' ? 'T.ex. +46701234567' : 'T.ex. ekonomi@example.org') + '\" data-flv-bind=\"' + type + '.toText\" value=\"' + esc(sub.toText) + '\">';\n            } else if (sub.toMode === 'path') {\n              html += '<input class=\"flv-underline-input\" placeholder=\"T.ex. $.phoneNumber\" data-flv-bind=\"' + type + '.toText\" value=\"' + esc(sub.toText) + '\">';\n            }\n          }\n          if (type === 'sms') {\n            var msgComplex = d.wraps.smsMessage && d.wraps.smsMessage.kind === 'complex';\n            html += '<div class=\"flv-input-label\">Meddelande</div>';\n            if (msgComplex) html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Meddelandet är ett komplext uttryck och bevaras. Redigera via JSON-fliken.</div>';\n            else html += '<textarea class=\"flv-underline-input\" rows=\"3\" data-flv-bind=\"sms.message\" placeholder=\"Hej!\">' + esc(d.sms.message) + '</textarea>';\n            html += '<div class=\"flv-input-label\">Avsändar-ID (valfritt)</div>' +\n              '<input class=\"flv-underline-input\" data-flv-bind=\"sms.senderId\" placeholder=\"T.ex. SaleSys\" value=\"' + esc(d.sms.senderId) + '\">';\n          } else {\n            var subjComplex = d.wraps.emailSubject && d.wraps.emailSubject.kind === 'complex';\n            var bodyComplex = d.wraps.emailBody && d.wraps.emailBody.kind === 'complex';\n            html += '<div class=\"flv-input-label\">Ämne</div>';\n            if (subjComplex) html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Ämnet är ett komplext uttryck och bevaras.</div>';\n            else html += '<input class=\"flv-underline-input\" data-flv-bind=\"email.subject\" value=\"' + esc(d.email.subject) + '\">';\n            html += '<div class=\"flv-input-label\">Meddelande (Markdown)</div>';\n            if (bodyComplex) html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Meddelandet är ett komplext uttryck och bevaras.</div>';\n            else html += '<textarea class=\"flv-underline-input\" rows=\"4\" data-flv-bind=\"email.body\">' + esc(d.email.body) + '</textarea>';\n            html += '<div class=\"flv-inline-row\"><div><div class=\"flv-input-label\">Avsändarnamn (valfritt)</div>' +\n              '<input class=\"flv-underline-input\" data-flv-bind=\"email.senderName\" value=\"' + esc(d.email.senderName) + '\"></div>' +\n              '<div><div class=\"flv-input-label\">Avsändaradress (valfritt)</div>' +\n              '<input class=\"flv-underline-input\" data-flv-bind=\"email.senderAddress\" value=\"' + esc(d.email.senderAddress) + '\"></div></div>';\n          }\n        } else if (type === 'http') {\n          if (state.devMode) {\n            html += '<button class=\"flv-mini-btn\" id=\"flv-curl-toggle\" style=\"margin-bottom:10px;\"><i class=\"fas fa-terminal\"></i> Klistra in curl</button>';\n            if (modal.curlOpen) {\n              html += '<textarea class=\"flv-textarea-code\" id=\"flv-curl-input\" placeholder=\"curl https://example.org/api -X POST -H ... -d ...\" style=\"min-height:90px;\">' + esc(modal.curlText) + '</textarea>' +\n                '<div style=\"margin-top:6px; display:flex; gap:8px; align-items:center;\">' +\n                '<button class=\"flv-mini-btn\" id=\"flv-curl-apply\"><i class=\"fas fa-magic\"></i> Tolka och fyll i</button>' +\n                (modal.curlNotes.length ? '<span class=\"flv-form-note\" style=\"margin:0;\">' + esc(modal.curlNotes.join(' ')) + '</span>' : '') +\n                '</div>';\n            }\n          }\n          html += '<div class=\"flv-input-label\">Metod</div>';\n          html += '<div class=\"flv-card-grid\">' + ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(function (m) {\n            return '<span class=\"flv-picktag' + (d.http.method === m ? ' flv-picked' : '') + '\" data-flv-httpmethod=\"' + m + '\">' + m + '</span>';\n          }).join('') + '</div>';\n          html += '<div class=\"flv-input-label\">URL</div>';\n          if (d.http.urlComplex) html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>URL:en är ett komplext uttryck och bevaras. Redigera via JSON-fliken.</div>';\n          else html += '<input class=\"flv-underline-input\" data-flv-bind=\"http.url\" placeholder=\"https://example.org/webhook\" value=\"' + esc(d.http.url) + '\">';\n          html += '<div class=\"flv-input-label\">Headers</div>' + kvRowsHtml(d.http.headers, 'headers', 'namn', 'värde');\n          html += '<div class=\"flv-input-label\">Query-parametrar</div>' + kvRowsHtml(d.http.query, 'query', 'namn', 'värde');\n          if (d.http.method === 'POST' || d.http.method === 'PUT' || d.http.method === 'PATCH') {\n            html += '<div class=\"flv-input-label\">Body (JSON — operators som _path fungerar)</div>' +\n              '<textarea class=\"flv-textarea-code\" data-flv-bind=\"http.bodyText\" placeholder=\\'T.ex. rotobjektet: [ \"_path\": \"$\" ] fast med klammerparenteser\\'>' + esc(d.http.bodyText) + '</textarea>';\n          }\n          var extraHttp = [];\n          var baseHttp = modal.base && modal.base.toDo && modal.base.toDo.http;\n          if (baseHttp && toDoType({ toDo: modal.base.toDo }) === 'http') {\n            ['set', 'setPath', 'pre', 'attachFiles', 'attachFilesPath', 'errorMessages', 'link', 'linkFileName', 'executeAgainAfterPath'].forEach(function (k) {\n              if (baseHttp[k] != null) extraHttp.push(k);\n            });\n          }\n          if (extraHttp.length) html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Avancerade inställningar bevaras oförändrade: ' + esc(extraHttp.join(', ')) + '.</div>';\n        } else if (type === 'preset') {\n          var presets = state.presets[kind] || [];\n          if (!presets.length) html += '<div class=\"flv-form-note\">Inga arbetsmallar finns för ' + (kind === 'order' ? 'ordrar' : 'samtal') + '.</div>';\n          else {\n            html += '<div class=\"flv-card-grid\">' + presets.filter(function (p) { return !p.isRemoved; }).map(function (p) {\n              return cardHtml('flv-presetpick', p.id, d.preset.presetId === p.id, 'fa-cube', p.name || 'Mall', '');\n            }).join('') + '</div>';\n          }\n          html += '<div class=\"flv-input-label\">State till mallen (JSON, valfritt)</div>' +\n            '<textarea class=\"flv-textarea-code\" data-flv-bind=\"preset.stateText\" style=\"min-height:70px;\">' + esc(d.preset.stateText) + '</textarea>';\n        } else if (type === 'document') {\n          html += '<div class=\"flv-form-note\"><i class=\"fas fa-lock\"></i>Dokumentåtgärder konfigureras via JSON (dev mode) — befintlig konfiguration bevaras.</div>';\n        } else {\n          html += '<div class=\"flv-form-note\">Ingen mer konfiguration behövs.</div>';\n        }\n        return html;\n      }\n\n      function renderJsonModal(wrap, modal) {\n        var html = '<div class=\"flv-modal flv-modal-sm\">';\n        html += '<div class=\"flv-modal-title\">Nytt arbete från JSON</div>';\n        html += '<div class=\"flv-modal-body\"><div class=\"flv-modal-main\">';\n        html += '<div class=\"flv-card-grid\" style=\"margin-bottom:12px;\">' +\n          cardHtml('flv-kindpick', 'order', modal.kind === 'order', 'fa-shopping-cart', 'Orderarbete', '') +\n          cardHtml('flv-kindpick', 'call', modal.kind === 'call', 'fa-phone-alt', 'Samtalsarbete', '') +\n          '</div>';\n        html += '<textarea class=\"flv-textarea-code\" id=\"flv-json-create\" style=\"min-height:260px;\" placeholder=\"Klistra in hela arbetets JSON här...\">' + esc(modal.jsonText || '') + '</textarea>';\n        if (modal.error) html += '<div class=\"flv-validation\" style=\"margin-top:8px;\">' + esc(modal.error) + '</div>';\n        html += '</div></div>';\n        html += '<div class=\"flv-modal-footer\">' +\n          '<button class=\"flv-btn flv-btn-primary\" id=\"flv-modal-save\"' + (modal.saving ? ' disabled' : '') + '>Skapa</button>' +\n          '<button class=\"flv-btn\" id=\"flv-modal-cancel\">Avbryt</button>' +\n          '</div></div>';\n        wrap.innerHTML = html;\n      }\n\n      function submitWorkModal() {\n        var modal = state.modal;\n        if (!modal || modal.saving) return;\n        if (modal.mode === 'json') {\n          var textarea = el('flv-json-create');\n          var text = textarea ? textarea.value : '';\n          var obj;\n          try { obj = JSON.parse(text); } catch (e) { modal.error = 'Ogiltig JSON: ' + e.message; renderWorkModal(); return; }\n          var jsonErrors = validateWorkJson(obj);\n          if (jsonErrors.length) { modal.error = jsonErrors.join(' '); modal.jsonText = text; renderWorkModal(); return; }\n          modal.saving = true; modal.jsonText = text; renderWorkModal();\n          saveWork(modal.kind, null, stripInternal(obj)).then(function () {\n            toast('Arbetet skapades');\n            closeWorkModal();\n            return refresh();\n          }).catch(function (err) {\n            modal.saving = false;\n            modal.error = 'Kunde inte skapa (' + (err && err.status) + ').';\n            renderWorkModal();\n          });\n          return;\n        }\n        var built = draftToWork(modal);\n        if (built.errors.length) {\n          modal.error = 'Rätta felen under Granska först.';\n          renderWorkModal();\n          return;\n        }\n        modal.saving = true; modal.error = '';\n        renderWorkModal();\n        var id = modal.base && modal.base.id;\n        saveWork(modal.kind, id, built.work).then(function (savedId) {\n          toast(id ? 'Arbetet sparades' : 'Arbetet skapades');\n          closeWorkModal();\n          return refresh().then(function () {\n            if (savedId) selectAction(modal.kind, savedId);\n          });\n        }).catch(function (err) {\n          modal.saving = false;\n          modal.error = err && err.status === 403 ? 'Endast admin kan ändra arbeten.' : 'Kunde inte spara (' + (err && err.status) + ').';\n          renderWorkModal();\n        });\n      }\n\n      /* ---------------- serie modal ---------------- */\n      function openSerieModal(kind, serie, seedActionId) {\n        var draft = {\n          name: serie ? serie.name || '' : '',\n          description: serie ? serie.description || '' : '',\n          maxExecutions: serie && serie.maxExecutions != null ? String(serie.maxExecutions) : '',\n          steps: serie ? (serie.actionIds || []).map(function (id) {\n            var block = true;\n            if (kind === 'order') {\n              block = serie.defaultOptions && serie.defaultOptions.blockOnUnmatchedConditions === false ? false : true;\n              (serie.options || []).forEach(function (o) { if (o.actionId === id && o.blockOnUnmatchedConditions != null) block = o.blockOnUnmatchedConditions; });\n            }\n            return { actionId: id, block: block };\n          }) : [],\n          pickerOpen: false, pickerSearch: ''\n        };\n        if (seedActionId && !draft.steps.some(function (s) { return s.actionId === seedActionId; })) {\n          draft.steps.push({ actionId: seedActionId, block: true });\n          draft.pickerOpen = true;\n        }\n        state.serieModal = { mode: serie ? 'edit' : 'create', kind: kind, serieId: serie ? serie.id : null, draft: draft, error: '', saving: false };\n        renderSerieModal();\n      }\n      function closeSerieModal() { state.serieModal = null; renderSerieModal(); }\n\n      function renderSerieModal() {\n        var wrap = el('flv-serie-modal');\n        if (!wrap) return;\n        var modal = state.serieModal;\n        if (!modal) { wrap.className = 'flv-modal-backdrop'; wrap.innerHTML = ''; return; }\n        wrap.className = 'flv-modal-backdrop flv-open';\n        var d = modal.draft, kind = modal.kind;\n        var html = '<div class=\"flv-modal flv-modal-sm\">';\n        html += '<div class=\"flv-modal-title\">' + (modal.mode === 'edit' ? 'Redigera serie' : 'Ny serie') + '</div>';\n        html += '<div class=\"flv-modal-body\"><div class=\"flv-modal-main\">';\n\n        if (modal.mode === 'create' && !d.steps.length) {\n          html += '<div class=\"flv-card-grid\" style=\"margin-bottom:12px;\">' +\n            cardHtml('flv-seriekind', 'order', kind === 'order', 'fa-shopping-cart', 'Orderserie', '') +\n            cardHtml('flv-seriekind', 'call', kind === 'call', 'fa-phone-alt', 'Samtalsserie', '') +\n            '</div>';\n        }\n        html += '<div class=\"flv-input-label\">Namn</div>' +\n          '<input class=\"flv-underline-input\" data-flv-seriebind=\"name\" maxlength=\"50\" placeholder=\"T.ex. Fakturaflöde\" value=\"' + esc(d.name) + '\">';\n        if (kind === 'order') {\n          html += '<div class=\"flv-input-label\">Beskrivning (valfri)</div>' +\n            '<input class=\"flv-underline-input\" data-flv-seriebind=\"description\" value=\"' + esc(d.description) + '\">';\n        }\n\n        html += '<div class=\"flv-input-label\">Steg (körs i ordning)</div>';\n        if (!d.steps.length) html += '<div class=\"flv-form-note\">Inga steg ännu — lägg till arbeten nedan.</div>';\n        d.steps.forEach(function (step, i) {\n          var action = findWork(kind, step.actionId);\n          html += '<div class=\"flv-serie-step-row\">' +\n            '<span style=\"font-weight:800; color:var(--flv-muted-fg); font-size:12px;\">' + (i + 1) + '</span>' +\n            '<i class=\"fas ' + TODO_ICONS[action ? toDoType(action) : 'unknown'] + '\" style=\"color:var(--flv-primary); width:15px; text-align:center; font-size:12px;\"></i>' +\n            '<span class=\"flv-serie-step-name\">' + esc(action ? action.name : 'Saknat arbete') + '</span>' +\n            (kind === 'order' && i > 0 ?\n              '<span style=\"display:flex; align-items:center; gap:5px; cursor:pointer;\" data-flv-stepblock=\"' + i + '\" title=\"Blockerande: serien stannar här om villkoren inte matchar\">' +\n              '<span class=\"flv-switch' + (step.block ? ' flv-on' : '') + '\" style=\"width:28px; height:16px;\"></span>' +\n              '<span style=\"font-size:10.5px; font-weight:700; color:var(--flv-muted-fg);\">Blockerande</span></span>' : '') +\n            '<button class=\"flv-mini-btn\" data-flv-stepmove=\"' + i + ':-1\"' + (i === 0 ? ' disabled style=\"opacity:0.3;\"' : '') + ' title=\"Flytta upp\"><i class=\"fas fa-arrow-up\"></i></button>' +\n            '<button class=\"flv-mini-btn\" data-flv-stepmove=\"' + i + ':1\"' + (i === d.steps.length - 1 ? ' disabled style=\"opacity:0.3;\"' : '') + ' title=\"Flytta ner\"><i class=\"fas fa-arrow-down\"></i></button>' +\n            '<button class=\"flv-mini-btn flv-danger-btn\" data-flv-stepdel=\"' + i + '\" title=\"Ta bort steget\"><i class=\"fas fa-times\"></i></button>' +\n            '</div>';\n        });\n\n        html += '<button class=\"flv-add-row-btn\" id=\"flv-step-picker-toggle\"><i class=\"fas fa-plus\"></i> Lägg till steg</button>';\n        if (d.pickerOpen) {\n          html += '<input class=\"flv-underline-input\" style=\"margin-top:8px;\" placeholder=\"Sök arbete...\" data-flv-seriebind=\"pickerSearch\" value=\"' + esc(d.pickerSearch) + '\">';\n          html += '<div class=\"flv-worklist\" id=\"flv-serie-picklist\">' + seriePickItemsHtml(d, kind) + '</div>';\n        }\n\n        if (kind === 'order') {\n          html += '<div class=\"flv-input-label\">Max körningar totalt (valfritt)</div>' +\n            '<input class=\"flv-underline-input flv-num-input\" type=\"number\" min=\"1\" data-flv-seriebind=\"maxExecutions\" value=\"' + esc(d.maxExecutions) + '\">';\n        }\n        html += '</div></div>';\n        html += '<div class=\"flv-modal-footer\">' +\n          '<button class=\"flv-btn flv-btn-primary\" id=\"flv-serie-save\"' + (modal.saving ? ' disabled' : '') + '>' + (modal.mode === 'edit' ? 'Spara' : 'Skapa') + '</button>' +\n          '<button class=\"flv-btn\" id=\"flv-serie-cancel\">Avbryt</button>' +\n          (modal.error ? '<div class=\"flv-modal-error\">' + esc(modal.error) + '</div>' : '') +\n          '</div></div>';\n        wrap.innerHTML = html;\n      }\n\n      function submitSerieModal() {\n        var modal = state.serieModal;\n        if (!modal || modal.saving) return;\n        var d = modal.draft;\n        if (!d.name.trim()) { modal.error = 'Ange ett namn.'; renderSerieModal(); return; }\n        if (d.steps.length < 1) { modal.error = 'Lägg till minst ett steg.'; renderSerieModal(); return; }\n        var body = { name: d.name.trim(), actionIds: d.steps.map(function (s) { return s.actionId; }) };\n        if (modal.kind === 'order') {\n          body.options = d.steps.filter(function (s) { return s.block === false; })\n            .map(function (s) { return { actionId: s.actionId, blockOnUnmatchedConditions: false }; });\n          if (d.description.trim()) body.description = d.description.trim();\n          if (d.maxExecutions !== '' && !isNaN(parseInt(d.maxExecutions, 10))) body.maxExecutions = parseInt(d.maxExecutions, 10);\n        }\n        modal.saving = true; modal.error = '';\n        renderSerieModal();\n        var promise = modal.serieId\n          ? api('PUT', seriePath(modal.kind) + '/' + modal.serieId, null, body)\n          : api('POST', seriePath(modal.kind), null, body);\n        promise.then(function () {\n          toast(modal.serieId ? 'Serien sparades' : 'Serien skapades');\n          closeSerieModal();\n          return refresh();\n        }).catch(function (err) {\n          modal.saving = false;\n          modal.error = err && err.status === 403 ? 'Endast admin kan ändra serier.' : 'Kunde inte spara (' + (err && err.status) + ').';\n          renderSerieModal();\n        });\n      }\n\n      /* ---------------- webhook modal ---------------- */\n      function openWebhookModal(base) {\n        state.webhookModal = {\n          mode: base ? 'edit' : 'create',\n          base: base ? deepClone(base) : null,\n          draft: {\n            url: base ? base.url || '' : '',\n            isEnabled: base ? base.isEnabled !== false : true,\n            retry: base ? !!base.retry : true,\n            auth: base ? !!base.auth : false,\n            authRights: base && base.authRights ? base.authRights.join(', ') : '',\n            events: base ? (base.events || []).map(function (e) { return e.name; }) : [],\n            eventSearch: ''\n          },\n          error: '', saving: false\n        };\n        renderWebhookModal();\n      }\n      function closeWebhookModal() { state.webhookModal = null; renderWebhookModal(); }\n\n      function eventGroups() {\n        var groups = {};\n        state.allowedEvents.forEach(function (name) {\n          var topic = name.split('-')[0];\n          if (!groups[topic]) groups[topic] = [];\n          groups[topic].push(name);\n        });\n        return groups;\n      }\n      function eventListHtml(d) {\n        var q = d.eventSearch.trim().toLowerCase();\n        var groups = eventGroups();\n        var topics = Object.keys(groups).sort();\n        if (!topics.length) return '<div class=\"flv-form-note\">Kunde inte hämta händelselistan.</div>';\n        var html = '';\n        topics.forEach(function (topic) {\n          var names = groups[topic].filter(function (n) { return !q || n.toLowerCase().indexOf(q) !== -1; });\n          if (!names.length) return;\n          html += '<div class=\"flv-input-label\">' + esc(topic) + '</div><div class=\"flv-tagpick\">' +\n            names.map(function (n) {\n              var picked = d.events.indexOf(n) !== -1;\n              return '<span class=\"flv-picktag' + (picked ? ' flv-picked' : '') + '\" data-flv-eventtoggle=\"' + esc(n) + '\">' + esc(n) + '</span>';\n            }).join('') + '</div>';\n        });\n        return html || '<div class=\"flv-form-note\">Inga händelser matchar.</div>';\n      }\n\n      function renderWebhookModal() {\n        var wrap = el('flv-webhook-modal');\n        if (!wrap) return;\n        var modal = state.webhookModal;\n        if (!modal) { wrap.className = 'flv-modal-backdrop'; wrap.innerHTML = ''; return; }\n        wrap.className = 'flv-modal-backdrop flv-open';\n        var d = modal.draft;\n        var html = '<div class=\"flv-modal\">';\n        html += '<div class=\"flv-modal-title\">' + (modal.mode === 'edit' ? 'Redigera webhook' : 'Ny webhook') + '</div>';\n        html += '<div class=\"flv-modal-body\"><div class=\"flv-modal-main\">';\n\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">1</div><div class=\"flv-step-title\">Grund</div></div><div class=\"flv-step-body\">';\n        html += '<div class=\"flv-input-label\">URL (SaleSys gör POST hit)</div>' +\n          '<input class=\"flv-underline-input\" data-flv-whbind=\"url\" placeholder=\"https://example.org/handle-event\" value=\"' + esc(d.url) + '\">';\n        html += '<div style=\"display:flex; gap:22px; margin-top:14px; flex-wrap:wrap;\">' +\n          '<div style=\"display:flex; align-items:center; gap:8px; cursor:pointer;\" data-flv-whtoggle=\"isEnabled\"><span class=\"flv-switch' + (d.isEnabled ? ' flv-on' : '') + '\"></span><span style=\"font-size:13px; font-weight:700;\">Aktiv</span></div>' +\n          '<div style=\"display:flex; align-items:center; gap:8px; cursor:pointer;\" data-flv-whtoggle=\"retry\"><span class=\"flv-switch' + (d.retry ? ' flv-on' : '') + '\"></span><span style=\"font-size:13px; font-weight:700;\">Retry vid fel (~20 min)</span></div>' +\n          '<div style=\"display:flex; align-items:center; gap:8px; cursor:pointer;\" data-flv-whtoggle=\"auth\"><span class=\"flv-switch' + (d.auth ? ' flv-on' : '') + '\"></span><span style=\"font-size:13px; font-weight:700;\">Skicka auth-token</span></div>' +\n          '</div>';\n        if (d.auth) {\n          html += '<div class=\"flv-input-label\">Rättigheter på token (kommaseparerade)</div>' +\n            '<input class=\"flv-underline-input\" data-flv-whbind=\"authRights\" placeholder=\"T.ex. orders.getAll, contacts.getAll\" value=\"' + esc(d.authRights) + '\">' +\n            '<div class=\"flv-form-note\"><i class=\"fas fa-info-circle\"></i>Verifiera tokens äkthet i din mottagare via GET /api/users/me-v1/identifiable.</div>';\n        }\n        html += '</div></div>';\n\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">2</div><div class=\"flv-step-title\">Händelser (' + d.events.length + ' valda)</div></div><div class=\"flv-step-body\">';\n        html += '<input class=\"flv-underline-input\" placeholder=\"Sök händelse...\" data-flv-whbind=\"eventSearch\" value=\"' + esc(d.eventSearch) + '\">';\n        html += '<div id=\"flv-event-list\" style=\"max-height:260px; overflow-y:auto; margin-top:4px;\">' + eventListHtml(d) + '</div>';\n        html += '</div></div>';\n\n        var preview = webhookDraftToBody(modal);\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">3</div><div class=\"flv-step-title\">Granska</div></div><div class=\"flv-step-body\" id=\"flv-wh-preview\">';\n        html += preview.errors.map(function (e) { return '<div class=\"flv-validation\"><i class=\"fas fa-exclamation-circle\"></i> ' + esc(e) + '</div>'; }).join('');\n        html += '<pre class=\"flv-pre\" style=\"max-height:160px;\">' + esc(pretty(preview.body)) + '</pre>';\n        html += '</div></div>';\n\n        html += '</div>';\n        html += '<div class=\"flv-modal-side\"><i class=\"fas fa-bolt\"></i>' +\n          '<div class=\"flv-modal-side-title\">Webhook</div>' +\n          'När någon av de valda händelserna sker POST:ar SaleSys till din URL med händelsens id:n ' +\n          '(inte hela objektet — hämta det via API:t vid behov).' +\n          '<div style=\"margin-top:12px; font-size:11.5px;\">Begränsningar: leveransförsök loggas inte i SaleSys, ' +\n          'retry är fast ~20 min, och webhooks kan inte raderas via API — inaktivera istället.</div>' +\n          '</div>';\n        html += '</div>';\n        html += '<div class=\"flv-modal-footer\">' +\n          '<button class=\"flv-btn flv-btn-primary\" id=\"flv-wh-save\"' + (modal.saving ? ' disabled' : '') + '>' + (modal.mode === 'edit' ? 'Spara' : 'Skapa') + '</button>' +\n          '<button class=\"flv-btn\" id=\"flv-wh-cancel\">Avbryt</button>' +\n          (modal.error ? '<div class=\"flv-modal-error\">' + esc(modal.error) + '</div>' : '') +\n          '</div></div>';\n        wrap.innerHTML = html;\n      }\n\n      function webhookDraftToBody(modal) {\n        var d = modal.draft;\n        var errors = [];\n        if (!d.url.trim()) errors.push('Ange en URL.');\n        else if (!/^https?:\\/\\//.test(d.url.trim())) errors.push('URL måste börja med http(s)://');\n        if (!d.events.length) errors.push('Välj minst en händelse.');\n        var body = {\n          url: d.url.trim(),\n          isEnabled: !!d.isEnabled,\n          retry: !!d.retry,\n          auth: !!d.auth,\n          authRights: d.auth ? d.authRights.split(',').map(function (s) { return s.trim(); }).filter(Boolean) : [],\n          events: d.events.map(function (n) { return { name: n }; })\n        };\n        return { body: body, errors: errors };\n      }\n\n      function submitWebhookModal() {\n        var modal = state.webhookModal;\n        if (!modal || modal.saving) return;\n        var built = webhookDraftToBody(modal);\n        if (built.errors.length) { modal.error = built.errors.join(' '); renderWebhookModal(); return; }\n        modal.saving = true; modal.error = '';\n        renderWebhookModal();\n        var whId = modal.base && (modal.base.id || modal.base._id);\n        var promise = whId\n          ? api('PUT', '/events/webhooks-v1/' + whId, null, built.body)\n          : api('POST', '/events/webhooks-v1', null, built.body);\n        promise.then(function () {\n          toast(whId ? 'Webhooken sparades' : 'Webhooken skapades');\n          closeWebhookModal();\n          return refreshWebhooks();\n        }).catch(function (err) {\n          modal.saving = false;\n          modal.error = err && err.status === 403 ? 'Endast admin kan hantera webhooks.' : 'Kunde inte spara (' + (err && err.status) + ').';\n          renderWebhookModal();\n        });\n      }\n\n      function refreshWebhooks() {\n        return tryApi('GET', '/events/webhooks-v1').then(function (list) {\n          if (list) state.webhooks = normalizeList(list);\n          buildRows();\n          renderAll();\n        });\n      }\n      function toggleWebhookEnabled(wh) {\n        var next = wh.isEnabled === false;\n        wh.isEnabled = next;\n        renderAll();\n        api('PUT', '/events/webhooks-v1/' + (wh.id || wh._id), null, { isEnabled: next }).then(function () {\n          toast(next ? 'Webhooken aktiverades' : 'Webhooken inaktiverades');\n        }).catch(function () {\n          wh.isEnabled = !next;\n          renderAll();\n          toast('Kunde inte ändra status');\n        });\n      }\n\n      /* ---------------- automation recipe (Nivå 1.5): händelse -> relä -> arbete ---------------- */\n      function defaultRights(kind) { return kind === 'call' ? 'dial.getAll' : 'orders.getAll'; }\n      function openRecipeModal() {\n        state.recipeModal = {\n          events: [], eventSearch: '',\n          workKind: null, workId: null, workSearch: '',\n          routes: {}, routePickFor: null, routeSearch: '',\n          setManual: true, showAdvanced: false,\n          authRights: 'orders.getAll',\n          error: '', saving: false\n        };\n        renderRecipeModal();\n      }\n      function closeRecipeModal() { state.recipeModal = null; renderRecipeModal(); }\n      function recipeRoutable(r) { return r.events.length === 1 && ROUTABLE[r.events[0]] ? ROUTABLE[r.events[0]] : null; }\n\n      function recipeWorkListHtml(r) {\n        var q = r.workSearch.trim().toLowerCase();\n        var rows = [];\n        ['order', 'call'].forEach(function (kind) {\n          (state.works[kind] || []).forEach(function (w) {\n            if (q && String(w.name || '').toLowerCase().indexOf(q) === -1) return;\n            rows.push({ kind: kind, w: w });\n          });\n        });\n        if (!rows.length) return '<div class=\"flv-worklist-item\" style=\"color:var(--flv-muted-fg); cursor:default;\">Inga arbeten matchar.</div>';\n        return rows.slice(0, 60).map(function (row) {\n          var picked = r.workId === row.w.id && r.workKind === row.kind;\n          return '<div class=\"flv-worklist-item\" data-flv-recipework=\"' + row.kind + ':' + esc(row.w.id) + '\">' +\n            '<i class=\"fas ' + (picked ? 'fa-check-circle' : (TODO_ICONS[toDoType(row.w)] || 'fa-cog')) + '\"></i>' +\n            esc(row.w.name || '?') +\n            '<span style=\"margin-left:auto; font-size:10.5px; color:var(--flv-muted-fg);\">' + (row.kind === 'call' ? 'samtal' : 'order') + '</span></div>';\n        }).join('');\n      }\n\n      function routeWorkListHtml(r, cat) {\n        var q = r.routeSearch.trim().toLowerCase();\n        var list = (state.works[cat.entity] || []).filter(function (w) { return !q || String(w.name || '').toLowerCase().indexOf(q) !== -1; });\n        if (!list.length) return '<div class=\"flv-worklist-item\" style=\"color:var(--flv-muted-fg); cursor:default;\">Inga ' + (cat.entity === 'call' ? 'samtalsarbeten' : 'orderarbeten') + ' matchar.</div>';\n        return list.slice(0, 40).map(function (w) {\n          return '<div class=\"flv-worklist-item\" data-flv-routeassign=\"' + esc(w.id) + '\"><i class=\"fas ' + (TODO_ICONS[toDoType(w)] || 'fa-cog') + '\"></i>' + esc(w.name || '?') + '</div>';\n        }).join('');\n      }\n\n      function recipeComposeBody(r) {\n        var errors = [];\n        var orgId = state.me && state.me.organizationId;\n        var cat = recipeRoutable(r);\n        var body;\n        if (cat) {\n          var cases = {};\n          Object.keys(r.routes).forEach(function (val) { if (r.routes[val]) cases[val] = r.routes[val]; });\n          if (!Object.keys(cases).length) errors.push('Mappa minst en status till ett arbete.');\n          var routesObj = { field: cat.field, entity: cat.entity, idPath: cat.idPath, cases: cases };\n          var url = RELAY_BASE + '?do=route&routes=' + encodeURIComponent(JSON.stringify(routesObj)) + (orgId ? '&organizationId=' + orgId : '');\n          body = {\n            url: url, isEnabled: true, auth: true, retry: true,\n            authRights: (cat.entity === 'call' ? 'dial.getAll' : 'orders.getAll').split(','),\n            events: [{ name: r.events[0] }]\n          };\n        } else {\n          if (!r.events.length) errors.push('Välj minst en händelse som ska trigga.');\n          if (!r.workId) errors.push('Välj vilket arbete som ska köras.');\n          var u = RELAY_BASE + '?do=execute&entity=' + (r.workKind || 'order') + '&actionId=' + (r.workId || '') + (orgId ? '&organizationId=' + orgId : '');\n          body = {\n            url: u, isEnabled: true, auth: true, retry: true,\n            authRights: r.authRights.split(',').map(function (s) { return s.trim(); }).filter(Boolean),\n            events: r.events.map(function (n) { return { name: n }; })\n          };\n        }\n        return { body: body, errors: errors };\n      }\n\n      function renderRecipeModal() {\n        var wrap = el('flv-recipe-modal');\n        if (!wrap) return;\n        var r = state.recipeModal;\n        if (!r) { wrap.className = 'flv-modal-backdrop'; wrap.innerHTML = ''; return; }\n        wrap.className = 'flv-modal-backdrop flv-open';\n        var cat = recipeRoutable(r);\n        var work = r.workId ? findWork(r.workKind, r.workId) : null;\n        var html = '<div class=\"flv-modal\">';\n        html += '<div class=\"flv-modal-title\">Ny automation</div>';\n        html += '<div class=\"flv-modal-body\"><div class=\"flv-modal-main\">';\n\n        /* live sentence */\n        if (cat) {\n          var nMapped = Object.keys(r.routes).length;\n          html += '<div style=\"font-size:15px; margin-bottom:16px; line-height:1.6;\">När <b style=\"color:var(--flv-warn);\">' +\n            esc(r.events[0]) + '</b> sker → kör <b style=\"color:var(--flv-primary);\">olika arbeten beroende på ' + esc(cat.title) + '</b>' +\n            (nMapped ? ' (' + nMapped + ' mappade)' : '') + '.</div>';\n        } else {\n          html += '<div style=\"font-size:15px; margin-bottom:16px; line-height:1.6;\">När <b style=\"color:var(--flv-warn);\">' +\n            (r.events.length ? esc(r.events[0]) + (r.events.length > 1 ? ' +' + (r.events.length - 1) : '') : '[händelse]') +\n            '</b> sker → kör <b style=\"color:var(--flv-primary);\">' + (work ? esc(work.name) : '[arbete]') + '</b>.</div>';\n        }\n\n        /* Step 1: trigger events */\n        html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">1</div><div class=\"flv-step-title\">När detta händer (' + r.events.length + ' valda)</div></div><div class=\"flv-step-body\">';\n        html += '<input class=\"flv-underline-input\" placeholder=\"Sök händelse...\" data-flv-recipebind=\"eventSearch\" value=\"' + esc(r.eventSearch) + '\">';\n        html += '<div id=\"flv-recipe-events\" style=\"max-height:200px; overflow-y:auto; margin-top:4px;\">' + recipeEventListHtml(r) + '</div>';\n        if (cat) html += '<div class=\"flv-form-note\"><i class=\"fas fa-code-branch\"></i>Den här händelsen bär ett tillstånd (' + esc(cat.title) + ') — mappa olika arbeten per värde nedan.</div>';\n        html += '</div></div>';\n\n        if (cat) {\n          /* Step 2 (routing): one work per status value */\n          html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">2</div><div class=\"flv-step-title\">Kör olika arbeten per ' + esc(cat.title) + '</div></div><div class=\"flv-step-body\">';\n          cat.values.forEach(function (val) {\n            var chosenId = r.routes[val];\n            var chosen = chosenId ? findWork(cat.entity, chosenId) : null;\n            html += '<div class=\"flv-serie-step-row\">' +\n              '<span class=\"flv-tag-chip\" style=\"min-width:96px; border:1px solid var(--flv-border); background:var(--flv-bg);\">' + esc(cat.labels[val] || val) + '</span>';\n            if (chosen) {\n              html += '<i class=\"fas ' + (TODO_ICONS[toDoType(chosen)] || 'fa-cog') + '\" style=\"color:var(--flv-primary); font-size:12px;\"></i>' +\n                '<span class=\"flv-serie-step-name\">' + esc(chosen.name) + '</span>' +\n                '<button class=\"flv-mini-btn flv-danger-btn\" data-flv-routeclear=\"' + esc(val) + '\"><i class=\"fas fa-times\"></i></button>';\n            } else {\n              html += '<span class=\"flv-serie-step-name\" style=\"color:var(--flv-muted-fg);\">' + (r.routePickFor === val ? 'välj nedan…' : 'ignoreras') + '</span>' +\n                '<button class=\"flv-mini-btn\" data-flv-routepick=\"' + esc(val) + '\"><i class=\"fas fa-plus\"></i> Välj arbete</button>';\n            }\n            html += '</div>';\n            if (r.routePickFor === val) {\n              html += '<input class=\"flv-underline-input\" placeholder=\"Sök arbete...\" data-flv-recipebind=\"routeSearch\" value=\"' + esc(r.routeSearch) + '\">';\n              html += '<div class=\"flv-worklist\" id=\"flv-route-works\">' + routeWorkListHtml(r, cat) + '</div>';\n            }\n          });\n          html += '<div class=\"flv-form-note\"><i class=\"fas fa-info-circle\"></i>Statusar utan arbete ignoreras. Arbetena körs på samtalet avtalet kom från (' + esc(cat.entity === 'call' ? 'samtalsarbeten' : 'orderarbeten') + ').</div>';\n          html += '</div></div>';\n        } else {\n          /* Step 2 (simple): one work */\n          html += '<div class=\"flv-step\"><div class=\"flv-step-head\"><div class=\"flv-step-num\">2</div><div class=\"flv-step-title\">Kör detta arbete</div></div><div class=\"flv-step-body\">';\n          html += '<input class=\"flv-underline-input\" placeholder=\"Sök arbete...\" data-flv-recipebind=\"workSearch\" value=\"' + esc(r.workSearch) + '\">';\n          html += '<div class=\"flv-worklist\" id=\"flv-recipe-works\">' + recipeWorkListHtml(r) + '</div>';\n          if (work && work.isAutomatic !== false) {\n            html += '<div style=\"display:flex; align-items:center; gap:8px; margin-top:12px; cursor:pointer;\" data-flv-recipetoggle=\"setManual\">' +\n              '<span class=\"flv-switch' + (r.setManual ? ' flv-on' : '') + '\"></span>' +\n              '<span style=\"font-size:12.5px; font-weight:700;\">Sätt arbetet till manuellt</span></div>' +\n              '<div class=\"flv-form-note\"><i class=\"fas fa-info-circle\"></i>Rekommenderas: då körs arbetet bara av den här händelsen, inte även av schemaläggaren.</div>';\n          }\n          html += '</div></div>';\n        }\n\n        /* Advanced */\n        html += '<div class=\"flv-step\"><div class=\"flv-step-body\">';\n        html += '<button class=\"flv-mini-btn\" data-flv-recipetoggle=\"showAdvanced\"><i class=\"fas fa-cog\"></i> ' + (r.showAdvanced ? 'Dölj' : 'Avancerat') + '</button>';\n        if (r.showAdvanced) {\n          html += '<div class=\"flv-input-label\" style=\"margin-top:10px;\">Rättigheter på reläets token</div>' +\n            '<input class=\"flv-underline-input\" data-flv-recipebind=\"authRights\" value=\"' + esc(r.authRights) + '\">' +\n            '<div class=\"flv-form-note\">Måste räcka för att köra arbetet. Order: <code>orders.getAll</code>, samtal: <code>dial.getAll</code>.</div>';\n          var composed = recipeComposeBody(r);\n          html += '<div class=\"flv-input-label\" style=\"margin-top:10px;\">Webhook som skapas</div>' +\n            '<pre class=\"flv-pre\" style=\"max-height:150px;\">' + esc(pretty(composed.body)) + '</pre>';\n        }\n        html += '</div></div>';\n\n        html += '</div>'; /* main */\n        html += '<div class=\"flv-modal-side\"><i class=\"fas fa-bolt\"></i>' +\n          '<div class=\"flv-modal-side-title\">Så funkar det</div>' +\n          'SaleSys saknar en inbyggd \"händelse kör arbete\"-koppling. Automationen skapar en webhook som ' +\n          'pekar på ett litet relä (hook-runner) som kör arbetet åt dig när händelsen sker.' +\n          '<div style=\"margin-top:12px; font-size:11.5px;\">Reläet lagrar inga hemligheter — det använder bara ' +\n          'den token webhooken redan skickar med. Kedja flöden genom att låta arbetet sätta en tagg som ett annat ' +\n          'arbete triggar på.</div>' +\n          '</div>';\n        html += '</div>'; /* body */\n        html += '<div class=\"flv-modal-footer\">' +\n          '<button class=\"flv-btn flv-btn-primary\" id=\"flv-recipe-save\"' + (r.saving ? ' disabled' : '') + '>Skapa automation</button>' +\n          '<button class=\"flv-btn\" id=\"flv-recipe-cancel\">Avbryt</button>' +\n          (r.error ? '<div class=\"flv-modal-error\">' + esc(r.error) + '</div>' : '') +\n          '</div></div>';\n        wrap.innerHTML = html;\n      }\n\n      function recipeEventListHtml(r) {\n        var q = r.eventSearch.trim().toLowerCase();\n        var groups = eventGroups();\n        var topics = Object.keys(groups).sort();\n        if (!topics.length) return '<div class=\"flv-form-note\">Kunde inte hämta händelselistan.</div>';\n        var html = '';\n        topics.forEach(function (topic) {\n          var names = groups[topic].filter(function (n) { return !q || n.toLowerCase().indexOf(q) !== -1; });\n          if (!names.length) return;\n          html += '<div class=\"flv-input-label\">' + esc(topic) + '</div><div class=\"flv-tagpick\">' +\n            names.map(function (n) {\n              var picked = r.events.indexOf(n) !== -1;\n              return '<span class=\"flv-picktag' + (picked ? ' flv-picked' : '') + '\" data-flv-recipeevent=\"' + esc(n) + '\">' + esc(n) + '</span>';\n            }).join('') + '</div>';\n        });\n        return html || '<div class=\"flv-form-note\">Inga händelser matchar.</div>';\n      }\n\n      function submitRecipeModal() {\n        var r = state.recipeModal;\n        if (!r || r.saving) return;\n        var composed = recipeComposeBody(r);\n        if (composed.errors.length) { r.error = composed.errors.join(' '); renderRecipeModal(); return; }\n        r.saving = true; r.error = '';\n        renderRecipeModal();\n        api('POST', '/events/webhooks-v1', null, composed.body).then(function () {\n          /* optionally flip the work to manual so only the event triggers it */\n          if (r.setManual) {\n            var work = findWork(r.workKind, r.workId);\n            if (work && work.isAutomatic !== false) {\n              return api('PUT', workPath(r.workKind) + '/' + r.workId, null, { isAutomatic: false }).catch(function () {});\n            }\n          }\n        }).then(function () {\n          toast('Automation skapad');\n          closeRecipeModal();\n          return Promise.all([refreshWebhooks(), refresh()]);\n        }).catch(function (err) {\n          r.saving = false;\n          r.error = err && err.status === 403 ? 'Endast admin kan skapa automationer.' : 'Kunde inte skapa (' + (err && err.status) + ').';\n          renderRecipeModal();\n        });\n      }\n\n      /* draft binding helper */\n      function setDraftPath(obj, path, value) {\n        var parts = path.split('.');\n        var target = obj;\n        for (var i = 0; i < parts.length - 1; i++) target = target[parts[i]];\n        target[parts[parts.length - 1]] = value;\n      }\n\n      /* ---------------- events ---------------- */\n      function inRoot(node) {\n        var root = document.getElementById('flv-root');\n        return root && node && root.contains(node);\n      }\n\n      function selectAction(kind, actionId) {\n        state.selected = { type: 'action', kind: kind, id: actionId };\n        if (state.tab === 'history') state.tab = 'overview';\n        state.jsonEdit = false;\n        state.confirmDelete = null;\n        renderBoard();\n        renderInspector();\n      }\n\n      function onClick(event) {\n        var target = event.target;\n        if (!target.closest) return;\n\n        /* ---------- phase 2: toolbar ---------- */\n        var newWork = target.closest('#flv-new-work');\n        if (newWork && inRoot(newWork)) { openWorkModal('create', state.filter === 'call' ? 'call' : 'order', null); return; }\n        var newSerie = target.closest('#flv-new-serie');\n        if (newSerie && inRoot(newSerie)) { openSerieModal(state.filter === 'call' ? 'call' : 'order', null, null); return; }\n        var newJson = target.closest('#flv-new-json');\n        if (newJson && inRoot(newJson)) { openWorkModal('json', state.filter === 'call' ? 'call' : 'order', null); return; }\n        var newWebhook = target.closest('#flv-new-webhook');\n        if (newWebhook && inRoot(newWebhook)) { openWebhookModal(null); return; }\n        var newAutomation = target.closest('#flv-new-automation');\n        if (newAutomation && inRoot(newAutomation)) { openRecipeModal(); return; }\n        var devToggle = target.closest('#flv-dev-toggle');\n        if (devToggle && inRoot(devToggle)) {\n          state.devMode = !state.devMode;\n          state.jsonEdit = false;\n          savePrefs();\n          renderAll();\n          toast(state.devMode ? 'Developer mode på' : 'Developer mode av');\n          return;\n        }\n\n        /* ---------- phase 2: work modal ---------- */\n        var workModalEl = el('flv-work-modal');\n        if (state.modal && workModalEl && workModalEl.contains(target)) {\n          if (target === workModalEl) { closeWorkModal(); return; }\n          var cancel = target.closest('#flv-modal-cancel');\n          if (cancel) { closeWorkModal(); return; }\n          var save = target.closest('#flv-modal-save');\n          if (save) { submitWorkModal(); return; }\n          var kindPick = target.closest('[data-flv-kindpick]');\n          if (kindPick) {\n            var newKind = kindPick.getAttribute('data-flv-kindpick');\n            if (newKind !== state.modal.kind) {\n              state.modal.kind = newKind;\n              if (state.modal.mode !== 'json') state.modal.draft = emptyDraft(newKind);\n              renderWorkModal();\n            }\n            return;\n          }\n          if (state.modal.mode !== 'json') {\n            var d = state.modal.draft;\n            var draftToggle = target.closest('[data-flv-drafttoggle]');\n            if (draftToggle) { var key = draftToggle.getAttribute('data-flv-drafttoggle'); d[key] = !d[key]; renderWorkModal(); return; }\n            var timeUnit = target.closest('[data-flv-timeunit]');\n            if (timeUnit) { d.time.unit = timeUnit.getAttribute('data-flv-timeunit'); renderWorkModal(); return; }\n            var timeTrigger = target.closest('[data-flv-timetrigger]');\n            if (timeTrigger) { d.time.trigger = timeTrigger.getAttribute('data-flv-timetrigger'); renderWorkModal(); return; }\n            var trigTag = target.closest('[data-flv-trigtag]');\n            if (trigTag) {\n              var tid = trigTag.getAttribute('data-flv-trigtag');\n              var ti = d.time.trigTags.indexOf(tid);\n              if (ti === -1) d.time.trigTags.push(tid); else d.time.trigTags.splice(ti, 1);\n              renderWorkModal(); return;\n            }\n            var tagToggle = target.closest('[data-flv-tagtoggle]');\n            if (tagToggle) {\n              var ref = tagToggle.getAttribute('data-flv-tagtoggle').split(':');\n              var list = d.tags[ref[0]];\n              var idx = list.indexOf(ref[1]);\n              if (idx === -1) list.push(ref[1]); else list.splice(idx, 1);\n              renderWorkModal(); return;\n            }\n            var todoTypeCard = target.closest('[data-flv-todotype]');\n            if (todoTypeCard) { d.todoType = todoTypeCard.getAttribute('data-flv-todotype'); renderWorkModal(); return; }\n            var toMode = target.closest('[data-flv-tomode]');\n            if (toMode) {\n              var sub2 = d.todoType === 'sms' ? d.sms : d.email;\n              sub2.toMode = toMode.getAttribute('data-flv-tomode');\n              renderWorkModal(); return;\n            }\n            var fieldPick = target.closest('[data-flv-fieldpick]');\n            if (fieldPick) {\n              var sub3 = d.todoType === 'sms' ? d.sms : d.email;\n              sub3.toField = fieldPick.getAttribute('data-flv-fieldpick');\n              renderWorkModal(); return;\n            }\n            var httpMethod = target.closest('[data-flv-httpmethod]');\n            if (httpMethod) { d.http.method = httpMethod.getAttribute('data-flv-httpmethod'); renderWorkModal(); return; }\n            var presetPick = target.closest('[data-flv-presetpick]');\n            if (presetPick) { d.preset.presetId = presetPick.getAttribute('data-flv-presetpick'); renderWorkModal(); return; }\n            var kvAdd = target.closest('[data-flv-kv-add]');\n            if (kvAdd) { d.http[kvAdd.getAttribute('data-flv-kv-add')].push({ k: '', v: '' }); renderWorkModal(); return; }\n            var kvDel = target.closest('[data-flv-kv-del]');\n            if (kvDel) {\n              var delRef = kvDel.getAttribute('data-flv-kv-del').split(':');\n              d.http[delRef[0]].splice(parseInt(delRef[1], 10), 1);\n              renderWorkModal(); return;\n            }\n            var curlToggle = target.closest('#flv-curl-toggle');\n            if (curlToggle) { state.modal.curlOpen = !state.modal.curlOpen; renderWorkModal(); return; }\n            var curlApply = target.closest('#flv-curl-apply');\n            if (curlApply) {\n              var curlInput = el('flv-curl-input');\n              var parsed = parseCurl(curlInput ? curlInput.value : '');\n              if (parsed.error) { state.modal.curlNotes = [parsed.error]; }\n              else {\n                applyCurlToDraft(state.modal, parsed);\n                state.modal.curlNotes = parsed.notes.length ? parsed.notes : ['Importerad.'];\n                state.modal.curlOpen = false;\n              }\n              state.modal.curlText = curlInput ? curlInput.value : '';\n              renderWorkModal(); return;\n            }\n          }\n          return; /* swallow all other clicks inside the modal */\n        }\n\n        /* ---------- phase 3: webhook modal ---------- */\n        var whModalEl = el('flv-webhook-modal');\n        if (state.webhookModal && whModalEl && whModalEl.contains(target)) {\n          if (target === whModalEl) { closeWebhookModal(); return; }\n          var whCancel = target.closest('#flv-wh-cancel');\n          if (whCancel) { closeWebhookModal(); return; }\n          var whSave = target.closest('#flv-wh-save');\n          if (whSave) { submitWebhookModal(); return; }\n          var whToggle = target.closest('[data-flv-whtoggle]');\n          if (whToggle) {\n            var whKey = whToggle.getAttribute('data-flv-whtoggle');\n            state.webhookModal.draft[whKey] = !state.webhookModal.draft[whKey];\n            renderWebhookModal(); return;\n          }\n          var eventToggle = target.closest('[data-flv-eventtoggle]');\n          if (eventToggle) {\n            var evName = eventToggle.getAttribute('data-flv-eventtoggle');\n            var evList = state.webhookModal.draft.events;\n            var evIdx = evList.indexOf(evName);\n            if (evIdx === -1) evList.push(evName); else evList.splice(evIdx, 1);\n            renderWebhookModal(); return;\n          }\n          return;\n        }\n\n        /* ---------- automation recipe modal ---------- */\n        var recipeModalEl = el('flv-recipe-modal');\n        if (state.recipeModal && recipeModalEl && recipeModalEl.contains(target)) {\n          if (target === recipeModalEl) { closeRecipeModal(); return; }\n          var rCancel = target.closest('#flv-recipe-cancel');\n          if (rCancel) { closeRecipeModal(); return; }\n          var rSave = target.closest('#flv-recipe-save');\n          if (rSave) { submitRecipeModal(); return; }\n          var rEvent = target.closest('[data-flv-recipeevent]');\n          if (rEvent) {\n            var en = rEvent.getAttribute('data-flv-recipeevent');\n            var ei = state.recipeModal.events.indexOf(en);\n            if (ei === -1) state.recipeModal.events.push(en); else state.recipeModal.events.splice(ei, 1);\n            renderRecipeModal(); return;\n          }\n          var rWork = target.closest('[data-flv-recipework]');\n          if (rWork) {\n            var ref = rWork.getAttribute('data-flv-recipework').split(':');\n            var rm = state.recipeModal;\n            var prevDefault = defaultRights(rm.workKind);\n            rm.workKind = ref[0]; rm.workId = ref[1];\n            /* keep authRights sensible unless the user has customised it */\n            if (!rm.authRights || rm.authRights === prevDefault) rm.authRights = defaultRights(ref[0]);\n            renderRecipeModal(); return;\n          }\n          var rToggle = target.closest('[data-flv-recipetoggle]');\n          if (rToggle) {\n            var rk = rToggle.getAttribute('data-flv-recipetoggle');\n            state.recipeModal[rk] = !state.recipeModal[rk];\n            renderRecipeModal(); return;\n          }\n          var rPick = target.closest('[data-flv-routepick]');\n          if (rPick) { state.recipeModal.routePickFor = rPick.getAttribute('data-flv-routepick'); state.recipeModal.routeSearch = ''; renderRecipeModal(); return; }\n          var rClear = target.closest('[data-flv-routeclear]');\n          if (rClear) { delete state.recipeModal.routes[rClear.getAttribute('data-flv-routeclear')]; renderRecipeModal(); return; }\n          var rAssign = target.closest('[data-flv-routeassign]');\n          if (rAssign) {\n            if (state.recipeModal.routePickFor) state.recipeModal.routes[state.recipeModal.routePickFor] = rAssign.getAttribute('data-flv-routeassign');\n            state.recipeModal.routePickFor = null;\n            renderRecipeModal(); return;\n          }\n          return;\n        }\n\n        /* ---------- phase 3: webhook inspector + board ---------- */\n        var whToggleBtn = target.closest('[data-flv-toggle-webhook]');\n        if (whToggleBtn && inRoot(whToggleBtn)) {\n          var tWh = selectedWebhook();\n          if (tWh) toggleWebhookEnabled(tWh);\n          return;\n        }\n        var whEditBtn = target.closest('[data-flv-edit-webhook]');\n        if (whEditBtn && inRoot(whEditBtn)) {\n          var eWh = selectedWebhook();\n          if (eWh) openWebhookModal(eWh);\n          return;\n        }\n        var whNode = target.closest('[data-flv-webhook]');\n        if (whNode && inRoot(whNode)) {\n          state.selected = { type: 'webhook', kind: 'webhook', id: whNode.getAttribute('data-flv-webhook') };\n          if (state.tab !== 'json') state.tab = 'overview';\n          state.jsonEdit = false;\n          state.confirmDelete = null;\n          renderBoard();\n          renderInspector();\n          return;\n        }\n\n        /* ---------- phase 2: serie modal ---------- */\n        var serieModalEl = el('flv-serie-modal');\n        if (state.serieModal && serieModalEl && serieModalEl.contains(target)) {\n          if (target === serieModalEl) { closeSerieModal(); return; }\n          var sCancel = target.closest('#flv-serie-cancel');\n          if (sCancel) { closeSerieModal(); return; }\n          var sSave = target.closest('#flv-serie-save');\n          if (sSave) { submitSerieModal(); return; }\n          var sKind = target.closest('[data-flv-seriekind]');\n          if (sKind) {\n            var sk = sKind.getAttribute('data-flv-seriekind');\n            if (sk !== state.serieModal.kind) { state.serieModal.kind = sk; state.serieModal.draft.steps = []; renderSerieModal(); }\n            return;\n          }\n          var sd = state.serieModal.draft;\n          var pickerToggle = target.closest('#flv-step-picker-toggle');\n          if (pickerToggle) { sd.pickerOpen = !sd.pickerOpen; renderSerieModal(); return; }\n          var stepAdd = target.closest('[data-flv-stepadd]');\n          if (stepAdd) { sd.steps.push({ actionId: stepAdd.getAttribute('data-flv-stepadd'), block: true }); renderSerieModal(); return; }\n          var stepDel = target.closest('[data-flv-stepdel]');\n          if (stepDel) { sd.steps.splice(parseInt(stepDel.getAttribute('data-flv-stepdel'), 10), 1); renderSerieModal(); return; }\n          var stepMove = target.closest('[data-flv-stepmove]');\n          if (stepMove) {\n            var mv = stepMove.getAttribute('data-flv-stepmove').split(':');\n            var from = parseInt(mv[0], 10), dir = parseInt(mv[1], 10), to = from + dir;\n            if (to >= 0 && to < sd.steps.length) {\n              var tmp = sd.steps[from]; sd.steps[from] = sd.steps[to]; sd.steps[to] = tmp;\n              renderSerieModal();\n            }\n            return;\n          }\n          var stepBlock = target.closest('[data-flv-stepblock]');\n          if (stepBlock) {\n            var bi = parseInt(stepBlock.getAttribute('data-flv-stepblock'), 10);\n            sd.steps[bi].block = !sd.steps[bi].block;\n            renderSerieModal(); return;\n          }\n          return;\n        }\n\n        /* ---------- phase 2: inspector quick actions ---------- */\n        var toggleBtn = target.closest('[data-flv-toggle-enabled]');\n        if (toggleBtn && inRoot(toggleBtn)) {\n          var tAction = selectedAction();\n          if (tAction) toggleEnabled(state.selected.kind, tAction);\n          return;\n        }\n        var editBtn = target.closest('[data-flv-edit-action]');\n        if (editBtn && inRoot(editBtn)) {\n          var eAction = selectedAction();\n          if (eAction) openWorkModal('edit', state.selected.kind, eAction);\n          return;\n        }\n        var dupBtn = target.closest('[data-flv-duplicate]');\n        if (dupBtn && inRoot(dupBtn)) {\n          var dAction = selectedAction();\n          if (dAction) duplicateWork(state.selected.kind, dAction);\n          return;\n        }\n        var delBtn = target.closest('[data-flv-delete]');\n        if (delBtn && inRoot(delBtn)) {\n          var delAction = selectedAction();\n          if (!delAction) return;\n          if (state.confirmDelete === delAction.id) { state.confirmDelete = null; deleteWork(state.selected.kind, delAction.id); }\n          else { state.confirmDelete = delAction.id; renderInspector(); }\n          return;\n        }\n        var editSerieBtn = target.closest('[data-flv-edit-serie]');\n        if (editSerieBtn && inRoot(editSerieBtn)) {\n          var eSerie = selectedSerie();\n          if (eSerie) openSerieModal(state.selected.kind, eSerie, null);\n          return;\n        }\n        var delSerieBtn = target.closest('[data-flv-delete-serie]');\n        if (delSerieBtn && inRoot(delSerieBtn)) {\n          var dSerie = selectedSerie();\n          if (!dSerie) return;\n          if (state.confirmDelete === dSerie.id) { state.confirmDelete = null; deleteSerie(state.selected.kind, dSerie.id); }\n          else { state.confirmDelete = dSerie.id; renderInspector(); }\n          return;\n        }\n        var jsonEditBtn = target.closest('#flv-json-edit');\n        if (jsonEditBtn && inRoot(jsonEditBtn)) { state.jsonEdit = true; renderInspector(); return; }\n        var jsonCancelBtn = target.closest('#flv-json-cancel');\n        if (jsonCancelBtn && inRoot(jsonCancelBtn)) { state.jsonEdit = false; renderInspector(); return; }\n        var jsonSaveBtn = target.closest('#flv-json-save');\n        if (jsonSaveBtn && inRoot(jsonSaveBtn)) { submitJsonEdit(); return; }\n        var copyCurl = target.closest('#flv-copy-curl');\n        if (copyCurl && inRoot(copyCurl)) {\n          var cAction = selectedAction();\n          if (cAction && navigator.clipboard) {\n            navigator.clipboard.writeText(workToCurl(state.selected.kind, cAction))\n              .then(function () { toast('curl kopierad (PUT — byt till POST utan id för att skapa ny)'); }, function () { toast('Kunde inte kopiera'); });\n          }\n          return;\n        }\n\n        /* ---------- phase 2: board ghost node ---------- */\n        var addStep = target.closest('[data-flv-addstep]');\n        if (addStep && inRoot(addStep)) {\n          var rowRef = addStep.getAttribute('data-flv-addstep').split(':'); /* serie:kind:id | work:kind:id */\n          if (rowRef[0] === 'serie') {\n            var serieObj = findSerie(rowRef[1], rowRef[2]);\n            if (serieObj) openSerieModal(rowRef[1], serieObj, null);\n          } else {\n            openSerieModal(rowRef[1], null, rowRef[2]);\n          }\n          return;\n        }\n\n        var chip = target.closest('[data-flv-filter]');\n        if (chip && inRoot(chip)) {\n          state.filter = chip.getAttribute('data-flv-filter');\n          savePrefs();\n          renderChips();\n          renderBoard();\n          return;\n        }\n\n        var reload = target.closest('#flv-reload');\n        if (reload && inRoot(reload)) {\n          state.execs = { order: {}, call: {} };\n          state.execsStatus = { order: 'pending', call: 'pending' };\n          state.history = {};\n          load();\n          return;\n        }\n\n        var close = target.closest('#flv-insp-close');\n        if (close && inRoot(close)) {\n          state.selected = null;\n          renderBoard();\n          renderInspector();\n          return;\n        }\n\n        var tab = target.closest('[data-flv-tab]');\n        if (tab && inRoot(tab)) {\n          state.tab = tab.getAttribute('data-flv-tab');\n          if (state.tab === 'history') {\n            var a = selectedAction();\n            if (a) loadHistory(state.selected.kind, a.id);\n          }\n          renderInspector();\n          return;\n        }\n\n        var copy = target.closest('#flv-copy-json');\n        if (copy && inRoot(copy)) {\n          var src = el('flv-json-src');\n          if (src && navigator.clipboard) {\n            navigator.clipboard.writeText(src.textContent).then(function () { toast('JSON kopierad'); }, function () { toast('Kunde inte kopiera'); });\n          }\n          return;\n        }\n\n        var runToggle = target.closest('[data-flv-run-toggle]');\n        if (runToggle && inRoot(runToggle)) {\n          var item = runToggle.closest('.flv-run-item');\n          if (item) item.className = item.className.indexOf('flv-expanded') !== -1 ? 'flv-run-item' : 'flv-run-item flv-expanded';\n          return;\n        }\n\n        var actionNode = target.closest('[data-flv-action]');\n        if (actionNode && inRoot(actionNode)) {\n          var ref = actionNode.getAttribute('data-flv-action').split(':');\n          selectAction(ref[0], ref[1]);\n          return;\n        }\n\n        var trigger = target.closest('[data-flv-trigger]');\n        if (trigger && inRoot(trigger)) {\n          var rowId = trigger.getAttribute('data-flv-trigger');\n          var parts = rowId.split(':'); /* serie:kind:id | work:kind:id */\n          if (parts[0] === 'serie') {\n            state.selected = { type: 'serie', kind: parts[1], id: parts[2] };\n            if (state.tab === 'runs' || state.tab === 'history') state.tab = 'overview';\n            renderBoard();\n            renderInspector();\n          } else {\n            selectAction(parts[1], parts[2]);\n          }\n          return;\n        }\n      }\n\n      var searchTimer = null;\n      function onInput(event) {\n        var target = event.target;\n        if (!target.closest) return;\n        var search = target.closest('#flv-search');\n        if (search && inRoot(search)) {\n          clearTimeout(searchTimer);\n          searchTimer = setTimeout(function () {\n            state.search = search.value || '';\n            renderBoard();\n          }, 180);\n          return;\n        }\n\n        /* work modal bindings */\n        if (state.modal && state.modal.mode !== 'json') {\n          var d = state.modal.draft;\n          var bind = target.closest('[data-flv-bind]');\n          if (bind) {\n            var path = bind.getAttribute('data-flv-bind');\n            setDraftPath(d, path, bind.value);\n            if (path === 'fieldSearch') {\n              var list = el('flv-field-list');\n              if (list) list.innerHTML = fieldListItemsHtml(d, d.todoType === 'sms' ? d.sms : d.email);\n            }\n            return;\n          }\n          var kv = target.closest('[data-flv-kv]');\n          if (kv) {\n            var ref = kv.getAttribute('data-flv-kv').split(':');\n            var row = d.http[ref[0]][parseInt(ref[1], 10)];\n            if (row) row[ref[2]] = kv.value;\n            return;\n          }\n          var curlArea = target.closest('#flv-curl-input');\n          if (curlArea) { state.modal.curlText = curlArea.value; return; }\n        }\n        if (state.modal && state.modal.mode === 'json') {\n          var jsonArea = target.closest('#flv-json-create');\n          if (jsonArea) { state.modal.jsonText = jsonArea.value; return; }\n        }\n\n        /* recipe modal bindings */\n        if (state.recipeModal) {\n          var rBind = target.closest('[data-flv-recipebind]');\n          if (rBind) {\n            var rBindKey = rBind.getAttribute('data-flv-recipebind');\n            state.recipeModal[rBindKey] = rBind.value;\n            if (rBindKey === 'eventSearch') {\n              var rEvList = el('flv-recipe-events');\n              if (rEvList) rEvList.innerHTML = recipeEventListHtml(state.recipeModal);\n            } else if (rBindKey === 'workSearch') {\n              var rWkList = el('flv-recipe-works');\n              if (rWkList) rWkList.innerHTML = recipeWorkListHtml(state.recipeModal);\n            } else if (rBindKey === 'routeSearch') {\n              var rtList = el('flv-route-works');\n              var rcat = recipeRoutable(state.recipeModal);\n              if (rtList && rcat) rtList.innerHTML = routeWorkListHtml(state.recipeModal, rcat);\n            }\n            return;\n          }\n        }\n\n        /* webhook modal bindings */\n        if (state.webhookModal) {\n          var whBind = target.closest('[data-flv-whbind]');\n          if (whBind) {\n            var whBindKey = whBind.getAttribute('data-flv-whbind');\n            state.webhookModal.draft[whBindKey] = whBind.value;\n            if (whBindKey === 'eventSearch') {\n              var evListEl = el('flv-event-list');\n              if (evListEl) evListEl.innerHTML = eventListHtml(state.webhookModal.draft);\n            }\n            return;\n          }\n        }\n\n        /* serie modal bindings */\n        if (state.serieModal) {\n          var sBind = target.closest('[data-flv-seriebind]');\n          if (sBind) {\n            var sKey = sBind.getAttribute('data-flv-seriebind');\n            state.serieModal.draft[sKey] = sBind.value;\n            if (sKey === 'pickerSearch') {\n              var pickList = el('flv-serie-picklist');\n              if (pickList) pickList.innerHTML = seriePickItemsHtml(state.serieModal.draft, state.serieModal.kind);\n            }\n            return;\n          }\n        }\n      }\n\n      function onChange(event) {\n        /* update the Granska preview when a modal field loses focus */\n        if (state.modal && state.modal.mode !== 'json' && event.target.closest) {\n          if (event.target.closest('[data-flv-bind]') || event.target.closest('[data-flv-kv]')) updatePreview();\n        }\n      }\n\n      function onKeyDown(event) {\n        if (event.key !== 'Escape') return;\n        if (state.modal) { closeWorkModal(); return; }\n        if (state.serieModal) { closeSerieModal(); return; }\n        if (state.webhookModal) { closeWebhookModal(); return; }\n        if (state.recipeModal) { closeRecipeModal(); return; }\n        if (state.jsonEdit) { state.jsonEdit = false; renderInspector(); return; }\n        if (state.selected) {\n          state.selected = null;\n          renderBoard();\n          renderInspector();\n        }\n      }\n\n      /* ---------------- contrast probe ---------------- */\n      function applyContrast() {\n        var root = document.getElementById('flv-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--flv-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var flvThemeObserver = new MutationObserver(applyContrast);\n      flvThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      /* Theme switches rewrite a style tag in head - html/body attributes never change. */\n      flvThemeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) flvThemeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      /* ---------------- re-render guard ----------------\n         The app can re-render the view DOM after script init (markup resets to the\n         loading state while our closure state survives). Detect and re-paint. */\n      var guardTimer = setInterval(function () {\n        var wrap = el('flv-board-wrap');\n        if (!wrap) return;\n        if (!state.loading && wrap.getAttribute('data-flv-stamp') !== String(state.renderStamp)) {\n          renderAll();\n          applyContrast();\n        }\n      }, 1200);\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('input', onInput);\n      document.addEventListener('change', onChange);\n      document.addEventListener('keydown', onKeyDown);\n\n      window.__flvCleanup = function () {\n        flvThemeObserver.disconnect();\n        clearInterval(guardTimer);\n        clearTimeout(prefsTimer);\n        clearTimeout(searchTimer);\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('input', onInput);\n        document.removeEventListener('change', onChange);\n        document.removeEventListener('keydown', onKeyDown);\n      };\n\n      load();\n    })();\n  </script>\n</div>","iconName":"project-diagram","iconColor":"#00897B","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a50f1a6c1a0f81e5d6d268d","name":"Kvalitetssäkring","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":1,"label":"Kvalitetssäkring"}],"html":"<div id=\"qav-root\">\n  <style>\n    #qav-root {\n      --qav-bg: var(--color-background-2, #f4f5f7);\n      --qav-card: var(--color-background-1, #ffffff);\n      --qav-secondary: var(--color-background-3, #eeeeee);\n      --qav-fg: var(--color-text-1, #1f2430);\n      --qav-fg2: var(--color-text-2, #5a6472);\n      --qav-muted-fg: var(--color-text-3, #8a93a2);\n      --qav-primary: var(--color-primary, #1565C0);\n      --qav-danger: var(--color-danger, #E53935);\n      --qav-success: #2E7D32;\n      --qav-hover: var(--color-element-hover, rgba(20, 20, 20, 0.04));\n      --qav-border: color-mix(in srgb, var(--qav-fg) 13%, transparent);\n      --qav-border-soft: color-mix(in srgb, var(--qav-fg) 8%, transparent);\n      --qav-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--qav-fg);\n      display: block;\n      background: var(--qav-bg);\n      border: 1px solid var(--qav-border-soft);\n      border-radius: 12px;\n      overflow: hidden;\n    }\n    #qav-root * { box-sizing: border-box; }\n    #qav-root button { font-family: inherit; cursor: pointer; }\n    #qav-root input, #qav-root textarea { font-family: inherit; font-size: 14px; color: var(--qav-fg); }\n    #qav-root a { color: var(--qav-primary); }\n\n    /* Hide the app's redundant page heading (\"Kvalitetssäkring\") above the view.\n       Scoped to the ribbon title only; reverts automatically when the view unmounts. */\n    .ribbon_title__3ukce { display: none !important; }\n\n    /* ---------- top bar ---------- */\n    .qav-topbar {\n      display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\n      padding: 9px 14px; border-bottom: 1px solid var(--qav-border-soft);\n      background: var(--qav-card);\n    }\n    .qav-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; }\n    .qav-title .fas { color: var(--qav-primary); }\n    .qav-remaining {\n      display: inline-flex; align-items: center; gap: 8px;\n      background: color-mix(in srgb, var(--qav-primary) 12%, var(--qav-card));\n      color: var(--qav-primary); font-weight: 800; font-size: 13px;\n      padding: 5px 12px; border-radius: 999px;\n    }\n    .qav-remaining .qav-num { font-size: 15px; }\n    .qav-topbar-spacer { flex: 1; }\n    .qav-tool-btn {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--qav-border); background: var(--qav-card);\n      border-radius: var(--qav-radius); padding: 7px 12px; font-size: 13px; font-weight: 700; color: var(--qav-fg);\n    }\n    .qav-tool-btn:hover { background: var(--qav-hover); }\n    .qav-icon-btn {\n      border: 1px solid var(--qav-border); background: var(--qav-card); color: var(--qav-fg2);\n      width: 34px; height: 34px; border-radius: var(--qav-radius); display: inline-flex; align-items: center; justify-content: center;\n    }\n    .qav-icon-btn:hover { background: var(--qav-hover); }\n\n    /* ---------- layout ---------- */\n    .qav-stage { padding: 12px 14px; }\n    /* 3 columns: [listen & decide] [order data] [contract preview]. */\n    .qav-grid { display: grid; grid-template-columns: minmax(0, 0.95fr) minmax(0, 0.9fr) minmax(0, 1.05fr); gap: 12px; align-items: start; }\n    @media (max-width: 1200px) { .qav-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }\n    @media (max-width: 820px) { .qav-grid { grid-template-columns: 1fr; } }\n    /* Left (controls) and right (preview) columns stick; the middle data column scrolls. */\n    .qav-left, .qav-preview-col { position: sticky; top: 8px; align-self: start; }\n    @media (max-width: 1200px) { .qav-preview-col { position: static; grid-column: 1 / -1; } }\n\n    .qav-panel {\n      background: var(--qav-card); border: 1px solid var(--qav-border-soft);\n      border-radius: 11px; padding: 12px 14px; margin-bottom: 12px;\n    }\n    .qav-panel:last-child { margin-bottom: 0; }\n    .qav-panel-h {\n      font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em;\n      color: var(--qav-muted-fg); display: flex; align-items: center; gap: 8px; margin-bottom: 9px;\n    }\n    .qav-panel-h .fas { color: var(--qav-primary); font-size: 12px; }\n    .qav-panel-h .qav-h-spacer { flex: 1; }\n\n    /* ---------- order header ---------- */\n    .qav-order-head { display: flex; align-items: flex-start; gap: 11px; }\n    .qav-order-serial {\n      font-size: 11.5px; font-weight: 800; color: var(--qav-primary);\n      background: color-mix(in srgb, var(--qav-primary) 12%, var(--qav-card));\n      border-radius: 8px; padding: 5px 9px; white-space: nowrap;\n    }\n    .qav-order-name { font-size: 16px; font-weight: 800; line-height: 1.2; }\n    .qav-order-sub { font-size: 12px; color: var(--qav-muted-fg); margin-top: 3px; display: flex; gap: 11px; flex-wrap: wrap; }\n    .qav-order-sub .fas { font-size: 11px; margin-right: 3px; }\n    .qav-chips { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 7px; }\n    .qav-chip {\n      font-size: 10.5px; font-weight: 700; padding: 2px 9px; border-radius: 999px;\n      background: var(--qav-secondary); color: var(--qav-fg2);\n    }\n\n    /* ---------- audio ---------- */\n    .qav-audio-wrap { display: flex; flex-direction: column; gap: 10px; }\n    .qav-audio-row { display: flex; align-items: center; gap: 12px; }\n    .qav-play {\n      width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;\n      background: var(--qav-primary); color: var(--qav-primary-fg, #fff); border: none;\n      display: inline-flex; align-items: center; justify-content: center; font-size: 14px;\n    }\n    .qav-play:disabled { opacity: 0.45; cursor: default; }\n    .qav-seek-col { flex: 1; display: flex; flex-direction: column; gap: 5px; }\n    .qav-viz { width: 100%; height: 42px; display: block; cursor: pointer; border-radius: 8px; }\n    .qav-seek-time { display: flex; justify-content: space-between; font-size: 11px; color: var(--qav-muted-fg); font-variant-numeric: tabular-nums; }\n    .qav-rate { display: flex; gap: 4px; }\n    .qav-rate button {\n      font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 7px;\n      border: 1px solid var(--qav-border); background: var(--qav-card); color: var(--qav-fg2);\n    }\n    .qav-rate button.qav-active { background: var(--qav-primary); border-color: var(--qav-primary); color: var(--qav-primary-fg, #fff); }\n    .qav-audio-note { font-size: 12.5px; color: var(--qav-muted-fg); display: flex; align-items: center; gap: 8px; padding: 6px 0; }\n\n    /* ---------- call list ---------- */\n    .qav-calls { display: flex; flex-direction: column; gap: 5px; max-height: 150px; overflow-y: auto; margin-bottom: 10px; padding-right: 3px; }\n    .qav-call { display: flex; align-items: center; gap: 9px; text-align: left; width: 100%; padding: 7px 9px; border: 1px solid var(--qav-border-soft); border-radius: 9px; background: var(--qav-bg); color: var(--qav-fg); }\n    .qav-call:hover { background: var(--qav-hover); }\n    .qav-call-sel { border-color: var(--qav-primary); background: color-mix(in srgb, var(--qav-primary) 10%, var(--qav-card)); }\n    .qav-call-ic { color: var(--qav-primary); font-size: 15px; flex-shrink: 0; }\n    .qav-call-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }\n    .qav-call-when { font-size: 12.5px; font-weight: 700; }\n    .qav-call-chips { display: flex; gap: 4px; flex-wrap: wrap; }\n    .qav-callchip { font-size: 9.5px; font-weight: 700; padding: 1px 6px; border-radius: 999px; }\n    .qav-call-norec { font-size: 10px; color: var(--qav-muted-fg); flex-shrink: 0; }\n\n    /* ---------- transcript ---------- */\n    .qav-transcript { max-height: 300px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px; }\n    .qav-seg {\n      display: flex; gap: 9px; padding: 7px 10px; border-radius: 9px; cursor: pointer;\n      border: 1px solid transparent; transition: background 120ms ease;\n    }\n    .qav-seg:hover { background: var(--qav-hover); }\n    .qav-seg.qav-seg-active { background: color-mix(in srgb, var(--qav-primary) 10%, var(--qav-card)); border-color: color-mix(in srgb, var(--qav-primary) 30%, transparent); }\n    .qav-seg-time { font-size: 10.5px; color: var(--qav-muted-fg); font-variant-numeric: tabular-nums; min-width: 38px; padding-top: 2px; }\n    .qav-seg-body { flex: 1; }\n    .qav-seg-who { font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 2px; }\n    .qav-seg-them .qav-seg-who { color: var(--qav-primary); }\n    .qav-seg-us .qav-seg-who { color: var(--qav-success); }\n    .qav-seg-text { font-size: 13.5px; line-height: 1.45; }\n\n    /* ---------- fields ---------- */\n    .qav-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 7px 14px; }\n    @media (max-width: 520px) { .qav-fields { grid-template-columns: 1fr; } }\n    .qav-field { position: relative; padding: 3px 26px 3px 0; border-radius: 6px; }\n    .qav-field:hover { background: var(--qav-hover); }\n    .qav-field-label { font-size: 10.5px; font-weight: 700; color: var(--qav-muted-fg); text-transform: uppercase; letter-spacing: 0.04em; }\n    .qav-field-value { font-size: 13.5px; font-weight: 600; margin-top: 1px; word-break: break-word; }\n    .qav-field-copy {\n      position: absolute; top: 50%; right: 2px; transform: translateY(-50%);\n      width: 24px; height: 24px; border-radius: 6px; border: 1px solid var(--qav-border);\n      background: var(--qav-card); color: var(--qav-fg2); font-size: 11px;\n      display: inline-flex; align-items: center; justify-content: center;\n      opacity: 0; transition: opacity 120ms ease;\n    }\n    .qav-field:hover .qav-field-copy { opacity: 1; }\n    .qav-field-copy:hover { color: var(--qav-primary); border-color: var(--qav-primary); }\n    .qav-field-copy.qav-copied { color: var(--qav-success); border-color: var(--qav-success); opacity: 1; }\n\n    /* ---------- files ---------- */\n    .qav-file-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--qav-border-soft); }\n    .qav-file-row:last-child { border-bottom: none; }\n    .qav-file-ic { width: 32px; height: 32px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: var(--qav-secondary); color: var(--qav-danger); font-size: 14px; }\n    .qav-file-name { flex: 1; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .qav-file-actions { display: flex; gap: 6px; }\n    .qav-file-row.qav-file-active { }\n    .qav-file-row.qav-file-active .qav-file-name { color: var(--qav-primary); }\n    .qav-view-btn.qav-active { background: var(--qav-primary); border-color: var(--qav-primary); color: var(--qav-primary-fg,#fff); }\n\n    /* ---------- contract preview column ---------- */\n    .qav-preview-panel { display: flex; flex-direction: column; padding-bottom: 12px; }\n    .qav-preview-body { flex: 1; }\n    /* Fills the viewport height so the full contract is readable; the PDF viewer scrolls pages internally. */\n    .qav-preview-frame { width: 100%; height: calc(100vh - 130px); min-height: 480px; border: 1px solid var(--qav-border-soft); border-radius: 10px; background: var(--qav-secondary); display: block; }\n    .qav-preview-empty { height: calc(100vh - 130px); min-height: 480px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; color: var(--qav-muted-fg); text-align: center; padding: 20px; border: 1px dashed var(--qav-border); border-radius: 10px; }\n    .qav-preview-empty .fas { font-size: 34px; opacity: 0.5; }\n\n    /* ---------- works ---------- */\n    .qav-work { display: flex; align-items: center; gap: 11px; padding: 8px 0; border-bottom: 1px solid var(--qav-border-soft); }\n    .qav-work:last-child { border-bottom: none; }\n    .qav-work-pinned {\n      display: flex; align-items: center; gap: 11px; padding: 11px 12px; margin-bottom: 12px;\n      border: 1px solid color-mix(in srgb, var(--qav-primary) 35%, transparent);\n      background: color-mix(in srgb, var(--qav-primary) 8%, var(--qav-card)); border-radius: 10px;\n    }\n    .qav-pin-badge { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; color: var(--qav-primary); display: flex; align-items: center; gap: 4px; margin-bottom: 2px; }\n    .qav-work-ic { width: 32px; height: 32px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--qav-primary) 12%, var(--qav-card)); color: var(--qav-primary); font-size: 13px; flex-shrink: 0; }\n    .qav-work-body { flex: 1; min-width: 0; }\n    .qav-work-name { font-size: 13.5px; font-weight: 700; }\n    .qav-work-sub { font-size: 11.5px; color: var(--qav-muted-fg); margin-top: 1px; }\n    .qav-work-sub.qav-ok { color: var(--qav-success); }\n    .qav-work-sub.qav-err { color: var(--qav-danger); }\n    .qav-run-btn { border: 1px solid var(--qav-border); background: var(--qav-card); color: var(--qav-fg); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; font-weight: 700; white-space: nowrap; }\n    .qav-run-btn:hover { background: var(--qav-hover); }\n    .qav-run-btn:disabled { opacity: 0.5; cursor: default; }\n\n    /* ---------- action bar ---------- */\n    .qav-actionbar {\n      background: var(--qav-card);\n      border: 1px solid var(--qav-border-soft); border-radius: 11px; padding: 12px 14px; margin-top: 12px;\n      display: flex; flex-direction: column; gap: 9px;\n    }\n    .qav-comment {\n      width: 100%; min-height: 52px; resize: vertical; padding: 9px 11px;\n      border: 1px solid var(--qav-border); border-radius: 10px; background: var(--qav-bg); outline: none; line-height: 1.4;\n    }\n    .qav-comment:focus { border-color: var(--qav-primary); }\n    .qav-comment.qav-required { border-color: var(--qav-danger); }\n    .qav-comment-hint { font-size: 11.5px; color: var(--qav-muted-fg); margin-top: -4px; min-height: 0; }\n    .qav-comment-hint:empty { display: none; }\n    .qav-comment-hint.qav-err { color: var(--qav-danger); }\n    .qav-btn-row { display: flex; gap: 9px; flex-wrap: wrap; }\n    .qav-decision {\n      flex: 1 1 auto; min-width: 120px;\n      display: inline-flex; align-items: center; justify-content: center; gap: 9px;\n      border: none; border-radius: 10px; padding: 12px 16px; font-size: 14px; font-weight: 800; color: #fff;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.10);\n    }\n    .qav-decision:disabled { opacity: 0.6; cursor: default; }\n    .qav-decision .fas { font-size: 15px; }\n    .qav-skip { background: var(--qav-card); color: var(--qav-fg2); border: 1px solid var(--qav-border); box-shadow: none; flex: 0 0 auto; min-width: 0; }\n    .qav-skip:hover { background: var(--qav-hover); }\n\n    /* ---------- states ---------- */\n    .qav-loading { display: flex; align-items: center; gap: 10px; color: var(--qav-muted-fg); font-size: 14px; padding: 60px 0; justify-content: center; }\n    .qav-spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--qav-border); border-top-color: var(--qav-primary); animation: qav-spin 0.8s linear infinite; }\n    @keyframes qav-spin { to { transform: rotate(360deg); } }\n    .qav-empty { text-align: center; padding: 70px 24px; color: var(--qav-fg2); }\n    .qav-empty .qav-big-ic { font-size: 46px; color: var(--qav-success); margin-bottom: 16px; }\n    .qav-empty-title { font-size: 20px; font-weight: 800; color: var(--qav-fg); margin-bottom: 6px; }\n    .qav-empty-sub { font-size: 14px; color: var(--qav-muted-fg); max-width: 420px; margin: 0 auto; line-height: 1.5; }\n    .qav-error-banner {\n      margin: 16px 18px; padding: 12px 16px; border-radius: var(--qav-radius);\n      background: color-mix(in srgb, var(--qav-danger) 8%, var(--qav-card));\n      border: 1px solid color-mix(in srgb, var(--qav-danger) 30%, transparent);\n      color: var(--qav-danger); font-size: 13.5px;\n    }\n\n    /* ---------- toast ---------- */\n    .qav-toast {\n      position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 90;\n      background: var(--qav-fg); color: var(--qav-card); font-size: 13.5px; font-weight: 600;\n      padding: 10px 18px; border-radius: 999px; box-shadow: 0 6px 24px rgba(0,0,0,0.25);\n      opacity: 0; pointer-events: none; transition: opacity 200ms ease; max-width: 80vw; text-align: center;\n    }\n    .qav-toast.qav-show { opacity: 1; }\n\n    /* ---------- config / settings ---------- */\n    .qav-modal-backdrop { position: fixed; inset: 0; z-index: 70; background: rgba(0,0,0,0.42); display: none; align-items: flex-start; justify-content: center; overflow-y: auto; padding: 40px 16px; }\n    .qav-modal-backdrop.qav-open { display: flex; }\n    .qav-modal { background: var(--qav-card); color: var(--qav-fg); border-radius: 14px; width: 640px; max-width: 100%; box-shadow: 0 16px 60px rgba(0,0,0,0.3); }\n    .qav-modal-head { display: flex; align-items: center; gap: 10px; padding: 20px 24px 0; }\n    .qav-modal-title { font-size: 22px; font-weight: 800; flex: 1; }\n    .qav-modal-body { padding: 8px 24px 4px; }\n    .qav-modal-foot { display: flex; flex-direction: row-reverse; gap: 10px; padding: 16px 24px 22px; }\n    .qav-step { padding: 18px 0; border-top: 1px solid var(--qav-border-soft); }\n    .qav-step:first-child { border-top: none; }\n    .qav-step-h { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }\n    .qav-step-num { width: 24px; height: 24px; border-radius: 50%; background: var(--qav-primary); color: var(--qav-primary-fg,#fff); font-size: 12px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }\n    .qav-step-title { font-size: 15px; font-weight: 800; }\n    .qav-step-desc { font-size: 12.5px; color: var(--qav-muted-fg); margin: 0 0 12px 34px; line-height: 1.45; }\n    .qav-step-inner { margin-left: 34px; }\n\n    .qav-tagpick { display: flex; gap: 7px; flex-wrap: wrap; }\n    .qav-tagopt {\n      display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 999px;\n      border: 1.5px solid var(--qav-border); background: var(--qav-card); font-size: 12.5px; font-weight: 700; color: var(--qav-fg2);\n    }\n    .qav-tagopt .qav-dot { width: 9px; height: 9px; border-radius: 50%; }\n    .qav-tagopt.qav-picked { border-color: var(--qav-primary); background: color-mix(in srgb, var(--qav-primary) 10%, var(--qav-card)); color: var(--qav-fg); }\n    .qav-tagopt.qav-none { font-style: italic; }\n\n    .qav-scn-card { border: 1px solid var(--qav-border-soft); border-radius: 11px; padding: 13px 14px; margin-bottom: 12px; background: var(--qav-bg); }\n    .qav-scn-top { display: flex; align-items: center; gap: 10px; }\n    .qav-scn-icpick { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 9px; color: #fff; font-size: 16px; flex-shrink: 0; }\n    .qav-scn-label-in { flex: 1; padding: 8px 11px; border: 1px solid var(--qav-border); border-radius: 8px; background: var(--qav-card); }\n    .qav-scn-del { border: none; background: transparent; color: var(--qav-muted-fg); font-size: 15px; padding: 6px; }\n    .qav-scn-del:hover { color: var(--qav-danger); }\n    .qav-scn-sec { margin-top: 11px; }\n    .qav-scn-sec-l { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--qav-muted-fg); margin-bottom: 6px; }\n    .qav-mini-icons { display: flex; gap: 5px; flex-wrap: wrap; }\n    .qav-mini-icons button { width: 30px; height: 30px; border-radius: 7px; border: 1px solid var(--qav-border); background: var(--qav-card); color: var(--qav-fg2); font-size: 13px; }\n    .qav-mini-icons button.qav-active { background: var(--qav-primary); border-color: var(--qav-primary); color: var(--qav-primary-fg,#fff); }\n    .qav-swatches { display: flex; gap: 6px; flex-wrap: wrap; }\n    .qav-swatch { width: 24px; height: 24px; border-radius: 50%; border: 2px solid transparent; }\n    .qav-swatch.qav-active { border-color: var(--qav-fg); }\n    .qav-toggle-row { display: flex; align-items: center; gap: 10px; margin-top: 11px; font-size: 13px; font-weight: 600; cursor: pointer; }\n    .qav-toggle { width: 40px; height: 22px; border-radius: 999px; background: var(--qav-border); position: relative; transition: background 150ms; flex-shrink: 0; }\n    .qav-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left 150ms; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }\n    .qav-toggle.qav-on { background: var(--qav-primary); }\n    .qav-toggle.qav-on::after { left: 20px; }\n    .qav-add-scn { display: inline-flex; align-items: center; gap: 8px; border: 1.5px dashed var(--qav-border); background: transparent; color: var(--qav-primary); border-radius: 10px; padding: 10px 14px; font-size: 13px; font-weight: 700; width: 100%; justify-content: center; }\n    .qav-add-scn:hover { background: var(--qav-hover); }\n    .qav-primary-btn { background: var(--qav-primary); border: 1px solid var(--qav-primary); color: var(--qav-primary-fg, #fff); border-radius: 10px; padding: 11px 20px; font-size: 14px; font-weight: 800; }\n    .qav-primary-btn:disabled { opacity: 0.6; cursor: default; }\n    .qav-ghost-btn { background: var(--qav-card); border: 1px solid var(--qav-border); color: var(--qav-fg); border-radius: 10px; padding: 11px 18px; font-size: 14px; font-weight: 700; }\n    .qav-cfg-note { font-size: 12.5px; color: var(--qav-muted-fg); background: color-mix(in srgb, var(--qav-primary) 7%, var(--qav-card)); border-radius: 8px; padding: 9px 12px; margin-left: 34px; line-height: 1.4; }\n    .qav-wcfg-row { display: flex; align-items: center; gap: 10px; padding: 8px 11px; border: 1px solid var(--qav-border-soft); border-radius: 9px; margin-bottom: 7px; background: var(--qav-card); }\n    .qav-wcfg-ic { width: 28px; height: 28px; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--qav-primary) 12%, var(--qav-card)); color: var(--qav-primary); font-size: 12px; flex-shrink: 0; }\n    .qav-wcfg-name { flex: 1; font-size: 13px; font-weight: 700; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .qav-wcfg-pin { width: 30px; height: 30px; border-radius: 7px; border: 1px solid var(--qav-border); background: var(--qav-card); color: var(--qav-muted-fg); font-size: 12px; flex-shrink: 0; }\n    .qav-wcfg-pin.qav-active { background: var(--qav-primary); border-color: var(--qav-primary); color: var(--qav-primary-fg,#fff); }\n    .qav-wcfg-row.qav-hidden { opacity: 0.5; }\n    .qav-dur-row { display: flex; gap: 12px; margin-top: 10px; }\n    .qav-dur-row label { flex: 1; display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--qav-muted-fg); }\n    .qav-dur-in { padding: 7px 10px; border: 1px solid var(--qav-border); border-radius: 8px; background: var(--qav-card); }\n    .qav-dur-in:focus { border-color: var(--qav-primary); outline: none; }\n\n    /* ---------- settings accordion cards ---------- */\n    .qav-cfg-card { border: 1px solid var(--qav-border-soft); border-radius: 12px; margin-bottom: 10px; overflow: hidden; background: var(--qav-card); }\n    .qav-cfg-card.qav-open { border-color: color-mix(in srgb, var(--qav-primary) 35%, var(--qav-border)); box-shadow: 0 2px 12px color-mix(in srgb, var(--qav-primary) 8%, transparent); }\n    .qav-cfg-head { display: flex; align-items: center; gap: 12px; padding: 13px 15px; cursor: pointer; }\n    .qav-cfg-head:hover { background: var(--qav-hover); }\n    .qav-cfg-ic { width: 34px; height: 34px; border-radius: 9px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--qav-primary) 12%, var(--qav-card)); color: var(--qav-primary); font-size: 15px; flex-shrink: 0; }\n    .qav-cfg-htext { flex: 1; min-width: 0; }\n    .qav-cfg-title { font-size: 14.5px; font-weight: 800; display: block; }\n    .qav-cfg-sum { font-size: 12px; color: var(--qav-muted-fg); margin-top: 1px; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n    .qav-cfg-chev { color: var(--qav-muted-fg); transition: transform 0.18s ease; flex-shrink: 0; font-size: 13px; }\n    .qav-cfg-card.qav-open .qav-cfg-chev { transform: rotate(180deg); }\n    .qav-cfg-body { padding: 0 15px 15px; }\n    .qav-cfg-desc { font-size: 12.5px; color: var(--qav-muted-fg); line-height: 1.45; margin: 2px 0 12px; }\n    .qav-ord-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 15px; height: 15px; border-radius: 7px; background: var(--qav-primary); color: var(--qav-primary-fg, #fff); font-size: 9.5px; font-weight: 800; margin-right: 5px; padding: 0 4px; }\n    .qav-colseg-wrap { display: inline-flex; gap: 3px; background: var(--qav-secondary); border-radius: 8px; padding: 2px; }\n    .qav-colseg { width: 26px; height: 24px; border: none; background: transparent; color: var(--qav-fg2); font-size: 12px; font-weight: 800; border-radius: 6px; }\n    .qav-colseg.qav-active { background: var(--qav-primary); color: var(--qav-primary-fg, #fff); }\n    .qav-colseg:disabled { opacity: 0.4; }\n  </style>\n\n  <div class=\"qav-topbar\">\n    <div class=\"qav-remaining\" id=\"qav-remaining\" style=\"display:none;\"><i class=\"fas fa-layer-group\"></i> <span class=\"qav-num\" id=\"qav-remaining-num\">0</span> kvar att granska</div>\n    <div class=\"qav-topbar-spacer\"></div>\n    <button class=\"qav-icon-btn\" id=\"qav-reload\" title=\"Uppdatera\"><i class=\"fas fa-sync-alt\"></i></button>\n    <button class=\"qav-icon-btn\" id=\"qav-customize-btn\" title=\"Anpassa min vy\"><i class=\"fas fa-columns\"></i></button>\n    <button class=\"qav-icon-btn\" id=\"qav-config-btn\" title=\"Inställningar\" style=\"display:none;\"><i class=\"fas fa-cog\"></i></button>\n  </div>\n\n  <div id=\"qav-stage\">\n    <div class=\"qav-loading\"><div class=\"qav-spinner\"></div> Laddar…</div>\n  </div>\n\n  <div class=\"qav-modal-backdrop\" id=\"qav-config-backdrop\">\n    <div class=\"qav-modal\" id=\"qav-config-modal\"></div>\n  </div>\n\n  <div class=\"qav-modal-backdrop\" id=\"qav-cust-backdrop\">\n    <div class=\"qav-modal\" id=\"qav-cust-modal\"></div>\n  </div>\n\n  <div class=\"qav-toast\" id=\"qav-toast\"></div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n\n      if (window.__qavCleanup) { try { window.__qavCleanup(); } catch (e) {} }\n\n      var COLORS = ['#2E7D32', '#E53935', '#1565C0', '#0288D1', '#00897B', '#F9A825', '#EF6C00', '#8E24AA', '#5E35B1', '#546E7A'];\n      var ICONS = ['check', 'times', 'thumbs-up', 'thumbs-down', 'flag', 'star', 'phone', 'redo', 'pause', 'exclamation-triangle', 'ban', 'user-clock', 'clipboard-check', 'comment-dots', 'headset', 'undo'];\n\n      var state = {\n        me: null,\n        isAdmin: false,\n        viewId: null,\n        config: null,\n        orderTags: [],\n        orderFields: [],\n        works: [],\n        projects: [],\n        users: [],\n        queue: [],\n        remaining: 0,\n        current: null,        /* order being reviewed */\n        calls: null,          /* list of calls for the order (null = loading) */\n        selectedCallId: null,\n        callTags: [],\n        media: null,          /* { callId, transcript, status, audioUrl } for selected call */\n        audioCtx: null,       /* shared Web Audio context for the visualiser */\n        viz: null,            /* { raf, source, analyser, refreshColors } */\n        preview: null,        /* { files, index, blobUrl, loading, error } */\n        loading: true,\n        deciding: false,\n        fatalError: null,\n        objectUrls: [],\n        cfgDraft: null,\n        cfgSaving: false,\n        cfgOpenCard: 'queue',\n        userPrefs: { blocks: {} },  /* per-user layout, from own reference */\n        custDraft: null,\n        custSaving: false\n      };\n\n      /* ---------- utils ---------- */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n      function el(id) { return document.getElementById(id); }\n      function esc(s) {\n        return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n\n      function api(method, path, params, body) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null && params[k] !== ''; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += '?' + qs;\n        }\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }\n        return fetch(url, opts).then(function (r) {\n          if (!r.ok) { var e = new Error('HTTP ' + r.status); e.status = r.status; throw e; }\n          return r.json().catch(function () { return null; });\n        });\n      }\n\n      function apiBlob(path) {\n        return fetch(API + path, { headers: { 'Authorization': 'Bearer ' + token() } }).then(function (r) {\n          if (!r.ok) { var e = new Error('HTTP ' + r.status); e.status = r.status; throw e; }\n          return r.blob();\n        });\n      }\n\n      var toastTimer = null;\n      function toast(text) {\n        var t = el('qav-toast'); if (!t) return;\n        t.textContent = text; t.className = 'qav-toast qav-show';\n        clearTimeout(toastTimer);\n        toastTimer = setTimeout(function () { t.className = 'qav-toast'; }, 3400);\n      }\n\n      function fmtTime(sec) {\n        sec = Math.max(0, Math.floor(sec || 0));\n        var m = Math.floor(sec / 60), s = sec % 60;\n        return m + ':' + (s < 10 ? '0' : '') + s;\n      }\n      function revokeUrls() {\n        state.objectUrls.forEach(function (u) { try { URL.revokeObjectURL(u); } catch (e) {} });\n        state.objectUrls = [];\n      }\n\n      /* ---------- config helpers ---------- */\n      function defaultConfig() {\n        return {\n          configured: false,\n          sourceTagIds: [],\n          reviewedTagIds: [],\n          removeSourceOnDecision: false,\n          matchByPhone: true,\n          projectIds: [],\n          nameFieldIds: [],\n          phoneFieldId: null,\n          callTagIds: [],\n          callFilter: {},\n          hiddenWorkIds: [],\n          pinnedWorkId: null,\n          scenarios: [\n            { id: 'approve', label: 'Godkänn', icon: 'check', color: '#2E7D32', addTagIds: [], removeTagIds: [], requireComment: false },\n            { id: 'reject', label: 'Neka', icon: 'times', color: '#E53935', addTagIds: [], removeTagIds: [], requireComment: true }\n          ]\n        };\n      }\n      /* Tags that mark an order as reviewed: explicit config, else union of all scenario add-tags. */\n      function reviewedTagIds() {\n        var c = state.config || {};\n        if (c.reviewedTagIds && c.reviewedTagIds.length) return c.reviewedTagIds;\n        var set = {};\n        (c.scenarios || []).forEach(function (s) { (s.addTagIds || []).forEach(function (t) { set[t] = true; }); });\n        return Object.keys(set);\n      }\n      function tagById(id) {\n        for (var i = 0; i < state.orderTags.length; i++) if (state.orderTags[i].id === id) return state.orderTags[i];\n        return null;\n      }\n      function tagName(id) { var t = tagById(id); return t ? t.name : '?'; }\n      function fieldLabel(id) {\n        for (var i = 0; i < state.orderFields.length; i++) if (state.orderFields[i].id === id) return state.orderFields[i].label;\n        return null;\n      }\n      function userName(id) {\n        for (var i = 0; i < state.users.length; i++) if (state.users[i].id === id) return state.users[i].name || state.users[i].username;\n        return null;\n      }\n      function projectName(id) {\n        for (var i = 0; i < state.projects.length; i++) if (state.projects[i].id === id) return state.projects[i].name;\n        return null;\n      }\n\n      /* Best-effort display name / phone from order fields by label. */\n      var NAME_LABELS = ['fullständigt namn', 'namn', 'kontaktnamn', 'full name', 'name'];\n      var FIRST_LABELS = ['förnamn', 'first name'];\n      var LAST_LABELS = ['efternamn', 'last name', 'surname'];\n      var PHONE_LABELS = ['telefonnummer', 'telefon', 'mobil', 'mobilnummer', 'phone', 'mobile'];\n      function fieldValByLabels(order, labels) {\n        for (var i = 0; i < (order.fields || []).length; i++) {\n          var lbl = (fieldLabel(order.fields[i].fieldId) || '').toLowerCase().trim();\n          if (labels.indexOf(lbl) !== -1 && order.fields[i].value) return order.fields[i].value;\n        }\n        return '';\n      }\n      function fieldValById(order, fieldId) {\n        for (var i = 0; i < (order.fields || []).length; i++) {\n          if (order.fields[i].fieldId === fieldId && order.fields[i].value) return order.fields[i].value;\n        }\n        return '';\n      }\n      function orderName(order) {\n        /* Admin-configured name fields take precedence (joined in order); else label-match. */\n        var cfgIds = (state.config && state.config.nameFieldIds) || [];\n        if (cfgIds.length) {\n          var parts = cfgIds.map(function (id) { return fieldValById(order, id); }).filter(function (v) { return v; });\n          if (parts.length) return parts.join(' ');\n        }\n        var full = fieldValByLabels(order, NAME_LABELS);\n        if (full) return full;\n        var n = (fieldValByLabels(order, FIRST_LABELS) + ' ' + fieldValByLabels(order, LAST_LABELS)).trim();\n        return n || ('Order #' + (order.serialId != null ? order.serialId : '?'));\n      }\n      function orderPhone(order) {\n        var cfgId = state.config && state.config.phoneFieldId;\n        if (cfgId) { var v = fieldValById(order, cfgId); if (v) return v; }\n        return fieldValByLabels(order, PHONE_LABELS);\n      }\n\n      /* ---------- data loading ---------- */\n      function parseViewId() {\n        var m = /\\/views\\/([a-f0-9]{24})/i.exec(location.pathname) || /\\/views\\/([a-f0-9]{24})/i.exec(location.href);\n        return m ? m[1] : null;\n      }\n\n      function boot() {\n        state.loading = true; state.fatalError = null; render();\n        state.viewId = parseViewId();\n        Promise.all([\n          api('GET', '/users/users-v1/me'),\n          api('GET', '/orders/tags-v1'),\n          api('GET', '/orders/fields-v1'),\n          api('GET', '/orders/after-order-work-v1').catch(function () { return []; }),\n          api('GET', '/users/users-v1').catch(function () { return []; }),\n          api('GET', '/dial/tags-v1').catch(function () { return []; }),\n          loadProjects(),\n          loadConfig()\n        ]).then(function (r) {\n          state.me = r[0];\n          state.isAdmin = r[0] && r[0].type === 'admin';\n          state.orderTags = Array.isArray(r[1]) ? r[1] : ((r[1] && r[1].tags) || []);\n          state.orderFields = Array.isArray(r[2]) ? r[2] : ((r[2] && r[2].fields) || []);\n          state.works = (Array.isArray(r[3]) ? r[3] : ((r[3] && r[3].actions) || [])).filter(function (w) { return w && w.isEnabled && !w.isAutomatic; });\n          state.users = Array.isArray(r[4]) ? r[4] : ((r[4] && r[4].users) || []);\n          state.callTags = Array.isArray(r[5]) ? r[5] : ((r[5] && r[5].tags) || []);\n          var refPrefs = state.me && state.me.reference && state.me.reference.qaViewPrefs;\n          state.userPrefs = (refPrefs && state.viewId && refPrefs[state.viewId]) || { blocks: {} };\n          if (!state.userPrefs.blocks) state.userPrefs.blocks = {};\n          var cfgBtn = el('qav-config-btn');\n          if (cfgBtn) cfgBtn.style.display = state.isAdmin ? '' : 'none';\n          if (!state.config || !state.config.configured || !(state.config.sourceTagIds || []).length) {\n            state.loading = false; render(); return;\n          }\n          return loadQueue();\n        }).catch(function (err) {\n          state.loading = false;\n          state.fatalError = err && err.status === 403\n            ? 'Du saknar behörighet att läsa ordrar i denna organisation.'\n            : 'Kunde inte ladda data. Försök igen.';\n          render();\n        });\n      }\n\n      function loadProjects() {\n        return api('GET', '/users/projects-v1').then(function (r) {\n          state.projects = Array.isArray(r) ? r : ((r && r.projects) || []);\n        }).catch(function () { state.projects = []; });\n      }\n\n      function loadConfig() {\n        if (!state.viewId) { state.config = defaultConfig(); return Promise.resolve(); }\n        return api('GET', '/users/views-v1').then(function (views) {\n          var list = Array.isArray(views) ? views : ((views && views.views) || []);\n          var mine = null;\n          for (var i = 0; i < list.length; i++) if (list[i].id === state.viewId) { mine = list[i]; break; }\n          var ps = (mine && mine.presetState) || null;\n          state.config = (ps && ps.configured) ? mergeConfig(ps) : defaultConfig();\n        }).catch(function () { state.config = defaultConfig(); });\n      }\n      function mergeConfig(ps) {\n        var d = defaultConfig();\n        return {\n          configured: true,\n          sourceTagIds: ps.sourceTagIds || [],\n          reviewedTagIds: ps.reviewedTagIds || [],\n          removeSourceOnDecision: !!ps.removeSourceOnDecision,\n          matchByPhone: ps.matchByPhone !== false,\n          projectIds: ps.projectIds || [],\n          nameFieldIds: ps.nameFieldIds || [],\n          phoneFieldId: ps.phoneFieldId || null,\n          callTagIds: ps.callTagIds || [],\n          callFilter: ps.callFilter || {},\n          hiddenWorkIds: ps.hiddenWorkIds || [],\n          pinnedWorkId: ps.pinnedWorkId || null,\n          scenarios: (ps.scenarios && ps.scenarios.length) ? ps.scenarios : d.scenarios\n        };\n      }\n\n      function queueParams() {\n        var c = state.config;\n        var p = {\n          anyTagId: (c.sourceTagIds || []).join(','),\n          notAnyTagId: reviewedTagIds().join(','),\n          isTest: 'false',\n          sortBy: 'serialId',\n          sortOrder: 'desc'\n        };\n        if (c.projectIds && c.projectIds.length) p.projectIds = c.projectIds.join(',');\n        return p;\n      }\n\n      function loadQueue() {\n        state.loading = true; render();\n        var listParams = {}; var qp = queueParams();\n        Object.keys(qp).forEach(function (k) { listParams[k] = qp[k]; });\n        listParams.count = 25; listParams.offset = 0;\n        return Promise.all([\n          api('GET', '/orders/orders-v3', listParams),\n          api('GET', '/orders/orders-v2/count', qp).catch(function () { return null; })\n        ]).then(function (r) {\n          state.queue = Array.isArray(r[0]) ? r[0] : ((r[0] && r[0].orders) || []);\n          state.remaining = (r[1] && typeof r[1].count === 'number') ? r[1].count : state.queue.length;\n          state.loading = false;\n          setCurrent(state.queue[0] || null);\n        }).catch(function (err) {\n          state.loading = false;\n          state.fatalError = err && err.status === 403 ? 'Du saknar behörighet att läsa ordrar.' : 'Kunde inte ladda granskningskön.';\n          render();\n        });\n      }\n\n      function setCurrent(order) {\n        revokeUrls();\n        state.current = order;\n        state.media = null;\n        state.preview = null;\n        state.calls = null; state.selectedCallId = null;\n        if (order) setupPreview(order);\n        render();\n        if (order) loadCalls(order);\n      }\n\n      /* ---------- contract preview (dedicated column) ---------- */\n      function setupPreview(order) {\n        var files = (order.files || []).map(function (f) {\n          return {\n            name: f.name || 'Fil', url: filesV1Url(f.uri), mimetype: f.mimetype || '',\n            isPdf: (f.mimetype === 'application/pdf') || /\\.pdf$/i.test(f.name || ''),\n            isImage: /^image\\//.test(f.mimetype || '') || /\\.(png|jpe?g|gif|webp|svg)$/i.test(f.name || '')\n          };\n        });\n        var idx = -1;\n        for (var i = 0; i < files.length; i++) { if (files[i].isPdf) { idx = i; break; } }\n        if (idx === -1 && files.length) idx = 0;\n        state.preview = { files: files, index: idx, blobUrl: null, loading: false, error: false };\n        if (idx >= 0) loadPreview(order, idx);\n      }\n      function loadPreview(order, idx) {\n        var pv = state.preview; if (!pv) return;\n        pv.index = idx; pv.blobUrl = null; pv.error = false; pv.loading = false;\n        var f = pv.files[idx];\n        if (!f) { renderPreviewPane(); return; }\n        if (!f.isPdf && !f.isImage) { renderPreviewPane(); return; }\n        pv.loading = true; renderPreviewPane();\n        fetch(f.url, { headers: { 'Authorization': 'Bearer ' + token() } })\n          .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.blob(); })\n          .then(function (blob) {\n            if (state.current !== order || state.preview !== pv || pv.index !== idx) return;\n            var typed = f.isPdf ? (blob.type === 'application/pdf' ? blob : new Blob([blob], { type: 'application/pdf' })) : blob;\n            var u = URL.createObjectURL(typed);\n            state.objectUrls.push(u);\n            pv.blobUrl = u; pv.loading = false; renderPreviewPane();\n          })\n          .catch(function () {\n            if (state.current !== order || state.preview !== pv) return;\n            pv.loading = false; pv.error = true; renderPreviewPane();\n          });\n      }\n      function renderPreviewPane() {\n        var body = el('qav-preview-body'); if (!body) return;\n        var nameEl = el('qav-preview-name');\n        var pv = state.preview;\n        if (!pv || !pv.files.length) {\n          if (nameEl) nameEl.textContent = '';\n          body.innerHTML = '<div class=\"qav-preview-empty\"><i class=\"fas fa-file-contract\"></i><div>Inga filer bifogade på ordern.</div></div>';\n          return;\n        }\n        var f = pv.files[pv.index] || pv.files[0];\n        if (nameEl) nameEl.textContent = f.name;\n        if (pv.loading) { body.innerHTML = '<div class=\"qav-preview-empty\"><div class=\"qav-spinner\"></div><div>Laddar ' + esc(f.name) + '…</div></div>'; return; }\n        if (pv.error) { body.innerHTML = '<div class=\"qav-preview-empty\"><i class=\"fas fa-exclamation-triangle\"></i><div>Kunde inte ladda filen.</div><a class=\"qav-run-btn\" href=\"' + esc(f.url) + '\" target=\"_blank\" rel=\"noopener\"><i class=\"fas fa-external-link-alt\"></i> Öppna i ny flik</a></div>'; return; }\n        if (f.isPdf && pv.blobUrl) { body.innerHTML = '<iframe class=\"qav-preview-frame\" src=\"' + esc(pv.blobUrl) + '\"></iframe>'; return; }\n        if (f.isImage && pv.blobUrl) { body.innerHTML = '<div style=\"text-align:center;\"><img src=\"' + esc(pv.blobUrl) + '\" style=\"max-width:100%;border-radius:10px;\"></div>'; return; }\n        body.innerHTML = '<div class=\"qav-preview-empty\"><i class=\"fas fa-file\"></i><div>' + esc(f.name) + ' kan inte förhandsgranskas.</div><a class=\"qav-run-btn\" href=\"' + esc(f.url) + '\" target=\"_blank\" rel=\"noopener\"><i class=\"fas fa-external-link-alt\"></i> Öppna i ny flik</a></div>';\n      }\n\n      /* ---------- calls, recording & transcript ---------- */\n      function callTagById(id) {\n        for (var i = 0; i < state.callTags.length; i++) if (state.callTags[i].id === id) return state.callTags[i];\n        return null;\n      }\n\n      function loadCalls(order) {\n        var reqOrder = order;\n        state.calls = null; state.selectedCallId = null; state.media = null;\n        renderCallsPanel(); renderMediaPanels();\n        fetchCalls(order).then(function (calls) {\n          if (state.current !== reqOrder) return;\n          state.calls = calls;\n          var def = null, i;\n          if (order.callId) for (i = 0; i < calls.length; i++) if (calls[i].id === order.callId) { def = calls[i]; break; }\n          if (!def) for (i = 0; i < calls.length; i++) if (calls[i].isRecordingAvailable !== false) { def = calls[i]; break; }\n          if (!def) def = calls[0] || null;\n          renderCallsPanel();\n          if (def) selectCall(order, def.id);\n          else { state.media = { callId: null }; renderMediaPanels(); }\n        });\n      }\n\n      function fetchCalls(order) {\n        var params = { sortBy: 'connectingAt', sortOrder: 'desc', count: 50 };\n        var scoped = false;\n        if (order.contactId) { params.contactIds = order.contactId; scoped = true; }\n        else if (state.config.matchByPhone) {\n          var phone = orderPhone(order);\n          if (phone) { params.recipients = phone; scoped = true; }\n        }\n        /* Apply the admin's call filters (durations in the model are milliseconds). */\n        var cf = state.config.callFilter || {};\n        if (cf.isConnected != null) params.isConnected = cf.isConnected;\n        if (cf.isContactAnswer != null) params.isContactAnswer = cf.isContactAnswer;\n        if (cf.voicemail != null) params.voicemail = cf.voicemail;\n        if (cf.isInbound != null) params.isInbound = cf.isInbound;\n        if (cf.isRecordingAvailable != null) params.isRecordingAvailable = cf.isRecordingAvailable;\n        if (cf.isLatest != null) params.isLatest = cf.isLatest;\n        if (cf.minDurationSec) params.totalDurationMin = Math.round(cf.minDurationSec * 1000);\n        if (cf.maxDurationSec) params.totalDurationMax = Math.round(cf.maxDurationSec * 1000);\n        var callTagIds = (state.config && state.config.callTagIds) || [];\n        var listP = scoped\n          ? api('GET', '/dial/calls-v1', params).then(function (r) { return (r && r.calls) || (Array.isArray(r) ? r : []); }).catch(function () { return []; })\n          : Promise.resolve([]);\n        return listP.then(function (calls) {\n          if (callTagIds.length) {\n            calls = calls.filter(function (c) {\n              return c.id === order.callId || (c.tagIds || []).some(function (t) { return callTagIds.indexOf(t) !== -1; });\n            });\n          }\n          /* Always include the order's directly linked call, even if the scope query missed it. */\n          if (order.callId && !calls.some(function (c) { return c.id === order.callId; })) {\n            return api('GET', '/dial/calls-v1', { ids: order.callId }).then(function (r) {\n              var extra = (r && r.calls) || (Array.isArray(r) ? r : []);\n              return extra.concat(calls);\n            }).catch(function () { return calls; });\n          }\n          return calls;\n        });\n      }\n\n      function selectCall(order, callId) {\n        state.selectedCallId = callId;\n        var callObj = (state.calls || []).filter(function (c) { return c.id === callId; })[0];\n        var hasRec = !callObj || callObj.isRecordingAvailable !== false;\n        state.media = { callId: callId, transcript: null, status: null, audioUrl: null, waveform: null, audioLoading: hasRec, audioError: false, noRecording: !hasRec };\n        renderCallsPanel(); renderMediaPanels();\n        if (hasRec) {\n          apiBlob('/dial/calls-v1/' + callId + '/recording.wav').then(function (blob) {\n            if (state.current !== order || state.selectedCallId !== callId) return;\n            var url = URL.createObjectURL(blob);\n            state.objectUrls.push(url);\n            state.media.audioUrl = url; state.media.audioLoading = false;\n            renderMediaPanels();\n            /* Decode the whole file into a static waveform (peaks per position). */\n            var ctx = getAudioCtx();\n            if (ctx && blob.arrayBuffer) {\n              blob.arrayBuffer().then(function (buf) { return ctx.decodeAudioData(buf); }).then(function (ab) {\n                if (state.current !== order || state.selectedCallId !== callId || !state.media) return;\n                state.media.waveform = computeWaveform(ab, 72);\n              }).catch(function () {});\n            }\n          }).catch(function () {\n            if (state.current !== order || state.selectedCallId !== callId) return;\n            state.media.audioLoading = false; state.media.audioError = true; renderMediaPanels();\n          });\n        }\n        api('GET', '/dial/call-insights-v1/' + callId + '/transcript').then(function (r) {\n          if (state.current !== order || state.selectedCallId !== callId) return;\n          state.media.transcript = (r && r.transcript) || [];\n          state.media.status = r && r.status;\n          renderMediaPanels();\n        }).catch(function () {});\n      }\n\n      /* ---------- render ---------- */\n      function render() {\n        var stage = el('qav-stage'); if (!stage) return;\n        var rem = el('qav-remaining'), remNum = el('qav-remaining-num');\n\n        if (state.loading) {\n          stage.innerHTML = '<div class=\"qav-loading\"><div class=\"qav-spinner\"></div> Laddar…</div>';\n          if (rem) rem.style.display = 'none';\n          return;\n        }\n        if (state.fatalError) {\n          stage.innerHTML = '<div class=\"qav-error-banner\"><i class=\"fas fa-exclamation-triangle\"></i> ' + esc(state.fatalError) + '</div>';\n          if (rem) rem.style.display = 'none';\n          return;\n        }\n        if (!state.config || !state.config.configured || !(state.config.sourceTagIds || []).length) {\n          if (rem) rem.style.display = 'none';\n          stage.innerHTML = renderNotConfigured();\n          return;\n        }\n        if (rem) { rem.style.display = ''; }\n        if (remNum) remNum.textContent = String(state.remaining);\n\n        if (!state.current) { stage.innerHTML = renderAllDone(); return; }\n        stage.innerHTML = renderReview(state.current);\n        renderCallsPanel();\n        renderMediaPanels();\n        renderPreviewPane();\n      }\n\n      function renderNotConfigured() {\n        if (state.isAdmin) {\n          return '<div class=\"qav-empty\">' +\n            '<div class=\"qav-big-ic\" style=\"color:var(--qav-primary)\"><i class=\"fas fa-sliders-h\"></i></div>' +\n            '<div class=\"qav-empty-title\">Konfigurera kvalitetssäkringen</div>' +\n            '<div class=\"qav-empty-sub\">Välj vilken ordertagg som fyller granskningskön och vilka knappar granskaren ska ha. Allt sparas på vyn.</div>' +\n            '<div style=\"margin-top:20px;\"><button class=\"qav-primary-btn\" id=\"qav-open-config-cta\"><i class=\"fas fa-cog\"></i> Öppna inställningar</button></div>' +\n            '</div>';\n        }\n        return '<div class=\"qav-empty\">' +\n          '<div class=\"qav-big-ic\" style=\"color:var(--qav-muted-fg)\"><i class=\"fas fa-hourglass-half\"></i></div>' +\n          '<div class=\"qav-empty-title\">Vyn är inte konfigurerad än</div>' +\n          '<div class=\"qav-empty-sub\">Be en administratör att ställa in kvalitetssäkringsvyn under inställningar.</div>' +\n          '</div>';\n      }\n\n      function renderAllDone() {\n        return '<div class=\"qav-empty\">' +\n          '<div class=\"qav-big-ic\"><i class=\"fas fa-check-circle\"></i></div>' +\n          '<div class=\"qav-empty-title\">Allt granskat</div>' +\n          '<div class=\"qav-empty-sub\">Det finns inga ordrar kvar att kvalitetssäkra just nu. Bra jobbat!</div>' +\n          '<div style=\"margin-top:20px;\"><button class=\"qav-tool-btn\" id=\"qav-done-reload\"><i class=\"fas fa-sync-alt\"></i> Leta efter fler</button></div>' +\n          '</div>';\n      }\n\n      function renderReview(order) {\n        var seller = userName(order.userId);\n        var proj = order.projectId ? projectName(order.projectId) : null;\n        var date = order.date ? new Date(order.date).toLocaleString('sv-SE', { dateStyle: 'medium', timeStyle: 'short' }) : '';\n        var chips = (order.tagIds || []).map(function (tid) {\n          var t = tagById(tid);\n          if (!t) return '';\n          var col = t.color || '#9E9E9E';\n          return '<span class=\"qav-chip\" style=\"background:color-mix(in srgb,' + esc(col) + ' 16%, transparent);color:' + esc(col) + ';\">' + esc(t.name) + '</span>';\n        }).join('');\n\n        var headerHtml = '<div class=\"qav-panel\">' +\n          '<div class=\"qav-order-head\">' +\n          '<div class=\"qav-order-serial\">#' + esc(order.serialId) + '</div>' +\n          '<div style=\"flex:1;min-width:0;\">' +\n          '<div class=\"qav-order-name\">' + esc(orderName(order)) + '</div>' +\n          '<div class=\"qav-order-sub\">' +\n          (orderPhone(order) ? '<span><i class=\"fas fa-phone-alt\"></i>' + esc(orderPhone(order)) + '</span>' : '') +\n          (seller ? '<span><i class=\"fas fa-user-headset\"></i>' + esc(seller) + '</span>' : '') +\n          (proj ? '<span><i class=\"fas fa-folder\"></i>' + esc(proj) + '</span>' : '') +\n          (date ? '<span><i class=\"fas fa-clock\"></i>' + esc(date) + '</span>' : '') +\n          '</div>' +\n          (chips ? '<div class=\"qav-chips\">' + chips + '</div>' : '') +\n          '</div></div></div>';\n\n        /* Distribute blocks across the 3 columns using per-user prefs over the defaults. */\n        var prefs = (state.userPrefs && state.userPrefs.blocks) || {};\n        var cols = { 1: '', 2: '', 3: '' };\n        BLOCKS.forEach(function (b) {\n          var pr = prefs[b.key] || {};\n          if (pr.hidden) return;\n          var col = pr.col || b.def; if (col < 1 || col > 3) col = b.def;\n          cols[col] += blockHtml(b.key, order);\n        });\n        cols[1] = headerHtml + cols[1];\n\n        var html = '<div class=\"qav-stage\"><div class=\"qav-grid\">';\n        html += '<div class=\"qav-left\">' + cols[1] + '</div>';\n        html += '<div>' + cols[2] + '</div>';\n        html += '<div class=\"qav-preview-col\">' + cols[3] + '</div>';\n        html += '</div></div>';\n        return html;\n      }\n\n      /* Movable content blocks — default column + label for the per-user customiser. */\n      var BLOCKS = [\n        { key: 'callsrec', label: 'Samtal & inspelning', icon: 'headset', def: 1 },\n        { key: 'decision', label: 'Kommentar & beslut', icon: 'gavel', def: 1 },\n        { key: 'transcript', label: 'Transkript', icon: 'align-left', def: 1 },\n        { key: 'fields', label: 'Orderuppgifter', icon: 'list-ul', def: 2 },\n        { key: 'files', label: 'Filer', icon: 'paperclip', def: 2 },\n        { key: 'works', label: 'Orderarbeten', icon: 'bolt', def: 2 },\n        { key: 'preview', label: 'Förhandsgranskning', icon: 'file-contract', def: 3 }\n      ];\n      function blockHtml(key, order) {\n        switch (key) {\n          case 'callsrec':\n            return '<div class=\"qav-panel\"><div class=\"qav-panel-h\"><i class=\"fas fa-headset\"></i> Samtal<span class=\"qav-h-spacer\"></span><span id=\"qav-calls-count\" style=\"text-transform:none;letter-spacing:0;font-weight:700;color:var(--qav-fg2);\"></span></div>' +\n              '<div id=\"qav-calls-panel\"></div><div id=\"qav-audio-panel\"></div></div>';\n          case 'decision': return renderActionBar(order);\n          case 'transcript': return '<div class=\"qav-panel\"><div class=\"qav-panel-h\"><i class=\"fas fa-align-left\"></i> Transkript</div><div id=\"qav-transcript-panel\"></div></div>';\n          case 'fields': return '<div class=\"qav-panel\"><div class=\"qav-panel-h\"><i class=\"fas fa-list-ul\"></i> Orderuppgifter</div>' + renderFields(order) + '</div>';\n          case 'files': return '<div class=\"qav-panel\"><div class=\"qav-panel-h\"><i class=\"fas fa-paperclip\"></i> Filer</div>' + renderFiles(order) + '</div>';\n          case 'works': return state.works.length ? '<div class=\"qav-panel\"><div class=\"qav-panel-h\"><i class=\"fas fa-bolt\"></i> Orderarbeten</div>' + renderWorks(order) + '</div>' : '';\n          case 'preview':\n            return '<div class=\"qav-panel qav-preview-panel\"><div class=\"qav-panel-h\"><i class=\"fas fa-file-contract\"></i> Förhandsgranskning<span class=\"qav-h-spacer\"></span><span id=\"qav-preview-name\" style=\"text-transform:none;letter-spacing:0;font-weight:700;color:var(--qav-fg2);\"></span></div>' +\n              '<div class=\"qav-preview-body\" id=\"qav-preview-body\"></div></div>';\n        }\n        return '';\n      }\n\n      function renderFields(order) {\n        var rows = (order.fields || []).filter(function (f) { return f.value != null && String(f.value).trim() !== ''; }).map(function (f) {\n          var lbl = fieldLabel(f.fieldId) || 'Fält';\n          return '<div class=\"qav-field\">' +\n            '<div class=\"qav-field-label\">' + esc(lbl) + '</div>' +\n            '<div class=\"qav-field-value\">' + esc(f.value) + '</div>' +\n            '<button class=\"qav-field-copy\" data-copy=\"' + esc(f.value) + '\" title=\"Kopiera\"><i class=\"fas fa-copy\"></i></button>' +\n            '</div>';\n        }).join('');\n        if (!rows) return '<div class=\"qav-audio-note\">Inga ifyllda fält på ordern.</div>';\n        return '<div class=\"qav-fields\">' + rows + '</div>';\n      }\n\n      function filesV1Url(uri) {\n        try { var u = new URL(uri); return API + '/users/files-v1' + u.pathname; } catch (e) { return uri; }\n      }\n      function renderFiles(order) {\n        var files = order.files || [];\n        if (!files.length) return '<div class=\"qav-audio-note\">Inga filer bifogade på ordern.</div>';\n        var previewIndex = state.preview ? state.preview.index : -1;\n        return files.map(function (f, i) {\n          var url = filesV1Url(f.uri);\n          var isPdf = (f.mimetype === 'application/pdf') || /\\.pdf$/i.test(f.name || '');\n          var isImage = /^image\\//.test(f.mimetype || '') || /\\.(png|jpe?g|gif|webp|svg)$/i.test(f.name || '');\n          var canPreview = isPdf || isImage;\n          var active = i === previewIndex;\n          return '<div class=\"qav-file-row' + (active ? ' qav-file-active' : '') + '\">' +\n            '<span class=\"qav-file-ic\"><i class=\"fas fa-' + (isPdf ? 'file-pdf' : (isImage ? 'file-image' : 'file-alt')) + '\"></i></span>' +\n            '<span class=\"qav-file-name\">' + esc(f.name || 'Fil') + '</span>' +\n            '<span class=\"qav-file-actions\">' +\n            (canPreview ? '<button class=\"qav-run-btn qav-view-btn' + (active ? ' qav-active' : '') + '\" data-view-file=\"' + i + '\"><i class=\"fas fa-eye\"></i> Visa</button>' : '') +\n            '<a class=\"qav-run-btn\" href=\"' + esc(url) + '\" target=\"_blank\" rel=\"noopener\"><i class=\"fas fa-external-link-alt\"></i> Öppna</a>' +\n            '</span></div>';\n        }).join('');\n      }\n\n      function conditionMatchesOrder(work, order) {\n        var c = work.conditions || {};\n        var otags = order.tagIds || [];\n        function arr(x) { return Array.isArray(x) ? x : (x ? [x] : []); }\n        var any = arr(c.anyTagId), all = arr(c.allTagIds || c.tagIds), notAny = arr(c.notAnyTagId || c.excludingTagIds);\n        if (any.length && !any.some(function (t) { return otags.indexOf(t) !== -1; })) return false;\n        if (all.length && !all.every(function (t) { return otags.indexOf(t) !== -1; })) return false;\n        if (notAny.length && notAny.some(function (t) { return otags.indexOf(t) !== -1; })) return false;\n        return true;\n      }\n      function workIcon(w) {\n        var pres = w.presentation || {};\n        return pres.iconName || (w.toDo && w.toDo.email ? 'envelope' : (w.toDo && w.toDo.sms ? 'sms' : (w.toDo && w.toDo.http ? 'globe' : 'bolt')));\n      }\n      function workRow(w, pinned) {\n        var icon = workIcon(w);\n        return '<div class=\"' + (pinned ? 'qav-work-pinned' : 'qav-work') + '\" data-work-id=\"' + esc(w.id) + '\">' +\n          '<span class=\"qav-work-ic\"><i class=\"fas fa-' + esc(icon) + '\"></i></span>' +\n          '<div class=\"qav-work-body\">' +\n          (pinned ? '<div class=\"qav-pin-badge\"><i class=\"fas fa-thumbtack\"></i> Snabbåtgärd</div>' : '') +\n          '<div class=\"qav-work-name\">' + esc(w.name) + '</div>' +\n          '<div class=\"qav-work-sub\" data-work-status=\"' + esc(w.id) + '\">Ej körd på denna order</div>' +\n          '</div>' +\n          '<button class=\"qav-run-btn\" data-run-work=\"' + esc(w.id) + '\"><i class=\"fas fa-play\"></i> Kör</button>' +\n          '</div>';\n      }\n      function renderWorks(order) {\n        var c = state.config || {};\n        var hidden = c.hiddenWorkIds || [];\n        var applicable = state.works.filter(function (w) {\n          if (hidden.indexOf(w.id) !== -1) return false;\n          return conditionMatchesOrder(w, order);\n        });\n        if (!applicable.length) return '<div class=\"qav-audio-note\">Inga manuella orderarbeten matchar den här ordern.</div>';\n        var pinnedId = c.pinnedWorkId;\n        var pinned = null, rest = [];\n        applicable.forEach(function (w) { if (w.id === pinnedId && !pinned) pinned = w; else rest.push(w); });\n        return (pinned ? workRow(pinned, true) : '') + rest.map(function (w) { return workRow(w, false); }).join('');\n      }\n\n      function renderActionBar(order) {\n        var scns = (state.config.scenarios || []).map(function (s) {\n          var col = s.color || '#607D8B';\n          return '<button class=\"qav-decision\" data-scn=\"' + esc(s.id) + '\" style=\"background:' + esc(col) + ';\">' +\n            '<i class=\"fas fa-' + esc(s.icon || 'check') + '\"></i> ' + esc(s.label || 'Åtgärd') + '</button>';\n        }).join('');\n        return '<div class=\"qav-actionbar\">' +\n          '<textarea class=\"qav-comment\" id=\"qav-comment\" placeholder=\"Kommentar (läggs till på ordern)…\"></textarea>' +\n          '<div class=\"qav-comment-hint\" id=\"qav-comment-hint\"></div>' +\n          '<div class=\"qav-btn-row\">' + scns +\n          '<button class=\"qav-decision qav-skip\" data-skip=\"1\" title=\"Hoppa till nästa utan beslut\"><i class=\"fas fa-forward\"></i> Hoppa</button>' +\n          '</div></div>';\n      }\n\n      function renderMediaPanels() {\n        renderAudioPanel();\n        renderTranscriptPanel();\n      }\n\n      function renderCallsPanel() {\n        var p = el('qav-calls-panel'); if (!p) return;\n        var countEl = el('qav-calls-count');\n        var calls = state.calls;\n        if (calls == null) { if (countEl) countEl.textContent = ''; p.innerHTML = '<div class=\"qav-audio-note\"><div class=\"qav-spinner\"></div> Söker samtal…</div>'; return; }\n        if (!calls.length) { if (countEl) countEl.textContent = ''; p.innerHTML = '<div class=\"qav-audio-note\" style=\"padding-bottom:0;\"><i class=\"fas fa-phone-slash\"></i> Inga samtal hittades för ordern.</div>'; return; }\n        if (countEl) countEl.textContent = calls.length + (calls.length === 1 ? ' samtal' : ' samtal');\n        /* Single call: no need for a selectable list — the player below is enough. */\n        if (calls.length === 1) { p.innerHTML = ''; return; }\n        var order = state.current || {};\n        var html = '<div class=\"qav-calls\">';\n        calls.forEach(function (c) {\n          var sel = c.id === state.selectedCallId;\n          var d = c.connectingAt ? new Date(c.connectingAt) : null;\n          var when = d ? d.toLocaleString('sv-SE', { dateStyle: 'short', timeStyle: 'short' }) : '–';\n          var dur = c.totalDuration || c.billedDuration || c.connectedDuration || 0;\n          var durTxt = dur ? ' · ' + fmtTime(dur > 1000 ? dur / 1000 : dur) : '';\n          var linked = c.id === order.callId ? ' · kopplad' : '';\n          var rec = c.isRecordingAvailable !== false;\n          var chips = (c.tagIds || []).map(function (tid) {\n            var t = callTagById(tid); if (!t) return '';\n            var col = t.color || '#9E9E9E';\n            return '<span class=\"qav-callchip\" style=\"background:color-mix(in srgb,' + esc(col) + ' 16%, transparent);color:' + esc(col) + ';\">' + esc(t.name) + '</span>';\n          }).join('');\n          html += '<button class=\"qav-call' + (sel ? ' qav-call-sel' : '') + '\" data-call-select=\"' + esc(c.id) + '\">' +\n            '<span class=\"qav-call-ic\"><i class=\"fas fa-' + (rec ? 'play-circle' : 'phone-slash') + '\"></i></span>' +\n            '<span class=\"qav-call-body\">' +\n            '<span class=\"qav-call-when\">' + esc(when) + durTxt + linked + '</span>' +\n            (chips ? '<span class=\"qav-call-chips\">' + chips + '</span>' : '') +\n            '</span>' +\n            (rec ? '' : '<span class=\"qav-call-norec\">ingen insp.</span>') +\n            '</button>';\n        });\n        html += '</div>';\n        p.innerHTML = html;\n      }\n\n      function renderAudioPanel() {\n        var p = el('qav-audio-panel'); if (!p) return;\n        var m = state.media;\n        if (!m) { p.innerHTML = ''; return; }\n        if (!m.callId) { p.innerHTML = '<div class=\"qav-audio-note\"><i class=\"fas fa-microphone-slash\"></i> Ingen samtalsinspelning kopplad till ordern.</div>'; return; }\n        if (m.noRecording) { p.innerHTML = '<div class=\"qav-audio-note\"><i class=\"fas fa-microphone-slash\"></i> Det valda samtalet saknar inspelning.</div>'; return; }\n        if (m.audioLoading) { p.innerHTML = '<div class=\"qav-audio-note\"><div class=\"qav-spinner\"></div> Laddar ljud…</div>'; return; }\n        if (m.audioError || !m.audioUrl) { p.innerHTML = '<div class=\"qav-audio-note\"><i class=\"fas fa-exclamation-triangle\"></i> Inspelningen kunde inte laddas.</div>'; return; }\n        p.innerHTML =\n          '<div class=\"qav-audio-wrap\">' +\n          '<audio id=\"qav-audio\" src=\"' + esc(m.audioUrl) + '\" preload=\"metadata\"></audio>' +\n          '<div class=\"qav-audio-row\">' +\n          '<button class=\"qav-play\" id=\"qav-play\"><i class=\"fas fa-play\"></i></button>' +\n          '<div class=\"qav-seek-col\">' +\n          '<canvas id=\"qav-viz\" class=\"qav-viz\" title=\"Klicka för att spola\"></canvas>' +\n          '<div class=\"qav-seek-time\"><span id=\"qav-cur\">0:00</span><span id=\"qav-dur\">0:00</span></div>' +\n          '</div>' +\n          '<div class=\"qav-rate\" id=\"qav-rate\">' +\n          ['1', '1.25', '1.5', '2'].map(function (r) { return '<button data-rate=\"' + r + '\"' + (r === '1' ? ' class=\"qav-active\"' : '') + '>' + r + 'x</button>'; }).join('') +\n          '</div>' +\n          '</div></div>';\n        bindAudio();\n      }\n\n      function renderTranscriptPanel() {\n        var p = el('qav-transcript-panel'); if (!p) return;\n        var m = state.media;\n        if (!m || !m.callId) { p.innerHTML = '<div class=\"qav-audio-note\"><i class=\"fas fa-align-left\"></i> Inget transkript.</div>'; return; }\n        if (m.transcript == null) { p.innerHTML = '<div class=\"qav-audio-note\"><div class=\"qav-spinner\"></div> Laddar transkript…</div>'; return; }\n        if (!m.transcript.length) {\n          p.innerHTML = '<div class=\"qav-audio-note\"><i class=\"fas fa-align-left\"></i> Transkript saknas för det här samtalet' + (m.status ? ' (' + esc(m.status) + ')' : '') + '.</div>';\n          return;\n        }\n        var html = '<div class=\"qav-transcript\" id=\"qav-transcript\">';\n        m.transcript.forEach(function (seg, i) {\n          html += '<div class=\"qav-seg ' + (seg.them ? 'qav-seg-them' : 'qav-seg-us') + '\" data-seg=\"' + i + '\" data-start=\"' + (seg.startMs || 0) + '\" data-end=\"' + (seg.endMs || 0) + '\">' +\n            '<div class=\"qav-seg-time\">' + fmtTime((seg.startMs || 0) / 1000) + '</div>' +\n            '<div class=\"qav-seg-body\">' +\n            '<div class=\"qav-seg-who\">' + (seg.them ? 'Kund' : 'Säljare') + '</div>' +\n            '<div class=\"qav-seg-text\">' + esc(seg.text) + '</div>' +\n            '</div></div>';\n        });\n        html += '</div>';\n        p.innerHTML = html;\n      }\n\n      /* ---------- audio behaviour ---------- */\n      function bindAudio() {\n        var audio = el('qav-audio'); if (!audio) return;\n        audio.onloadedmetadata = function () { var d = el('qav-dur'); if (d) d.textContent = fmtTime(audio.duration); };\n        audio.ontimeupdate = function () {\n          var cur = el('qav-cur');\n          if (cur) cur.textContent = fmtTime(audio.currentTime);\n          highlightSegment(audio.currentTime * 1000);\n        };\n        audio.onplay = function () {\n          var b = el('qav-play'); if (b) b.innerHTML = '<i class=\"fas fa-pause\"></i>';\n          if (state.audioCtx && state.audioCtx.state === 'suspended') { try { state.audioCtx.resume(); } catch (e) {} }\n        };\n        audio.onpause = function () { var b = el('qav-play'); if (b) b.innerHTML = '<i class=\"fas fa-play\"></i>'; };\n        audio.onended = function () { var b = el('qav-play'); if (b) b.innerHTML = '<i class=\"fas fa-play\"></i>'; };\n        setupViz(audio);\n      }\n\n      function getAudioCtx() {\n        var AC = window.AudioContext || window.webkitAudioContext;\n        if (!AC) return null;\n        if (!state.audioCtx) { try { state.audioCtx = new AC(); } catch (e) { return null; } }\n        return state.audioCtx;\n      }\n      /* Decode the recording and reduce it to N peak-amplitude buckets (a SoundCloud-style\n         static waveform of the whole call — not realtime). */\n      function computeWaveform(audioBuffer, N) {\n        var ch = audioBuffer.getChannelData(0);\n        var block = Math.floor(ch.length / N) || 1;\n        var peaks = new Array(N), max = 0.0001, stride = Math.max(1, Math.floor(block / 240));\n        for (var i = 0; i < N; i++) {\n          var start = i * block, end = Math.min(ch.length, start + block), p = 0;\n          for (var j = start; j < end; j += stride) { var v = Math.abs(ch[j]); if (v > p) p = v; }\n          peaks[i] = p; if (p > max) max = p;\n        }\n        for (var k = 0; k < N; k++) peaks[k] = peaks[k] / max;\n        return peaks;\n      }\n\n      /* Static waveform drawn on a canvas; bars up to the playhead are coloured (progress),\n         the rest muted. Also the seek surface (click to jump). */\n      function setupViz(audio) {\n        var canvas = el('qav-viz'); if (!canvas) return;\n        if (state.viz && state.viz.raf) { try { cancelAnimationFrame(state.viz.raf); } catch (e) {} }\n        var cctx = canvas.getContext('2d');\n        var dpr = window.devicePixelRatio || 1;\n        function colors() {\n          var root = el('qav-root'), cs = root ? getComputedStyle(root) : null;\n          return {\n            prim: (cs && cs.getPropertyValue('--qav-primary').trim()) || '#1565C0',\n            mute: (cs && cs.getPropertyValue('--qav-border').trim()) || 'rgba(120,120,120,0.4)'\n          };\n        }\n        var col = colors();\n        function roundBar(c, x, y, bw, bh, r) {\n          r = Math.min(r, bw / 2, bh / 2);\n          c.beginPath();\n          c.moveTo(x + r, y);\n          c.arcTo(x + bw, y, x + bw, y + bh, r);\n          c.arcTo(x + bw, y + bh, x, y + bh, r);\n          c.arcTo(x, y + bh, x, y, r);\n          c.arcTo(x, y, x + bw, y, r);\n          c.closePath(); c.fill();\n        }\n        function draw() {\n          state.viz.raf = requestAnimationFrame(draw);\n          var rect = canvas.getBoundingClientRect();\n          var w = Math.max(1, Math.round(rect.width * dpr)), h = Math.max(1, Math.round(rect.height * dpr));\n          if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n          var cy = h / 2;\n          cctx.clearRect(0, 0, w, h);\n          var wf = state.media && state.media.waveform;\n          var loadingWf = state.media && state.media.audioUrl && !wf;\n          var N = wf ? wf.length : 56;\n          var prog = audio.duration ? (audio.currentTime / audio.duration) : 0;\n          var gap = w / N, barW = Math.max(1.5 * dpr, gap * 0.6);\n          for (var i = 0; i < N; i++) {\n            var lvl;\n            if (wf) lvl = Math.max(0.05, wf[i]);\n            else lvl = 0.12 + (loadingWf ? 0.05 : 0.14) * Math.abs(Math.sin(i * 0.5 + (loadingWf ? Date.now() / 300 : 0)));\n            var bh = lvl * (cy - 2 * dpr);\n            var x = i * gap + (gap - barW) / 2;\n            cctx.fillStyle = (i / N) <= prog ? col.prim : col.mute;\n            roundBar(cctx, x, cy - bh, barW, bh * 2, barW / 2);\n          }\n          cctx.fillStyle = col.prim;\n          cctx.fillRect(Math.min(w - dpr, prog * w), 2 * dpr, Math.max(1, dpr), h - 4 * dpr);\n        }\n        state.viz = { raf: null, refreshColors: function () { col = colors(); } };\n        draw();\n      }\n      function highlightSegment(ms) {\n        var t = el('qav-transcript'); if (!t) return;\n        var segs = t.querySelectorAll('.qav-seg'), activeEl = null;\n        for (var i = 0; i < segs.length; i++) {\n          var s = +segs[i].getAttribute('data-start'), e = +segs[i].getAttribute('data-end');\n          var on = ms >= s && (e ? ms < e : true);\n          if (on && !activeEl) { activeEl = segs[i]; segs[i].className = segs[i].className.indexOf('qav-seg-active') === -1 ? segs[i].className + ' qav-seg-active' : segs[i].className; }\n          else segs[i].className = segs[i].className.replace(' qav-seg-active', '');\n        }\n      }\n\n      /* ---------- decisions ---------- */\n      function currentComment() { var c = el('qav-comment'); return c ? c.value.trim() : ''; }\n\n      function decide(scenarioId) {\n        if (state.deciding) return;\n        var order = state.current; if (!order) return;\n        var scn = null;\n        (state.config.scenarios || []).forEach(function (s) { if (s.id === scenarioId) scn = s; });\n        if (!scn) return;\n        var comment = currentComment();\n        var hint = el('qav-comment-hint'), cbox = el('qav-comment');\n        if (scn.requireComment && !comment) {\n          if (cbox) { cbox.className = 'qav-comment qav-required'; cbox.focus(); }\n          if (hint) { hint.className = 'qav-comment-hint qav-err'; hint.textContent = 'Den här åtgärden kräver en kommentar.'; }\n          return;\n        }\n\n        state.deciding = true;\n        document.querySelectorAll('#qav-root .qav-decision').forEach(function (b) { b.disabled = true; });\n\n        var addTagIds = (scn.addTagIds || []).slice();\n        var removeTagIds = (scn.removeTagIds || []).slice();\n        if (state.config.removeSourceOnDecision) {\n          (state.config.sourceTagIds || []).forEach(function (t) { if (removeTagIds.indexOf(t) === -1) removeTagIds.push(t); });\n        }\n\n        var chain = Promise.resolve();\n        if (comment) {\n          chain = chain.then(function () { return api('POST', '/orders/orders-v1/' + order.id + '/comments', null, { message: comment }); });\n        }\n        if (addTagIds.length || removeTagIds.length) {\n          chain = chain.then(function () {\n            var body = {};\n            if (addTagIds.length) body.addTagIds = addTagIds;\n            if (removeTagIds.length) body.removeTagIds = removeTagIds;\n            return api('PUT', '/orders/orders-v2/' + order.id, null, body);\n          });\n        }\n        chain.then(function () {\n          toast(scn.label + ' — order #' + order.serialId + ' klar');\n          advanceAfterDecision(order);\n        }).catch(function (err) {\n          state.deciding = false;\n          document.querySelectorAll('#qav-root .qav-decision').forEach(function (b) { b.disabled = false; });\n          toast(err && err.status === 403 ? 'Behörighet saknas för att ändra ordern' : 'Något gick fel — försök igen');\n        });\n      }\n\n      function advanceAfterDecision(order) {\n        state.deciding = false;\n        state.queue = state.queue.filter(function (o) { return o.id !== order.id; });\n        state.remaining = Math.max(0, state.remaining - 1);\n        if (state.queue.length) { setCurrent(state.queue[0]); }\n        else if (state.remaining > 0) { loadQueue(); }\n        else { setCurrent(null); }\n      }\n\n      function skipCurrent() {\n        var order = state.current; if (!order) return;\n        state.queue = state.queue.filter(function (o) { return o.id !== order.id; });\n        state.queue.push(order); /* send to back */\n        setCurrent(state.queue[0] || null);\n      }\n\n      function runWork(workId) {\n        var order = state.current; if (!order) return;\n        var btn = document.querySelector('#qav-root [data-run-work=\"' + workId + '\"]');\n        var status = document.querySelector('#qav-root [data-work-status=\"' + workId + '\"]');\n        if (btn) { btn.disabled = true; btn.innerHTML = '<div class=\"qav-spinner\"></div>'; }\n        if (status) { status.className = 'qav-work-sub'; status.textContent = 'Kör…'; }\n        api('POST', '/orders/after-order-work-v1/' + workId + '/execute/' + order.id).then(function (res) {\n          var ok = true, errMsg = null;\n          if (res && res.results && res.results.length) {\n            res.results.forEach(function (r) { if (r && r.error) { ok = false; errMsg = r.error.message || r.error; } });\n          } else if (res && res.error) { ok = false; errMsg = res.error.message || res.error; }\n          if (btn) { btn.disabled = false; btn.innerHTML = '<i class=\"fas fa-redo\"></i> Kör igen'; }\n          if (status) {\n            status.className = 'qav-work-sub ' + (ok ? 'qav-ok' : 'qav-err');\n            status.textContent = ok ? 'Körd nyss' : ('Fel: ' + esc(String(errMsg || 'okänt')));\n          }\n          toast(ok ? 'Orderarbetet kördes' : 'Orderarbetet gav ett fel');\n        }).catch(function (err) {\n          if (btn) { btn.disabled = false; btn.innerHTML = '<i class=\"fas fa-play\"></i> Kör'; }\n          if (status) { status.className = 'qav-work-sub qav-err'; status.textContent = err && err.status === 403 ? 'Behörighet saknas' : 'Kunde inte köra'; }\n          toast('Kunde inte köra orderarbetet');\n        });\n      }\n\n      /* ================= CONFIG PANEL ================= */\n      function openConfig() {\n        state.cfgDraft = JSON.parse(JSON.stringify(state.config || defaultConfig()));\n        if (!state.cfgDraft.scenarios || !state.cfgDraft.scenarios.length) state.cfgDraft.scenarios = defaultConfig().scenarios;\n        state.cfgOpenCard = state.cfgOpenCard || 'queue';\n        renderConfig();\n        var b = el('qav-config-backdrop'); if (b) b.className = 'qav-modal-backdrop qav-open';\n      }\n      function closeConfig() { var b = el('qav-config-backdrop'); if (b) b.className = 'qav-modal-backdrop'; }\n\n      function tagPickHtml(selectedIds, group, opts) {\n        opts = opts || {};\n        var sel = selectedIds || [];\n        var tags = opts.tags || state.orderTags;\n        var html = '<div class=\"qav-tagpick\" data-tagpick=\"' + esc(group) + '\">';\n        if (opts.includeNoneNote && !tags.length) html += '<span class=\"qav-audio-note\">Inga taggar finns.</span>';\n        tags.forEach(function (t) {\n          var picked = sel.indexOf(t.id) !== -1;\n          var col = t.color || '#9E9E9E';\n          html += '<button class=\"qav-tagopt' + (picked ? ' qav-picked' : '') + '\" data-tag-toggle=\"' + esc(t.id) + '\" data-tag-group=\"' + esc(group) + '\">' +\n            '<span class=\"qav-dot\" style=\"background:' + esc(col) + '\"></span>' + esc(t.name) + '</button>';\n        });\n        html += '</div>';\n        return html;\n      }\n\n      function orderFieldPickHtml(selected, group, opts) {\n        opts = opts || {};\n        var sel = selected || [];\n        var fields = (state.orderFields || []).filter(function (f) { return !f.isRemoved; });\n        var html = '<div class=\"qav-tagpick\" data-fieldpick=\"' + esc(group) + '\">';\n        if (!fields.length) html += '<span class=\"qav-audio-note\">Inga orderfält finns.</span>';\n        fields.forEach(function (f) {\n          var idx = sel.indexOf(f.id), picked = idx !== -1;\n          var badge = (opts.ordered && picked) ? '<span class=\"qav-ord-badge\">' + (idx + 1) + '</span>' : '';\n          html += '<button class=\"qav-tagopt' + (picked ? ' qav-picked' : '') + '\" data-field-toggle=\"' + esc(f.id) + '\" data-field-group=\"' + esc(group) + '\">' +\n            badge + esc(f.label || 'Fält') + '</button>';\n        });\n        html += '</div>';\n        return html;\n      }\n\n      function cfgCard(key, icon, title, summary, body) {\n        var open = state.cfgOpenCard === key;\n        return '<div class=\"qav-cfg-card' + (open ? ' qav-open' : '') + '\">' +\n          '<div class=\"qav-cfg-head\" data-card-toggle=\"' + key + '\">' +\n          '<span class=\"qav-cfg-ic\"><i class=\"fas fa-' + icon + '\"></i></span>' +\n          '<span class=\"qav-cfg-htext\"><span class=\"qav-cfg-title\">' + title + '</span>' +\n          '<span class=\"qav-cfg-sum\">' + esc(summary) + '</span></span>' +\n          '<i class=\"fas fa-chevron-down qav-cfg-chev\"></i>' +\n          '</div>' +\n          (open ? '<div class=\"qav-cfg-body\">' + body + '</div>' : '') +\n          '</div>';\n      }\n\n      function tagNames(ids) {\n        return (ids || []).map(function (id) { return tagName(id); }).filter(function (n) { return n && n !== '?'; });\n      }\n      function fieldLabelsOf(ids) {\n        return (ids || []).map(function (id) { return fieldLabel(id); }).filter(function (l) { return l; });\n      }\n\n      function renderConfig() {\n        var m = el('qav-config-modal'); if (!m) return;\n        var d = state.cfgDraft;\n        var html = '';\n        html += '<div class=\"qav-modal-head\"><div class=\"qav-modal-title\">Inställningar</div>' +\n          '<button class=\"qav-icon-btn\" id=\"qav-cfg-close\"><i class=\"fas fa-times\"></i></button></div>';\n        html += '<div class=\"qav-modal-body\">';\n\n        /* 1 — Queue */\n        var qn = tagNames(d.sourceTagIds);\n        html += cfgCard('queue', 'layer-group', 'Granskningskö',\n          qn.length ? qn.join(', ') : 'Välj vilka ordertaggar som fyller kön',\n          '<div class=\"qav-cfg-desc\">Ordrar med någon av dessa taggar hamnar i granskningskön.</div>' +\n          tagPickHtml(d.sourceTagIds, 'source', { includeNoneNote: true }));\n\n        /* 2 — Customer display (name/phone field mapping) */\n        var nameLbls = fieldLabelsOf(d.nameFieldIds);\n        var phoneLbl = d.phoneFieldId ? fieldLabel(d.phoneFieldId) : null;\n        html += cfgCard('display', 'id-card', 'Kunduppgifter',\n          nameLbls.length ? ('Namn: ' + nameLbls.join(' + ')) : 'Automatiskt (gissar från fältnamn)',\n          '<div class=\"qav-cfg-desc\">Välj vilka orderfält som bildar kundens namn (i vald ordning). Lämna tomt för automatisk gissning.</div>' +\n          '<div class=\"qav-scn-sec-l\">Namnfält (klicka i ordning)</div>' + orderFieldPickHtml(d.nameFieldIds, 'name', { ordered: true }) +\n          '<div class=\"qav-scn-sec-l\" style=\"margin-top:14px;\">Telefonfält</div>' + orderFieldPickHtml(d.phoneFieldId ? [d.phoneFieldId] : [], 'phone', {}));\n\n        /* 3 — Buttons */\n        html += cfgCard('buttons', 'hand-pointer', 'Knappar & scenarion',\n          (d.scenarios || []).map(function (s) { return s.label; }).filter(Boolean).join(', ') || 'Inga knappar',\n          '<div class=\"qav-cfg-desc\">Varje knapp lägger till (och kan ta bort) ordertaggar och kan kräva en kommentar.</div>' +\n          '<div id=\"qav-scn-list\">' + d.scenarios.map(renderScnCard).join('') +\n          '<button class=\"qav-add-scn\" id=\"qav-add-scn\"><i class=\"fas fa-plus\"></i> Lägg till scenario</button></div>');\n\n        /* 4 — Done marking */\n        var rn = tagNames(d.reviewedTagIds);\n        html += cfgCard('done', 'clipboard-check', 'Klarmarkering',\n          (rn.length ? rn.join(', ') : 'Auto (knapparnas taggar)') + (d.removeSourceOnDecision ? ' · tar bort kö-tagg' : ''),\n          '<div class=\"qav-cfg-desc\">En order räknas som färdiggranskad när den fått någon av dessa statustaggar. Tomt = använd taggarna som knapparna lägger till.</div>' +\n          tagPickHtml(d.reviewedTagIds, 'reviewed') +\n          '<label class=\"qav-toggle-row\" data-toggle=\"removeSourceOnDecision\"><span class=\"qav-toggle' + (d.removeSourceOnDecision ? ' qav-on' : '') + '\"></span> Ta även bort kö-taggen från ordern vid beslut</label>');\n\n        /* 5 — Order works */\n        var shown = (state.works || []).filter(function (w) { return (d.hiddenWorkIds || []).indexOf(w.id) === -1; }).length;\n        html += cfgCard('works', 'bolt', 'Orderarbeten',\n          state.works.length ? (shown + ' av ' + state.works.length + ' synliga' + (d.pinnedWorkId ? ' · 1 nålad' : '')) : 'Inga manuella arbeten',\n          '<div class=\"qav-cfg-desc\">Välj vilka manuella orderarbeten granskaren ser. Nåla ett arbete för att lyfta fram det överst.</div>' +\n          renderWorkConfig(d));\n\n        /* 6 — Calls & recording */\n        var cn = tagNames(d.callTagIds);\n        var activeFilters = Object.keys(d.callFilter || {}).length;\n        html += cfgCard('calls', 'headset', 'Samtal & inspelning',\n          (cn.length ? cn.join(', ') : 'Alla samtal') + (activeFilters ? ' · ' + activeFilters + ' filter' : ''),\n          '<div class=\"qav-cfg-desc\">Granskaren ser en lista med kundens samtal och navigerar mellan deras inspelningar och transkript.</div>' +\n          '<div class=\"qav-scn-sec-l\">Lista samtal med taggar (tomt = alla)</div>' + tagPickHtml(d.callTagIds, 'callTags', { tags: state.callTags, includeNoneNote: true }) +\n          renderCallFilters(d) +\n          '<label class=\"qav-toggle-row\" data-toggle=\"matchByPhone\"><span class=\"qav-toggle' + (d.matchByPhone ? ' qav-on' : '') + '\"></span> Matcha på telefonnummer när ordern saknar kontakt</label>');\n\n        html += '</div>';\n        html += '<div class=\"qav-modal-foot\">' +\n          '<button class=\"qav-primary-btn\" id=\"qav-cfg-save\"' + (state.cfgSaving ? ' disabled' : '') + '>' + (state.cfgSaving ? 'Sparar…' : '<i class=\"fas fa-check\"></i> Spara') + '</button>' +\n          '<button class=\"qav-ghost-btn\" id=\"qav-cfg-cancel\">Avbryt</button>' +\n          '</div>';\n        m.innerHTML = html;\n      }\n\n      function renderScnCard(scn, i) {\n        var col = scn.color || '#607D8B';\n        var icons = ICONS.map(function (ic) {\n          return '<button data-scn-icon=\"' + i + '\" data-icon=\"' + esc(ic) + '\"' + (scn.icon === ic ? ' class=\"qav-active\"' : '') + '><i class=\"fas fa-' + esc(ic) + '\"></i></button>';\n        }).join('');\n        var swatches = COLORS.map(function (c) {\n          return '<span class=\"qav-swatch' + (col === c ? ' qav-active' : '') + '\" data-scn-color=\"' + i + '\" data-color=\"' + c + '\" style=\"background:' + c + '\"></span>';\n        }).join('');\n        return '<div class=\"qav-scn-card\" data-scn-card=\"' + i + '\">' +\n          '<div class=\"qav-scn-top\">' +\n          '<span class=\"qav-scn-icpick\" style=\"background:' + esc(col) + '\"><i class=\"fas fa-' + esc(scn.icon || 'check') + '\"></i></span>' +\n          '<input class=\"qav-scn-label-in\" data-scn-label=\"' + i + '\" value=\"' + esc(scn.label || '') + '\" placeholder=\"Knapptext\">' +\n          '<button class=\"qav-scn-del\" data-scn-del=\"' + i + '\" title=\"Ta bort\"><i class=\"fas fa-trash-alt\"></i></button>' +\n          '</div>' +\n          '<div class=\"qav-scn-sec\"><div class=\"qav-scn-sec-l\">Ikon</div><div class=\"qav-mini-icons\">' + icons + '</div></div>' +\n          '<div class=\"qav-scn-sec\"><div class=\"qav-scn-sec-l\">Färg</div><div class=\"qav-swatches\">' + swatches + '</div></div>' +\n          '<div class=\"qav-scn-sec\"><div class=\"qav-scn-sec-l\">Taggar att lägga till</div>' + tagPickHtml(scn.addTagIds, 'add:' + i) + '</div>' +\n          '<div class=\"qav-scn-sec\"><div class=\"qav-scn-sec-l\">Taggar att ta bort</div>' + tagPickHtml(scn.removeTagIds, 'remove:' + i) + '</div>' +\n          '<label class=\"qav-toggle-row\" data-scn-reqcomment=\"' + i + '\"><span class=\"qav-toggle' + (scn.requireComment ? ' qav-on' : '') + '\"></span> Kräv kommentar för den här knappen</label>' +\n          '</div>';\n      }\n\n      function renderCallFilters(d) {\n        var cf = d.callFilter || {};\n        function chip(key, val, label, icon) {\n          var active = cf[key] === val;\n          return '<button class=\"qav-tagopt' + (active ? ' qav-picked' : '') + '\" data-cf=\"' + key + '\" data-cfval=\"' + val + '\">' +\n            '<i class=\"fas fa-' + (active ? 'check' : (icon || 'filter')) + '\" style=\"font-size:10px;\"></i> ' + esc(label) + '</button>';\n        }\n        function dirChip(v, label) {\n          var active = v === 'any' ? cf.isInbound == null : (v === 'in' ? cf.isInbound === true : cf.isInbound === false);\n          return '<button class=\"qav-tagopt' + (active ? ' qav-picked' : '') + '\" data-cf-dir=\"' + v + '\">' + esc(label) + '</button>';\n        }\n        return '<div class=\"qav-scn-sec-l\" style=\"margin-top:16px;\">Filtrera samtal</div>' +\n          '<div class=\"qav-tagpick\">' +\n          chip('isConnected', 'true', 'Endast kopplade', 'link') +\n          chip('isContactAnswer', 'true', 'Kontakt svarade', 'user-check') +\n          chip('voicemail', 'false', 'Dölj röstbrevlåda', 'voicemail') +\n          chip('isRecordingAvailable', 'true', 'Har inspelning', 'microphone') +\n          chip('isLatest', 'true', 'Senaste per kontakt', 'clock') +\n          '</div>' +\n          '<div class=\"qav-tagpick\" style=\"margin-top:8px;\">' + dirChip('any', 'Alla riktningar') + dirChip('out', 'Utgående') + dirChip('in', 'Ingående') + '</div>' +\n          '<div class=\"qav-dur-row\">' +\n          '<label>Min längd (sek)<input type=\"number\" min=\"0\" class=\"qav-dur-in\" data-cf-dur=\"min\" value=\"' + (cf.minDurationSec != null ? cf.minDurationSec : '') + '\"></label>' +\n          '<label>Max längd (sek)<input type=\"number\" min=\"0\" class=\"qav-dur-in\" data-cf-dur=\"max\" value=\"' + (cf.maxDurationSec != null ? cf.maxDurationSec : '') + '\"></label>' +\n          '</div>';\n      }\n\n      function renderWorkConfig(d) {\n        if (!state.works.length) return '<div class=\"qav-audio-note\">Inga manuella orderarbeten finns i organisationen.</div>';\n        var hidden = d.hiddenWorkIds || [];\n        return state.works.map(function (w) {\n          var isHidden = hidden.indexOf(w.id) !== -1;\n          var isPinned = d.pinnedWorkId === w.id;\n          return '<div class=\"qav-wcfg-row' + (isHidden ? ' qav-hidden' : '') + '\">' +\n            '<span class=\"qav-wcfg-ic\"><i class=\"fas fa-' + esc(workIcon(w)) + '\"></i></span>' +\n            '<span class=\"qav-wcfg-name\">' + esc(w.name) + '</span>' +\n            '<button class=\"qav-wcfg-pin' + (isPinned ? ' qav-active' : '') + '\" data-work-pin=\"' + esc(w.id) + '\" title=\"Nåla överst\"><i class=\"fas fa-thumbtack\"></i></button>' +\n            '<span class=\"qav-toggle' + (isHidden ? '' : ' qav-on') + '\" data-work-show=\"' + esc(w.id) + '\" title=\"Visa för granskaren\"></span>' +\n            '</div>';\n        }).join('');\n      }\n\n      function saveConfig() {\n        var d = state.cfgDraft;\n        if (!(d.sourceTagIds || []).length) { toast('Välj minst en tagg för granskningskön (steg 1)'); return; }\n        d.scenarios = (d.scenarios || []).filter(function (s) { return (s.label || '').trim(); });\n        if (!d.scenarios.length) { toast('Lägg till minst ett scenario/knapp (steg 2)'); return; }\n        if (!state.viewId) { toast('Kan inte spara: vy-id saknas'); return; }\n        state.cfgSaving = true; renderConfig();\n        var presetState = {\n          configured: true,\n          sourceTagIds: d.sourceTagIds,\n          reviewedTagIds: d.reviewedTagIds || [],\n          removeSourceOnDecision: !!d.removeSourceOnDecision,\n          matchByPhone: !!d.matchByPhone,\n          projectIds: d.projectIds || [],\n          nameFieldIds: d.nameFieldIds || [],\n          phoneFieldId: d.phoneFieldId || null,\n          callTagIds: d.callTagIds || [],\n          callFilter: d.callFilter || {},\n          hiddenWorkIds: d.hiddenWorkIds || [],\n          pinnedWorkId: d.pinnedWorkId || null,\n          scenarios: d.scenarios\n        };\n        api('PUT', '/users/views-v1/' + state.viewId, null, { presetState: presetState }).then(function () {\n          state.cfgSaving = false;\n          state.config = mergeConfig(presetState);\n          closeConfig();\n          toast('Inställningarna sparades');\n          loadQueue();\n        }).catch(function (err) {\n          state.cfgSaving = false; renderConfig();\n          toast(err && err.status === 403 ? 'Behörighet saknas för att spara vyn' : 'Kunde inte spara inställningarna');\n        });\n      }\n\n      /* ================= PER-USER LAYOUT (Anpassa) ================= */\n      function openCustomize() {\n        state.custDraft = JSON.parse(JSON.stringify(state.userPrefs || { blocks: {} }));\n        if (!state.custDraft.blocks) state.custDraft.blocks = {};\n        renderCustomize();\n        var b = el('qav-cust-backdrop'); if (b) b.className = 'qav-modal-backdrop qav-open';\n      }\n      function closeCustomize() { var b = el('qav-cust-backdrop'); if (b) b.className = 'qav-modal-backdrop'; }\n\n      function renderCustomize() {\n        var m = el('qav-cust-modal'); if (!m) return;\n        var blocks = state.custDraft.blocks;\n        var rows = BLOCKS.map(function (b) {\n          var pr = blocks[b.key] || {};\n          var col = pr.col || b.def;\n          var hidden = !!pr.hidden;\n          var seg = [1, 2, 3].map(function (c) {\n            return '<button class=\"qav-colseg' + (col === c && !hidden ? ' qav-active' : '') + '\" data-cust-col=\"' + b.key + ':' + c + '\"' + (hidden ? ' disabled' : '') + '>' + c + '</button>';\n          }).join('');\n          return '<div class=\"qav-wcfg-row' + (hidden ? ' qav-hidden' : '') + '\">' +\n            '<span class=\"qav-wcfg-ic\"><i class=\"fas fa-' + b.icon + '\"></i></span>' +\n            '<span class=\"qav-wcfg-name\">' + esc(b.label) + '</span>' +\n            '<span class=\"qav-colseg-wrap\">' + seg + '</span>' +\n            '<span class=\"qav-toggle' + (hidden ? '' : ' qav-on') + '\" data-cust-show=\"' + b.key + '\" title=\"Visa blocket\"></span>' +\n            '</div>';\n        }).join('');\n        m.innerHTML = '<div class=\"qav-modal-head\"><div class=\"qav-modal-title\">Anpassa min vy</div>' +\n          '<button class=\"qav-icon-btn\" id=\"qav-cust-close\"><i class=\"fas fa-times\"></i></button></div>' +\n          '<div class=\"qav-modal-body\"><div class=\"qav-cfg-desc\">Välj vilka block du ser och i vilken kolumn (1 = vänster, 2 = mitten, 3 = höger). Sparas bara för dig — administratörens standard gäller annars.</div>' +\n          rows + '</div>' +\n          '<div class=\"qav-modal-foot\">' +\n          '<button class=\"qav-primary-btn\" id=\"qav-cust-save\"' + (state.custSaving ? ' disabled' : '') + '>' + (state.custSaving ? 'Sparar…' : '<i class=\"fas fa-check\"></i> Spara') + '</button>' +\n          '<button class=\"qav-ghost-btn\" id=\"qav-cust-reset\">Återställ</button>' +\n          '<button class=\"qav-ghost-btn\" id=\"qav-cust-cancel\">Avbryt</button>' +\n          '</div>';\n      }\n\n      function saveUserPrefs() {\n        if (!state.viewId) { toast('Kan inte spara: vy-id saknas'); return; }\n        state.custSaving = true; renderCustomize();\n        var ref = (state.me && state.me.reference) || {};\n        var all = ref.qaViewPrefs ? JSON.parse(JSON.stringify(ref.qaViewPrefs)) : {};\n        all[state.viewId] = state.custDraft;\n        api('PUT', '/users/users-v1/me', null, { reference: { qaViewPrefs: all }, replaceReference: false }).then(function () {\n          state.custSaving = false;\n          state.userPrefs = state.custDraft;\n          if (state.me) { state.me.reference = state.me.reference || {}; state.me.reference.qaViewPrefs = all; }\n          closeCustomize();\n          toast('Din vy sparades');\n          render();\n        }).catch(function () {\n          state.custSaving = false; renderCustomize();\n          toast('Kunde inte spara din vy');\n        });\n      }\n\n      /* ---------- events ---------- */\n      function inRoot(node) { var r = el('qav-root'); return r && node && r.contains(node); }\n      function closestAttr(target, attr) { return target && target.closest ? target.closest('[' + attr + ']') : null; }\n\n      function onClick(event) {\n        var target = event.target;\n        if (!target || !target.closest) return;\n\n        if (target.closest('#qav-reload') && inRoot(target)) { if (state.config && state.config.configured) loadQueue(); else boot(); return; }\n        if (target.closest('#qav-done-reload')) { loadQueue(); return; }\n        if (target.closest('#qav-config-btn') || target.closest('#qav-open-config-cta')) { openConfig(); return; }\n        if (target.closest('#qav-customize-btn')) { openCustomize(); return; }\n\n        /* ---- customize (per-user layout) interactions ---- */\n        if (target.closest('#qav-cust-close') || target.closest('#qav-cust-cancel')) { closeCustomize(); return; }\n        if (target.closest('#qav-cust-save')) { saveUserPrefs(); return; }\n        if (target.closest('#qav-cust-reset')) { state.custDraft = { blocks: {} }; renderCustomize(); return; }\n        var custBack = target.closest('#qav-cust-backdrop');\n        if (custBack && target === custBack) { closeCustomize(); return; }\n        var custCol = closestAttr(target, 'data-cust-col');\n        if (custCol && state.custDraft) {\n          var parts = custCol.getAttribute('data-cust-col').split(':');\n          var bk = parts[0], c = +parts[1];\n          var bl = state.custDraft.blocks[bk] = state.custDraft.blocks[bk] || {};\n          bl.col = c; bl.hidden = false;\n          renderCustomize(); return;\n        }\n        var custShow = closestAttr(target, 'data-cust-show');\n        if (custShow && state.custDraft) {\n          var sk = custShow.getAttribute('data-cust-show');\n          var sb = state.custDraft.blocks[sk] = state.custDraft.blocks[sk] || {};\n          sb.hidden = !sb.hidden;\n          renderCustomize(); return;\n        }\n\n        /* ---- review interactions ---- */\n        var copyBtn = closestAttr(target, 'data-copy');\n        if (copyBtn && inRoot(copyBtn)) { copyField(copyBtn); return; }\n\n        var callSel = closestAttr(target, 'data-call-select');\n        if (callSel && inRoot(callSel) && state.current) {\n          var cid = callSel.getAttribute('data-call-select');\n          if (cid !== state.selectedCallId) selectCall(state.current, cid);\n          return;\n        }\n\n        var runWorkBtn = closestAttr(target, 'data-run-work');\n        if (runWorkBtn && inRoot(runWorkBtn)) { runWork(runWorkBtn.getAttribute('data-run-work')); return; }\n\n        var viewFile = closestAttr(target, 'data-view-file');\n        if (viewFile && inRoot(viewFile) && state.current && state.preview) {\n          var vi = +viewFile.getAttribute('data-view-file');\n          if (vi !== state.preview.index || !state.preview.blobUrl) loadPreview(state.current, vi);\n          /* Update the file-list highlight in place (avoid a full re-render that would reset audio). */\n          document.querySelectorAll('#qav-root [data-view-file]').forEach(function (b) {\n            var on = +b.getAttribute('data-view-file') === vi;\n            b.className = 'qav-run-btn qav-view-btn' + (on ? ' qav-active' : '');\n            var row = b.closest('.qav-file-row'); if (row) row.className = 'qav-file-row' + (on ? ' qav-file-active' : '');\n          });\n          return;\n        }\n\n        var play = target.closest('#qav-play');\n        if (play && inRoot(play)) { var a = el('qav-audio'); if (a) { if (a.paused) a.play(); else a.pause(); } return; }\n\n        var rateBtn = closestAttr(target, 'data-rate');\n        if (rateBtn && inRoot(rateBtn)) {\n          var a2 = el('qav-audio');\n          if (a2) a2.playbackRate = parseFloat(rateBtn.getAttribute('data-rate'));\n          var rc = el('qav-rate'); if (rc) rc.querySelectorAll('button').forEach(function (b) { b.className = b === rateBtn ? 'qav-active' : ''; });\n          return;\n        }\n\n        var seek = target.closest('#qav-viz');\n        if (seek && inRoot(seek)) {\n          var au = el('qav-audio');\n          if (au && au.duration) { var rect = seek.getBoundingClientRect(); au.currentTime = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)) * au.duration; }\n          return;\n        }\n\n        var seg = closestAttr(target, 'data-seg');\n        if (seg && inRoot(seg)) {\n          var aud = el('qav-audio');\n          if (aud) { aud.currentTime = (+seg.getAttribute('data-start')) / 1000; aud.play(); }\n          return;\n        }\n\n        var scnBtn = closestAttr(target, 'data-scn');\n        if (scnBtn && inRoot(scnBtn) && scnBtn.classList.contains('qav-decision')) { decide(scnBtn.getAttribute('data-scn')); return; }\n        if (closestAttr(target, 'data-skip') && inRoot(closestAttr(target, 'data-skip'))) { skipCurrent(); return; }\n\n        /* ---- config interactions ---- */\n        if (target.closest('#qav-cfg-close') || target.closest('#qav-cfg-cancel')) { closeConfig(); return; }\n        if (target.closest('#qav-cfg-save')) { saveConfig(); return; }\n        var backdrop = target.closest('#qav-config-backdrop');\n        if (backdrop && target === backdrop) { closeConfig(); return; }\n\n        var cardHead = closestAttr(target, 'data-card-toggle');\n        if (cardHead && state.cfgDraft) {\n          var ck = cardHead.getAttribute('data-card-toggle');\n          state.cfgOpenCard = state.cfgOpenCard === ck ? null : ck;\n          renderConfig(); return;\n        }\n        var fieldToggle = closestAttr(target, 'data-field-toggle');\n        if (fieldToggle && state.cfgDraft) {\n          var fid = fieldToggle.getAttribute('data-field-toggle');\n          var fg = fieldToggle.getAttribute('data-field-group');\n          if (fg === 'name') {\n            var arr = state.cfgDraft.nameFieldIds = state.cfgDraft.nameFieldIds || [];\n            var pos = arr.indexOf(fid); if (pos === -1) arr.push(fid); else arr.splice(pos, 1);\n          } else if (fg === 'phone') {\n            state.cfgDraft.phoneFieldId = state.cfgDraft.phoneFieldId === fid ? null : fid;\n          }\n          renderConfig(); return;\n        }\n\n        var tagToggle = closestAttr(target, 'data-tag-toggle');\n        if (tagToggle && state.cfgDraft) { toggleTag(tagToggle.getAttribute('data-tag-group'), tagToggle.getAttribute('data-tag-toggle')); return; }\n\n        if (target.closest('#qav-add-scn')) { addScenario(); return; }\n        var scnDel = closestAttr(target, 'data-scn-del');\n        if (scnDel) { state.cfgDraft.scenarios.splice(+scnDel.getAttribute('data-scn-del'), 1); renderConfig(); return; }\n        var scnIcon = closestAttr(target, 'data-scn-icon');\n        if (scnIcon) { state.cfgDraft.scenarios[+scnIcon.getAttribute('data-scn-icon')].icon = scnIcon.getAttribute('data-icon'); renderConfig(); return; }\n        var scnColor = closestAttr(target, 'data-scn-color');\n        if (scnColor) { state.cfgDraft.scenarios[+scnColor.getAttribute('data-scn-color')].color = scnColor.getAttribute('data-color'); renderConfig(); return; }\n        var toggle = closestAttr(target, 'data-toggle');\n        if (toggle && state.cfgDraft) { var key = toggle.getAttribute('data-toggle'); state.cfgDraft[key] = !state.cfgDraft[key]; renderConfig(); return; }\n        var reqc = closestAttr(target, 'data-scn-reqcomment');\n        if (reqc) { var idx = +reqc.getAttribute('data-scn-reqcomment'); state.cfgDraft.scenarios[idx].requireComment = !state.cfgDraft.scenarios[idx].requireComment; renderConfig(); return; }\n        var cfChip = closestAttr(target, 'data-cf');\n        if (cfChip && state.cfgDraft) {\n          var key = cfChip.getAttribute('data-cf');\n          var val = cfChip.getAttribute('data-cfval') === 'true';\n          var cf = state.cfgDraft.callFilter = state.cfgDraft.callFilter || {};\n          if (cf[key] === val) delete cf[key]; else cf[key] = val;\n          renderConfig(); return;\n        }\n        var cfDir = closestAttr(target, 'data-cf-dir');\n        if (cfDir && state.cfgDraft) {\n          var dv = cfDir.getAttribute('data-cf-dir');\n          var cfd = state.cfgDraft.callFilter = state.cfgDraft.callFilter || {};\n          if (dv === 'any') delete cfd.isInbound; else cfd.isInbound = (dv === 'in');\n          renderConfig(); return;\n        }\n        var workShow = closestAttr(target, 'data-work-show');\n        if (workShow && state.cfgDraft) {\n          var wid = workShow.getAttribute('data-work-show');\n          var h = state.cfgDraft.hiddenWorkIds = state.cfgDraft.hiddenWorkIds || [];\n          var hp = h.indexOf(wid);\n          if (hp === -1) { h.push(wid); if (state.cfgDraft.pinnedWorkId === wid) state.cfgDraft.pinnedWorkId = null; }\n          else h.splice(hp, 1);\n          renderConfig(); return;\n        }\n        var workPin = closestAttr(target, 'data-work-pin');\n        if (workPin && state.cfgDraft) {\n          var pid = workPin.getAttribute('data-work-pin');\n          state.cfgDraft.pinnedWorkId = state.cfgDraft.pinnedWorkId === pid ? null : pid;\n          var hh = state.cfgDraft.hiddenWorkIds || [];\n          var hpi = hh.indexOf(pid); if (hpi !== -1) hh.splice(hpi, 1); /* pinning un-hides */\n          renderConfig(); return;\n        }\n      }\n\n      function copyField(btn) {\n        var val = btn.getAttribute('data-copy') || '';\n        function done() {\n          btn.className = 'qav-field-copy qav-copied';\n          btn.innerHTML = '<i class=\"fas fa-check\"></i>';\n          setTimeout(function () { btn.className = 'qav-field-copy'; btn.innerHTML = '<i class=\"fas fa-copy\"></i>'; }, 1400);\n          toast('Kopierat: ' + (val.length > 40 ? val.slice(0, 40) + '…' : val));\n        }\n        if (navigator.clipboard && navigator.clipboard.writeText) {\n          navigator.clipboard.writeText(val).then(done).catch(function () { legacyCopy(val); done(); });\n        } else { legacyCopy(val); done(); }\n      }\n      function legacyCopy(val) {\n        try {\n          var ta = document.createElement('textarea');\n          ta.value = val; ta.style.cssText = 'position:fixed;opacity:0;';\n          document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();\n        } catch (e) {}\n      }\n\n      function toggleTag(group, tagId) {\n        var d = state.cfgDraft; if (!d) return;\n        var arr;\n        if (group === 'source') arr = d.sourceTagIds = d.sourceTagIds || [];\n        else if (group === 'reviewed') arr = d.reviewedTagIds = d.reviewedTagIds || [];\n        else if (group === 'callTags') arr = d.callTagIds = d.callTagIds || [];\n        else if (group.indexOf('add:') === 0) { var ai = +group.slice(4); arr = d.scenarios[ai].addTagIds = d.scenarios[ai].addTagIds || []; }\n        else if (group.indexOf('remove:') === 0) { var ri = +group.slice(7); arr = d.scenarios[ri].removeTagIds = d.scenarios[ri].removeTagIds || []; }\n        else return;\n        var pos = arr.indexOf(tagId);\n        if (pos === -1) arr.push(tagId); else arr.splice(pos, 1);\n        renderConfig();\n      }\n      function addScenario() {\n        var n = state.cfgDraft.scenarios.length;\n        state.cfgDraft.scenarios.push({ id: 'scn_' + n + '_' + (state.orderTags.length), label: 'Nytt scenario', icon: 'flag', color: COLORS[(n + 2) % COLORS.length], addTagIds: [], removeTagIds: [], requireComment: false });\n        renderConfig();\n      }\n\n      /* keep label inputs in draft as user types */\n      function onInput(event) {\n        var lbl = closestAttr(event.target, 'data-scn-label');\n        if (lbl && state.cfgDraft) { state.cfgDraft.scenarios[+lbl.getAttribute('data-scn-label')].label = event.target.value; return; }\n        var durIn = closestAttr(event.target, 'data-cf-dur');\n        if (durIn && state.cfgDraft) {\n          var which = durIn.getAttribute('data-cf-dur');\n          var cf = state.cfgDraft.callFilter = state.cfgDraft.callFilter || {};\n          var n = parseInt(event.target.value, 10);\n          var k = which === 'min' ? 'minDurationSec' : 'maxDurationSec';\n          if (isNaN(n) || n <= 0) delete cf[k]; else cf[k] = n;\n          return;\n        }\n        var c = event.target.closest ? event.target.closest('#qav-comment') : null;\n        if (c) { var h = el('qav-comment-hint'); if (h) { h.className = 'qav-comment-hint'; h.textContent = ''; } c.className = 'qav-comment'; }\n      }\n      function onKeyDown(event) { if (event.key === 'Escape') { closeConfig(); closeCustomize(); } }\n\n      /* ---------- contrast probe ---------- */\n      function applyContrast() {\n        var root = el('qav-root'); if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        root.style.setProperty('--qav-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n        if (state.viz && state.viz.refreshColors) state.viz.refreshColors();\n      }\n      var themeObserver = new MutationObserver(applyContrast);\n      themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      themeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('input', onInput);\n      document.addEventListener('keydown', onKeyDown);\n\n      window.__qavCleanup = function () {\n        themeObserver.disconnect();\n        if (state.viz && state.viz.raf) { try { cancelAnimationFrame(state.viz.raf); } catch (e) {} }\n        if (state.audioCtx) { try { state.audioCtx.close(); } catch (e) {} state.audioCtx = null; }\n        revokeUrls();\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('input', onInput);\n        document.removeEventListener('keydown', onKeyDown);\n      };\n\n      boot();\n    })();\n  </script>\n</div>","iconName":"clipboard-check","iconColor":"#1565c0","settingsHtml":".hej","description":"","rights":[],"components":[]},{"id":"6a511715736b0f51645c89b0","name":"Säljskolan","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":0,"label":"Säljskolan"}],"html":"<div id=\"sqv-root\">\n  <style>\n    #sqv-root {\n      --sqv-bg: var(--color-background-2, #fafafa);\n      --sqv-card: var(--color-background-1, #ffffff);\n      --sqv-fg: var(--color-text-1, #212121);\n      --sqv-fg2: var(--color-text-2, #616161);\n      --sqv-muted: var(--color-text-3, #757575);\n      --sqv-primary: var(--color-primary, #1565C0);\n      --sqv-danger: var(--color-danger, #E53935);\n      --sqv-ok: #2E7D32;\n      --sqv-warn: #F9A825;\n      --sqv-hover: var(--color-element-hover, rgba(20,20,20,0.035));\n      --sqv-border: color-mix(in srgb, var(--sqv-fg) 13%, transparent);\n      --sqv-border-soft: color-mix(in srgb, var(--sqv-fg) 8%, transparent);\n      --sqv-radius: 10px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--sqv-fg);\n      display: block;\n    }\n    #sqv-root * { box-sizing: border-box; }\n    #sqv-root button { font-family: inherit; cursor: pointer; }\n    #sqv-root input, #sqv-root textarea { font-family: inherit; color: var(--sqv-fg); }\n\n    .sqv-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }\n    .sqv-tabs { display: flex; gap: 6px; }\n    .sqv-tab {\n      border: 1px solid var(--sqv-border-soft); background: var(--sqv-card); color: var(--sqv-fg2);\n      border-radius: 999px; padding: 7px 16px; font-size: 13.5px; font-weight: 700;\n      display: inline-flex; align-items: center; gap: 8px;\n    }\n    .sqv-tab:hover { background: var(--sqv-hover); }\n    .sqv-tab.sqv-on { background: var(--sqv-primary); border-color: var(--sqv-primary); color: var(--sqv-primary-fg, #fff); }\n    .sqv-toolbar-spacer { flex: 1; }\n    .sqv-iconbtn {\n      border: 1px solid var(--sqv-border-soft); background: var(--sqv-card); color: var(--sqv-fg2);\n      border-radius: 8px; padding: 7px 12px; font-size: 13px; font-weight: 700;\n      display: inline-flex; align-items: center; gap: 7px;\n    }\n    .sqv-iconbtn:hover { background: var(--sqv-hover); }\n\n    .sqv-split { display: flex; align-items: stretch; gap: 0; min-height: 560px; height: calc(100vh - 190px); }\n    .sqv-pane { overflow-y: auto; overflow-x: hidden; min-width: 280px; border-radius: 12px; }\n    #sqv-activity {\n      background: var(--sqv-bg); border: 1px solid var(--sqv-border-soft); padding: 16px;\n    }\n    #sqv-customer {\n      background: var(--sqv-bg); border: 1px solid var(--sqv-border-soft); padding: 16px;\n    }\n    .sqv-divider {\n      width: 14px; flex: 0 0 14px; cursor: col-resize; display: flex; align-items: center; justify-content: center;\n      touch-action: none;\n    }\n    .sqv-divider::after {\n      content: ''; width: 4px; height: 46px; border-radius: 4px;\n      background: color-mix(in srgb, var(--sqv-fg) 18%, transparent);\n    }\n    .sqv-divider:hover::after { background: var(--sqv-primary); }\n\n    .sqv-card {\n      background: var(--sqv-card); border: 1px solid var(--sqv-border-soft);\n      border-radius: var(--sqv-radius); padding: 14px 16px; margin-bottom: 12px;\n    }\n    .sqv-card-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }\n    .sqv-card-title .fas, .sqv-card-title .far { color: var(--sqv-primary); }\n    .sqv-h { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 9px; margin: 2px 0 12px; }\n    .sqv-h .fas { color: var(--sqv-primary); }\n    .sqv-muted { color: var(--sqv-muted); font-size: 12.5px; }\n    .sqv-empty { color: var(--sqv-muted); font-size: 13px; padding: 8px 0; }\n\n    .sqv-btn {\n      background: var(--sqv-primary); color: var(--sqv-primary-fg, #fff); border: none;\n      border-radius: var(--border-radius-button, 8px); padding: 9px 18px; font-size: 13.5px; font-weight: 800;\n      display: inline-flex; align-items: center; gap: 8px;\n    }\n    .sqv-btn:hover { filter: brightness(1.08); }\n    .sqv-btn.sqv-ghost { background: transparent; color: var(--sqv-fg2); border: 1px solid var(--sqv-border); }\n    .sqv-btn.sqv-ghost:hover { background: var(--sqv-hover); filter: none; }\n    .sqv-btn.sqv-sm { padding: 6px 12px; font-size: 12.5px; }\n    .sqv-btn:disabled { opacity: 0.5; cursor: default; }\n\n    /* Quiz list */\n    .sqv-quiz-row {\n      display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-bottom: 10px;\n      background: var(--sqv-card); border: 1px solid var(--sqv-border-soft); border-radius: var(--sqv-radius);\n    }\n    .sqv-quiz-ic {\n      width: 42px; height: 42px; border-radius: 10px; flex-shrink: 0;\n      background: color-mix(in srgb, var(--sqv-primary) 12%, transparent);\n      color: var(--sqv-primary); display: flex; align-items: center; justify-content: center; font-size: 17px;\n    }\n    .sqv-quiz-main { flex: 1; min-width: 0; }\n    .sqv-quiz-name { font-weight: 800; font-size: 14.5px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n    .sqv-quiz-sub { font-size: 12px; color: var(--sqv-muted); margin-top: 2px; }\n    .sqv-pill {\n      font-size: 11px; font-weight: 800; border-radius: 999px; padding: 2px 9px;\n      background: color-mix(in srgb, var(--sqv-fg) 8%, transparent); color: var(--sqv-fg2);\n    }\n    .sqv-pill.sqv-pill-ok { background: color-mix(in srgb, var(--sqv-ok) 15%, transparent); color: var(--sqv-ok); }\n    .sqv-pill.sqv-pill-warn { background: color-mix(in srgb, var(--sqv-warn) 20%, transparent); color: #8a6d00; }\n    .sqv-quiz-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }\n\n    /* Play */\n    .sqv-progress { height: 6px; border-radius: 4px; background: color-mix(in srgb, var(--sqv-fg) 9%, transparent); overflow: hidden; margin: 8px 0 16px; }\n    .sqv-progress > div { height: 100%; background: var(--sqv-primary); transition: width 0.25s; }\n    .sqv-q-text { font-size: 16px; font-weight: 800; margin-bottom: 14px; }\n    .sqv-opt {\n      display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;\n      background: var(--sqv-card); border: 1.5px solid var(--sqv-border-soft); border-radius: var(--sqv-radius);\n      padding: 11px 14px; font-size: 14px; font-weight: 600; margin-bottom: 8px; color: var(--sqv-fg);\n    }\n    .sqv-opt:hover { border-color: var(--sqv-primary); }\n    .sqv-opt .sqv-opt-dot {\n      width: 20px; height: 20px; border-radius: 50%; border: 2px solid var(--sqv-border); flex-shrink: 0;\n      display: flex; align-items: center; justify-content: center; font-size: 11px;\n    }\n    .sqv-opt.sqv-sel { border-color: var(--sqv-primary); }\n    .sqv-opt.sqv-sel .sqv-opt-dot { border-color: var(--sqv-primary); background: var(--sqv-primary); color: var(--sqv-primary-fg, #fff); }\n    .sqv-opt.sqv-right { border-color: var(--sqv-ok); background: color-mix(in srgb, var(--sqv-ok) 7%, var(--sqv-card)); }\n    .sqv-opt.sqv-right .sqv-opt-dot { border-color: var(--sqv-ok); background: var(--sqv-ok); color: #fff; }\n    .sqv-opt.sqv-wrong { border-color: var(--sqv-danger); background: color-mix(in srgb, var(--sqv-danger) 7%, var(--sqv-card)); }\n    .sqv-opt.sqv-wrong .sqv-opt-dot { border-color: var(--sqv-danger); background: var(--sqv-danger); color: #fff; }\n    .sqv-text-answer {\n      width: 100%; border: none; border-bottom: 2px solid var(--sqv-border); background: transparent;\n      padding: 9px 2px; font-size: 15px; outline: none;\n    }\n    .sqv-text-answer:focus { border-bottom-color: var(--sqv-primary); }\n    .sqv-feedback { font-size: 13.5px; font-weight: 700; margin: 10px 0; display: flex; align-items: center; gap: 8px; }\n    .sqv-feedback.sqv-fb-ok { color: var(--sqv-ok); }\n    .sqv-feedback.sqv-fb-bad { color: var(--sqv-danger); }\n    .sqv-score-big { font-size: 42px; font-weight: 800; color: var(--sqv-primary); text-align: center; }\n\n    /* Leaderboard */\n    .sqv-lb-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; font-size: 13.5px; border-bottom: 1px solid var(--sqv-border-soft); }\n    .sqv-lb-row:last-child { border-bottom: none; }\n    .sqv-lb-rank { width: 24px; font-weight: 800; color: var(--sqv-muted); flex-shrink: 0; }\n    .sqv-lb-row.sqv-top1 .sqv-lb-rank { color: #F9A825; }\n    .sqv-lb-row.sqv-top2 .sqv-lb-rank { color: #9E9E9E; }\n    .sqv-lb-row.sqv-top3 .sqv-lb-rank { color: #8D6E63; }\n    .sqv-lb-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }\n    .sqv-lb-row.sqv-me-row .sqv-lb-name { color: var(--sqv-primary); font-weight: 800; }\n    .sqv-lb-val { font-weight: 800; }\n    .sqv-chiprow { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }\n    .sqv-chip {\n      border: 1px solid var(--sqv-border-soft); background: var(--sqv-card); color: var(--sqv-fg2);\n      border-radius: 999px; padding: 5px 13px; font-size: 12.5px; font-weight: 700;\n    }\n    .sqv-chip:hover { background: var(--sqv-hover); }\n    .sqv-chip.sqv-on { background: var(--sqv-primary); border-color: var(--sqv-primary); color: var(--sqv-primary-fg, #fff); }\n\n    /* Customer card */\n    .sqv-callstatus {\n      display: flex; align-items: center; gap: 12px; padding: 13px 16px; border-radius: var(--sqv-radius);\n      margin-bottom: 12px; font-weight: 800; font-size: 14px;\n      background: var(--sqv-card); border: 1px solid var(--sqv-border-soft);\n    }\n    .sqv-callstatus .sqv-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; }\n    .sqv-callstatus.sqv-cs-active { border-color: color-mix(in srgb, var(--sqv-ok) 45%, transparent); }\n    .sqv-callstatus.sqv-cs-active .sqv-dot { background: var(--sqv-ok); animation: sqv-pulse 1.2s infinite; }\n    .sqv-callstatus.sqv-cs-ringing .sqv-dot { background: var(--sqv-warn); animation: sqv-pulse 0.7s infinite; }\n    .sqv-callstatus.sqv-cs-wrap .sqv-dot { background: var(--sqv-primary); }\n    .sqv-callstatus.sqv-cs-idle .sqv-dot { background: var(--sqv-muted); }\n    @keyframes sqv-pulse { 0% { opacity: 1; } 50% { opacity: 0.35; } 100% { opacity: 1; } }\n    .sqv-timer { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--sqv-fg2); font-weight: 700; }\n\n    .sqv-cname { font-size: 19px; font-weight: 800; margin-bottom: 2px; }\n    .sqv-cphone { font-size: 13px; color: var(--sqv-muted); margin-bottom: 10px; }\n    .sqv-field { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--sqv-border-soft); font-size: 13.5px; }\n    .sqv-field:last-child { border-bottom: none; }\n    .sqv-field-label { width: 130px; flex-shrink: 0; color: var(--sqv-muted); font-size: 12px; font-weight: 700; }\n    .sqv-field-value { flex: 1; min-width: 0; word-break: break-word; font-weight: 600; }\n    .sqv-field-value.sqv-editable { cursor: pointer; border-radius: 5px; padding: 1px 5px; margin-left: -5px; }\n    .sqv-field-value.sqv-editable:hover { background: var(--sqv-hover); }\n    .sqv-field-input {\n      width: 100%; border: none; border-bottom: 2px solid var(--sqv-primary); background: transparent;\n      font-size: 13.5px; font-weight: 600; padding: 1px 5px; margin-left: -5px; outline: none;\n    }\n    .sqv-hist-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; font-size: 12.5px; border-bottom: 1px solid var(--sqv-border-soft); }\n    .sqv-hist-row:last-child { border-bottom: none; }\n    .sqv-hist-ic { color: var(--sqv-muted); width: 16px; text-align: center; }\n    .sqv-hist-main { flex: 1; }\n    .sqv-hist-when { color: var(--sqv-muted); }\n    .sqv-evalchips { display: flex; gap: 6px; flex-wrap: wrap; margin: 4px 0 10px; }\n    .sqv-comment-box {\n      width: 100%; border: 1px solid var(--sqv-border); border-radius: 8px; background: var(--sqv-card);\n      padding: 9px 11px; font-size: 13.5px; resize: vertical; min-height: 60px; outline: none;\n    }\n    .sqv-comment-box:focus { border-color: var(--sqv-primary); }\n    .sqv-stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n    .sqv-stat-box { background: var(--sqv-card); border: 1px solid var(--sqv-border-soft); border-radius: var(--sqv-radius); padding: 12px 14px; }\n    .sqv-stat-num { font-size: 26px; font-weight: 800; color: var(--sqv-primary); }\n    .sqv-stat-lbl { font-size: 12px; color: var(--sqv-muted); font-weight: 700; }\n    .sqv-tip { display: flex; gap: 10px; align-items: flex-start; font-size: 12.5px; color: var(--sqv-fg2); background: color-mix(in srgb, var(--sqv-primary) 7%, transparent); border-radius: 8px; padding: 10px 12px; }\n    .sqv-tip .fas { color: var(--sqv-primary); margin-top: 1px; }\n\n    /* Snake mini */\n    .sqv-snake-wrap { display: flex; flex-direction: column; align-items: center; gap: 10px; }\n    .sqv-canvas-wrap { position: relative; border-radius: var(--sqv-radius); overflow: hidden; border: 1px solid var(--sqv-border); }\n    #sqv-snake-canvas { display: block; background: #17241b; }\n    .sqv-overlay {\n      position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;\n      background: rgba(10,18,12,0.72); color: #fff; text-align: center; gap: 8px; padding: 16px;\n    }\n    .sqv-overlay.sqv-hidden { display: none; }\n\n    /* Modal */\n    .sqv-modal-bg {\n      position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 1200;\n      display: flex; align-items: center; justify-content: center; padding: 24px;\n    }\n    .sqv-modal {\n      background: var(--color-background-2, #fff); border-radius: 4px; width: 640px; max-width: 96vw;\n      max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 14px 50px rgba(0,0,0,0.3);\n    }\n    .sqv-modal-head { padding: 22px 24px 10px; font-size: 24px; font-weight: 800; color: var(--sqv-fg2); display: flex; align-items: center; gap: 12px; }\n    .sqv-modal-body { padding: 6px 24px 12px; overflow-y: auto; flex: 1; }\n    .sqv-modal-foot { padding: 14px 24px 20px; display: flex; flex-direction: row-reverse; gap: 10px; }\n    .sqv-step { font-size: 12px; font-weight: 800; color: var(--sqv-primary); text-transform: uppercase; letter-spacing: 0.5px; margin: 16px 0 8px; display: flex; align-items: center; gap: 8px; }\n    .sqv-step .sqv-step-n {\n      width: 20px; height: 20px; border-radius: 50%; background: var(--sqv-primary); color: var(--sqv-primary-fg, #fff);\n      display: inline-flex; align-items: center; justify-content: center; font-size: 11px;\n    }\n    .sqv-input {\n      width: 100%; border: none; border-bottom: 2px solid var(--sqv-border); background: transparent;\n      padding: 9px 2px; font-size: 15px; outline: none; margin-bottom: 6px;\n    }\n    .sqv-input:focus { border-bottom-color: var(--sqv-primary); }\n    .sqv-icongrid { display: flex; gap: 8px; flex-wrap: wrap; }\n    .sqv-iconcard {\n      width: 44px; height: 44px; border-radius: 9px; border: 1.5px solid var(--sqv-border-soft); background: var(--sqv-card);\n      color: var(--sqv-fg2); font-size: 16px; display: flex; align-items: center; justify-content: center;\n    }\n    .sqv-iconcard.sqv-on { border-color: var(--sqv-primary); color: var(--sqv-primary); background: color-mix(in srgb, var(--sqv-primary) 10%, transparent); }\n    .sqv-tplcard {\n      display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; background: var(--sqv-card);\n      border: 1.5px solid var(--sqv-border-soft); border-radius: var(--sqv-radius); padding: 11px 13px; margin-bottom: 8px;\n    }\n    .sqv-tplcard:hover { border-color: var(--sqv-primary); }\n    .sqv-qcard { background: var(--sqv-card); border: 1px solid var(--sqv-border-soft); border-radius: var(--sqv-radius); padding: 12px 14px; margin-bottom: 10px; }\n    .sqv-qcard-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }\n    .sqv-qcard-head .sqv-pill { flex-shrink: 0; }\n    .sqv-qopt-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }\n    .sqv-correct-toggle {\n      width: 26px; height: 26px; border-radius: 50%; border: 2px solid var(--sqv-border); background: transparent;\n      color: transparent; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;\n    }\n    .sqv-correct-toggle.sqv-on { border-color: var(--sqv-ok); background: var(--sqv-ok); color: #fff; }\n    .sqv-mini-btn { border: none; background: transparent; color: var(--sqv-muted); font-size: 13px; padding: 4px 7px; border-radius: 6px; }\n    .sqv-mini-btn:hover { background: var(--sqv-hover); color: var(--sqv-danger); }\n    .sqv-typegrid { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }\n\n    .sqv-toast {\n      position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%); z-index: 1400;\n      background: #263238; color: #fff; border-radius: 8px; padding: 11px 20px; font-size: 13.5px; font-weight: 700;\n      box-shadow: 0 8px 30px rgba(0,0,0,0.3);\n    }\n\n    @media (max-width: 980px) {\n      .sqv-split { flex-direction: column; height: auto; }\n      .sqv-divider { display: none; }\n      .sqv-pane { width: 100% !important; min-height: 420px; }\n    }\n  </style>\n\n  <div class=\"sqv-toolbar\" id=\"sqv-toolbar\"></div>\n  <div class=\"sqv-split\" id=\"sqv-splitwrap\">\n    <div class=\"sqv-pane\" id=\"sqv-activity\"><div class=\"sqv-empty\">Laddar…</div></div>\n    <div class=\"sqv-divider\" id=\"sqv-divider\"></div>\n    <div class=\"sqv-pane\" id=\"sqv-customer\"><div class=\"sqv-empty\">Laddar…</div></div>\n  </div>\n  <div id=\"sqv-modal-root\"></div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      if (window.__sqvCleanup) { try { window.__sqvCleanup(); } catch (e) {} }\n\n      /* ============================ state ============================ */\n      var state = {\n        stamp: String(Date.now()),\n        me: null, users: [], isAdmin: false, canCreate: false,\n        viewId: null, cfg: { creatorMode: 'supervisors', tabs: { quiz: true, toplist: true, snake: true }, defaultSide: 'left' },\n        prefs: { side: null, tab: 'quiz', split: 50 },\n        quizzes: [],        /* merged, visible */\n        myResults: {},\n        contactFields: [],  /* GET /contacts/fields-v1 */\n        dialTags: [],\n        tab: 'quiz',\n        play: null,         /* { quiz, i, score, picked, textVal, revealed, done } */\n        editor: null,       /* { quiz, isNew, showTemplates } */\n        settingsOpen: false,\n        /* customer pane */\n        call: null, callStatus: 'idle', contact: null, contactReadOnly: false,\n        history: [], evalPicked: [], evalSavedFor: null, editingFieldId: null,\n        statsToday: null, salesPeriod: 'day', toplistMode: 'quiz',\n        timers: [], dragging: false\n      };\n\n      /* ============================ helpers ============================ */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, c) {\n          var p = /(.+?)=(.+)/.exec(c.trim());\n          if (p) obj[p[1]] = p[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n      function myId() { return cookies()['s2_uid']; }\n      function api(method, path, body) {\n        var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token() } };\n        if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }\n        return fetch(API + path, opts).then(function (r) {\n          if (!r.ok) { var err = new Error('HTTP ' + r.status); err.status = r.status; throw err; }\n          return r.json().catch(function () { return null; });\n        });\n      }\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n      function root() { return document.getElementById('sqv-root'); }\n      function uid() { return 'q' + Math.random().toString(36).slice(2, 10); }\n      function toast(msg, isErr) {\n        var old = document.querySelector('.sqv-toast'); if (old) old.remove();\n        var t = document.createElement('div');\n        t.className = 'sqv-toast'; t.textContent = msg;\n        if (isErr) t.style.background = '#B71C1C';\n        document.body.appendChild(t);\n        setTimeout(function () { t.remove(); }, 3200);\n      }\n      function fmtDur(ms) {\n        var s = Math.max(0, Math.floor(ms / 1000));\n        var m = Math.floor(s / 60); s = s % 60;\n        return m + ':' + (s < 10 ? '0' : '') + s;\n      }\n      function fmtWhen(d) {\n        if (!d) return '';\n        var dt = new Date(d), now = new Date();\n        var sameDay = dt.toDateString() === now.toDateString();\n        var hm = ('0' + dt.getHours()).slice(-2) + ':' + ('0' + dt.getMinutes()).slice(-2);\n        if (sameDay) return 'Idag ' + hm;\n        return dt.getDate() + '/' + (dt.getMonth() + 1) + ' ' + hm;\n      }\n      function dayStart(offsetDays) {\n        var d = new Date(); d.setHours(0, 0, 0, 0);\n        if (offsetDays) d.setDate(d.getDate() + offsetDays);\n        return d;\n      }\n      function weekStart() {\n        var d = dayStart(0); var wd = (d.getDay() + 6) % 7;\n        d.setDate(d.getDate() - wd); return d;\n      }\n      function monthStart() { var d = dayStart(0); d.setDate(1); return d; }\n      function userName(id) {\n        for (var i = 0; i < state.users.length; i++) if (state.users[i].id === id) return state.users[i].fullName || state.users[i].username || '?';\n        return '?';\n      }\n\n      /* ============================ quiz templates ============================ */\n      function T(type, text, extra) {\n        var q = { id: uid(), type: type, text: text, options: [], answer: true, accepted: [] };\n        if (type === 'single' || type === 'multi') {\n          (extra.opts || []).forEach(function (o) { q.options.push({ id: uid(), text: o[0], correct: !!o[1] }); });\n        } else if (type === 'boolean') { q.answer = !!extra.answer; }\n        else if (type === 'text') { q.accepted = extra.accepted || []; }\n        return q;\n      }\n      function TEMPLATES() {\n        return [\n          { name: 'Invändningshantering — grunderna', icon: 'fa-shield-alt', desc: 'Bemöt de vanligaste invändningarna rätt.', questions: [\n            T('single', 'Kunden säger \"det är för dyrt\". Vad är bästa första steget?', { opts: [['Bekräfta invändningen och ställ en följdfråga', 1], ['Sänk priset direkt', 0], ['Byt ämne till något annat', 0], ['Tacka för dig och avsluta samtalet', 0]] }),\n            T('boolean', 'En invändning betyder oftast att kunden är helt ointresserad.', { answer: false }),\n            T('single', 'Kunden säger \"jag måste tänka på saken\". Vad gör du?', { opts: [['Frågar vänligt vad kunden vill fundera över', 1], ['Säger \"okej, jag ringer aldrig igen\"', 0], ['Upprepar hela presentationen en gång till', 0], ['Lägger på direkt', 0]] }),\n            T('multi', 'Vilka tekniker är bra vid invändningar? (flera rätt)', { opts: [['Lyssna klart utan att avbryta', 1], ['Spegla och bekräfta det kunden säger', 1], ['Ställa öppna följdfrågor', 1], ['Prata snabbare och högre', 0]] }),\n            T('single', '\"Vi har redan en leverantör\" — vad är bästa svaret?', { opts: [['Fråga vad de uppskattar med sin nuvarande leverantör', 1], ['Säga att den leverantören är dålig', 0], ['Be om ursäkt för att du ringde', 0], ['Erbjuda rabatt direkt', 0]] }),\n            T('boolean', 'Att bemöta en invändning med en fråga är oftast bättre än med ett motargument.', { answer: true }),\n            T('text', 'Vad kallas tekniken där du upprepar kundens invändning med egna ord?', { accepted: ['spegling', 'spegla', 'speglingsteknik', 'att spegla'] }),\n            T('single', 'När är rätt läge att bemöta prisinvändningen?', { opts: [['Efter att värdet är tydligt för kunden', 1], ['Direkt i öppningsfrasen', 0], ['Aldrig — undvik prisfrågan', 0], ['Först efter avslut', 0]] })\n          ] },\n          { name: 'Avslutstekniker', icon: 'fa-handshake', desc: 'Våga fråga — och fråga rätt.', questions: [\n            T('single', 'Vad är ett alternativavslut?', { opts: [['Att ge kunden två val som båda leder till affär', 1], ['Att erbjuda en alternativ produkt', 0], ['Att avsluta samtalet på ett alternativt sätt', 0], ['Att låta kunden välja mellan ja och nej', 0]] }),\n            T('boolean', 'Man ska alltid vänta på att kunden själv föreslår avslut.', { answer: false }),\n            T('single', 'Vilken signal tyder på köpintresse?', { opts: [['Kunden frågar om leveranstid', 1], ['Kunden svarar enstavigt', 0], ['Kunden byter ämne', 0], ['Kunden ber dig ringa om ett år', 0]] }),\n            T('multi', 'Vilka är vanliga avslutstekniker? (flera rätt)', { opts: [['Direkt avslutsfråga', 1], ['Alternativavslut', 1], ['Sammanfattningsavslut', 1], ['Att aldrig ställa frågan', 0]] }),\n            T('text', 'Vad kallas avslutet där du sammanfattar kundens ja-svar innan avslutsfrågan?', { accepted: ['sammanfattningsavslut', 'sammanfattningsavslutet', 'sammanfattning'] }),\n            T('single', 'Kunden tvekar vid avslutsfrågan. Vad gör du först?', { opts: [['Frågar lugnt vad som känns osäkert', 1], ['Pressar hårdare på en gång', 0], ['Drar tillbaka erbjudandet', 0], ['Börjar om från början', 0]] }),\n            T('boolean', 'Tystnad efter avslutsfrågan är okej — låt kunden svara först.', { answer: true }),\n            T('single', 'Hur många gånger kan man ställa avslutsfrågan i ett samtal?', { opts: [['Flera gånger, om läget är rätt', 1], ['Max en gång', 0], ['Aldrig — kunden ska fråga', 0], ['Exakt tre gånger, alltid', 0]] })\n          ] },\n          { name: 'Samtalets faser', icon: 'fa-route', desc: 'Från öppning till avslut i rätt ordning.', questions: [\n            T('single', 'Vilken är den naturliga ordningen i ett säljsamtal?', { opts: [['Öppning, behovsanalys, presentation, invändningar, avslut', 1], ['Presentation, öppning, avslut, behovsanalys', 0], ['Avslut, invändningar, öppning', 0], ['Behovsanalys, öppning, avslut, presentation', 0]] }),\n            T('boolean', 'Behovsanalysen är viktigare än produktpresentationen.', { answer: true }),\n            T('multi', 'Vad hör hemma i öppningen? (flera rätt)', { opts: [['Presentera dig och företaget', 1], ['Berätta syftet med samtalet', 1], ['Väcka intresse', 1], ['Gå igenom hela prislistan i detalj', 0]] }),\n            T('single', 'Hur stor del av samtalet bör kunden prata under behovsanalysen?', { opts: [['Mer än hälften', 1], ['Nästan inget', 0], ['Exakt 10 procent', 0], ['Kunden ska inte prata alls', 0]] }),\n            T('text', 'Vad kallas fasen där du kartlägger kundens situation och utmaningar?', { accepted: ['behovsanalys', 'behovsanalysen', 'behovsfasen', 'behovsanalys-fasen'] }),\n            T('single', 'När presenterar du priset?', { opts: [['När värdet är etablerat', 1], ['Först av allt i samtalet', 0], ['Aldrig', 0], ['Bara om kunden hotar att lägga på', 0]] }),\n            T('boolean', 'Det är okej att hoppa över behovsanalysen om kunden verkar stressad.', { answer: false }),\n            T('single', 'Vad avslutar du alltid samtalet med?', { opts: [['En tydlig överenskommelse om nästa steg', 1], ['Ett skämt', 0], ['En lång sammanfattning av allt du sagt', 0], ['Att snabbt lägga på', 0]] })\n          ] },\n          { name: 'Frågeteknik & aktivt lyssnande', icon: 'fa-comments', desc: 'Ställ frågor som öppnar samtalet.', questions: [\n            T('single', 'Vad är en öppen fråga?', { opts: [['En fråga som inte kan besvaras med bara ja eller nej', 1], ['En fråga som alla får höra', 0], ['En fråga utan svar', 0], ['En fråga om öppettider', 0]] }),\n            T('text', 'Skriv ett frågeord som inleder en öppen fråga.', { accepted: ['hur', 'vad', 'varför', 'när', 'vilka', 'vilken', 'vem', 'var', 'berätta'] }),\n            T('multi', 'Vad kännetecknar aktivt lyssnande? (flera rätt)', { opts: [['Bekräfta med småord som \"jag förstår\"', 1], ['Sammanfatta det kunden sagt', 1], ['Anteckna viktiga detaljer', 1], ['Planera nästa argument medan kunden pratar', 0]] }),\n            T('boolean', 'Ledande frågor är det bästa verktyget i behovsanalysen.', { answer: false }),\n            T('single', 'Kunden ger ett kort svar. Vad gör du?', { opts: [['Ställer en följdfråga, t.ex. \"berätta mer?\"', 1], ['Går direkt till avslut', 0], ['Fyller tystnaden med produktfakta', 0], ['Avslutar samtalet', 0]] }),\n            T('single', 'Varför sammanfattar man kundens svar?', { opts: [['För att visa att man lyssnat och bekräfta bilden', 1], ['För att dra ut på tiden', 0], ['För att kunden glömt vad den sagt', 0], ['Det ska man inte göra', 0]] }),\n            T('boolean', 'Man bör avbryta kunden om svaret drar ut på tiden.', { answer: false }),\n            T('single', 'Vad står P för i SPIN-modellen?', { opts: [['Problemfrågor', 1], ['Prisfrågor', 0], ['Personfrågor', 0], ['Pausfrågor', 0]] })\n          ] },\n          { name: 'Kalla samtal — öppningen', icon: 'fa-phone', desc: 'Vinn de första 30 sekunderna.', questions: [\n            T('single', 'Hur lång tid har du normalt på dig att väcka intresse i ett kallt samtal?', { opts: [['Under 30 sekunder', 1], ['Cirka 10 minuter', 0], ['En timme', 0], ['Tiden spelar ingen roll', 0]] }),\n            T('boolean', 'Att öppna med \"stör jag?\" är en stark öppning.', { answer: false }),\n            T('single', 'Vad är den bästa öppningen?', { opts: [['Kort presentation och en tydlig anledning till samtalet', 1], ['En lång historia om företaget', 0], ['Att direkt fråga om kunden vill köpa', 0], ['Att låtsas ringa fel', 0]] }),\n            T('multi', 'Vad ska du undvika i öppningen? (flera rätt)', { opts: [['Att ursäkta dig för att du ringer', 1], ['Långa monologer', 1], ['Att läsa innantill från manus', 1], ['Att använda kundens namn', 0]] }),\n            T('text', 'Vad kallas en kort, förberedd presentation av vem du är och varför du ringer?', { accepted: ['hisspitch', 'elevator pitch', 'pitch', 'hisspresentation'] }),\n            T('single', 'En gatekeeper svarar. Vad gör du?', { opts: [['Är artig, tydlig med ditt ärende och ber att bli kopplad', 1], ['Låtsas vara en gammal vän', 0], ['Lägger på direkt', 0], ['Kräver att få prata med chefen', 0]] })\n          ] }\n        ];\n      }\n\n      /* ============================ data loading ============================ */\n      function detectViewId() {\n        var m = /\\/views\\/([0-9a-f]{24})/.exec(location.pathname);\n        state.viewId = m ? m[1] : null;\n      }\n\n      function loadCore() {\n        detectViewId();\n        return Promise.all([\n          api('GET', '/users/users-v1/me'),\n          api('GET', '/users/users-v1'),\n          api('GET', '/contacts/fields-v1').catch(function () { return []; }),\n          api('GET', '/dial/tags-v1').catch(function () { return []; }),\n          state.viewId ? api('GET', '/users/views-v1').catch(function () { return []; }) : Promise.resolve([])\n        ]).then(function (r) {\n          state.me = r[0];\n          state.users = (r[1] || []).filter(function (u) { return u.type !== 'systemUser'; });\n          state.contactFields = Array.isArray(r[2]) ? r[2] : ((r[2] && r[2].fields) || []);\n          state.dialTags = Array.isArray(r[3]) ? r[3] : ((r[3] && r[3].tags) || []);\n          state.isAdmin = state.me && state.me.type === 'admin';\n          var views = Array.isArray(r[4]) ? r[4] : [];\n          for (var i = 0; i < views.length; i++) {\n            if (views[i].id === state.viewId && views[i].presetState) {\n              var ps = views[i].presetState;\n              if (ps.creatorMode) state.cfg.creatorMode = ps.creatorMode;\n              if (ps.tabs) state.cfg.tabs = { quiz: ps.tabs.quiz !== false, toplist: ps.tabs.toplist !== false, snake: ps.tabs.snake !== false };\n              if (ps.defaultSide) state.cfg.defaultSide = ps.defaultSide;\n            }\n          }\n          var ref = (state.me && state.me.reference) || {};\n          state.myResults = ref.sqzResults || {};\n          var prefs = ref.sqzPrefs || {};\n          state.prefs.side = prefs.side || state.cfg.defaultSide;\n          state.prefs.split = typeof prefs.split === 'number' ? Math.min(75, Math.max(25, prefs.split)) : 50;\n          state.tab = prefs.tab && state.cfg.tabs[prefs.tab] ? prefs.tab : (state.cfg.tabs.quiz ? 'quiz' : state.cfg.tabs.toplist ? 'toplist' : 'snake');\n          var mode = state.cfg.creatorMode;\n          state.canCreate = mode === 'all' || state.isAdmin ||\n            (mode === 'supervisors' && ((state.me.supervisedTeamIds || []).length > 0));\n          computeQuizzes();\n          gc();\n        });\n      }\n\n      function refreshUsers() {\n        return api('GET', '/users/users-v1').then(function (users) {\n          state.users = (users || []).filter(function (u) { return u.type !== 'systemUser'; });\n          var meFresh = null;\n          for (var i = 0; i < state.users.length; i++) if (state.users[i].id === state.me.id) meFresh = state.users[i];\n          if (meFresh) { state.me = meFresh; state.myResults = (meFresh.reference || {}).sqzResults || {}; }\n          computeQuizzes();\n        }).catch(function () {});\n      }\n\n      function computeQuizzes() {\n        var list = [];\n        state.users.forEach(function (u) {\n          /* own quizzes: read from state.me (fresher after local saves than the users-v1 copy) */\n          var ref = u.id === state.me.id ? (state.me.reference || {}) : (u.reference || {});\n          var qs = ref.sqzQuizzes || {};\n          Object.keys(qs).forEach(function (id) {\n            var q = qs[id];\n            if (!q || !q.questions || !q.questions.length) return;\n            var mine = u.id === state.me.id;\n            if (!q.isActive && !mine) return;\n            list.push({ quiz: q, ownerId: u.id, mine: mine });\n          });\n        });\n        list.sort(function (a, b) { return String(b.quiz.createdAt || '').localeCompare(String(a.quiz.createdAt || '')); });\n        state.quizzes = list;\n      }\n\n      /* GC: prune own results for quizzes that no longer exist (throttled 6h) */\n      function gc() {\n        var ref = (state.me && state.me.reference) || {};\n        var meta = ref.sqzMeta || {};\n        if (meta.gcAt && Date.now() - new Date(meta.gcAt).getTime() < 6 * 3600 * 1000) return;\n        var known = {};\n        state.users.forEach(function (u) {\n          Object.keys((u.reference || {}).sqzQuizzes || {}).forEach(function (id) { known[id] = true; });\n        });\n        var results = ref.sqzResults || {};\n        var ids = Object.keys(results);\n        var changed = false;\n        ids.forEach(function (id) { if (!known[id]) { delete results[id]; changed = true; } });\n        ids = Object.keys(results);\n        if (ids.length > 60) {\n          ids.sort(function (a, b) { return String(results[a].lastAt || '').localeCompare(String(results[b].lastAt || '')); });\n          ids.slice(0, ids.length - 60).forEach(function (id) { delete results[id]; });\n          changed = true;\n        }\n        var payload = { sqzMeta: { gcAt: new Date().toISOString() } };\n        if (changed) payload.sqzResults = results;\n        api('PUT', '/users/users-v1/me', { reference: payload, replaceReference: false }).catch(function () {});\n      }\n\n      function savePrefs() {\n        api('PUT', '/users/users-v1/me', {\n          reference: { sqzPrefs: { side: state.prefs.side, tab: state.tab, split: state.prefs.split } },\n          replaceReference: false\n        }).catch(function () {});\n      }\n\n      function saveMyQuizzes() {\n        var ref = (state.me && state.me.reference) || {};\n        return api('PUT', '/users/users-v1/me', {\n          reference: { sqzQuizzes: ref.sqzQuizzes || {} },\n          replaceReference: false\n        });\n      }\n\n      /* ============================ rendering ============================ */\n      function renderAll() {\n        var r = root(); if (!r) return;\n        renderToolbar();\n        applySplit();\n        renderActivity();\n        renderCustomer();\n      }\n\n      function renderToolbar() {\n        var bar = el('sqv-toolbar'); if (!bar) return;\n        var tabs = '';\n        var defs = [\n          { key: 'quiz', label: 'Quiz', icon: 'fa-graduation-cap' },\n          { key: 'toplist', label: 'Topplista', icon: 'fa-trophy' },\n          { key: 'snake', label: 'Snake', icon: 'fa-gamepad' }\n        ];\n        defs.forEach(function (d) {\n          if (!state.cfg.tabs[d.key]) return;\n          tabs += '<button class=\"sqv-tab' + (state.tab === d.key ? ' sqv-on' : '') + '\" data-sqv-tab=\"' + d.key + '\">' +\n            '<i class=\"fas ' + d.icon + '\"></i> ' + d.label + '</button>';\n        });\n        bar.innerHTML = '<div class=\"sqv-tabs\" data-sqv-stamp=\"' + state.stamp + '\">' + tabs + '</div>' +\n          '<div class=\"sqv-toolbar-spacer\"></div>' +\n          '<button class=\"sqv-iconbtn\" data-sqv-act=\"swap-side\" title=\"Byt sida på aktiviteten\">' +\n          '<i class=\"fas fa-exchange-alt\"></i> Byt sida</button>' +\n          (state.isAdmin && state.viewId ? '<button class=\"sqv-iconbtn\" data-sqv-act=\"settings\" title=\"Inställningar\"><i class=\"fas fa-cog\"></i></button>' : '');\n      }\n\n      function applySplit() {\n        var act = el('sqv-activity'), cust = el('sqv-customer'), div = el('sqv-divider');\n        if (!act || !cust || !div) return;\n        var left = state.prefs.side === 'left';\n        act.style.order = left ? 0 : 2;\n        div.style.order = 1;\n        cust.style.order = left ? 2 : 0;\n        var pct = state.prefs.split;\n        act.style.width = 'calc(' + pct + '% - 7px)';\n        act.style.flex = '0 0 auto';\n        cust.style.width = 'calc(' + (100 - pct) + '% - 7px)';\n        cust.style.flex = '0 0 auto';\n      }\n\n      /* ---------- activity pane ---------- */\n      function renderActivity() {\n        var pane = el('sqv-activity'); if (!pane) return;\n        stopSnake();\n        if (state.tab === 'quiz') pane.innerHTML = state.play ? playHtml() : quizListHtml();\n        else if (state.tab === 'toplist') pane.innerHTML = toplistHtml();\n        else if (state.tab === 'snake') { pane.innerHTML = snakeHtml(); snakeDraw(); }\n      }\n\n      function quizListHtml() {\n        var h = '<div class=\"sqv-h\"><i class=\"fas fa-graduation-cap\"></i> Säljskolan' +\n          (state.canCreate ? '<span style=\"flex:1\"></span><button class=\"sqv-btn sqv-sm\" data-sqv-act=\"new-quiz\"><i class=\"fas fa-plus\"></i> Nytt quiz</button>' : '') +\n          '</div>';\n        if (!state.quizzes.length) {\n          h += '<div class=\"sqv-card\"><div class=\"sqv-empty\">Inga quiz ännu.' +\n            (state.canCreate ? ' Skapa det första med \"Nytt quiz\" — du kan utgå från en färdig mall.' : ' Be din teamledare lägga upp ett.') +\n            '</div></div>';\n          return h;\n        }\n        state.quizzes.forEach(function (item) {\n          var q = item.quiz;\n          var res = state.myResults[q.id];\n          var badge = '';\n          if (res && res.best >= (res.total || q.questions.length)) badge = '<span class=\"sqv-pill sqv-pill-ok\"><i class=\"fas fa-check\"></i> ' + res.best + '/' + (res.total || q.questions.length) + '</span>';\n          else if (res) badge = '<span class=\"sqv-pill\">Bäst: ' + res.best + '/' + (res.total || q.questions.length) + '</span>';\n          if (!q.isActive) badge += ' <span class=\"sqv-pill sqv-pill-warn\">Inaktiv</span>';\n          h += '<div class=\"sqv-quiz-row\">' +\n            '<div class=\"sqv-quiz-ic\"><i class=\"fas ' + esc(q.icon || 'fa-graduation-cap') + '\"></i></div>' +\n            '<div class=\"sqv-quiz-main\">' +\n              '<div class=\"sqv-quiz-name\">' + esc(q.name) + ' ' + badge + '</div>' +\n              '<div class=\"sqv-quiz-sub\">' + q.questions.length + (q.questions.length === 1 ? ' fråga' : ' frågor') + ' · av ' + esc(q.createdByName || userName(item.ownerId)) +\n              (q.desc ? ' · ' + esc(q.desc) : '') + '</div>' +\n            '</div>' +\n            '<div class=\"sqv-quiz-actions\">' +\n              (item.mine ? '<button class=\"sqv-mini-btn\" data-sqv-act=\"edit-quiz\" data-sqv-id=\"' + q.id + '\" title=\"Redigera\"><i class=\"fas fa-pen\"></i></button>' : '') +\n              (q.isActive ? '<button class=\"sqv-btn sqv-sm\" data-sqv-act=\"play\" data-sqv-id=\"' + q.id + '\"><i class=\"fas fa-play\"></i> Starta</button>' : '') +\n            '</div>' +\n          '</div>';\n        });\n        return h;\n      }\n\n      function findQuiz(id) {\n        for (var i = 0; i < state.quizzes.length; i++) if (state.quizzes[i].quiz.id === id) return state.quizzes[i].quiz;\n        return null;\n      }\n\n      /* ---------- play ---------- */\n      function startPlay(id) {\n        var q = findQuiz(id); if (!q) return;\n        state.play = { quiz: q, i: 0, score: 0, picked: [], textVal: '', revealed: false, done: false };\n        renderActivity();\n      }\n\n      function playHtml() {\n        var p = state.play, q = p.quiz;\n        if (p.done) {\n          var total = q.questions.length;\n          var prev = state.myResults[q.id];\n          var isRecord = !prev || p.score > prev.best;\n          return '<div class=\"sqv-h\"><i class=\"fas ' + esc(q.icon || 'fa-graduation-cap') + '\"></i> ' + esc(q.name) + '</div>' +\n            '<div class=\"sqv-card\" style=\"text-align:center;padding:28px 16px\">' +\n            '<div class=\"sqv-score-big\">' + p.score + ' / ' + total + '</div>' +\n            '<div style=\"font-weight:800;margin:8px 0 4px\">' +\n              (p.score === total ? 'Alla rätt — grymt jobbat!' : p.score >= total * 0.7 ? 'Starkt resultat!' : 'Bra försök — kör igen!') + '</div>' +\n            (isRecord && p.score > 0 ? '<div class=\"sqv-muted\" style=\"margin-bottom:12px\"><i class=\"fas fa-crown\" style=\"color:#F9A825\"></i> Nytt personligt rekord</div>' : '<div style=\"height:12px\"></div>') +\n            '<button class=\"sqv-btn\" data-sqv-act=\"play\" data-sqv-id=\"' + q.id + '\"><i class=\"fas fa-redo\"></i> Spela igen</button> ' +\n            '<button class=\"sqv-btn sqv-ghost\" data-sqv-act=\"exit-play\">Till listan</button>' +\n            '</div>';\n        }\n        var question = q.questions[p.i];\n        var h = '<div class=\"sqv-h\"><i class=\"fas ' + esc(q.icon || 'fa-graduation-cap') + '\"></i> ' + esc(q.name) +\n          '<span style=\"flex:1\"></span><button class=\"sqv-mini-btn\" data-sqv-act=\"exit-play\" title=\"Avbryt\"><i class=\"fas fa-times\"></i></button></div>' +\n          '<div class=\"sqv-muted\">Fråga ' + (p.i + 1) + ' av ' + q.questions.length + ' · ' + p.score + ' rätt hittills</div>' +\n          '<div class=\"sqv-progress\"><div style=\"width:' + Math.round((p.i / q.questions.length) * 100) + '%\"></div></div>' +\n          '<div class=\"sqv-q-text\">' + esc(question.text) + '</div>';\n\n        if (question.type === 'single' || question.type === 'multi') {\n          if (question.type === 'multi') h += '<div class=\"sqv-muted\" style=\"margin-bottom:8px\">Flera svar kan vara rätt — markera alla och rätta.</div>';\n          question.options.forEach(function (o, idx) {\n            var cls = 'sqv-opt';\n            var picked = p.picked.indexOf(idx) !== -1;\n            if (!p.revealed && picked) cls += ' sqv-sel';\n            if (p.revealed) {\n              if (o.correct) cls += ' sqv-right';\n              else if (picked) cls += ' sqv-wrong';\n            }\n            h += '<button class=\"' + cls + '\" data-sqv-act=\"pick\" data-sqv-idx=\"' + idx + '\"' + (p.revealed ? ' disabled' : '') + '>' +\n              '<span class=\"sqv-opt-dot\">' + (p.revealed && o.correct ? '<i class=\"fas fa-check\"></i>' : picked ? '<i class=\"fas fa-check\"></i>' : '') + '</span>' +\n              '<span>' + esc(o.text) + '</span></button>';\n          });\n        } else if (question.type === 'boolean') {\n          [['Sant', true], ['Falskt', false]].forEach(function (pair, idx) {\n            var cls = 'sqv-opt';\n            var picked = p.picked.indexOf(idx) !== -1;\n            if (!p.revealed && picked) cls += ' sqv-sel';\n            if (p.revealed) {\n              if (question.answer === pair[1]) cls += ' sqv-right';\n              else if (picked) cls += ' sqv-wrong';\n            }\n            h += '<button class=\"' + cls + '\" data-sqv-act=\"pick\" data-sqv-idx=\"' + idx + '\"' + (p.revealed ? ' disabled' : '') + '>' +\n              '<span class=\"sqv-opt-dot\">' + (picked ? '<i class=\"fas fa-check\"></i>' : '') + '</span><span>' + pair[0] + '</span></button>';\n          });\n        } else if (question.type === 'text') {\n          h += '<input class=\"sqv-text-answer\" id=\"sqv-text-answer\" placeholder=\"Skriv ditt svar…\" value=\"' + esc(p.textVal) + '\"' + (p.revealed ? ' disabled' : '') + '>';\n          if (p.revealed) {\n            h += '<div class=\"sqv-muted\" style=\"margin-top:8px\">Godkända svar: ' + esc((question.accepted || []).join(', ')) + '</div>';\n          }\n        }\n\n        if (p.revealed) {\n          var ok = p.lastCorrect;\n          h += '<div class=\"sqv-feedback ' + (ok ? 'sqv-fb-ok' : 'sqv-fb-bad') + '\"><i class=\"fas ' + (ok ? 'fa-check-circle' : 'fa-times-circle') + '\"></i> ' +\n            (ok ? 'Rätt svar!' : 'Fel svar.') + '</div>' +\n            '<button class=\"sqv-btn\" data-sqv-act=\"next-q\">' + (p.i + 1 >= q.questions.length ? 'Se resultat' : 'Nästa fråga') + ' <i class=\"fas fa-arrow-right\"></i></button>';\n        } else {\n          h += '<div style=\"margin-top:10px\"><button class=\"sqv-btn\" data-sqv-act=\"check-q\">Rätta <i class=\"fas fa-check\"></i></button></div>';\n        }\n        return h;\n      }\n\n      function checkAnswer() {\n        var p = state.play; if (!p || p.revealed) return;\n        var question = p.quiz.questions[p.i];\n        var correct = false;\n        if (question.type === 'single') {\n          correct = p.picked.length === 1 && !!(question.options[p.picked[0]] || {}).correct;\n        } else if (question.type === 'multi') {\n          var want = [];\n          question.options.forEach(function (o, idx) { if (o.correct) want.push(idx); });\n          correct = want.length === p.picked.length && want.every(function (i) { return p.picked.indexOf(i) !== -1; });\n        } else if (question.type === 'boolean') {\n          correct = p.picked.length === 1 && (p.picked[0] === 0) === !!question.answer;\n        } else if (question.type === 'text') {\n          var inp = el('sqv-text-answer');\n          p.textVal = inp ? inp.value : p.textVal;\n          var norm = p.textVal.trim().toLowerCase().replace(/\\s+/g, ' ');\n          correct = (question.accepted || []).some(function (a) { return String(a).trim().toLowerCase().replace(/\\s+/g, ' ') === norm; });\n        }\n        if ((question.type === 'single' || question.type === 'boolean' || question.type === 'multi') && !p.picked.length) { toast('Välj ett svar först'); return; }\n        if (question.type === 'text' && !p.textVal.trim()) { toast('Skriv ett svar först'); return; }\n        p.revealed = true; p.lastCorrect = correct;\n        if (correct) p.score++;\n        renderActivity();\n      }\n\n      function nextQuestion() {\n        var p = state.play; if (!p) return;\n        p.i++; p.picked = []; p.textVal = ''; p.revealed = false;\n        if (p.i >= p.quiz.questions.length) { p.done = true; saveResult(); }\n        renderActivity();\n      }\n\n      function saveResult() {\n        var p = state.play; if (!p) return;\n        var q = p.quiz;\n        var prev = state.myResults[q.id] || { attempts: 0, best: 0 };\n        state.myResults[q.id] = {\n          attempts: (prev.attempts || 0) + 1,\n          best: Math.max(prev.best || 0, p.score),\n          total: q.questions.length,\n          lastAt: new Date().toISOString()\n        };\n        var ref = (state.me && state.me.reference) || {};\n        ref.sqzResults = state.myResults;\n        api('PUT', '/users/users-v1/me', { reference: { sqzResults: state.myResults }, replaceReference: false })\n          .catch(function () { toast('Kunde inte spara resultatet', true); });\n      }\n\n      /* ---------- toplist ---------- */\n      function toplistHtml() {\n        var h = '<div class=\"sqv-h\"><i class=\"fas fa-trophy\"></i> Topplista</div>' +\n          '<div class=\"sqv-chiprow\">' +\n          '<button class=\"sqv-chip' + (state.toplistMode === 'quiz' ? ' sqv-on' : '') + '\" data-sqv-toplist=\"quiz\"><i class=\"fas fa-graduation-cap\"></i> Quizmästare</button>' +\n          '<button class=\"sqv-chip' + (state.toplistMode === 'sales' ? ' sqv-on' : '') + '\" data-sqv-toplist=\"sales\"><i class=\"fas fa-file-signature\"></i> Säljtoppen</button>' +\n          '</div>';\n        if (state.toplistMode === 'quiz') {\n          var activeIds = {};\n          state.quizzes.forEach(function (it) { if (it.quiz.isActive) activeIds[it.quiz.id] = true; });\n          var rows = [];\n          state.users.forEach(function (u) {\n            var res = (u.reference || {}).sqzResults || {};\n            var pts = 0, done = 0;\n            Object.keys(res).forEach(function (id) {\n              if (!activeIds[id]) return;\n              pts += res[id].best || 0; done++;\n            });\n            if (pts > 0 || done > 0) rows.push({ userId: u.id, name: u.fullName || u.username || '?', pts: pts, done: done });\n          });\n          rows.sort(function (a, b) { return b.pts - a.pts || b.done - a.done; });\n          h += '<div class=\"sqv-card\"><div class=\"sqv-card-title\"><i class=\"fas fa-medal\"></i> Poäng (bästa resultat per quiz)</div>';\n          if (!rows.length) h += '<div class=\"sqv-empty\">Inga resultat ännu — bli först!</div>';\n          rows.slice(0, 15).forEach(function (r2, i) {\n            h += lbRow(i, r2.name, r2.pts + ' p', r2.userId) ;\n          });\n          h += '</div><div class=\"sqv-muted\">Poäng = summan av ditt bästa resultat på varje aktivt quiz.</div>';\n        } else {\n          h += '<div class=\"sqv-chiprow\">' +\n            [['day', 'Idag'], ['week', 'Veckan'], ['month', 'Månaden']].map(function (p2) {\n              return '<button class=\"sqv-chip' + (state.salesPeriod === p2[0] ? ' sqv-on' : '') + '\" data-sqv-period=\"' + p2[0] + '\">' + p2[1] + '</button>';\n            }).join('') + '</div>' +\n            '<div class=\"sqv-card\"><div class=\"sqv-card-title\"><i class=\"fas fa-file-signature\"></i> Signerade avtal</div>' +\n            '<div id=\"sqv-salestop\"><div class=\"sqv-empty\">Laddar…</div></div></div>';\n          loadSalesTop();\n        }\n        return h;\n      }\n\n      function lbRow(i, name, val, userId) {\n        var cls = 'sqv-lb-row' + (i === 0 ? ' sqv-top1' : i === 1 ? ' sqv-top2' : i === 2 ? ' sqv-top3' : '') +\n          (state.me && userId === state.me.id ? ' sqv-me-row' : '');\n        return '<div class=\"' + cls + '\"><span class=\"sqv-lb-rank\">' + (i + 1) + '</span>' +\n          '<span class=\"sqv-lb-name\">' + esc(name) + '</span><span class=\"sqv-lb-val\">' + esc(val) + '</span></div>';\n      }\n\n      function loadSalesTop() {\n        var from = state.salesPeriod === 'day' ? dayStart(0) : state.salesPeriod === 'week' ? weekStart() : monthStart();\n        api('GET', '/offers/statistics-v1/own/issue_1238_status_user?from=' + encodeURIComponent(from.toISOString()) + '&to=' + encodeURIComponent(new Date().toISOString()))\n          .then(function (rows) {\n            var byUser = {};\n            (rows || []).forEach(function (r2) {\n              if (r2.status !== 'signed') return;\n              byUser[r2.userId] = (byUser[r2.userId] || 0) + (r2.count || 0);\n            });\n            var list = Object.keys(byUser).map(function (id) { return { userId: id, name: userName(id), n: byUser[id] }; });\n            list.sort(function (a, b) { return b.n - a.n; });\n            var target = el('sqv-salestop'); if (!target) return;\n            if (!list.length) { target.innerHTML = '<div class=\"sqv-empty\">Inga signerade avtal i perioden ännu.</div>'; return; }\n            target.innerHTML = list.slice(0, 15).map(function (r2, i) { return lbRow(i, r2.name, String(r2.n), r2.userId); }).join('');\n          })\n          .catch(function () {\n            var target = el('sqv-salestop');\n            if (target) target.innerHTML = '<div class=\"sqv-empty\">Kunde inte ladda statistiken.</div>';\n          });\n      }\n\n      /* ---------- snake (compact, same highscore key as Snake view) ---------- */\n      var snake = { running: false, paused: false, body: [], dir: { x: 1, y: 0 }, nextDir: { x: 1, y: 0 }, food: null, score: 0, tickMs: 140, timer: null, best: 0 };\n      var SNK_GRID = 17, SNK_CELL = 20;\n\n      function snakeHtml() {\n        var ref = (state.me && state.me.reference) || {};\n        snake.best = (ref.snakeHighScore && ref.snakeHighScore.score) || snake.best || 0;\n        var rows = [];\n        state.users.forEach(function (u) {\n          var hs = (u.reference || {}).snakeHighScore;\n          if (hs && hs.score > 0) rows.push({ userId: u.id, name: u.fullName || u.username || '?', score: hs.score });\n        });\n        rows.sort(function (a, b) { return b.score - a.score; });\n        return '<div class=\"sqv-h\"><i class=\"fas fa-gamepad\"></i> Snake' +\n          '<span style=\"flex:1\"></span><span class=\"sqv-pill\">Rekord: <b id=\"sqv-snake-best\">' + snake.best + '</b></span>' +\n          '<span class=\"sqv-pill\">Poäng: <b id=\"sqv-snake-score\">0</b></span></div>' +\n          '<div class=\"sqv-snake-wrap\">' +\n          '<div class=\"sqv-canvas-wrap\"><canvas id=\"sqv-snake-canvas\" width=\"' + (SNK_GRID * SNK_CELL) + '\" height=\"' + (SNK_GRID * SNK_CELL) + '\"></canvas>' +\n          '<div class=\"sqv-overlay\" id=\"sqv-snake-overlay\">' +\n          '<div style=\"font-size:22px;font-weight:800\" id=\"sqv-snake-title\">Snake</div>' +\n          '<div style=\"font-size:13px;opacity:0.85\" id=\"sqv-snake-sub\">Piltangenter/WASD · paus med mellanslag</div>' +\n          '<button class=\"sqv-btn\" data-sqv-act=\"snake-play\" id=\"sqv-snake-play\"><i class=\"fas fa-play\"></i> Spela</button>' +\n          '</div></div>' +\n          '<div class=\"sqv-card\" style=\"width:100%;max-width:' + (SNK_GRID * SNK_CELL) + 'px\">' +\n          '<div class=\"sqv-card-title\"><i class=\"fas fa-trophy\"></i> Topplista</div>' +\n          (rows.length ? rows.slice(0, 8).map(function (r2, i) { return lbRow(i, r2.name, String(r2.score), r2.userId); }).join('') : '<div class=\"sqv-empty\">Inga resultat ännu.</div>') +\n          '</div></div>';\n      }\n\n      function snakeReset() {\n        var mid = Math.floor(SNK_GRID / 2);\n        snake.body = [{ x: mid - 1, y: mid }, { x: mid - 2, y: mid }, { x: mid - 3, y: mid }];\n        snake.dir = { x: 1, y: 0 }; snake.nextDir = { x: 1, y: 0 };\n        snake.score = 0; snake.tickMs = 140;\n        snakeFood();\n        var s = el('sqv-snake-score'); if (s) s.textContent = '0';\n      }\n      function snakeFood() {\n        var free = [];\n        for (var y = 0; y < SNK_GRID; y++) for (var x = 0; x < SNK_GRID; x++) {\n          var taken = snake.body.some(function (seg) { return seg.x === x && seg.y === y; });\n          if (!taken) free.push({ x: x, y: y });\n        }\n        snake.food = free[Math.floor(Math.random() * free.length)] || null;\n      }\n      function snakeTick() {\n        if (!snake.running || snake.paused) return;\n        if (!el('sqv-snake-canvas')) { stopSnake(); return; }\n        snake.dir = snake.nextDir;\n        var head = { x: snake.body[0].x + snake.dir.x, y: snake.body[0].y + snake.dir.y };\n        var crash = head.x < 0 || head.y < 0 || head.x >= SNK_GRID || head.y >= SNK_GRID ||\n          snake.body.slice(0, -1).some(function (seg) { return seg.x === head.x && seg.y === head.y; });\n        if (crash) { snakeOver(); return; }\n        snake.body.unshift(head);\n        if (snake.food && head.x === snake.food.x && head.y === snake.food.y) {\n          snake.score++; snake.tickMs = Math.max(60, snake.tickMs - 3);\n          var s = el('sqv-snake-score'); if (s) s.textContent = String(snake.score);\n          snakeFood();\n        } else snake.body.pop();\n        snakeDraw();\n        snake.timer = setTimeout(snakeTick, snake.tickMs);\n      }\n      function snakeDraw() {\n        var canvas = el('sqv-snake-canvas'); if (!canvas) return;\n        var ctx = canvas.getContext('2d');\n        ctx.fillStyle = '#17241b'; ctx.fillRect(0, 0, canvas.width, canvas.height);\n        if (snake.food) {\n          ctx.fillStyle = '#E53935';\n          ctx.beginPath();\n          ctx.arc(snake.food.x * SNK_CELL + SNK_CELL / 2, snake.food.y * SNK_CELL + SNK_CELL / 2, SNK_CELL * 0.32, 0, Math.PI * 2);\n          ctx.fill();\n        }\n        snake.body.forEach(function (seg, i) {\n          var t = i / Math.max(1, snake.body.length - 1);\n          ctx.fillStyle = i === 0 ? '#AEEA00' : 'rgb(' + Math.round(90 - t * 40) + ',' + Math.round(200 - t * 90) + ',80)';\n          ctx.fillRect(seg.x * SNK_CELL + 2, seg.y * SNK_CELL + 2, SNK_CELL - 4, SNK_CELL - 4);\n        });\n      }\n      function snakeStart() {\n        snakeReset();\n        snake.running = true; snake.paused = false;\n        var o = el('sqv-snake-overlay'); if (o) o.classList.add('sqv-hidden');\n        snakeDraw();\n        snake.timer = setTimeout(snakeTick, snake.tickMs);\n      }\n      function stopSnake() { snake.running = false; clearTimeout(snake.timer); }\n      function snakeOver() {\n        stopSnake();\n        var o = el('sqv-snake-overlay'), tt = el('sqv-snake-title'), sub = el('sqv-snake-sub'), btn = el('sqv-snake-play');\n        if (o) o.classList.remove('sqv-hidden');\n        if (tt) tt.textContent = 'Game over!';\n        if (sub) sub.textContent = 'Du fick ' + snake.score + ' poäng' + (snake.score > snake.best ? ' — nytt rekord!' : '');\n        if (btn) btn.innerHTML = '<i class=\"fas fa-play\"></i> Spela igen';\n        if (snake.score > snake.best) {\n          snake.best = snake.score;\n          var b = el('sqv-snake-best'); if (b) b.textContent = String(snake.best);\n          api('PUT', '/users/users-v1/me', {\n            reference: { snakeHighScore: { score: snake.score, date: new Date().toISOString() } },\n            replaceReference: false\n          }).catch(function () {});\n        }\n      }\n\n      /* ---------- customer pane ---------- */\n      var custKey = '';\n      function renderCustomer(force) {\n        var pane = el('sqv-customer'); if (!pane) return;\n        var key = [state.callStatus, state.call && state.call.id, state.contact && state.contact.id,\n          state.history.length, state.evalPicked.join(','), state.editingFieldId,\n          state.statsToday ? state.statsToday.calls + '-' + state.statsToday.signed : '',\n          state.contactReadOnly].join('|');\n        if (!force && key === custKey && pane.firstChild) return;\n        custKey = key;\n        var h = '';\n        var st = state.callStatus;\n        var statusDef = {\n          active:  { cls: 'sqv-cs-active',  icon: 'fa-phone-volume', label: 'Pågående samtal' },\n          ringing: { cls: 'sqv-cs-ringing', icon: 'fa-phone',        label: 'Ringer upp…' },\n          wrap:    { cls: 'sqv-cs-wrap',    icon: 'fa-clipboard-check', label: 'Samtal avslutat — utvärdera' },\n          idle:    { cls: 'sqv-cs-idle',    icon: 'fa-mug-hot',      label: 'Inget pågående samtal' }\n        }[st];\n        h += '<div class=\"sqv-callstatus ' + statusDef.cls + '\"><span class=\"sqv-dot\"></span>' +\n          '<i class=\"fas ' + statusDef.icon + '\"></i> ' + statusDef.label;\n        if (st === 'active') h += '<span class=\"sqv-timer\" id=\"sqv-timer\">0:00</span>';\n        else if (st !== 'idle' && state.call && state.call.recipient) h += '<span class=\"sqv-timer\">' + esc(state.call.recipient) + '</span>';\n        h += '</div>';\n\n        if (st === 'idle') {\n          var s2 = state.statsToday || {};\n          h += '<div class=\"sqv-card\"><div class=\"sqv-card-title\"><i class=\"fas fa-chart-bar\"></i> Din dag hittills</div>' +\n            '<div class=\"sqv-stat-grid\">' +\n            '<div class=\"sqv-stat-box\"><div class=\"sqv-stat-num\">' + (s2.calls != null ? s2.calls : '–') + '</div><div class=\"sqv-stat-lbl\">Samtal idag</div></div>' +\n            '<div class=\"sqv-stat-box\"><div class=\"sqv-stat-num\">' + (s2.connected != null ? s2.connected : '–') + '</div><div class=\"sqv-stat-lbl\">Kopplade</div></div>' +\n            '<div class=\"sqv-stat-box\"><div class=\"sqv-stat-num\">' + (s2.signed != null ? s2.signed : '–') + '</div><div class=\"sqv-stat-lbl\">Signerade idag</div></div>' +\n            '<div class=\"sqv-stat-box\"><div class=\"sqv-stat-num\">' + quizDoneCount() + '</div><div class=\"sqv-stat-lbl\">Avklarade quiz</div></div>' +\n            '</div></div>' +\n            '<div class=\"sqv-tip\"><i class=\"fas fa-info-circle\"></i><span>När ett samtal kopplas visas kundkortet här automatiskt. ' +\n            'Använder du helskärmsläget för samtal? Välj widget-läget i telefoninställningarna så ligger den här vyn kvar under samtalet.</span></div>';\n          pane.innerHTML = h;\n          return;\n        }\n\n        /* contact card (active / ringing / wrap) */\n        if (state.contact) {\n          var c = state.contact;\n          var labels = fieldLabelMap();\n          var nameStr = contactName(c);\n          h += '<div class=\"sqv-card\">' +\n            '<div class=\"sqv-cname\">' + esc(nameStr || 'Okänd kontakt') + '</div>' +\n            '<div class=\"sqv-cphone\"><i class=\"fas fa-phone\" style=\"margin-right:5px\"></i>' + esc((state.call && state.call.recipient) || '') +\n            (state.call && state.call.previousCallCount ? ' · ' + state.call.previousCallCount + ' tidigare samtal' : '') + '</div>';\n          (c.fields || []).forEach(function (f) {\n            var label = labels[f.fieldId] || 'Fält';\n            var editable = !state.contactReadOnly && c.id;\n            if (state.editingFieldId === f.fieldId) {\n              h += '<div class=\"sqv-field\"><span class=\"sqv-field-label\">' + esc(label) + '</span>' +\n                '<input class=\"sqv-field-input\" id=\"sqv-field-edit\" data-sqv-fieldid=\"' + esc(f.fieldId) + '\" value=\"' + esc(f.value) + '\"></div>';\n            } else {\n              h += '<div class=\"sqv-field\"><span class=\"sqv-field-label\">' + esc(label) + '</span>' +\n                '<span class=\"sqv-field-value' + (editable ? ' sqv-editable\" title=\"Klicka för att redigera' : '') + '\"' +\n                (editable ? ' data-sqv-editfield=\"' + esc(f.fieldId) + '\"' : '') + '>' + (esc(f.value) || '<span class=\"sqv-muted\">–</span>') + '</span></div>';\n            }\n          });\n          if (state.contactReadOnly) h += '<div class=\"sqv-muted\" style=\"margin-top:8px\"><i class=\"fas fa-lock\"></i> Skrivskyddad kontaktinfo</div>';\n          h += '</div>';\n\n          /* comment */\n          if (c.id) {\n            h += '<div class=\"sqv-card\"><div class=\"sqv-card-title\"><i class=\"fas fa-comment\"></i> Anteckning</div>' +\n              '<textarea class=\"sqv-comment-box\" id=\"sqv-comment\" placeholder=\"Skriv en anteckning på kontakten…\"></textarea>' +\n              '<div style=\"margin-top:8px;text-align:right\"><button class=\"sqv-btn sqv-sm\" data-sqv-act=\"save-comment\">Spara anteckning</button></div></div>';\n          }\n        } else {\n          h += '<div class=\"sqv-card\"><div class=\"sqv-empty\">Hämtar kontakt…</div></div>';\n        }\n\n        /* evaluation in wrap-up */\n        if (st === 'wrap' && state.call && state.dialTags.length) {\n          h += '<div class=\"sqv-card\"><div class=\"sqv-card-title\"><i class=\"fas fa-tags\"></i> Utvärdera samtalet</div><div class=\"sqv-evalchips\">';\n          state.dialTags.forEach(function (t) {\n            var on = state.evalPicked.indexOf(t.id) !== -1;\n            h += '<button class=\"sqv-chip' + (on ? ' sqv-on' : '') + '\" data-sqv-eval=\"' + esc(t.id) + '\">' + esc(t.name) + '</button>';\n          });\n          h += '</div><button class=\"sqv-btn sqv-sm\" data-sqv-act=\"save-eval\"' + (state.evalPicked.length ? '' : ' disabled') + '>Spara utvärdering</button></div>';\n        }\n\n        /* history */\n        if (state.history.length) {\n          h += '<div class=\"sqv-card\"><div class=\"sqv-card-title\"><i class=\"fas fa-history\"></i> Tidigare samtal</div>';\n          state.history.forEach(function (call) {\n            var dur = call.connectedDuration ? fmtDur(call.connectedDuration) : null;\n            h += '<div class=\"sqv-hist-row\">' +\n              '<span class=\"sqv-hist-ic\"><i class=\"fas ' + (call.isConnected ? 'fa-phone' : 'fa-phone-slash') + '\"></i></span>' +\n              '<span class=\"sqv-hist-main\">' + (call.isConnected ? 'Kopplat' + (dur ? ' · ' + dur : '') : 'Ej svar') + '</span>' +\n              '<span class=\"sqv-hist-when\">' + fmtWhen(call.connectingAt || call.date) + '</span></div>';\n          });\n          h += '</div>';\n        }\n        pane.innerHTML = h;\n      }\n\n      function quizDoneCount() {\n        var n = 0;\n        Object.keys(state.myResults).forEach(function (id) { if ((state.myResults[id].best || 0) > 0) n++; });\n        return n;\n      }\n      function fieldLabelMap() {\n        var m = {};\n        state.contactFields.forEach(function (f) { m[f.id] = f.label || f.name || ''; });\n        return m;\n      }\n      function contactName(c) {\n        var labels = fieldLabelMap();\n        var first = '', last = '', full = '', company = '';\n        (c.fields || []).forEach(function (f) {\n          var lbl = (labels[f.fieldId] || '').toLowerCase();\n          if (/förnamn|first/.test(lbl) && !first) first = f.value;\n          else if (/efternamn|last/.test(lbl) && !last) last = f.value;\n          else if (/^namn$|full/.test(lbl) && !full) full = f.value;\n          else if (/företag|company|bolag/.test(lbl) && !company) company = f.value;\n        });\n        var name = full || ((first + ' ' + last).trim());\n        if (name && company) return name + ' · ' + company;\n        return name || company;\n      }\n\n      /* ---------- call polling ---------- */\n      function pollCall() {\n        if (!root()) return;\n        api('GET', '/dial/calls-v1?userIds=' + myId() + '&offset=0&count=1&connectionState=any&sortBy=date&sortOrder=desc')\n          .then(function (resp) {\n            var call = ((resp && resp.calls) || [])[0] || null;\n            var prevId = state.call && state.call.id;\n            var newStatus = 'idle';\n            if (call && !call.isDisconnected) {\n              newStatus = call.connectedAt ? 'active' : 'ringing';\n            } else if (call && call.disconnectedAt) {\n              var age = Date.now() - new Date(call.disconnectedAt).getTime();\n              var evaluated = (call.evaluationTagIds || []).length > 0 || state.evalSavedFor === call.id;\n              if (age < 3 * 60 * 1000 && !evaluated && call.isConnected && state.dialTags.length) newStatus = 'wrap';\n            }\n            state.call = call;\n            state.callStatus = newStatus;\n            if (newStatus === 'idle') { state.contact = null; state.history = []; }\n            else if (call && call.id !== prevId) {\n              state.evalPicked = []; state.editingFieldId = null;\n              loadContactForCall(call);\n            }\n            renderCustomer();\n            tickTimer();\n          })\n          .catch(function () {});\n      }\n\n      function loadContactForCall(call) {\n        state.contact = null; state.history = []; state.contactReadOnly = false;\n        var done = function () { renderCustomer(true); };\n        var loadHist = function (contactId) {\n          if (!contactId) return;\n          api('GET', '/dial/calls-v1?contactIds=' + contactId + '&offset=0&count=6&connectionState=disconnected&sortBy=date&sortOrder=desc')\n            .then(function (resp) {\n              state.history = ((resp && resp.calls) || []).filter(function (c) { return c.id !== call.id; }).slice(0, 5);\n              renderCustomer(true);\n            }).catch(function () {});\n        };\n        if (call.contactId) {\n          api('GET', '/contacts/contacts-v1/' + call.contactId)\n            .then(function (c) { state.contact = c; done(); loadHist(call.contactId); })\n            .catch(function () { fallbackContact(call, done, loadHist); });\n        } else {\n          fallbackContact(call, done, loadHist);\n        }\n      }\n\n      function fallbackContact(call, done, loadHist) {\n        var params = [];\n        if (call.easyTelecomUserCallId) params.push('easyTelecomUserCallId=' + encodeURIComponent(call.easyTelecomUserCallId));\n        if (call.recipient) params.push('recipient=' + encodeURIComponent(call.recipient));\n        if (!params.length) { state.contact = { fields: [] }; state.contactReadOnly = true; done(); return; }\n        api('GET', '/dial/call-contact-v1?' + params.join('&'))\n          .then(function (info) {\n            state.contact = { id: info && info.contactId, fields: (info && info.contactInfo) || [] };\n            state.contactReadOnly = true;\n            done();\n            if (info && info.contactId) loadHist(info.contactId);\n          })\n          .catch(function () { state.contact = { fields: [] }; state.contactReadOnly = true; done(); });\n      }\n\n      function tickTimer() {\n        var t = el('sqv-timer');\n        if (t && state.call && state.call.connectedAt && state.callStatus === 'active') {\n          t.textContent = fmtDur(Date.now() - new Date(state.call.connectedAt).getTime());\n        }\n      }\n\n      function loadStatsToday() {\n        if (!root()) return;\n        var from = dayStart(0).toISOString(), to = new Date().toISOString();\n        Promise.all([\n          api('GET', '/dial/statistics-v1/own/issue_1238_2?from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to) + '&fixedIntervalType=hour').catch(function () { return []; }),\n          api('GET', '/offers/statistics-v1/own/issue_1238_status_user?from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to)).catch(function () { return []; })\n        ]).then(function (r) {\n          var calls = 0, connected = 0, signed = 0;\n          (r[0] || []).forEach(function (row) {\n            if (row.userId !== state.me.id) return;\n            calls += row.count || 0; connected += row.connectedCount || 0;\n          });\n          (r[1] || []).forEach(function (row) {\n            if (row.userId === state.me.id && row.status === 'signed') signed += row.count || 0;\n          });\n          state.statsToday = { calls: calls, connected: connected, signed: signed };\n          if (state.callStatus === 'idle') renderCustomer();\n        });\n      }\n\n      /* ---------- editor modal ---------- */\n      function newQuizDoc() {\n        return { id: uid(), name: '', desc: '', icon: 'fa-graduation-cap', questions: [], isActive: true,\n          createdBy: state.me.id, createdByName: state.me.fullName || state.me.username,\n          createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };\n      }\n      function openEditor(quiz, isNew) {\n        state.editor = { quiz: JSON.parse(JSON.stringify(quiz)), isNew: !!isNew, showTemplates: !!isNew && !quiz.questions.length };\n        renderModal();\n      }\n\n      var EDITOR_ICONS = ['fa-graduation-cap', 'fa-shield-alt', 'fa-handshake', 'fa-route', 'fa-comments', 'fa-phone', 'fa-lightbulb', 'fa-bullseye', 'fa-fire', 'fa-star'];\n      var QTYPES = [\n        { key: 'single', label: 'Ett rätt svar', icon: 'fa-dot-circle' },\n        { key: 'multi', label: 'Flera rätt', icon: 'fa-check-double' },\n        { key: 'boolean', label: 'Sant / falskt', icon: 'fa-balance-scale' },\n        { key: 'text', label: 'Fritext', icon: 'fa-keyboard' }\n      ];\n\n      function renderModal() {\n        var mr = el('sqv-modal-root'); if (!mr) return;\n        if (state.settingsOpen) { mr.innerHTML = settingsHtml(); return; }\n        if (!state.editor) { mr.innerHTML = ''; return; }\n        var e = state.editor, q = e.quiz;\n        var h = '<div class=\"sqv-modal-bg\" data-sqv-act=\"modal-bg\"><div class=\"sqv-modal\" data-sqv-stopclick=\"1\">' +\n          '<div class=\"sqv-modal-head\"><i class=\"fas ' + esc(q.icon) + '\" style=\"color:var(--sqv-primary)\"></i> ' +\n          (e.isNew ? 'Nytt quiz' : 'Redigera quiz') + '</div><div class=\"sqv-modal-body\">';\n\n        if (e.showTemplates) {\n          h += '<div class=\"sqv-step\"><span class=\"sqv-step-n\">1</span> Välj startpunkt</div>' +\n            '<button class=\"sqv-tplcard\" data-sqv-tpl=\"-1\"><div class=\"sqv-quiz-ic\"><i class=\"fas fa-file\"></i></div>' +\n            '<div><div style=\"font-weight:800\">Tomt quiz</div><div class=\"sqv-muted\">Börja från noll med egna frågor.</div></div></button>';\n          TEMPLATES().forEach(function (tpl, i) {\n            h += '<button class=\"sqv-tplcard\" data-sqv-tpl=\"' + i + '\"><div class=\"sqv-quiz-ic\"><i class=\"fas ' + tpl.icon + '\"></i></div>' +\n              '<div><div style=\"font-weight:800\">' + esc(tpl.name) + '</div><div class=\"sqv-muted\">' + esc(tpl.desc) + ' · ' + tpl.questions.length + ' frågor</div></div></button>';\n          });\n          h += '</div><div class=\"sqv-modal-foot\"><button class=\"sqv-btn sqv-ghost\" data-sqv-act=\"close-modal\">Avbryt</button></div></div></div>';\n          mr.innerHTML = h;\n          return;\n        }\n\n        h += '<div class=\"sqv-step\"><span class=\"sqv-step-n\">1</span> Grund</div>' +\n          '<input class=\"sqv-input\" id=\"sqv-ed-name\" placeholder=\"Namn på quizet\" value=\"' + esc(q.name) + '\">' +\n          '<input class=\"sqv-input\" id=\"sqv-ed-desc\" placeholder=\"Kort beskrivning (valfritt)\" value=\"' + esc(q.desc || '') + '\">' +\n          '<div class=\"sqv-step\"><span class=\"sqv-step-n\">2</span> Ikon</div><div class=\"sqv-icongrid\">' +\n          EDITOR_ICONS.map(function (ic) {\n            return '<button class=\"sqv-iconcard' + (q.icon === ic ? ' sqv-on' : '') + '\" data-sqv-icon=\"' + ic + '\"><i class=\"fas ' + ic + '\"></i></button>';\n          }).join('') + '</div>' +\n          '<div class=\"sqv-step\"><span class=\"sqv-step-n\">3</span> Frågor (' + q.questions.length + ')</div>';\n\n        q.questions.forEach(function (question, qi) {\n          h += '<div class=\"sqv-qcard\"><div class=\"sqv-qcard-head\">' +\n            '<span class=\"sqv-pill\">' + (qi + 1) + '</span>' +\n            '<span class=\"sqv-pill\">' + esc(qtypeLabel(question.type)) + '</span>' +\n            '<span style=\"flex:1\"></span>' +\n            '<button class=\"sqv-mini-btn\" data-sqv-qmove=\"up\" data-sqv-qi=\"' + qi + '\" title=\"Flytta upp\"><i class=\"fas fa-arrow-up\"></i></button>' +\n            '<button class=\"sqv-mini-btn\" data-sqv-qmove=\"down\" data-sqv-qi=\"' + qi + '\" title=\"Flytta ned\"><i class=\"fas fa-arrow-down\"></i></button>' +\n            '<button class=\"sqv-mini-btn\" data-sqv-qdel=\"' + qi + '\" title=\"Ta bort\"><i class=\"fas fa-trash\"></i></button></div>' +\n            '<input class=\"sqv-input\" data-sqv-qtext=\"' + qi + '\" placeholder=\"Frågetext\" value=\"' + esc(question.text) + '\">';\n          if (question.type === 'single' || question.type === 'multi') {\n            question.options.forEach(function (o, oi) {\n              h += '<div class=\"sqv-qopt-row\">' +\n                '<button class=\"sqv-correct-toggle' + (o.correct ? ' sqv-on' : '') + '\" data-sqv-correct=\"' + qi + ':' + oi + '\" title=\"Markera som rätt svar\"><i class=\"fas fa-check\"></i></button>' +\n                '<input class=\"sqv-input\" style=\"margin:0\" data-sqv-otext=\"' + qi + ':' + oi + '\" placeholder=\"Svarsalternativ\" value=\"' + esc(o.text) + '\">' +\n                '<button class=\"sqv-mini-btn\" data-sqv-odel=\"' + qi + ':' + oi + '\"><i class=\"fas fa-times\"></i></button></div>';\n            });\n            h += '<button class=\"sqv-btn sqv-ghost sqv-sm\" data-sqv-oadd=\"' + qi + '\"><i class=\"fas fa-plus\"></i> Alternativ</button>';\n          } else if (question.type === 'boolean') {\n            h += '<div class=\"sqv-chiprow\" style=\"margin:4px 0 0\">' +\n              '<button class=\"sqv-chip' + (question.answer ? ' sqv-on' : '') + '\" data-sqv-bool=\"' + qi + ':1\">Rätt svar: Sant</button>' +\n              '<button class=\"sqv-chip' + (!question.answer ? ' sqv-on' : '') + '\" data-sqv-bool=\"' + qi + ':0\">Rätt svar: Falskt</button></div>';\n          } else if (question.type === 'text') {\n            h += '<input class=\"sqv-input\" data-sqv-accepted=\"' + qi + '\" placeholder=\"Godkända svar, kommaseparerade\" value=\"' + esc((question.accepted || []).join(', ')) + '\">';\n          }\n          h += '</div>';\n        });\n\n        h += '<div class=\"sqv-muted\" style=\"margin-bottom:6px\">Lägg till fråga:</div><div class=\"sqv-typegrid\">' +\n          QTYPES.map(function (t) {\n            return '<button class=\"sqv-iconbtn\" data-sqv-qadd=\"' + t.key + '\"><i class=\"fas ' + t.icon + '\"></i> ' + t.label + '</button>';\n          }).join('') + '</div>' +\n          '<div class=\"sqv-step\"><span class=\"sqv-step-n\">4</span> Status</div>' +\n          '<div class=\"sqv-chiprow\"><button class=\"sqv-chip' + (q.isActive ? ' sqv-on' : '') + '\" data-sqv-act=\"ed-active\">Aktivt — synligt för alla</button>' +\n          '<button class=\"sqv-chip' + (!q.isActive ? ' sqv-on' : '') + '\" data-sqv-act=\"ed-inactive\">Utkast — bara du ser det</button></div>';\n\n        h += '</div><div class=\"sqv-modal-foot\">' +\n          '<button class=\"sqv-btn\" data-sqv-act=\"save-quiz\"><i class=\"fas fa-check\"></i> Spara</button>' +\n          '<button class=\"sqv-btn sqv-ghost\" data-sqv-act=\"close-modal\">Avbryt</button>' +\n          (!e.isNew ? '<span style=\"flex:1\"></span><button class=\"sqv-btn sqv-ghost\" style=\"color:var(--sqv-danger)\" data-sqv-act=\"del-quiz\"><i class=\"fas fa-trash\"></i> Ta bort</button>' : '') +\n          '</div></div></div>';\n        mr.innerHTML = h;\n      }\n\n      function qtypeLabel(t) {\n        for (var i = 0; i < QTYPES.length; i++) if (QTYPES[i].key === t) return QTYPES[i].label;\n        return t;\n      }\n\n      function syncEditorInputs() {\n        var e = state.editor; if (!e) return;\n        var q = e.quiz;\n        var name = el('sqv-ed-name'); if (name) q.name = name.value;\n        var desc = el('sqv-ed-desc'); if (desc) q.desc = desc.value;\n        document.querySelectorAll('[data-sqv-qtext]').forEach(function (inp) {\n          var qi = +inp.getAttribute('data-sqv-qtext');\n          if (q.questions[qi]) q.questions[qi].text = inp.value;\n        });\n        document.querySelectorAll('[data-sqv-otext]').forEach(function (inp) {\n          var parts = inp.getAttribute('data-sqv-otext').split(':');\n          var question = q.questions[+parts[0]];\n          if (question && question.options[+parts[1]]) question.options[+parts[1]].text = inp.value;\n        });\n        document.querySelectorAll('[data-sqv-accepted]').forEach(function (inp) {\n          var qi = +inp.getAttribute('data-sqv-accepted');\n          if (q.questions[qi]) q.questions[qi].accepted = inp.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);\n        });\n      }\n\n      function validateQuiz(q) {\n        if (!q.name.trim()) return 'Ge quizet ett namn';\n        if (!q.questions.length) return 'Lägg till minst en fråga';\n        for (var i = 0; i < q.questions.length; i++) {\n          var question = q.questions[i];\n          if (!question.text.trim()) return 'Fråga ' + (i + 1) + ' saknar text';\n          if (question.type === 'single' || question.type === 'multi') {\n            var opts = question.options.filter(function (o) { return o.text.trim(); });\n            if (opts.length < 2) return 'Fråga ' + (i + 1) + ' behöver minst två alternativ';\n            var correct = opts.filter(function (o) { return o.correct; }).length;\n            if (question.type === 'single' && correct !== 1) return 'Fråga ' + (i + 1) + ' ska ha exakt ett rätt svar';\n            if (question.type === 'multi' && correct < 1) return 'Fråga ' + (i + 1) + ' behöver minst ett rätt svar';\n          }\n          if (question.type === 'text' && !(question.accepted || []).length) return 'Fråga ' + (i + 1) + ' behöver minst ett godkänt svar';\n        }\n        return null;\n      }\n\n      function saveQuiz() {\n        syncEditorInputs();\n        var e = state.editor; if (!e) return;\n        var q = e.quiz;\n        q.questions.forEach(function (question) {\n          if (question.type === 'single' || question.type === 'multi') {\n            question.options = question.options.filter(function (o) { return o.text.trim(); });\n          }\n        });\n        var err = validateQuiz(q);\n        if (err) { toast(err, true); renderModal(); return; }\n        q.updatedAt = new Date().toISOString();\n        var ref = state.me.reference = state.me.reference || {};\n        ref.sqzQuizzes = ref.sqzQuizzes || {};\n        ref.sqzQuizzes[q.id] = q;\n        saveMyQuizzes().then(function () {\n          state.editor = null;\n          computeQuizzes();\n          renderModal(); renderActivity();\n          toast('Quizet sparades');\n        }).catch(function () { toast('Kunde inte spara quizet', true); });\n      }\n\n      function deleteQuiz() {\n        var e = state.editor; if (!e) return;\n        if (!e.confirmDelete) {\n          e.confirmDelete = true;\n          toast('Klicka \"Ta bort\" igen för att bekräfta');\n          return;\n        }\n        var ref = state.me.reference = state.me.reference || {};\n        ref.sqzQuizzes = ref.sqzQuizzes || {};\n        delete ref.sqzQuizzes[e.quiz.id];\n        saveMyQuizzes().then(function () {\n          state.editor = null;\n          computeQuizzes();\n          renderModal(); renderActivity();\n          toast('Quizet togs bort');\n        }).catch(function () { toast('Kunde inte ta bort quizet', true); });\n      }\n\n      /* ---------- settings modal (admin, presetState) ---------- */\n      function settingsHtml() {\n        var cfg = state.cfg;\n        function chip(on, act, label) {\n          return '<button class=\"sqv-chip' + (on ? ' sqv-on' : '') + '\" data-sqv-cfg=\"' + act + '\">' + label + '</button>';\n        }\n        return '<div class=\"sqv-modal-bg\" data-sqv-act=\"modal-bg\"><div class=\"sqv-modal\" data-sqv-stopclick=\"1\">' +\n          '<div class=\"sqv-modal-head\"><i class=\"fas fa-cog\" style=\"color:var(--sqv-primary)\"></i> Inställningar</div>' +\n          '<div class=\"sqv-modal-body\">' +\n          '<div class=\"sqv-step\"><span class=\"sqv-step-n\">1</span> Vem får skapa quiz?</div><div class=\"sqv-chiprow\">' +\n          chip(cfg.creatorMode === 'admins', 'creator:admins', 'Endast admin') +\n          chip(cfg.creatorMode === 'supervisors', 'creator:supervisors', 'Admin + teamledare') +\n          chip(cfg.creatorMode === 'all', 'creator:all', 'Alla användare') + '</div>' +\n          '<div class=\"sqv-step\"><span class=\"sqv-step-n\">2</span> Flikar i aktivitetspanelen</div><div class=\"sqv-chiprow\">' +\n          chip(cfg.tabs.quiz, 'tab:quiz', 'Quiz') +\n          chip(cfg.tabs.toplist, 'tab:toplist', 'Topplista') +\n          chip(cfg.tabs.snake, 'tab:snake', 'Snake') + '</div>' +\n          '<div class=\"sqv-step\"><span class=\"sqv-step-n\">3</span> Standardsida för aktiviteten</div><div class=\"sqv-chiprow\">' +\n          chip(cfg.defaultSide === 'left', 'side:left', 'Vänster') +\n          chip(cfg.defaultSide === 'right', 'side:right', 'Höger') + '</div>' +\n          '<div class=\"sqv-muted\" style=\"margin-top:14px\">Inställningarna sparas på vyn (presetState) och gäller alla användare. Varje användare kan själv byta sida med \"Byt sida\".</div>' +\n          '</div><div class=\"sqv-modal-foot\">' +\n          '<button class=\"sqv-btn\" data-sqv-act=\"save-settings\"><i class=\"fas fa-check\"></i> Spara</button>' +\n          '<button class=\"sqv-btn sqv-ghost\" data-sqv-act=\"close-modal\">Stäng</button></div></div></div>';\n      }\n\n      function saveSettings() {\n        if (!state.viewId) return;\n        api('PUT', '/users/views-v1/' + state.viewId, { presetState: {\n          configured: true,\n          creatorMode: state.cfg.creatorMode,\n          tabs: state.cfg.tabs,\n          defaultSide: state.cfg.defaultSide\n        } }).then(function () {\n          state.settingsOpen = false;\n          renderModal(); renderToolbar(); renderActivity();\n          toast('Inställningarna sparades');\n        }).catch(function () { toast('Kunde inte spara (kräver admin)', true); });\n      }\n\n      /* ---------- field editing ---------- */\n      function commitFieldEdit() {\n        var inp = el('sqv-field-edit');\n        if (!inp || !state.contact || !state.contact.id) { state.editingFieldId = null; return; }\n        var fieldId = inp.getAttribute('data-sqv-fieldid');\n        var val = inp.value;\n        var fields = (state.contact.fields || []).map(function (f) {\n          return { fieldId: f.fieldId, value: f.fieldId === fieldId ? val : f.value };\n        });\n        var old = JSON.parse(JSON.stringify(state.contact.fields || []));\n        state.contact.fields = fields;\n        state.editingFieldId = null;\n        renderCustomer(true);\n        api('PUT', '/contacts/contacts-v1/' + state.contact.id, { fields: fields })\n          .catch(function (err) {\n            state.contact.fields = old;\n            renderCustomer(true);\n            toast(err.status === 403 ? 'Du saknar rättighet att ändra kontakter' : 'Kunde inte spara fältet', true);\n          });\n      }\n\n      /* ============================ events (document delegation) ============================ */\n      function closest(node, sel) { return node && node.closest ? node.closest(sel) : null; }\n      function inRoot(node) { var r = root(); return r && node && r.contains(node); }\n\n      function onClick(ev) {\n        var t = ev.target;\n        var modalRoot = el('sqv-modal-root');\n        var inModal = modalRoot && modalRoot.contains(t);\n        if (!inRoot(t) && !inModal) return;\n\n        var tab = closest(t, '[data-sqv-tab]');\n        if (tab) { state.tab = tab.getAttribute('data-sqv-tab'); state.play = null; renderToolbar(); renderActivity(); savePrefs(); return; }\n\n        var tl = closest(t, '[data-sqv-toplist]');\n        if (tl) { state.toplistMode = tl.getAttribute('data-sqv-toplist'); renderActivity(); return; }\n        var per = closest(t, '[data-sqv-period]');\n        if (per) { state.salesPeriod = per.getAttribute('data-sqv-period'); renderActivity(); return; }\n\n        var evalChip = closest(t, '[data-sqv-eval]');\n        if (evalChip) {\n          var tagId = evalChip.getAttribute('data-sqv-eval');\n          var i = state.evalPicked.indexOf(tagId);\n          if (i === -1) state.evalPicked.push(tagId); else state.evalPicked.splice(i, 1);\n          renderCustomer(true);\n          return;\n        }\n\n        var editField = closest(t, '[data-sqv-editfield]');\n        if (editField) {\n          state.editingFieldId = editField.getAttribute('data-sqv-editfield');\n          renderCustomer(true);\n          var inp = el('sqv-field-edit'); if (inp) { inp.focus(); inp.select(); }\n          return;\n        }\n\n        /* editor structural buttons */\n        if (state.editor) {\n          var tpl = closest(t, '[data-sqv-tpl]');\n          if (tpl) {\n            var idx = +tpl.getAttribute('data-sqv-tpl');\n            var e = state.editor;\n            e.showTemplates = false;\n            if (idx >= 0) {\n              var tdef = TEMPLATES()[idx];\n              e.quiz.name = tdef.name; e.quiz.desc = tdef.desc; e.quiz.icon = tdef.icon;\n              e.quiz.questions = tdef.questions;\n            }\n            renderModal();\n            return;\n          }\n          var icon = closest(t, '[data-sqv-icon]');\n          if (icon) { syncEditorInputs(); state.editor.quiz.icon = icon.getAttribute('data-sqv-icon'); renderModal(); return; }\n          var qadd = closest(t, '[data-sqv-qadd]');\n          if (qadd) {\n            syncEditorInputs();\n            var type = qadd.getAttribute('data-sqv-qadd');\n            var nq = { id: uid(), type: type, text: '', options: [], answer: true, accepted: [] };\n            if (type === 'single' || type === 'multi') nq.options = [{ id: uid(), text: '', correct: type === 'single' }, { id: uid(), text: '', correct: false }];\n            state.editor.quiz.questions.push(nq);\n            renderModal();\n            return;\n          }\n          var qdel = closest(t, '[data-sqv-qdel]');\n          if (qdel) { syncEditorInputs(); state.editor.quiz.questions.splice(+qdel.getAttribute('data-sqv-qdel'), 1); renderModal(); return; }\n          var qmove = closest(t, '[data-sqv-qmove]');\n          if (qmove) {\n            syncEditorInputs();\n            var qi = +qmove.getAttribute('data-sqv-qi');\n            var dirn = qmove.getAttribute('data-sqv-qmove') === 'up' ? -1 : 1;\n            var arr = state.editor.quiz.questions;\n            if (qi + dirn >= 0 && qi + dirn < arr.length) {\n              var tmp = arr[qi]; arr[qi] = arr[qi + dirn]; arr[qi + dirn] = tmp;\n              renderModal();\n            }\n            return;\n          }\n          var oadd = closest(t, '[data-sqv-oadd]');\n          if (oadd) {\n            syncEditorInputs();\n            state.editor.quiz.questions[+oadd.getAttribute('data-sqv-oadd')].options.push({ id: uid(), text: '', correct: false });\n            renderModal();\n            return;\n          }\n          var odel = closest(t, '[data-sqv-odel]');\n          if (odel) {\n            syncEditorInputs();\n            var p2 = odel.getAttribute('data-sqv-odel').split(':');\n            state.editor.quiz.questions[+p2[0]].options.splice(+p2[1], 1);\n            renderModal();\n            return;\n          }\n          var corr = closest(t, '[data-sqv-correct]');\n          if (corr) {\n            syncEditorInputs();\n            var p3 = corr.getAttribute('data-sqv-correct').split(':');\n            var question = state.editor.quiz.questions[+p3[0]];\n            var opt = question.options[+p3[1]];\n            if (question.type === 'single') {\n              question.options.forEach(function (o) { o.correct = false; });\n              opt.correct = true;\n            } else opt.correct = !opt.correct;\n            renderModal();\n            return;\n          }\n          var boolBtn = closest(t, '[data-sqv-bool]');\n          if (boolBtn) {\n            syncEditorInputs();\n            var p4 = boolBtn.getAttribute('data-sqv-bool').split(':');\n            state.editor.quiz.questions[+p4[0]].answer = p4[1] === '1';\n            renderModal();\n            return;\n          }\n        }\n\n        /* settings chips */\n        if (state.settingsOpen) {\n          var cfgBtn = closest(t, '[data-sqv-cfg]');\n          if (cfgBtn) {\n            var parts = cfgBtn.getAttribute('data-sqv-cfg').split(':');\n            if (parts[0] === 'creator') state.cfg.creatorMode = parts[1];\n            else if (parts[0] === 'side') state.cfg.defaultSide = parts[1];\n            else if (parts[0] === 'tab') {\n              state.cfg.tabs[parts[1]] = !state.cfg.tabs[parts[1]];\n              if (!state.cfg.tabs.quiz && !state.cfg.tabs.toplist && !state.cfg.tabs.snake) state.cfg.tabs[parts[1]] = true;\n            }\n            renderModal();\n            return;\n          }\n        }\n\n        var act = closest(t, '[data-sqv-act]');\n        if (!act) return;\n        var action = act.getAttribute('data-sqv-act');\n        if (action === 'modal-bg' && closest(t, '[data-sqv-stopclick]')) return;\n\n        switch (action) {\n          case 'swap-side':\n            state.prefs.side = state.prefs.side === 'left' ? 'right' : 'left';\n            applySplit(); savePrefs();\n            break;\n          case 'settings': state.settingsOpen = true; renderModal(); break;\n          case 'save-settings': saveSettings(); break;\n          case 'new-quiz': openEditor(newQuizDoc(), true); break;\n          case 'edit-quiz': {\n            var q2 = findQuiz(act.getAttribute('data-sqv-id'));\n            if (q2) openEditor(q2, false);\n            break;\n          }\n          case 'play': startPlay(act.getAttribute('data-sqv-id')); break;\n          case 'exit-play': state.play = null; renderActivity(); refreshUsers().then(renderActivity); break;\n          case 'pick': {\n            var p5 = state.play;\n            if (!p5 || p5.revealed) break;\n            var idx2 = +act.getAttribute('data-sqv-idx');\n            var question2 = p5.quiz.questions[p5.i];\n            if (question2.type === 'multi') {\n              var j = p5.picked.indexOf(idx2);\n              if (j === -1) p5.picked.push(idx2); else p5.picked.splice(j, 1);\n            } else p5.picked = [idx2];\n            renderActivity();\n            break;\n          }\n          case 'check-q': checkAnswer(); break;\n          case 'next-q': nextQuestion(); break;\n          case 'snake-play': snakeStart(); break;\n          case 'save-quiz': saveQuiz(); break;\n          case 'del-quiz': deleteQuiz(); break;\n          case 'close-modal': case 'modal-bg':\n            state.editor = null; state.settingsOpen = false; renderModal();\n            break;\n          case 'save-comment': {\n            var box = el('sqv-comment');\n            var msg = box && box.value.trim();\n            if (!msg || !state.contact || !state.contact.id) break;\n            api('POST', '/contacts/contacts-v1/' + state.contact.id + '/comments', { message: msg })\n              .then(function () { if (box) box.value = ''; toast('Anteckningen sparades'); })\n              .catch(function () { toast('Kunde inte spara anteckningen', true); });\n            break;\n          }\n          case 'save-eval': {\n            if (!state.call || !state.evalPicked.length) break;\n            var callId = state.call.id;\n            api('PUT', '/dial/calls-v1/' + callId, { evaluationTagIds: state.evalPicked.slice(), evaluatedAt: new Date().toISOString() })\n              .then(function () {\n                state.evalSavedFor = callId;\n                state.callStatus = 'idle'; state.call = null; state.contact = null; state.history = []; state.evalPicked = [];\n                renderCustomer(true);\n                toast('Utvärderingen sparades');\n              })\n              .catch(function () { toast('Kunde inte spara utvärderingen', true); });\n            break;\n          }\n        }\n      }\n\n      function onKeyDown(ev) {\n        if (!root()) return;\n        var tag = (ev.target && ev.target.tagName) || '';\n        var typing = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';\n        if (ev.key === 'Enter' && ev.target && ev.target.id === 'sqv-field-edit') { ev.preventDefault(); commitFieldEdit(); return; }\n        if (ev.key === 'Escape' && state.editingFieldId) { state.editingFieldId = null; renderCustomer(true); return; }\n        if (ev.key === 'Enter' && ev.target && ev.target.id === 'sqv-text-answer') { ev.preventDefault(); checkAnswer(); return; }\n        if (typing) return;\n        /* snake controls */\n        if (state.tab === 'snake' && snake.running) {\n          var dirs = { ArrowUp: { x: 0, y: -1 }, ArrowDown: { x: 0, y: 1 }, ArrowLeft: { x: -1, y: 0 }, ArrowRight: { x: 1, y: 0 },\n            w: { x: 0, y: -1 }, s: { x: 0, y: 1 }, a: { x: -1, y: 0 }, d: { x: 1, y: 0 } };\n          var d2 = dirs[ev.key] || dirs[(ev.key || '').toLowerCase()];\n          if (ev.key === ' ' || ev.code === 'Space') { ev.preventDefault(); snake.paused = !snake.paused; if (!snake.paused) snake.timer = setTimeout(snakeTick, snake.tickMs); return; }\n          if (d2 && !snake.paused) {\n            ev.preventDefault();\n            if (d2.x !== -snake.dir.x || d2.y !== -snake.dir.y) snake.nextDir = d2;\n          }\n        }\n      }\n\n      function onFocusOut(ev) {\n        if (ev.target && ev.target.id === 'sqv-field-edit') {\n          setTimeout(function () { if (state.editingFieldId) commitFieldEdit(); }, 120);\n        }\n      }\n\n      /* divider drag */\n      function onPointerDown(ev) {\n        var div = closest(ev.target, '#sqv-divider');\n        if (!div || !inRoot(div)) return;\n        state.dragging = true;\n        ev.preventDefault();\n      }\n      function onPointerMove(ev) {\n        if (!state.dragging) return;\n        var wrap = el('sqv-splitwrap'); if (!wrap) { state.dragging = false; return; }\n        var rect = wrap.getBoundingClientRect();\n        var pct = ((ev.clientX - rect.left) / rect.width) * 100;\n        /* pct is position of divider from wrap left; activity width depends on side */\n        var actPct = state.prefs.side === 'left' ? pct : 100 - pct;\n        state.prefs.split = Math.min(75, Math.max(25, Math.round(actPct)));\n        applySplit();\n      }\n      function onPointerUp() {\n        if (!state.dragging) return;\n        state.dragging = false;\n        savePrefs();\n      }\n\n      /* ============================ contrast probe ============================ */\n      function applyContrast() {\n        var r = root(); if (!r) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        r.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        r.style.setProperty('--sqv-primary-fg', lum > 0.6 ? '#1b2430' : '#ffffff');\n      }\n      var themeObserver = new MutationObserver(applyContrast);\n      themeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n\n      /* ============================ boot / timers / cleanup ============================ */\n      document.addEventListener('click', onClick);\n      document.addEventListener('keydown', onKeyDown);\n      document.addEventListener('focusout', onFocusOut);\n      document.addEventListener('pointerdown', onPointerDown);\n      document.addEventListener('pointermove', onPointerMove);\n      document.addEventListener('pointerup', onPointerUp);\n\n      var callTimer = setInterval(pollCall, 4000);\n      var userTimer = setInterval(function () { refreshUsers().then(function () { if (state.tab !== 'quiz' || !state.play) renderActivity(); }); }, 60000);\n      var statsTimer = setInterval(loadStatsToday, 60000);\n      var clockTimer = setInterval(tickTimer, 1000);\n      /* re-render guard: the app re-renders view DOM after script init */\n      var guardTimer = setInterval(function () {\n        var r = root(); if (!r) return;\n        if (!r.querySelector('[data-sqv-stamp=\"' + state.stamp + '\"]')) {\n          custKey = '';\n          renderAll();\n        }\n      }, 2000);\n\n      window.__sqvCleanup = function () {\n        themeObserver.disconnect();\n        stopSnake();\n        clearInterval(callTimer); clearInterval(userTimer); clearInterval(statsTimer);\n        clearInterval(clockTimer); clearInterval(guardTimer);\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('keydown', onKeyDown);\n        document.removeEventListener('focusout', onFocusOut);\n        document.removeEventListener('pointerdown', onPointerDown);\n        document.removeEventListener('pointermove', onPointerMove);\n        document.removeEventListener('pointerup', onPointerUp);\n      };\n\n      applyContrast();\n      loadCore().then(function () {\n        renderAll();\n        pollCall();\n        loadStatsToday();\n      }).catch(function () {\n        var pane = el('sqv-activity');\n        if (pane) pane.innerHTML = '<div class=\"sqv-empty\">Kunde inte ladda vyn. Kontrollera att du är inloggad.</div>';\n      });\n    })();\n  </script>\n</div>","iconName":"graduation-cap","iconColor":"#5E35B1","settingsHtml":"","description":"","rights":[],"components":[]},{"id":"6a54f9901246a83b6cb008b3","name":"Tidsrapportering","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":2,"label":"Tidsrapportering"}],"html":"<div id=\"ttv-root\">\n  <style>\n    #ttv-root {\n      --ttv-bg: var(--color-background-2, #fafafa);\n      --ttv-card: var(--color-background-1, #ffffff);\n      --ttv-secondary: var(--color-background-3, #eeeeee);\n      --ttv-fg: var(--color-text-1, #212121);\n      --ttv-fg2: var(--color-text-2, #616161);\n      --ttv-muted-fg: var(--color-text-3, #757575);\n      --ttv-primary: var(--color-primary, #1565C0);\n      --ttv-primary-fg: #ffffff;\n      --ttv-danger: var(--color-danger, #E53935);\n      --ttv-ok: #2E7D32;\n      --ttv-warn: #EF6C00;\n      --ttv-hover: var(--color-element-hover, rgba(20, 20, 20, 0.035));\n      --ttv-border: color-mix(in srgb, var(--ttv-fg) 13%, transparent);\n      --ttv-border-soft: color-mix(in srgb, var(--ttv-fg) 8%, transparent);\n      --ttv-radius: 10px;\n      font-family: 'Nunito', sans-serif;\n      color: var(--ttv-fg);\n      display: block;\n      background: var(--ttv-bg);\n      border: 1px solid var(--ttv-border-soft);\n      border-radius: 12px;\n      padding: 22px;\n      height: fit-content !important;\n      min-height: 0 !important;\n      max-height: none !important;\n      align-self: flex-start;\n      flex: 0 0 auto;\n    }\n    #ttv-root * { box-sizing: border-box; }\n    #ttv-root button { font-family: inherit; cursor: pointer; }\n    #ttv-root input { font-family: inherit; color: inherit; }\n\n    .ttv-header { display: flex; align-items: baseline; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }\n    .ttv-date { font-size: 13px; color: var(--ttv-muted-fg); text-transform: capitalize; }\n\n    .ttv-grid { display: grid; grid-template-columns: minmax(300px, 380px) 1fr; gap: 16px; align-items: start; }\n    @media (max-width: 980px) { .ttv-grid { grid-template-columns: 1fr; } }\n\n    .ttv-card {\n      background: var(--ttv-card); border: 1px solid var(--ttv-border-soft);\n      border-radius: var(--ttv-radius); padding: 16px 18px;\n    }\n    .ttv-card-title { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }\n    .ttv-card-title .fas, .ttv-card-title .far { color: var(--ttv-primary); }\n    .ttv-card-title .ttv-spacer { flex: 1; }\n\n    /* ---------------- hero: tabs + clock + big button ---------------- */\n    .ttv-hero { display: flex; flex-direction: column; align-items: center; padding: 22px 18px; }\n\n    .ttv-tabs { display: inline-flex; align-self: stretch; border: 1px solid var(--ttv-border); border-radius: 9px; padding: 3px; margin-bottom: 22px; background: var(--ttv-bg); }\n    .ttv-tabs button {\n      flex: 1; border: none; background: transparent; color: var(--ttv-fg2);\n      font-size: 13px; font-weight: 800; padding: 8px 12px; border-radius: 7px;\n      display: inline-flex; align-items: center; justify-content: center; gap: 7px;\n      transition: background 120ms ease, color 120ms ease;\n    }\n    .ttv-tabs button .fas { font-size: 12px; }\n    .ttv-tabs button:hover { color: var(--ttv-fg); }\n    .ttv-tabs button.ttv-active { background: var(--ttv-primary); color: var(--ttv-primary-fg, #fff); box-shadow: 0 1px 3px color-mix(in srgb, var(--ttv-primary) 30%, transparent); }\n\n    .ttv-clock {\n      font-family: inherit;\n      font-variant-numeric: tabular-nums;\n      font-size: 46px; font-weight: 800; letter-spacing: 1px; line-height: 1.1;\n      color: var(--ttv-fg);\n    }\n    .ttv-clock.ttv-running { color: var(--ttv-primary); }\n    .ttv-clock-sub { font-size: 12.5px; color: var(--ttv-muted-fg); margin: 6px 0 18px; min-height: 16px; text-align: center; }\n\n    .ttv-bigbtn {\n      width: 148px; height: 148px; border-radius: 50%;\n      border: none; outline: none;\n      background: var(--ttv-primary); color: var(--ttv-primary-fg, #fff);\n      display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;\n      font-size: 15px; font-weight: 800;\n      box-shadow: 0 8px 24px color-mix(in srgb, var(--ttv-primary) 35%, transparent);\n      transition: transform 120ms ease, box-shadow 120ms ease, background 200ms ease;\n      user-select: none;\n    }\n    .ttv-bigbtn .fas { font-size: 34px; }\n    .ttv-bigbtn:hover { transform: scale(1.04); }\n    .ttv-bigbtn:active { transform: scale(0.98); }\n    .ttv-bigbtn.ttv-stop {\n      background: var(--ttv-danger); color: #fff;\n      box-shadow: 0 8px 24px color-mix(in srgb, var(--ttv-danger) 35%, transparent);\n      animation: ttv-pulse 2.2s ease-in-out infinite;\n    }\n    .ttv-bigbtn:disabled { opacity: 0.6; cursor: default; transform: none; }\n    @keyframes ttv-pulse {\n      0%, 100% { box-shadow: 0 8px 24px color-mix(in srgb, var(--ttv-danger) 35%, transparent); }\n      50% { box-shadow: 0 8px 34px color-mix(in srgb, var(--ttv-danger) 55%, transparent); }\n    }\n\n    .ttv-proj-label { font-size: 11.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.6px; color: var(--ttv-muted-fg); margin: 20px 0 8px; align-self: flex-start; }\n    .ttv-projects { display: flex; flex-wrap: wrap; gap: 8px; align-self: stretch; }\n    .ttv-proj-chip {\n      display: inline-flex; align-items: center; gap: 7px;\n      border: 1px solid var(--ttv-border); border-radius: 999px;\n      background: var(--ttv-card); color: var(--ttv-fg2);\n      padding: 7px 14px; font-size: 13px; font-weight: 700;\n      transition: background 120ms ease, border-color 120ms ease;\n    }\n    .ttv-proj-chip:hover { background: var(--ttv-hover); }\n    .ttv-proj-chip.ttv-active {\n      background: var(--ttv-primary); border-color: var(--ttv-primary); color: var(--ttv-primary-fg, #fff);\n    }\n    .ttv-proj-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }\n\n    /* ---------------- today's entries ---------------- */\n    .ttv-today-total { font-size: 13px; font-weight: 800; color: var(--ttv-fg2); }\n    .ttv-entry { display: flex; align-items: center; gap: 10px; padding: 8px 4px; border-bottom: 1px solid var(--ttv-border-soft); font-size: 13.5px; }\n    .ttv-entry:last-child { border-bottom: none; }\n    .ttv-entry-time { font-variant-numeric: tabular-nums; color: var(--ttv-fg2); white-space: nowrap; }\n    .ttv-entry-proj { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .ttv-entry-dur { font-weight: 800; white-space: nowrap; font-variant-numeric: tabular-nums; }\n    .ttv-entry.ttv-live .ttv-entry-dur { color: var(--ttv-primary); }\n    .ttv-empty { font-size: 13px; color: var(--ttv-muted-fg); padding: 8px 2px; }\n\n    /* ---------------- history ---------------- */\n    .ttv-toggle-btn {\n      background: transparent; border: 1px solid var(--ttv-border); border-radius: 8px;\n      color: var(--ttv-fg2); font-size: 12.5px; font-weight: 700; padding: 6px 12px;\n    }\n    .ttv-toggle-btn:hover { background: var(--ttv-hover); }\n    .ttv-hist-grid { display: grid; grid-template-columns: 1fr 260px; gap: 22px; align-items: start; }\n    @media (max-width: 800px) { .ttv-hist-grid { grid-template-columns: 1fr; } }\n    .ttv-chart { display: flex; align-items: flex-end; gap: 3px; height: 150px; padding-top: 10px; }\n    .ttv-bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; min-width: 0; }\n    .ttv-bar {\n      width: 100%; max-width: 20px; border-radius: 4px 4px 1px 1px;\n      background: linear-gradient(180deg, color-mix(in srgb, var(--ttv-primary) 80%, #ffffff), var(--ttv-primary));\n      min-height: 2px;\n      transition: height 500ms cubic-bezier(0.22, 1, 0.36, 1);\n    }\n    .ttv-bar-col.ttv-today-col .ttv-bar { background: linear-gradient(180deg, color-mix(in srgb, var(--ttv-ok) 70%, #ffffff), var(--ttv-ok)); }\n    .ttv-bar-day { font-size: 10px; color: var(--ttv-muted-fg); margin-top: 5px; white-space: nowrap; height: 13px; }\n    .ttv-hist-kpis { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }\n    .ttv-kpi { flex: 1; min-width: 110px; border: 1px solid var(--ttv-border-soft); border-radius: var(--ttv-radius); padding: 9px 13px; background: var(--ttv-bg); }\n    .ttv-kpi-num { font-size: 20px; font-weight: 800; line-height: 1.25; font-variant-numeric: tabular-nums; }\n    .ttv-kpi-label { font-size: 11px; color: var(--ttv-muted-fg); }\n    .ttv-donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }\n    .ttv-legend { display: flex; flex-direction: column; gap: 6px; align-self: stretch; }\n    .ttv-legend-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }\n    .ttv-legend-row .ttv-proj-dot { width: 10px; height: 10px; }\n    .ttv-legend-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .ttv-legend-val { color: var(--ttv-fg2); font-variant-numeric: tabular-nums; white-space: nowrap; }\n\n    /* ---------------- admin ---------------- */\n    .ttv-admin-section { margin-top: 16px; }\n    .ttv-admin-grid { display: grid; grid-template-columns: minmax(340px, 1fr) minmax(320px, 420px); gap: 16px; align-items: start; }\n    @media (max-width: 1100px) { .ttv-admin-grid { grid-template-columns: 1fr; } }\n    .ttv-user-row {\n      display: flex; align-items: center; gap: 11px; padding: 9px 8px;\n      border-radius: 8px; border: 1px solid transparent;\n    }\n    .ttv-user-row:hover { background: var(--ttv-hover); }\n    .ttv-user-row.ttv-selected { border-color: var(--ttv-primary); background: color-mix(in srgb, var(--ttv-primary) 6%, transparent); }\n    .ttv-avatar {\n      width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;\n      background: color-mix(in srgb, var(--ttv-primary) 14%, transparent);\n      color: var(--ttv-primary); font-size: 12.5px; font-weight: 800;\n      display: flex; align-items: center; justify-content: center;\n    }\n    .ttv-user-main { flex: 1; min-width: 0; }\n    .ttv-user-name { font-size: 13.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .ttv-user-status { font-size: 11.5px; color: var(--ttv-muted-fg); display: flex; align-items: center; gap: 5px; }\n    .ttv-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ttv-muted-fg); opacity: 0.5; flex-shrink: 0; }\n    .ttv-status-dot.ttv-on { background: var(--ttv-ok); opacity: 1; animation: ttv-blink 2s ease-in-out infinite; }\n    @keyframes ttv-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }\n    .ttv-user-today { font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ttv-fg2); white-space: nowrap; }\n    .ttv-badge {\n      font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 999px; white-space: nowrap;\n      background: color-mix(in srgb, var(--ttv-warn) 15%, transparent); color: var(--ttv-warn);\n    }\n    .ttv-iconbtn {\n      width: 30px; height: 30px; border-radius: 7px; border: none; background: transparent;\n      color: var(--ttv-muted-fg); font-size: 13px; flex-shrink: 0;\n    }\n    .ttv-iconbtn:hover { background: var(--ttv-hover); color: var(--ttv-fg); }\n    .ttv-iconbtn.ttv-danger:hover { color: var(--ttv-danger); }\n    .ttv-iconbtn.ttv-warn-on { color: var(--ttv-warn); }\n    .ttv-iconbtn.ttv-armed { background: var(--ttv-danger); color: #fff; }\n    .ttv-iconbtn:disabled { opacity: 0.4; cursor: default; }\n\n    /* editor */\n    .ttv-editor-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }\n    .ttv-editor-head .ttv-avatar { width: 30px; height: 30px; font-size: 11px; }\n    .ttv-editor-name { font-weight: 800; font-size: 14px; flex: 1; }\n    .ttv-date-input {\n      border: none; border-bottom: 2px solid var(--ttv-border); background: transparent;\n      padding: 5px 2px; font-size: 13px; outline: none;\n    }\n    .ttv-date-input:focus { border-bottom-color: var(--ttv-primary); }\n    .ttv-erow {\n      display: flex; align-items: center; gap: 8px; padding: 8px 0;\n      border-bottom: 1px solid var(--ttv-border-soft); flex-wrap: wrap;\n    }\n    .ttv-erow-projs { display: flex; gap: 4px; flex: 1 1 100%; flex-wrap: wrap; }\n    .ttv-mini-chip {\n      display: inline-flex; align-items: center; gap: 5px;\n      border: 1px solid var(--ttv-border); border-radius: 999px; background: var(--ttv-card);\n      color: var(--ttv-fg2); padding: 3px 9px; font-size: 11px; font-weight: 700;\n    }\n    .ttv-mini-chip.ttv-active { background: var(--ttv-primary); border-color: var(--ttv-primary); color: var(--ttv-primary-fg, #fff); }\n    .ttv-time-input {\n      border: none; border-bottom: 2px solid var(--ttv-border); background: transparent;\n      padding: 4px 2px; font-size: 13px; width: 74px; outline: none; font-variant-numeric: tabular-nums;\n    }\n    .ttv-time-input:focus { border-bottom-color: var(--ttv-primary); }\n    .ttv-time-input.ttv-invalid { border-bottom-color: var(--ttv-danger); }\n    .ttv-erow-dur { font-size: 12px; color: var(--ttv-muted-fg); font-variant-numeric: tabular-nums; min-width: 62px; }\n    .ttv-erow-arrow { color: var(--ttv-muted-fg); font-size: 11px; }\n    .ttv-editor-actions { display: flex; flex-direction: row-reverse; gap: 8px; margin-top: 14px; }\n    .ttv-btn {\n      border-radius: var(--border-radius-button, 8px); border: 1px solid var(--ttv-border);\n      background: var(--ttv-card); color: var(--ttv-fg); font-size: 13.5px; font-weight: 700;\n      padding: 10px 18px;\n    }\n    .ttv-btn:hover { background: var(--ttv-hover); }\n    .ttv-btn.ttv-primary { background: var(--ttv-primary); border-color: var(--ttv-primary); color: var(--ttv-primary-fg, #fff); }\n    .ttv-btn.ttv-primary:hover { filter: brightness(1.05); }\n    .ttv-btn.ttv-danger-btn { background: var(--ttv-danger); border-color: var(--ttv-danger); color: #fff; }\n    .ttv-btn:disabled { opacity: 0.5; cursor: default; }\n    .ttv-btn-sm { padding: 6px 12px; font-size: 12.5px; }\n    .ttv-running-banner {\n      display: flex; align-items: center; gap: 9px; font-size: 12.5px;\n      background: color-mix(in srgb, var(--ttv-ok) 9%, transparent);\n      border: 1px solid color-mix(in srgb, var(--ttv-ok) 30%, transparent);\n      border-radius: 8px; padding: 8px 11px; margin-bottom: 10px;\n    }\n    .ttv-running-banner .fas { color: var(--ttv-ok); }\n    .ttv-running-banner .ttv-spacer { flex: 1; }\n    .ttv-running-banner.ttv-banner-danger {\n      background: color-mix(in srgb, var(--ttv-danger) 9%, transparent);\n      border-color: color-mix(in srgb, var(--ttv-danger) 30%, transparent);\n    }\n    .ttv-running-banner.ttv-banner-danger .fas { color: var(--ttv-danger); }\n\n    /* modal */\n    .ttv-overlay {\n      position: fixed; inset: 0; background: rgba(10, 14, 20, 0.45); z-index: 900;\n      display: flex; align-items: center; justify-content: center; padding: 20px;\n    }\n    .ttv-modal {\n      background: var(--ttv-card); color: var(--ttv-fg);\n      border-radius: 12px; padding: 24px; width: 480px; max-width: 100%;\n      max-height: 85vh; overflow: auto; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);\n    }\n    .ttv-modal-title { font-size: 19px; font-weight: 800; margin-bottom: 4px; }\n    .ttv-modal-sub { font-size: 13px; color: var(--ttv-muted-fg); margin-bottom: 16px; }\n    .ttv-source-row {\n      display: flex; align-items: center; gap: 10px; padding: 9px 11px;\n      border: 1px solid var(--ttv-border-soft); border-radius: 8px; margin-bottom: 7px; font-size: 13px;\n    }\n    .ttv-source-row .fas { color: var(--ttv-warn); width: 16px; text-align: center; }\n    .ttv-source-kind { font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; color: var(--ttv-muted-fg); }\n    .ttv-modal-note {\n      font-size: 12px; color: var(--ttv-fg2); background: var(--ttv-bg);\n      border-radius: 8px; padding: 10px 12px; margin: 12px 0;\n    }\n    .ttv-modal-actions { display: flex; flex-direction: row-reverse; gap: 8px; margin-top: 16px; }\n\n    /* export modal */\n    .ttv-step-label { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; color: var(--ttv-muted-fg); margin: 14px 0 8px; }\n    .ttv-step-num { width: 20px; height: 20px; border-radius: 50%; background: var(--ttv-primary); color: var(--ttv-primary-fg, #fff); font-size: 11px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }\n    .ttv-preset-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n    .ttv-preset-card {\n      border: 1px solid var(--ttv-border); border-radius: 10px; background: var(--ttv-card);\n      padding: 13px 12px; text-align: left; display: flex; flex-direction: column; gap: 5px;\n    }\n    .ttv-preset-card:hover { background: var(--ttv-hover); }\n    .ttv-preset-card.ttv-active { border-color: var(--ttv-primary); background: color-mix(in srgb, var(--ttv-primary) 8%, transparent); }\n    .ttv-preset-card .fas { font-size: 17px; color: var(--ttv-primary); }\n    .ttv-preset-name { font-size: 13.5px; font-weight: 800; }\n    .ttv-preset-desc { font-size: 11.5px; color: var(--ttv-muted-fg); line-height: 1.35; }\n    .ttv-exp-users { display: flex; flex-wrap: wrap; gap: 6px; max-height: 150px; overflow: auto; padding: 2px; }\n    .ttv-range-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n    .ttv-check-row { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 700; color: var(--ttv-fg2); cursor: pointer; }\n    .ttv-mini-chip.ttv-chip-removed { border-style: dashed; color: var(--ttv-muted-fg); }\n    .ttv-mini-chip.ttv-chip-removed.ttv-active { color: var(--ttv-primary-fg, #fff); }\n\n    /* smart tracking */\n    .ttv-seg { display: inline-flex; border: 1px solid var(--ttv-border); border-radius: 8px; overflow: hidden; }\n    .ttv-seg button {\n      border: none; background: transparent; color: var(--ttv-fg2);\n      font-size: 12px; font-weight: 700; padding: 6px 11px;\n    }\n    .ttv-seg button + button { border-left: 1px solid var(--ttv-border); }\n    .ttv-seg button:hover { background: var(--ttv-hover); }\n    .ttv-seg button.ttv-active { background: var(--ttv-primary); color: var(--ttv-primary-fg, #fff); }\n    .ttv-kpi-pair { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }\n    .ttv-kpi-mini { font-size: 11.5px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ttv-fg2); white-space: nowrap; display: flex; align-items: center; gap: 4px; }\n    .ttv-kpi-mini .fas { font-size: 9.5px; opacity: 0.7; }\n    .ttv-kpi-mini.ttv-smart-kpi { color: var(--ttv-warn); }\n    .ttv-info-list { margin: 0; padding-left: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; }\n    .ttv-info-list li { display: flex; gap: 11px; font-size: 13px; line-height: 1.45; }\n    .ttv-info-list .fas { color: var(--ttv-primary); width: 18px; text-align: center; margin-top: 2px; flex-shrink: 0; }\n\n    /* dialer master panel (admin tab) — mirrors the seller card structure exactly */\n    .ttv-log-row { display: flex; align-items: flex-start; gap: 11px; padding: 10px 4px; border-bottom: 1px solid var(--ttv-border-soft); }\n    .ttv-log-row:last-child { border-bottom: none; }\n    .ttv-log-icon { width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 12px; }\n    .ttv-log-body { flex: 1; min-width: 0; }\n    .ttv-log-title { font-size: 13.5px; font-weight: 700; }\n    .ttv-log-count { font-weight: 800; color: var(--ttv-primary); font-size: 12.5px; }\n    .ttv-log-meta { font-size: 11.5px; color: var(--ttv-muted-fg); margin-top: 2px; }\n\n    /* multi-select + bulk actions */\n    .ttv-check {\n      width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0;\n      border: 1.5px solid var(--ttv-border); background: var(--ttv-card);\n      display: inline-flex; align-items: center; justify-content: center;\n      color: transparent; font-size: 11px; padding: 0;\n      transition: background 100ms ease, border-color 100ms ease;\n    }\n    .ttv-check:hover { border-color: var(--ttv-primary); }\n    .ttv-check.ttv-on { background: var(--ttv-primary); border-color: var(--ttv-primary); color: var(--ttv-primary-fg, #fff); }\n    .ttv-bulkbar {\n      display: flex; align-items: center; gap: 7px; flex-wrap: wrap;\n      border: 1px solid color-mix(in srgb, var(--ttv-primary) 35%, transparent);\n      background: color-mix(in srgb, var(--ttv-primary) 7%, transparent);\n      border-radius: 9px; padding: 8px 12px; margin-bottom: 12px;\n    }\n    .ttv-bulkbar-count { font-size: 12.5px; font-weight: 800; margin-right: 4px; }\n    .ttv-bulk-btn {\n      display: inline-flex; align-items: center; gap: 6px;\n      border: 1px solid var(--ttv-border); border-radius: 8px; background: var(--ttv-card);\n      color: var(--ttv-fg); font-size: 12px; font-weight: 700; padding: 6px 11px;\n    }\n    .ttv-bulk-btn:hover { background: var(--ttv-hover); }\n    .ttv-bulk-btn.ttv-armed { background: var(--ttv-danger); border-color: var(--ttv-danger); color: #fff; }\n    .ttv-bulk-btn .fas { font-size: 11px; }\n    .ttv-selall {\n      background: transparent; border: none; color: var(--ttv-fg2);\n      font-size: 12px; font-weight: 700; padding: 4px 6px; border-radius: 6px;\n    }\n    .ttv-selall:hover { background: var(--ttv-hover); }\n    .ttv-bulk-user-row { display: flex; align-items: center; gap: 9px; padding: 7px 10px; border: 1px solid var(--ttv-border-soft); border-radius: 8px; margin-bottom: 6px; font-size: 13px; }\n    .ttv-bulk-user-row.ttv-skipped { opacity: 0.55; }\n    .ttv-bulk-user-what { color: var(--ttv-muted-fg); font-size: 11.5px; margin-left: auto; text-align: right; }\n\n    .ttv-loading { display: flex; align-items: center; gap: 10px; color: var(--ttv-muted-fg); font-size: 13px; padding: 30px 0; justify-content: center; }\n    .ttv-spinner {\n      width: 16px; height: 16px; border-radius: 50%;\n      border: 2px solid var(--ttv-border); border-top-color: var(--ttv-primary);\n      animation: ttv-spin 0.8s linear infinite;\n    }\n    @keyframes ttv-spin { to { transform: rotate(360deg); } }\n    .ttv-error { color: var(--ttv-danger); font-size: 13px; padding: 10px 0; }\n\n    .ttv-toast {\n      position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);\n      background: var(--ttv-card); color: var(--ttv-fg);\n      border: 1px solid var(--ttv-border);\n      border-radius: 10px; padding: 11px 18px; font-size: 13.5px; font-weight: 700;\n      z-index: 950; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);\n      display: flex; align-items: center; gap: 9px; max-width: min(90vw, 560px);\n    }\n    .ttv-toast .fa-check-circle { color: var(--ttv-ok); }\n    .ttv-toast.ttv-toast-error { background: var(--ttv-danger); color: #fff; border-color: var(--ttv-danger); }\n    .ttv-toast.ttv-toast-error .fas { color: #fff; }\n    .ttv-toast-progress { display: flex; flex-direction: column; gap: 7px; min-width: 280px; }\n    .ttv-toast-bar { height: 6px; border-radius: 3px; background: color-mix(in srgb, var(--ttv-primary) 18%, transparent); overflow: hidden; }\n    .ttv-toast-bar-fill { height: 100%; border-radius: 3px; background: var(--ttv-primary); transition: width 300ms ease; }\n  </style>\n\n  <div class=\"ttv-header\">\n    <div class=\"ttv-date\" id=\"ttv-date\"></div>\n  </div>\n\n  <div class=\"ttv-grid\" id=\"ttv-grid\">\n    <div class=\"ttv-card ttv-hero\">\n      <div class=\"ttv-tabs\" id=\"ttv-tabs\" style=\"display: none;\"></div>\n      <div id=\"ttv-seller-controls\" style=\"display: contents;\">\n        <div class=\"ttv-clock\" id=\"ttv-clock\">00:00:00</div>\n        <div class=\"ttv-clock-sub\" id=\"ttv-clock-sub\"></div>\n        <button class=\"ttv-bigbtn\" id=\"ttv-bigbtn\" disabled>\n          <i class=\"fas fa-play\"></i>\n          <span>Starta</span>\n        </button>\n        <div class=\"ttv-proj-label\">Projekt</div>\n        <div class=\"ttv-projects\" id=\"ttv-projects\"><div class=\"ttv-loading\"><div class=\"ttv-spinner\"></div> Laddar…</div></div>\n      </div>\n      <div id=\"ttv-dialer-panel\" style=\"display: none;\"></div>\n    </div>\n\n    <div id=\"ttv-right-col\">\n      <div id=\"ttv-seller-right\">\n        <div class=\"ttv-card\" style=\"margin-bottom: 16px;\">\n          <div class=\"ttv-card-title\">\n            <i class=\"fas fa-list-ul\"></i> Idag\n            <span class=\"ttv-spacer\"></span>\n            <span class=\"ttv-today-total\" id=\"ttv-today-total\"></span>\n          </div>\n          <div id=\"ttv-today\"><div class=\"ttv-loading\"><div class=\"ttv-spinner\"></div> Laddar…</div></div>\n        </div>\n\n        <div class=\"ttv-card\">\n          <div class=\"ttv-card-title\">\n            <i class=\"fas fa-chart-bar\"></i> Historik\n            <span class=\"ttv-spacer\"></span>\n            <button class=\"ttv-toggle-btn\" data-ttv-export-open=\"self\" title=\"Exportera din tidrapport till Excel\"><i class=\"fas fa-file-excel\" style=\"margin-right:6px;\"></i>Exportera</button>\n            <button class=\"ttv-toggle-btn\" data-ttv-hist-toggle=\"1\" id=\"ttv-hist-btn\">Visa full historik</button>\n          </div>\n          <div id=\"ttv-history\"></div>\n        </div>\n      </div>\n      <div id=\"ttv-admin-right\" style=\"display: none;\"></div>\n    </div>\n  </div>\n\n  <div class=\"ttv-admin-section\" id=\"ttv-admin\" style=\"display: none;\">\n    <div class=\"ttv-card\">\n      <div class=\"ttv-card-title\"><i class=\"fas fa-users-cog\"></i> Användare <span style=\"font-weight: 400; color: var(--ttv-muted-fg);\">(administration)</span>\n        <span class=\"ttv-spacer\"></span>\n        <span id=\"ttv-mode-seg\"></span>\n        <button class=\"ttv-iconbtn\" data-ttv-smart-info=\"1\" title=\"Så fungerar Smart tracking\"><i class=\"fas fa-question-circle\"></i></button>\n        <button class=\"ttv-toggle-btn\" data-ttv-export-open=\"admin\" title=\"Exportera tidrapport till Excel\"><i class=\"fas fa-file-excel\" style=\"margin-right:6px;\"></i>Exportera</button>\n      </div>\n      <div id=\"ttv-admin-toolbar\"></div>\n      <div class=\"ttv-admin-grid\">\n        <div id=\"ttv-admin-users\"><div class=\"ttv-loading\"><div class=\"ttv-spinner\"></div> Laddar…</div></div>\n        <div id=\"ttv-admin-editor\"></div>\n      </div>\n    </div>\n  </div>\n\n  <div id=\"ttv-modal-mount\"></div>\n  <div id=\"ttv-toast-mount\"></div>\n\n  <script>\n    (function () {\n      var API = 'https://app.salesys.se/api';\n      var POLL_MS = 30000;\n      var CALL_RIGHT = 'calls.create';\n      var MAX_SHIFT_MS = 12 * 3600000;      /* auto-stop runaway timers after this */\n      var STALE_AFTER_MS = 14 * 3600000;    /* consider a timer runaway after this */\n      var GC_KEEP_MS = 180 * 86400000;      /* keep entries this long — admins edit retroactively up to here (chart shows 30 d) */\n      var GC_MAX_ENTRIES = 800;\n      var PALETTE = ['#1565C0', '#2E7D32', '#EF6C00', '#8E24AA', '#00838F', '#F9A825', '#C62828', '#5E35B1'];\n      var NO_PROJECT = { id: null, name: 'Allmänt', color: '#607D8B' };\n\n      if (window.__ttvCleanup) { try { window.__ttvCleanup(); } catch (e) {} }\n\n      var state = {\n        me: null,               /* fresh /me object incl. reference */\n        users: null,            /* visible (non-removed) team-scoped users */\n        removedUsers: [],       /* platform-suspended/deactivated, shown via export toggle */\n        projects: null,         /* visible projects for self */\n        allProjects: null,\n        teams: null,\n        roles: null,\n        loaded: false,\n        busy: false,            /* self start/stop in flight */\n        historyOpen: false,\n        adminAllowed: false,\n        isSuperAdmin: false,\n        canEditTime: false,\n        canManageDialer: false,\n        canExportTeam: false,\n        canUpdateUsers: false,\n        canLogOutUsers: false,\n        tab: 'seller',          /* 'seller' | 'admin' (only meaningful when adminAllowed) */\n        armedStopDialer: null,  /* timestamp while \"Stoppa dialer\" awaits second click */\n        dialerBusy: false,\n        selfSuspended: false,   /* was my own dialer paused at last poll (for reactivation reload) */\n        edit: null,             /* { userId, date, rows: [{id, projectId, start, stop}], dirty } */\n        suspendModal: null,     /* { userIds: [...] } */\n        exportModal: null,      /* { mode: 'self'|'all'|'user', userIds, range, data, busy } */\n        infoModal: false,       /* smart tracking explainer */\n        trackMode: 'manual',    /* 'manual' | 'smart' | 'both' (admin list) */\n        smart: null,            /* { today: {userId: ms}, tabs: {userId: tab} } */\n        selected: {},           /* userId -> true (admin multi-select) */\n        armedBulkKick: null,    /* timestamp while bulk kick awaits second click */\n        exporting: false,       /* an export is running in the background */\n        exportProgress: null,   /* { done, total, label } */\n        armedKick: null,        /* { userId, at } */\n        toastTimer: null\n      };\n\n      /* ---------------- api helpers ---------------- */\n      function cookies() {\n        return document.cookie.split(';').reduce(function (obj, cookie) {\n          var parts = /(.+?)=(.+)/.exec(cookie.trim());\n          if (parts) obj[parts[1]] = parts[2];\n          return obj;\n        }, {});\n      }\n      function token() { return cookies()['s2_utoken']; }\n\n      function api(path, params) {\n        var url = API + path;\n        if (params) {\n          var qs = Object.keys(params).filter(function (k) { return params[k] != null; })\n            .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');\n          if (qs) url += (url.indexOf('?') === -1 ? '?' : '&') + qs;\n        }\n        return fetch(url, { headers: { 'Authorization': 'Bearer ' + token() } })\n          .then(function (r) {\n            if (!r.ok) throw new Error('HTTP ' + r.status);\n            return r.json();\n          });\n      }\n      function apiSend(method, path, body) {\n        return fetch(API + path, {\n          method: method,\n          headers: { 'Authorization': 'Bearer ' + token(), 'Content-Type': 'application/json' },\n          body: body === undefined ? undefined : JSON.stringify(body)\n        }).then(function (r) {\n          if (!r.ok) throw new Error('HTTP ' + r.status);\n          return r.json().catch(function () { return null; });\n        });\n      }\n      function apiPut(path, body) { return apiSend('PUT', path, body); }\n      function apiPost(path, body) { return apiSend('POST', path, body); }\n\n      function esc(s) {\n        return String(s == null ? '' : s)\n          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n      function el(id) { return document.getElementById(id); }\n\n      /* ---------------- date/time helpers ---------------- */\n      function startOfDay(d) { var x = new Date(d); x.setHours(0, 0, 0, 0); return x; }\n      function dayKey(d) {\n        var x = new Date(d);\n        var m = x.getMonth() + 1, dd = x.getDate();\n        return x.getFullYear() + '-' + (m < 10 ? '0' + m : m) + '-' + (dd < 10 ? '0' + dd : dd);\n      }\n      function pad2(n) { return n < 10 ? '0' + n : String(n); }\n      function fmtClock(ms) {\n        if (ms < 0) ms = 0;\n        var s = Math.floor(ms / 1000);\n        return pad2(Math.floor(s / 3600)) + ':' + pad2(Math.floor(s / 60) % 60) + ':' + pad2(s % 60);\n      }\n      function fmtDur(ms) {\n        if (ms < 0) ms = 0;\n        var min = Math.round(ms / 60000);\n        var h = Math.floor(min / 60), m = min % 60;\n        if (!h) return m + ' min';\n        return h + ' tim' + (m ? ' ' + m + ' min' : '');\n      }\n      function fmtHm(ms) {\n        var min = Math.round(ms / 60000);\n        return Math.floor(min / 60) + ':' + pad2(min % 60);\n      }\n      function fmtTime(d) { var x = new Date(d); return pad2(x.getHours()) + ':' + pad2(x.getMinutes()); }\n      function timeInputValue(d) { return fmtTime(d); }\n      function entryMs(e) { return Math.max(0, new Date(e.stopAt).getTime() - new Date(e.startAt).getTime()); }\n      function newId() { return 'e' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }\n\n      /* ---------------- projects ---------------- */\n      function projectById(id) {\n        if (!id) return NO_PROJECT;\n        var p = (state.allProjects || []).filter(function (x) { return x.id === id; })[0];\n        return p || { id: id, name: 'Okänt projekt', color: '#607D8B' };\n      }\n      function projColor(id) {\n        if (!id) return NO_PROJECT.color;\n        var idx = (state.allProjects || []).map(function (p) { return p.id; }).indexOf(id);\n        return idx === -1 ? '#607D8B' : PALETTE[idx % PALETTE.length];\n      }\n      function visibleProjects(user) {\n        var all = state.allProjects || [];\n        if (!all.length) return [];\n        if (!user || user.type === 'admin') return all;\n        var teamIds = [user.teamId].concat(user.supervisedTeamIds || []).filter(Boolean).map(String);\n        return all.filter(function (p) {\n          if (!p.teamIds || !p.teamIds.length) return true;\n          return p.teamIds.some(function (t) { return teamIds.indexOf(String(t)) !== -1; });\n        });\n      }\n\n      /* ---------------- reference readers ---------------- */\n      function refOf(user) { return (user && user.reference) || {}; }\n      function entriesOf(user) {\n        var list = refOf(user).ttEntries;\n        return Array.isArray(list) ? list : [];\n      }\n      function activeOf(user) {\n        var a = refOf(user).ttActive;\n        return (a && a.startedAt) ? a : null;\n      }\n      function suspensionOf(user) {\n        var s = refOf(user).ttSuspension;\n        return (s && s.suspendedAt) ? s : null;\n      }\n      function entriesForDay(user, dk) {\n        return entriesOf(user).filter(function (e) { return e.startAt && dayKey(e.startAt) === dk; })\n          .sort(function (a, b) { return new Date(a.startAt) - new Date(b.startAt); });\n      }\n      function totalForDay(user, dk) {\n        var t = 0;\n        entriesForDay(user, dk).forEach(function (e) { t += entryMs(e); });\n        var a = activeOf(user);\n        if (a && dayKey(a.startedAt) === dk) t += Date.now() - new Date(a.startedAt).getTime();\n        return t;\n      }\n\n      /* ---------------- rights model ---------------- */\n      function rightsOfSource(src) { return (src && Array.isArray(src.rights)) ? src.rights : []; }\n      function callGrantingSources(user) {\n        /* Which of the user's teams/roles grant the right to call. */\n        var out = [];\n        var teams = state.teams || [];\n        var roles = state.roles || [];\n        function team(id) { return teams.filter(function (t) { return String(t.id) === String(id); })[0]; }\n        if (user.teamId) {\n          var t = team(user.teamId);\n          if (t && rightsOfSource(t).indexOf(CALL_RIGHT) !== -1) out.push({ kind: 'team', id: t.id, name: t.name });\n        }\n        (user.supervisedTeamIds || []).forEach(function (id) {\n          var st = team(id);\n          if (st && rightsOfSource(st).indexOf(CALL_RIGHT) !== -1) out.push({ kind: 'supervisedTeam', id: st.id, name: st.name });\n        });\n        (user.roleIds || []).forEach(function (id) {\n          var r = roles.filter(function (x) { return String(x.id) === String(id); })[0];\n          if (r && rightsOfSource(r).indexOf(CALL_RIGHT) !== -1) out.push({ kind: 'role', id: r.id, name: r.name });\n        });\n        return out;\n      }\n      function meHasRight(right) {\n        if (!state.me) return false;\n        if (state.me.type === 'admin') return true;\n        if (Array.isArray(state.me.rights) && state.me.rights.indexOf(right) !== -1) return true;\n        /* Fall back to union of own teams/roles. */\n        var union = {};\n        var meLike = state.me;\n        var teams = state.teams || [], roles = state.roles || [];\n        [meLike.teamId].concat(meLike.supervisedTeamIds || []).filter(Boolean).forEach(function (id) {\n          teams.filter(function (t) { return String(t.id) === String(id); }).forEach(function (t) {\n            rightsOfSource(t).forEach(function (r) { union[r] = true; });\n          });\n        });\n        (meLike.roleIds || []).forEach(function (id) {\n          roles.filter(function (x) { return String(x.id) === String(id); }).forEach(function (r0) {\n            rightsOfSource(r0).forEach(function (r) { union[r] = true; });\n          });\n        });\n        return Boolean(union[right]);\n      }\n\n      /* ---------------- toast ---------------- */\n      /* persistent progress-bar toast (no auto-hide) — survives modal dismissal.\n         Progress is REAL: one step per completed API fetch + one for the file build. */\n      function toastProgress() {\n        var pr = state.exportProgress;\n        var mount = el('ttv-toast-mount');\n        if (!pr || !mount) return;\n        if (state.toastTimer) { clearTimeout(state.toastTimer); state.toastTimer = null; }\n        var pct = Math.max(6, Math.min(100, Math.round(pr.done / Math.max(1, pr.total) * 100)));\n        mount.innerHTML = '<div class=\"ttv-toast\"><div class=\"ttv-toast-progress\">' +\n          '<span>' + esc(pr.label) + '</span>' +\n          '<div class=\"ttv-toast-bar\"><div class=\"ttv-toast-bar-fill\" style=\"width:' + pct + '%\"></div></div>' +\n          '</div></div>';\n      }\n      function trackStep(promise) {\n        return promise.then(function (r) {\n          if (state.exportProgress) { state.exportProgress.done++; toastProgress(); }\n          return r;\n        });\n      }\n      function toast(msg, isError) {\n        var mount = el('ttv-toast-mount');\n        if (!mount) return;\n        mount.innerHTML = '<div class=\"ttv-toast' + (isError ? ' ttv-toast-error' : '') + '\">' +\n          '<i class=\"fas ' + (isError ? 'fa-exclamation-circle' : 'fa-check-circle') + '\"></i>' +\n          '<span>' + esc(msg) + '</span></div>';\n        if (state.toastTimer) clearTimeout(state.toastTimer);\n        state.toastTimer = setTimeout(function () {\n          var m = el('ttv-toast-mount');\n          if (m) m.innerHTML = '';\n        }, 5000);\n      }\n\n      /* ---------------- data loading ---------------- */\n      function loadStatic() {\n        var pProjects = api('/users/projects-v1').catch(function () { return []; });\n        var pTeams = api('/users/teams-v1').catch(function () { return []; });\n        var pRoles = api('/users/user-roles-v1').catch(function () { return []; });\n        return Promise.all([pProjects, pTeams, pRoles]).then(function (res) {\n          state.allProjects = Array.isArray(res[0]) ? res[0] : [];\n          state.teams = Array.isArray(res[1]) ? res[1] : [];\n          state.roles = Array.isArray(res[2]) ? res[2] : [];\n        });\n      }\n      function loadMe() {\n        return api('/users/users-v1/me').then(function (me) {\n          state.me = me;\n          return me;\n        });\n      }\n      function sharesTeamWithMe(u) {\n        if (u.id === state.me.id) return true;\n        var mine = [state.me.teamId].concat(state.me.supervisedTeamIds || []).filter(Boolean).map(String);\n        var theirs = [u.teamId].concat(u.supervisedTeamIds || []).filter(Boolean).map(String);\n        return theirs.some(function (t) { return mine.indexOf(t) !== -1; });\n      }\n      /* platform-removed = SaleSys-suspended (avstängd) OR deactivated — hidden from the\n         normal list like the native user view, but selectable via the export toggle.\n         Note: u.suspension is the PLATFORM suspension, NOT our ttSuspension. */\n      function isRemovedUser(u) { return Boolean(u.suspension) || u.active === false; }\n      function loadUsers() {\n        if (!state.adminAllowed) return Promise.resolve();\n        return api('/users/users-v1').then(function (users) {\n          var scoped = (users || []).filter(function (u) {\n            if (u.type === 'systemUser') return false;\n            /* superadmin sees the whole org; team leaders only their own team(s) */\n            return state.isSuperAdmin || sharesTeamWithMe(u);\n          });\n          state.users = scoped.filter(function (u) { return !isRemovedUser(u); });\n          state.removedUsers = scoped.filter(isRemovedUser);\n        }).catch(function () { /* non-admins may lack access in odd setups */ });\n      }\n\n      function selfHealStaleTimer() {\n        /* A timer left running for a very long time (closed laptop, forgotten) is capped. */\n        var a = activeOf(state.me);\n        if (!a) return Promise.resolve();\n        var started = new Date(a.startedAt).getTime();\n        if (Date.now() - started < STALE_AFTER_MS) return Promise.resolve();\n        var stopAt = new Date(Math.min(started + MAX_SHIFT_MS, Date.now())).toISOString();\n        var entries = gcEntries(entriesOf(state.me).concat([{\n          id: newId(), projectId: a.projectId || null, startAt: a.startedAt, stopAt: stopAt\n        }]));\n        return apiPut('/users/users-v1/me', {\n          reference: { ttActive: null, ttEntries: entries },\n          replaceReference: false\n        }).then(function () {\n          state.me.reference = state.me.reference || {};\n          state.me.reference.ttActive = null;\n          state.me.reference.ttEntries = entries;\n          toast('Ett tidspass stoppades automatiskt (glömd timer, maxlängd 12 timmar).');\n        }).catch(function () {});\n      }\n\n      function gcEntries(entries) {\n        var cutoff = Date.now() - GC_KEEP_MS;\n        var kept = entries.filter(function (e) {\n          return e && e.startAt && e.stopAt && new Date(e.stopAt).getTime() > cutoff;\n        }).sort(function (a, b) { return new Date(a.startAt) - new Date(b.startAt); });\n        if (kept.length > GC_MAX_ENTRIES) kept = kept.slice(kept.length - GC_MAX_ENTRIES);\n        return kept;\n      }\n\n      /* ---------------- smart tracking (native SaleSys activity tracking) ---------------- */\n      function loadSmart() {\n        /* Native activity data: issue_1295 = summed active time (admin scope covers everyone),\n           user-tabs-v1 = live presence. Only fetched when the admin list needs it. */\n        if (!state.adminAllowed || state.trackMode === 'manual') return Promise.resolve();\n        var from = startOfDay(new Date()).toISOString();\n        var to = new Date(Date.now() + 60000).toISOString();\n        var pSum = api('/users/statistics-v1/own/issue_1295_active_user_time', { from: from, to: to })\n          .catch(function () { return []; });\n        var pTabs = api('/users/user-tabs-v1').catch(function () { return []; });\n        return Promise.all([pSum, pTabs]).then(function (res) {\n          var today = {};\n          (res[0] || []).forEach(function (row) {\n            today[row.userId] = (today[row.userId] || 0) + (row.activeDuration || 0);\n          });\n          /* one row per tab — keep the \"best\" tab per user (active+visible+freshest beat) */\n          var tabs = {};\n          (res[1] || []).forEach(function (tab) {\n            if (tab.endAt) return;\n            var prev = tabs[tab.userId];\n            function score(x) {\n              return (x.isUserActive ? 4 : 0) + (!x.isHidden ? 2 : 0) +\n                (new Date(x.heartbeatAt || 0).getTime() > Date.now() - 60000 ? 1 : 0);\n            }\n            if (!prev || score(tab) > score(prev)) tabs[tab.userId] = tab;\n          });\n          state.smart = { today: today, tabs: tabs };\n        });\n      }\n      function smartPresence(userId) {\n        var tab = state.smart && state.smart.tabs && state.smart.tabs[userId];\n        if (!tab) return null;\n        var fresh = new Date(tab.heartbeatAt || 0).getTime() > Date.now() - 60000;\n        return {\n          isActive: Boolean(tab.isUserActive && !tab.isHidden && fresh),\n          isOnline: fresh,\n          projectId: tab.projectId || null\n        };\n      }\n\n      function boot() {\n        renderDate();\n        Promise.all([loadStatic(), loadMe()]).then(function () {\n          state.isSuperAdmin = state.me.type === 'admin';\n          /* View-specific rights (window.form.rights, defined on the preset — assigned via\n             roles/teams in user settings; admins always have all). Falls back to the old\n             platform-rights heuristic when the org hasn't got the new rights API yet. */\n          function viewRightsList() {\n            try {\n              if (window.form && Array.isArray(window.form.rights)) return window.form.rights;\n            } catch (e) {}\n            return null;\n          }\n          var vr = viewRightsList();\n          var legacyManage = meHasRight('users.updateForOwnTeam');\n          var legacyKick = meHasRight('users.logOutForOwnTeam');\n          var legacySee = (legacyManage || legacyKick) &&\n            (meHasRight('calls.getAll') || meHasRight('calls.getGetByOwnTeam'));\n          function hasVR(right, legacy) {\n            if (state.isSuperAdmin) return true;\n            if (vr) return vr.indexOf(right) !== -1;\n            return legacy;\n          }\n          state.canEditTime = hasVR('editTime', legacyManage);\n          state.canManageDialer = hasVR('manageDialer', legacyManage);\n          /* kick uses the PLATFORM right \"Logga ut användare\" — it already exists there,\n             no duplicate view-right needed (and the server enforces it anyway) */\n          state.canLogOutUsers = state.isSuperAdmin || legacyKick;\n          state.canExportTeam = hasVR('exportTeam', legacySee);\n          state.adminAllowed = hasVR('viewTeamTime', legacySee) ||\n            state.canEditTime || state.canManageDialer || state.canExportTeam;\n          /* legacy alias used by write paths (stop timer before kick etc.) */\n          state.canUpdateUsers = state.canEditTime;\n          state.projects = visibleProjects(state.me);\n          state.trackMode = (refOf(state.me).ttPrefs || {}).trackMode || 'manual';\n          state.selfSuspended = Boolean(suspensionOf(state.me));\n          /* admins land on the company (Admin) tab by default */\n          state.tab = state.adminAllowed ? 'admin' : 'seller';\n          return selfHealStaleTimer();\n        }).then(function () {\n          return Promise.all([loadUsers(), loadSmart()]);\n        }).then(function () {\n          state.loaded = true;\n          renderAll();\n        }).catch(function (err) {\n          console.error('ttv boot', err);\n          var p = el('ttv-projects');\n          if (p) p.innerHTML = '<div class=\"ttv-error\">Kunde inte ladda vyn. Ladda om sidan.</div>';\n        });\n      }\n\n      function refresh() {\n        loadMe().then(function () {\n          /* Call rights are baked into the session token and only refresh on a full page load.\n             When an admin re-activates me (clears my pause), reload so my rights come back. */\n          var nowSusp = Boolean(suspensionOf(state.me));\n          if (state.selfSuspended && !nowSusp) {\n            window.location.reload();\n            return null;\n          }\n          state.selfSuspended = nowSusp;\n          return Promise.all([loadUsers(), loadSmart()]);\n        }).then(function (res) {\n          if (res === null) return; /* reloading */\n          if (state.loaded) renderAll();\n        }).catch(function () {});\n      }\n\n      /* ---------------- self: start / stop / switch ---------------- */\n      function isVisibleProject(pid) {\n        return (state.projects || []).some(function (p) { return p.id === pid; });\n      }\n      function lsCurrentProjectId() {\n        /* the app persists the active project as currentProjectId_<orgId> (ProjectContextProvider) */\n        try {\n          var me = state.me || {};\n          var orgId = me.organizationId || (me.organization && me.organization.id);\n          if (!orgId) return null;\n          var v = localStorage.getItem('currentProjectId_' + orgId);\n          return (v && v !== 'null' && isVisibleProject(v)) ? v : null;\n        } catch (e) { return null; }\n      }\n      function selectedProjectId() {\n        var prefs = refOf(state.me).ttPrefs || {};\n        var pid = prefs.projectId !== undefined ? prefs.projectId : null;\n        /* validate the pref against visible projects */\n        if (pid && !isVisibleProject(pid)) pid = null;\n        /* no explicit choice: default to the project the user works in (app localStorage) */\n        if (pid === null) pid = lsCurrentProjectId();\n        /* if the org has projects, default to the first visible; \"Allmänt\" only when none exist */\n        if (pid === null && (state.projects || []).length) pid = state.projects[0].id;\n        return pid;\n      }\n      function writePrefs(patch, after) {\n        writeSelf(function (me) {\n          var prefs = refOf(me).ttPrefs || {};\n          var next = {};\n          Object.keys(prefs).forEach(function (k) { next[k] = prefs[k]; });\n          Object.keys(patch).forEach(function (k) { next[k] = patch[k]; });\n          return { ttPrefs: next };\n        }, after);\n      }\n\n      function writeSelf(refPatch, after) {\n        state.busy = true;\n        renderSelf();\n        /* fresh read first to avoid clobbering entries written elsewhere */\n        loadMe().then(function () {\n          var patch = refPatch(state.me);\n          if (!patch) return null;\n          return apiPut('/users/users-v1/me', { reference: patch, replaceReference: false })\n            .then(function () {\n              state.me.reference = state.me.reference || {};\n              Object.keys(patch).forEach(function (k) { state.me.reference[k] = patch[k]; });\n            });\n        }).then(function () {\n          state.busy = false;\n          if (after) after();\n          renderAll();\n        }).catch(function (err) {\n          console.error('ttv write', err);\n          state.busy = false;\n          toast('Kunde inte spara. Försök igen.', true);\n          renderAll();\n        });\n      }\n\n      function startTimer(projectId) {\n        writeSelf(function (me) {\n          if (activeOf(me)) return null; /* already running (other tab) */\n          return { ttActive: { startedAt: new Date().toISOString(), projectId: projectId !== undefined ? projectId : selectedProjectId() } };\n        });\n      }\n      function stopTimer(thenStartProjectId) {\n        writeSelf(function (me) {\n          var a = activeOf(me);\n          if (!a) return null;\n          var stopAt = new Date().toISOString();\n          var entries = gcEntries(entriesOf(me).concat([{\n            id: newId(), projectId: a.projectId || null, startAt: a.startedAt, stopAt: stopAt\n          }]));\n          var patch = { ttEntries: entries };\n          patch.ttActive = thenStartProjectId === undefined ? null :\n            { startedAt: stopAt, projectId: thenStartProjectId };\n          return patch;\n        }, thenStartProjectId === undefined ? null : function () {\n          toast('Bytte projekt till ' + projectById(thenStartProjectId).name + '.');\n        });\n      }\n      function pickProject(projectId) {\n        var a = activeOf(state.me);\n        if (a) {\n          if ((a.projectId || null) === (projectId || null)) return;\n          stopTimer(projectId); /* switch: close entry, start new on the new project */\n          return;\n        }\n        writePrefs({ projectId: projectId });\n      }\n\n      /* ---------------- admin: entry editing ---------------- */\n      function openEditor(userId) {\n        var user = (state.users || []).filter(function (u) { return u.id === userId; })[0];\n        if (!user) return;\n        var dk = dayKey(new Date());\n        state.edit = { userId: userId, date: dk, rows: rowsForDay(user, dk), dirty: false };\n        renderAdmin();\n      }\n      function rowsForDay(user, dk) {\n        return entriesForDay(user, dk).map(function (e) {\n          return { id: e.id, projectId: e.projectId || null, start: timeInputValue(e.startAt), stop: timeInputValue(e.stopAt) };\n        });\n      }\n      function editUser() {\n        if (!state.edit) return null;\n        return (state.users || []).filter(function (u) { return u.id === state.edit.userId; })[0] || null;\n      }\n      function rowToDates(row, dk) {\n        var s = /^(\\d{1,2}):(\\d{2})$/.exec(row.start || '');\n        var e = /^(\\d{1,2}):(\\d{2})$/.exec(row.stop || '');\n        if (!s || !e) return null;\n        var base = new Date(dk + 'T00:00:00');\n        var start = new Date(base); start.setHours(Number(s[1]), Number(s[2]), 0, 0);\n        var stop = new Date(base); stop.setHours(Number(e[1]), Number(e[2]), 0, 0);\n        if (stop <= start) return null;\n        return { startAt: start.toISOString(), stopAt: stop.toISOString() };\n      }\n      function saveEdit() {\n        var edit = state.edit;\n        if (!edit) return;\n        var converted = [];\n        for (var i = 0; i < edit.rows.length; i++) {\n          var d = rowToDates(edit.rows[i], edit.date);\n          if (!d) { toast('Kontrollera tiderna: sluttid måste vara efter starttid.', true); renderAdmin(); return; }\n          converted.push({ id: edit.rows[i].id || newId(), projectId: edit.rows[i].projectId || null, startAt: d.startAt, stopAt: d.stopAt });\n        }\n        /* fresh read, replace only this local day's entries */\n        api('/users/users-v1/' + edit.userId).then(function (fresh) {\n          var others = entriesOf(fresh).filter(function (e) { return dayKey(e.startAt) !== edit.date; });\n          var entries = gcEntries(others.concat(converted));\n          return apiPut('/users/users-v1/' + edit.userId, {\n            reference: { ttEntries: entries },\n            replaceReference: false\n          });\n        }).then(function () {\n          toast('Tider sparade.');\n          state.edit.dirty = false;\n          refresh();\n        }).catch(function (err) {\n          console.error('ttv saveEdit', err);\n          toast('Kunde inte spara — kontrollera att du har rättigheten att uppdatera användare.', true);\n        });\n      }\n      function stopUserTimer(userId) {\n        stopTimerCore(userId).then(function () {\n          toast('Timern stoppades.');\n          refresh();\n        }).catch(function (err) {\n          console.error('ttv stopUserTimer', err);\n          toast('Kunde inte stoppa timern.', true);\n        });\n      }\n\n      /* ---------------- admin: kick ---------------- */\n      function kickUserCore(userId) {\n        var stopFirst = state.canUpdateUsers ?\n          api('/users/users-v1/' + userId).then(function (fresh) {\n            var a = activeOf(fresh);\n            if (!a) return null;\n            var entries = gcEntries(entriesOf(fresh).concat([{\n              id: newId(), projectId: a.projectId || null, startAt: a.startedAt, stopAt: new Date().toISOString()\n            }]));\n            return apiPut('/users/users-v1/' + userId, {\n              reference: { ttActive: null, ttEntries: entries },\n              replaceReference: false\n            });\n          }).catch(function () {}) :\n          Promise.resolve();\n        return stopFirst.then(function () {\n          return apiPost('/users/users-v1/logout/' + userId);\n        });\n      }\n      function kickUser(userId) {\n        var user = (state.users || []).filter(function (u) { return u.id === userId; })[0];\n        var name = user ? (user.fullName || user.username) : 'användaren';\n        kickUserCore(userId).then(function () {\n          toast('Loggade ut ' + name + '. Eventuell timer stoppades.');\n          refresh();\n        }).catch(function (err) {\n          console.error('ttv kick', err);\n          toast('Kunde inte logga ut användaren.', true);\n        });\n      }\n\n      /* ---------------- admin: suspend / restore call rights ---------------- */\n      /* resolves 'ok' or 'skip' */\n      function suspendUserCore(userId) {\n        return api('/users/users-v1/' + userId).then(function (fresh) {\n          if (suspensionOf(fresh)) return 'skip';\n          var sources = callGrantingSources(fresh);\n          if (!sources.length) return 'skip';\n          var removeRoleIds = sources.filter(function (s) { return s.kind === 'role'; }).map(function (s) { return String(s.id); });\n          var removeTeam = sources.some(function (s) { return s.kind === 'team'; });\n          var removeSupervised = sources.filter(function (s) { return s.kind === 'supervisedTeam'; }).map(function (s) { return String(s.id); });\n          var snapshot = {\n            teamId: fresh.teamId || null,\n            supervisedTeamIds: fresh.supervisedTeamIds || [],\n            roleIds: fresh.roleIds || [],\n            removed: sources.map(function (s) { return { kind: s.kind, id: s.id, name: s.name }; }),\n            suspendedAt: new Date().toISOString(),\n            byUserId: state.me.id,\n            byName: state.me.fullName || state.me.username\n          };\n          var body = {\n            reference: { ttSuspension: snapshot },\n            replaceReference: false,\n            roleIds: (fresh.roleIds || []).filter(function (id) { return removeRoleIds.indexOf(String(id)) === -1; }),\n            supervisedTeamIds: (fresh.supervisedTeamIds || []).filter(function (id) { return removeSupervised.indexOf(String(id)) === -1; })\n          };\n          if (removeTeam) body.teamId = null;\n          /* the suspend also logs the user out — close a running stamp-clock timer at the\n             pause moment so \"logged out => timer stops\" holds here too (same as kick) */\n          var act = activeOf(fresh);\n          if (act) {\n            body.reference.ttActive = null;\n            body.reference.ttEntries = gcEntries(entriesOf(fresh).concat([{\n              id: newId(), projectId: act.projectId || null, startAt: act.startedAt,\n              stopAt: new Date(Math.min(Date.now(), new Date(act.startedAt).getTime() + MAX_SHIFT_MS)).toISOString()\n            }]));\n          }\n          return apiPut('/users/users-v1/' + userId, body).then(function () {\n            /* the change takes effect on next token refresh — log the user out so it bites now */\n            if (state.canLogOutUsers) {\n              return apiPost('/users/users-v1/logout/' + userId).catch(function () {});\n            }\n          }).then(function () { return 'ok'; });\n        });\n      }\n      function restoreUserCore(userId) {\n        return api('/users/users-v1/' + userId).then(function (fresh) {\n          var snap = suspensionOf(fresh);\n          if (!snap) return 'skip';\n          var body = {\n            reference: { ttSuspension: null },\n            replaceReference: false,\n            roleIds: snap.roleIds || [],\n            supervisedTeamIds: snap.supervisedTeamIds || []\n          };\n          if (snap.teamId) body.teamId = snap.teamId;\n          return apiPut('/users/users-v1/' + userId, body).then(function () { return 'ok'; });\n        });\n      }\n      function suspendUsers(userIds) {\n        runSequential(userIds, suspendUserCore).then(function (r) {\n          toast(bulkSummary('Pausade samtalsrättigheter', r), r.fail > 0);\n          state.suspendModal = null;\n          logDialerEvent('suspend', r.ok).then(refresh);\n        });\n      }\n      function restoreUsers(userIds) {\n        runSequential(userIds, restoreUserCore).then(function (r) {\n          toast(bulkSummary('Återställde rättigheter', r), r.fail > 0);\n          state.suspendModal = null;\n          notifyReactivated(userIds);\n          logDialerEvent('restore', r.ok).then(refresh);\n        });\n      }\n\n      /* append a pause/start event to the acting admin's own reference (aggregated for the log) */\n      function logDialerEvent(action, count) {\n        if (!count) return Promise.resolve();\n        return api('/users/users-v1/me').then(function (me) {\n          var log = (me.reference && me.reference.ttDialerLog) || [];\n          log = log.concat([{\n            id: newId(), action: action, at: new Date().toISOString(),\n            byUserId: state.me.id, byName: state.me.fullName || state.me.username, count: count\n          }]);\n          if (log.length > 60) log = log.slice(log.length - 60);\n          return apiPut('/users/users-v1/me', { reference: { ttDialerLog: log }, replaceReference: false }).then(function () {\n            state.me.reference = state.me.reference || {};\n            state.me.reference.ttDialerLog = log;\n          });\n        }).catch(function () {});\n      }\n\n      /* notify re-activated users in-app (their token rights only refresh on reload) */\n      function notifyReactivated(userIds) {\n        var ids = (userIds || []).filter(function (id) { return id && id !== state.me.id; });\n        if (!ids.length) return Promise.resolve();\n        return apiPost('/users/user-messages-v1', {\n          target: { userIds: ids },\n          notification: {\n            title: 'Dialern är aktiverad',\n            text: 'Ladda om sidan för att kunna ringa igen.',\n            iconName: 'phone-volume',\n            link: '/',\n            confetti: true\n          },\n          expireAfterSeconds: 900\n        }).catch(function () {});\n      }\n\n      /* ---------------- bulk helpers ---------------- */\n      function selectedIds() {\n        return Object.keys(state.selected).filter(function (id) {\n          return state.selected[id] && (state.users || []).some(function (u) { return u.id === id; });\n        });\n      }\n      function runSequential(ids, coreFn) {\n        var result = { ok: 0, skip: 0, fail: 0 };\n        return ids.reduce(function (p, id) {\n          return p.then(function () {\n            return coreFn(id).then(function (r) {\n              if (r === 'skip') result.skip++; else result.ok++;\n            }, function (err) {\n              console.error('ttv bulk', id, err);\n              result.fail++;\n            });\n          });\n        }, Promise.resolve()).then(function () { return result; });\n      }\n      function bulkSummary(verb, r) {\n        var parts = [verb + ' för ' + r.ok + ' användare'];\n        if (r.skip) parts.push(r.skip + ' hoppades över');\n        if (r.fail) parts.push(r.fail + ' misslyckades');\n        return parts.join(', ') + '.';\n      }\n      function stopTimerCore(userId) {\n        return api('/users/users-v1/' + userId).then(function (fresh) {\n          var a = activeOf(fresh);\n          if (!a) return 'skip';\n          var entries = gcEntries(entriesOf(fresh).concat([{\n            id: newId(), projectId: a.projectId || null, startAt: a.startedAt,\n            stopAt: new Date(Math.min(Date.now(), new Date(a.startedAt).getTime() + MAX_SHIFT_MS)).toISOString()\n          }]));\n          return apiPut('/users/users-v1/' + userId, {\n            reference: { ttActive: null, ttEntries: entries },\n            replaceReference: false\n          }).then(function () { return 'ok'; });\n        });\n      }\n      function bulkStopTimers(userIds) {\n        runSequential(userIds, stopTimerCore).then(function (r) {\n          toast(bulkSummary('Stoppade timern', r), r.fail > 0);\n          refresh();\n        });\n      }\n      function bulkKick(userIds) {\n        runSequential(userIds, kickUserCore).then(function (r) {\n          toast(bulkSummary('Loggade ut', r), r.fail > 0);\n          state.selected = {};\n          refresh();\n        });\n      }\n\n      /* ---------------- dialer master switch (admin tab) ---------------- */\n      /* users who can currently be paused (have a call-granting team/role, not admin, not me, not already paused) */\n      function dialerActiveUsers() {\n        return (state.users || []).filter(function (u) {\n          return u.type !== 'admin' && u.id !== state.me.id && !suspensionOf(u) && callGrantingSources(u).length;\n        });\n      }\n      function dialerSuspendedUsers() {\n        return (state.users || []).filter(function (u) { return suspensionOf(u); });\n      }\n      function activateDialer() {\n        var ids = dialerSuspendedUsers().map(function (u) { return u.id; });\n        if (!ids.length) { toast('Dialern är redan aktiv — inga pausade användare.'); return; }\n        state.dialerBusy = true;\n        state.armedStopDialer = null;\n        renderDialerPanel();\n        runSequential(ids, restoreUserCore).then(function (r) {\n          state.dialerBusy = false;\n          toast(bulkSummary('Aktiverade dialern', r), r.fail > 0);\n          notifyReactivated(ids);\n          logDialerEvent('activate', r.ok).then(refresh);\n        });\n      }\n      function stopDialer() {\n        var ids = dialerActiveUsers().map(function (u) { return u.id; });\n        if (!ids.length) { toast('Inga användare med samtalsrättigheter att stoppa.', true); state.armedStopDialer = null; renderDialerPanel(); return; }\n        state.dialerBusy = true;\n        state.armedStopDialer = null;\n        renderDialerPanel();\n        runSequential(ids, suspendUserCore).then(function (r) {\n          state.dialerBusy = false;\n          toast(bulkSummary('Stoppade dialern och loggade ut', r), r.fail > 0);\n          logDialerEvent('stop', r.ok).then(refresh);\n        });\n      }\n\n      /* ---------------- export to xlsx (pivot workbook, no external libs) ----------------\n         Mirrors the Pivot_Greenpeace template: data sheets + real pivot-table sheets.\n         Trick from the template itself: pivotCacheDefinition with saveData=\"0\",\n         refreshOnLoad=\"1\" and dummy sharedItems — Excel rebuilds the pivot from the\n         data sheet when the file is opened, so no cache records are needed. */\n      /* __TTV_PXL_BEGIN__ */\n      var PXL = (function () {\n        var XMLDECL = '<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>';\n        var NS_MAIN = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';\n        var NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';\n        var NS_PKG_REL = 'http://schemas.openxmlformats.org/package/2006/relationships';\n        var CT = {\n          workbook: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',\n          sheet: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',\n          styles: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml',\n          cache: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',\n          pivot: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',\n          core: 'application/vnd.openxmlformats-package.core-properties+xml',\n          app: 'application/vnd.openxmlformats-officedocument.extended-properties+xml'\n        };\n        var REL = {\n          sheet: NS_R + '/worksheet',\n          styles: NS_R + '/styles',\n          cache: NS_R + '/pivotCacheDefinition',\n          pivot: NS_R + '/pivotTable',\n          workbook: NS_R + '/officeDocument',\n          core: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',\n          app: NS_R + '/extended-properties'\n        };\n\n        function escXml(v) {\n          return String(v == null ? '' : v)\n            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n            .replace(/\"/g, '&quot;').replace(/[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]/g, '');\n        }\n        function colLetter(i) {\n          var t = '';\n          i++;\n          while (i > 0) { var m = (i - 1) % 26; t = String.fromCharCode(65 + m) + t; i = Math.floor((i - 1) / 26); }\n          return t;\n        }\n        function cellRef(c, r) { return colLetter(c) + (r + 1); }\n\n        /* ---- stored (uncompressed) zip writer ---- */\n        var CRC_T = null;\n        function crcTable() {\n          if (CRC_T) return CRC_T;\n          CRC_T = [];\n          for (var n = 0; n < 256; n++) {\n            var c = n;\n            for (var k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);\n            CRC_T[n] = c >>> 0;\n          }\n          return CRC_T;\n        }\n        function crc32(bytes) {\n          var t = crcTable(), c = 0xFFFFFFFF;\n          for (var i = 0; i < bytes.length; i++) c = (c >>> 8) ^ t[(c ^ bytes[i]) & 0xFF];\n          return (c ^ 0xFFFFFFFF) >>> 0;\n        }\n        function pushN(arr, v, bytes) {\n          for (var i = 0; i < bytes; i++) arr.push((v >>> (8 * i)) & 0xFF);\n        }\n        function zipStore(files) {\n          var out = [], central = [], offset = 0;\n          var encoder = new TextEncoder();\n          files.forEach(function (f) {\n            var name = encoder.encode(f.name);\n            var data = (typeof f.data === 'string') ? encoder.encode(f.data) : f.data;\n            var crc = crc32(data);\n            var local = [];\n            pushN(local, 0x04034b50, 4); pushN(local, 20, 2); pushN(local, 0x0800, 2);\n            pushN(local, 0, 2); pushN(local, 0, 2); pushN(local, 0x21, 2);\n            pushN(local, crc, 4); pushN(local, data.length, 4); pushN(local, data.length, 4);\n            pushN(local, name.length, 2); pushN(local, 0, 2);\n            out.push(new Uint8Array(local), name, data);\n            var cd = [];\n            pushN(cd, 0x02014b50, 4); pushN(cd, 20, 2); pushN(cd, 20, 2); pushN(cd, 0x0800, 2);\n            pushN(cd, 0, 2); pushN(cd, 0, 2); pushN(cd, 0x21, 2);\n            pushN(cd, crc, 4); pushN(cd, data.length, 4); pushN(cd, data.length, 4);\n            pushN(cd, name.length, 2); pushN(cd, 0, 2); pushN(cd, 0, 2);\n            pushN(cd, 0, 2); pushN(cd, 0, 2); pushN(cd, 0, 4); pushN(cd, offset, 4);\n            central.push(new Uint8Array(cd), name);\n            offset += 30 + name.length + data.length;\n          });\n          var cdSize = 0;\n          central.forEach(function (bb) { cdSize += bb.length; });\n          var eocd = [];\n          pushN(eocd, 0x06054b50, 4); pushN(eocd, 0, 2); pushN(eocd, 0, 2);\n          pushN(eocd, files.length, 2); pushN(eocd, files.length, 2);\n          pushN(eocd, cdSize, 4); pushN(eocd, offset, 4); pushN(eocd, 0, 2);\n          var total = offset + cdSize + eocd.length;\n          var buf = new Uint8Array(total), pos = 0;\n          out.concat(central, [new Uint8Array(eocd)]).forEach(function (bb) { buf.set(bb, pos); pos += bb.length; });\n          return buf;\n        }\n\n        /* ---- sheet xml (inline strings + numbers); styleMap: rowIdx -> {colIdx -> styleId} ---- */\n        function sheetXml(aoa, styleMap) {\n          var rows = '', maxCols = 1;\n          for (var r = 0; r < aoa.length; r++) {\n            var cells = '';\n            if (aoa[r] && aoa[r].length > maxCols) maxCols = aoa[r].length;\n            for (var c = 0; c < (aoa[r] || []).length; c++) {\n              var v = aoa[r][c];\n              if (v == null || v === '') continue;\n              var st = styleMap && styleMap[r] && styleMap[r][c] ? ' s=\"' + styleMap[r][c] + '\"' : '';\n              if (typeof v === 'number' && isFinite(v)) {\n                cells += '<c r=\"' + cellRef(c, r) + '\"' + st + '><v>' + v + '</v></c>';\n              } else {\n                cells += '<c r=\"' + cellRef(c, r) + '\"' + st + ' t=\"inlineStr\"><is><t xml:space=\"preserve\">' + escXml(v) + '</t></is></c>';\n              }\n            }\n            if (cells) rows += '<row r=\"' + (r + 1) + '\">' + cells + '</row>';\n          }\n          var dim = 'A1:' + cellRef(maxCols - 1, Math.max(aoa.length, 1) - 1);\n          return XMLDECL + '<worksheet xmlns=\"' + NS_MAIN + '\"><dimension ref=\"' + dim + '\"/>' +\n            '<cols><col min=\"1\" max=\"2\" width=\"24\" customWidth=\"1\"/></cols>' +\n            '<sheetData>' + rows + '</sheetData></worksheet>';\n        }\n\n        /* ---- compute a 2-level pivot (rows: level0 > level1, one summed value) ---- */\n        function computePivot(srcAoa, header, rowCols, valueCol) {\n          var i0 = header.indexOf(rowCols[0]);\n          var i1 = header.indexOf(rowCols[1]);\n          var iv = header.indexOf(valueCol);\n          var l0 = [], l1 = [], seen0 = {}, seen1 = {};\n          var totals = {}, grand = 0;\n          for (var r = 1; r < srcAoa.length; r++) {\n            var row = srcAoa[r];\n            var a = String(row[i0]), b = String(row[i1]);\n            var v = Number(row[iv]) || 0;\n            if (!(a in seen0)) { seen0[a] = l0.length; l0.push(a); }\n            if (!(b in seen1)) { seen1[b] = l1.length; l1.push(b); }\n            var t = totals[a] || (totals[a] = { sum: 0, sub: {}, order: [] });\n            if (!(b in t.sub)) { t.sub[b] = 0; t.order.push(b); }\n            t.sub[b] += v;\n            t.sum += v;\n            grand += v;\n          }\n          l0.sort();\n          l1.sort();\n          var idx0 = {}, idx1 = {};\n          l0.forEach(function (x, i) { idx0[x] = i; });\n          l1.forEach(function (x, i) { idx1[x] = i; });\n          return { l0: l0, l1: l1, idx0: idx0, idx1: idx1, totals: totals, grand: grand, i0: i0, i1: i1, iv: iv };\n        }\n        function round2(v) { return Math.round(v * 100) / 100; }\n\n        function cacheXml(sourceSheet, ref, header, pv) {\n          var cf = '';\n          header.forEach(function (name, i) {\n            if (i === pv.i0 || i === pv.i1) {\n              var list = (i === pv.i0) ? pv.l0 : pv.l1;\n              var items = '';\n              list.forEach(function (v) { items += '<s v=\"' + escXml(v) + '\"/>'; });\n              cf += '<cacheField name=\"' + escXml(name) + '\" numFmtId=\"0\"><sharedItems count=\"' + list.length + '\">' + items + '</sharedItems></cacheField>';\n            } else if (i === pv.iv) {\n              cf += '<cacheField name=\"' + escXml(name) + '\" numFmtId=\"0\"><sharedItems containsSemiMixedTypes=\"0\" containsString=\"0\" containsNumber=\"1\"/></cacheField>';\n            } else {\n              cf += '<cacheField name=\"' + escXml(name) + '\" numFmtId=\"0\"><sharedItems/></cacheField>';\n            }\n          });\n          return XMLDECL +\n            '<pivotCacheDefinition xmlns=\"' + NS_MAIN + '\" xmlns:r=\"' + NS_R + '\" saveData=\"0\" refreshOnLoad=\"1\" createdVersion=\"5\" refreshedVersion=\"5\" minRefreshableVersion=\"3\" recordCount=\"' + Math.max(1, 0) + '\">' +\n            '<cacheSource type=\"worksheet\"><worksheetSource ref=\"' + ref + '\" sheet=\"' + escXml(sourceSheet) + '\"/></cacheSource>' +\n            '<cacheFields count=\"' + header.length + '\">' + cf + '</cacheFields>' +\n            '</pivotCacheDefinition>';\n        }\n\n        function pivotXml(name, cacheId, header, pv, dataName, locRef, caption) {\n          var pf = '';\n          header.forEach(function (h, i) {\n            if (i === pv.i0 || i === pv.i1) {\n              var list = (i === pv.i0) ? pv.l0 : pv.l1;\n              var items = '';\n              list.forEach(function (v, k) { items += '<item x=\"' + k + '\"/>'; });\n              items += '<item t=\"default\"/>';\n              pf += '<pivotField axis=\"axisRow\" showAll=\"0\"><items count=\"' + (list.length + 1) + '\">' + items + '</items></pivotField>';\n            } else if (i === pv.iv) {\n              pf += '<pivotField dataField=\"1\" showAll=\"0\"/>';\n            } else {\n              pf += '<pivotField showAll=\"0\"/>';\n            }\n          });\n          /* rowItems mirror the rendered layout: user row, its date rows (r=1), grand total */\n          var ri = '', riCount = 0;\n          pv.l0.forEach(function (a) {\n            ri += '<i><x v=\"' + pv.idx0[a] + '\"/></i>';\n            riCount++;\n            var t = pv.totals[a];\n            var dates = t.order.slice().sort();\n            dates.forEach(function (b) {\n              ri += '<i r=\"1\"><x v=\"' + pv.idx1[b] + '\"/></i>';\n              riCount++;\n            });\n          });\n          ri += '<i t=\"grand\"><x/></i>';\n          riCount++;\n          return XMLDECL +\n            '<pivotTableDefinition xmlns=\"' + NS_MAIN + '\" name=\"' + escXml(name) + '\" cacheId=\"' + cacheId + '\"' +\n            ' applyNumberFormats=\"0\" applyBorderFormats=\"0\" applyFontFormats=\"0\" applyPatternFormats=\"0\" applyAlignmentFormats=\"0\" applyWidthHeightFormats=\"1\"' +\n            ' dataCaption=\"Värden\" grandTotalCaption=\"Totalt\" missingCaption=\"0\" updatedVersion=\"5\" minRefreshableVersion=\"3\" useAutoFormatting=\"1\"' +\n            ' itemPrintTitles=\"1\" createdVersion=\"5\" indent=\"0\" outline=\"1\" outlineData=\"1\" multipleFieldFilters=\"0\" rowHeaderCaption=\"' + escXml(caption) + '\">' +\n            '<location ref=\"' + locRef + '\" firstHeaderRow=\"1\" firstDataRow=\"1\" firstDataCol=\"1\"/>' +\n            '<pivotFields count=\"' + header.length + '\">' + pf + '</pivotFields>' +\n            '<rowFields count=\"2\"><field x=\"' + pv.i0 + '\"/><field x=\"' + pv.i1 + '\"/></rowFields>' +\n            '<rowItems count=\"' + riCount + '\">' + ri + '</rowItems>' +\n            '<dataFields count=\"1\"><dataField name=\"' + escXml(dataName) + '\" fld=\"' + pv.iv + '\" baseField=\"0\" baseItem=\"0\"/></dataFields>' +\n            '<pivotTableStyleInfo name=\"PivotStyleLight16\" showRowHeaders=\"1\" showColHeaders=\"1\" showRowStripes=\"0\" showColStripes=\"0\" showLastColumn=\"1\"/>' +\n            '</pivotTableDefinition>';\n        }\n\n        /* rendered pivot cells so the sheet shows the result even before Excel refreshes */\n        function pivotSheetAoa(pv, dataName, caption) {\n          var aoa = [[], [], [caption, dataName]];\n          var styles = { 2: { 0: 1, 1: 1 } };\n          pv.l0.forEach(function (a) {\n            styles[aoa.length] = { 0: 1, 1: 1 };\n            aoa.push([a, round2(pv.totals[a].sum)]);\n            var dates = pv.totals[a].order.slice().sort();\n            dates.forEach(function (b) {\n              aoa.push(['   ' + b, round2(pv.totals[a].sub[b])]);\n            });\n          });\n          styles[aoa.length] = { 0: 1, 1: 1 };\n          aoa.push(['Totalt', round2(pv.grand)]);\n          return { aoa: aoa, styles: styles };\n        }\n\n        var STYLES = XMLDECL + '<styleSheet xmlns=\"' + NS_MAIN + '\">' +\n          '<fonts count=\"2\"><font><sz val=\"11\"/><name val=\"Calibri\"/></font><font><b/><sz val=\"11\"/><name val=\"Calibri\"/></font></fonts>' +\n          '<fills count=\"2\"><fill><patternFill patternType=\"none\"/></fill><fill><patternFill patternType=\"gray125\"/></fill></fills>' +\n          '<borders count=\"1\"><border/></borders>' +\n          '<cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs>' +\n          '<cellXfs count=\"2\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/><xf numFmtId=\"0\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/></cellXfs>' +\n          '<cellStyles count=\"1\"><cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\"/></cellStyles>' +\n          '</styleSheet>';\n\n        function relsXml(rels) {\n          var out = '';\n          rels.forEach(function (r) {\n            out += '<Relationship Id=\"' + r.id + '\" Type=\"' + r.type + '\" Target=\"' + r.target + '\"/>';\n          });\n          return XMLDECL + '<Relationships xmlns=\"' + NS_PKG_REL + '\">' + out + '</Relationships>';\n        }\n\n        /* dataSheets: [{name, aoa}], pivots: [{name, source, header, rows: [l0Col, l1Col], value, dataName}] */\n        function build(dataSheets, pivots) {\n          var files = [];\n          /* pivot sheets first so the workbook opens on a pivot, data sheets after (like the template) */\n          var allSheets = [];\n          pivots.forEach(function (pspec) { allSheets.push({ name: pspec.name, kind: 'pivot', pivot: pspec }); });\n          dataSheets.forEach(function (d) { allSheets.push({ name: d.name, kind: 'data', aoa: d.aoa }); });\n\n          var ctOverrides = '<Override PartName=\"/xl/workbook.xml\" ContentType=\"' + CT.workbook + '\"/>' +\n            '<Override PartName=\"/xl/styles.xml\" ContentType=\"' + CT.styles + '\"/>' +\n            '<Override PartName=\"/docProps/core.xml\" ContentType=\"' + CT.core + '\"/>' +\n            '<Override PartName=\"/docProps/app.xml\" ContentType=\"' + CT.app + '\"/>';\n          var wbSheets = '', wbRels = [];\n          allSheets.forEach(function (sh, i) {\n            var n = i + 1;\n            wbSheets += '<sheet name=\"' + escXml(sh.name) + '\" sheetId=\"' + n + '\" r:id=\"rId' + n + '\"/>';\n            wbRels.push({ id: 'rId' + n, type: REL.sheet, target: 'worksheets/sheet' + n + '.xml' });\n            ctOverrides += '<Override PartName=\"/xl/worksheets/sheet' + n + '.xml\" ContentType=\"' + CT.sheet + '\"/>';\n          });\n          var nextRel = allSheets.length + 1;\n          wbRels.push({ id: 'rId' + nextRel, type: REL.styles, target: 'styles.xml' });\n          nextRel++;\n\n          var wbCaches = '';\n          pivots.forEach(function (pspec, k) {\n            var cacheId = k + 1;\n            wbCaches += '<pivotCache cacheId=\"' + cacheId + '\" r:id=\"rId' + nextRel + '\"/>';\n            wbRels.push({ id: 'rId' + nextRel, type: REL.cache, target: 'pivotCache/pivotCacheDefinition' + cacheId + '.xml' });\n            nextRel++;\n            ctOverrides += '<Override PartName=\"/xl/pivotCache/pivotCacheDefinition' + cacheId + '.xml\" ContentType=\"' + CT.cache + '\"/>' +\n              '<Override PartName=\"/xl/pivotTables/pivotTable' + cacheId + '.xml\" ContentType=\"' + CT.pivot + '\"/>';\n          });\n\n          allSheets.forEach(function (sh, i) {\n            var n = i + 1;\n            if (sh.kind === 'data') {\n              files.push({ name: 'xl/worksheets/sheet' + n + '.xml', data: sheetXml(sh.aoa, null) });\n              return;\n            }\n            var pspec = sh.pivot;\n            var k = pivots.indexOf(pspec), cacheId = k + 1;\n            var src = null;\n            dataSheets.forEach(function (d) { if (d.name === pspec.source) src = d; });\n            var header = pspec.header || src.aoa[0];\n            var pv = computePivot(src.aoa, header, pspec.rows, pspec.value);\n            var caption = pspec.rows[0];\n            var dataName = pspec.dataName || ('Summa av ' + pspec.value);\n            var rendered = pivotSheetAoa(pv, dataName, caption);\n            var locRef = 'A3:B' + rendered.aoa.length;\n            var srcRef = 'A1:' + cellRef(header.length - 1, Math.max(src.aoa.length, 2) - 1);\n            files.push({ name: 'xl/worksheets/sheet' + n + '.xml', data: sheetXml(rendered.aoa, rendered.styles) });\n            files.push({\n              name: 'xl/worksheets/_rels/sheet' + n + '.xml.rels',\n              data: relsXml([{ id: 'rId1', type: REL.pivot, target: '../pivotTables/pivotTable' + cacheId + '.xml' }])\n            });\n            files.push({ name: 'xl/pivotTables/pivotTable' + cacheId + '.xml', data: pivotXml(pspec.name, cacheId, header, pv, dataName, locRef, caption) });\n            files.push({\n              name: 'xl/pivotTables/_rels/pivotTable' + cacheId + '.xml.rels',\n              data: relsXml([{ id: 'rId1', type: REL.cache, target: '../pivotCache/pivotCacheDefinition' + cacheId + '.xml' }])\n            });\n            files.push({ name: 'xl/pivotCache/pivotCacheDefinition' + cacheId + '.xml', data: cacheXml(pspec.source, srcRef, header, pv) });\n          });\n\n          var workbook = XMLDECL + '<workbook xmlns=\"' + NS_MAIN + '\" xmlns:r=\"' + NS_R + '\">' +\n            '<sheets>' + wbSheets + '</sheets>' +\n            (wbCaches ? '<pivotCaches>' + wbCaches + '</pivotCaches>' : '') +\n            '</workbook>';\n\n          files.unshift(\n            { name: '[Content_Types].xml', data: XMLDECL + '<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">' +\n              '<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>' +\n              '<Default Extension=\"xml\" ContentType=\"application/xml\"/>' + ctOverrides + '</Types>' },\n            { name: '_rels/.rels', data: relsXml([\n              { id: 'rId1', type: REL.workbook, target: 'xl/workbook.xml' },\n              { id: 'rId2', type: REL.core, target: 'docProps/core.xml' },\n              { id: 'rId3', type: REL.app, target: 'docProps/app.xml' }\n            ]) },\n            { name: 'docProps/core.xml', data: XMLDECL + '<cp:coreProperties xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"><dc:creator>SaleSys Tidsrapportering</dc:creator></cp:coreProperties>' },\n            { name: 'docProps/app.xml', data: XMLDECL + '<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\"><Application>SaleSys</Application></Properties>' },\n            { name: 'xl/workbook.xml', data: workbook },\n            { name: 'xl/_rels/workbook.xml.rels', data: relsXml(wbRels) },\n            { name: 'xl/styles.xml', data: STYLES }\n          );\n          return zipStore(files);\n        }\n\n        /* ---- stored-zip reader + template injection (rapportmall) ---- */\n        function readStoredZip(bytes) {\n          var i = bytes.length - 22;\n          while (i >= 0 && !(bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06)) i--;\n          if (i < 0) throw new Error('bad zip');\n          var count = bytes[i + 10] | (bytes[i + 11] << 8);\n          var p = (bytes[i + 16] | (bytes[i + 17] << 8) | (bytes[i + 18] << 16) | (bytes[i + 19] << 24)) >>> 0;\n          var td = new TextDecoder();\n          var files = [];\n          for (var n = 0; n < count; n++) {\n            var nameLen = bytes[p + 28] | (bytes[p + 29] << 8);\n            var extraLen = bytes[p + 30] | (bytes[p + 31] << 8);\n            var commentLen = bytes[p + 32] | (bytes[p + 33] << 8);\n            var method = bytes[p + 10] | (bytes[p + 11] << 8);\n            var usize = (bytes[p + 24] | (bytes[p + 25] << 8) | (bytes[p + 26] << 16) | (bytes[p + 27] << 24)) >>> 0;\n            var off = (bytes[p + 42] | (bytes[p + 43] << 8) | (bytes[p + 44] << 16) | (bytes[p + 45] << 24)) >>> 0;\n            var name = td.decode(bytes.subarray(p + 46, p + 46 + nameLen));\n            if (method !== 0) throw new Error('zip entry not stored: ' + name);\n            var lnl = bytes[off + 26] | (bytes[off + 27] << 8);\n            var lel = bytes[off + 28] | (bytes[off + 29] << 8);\n            var start = off + 30 + lnl + lel;\n            files.push({ name: name, data: bytes.subarray(start, start + usize) });\n            p += 46 + nameLen + extraLen + commentLen;\n          }\n          return files;\n        }\n        function b64ToBytes(b64) {\n          var bin = atob(b64);\n          var out = new Uint8Array(bin.length);\n          for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n          return out;\n        }\n        /* replacements: { 'xl/worksheets/sheet1.xml': '<xml string>', ... } */\n        function injectTemplate(tplB64, replacements) {\n          var files = readStoredZip(b64ToBytes(tplB64));\n          files.forEach(function (f) {\n            if (replacements[f.name] !== undefined) f.data = replacements[f.name];\n          });\n          return zipStore(files);\n        }\n        /* data sheet xml for the template's hidden data sheets.\n           colTypes per column: 's' = inline string (s=3), 'd' = date serial (s=4),\n           't' = time/day-fraction (s=1), 'n' = number (s=2) */\n        function tplSheetXml(header, rows, colTypes) {\n          var out = [];\n          var head = '';\n          for (var c = 0; c < header.length; c++) {\n            head += '<c r=\"' + cellRef(c, 0) + '\" s=\"3\" t=\"inlineStr\"><is><t>' + escXml(header[c]) + '</t></is></c>';\n          }\n          out.push('<row r=\"1\">' + head + '</row>');\n          for (var r = 0; r < rows.length; r++) {\n            var cells = '';\n            for (var c2 = 0; c2 < rows[r].length; c2++) {\n              var v = rows[r][c2], ref = cellRef(c2, r + 1), tp = colTypes[c2];\n              if (tp === 's') {\n                cells += '<c r=\"' + ref + '\" s=\"3\" t=\"inlineStr\"><is><t xml:space=\"preserve\">' + escXml(v) + '</t></is></c>';\n              } else {\n                var st = tp === 'd' ? '4' : tp === 't' ? '1' : '2';\n                cells += '<c r=\"' + ref + '\" s=\"' + st + '\"><v>' + (Number(v) || 0) + '</v></c>';\n              }\n            }\n            out.push('<row r=\"' + (r + 2) + '\">' + cells + '</row>');\n          }\n          var dim = 'A1:' + cellRef(header.length - 1, rows.length);\n          return XMLDECL + '<worksheet xmlns=\"' + NS_MAIN + '\"><dimension ref=\"' + dim + '\"/><sheetData>' + out.join('') + '</sheetData></worksheet>';\n        }\n\n        return { build: build, injectTemplate: injectTemplate, tplSheetXml: tplSheetXml };\n      })();\n      /* __TTV_PXL_END__ */\n      /* __TTV_TPL_BEGIN__ */\n      var TPL_B64 = 'UEsDBBQAAAAAAAAAIQD/////HAIAABwCAAAQAAAAW3RyYXNoXS8wMDAwLmRhdP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBLAwQUAAAAAAAAACEA/////+EAAADhAAAAEAAAAFt0cmFzaF0vMDAwMi5kYXT/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQSwMEFAAAAAAAAAAhAKyb9gZLDwAASw8AAA8AAAB4bC93b3JrYm9vay54bWzvu788P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8d29ya2Jvb2sgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgeG1sbnM6cj0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcyIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngxNSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+DQogIDxmaWxlVmVyc2lvbiBhcHBOYW1lPSJ4bCIgbGFzdEVkaXRlZD0iNiIgbG93ZXN0RWRpdGVkPSI2IiBydXBCdWlsZD0iMTQ0MjAiIC8+DQogIDx3b3JrYm9va1ByIGhpZGVQaXZvdEZpZWxkTGlzdD0iMSIgZGVmYXVsdFRoZW1lVmVyc2lvbj0iMTUzMjIyIiAvPg0KICA8bWM6QWx0ZXJuYXRlQ29udGVudCB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiPg0KICAgIDxtYzpDaG9pY2UgUmVxdWlyZXM9IngxNSI+DQogICAgICA8eDE1YWM6YWJzUGF0aCB1cmw9IkM6XFVzZXJzXHJpY21hZ1xEZXNrdG9wXERpdmVyZSB0ZXh0ZmlsZXIuICh0ZWxlZm9uc3RhbXRhbCBldGMpXFBpdm90RHJlYW1yZXBvcnRcIiB4bWxuczp4MTVhYz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDEwLzExL2FjIiAvPg0KICAgIDwvbWM6Q2hvaWNlPg0KICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogIDxib29rVmlld3M+DQogICAgPHdvcmtib29rVmlldyB4V2luZG93PSIwIiB5V2luZG93PSIwIiB3aW5kb3dXaWR0aD0iMjg4MDAiIHdpbmRvd0hlaWdodD0iMTMwMjAiIGZpcnN0U2hlZXQ9IjMiIGFjdGl2ZVRhYj0iMyIgLz4NCiAgPC9ib29rVmlld3M+DQogIDxzaGVldHM+DQogICAgPHNoZWV0IG5hbWU9IkRhdGFfQWdlbnRzdGF0aXN0aWsiIHNoZWV0SWQ9IjEiIHN0YXRlPSJoaWRkZW4iIHI6aWQ9InJJZDEiIC8+DQogICAgPHNoZWV0IG5hbWU9IkRhdGFfUHJvamVrdHN0YXRpc3RpayIgc2hlZXRJZD0iMiIgc3RhdGU9ImhpZGRlbiIgcjppZD0icklkMiIgLz4NCiAgICA8c2hlZXQgbmFtZT0iRGF0YV9VdGZhbGxzc3RhdGlzdGlrIiBzaGVldElkPSIzIiBzdGF0ZT0iaGlkZGVuIiByOmlkPSJySWQzIiAvPg0KICAgIDxzaGVldCBuYW1lPSJBZ2VudHN0YXRzIiBzaGVldElkPSI0IiByOmlkPSJySWQ0IiAvPg0KICAgIDxzaGVldCBuYW1lPSJQcm9qZWt0c3RhdHMiIHNoZWV0SWQ9IjUiIHI6aWQ9InJJZDUiIC8+DQogICAgPHNoZWV0IG5hbWU9IlV0ZmFsbHNzdGF0cyIgc2hlZXRJZD0iNiIgcjppZD0icklkNiIgLz4NCiAgPC9zaGVldHM+DQogIDxkZWZpbmVkTmFtZXM+DQogICAgPGRlZmluZWROYW1lIG5hbWU9IkRhdGFfQWdlbnRzdGF0aXN0aWtfVGFibGUiPk9GRlNFVChEYXRhX0FnZW50c3RhdGlzdGlrISRBJDEsMCwwLENPVU5UQShEYXRhX0FnZW50c3RhdGlzdGlrISRBJDE6JEEkNTAwMDAwKSxDT1VOVEEoRGF0YV9BZ2VudHN0YXRpc3RpayEkQSQxOiRYJDEpKTwvZGVmaW5lZE5hbWU+DQogICAgPGRlZmluZWROYW1lIG5hbWU9IkRhdGFfUHJvamVrdHN0YXRpc3Rpa19UYWJsZSI+T0ZGU0VUKERhdGFfUHJvamVrdHN0YXRpc3RpayEkQSQxLDAsMCxDT1VOVEEoRGF0YV9Qcm9qZWt0c3RhdGlzdGlrISRBJDE6JEEkNTAwMDAwKSxDT1VOVEEoRGF0YV9Qcm9qZWt0c3RhdGlzdGlrISRBJDE6JFokMSkpPC9kZWZpbmVkTmFtZT4NCiAgICA8ZGVmaW5lZE5hbWUgbmFtZT0iRGF0YV9VdGZhbGxzc3RhdGlzdGlrX1RhYmxlIj5PRkZTRVQoRGF0YV9VdGZhbGxzc3RhdGlzdGlrISRBJDEsMCwwLENPVU5UQShEYXRhX1V0ZmFsbHNzdGF0aXN0aWshJEEkMTokQSQ1MDAwMDApLENPVU5UQShEYXRhX1V0ZmFsbHNzdGF0aXN0aWshJEEkMTokWiQxKSk8L2RlZmluZWROYW1lPg0KICAgIDxkZWZpbmVkTmFtZSBuYW1lPSJTbGljZXJfQW52w6RuZGFyZSI+I04vQTwvZGVmaW5lZE5hbWU+DQogICAgPGRlZmluZWROYW1lIG5hbWU9IlNsaWNlcl9EYXR1bSI+I04vQTwvZGVmaW5lZE5hbWU+DQogICAgPGRlZmluZWROYW1lIG5hbWU9IlNsaWNlcl9EYXR1bTEiPiNOL0E8L2RlZmluZWROYW1lPg0KICAgIDxkZWZpbmVkTmFtZSBuYW1lPSJTbGljZXJfRGF0dW0yIj4jTi9BPC9kZWZpbmVkTmFtZT4NCiAgICA8ZGVmaW5lZE5hbWUgbmFtZT0iU2xpY2VyX1Byb2pla3QiPiNOL0E8L2RlZmluZWROYW1lPg0KICAgIDxkZWZpbmVkTmFtZSBuYW1lPSJTbGljZXJfUHJvamVrdDEiPiNOL0E8L2RlZmluZWROYW1lPg0KICAgIDxkZWZpbmVkTmFtZSBuYW1lPSJTbGljZXJfUHJvamVrdDIiPiNOL0E8L2RlZmluZWROYW1lPg0KICAgIDxkZWZpbmVkTmFtZSBuYW1lPSJTbGljZXJfU8OkbGphcmUiPiNOL0E8L2RlZmluZWROYW1lPg0KICAgIDxkZWZpbmVkTmFtZSBuYW1lPSJTbGljZXJfU8OkbGphcmUxIj4jTi9BPC9kZWZpbmVkTmFtZT4NCiAgICA8ZGVmaW5lZE5hbWUgbmFtZT0iU2xpY2VyX1V0ZmFsbHN0eXAiPiNOL0E8L2RlZmluZWROYW1lPg0KICA8L2RlZmluZWROYW1lcz4NCiAgPGNhbGNQciBjYWxjSWQ9IjE1MjUxMSIgZnVsbENhbGNPbkxvYWQ9IjEiIC8+DQogIDxwaXZvdENhY2hlcz4NCiAgICA8cGl2b3RDYWNoZSBjYWNoZUlkPSI3IiByOmlkPSJySWQ3IiAvPg0KICAgIDxwaXZvdENhY2hlIGNhY2hlSWQ9IjEyIiByOmlkPSJySWQ4IiAvPg0KICAgIDxwaXZvdENhY2hlIGNhY2hlSWQ9IjE5IiByOmlkPSJySWQ5IiAvPg0KICA8L3Bpdm90Q2FjaGVzPg0KICA8ZXh0THN0Pg0KICAgIDxleHQgdXJpPSJ7QkJFMUE5NTItQUExMy00NDhlLUFBREMtMTY0RjhBMjhBOTkxfSIgeG1sbnM6eDE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIj4NCiAgICAgIDx4MTQ6c2xpY2VyQ2FjaGVzPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDEwIiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDExIiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDEyIiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDEzIiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDE0IiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDE1IiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDE2IiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDE3IiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDE4IiAvPg0KICAgICAgICA8eDE0OnNsaWNlckNhY2hlIHI6aWQ9InJJZDE5IiAvPg0KICAgICAgPC94MTQ6c2xpY2VyQ2FjaGVzPg0KICAgIDwvZXh0Pg0KICAgIDxleHQgdXJpPSJ7NzlGNTQ5NzYtMURBNS00NjE4LUIxNDctNENERTRCOTUzQTM4fSIgeG1sbnM6eDE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIj4NCiAgICAgIDx4MTQ6d29ya2Jvb2tQciAvPg0KICAgIDwvZXh0Pg0KICAgIDxleHQgdXJpPSJ7MTQwQTcwOTQtMEUzNS00ODkyLTg0MzItQzREMkU1N0VERUI1fSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+DQogICAgICA8eDE1Ondvcmtib29rUHIgY2hhcnRUcmFja2luZ1JlZkJhc2U9IjEiIC8+DQogICAgPC9leHQ+DQogIDwvZXh0THN0Pg0KPC93b3JrYm9vaz5QSwMEFAAAAAAAAAAhAP////+iAQAAogEAABAAAABbdHJhc2hdLzAwMDEuZGF0/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBLAwQUAAAAAAAAACEAc/RBHLoVAAC6FQAAGAAAAHhsL3dvcmtzaGVldHMvc2hlZXQ0LnhtbO+7vzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz48d29ya3NoZWV0IHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIHhtbG5zOnI9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMiIHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgbWM6SWdub3JhYmxlPSJ4MTRhYyIgeG1sbnM6eDE0YWM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L2FjIj48ZGltZW5zaW9uIHJlZj0iQTE6VjQiIC8+PHNoZWV0Vmlld3M+PHNoZWV0VmlldyB0YWJTZWxlY3RlZD0iMSIgem9vbVNjYWxlTm9ybWFsPSIxMDAiIHdvcmtib29rVmlld0lkPSIwIj48cGFuZSB5U3BsaXQ9IjEiIHRvcExlZnRDZWxsPSJBMiIgYWN0aXZlUGFuZT0iYm90dG9tTGVmdCIgc3RhdGU9ImZyb3plbiIgLz48c2VsZWN0aW9uIHBhbmU9ImJvdHRvbUxlZnQiIGFjdGl2ZUNlbGw9IkEyIiBzcXJlZj0iQTIiIC8+PC9zaGVldFZpZXc+PC9zaGVldFZpZXdzPjxzaGVldEZvcm1hdFByIGRlZmF1bHRSb3dIZWlnaHQ9IjEyLjc1IiB4MTRhYzpkeURlc2NlbnQ9IjAuMiIgLz48Y29scz48Y29sIG1pbj0iMSIgbWF4PSIxIiB3aWR0aD0iMTYuMTQwNjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjIiIG1heD0iMiIgd2lkdGg9IjM1LjI4NTE1NjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjMiIG1heD0iMyIgYmVzdEZpdD0iMSIgd2lkdGg9IjExLjcxMDkzNzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iNCIgbWF4PSI0IiBiZXN0Rml0PSIxIiB3aWR0aD0iMTAuODU1NDY4NzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iNSIgbWF4PSI1IiBiZXN0Rml0PSIxIiB3aWR0aD0iMTIuMTQwNjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjYiIG1heD0iNiIgYmVzdEZpdD0iMSIgd2lkdGg9IjUuNzEwOTM3NSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSI3IiBtYXg9IjciIGJlc3RGaXQ9IjEiIHdpZHRoPSI5Ljg1NTQ2ODc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjgiIG1heD0iOCIgYmVzdEZpdD0iMSIgd2lkdGg9IjUuNzEwOTM3NSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSI5IiBtYXg9IjkiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxNC4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTAiIG1heD0iMTAiIGJlc3RGaXQ9IjEiIHdpZHRoPSI1LjcxMDkzNzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTEiIG1heD0iMTEiIGJlc3RGaXQ9IjEiIHdpZHRoPSI4IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjEyIiBtYXg9IjEyIiBiZXN0Rml0PSIxIiB3aWR0aD0iNS43MTA5Mzc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjEzIiBtYXg9IjEzIiBiZXN0Rml0PSIxIiB3aWR0aD0iMTEuODU1NDY4NzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTQiIG1heD0iMTQiIGJlc3RGaXQ9IjEiIHdpZHRoPSI1LjcxMDkzNzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTUiIG1heD0iMTUiIGJlc3RGaXQ9IjEiIHdpZHRoPSIyMS43MTA5Mzc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjE2IiBtYXg9IjE2IiBiZXN0Rml0PSIxIiB3aWR0aD0iNyIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxNyIgbWF4PSIxNyIgYmVzdEZpdD0iMSIgd2lkdGg9IjE3LjQyNTc4MTI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjE4IiBtYXg9IjE4IiBiZXN0Rml0PSIxIiB3aWR0aD0iNC40MjU3ODEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxOSIgbWF4PSIxOSIgYmVzdEZpdD0iMSIgd2lkdGg9IjE4Ljg1NTQ2ODc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjIwIiBtYXg9IjIwIiBiZXN0Rml0PSIxIiB3aWR0aD0iNC40MjU3ODEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMSIgbWF4PSIyMSIgYmVzdEZpdD0iMSIgd2lkdGg9IjIxLjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMiIgbWF4PSIyMiIgYmVzdEZpdD0iMSIgd2lkdGg9IjQuNDI1NzgxMjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMjMiIG1heD0iMTYzODQiIHdpZHRoPSI5LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjwvY29scz48c2hlZXREYXRhPjxyb3cgcj0iMSIgPjxjIHI9IkExIiBzPSIxMyIgLz48YyByPSJCMSIgcz0iMTMiIC8+PGMgcj0iQzEiIHM9IjUiIHQ9InMiPjx2PjA8L3Y+PC9jPjxjIHI9IkQxIiBzPSI2IiB0PSJzIj48dj4xPC92PjwvYz48YyByPSJFMSIgcz0iNiIgdD0icyI+PHY+Mjwvdj48L2M+PGMgcj0iRjEiIHM9IjYiIHQ9InMiPjx2PjM8L3Y+PC9jPjxjIHI9IkcxIiBzPSI2IiB0PSJzIj48dj40PC92PjwvYz48YyByPSJIMSIgcz0iNiIgdD0icyI+PHY+NTwvdj48L2M+PGMgcj0iSTEiIHM9IjYiIHQ9InMiPjx2PjY8L3Y+PC9jPjxjIHI9IkoxIiBzPSI2IiB0PSJzIj48dj43PC92PjwvYz48YyByPSJLMSIgcz0iNiIgdD0icyI+PHY+ODwvdj48L2M+PGMgcj0iTDEiIHM9IjYiIHQ9InMiPjx2Pjk8L3Y+PC9jPjxjIHI9Ik0xIiBzPSI2IiB0PSJzIj48dj4xMDwvdj48L2M+PGMgcj0iTjEiIHM9IjYiIHQ9InMiPjx2PjExPC92PjwvYz48YyByPSJPMSIgcz0iNiIgdD0icyI+PHY+MTI8L3Y+PC9jPjxjIHI9IlAxIiBzPSI2IiB0PSJzIj48dj4xMzwvdj48L2M+PGMgcj0iUTEiIHM9IjYiIHQ9InMiPjx2PjE0PC92PjwvYz48YyByPSJSMSIgcz0iNiIgdD0icyI+PHY+MTU8L3Y+PC9jPjxjIHI9IlMxIiBzPSI2IiB0PSJzIj48dj4xNjwvdj48L2M+PGMgcj0iVDEiIHM9IjYiIHQ9InMiPjx2PjE3PC92PjwvYz48YyByPSJVMSIgcz0iNiIgdD0icyI+PHY+MTg8L3Y+PC9jPjxjIHI9IlYxIiBzPSI2IiB0PSJzIj48dj4xOTwvdj48L2M+PC9yb3c+PHJvdyByPSIyIiA+PGMgcj0iQzIiIHM9IjciIHQ9InMiPjx2PjIwPC92PjwvYz48YyByPSJEMiIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkUyIiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iRjIiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJHMiIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkgyIiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iSTIiIHM9IjgiID48dj4wPC92PjwvYz48YyByPSJKMiIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9IksyIiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iTDIiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJNMiIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik4yIiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iTzIiIHM9IjgiIHQ9InMiPjx2PjIxPC92PjwvYz48YyByPSJQMiIgcz0iOCIgdD0icyI+PHY+MjE8L3Y+PC9jPjxjIHI9IlEyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IlIyIiBzPSIxMSIgPjx2PjA8L3Y+PC9jPjxjIHI9IlMyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IlQyIiBzPSIxMSIgPjx2PjA8L3Y+PC9jPjxjIHI9IlUyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IlYyIiBzPSIxMSIgPjx2PjA8L3Y+PC9jPjwvcm93Pjxyb3cgcj0iMyIgPjxjIHI9IkMzIiBzPSIxMiIgdD0icyI+PHY+MjA8L3Y+PC9jPjxjIHI9IkQzIiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iRTMiIHM9IjgiID48dj4wPC92PjwvYz48YyByPSJGMyIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9IkczIiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iSDMiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJJMyIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkozIiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iSzMiIHM9IjgiID48dj4wPC92PjwvYz48YyByPSJMMyIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik0zIiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iTjMiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJPMyIgcz0iOCIgdD0icyI+PHY+MjE8L3Y+PC9jPjxjIHI9IlAzIiBzPSI4IiB0PSJzIj48dj4yMTwvdj48L2M+PGMgcj0iUTMiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iUjMiIHM9IjExIiA+PHY+MDwvdj48L2M+PGMgcj0iUzMiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iVDMiIHM9IjExIiA+PHY+MDwvdj48L2M+PGMgcj0iVTMiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iVjMiIHM9IjExIiA+PHY+MDwvdj48L2M+PC9yb3c+PHJvdyByPSI0IiA+PGMgcj0iQzQiIHM9IjciIHQ9InMiPjx2PjIyPC92PjwvYz48YyByPSJENCIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkU0IiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iRjQiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJHNCIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ikg0IiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iSTQiIHM9IjgiID48dj4wPC92PjwvYz48YyByPSJKNCIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9Iks0IiBzPSI4IiA+PHY+MDwvdj48L2M+PGMgcj0iTDQiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJNNCIgcz0iOCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik40IiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iTzQiIHM9IjgiIHQ9InMiPjx2PjIxPC92PjwvYz48YyByPSJQNCIgcz0iOCIgdD0icyI+PHY+MjE8L3Y+PC9jPjxjIHI9IlE0IiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IlI0IiBzPSIxMSIgPjx2PjA8L3Y+PC9jPjxjIHI9IlM0IiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IlQ0IiBzPSIxMSIgPjx2PjA8L3Y+PC9jPjxjIHI9IlU0IiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IlY0IiBzPSIxMSIgPjx2PjA8L3Y+PC9jPjwvcm93Pjwvc2hlZXREYXRhPjxwYWdlTWFyZ2lucyBsZWZ0PSIwLjciIHJpZ2h0PSIwLjciIHRvcD0iMC43NSIgYm90dG9tPSIwLjc1IiBoZWFkZXI9IjAuMyIgZm9vdGVyPSIwLjMiIC8+PHBhZ2VTZXR1cCBwYXBlclNpemU9IjkiIG9yaWVudGF0aW9uPSJwb3J0cmFpdCIgLz48aGVhZGVyRm9vdGVyIC8+PGRyYXdpbmcgcjppZD0icklkMyIgLz48ZXh0THN0PjxleHQgdXJpPSJ7QTg3NjVCQTktNDU2QS00ZGFiLUI0RjMtQUNGODM4QzEyMURFfSIgeG1sbnM6eDE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIj48eDE0OnNsaWNlckxpc3Q+PHgxNDpzbGljZXIgcjppZD0icklkNCIgLz48L3gxNDpzbGljZXJMaXN0PjwvZXh0PjwvZXh0THN0Pjwvd29ya3NoZWV0PlBLAwQUAAAAAAAAACEAXJZaLz4CAAA+AgAAIwAAAHhsL3dvcmtzaGVldHMvX3JlbHMvc2hlZXQ0LnhtbC5yZWxzPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxSZWxhdGlvbnNoaXBzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L3JlbGF0aW9uc2hpcHMiPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDMiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvZHJhd2luZyIgVGFyZ2V0PSIuLi9kcmF3aW5ncy9kcmF3aW5nMS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3Bpdm90VGFibGUiIFRhcmdldD0iLi4vcGl2b3RUYWJsZXMvcGl2b3RUYWJsZTEueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkNCIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlciIgVGFyZ2V0PSIuLi9zbGljZXJzL3NsaWNlcjEueG1sIi8+PC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAAAAACEA2NUKdlsrAABbKwAAGAAAAHhsL2RyYXdpbmdzL2RyYXdpbmczLnhtbO+7vzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjx4ZHI6d3NEciB4bWxuczp4ZHI9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9kcmF3aW5nbWwvMjAwNi9zcHJlYWRzaGVldERyYXdpbmciIHhtbG5zOmE9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9kcmF3aW5nbWwvMjAwNi9tYWluIj4NCiAgPHhkcjp0d29DZWxsQW5jaG9yIGVkaXRBcz0iYWJzb2x1dGUiPg0KICAgIDx4ZHI6ZnJvbT4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj40NjMwPC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+MjU8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj40NTE1MzwveGRyOnJvd09mZj4NCiAgICA8L3hkcjpmcm9tPg0KICAgIDx4ZHI6dG8+DQogICAgICA8eGRyOmNvbD4xPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+MjM0ODIzMDwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjMzPC94ZHI6cm93Pg0KICAgICAgPHhkcjpyb3dPZmY+MjAwNzwveGRyOnJvd09mZj4NCiAgICA8L3hkcjp0bz4NCiAgICA8bWM6QWx0ZXJuYXRlQ29udGVudCB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIHhtbG5zOmExND0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL21haW4iPg0KICAgICAgPG1jOkNob2ljZSBSZXF1aXJlcz0iYTE0Ij4NCiAgICAgICAgPHhkcjpncmFwaGljRnJhbWUgbWFjcm89IiI+DQogICAgICAgICAgPHhkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMyIgbmFtZT0iVXRmYWxsc3R5cCIgLz4NCiAgICAgICAgICAgIDx4ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICAgIDxhOmdyYXBoaWNGcmFtZUxvY2tzIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8L3hkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDx4ZHI6eGZybT4NCiAgICAgICAgICAgIDxhOm9mZiB4PSIwIiB5PSIwIiAvPg0KICAgICAgICAgICAgPGE6ZXh0IGN4PSIwIiBjeT0iMCIgLz4NCiAgICAgICAgICA8L3hkcjp4ZnJtPg0KICAgICAgICAgIDxhOmdyYXBoaWM+DQogICAgICAgICAgICA8YTpncmFwaGljRGF0YSB1cmk9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiPg0KICAgICAgICAgICAgICA8c2xlOnNsaWNlciB4bWxuczpzbGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiIG5hbWU9IlV0ZmFsbHN0eXAiIC8+DQogICAgICAgICAgICA8L2E6Z3JhcGhpY0RhdGE+DQogICAgICAgICAgPC9hOmdyYXBoaWM+DQogICAgICAgIDwveGRyOmdyYXBoaWNGcmFtZT4NCiAgICAgIDwvbWM6Q2hvaWNlPg0KICAgICAgPG1jOkZhbGxiYWNrIHhtbG5zPSIiPg0KICAgICAgICA8eGRyOnNwIG1hY3JvPSIiIHRleHRsaW5rPSIiPg0KICAgICAgICAgIDx4ZHI6bnZTcFByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMCIgbmFtZT0iIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZTcFByPg0KICAgICAgICAgICAgICA8YTpzcExvY2tzIG5vVGV4dEVkaXQ9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZTcFByPg0KICAgICAgICAgIDwveGRyOm52U3BQcj4NCiAgICAgICAgICA8eGRyOnNwUHI+DQogICAgICAgICAgICA8YTp4ZnJtPg0KICAgICAgICAgICAgICA8YTpvZmYgeD0iMTA4MDk1NSIgeT0iMzkzMTM1MyIgLz4NCiAgICAgICAgICAgICAgPGE6ZXh0IGN4PSIyMzQzNjAwIiBjeT0iMTI1MjI1NCIgLz4NCiAgICAgICAgICAgIDwvYTp4ZnJtPg0KICAgICAgICAgICAgPGE6cHJzdEdlb20gcHJzdD0icmVjdCI+DQogICAgICAgICAgICAgIDxhOmF2THN0IC8+DQogICAgICAgICAgICA8L2E6cHJzdEdlb20+DQogICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgIDxhOnByc3RDbHIgdmFsPSJ3aGl0ZSIgLz4NCiAgICAgICAgICAgIDwvYTpzb2xpZEZpbGw+DQogICAgICAgICAgICA8YTpsbiB3PSIxIj4NCiAgICAgICAgICAgICAgPGE6c29saWRGaWxsPg0KICAgICAgICAgICAgICAgIDxhOnByc3RDbHIgdmFsPSJncmVlbiIgLz4NCiAgICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDwvYTpsbj4NCiAgICAgICAgICA8L3hkcjpzcFByPg0KICAgICAgICAgIDx4ZHI6dHhCb2R5Pg0KICAgICAgICAgICAgPGE6Ym9keVByIHZlcnRPdmVyZmxvdz0iY2xpcCIgaG9yek92ZXJmbG93PSJjbGlwIiAvPg0KICAgICAgICAgICAgPGE6bHN0U3R5bGUgLz4NCiAgICAgICAgICAgIDxhOnA+DQogICAgICAgICAgICAgIDxhOnI+DQogICAgICAgICAgICAgICAgPGE6clByIGxhbmc9InN2LVNFIiBzej0iMTEwMCIgLz4NCiAgICAgICAgICAgICAgICA8YTp0PlRoaXMgc2hhcGUgcmVwcmVzZW50cyBhIHNsaWNlci4gU2xpY2VycyBhcmUgc3VwcG9ydGVkIGluIEV4Y2VsIDIwMTAgb3IgbGF0ZXIuCgpJZiB0aGUgc2hhcGUgd2FzIG1vZGlmaWVkIGluIGFuIGVhcmxpZXIgdmVyc2lvbiBvZiBFeGNlbCwgb3IgaWYgdGhlIHdvcmtib29rIHdhcyBzYXZlZCBpbiBFeGNlbCAyMDAzIG9yIGVhcmxpZXIsIHRoZSBzbGljZXIgY2Fubm90IGJlIHVzZWQuPC9hOnQ+DQogICAgICAgICAgICAgIDwvYTpyPg0KICAgICAgICAgICAgPC9hOnA+DQogICAgICAgICAgPC94ZHI6dHhCb2R5Pg0KICAgICAgICA8L3hkcjpzcD4NCiAgICAgIDwvbWM6RmFsbGJhY2s+DQogICAgPC9tYzpBbHRlcm5hdGVDb250ZW50Pg0KICAgIDx4ZHI6Y2xpZW50RGF0YSAvPg0KICA8L3hkcjp0d29DZWxsQW5jaG9yPg0KICA8eGRyOnR3b0NlbGxBbmNob3IgZWRpdEFzPSJhYnNvbHV0ZSI+DQogICAgPHhkcjpmcm9tPg0KICAgICAgPHhkcjpjb2w+MTwveGRyOmNvbD4NCiAgICAgIDx4ZHI6Y29sT2ZmPjg0OTE8L3hkcjpjb2xPZmY+DQogICAgICA8eGRyOnJvdz4yPC94ZHI6cm93Pg0KICAgICAgPHhkcjpyb3dPZmY+NDMzMDwveGRyOnJvd09mZj4NCiAgICA8L3hkcjpmcm9tPg0KICAgIDx4ZHI6dG8+DQogICAgICA8eGRyOmNvbD4xPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+MjM0ODQ5MTwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjI1PC94ZHI6cm93Pg0KICAgICAgPHhkcjpyb3dPZmY+NDE5ODA8L3hkcjpyb3dPZmY+DQogICAgPC94ZHI6dG8+DQogICAgPG1jOkFsdGVybmF0ZUNvbnRlbnQgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiB4bWxuczphMTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9tYWluIj4NCiAgICAgIDxtYzpDaG9pY2UgUmVxdWlyZXM9ImExNCI+DQogICAgICAgIDx4ZHI6Z3JhcGhpY0ZyYW1lIG1hY3JvPSIiPg0KICAgICAgICAgIDx4ZHI6bnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICAgIDx4ZHI6Y052UHIgaWQ9IjQiIG5hbWU9IlByb2pla3QgMiIgLz4NCiAgICAgICAgICAgIDx4ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICAgIDxhOmdyYXBoaWNGcmFtZUxvY2tzIG5vQ2hhbmdlQXNwZWN0PSIxIiAvPg0KICAgICAgICAgICAgPC94ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgPC94ZHI6bnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8eGRyOnhmcm0+DQogICAgICAgICAgICA8YTpvZmYgeD0iMCIgeT0iMCIgLz4NCiAgICAgICAgICAgIDxhOmV4dCBjeD0iMCIgY3k9IjAiIC8+DQogICAgICAgICAgPC94ZHI6eGZybT4NCiAgICAgICAgICA8YTpncmFwaGljPg0KICAgICAgICAgICAgPGE6Z3JhcGhpY0RhdGEgdXJpPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvc2xpY2VyIj4NCiAgICAgICAgICAgICAgPHNsZTpzbGljZXIgeG1sbnM6c2xlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvc2xpY2VyIiBuYW1lPSJQcm9qZWt0IDIiIC8+DQogICAgICAgICAgICA8L2E6Z3JhcGhpY0RhdGE+DQogICAgICAgICAgPC9hOmdyYXBoaWM+DQogICAgICAgIDwveGRyOmdyYXBoaWNGcmFtZT4NCiAgICAgIDwvbWM6Q2hvaWNlPg0KICAgICAgPG1jOkZhbGxiYWNrIHhtbG5zPSIiPg0KICAgICAgICA8eGRyOnNwIG1hY3JvPSIiIHRleHRsaW5rPSIiPg0KICAgICAgICAgIDx4ZHI6bnZTcFByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMCIgbmFtZT0iIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZTcFByPg0KICAgICAgICAgICAgICA8YTpzcExvY2tzIG5vVGV4dEVkaXQ9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZTcFByPg0KICAgICAgICAgIDwveGRyOm52U3BQcj4NCiAgICAgICAgICA8eGRyOnNwUHI+DQogICAgICAgICAgICA8YTp4ZnJtPg0KICAgICAgICAgICAgICA8YTpvZmYgeD0iMTA4NDgxNiIgeT0iMTY2MjU1IiAvPg0KICAgICAgICAgICAgICA8YTpleHQgY3g9IjIzNDAwMDAiIGN5PSIzNzYxOTI1IiAvPg0KICAgICAgICAgICAgPC9hOnhmcm0+DQogICAgICAgICAgICA8YTpwcnN0R2VvbSBwcnN0PSJyZWN0Ij4NCiAgICAgICAgICAgICAgPGE6YXZMc3QgLz4NCiAgICAgICAgICAgIDwvYTpwcnN0R2VvbT4NCiAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9IndoaXRlIiAvPg0KICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDxhOmxuIHc9IjEiPg0KICAgICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9ImdyZWVuIiAvPg0KICAgICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPC9hOmxuPg0KICAgICAgICAgIDwveGRyOnNwUHI+DQogICAgICAgICAgPHhkcjp0eEJvZHk+DQogICAgICAgICAgICA8YTpib2R5UHIgdmVydE92ZXJmbG93PSJjbGlwIiBob3J6T3ZlcmZsb3c9ImNsaXAiIC8+DQogICAgICAgICAgICA8YTpsc3RTdHlsZSAvPg0KICAgICAgICAgICAgPGE6cD4NCiAgICAgICAgICAgICAgPGE6cj4NCiAgICAgICAgICAgICAgICA8YTpyUHIgbGFuZz0ic3YtU0UiIHN6PSIxMTAwIiAvPg0KICAgICAgICAgICAgICAgIDxhOnQ+VGhpcyBzaGFwZSByZXByZXNlbnRzIGEgc2xpY2VyLiBTbGljZXJzIGFyZSBzdXBwb3J0ZWQgaW4gRXhjZWwgMjAxMCBvciBsYXRlci4KCklmIHRoZSBzaGFwZSB3YXMgbW9kaWZpZWQgaW4gYW4gZWFybGllciB2ZXJzaW9uIG9mIEV4Y2VsLCBvciBpZiB0aGUgd29ya2Jvb2sgd2FzIHNhdmVkIGluIEV4Y2VsIDIwMDMgb3IgZWFybGllciwgdGhlIHNsaWNlciBjYW5ub3QgYmUgdXNlZC48L2E6dD4NCiAgICAgICAgICAgICAgPC9hOnI+DQogICAgICAgICAgICA8L2E6cD4NCiAgICAgICAgICA8L3hkcjp0eEJvZHk+DQogICAgICAgIDwveGRyOnNwPg0KICAgICAgPC9tYzpGYWxsYmFjaz4NCiAgICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogICAgPHhkcjpjbGllbnREYXRhIC8+DQogIDwveGRyOnR3b0NlbGxBbmNob3I+DQogIDx4ZHI6dHdvQ2VsbEFuY2hvciBlZGl0QXM9ImFic29sdXRlIj4NCiAgICA8eGRyOmZyb20+DQogICAgICA8eGRyOmNvbD4xPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+ODQ5MjwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjMzPC94ZHI6cm93Pg0KICAgICAgPHhkcjpyb3dPZmY+NzkxNzwveGRyOnJvd09mZj4NCiAgICA8L3hkcjpmcm9tPg0KICAgIDx4ZHI6dG8+DQogICAgICA8eGRyOmNvbD4xPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+MjM0ODQ5MjwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjYwPC94ZHI6cm93Pg0KICAgICAgPHhkcjpyb3dPZmY+MTUwMzAxPC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOnRvPg0KICAgIDxtYzpBbHRlcm5hdGVDb250ZW50IHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgeG1sbnM6YTE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvbWFpbiI+DQogICAgICA8bWM6Q2hvaWNlIFJlcXVpcmVzPSJhMTQiPg0KICAgICAgICA8eGRyOmdyYXBoaWNGcmFtZSBtYWNybz0iIj4NCiAgICAgICAgICA8eGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSI1IiBuYW1lPSJBbnbDpG5kYXJlIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICAgICAgPGE6Z3JhcGhpY0ZyYW1lTG9ja3MgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDwveGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgPHhkcjp4ZnJtPg0KICAgICAgICAgICAgPGE6b2ZmIHg9IjAiIHk9IjAiIC8+DQogICAgICAgICAgICA8YTpleHQgY3g9IjAiIGN5PSIwIiAvPg0KICAgICAgICAgIDwveGRyOnhmcm0+DQogICAgICAgICAgPGE6Z3JhcGhpYz4NCiAgICAgICAgICAgIDxhOmdyYXBoaWNEYXRhIHVyaT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL3NsaWNlciI+DQogICAgICAgICAgICAgIDxzbGU6c2xpY2VyIHhtbG5zOnNsZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL3NsaWNlciIgbmFtZT0iQW52w6RuZGFyZSIgLz4NCiAgICAgICAgICAgIDwvYTpncmFwaGljRGF0YT4NCiAgICAgICAgICA8L2E6Z3JhcGhpYz4NCiAgICAgICAgPC94ZHI6Z3JhcGhpY0ZyYW1lPg0KICAgICAgPC9tYzpDaG9pY2U+DQogICAgICA8bWM6RmFsbGJhY2sgeG1sbnM9IiI+DQogICAgICAgIDx4ZHI6c3AgbWFjcm89IiIgdGV4dGxpbms9IiI+DQogICAgICAgICAgPHhkcjpudlNwUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSIwIiBuYW1lPSIiIC8+DQogICAgICAgICAgICA8eGRyOmNOdlNwUHI+DQogICAgICAgICAgICAgIDxhOnNwTG9ja3Mgbm9UZXh0RWRpdD0iMSIgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdlNwUHI+DQogICAgICAgICAgPC94ZHI6bnZTcFByPg0KICAgICAgICAgIDx4ZHI6c3BQcj4NCiAgICAgICAgICAgIDxhOnhmcm0+DQogICAgICAgICAgICAgIDxhOm9mZiB4PSIxMDg0ODE3IiB5PSI1MTg5NTE3IiAvPg0KICAgICAgICAgICAgICA8YTpleHQgY3g9IjIzNDAwMDAiIGN5PSI0NTE0MzU5IiAvPg0KICAgICAgICAgICAgPC9hOnhmcm0+DQogICAgICAgICAgICA8YTpwcnN0R2VvbSBwcnN0PSJyZWN0Ij4NCiAgICAgICAgICAgICAgPGE6YXZMc3QgLz4NCiAgICAgICAgICAgIDwvYTpwcnN0R2VvbT4NCiAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9IndoaXRlIiAvPg0KICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDxhOmxuIHc9IjEiPg0KICAgICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9ImdyZWVuIiAvPg0KICAgICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPC9hOmxuPg0KICAgICAgICAgIDwveGRyOnNwUHI+DQogICAgICAgICAgPHhkcjp0eEJvZHk+DQogICAgICAgICAgICA8YTpib2R5UHIgdmVydE92ZXJmbG93PSJjbGlwIiBob3J6T3ZlcmZsb3c9ImNsaXAiIC8+DQogICAgICAgICAgICA8YTpsc3RTdHlsZSAvPg0KICAgICAgICAgICAgPGE6cD4NCiAgICAgICAgICAgICAgPGE6cj4NCiAgICAgICAgICAgICAgICA8YTpyUHIgbGFuZz0ic3YtU0UiIHN6PSIxMTAwIiAvPg0KICAgICAgICAgICAgICAgIDxhOnQ+VGhpcyBzaGFwZSByZXByZXNlbnRzIGEgc2xpY2VyLiBTbGljZXJzIGFyZSBzdXBwb3J0ZWQgaW4gRXhjZWwgMjAxMCBvciBsYXRlci4KCklmIHRoZSBzaGFwZSB3YXMgbW9kaWZpZWQgaW4gYW4gZWFybGllciB2ZXJzaW9uIG9mIEV4Y2VsLCBvciBpZiB0aGUgd29ya2Jvb2sgd2FzIHNhdmVkIGluIEV4Y2VsIDIwMDMgb3IgZWFybGllciwgdGhlIHNsaWNlciBjYW5ub3QgYmUgdXNlZC48L2E6dD4NCiAgICAgICAgICAgICAgPC9hOnI+DQogICAgICAgICAgICA8L2E6cD4NCiAgICAgICAgICA8L3hkcjp0eEJvZHk+DQogICAgICAgIDwveGRyOnNwPg0KICAgICAgPC9tYzpGYWxsYmFjaz4NCiAgICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogICAgPHhkcjpjbGllbnREYXRhIC8+DQogIDwveGRyOnR3b0NlbGxBbmNob3I+DQogIDx4ZHI6dHdvQ2VsbEFuY2hvciBlZGl0QXM9ImFic29sdXRlIj4NCiAgICA8eGRyOmZyb20+DQogICAgICA8eGRyOmNvbD4wPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+MTwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjI8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj44NjU5PC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOmZyb20+DQogICAgPHhkcjp0bz4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj4yMzE1PC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+NjA8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj4xNDk3MzQ8L3hkcjpyb3dPZmY+DQogICAgPC94ZHI6dG8+DQogICAgPG1jOkFsdGVybmF0ZUNvbnRlbnQgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiB4bWxuczphMTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9tYWluIj4NCiAgICAgIDxtYzpDaG9pY2UgUmVxdWlyZXM9ImExNCI+DQogICAgICAgIDx4ZHI6Z3JhcGhpY0ZyYW1lIG1hY3JvPSIiPg0KICAgICAgICAgIDx4ZHI6bnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICAgIDx4ZHI6Y052UHIgaWQ9IjIiIG5hbWU9IkRhdHVtIDIiIC8+DQogICAgICAgICAgICA8eGRyOmNOdkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgICAgICA8YTpncmFwaGljRnJhbWVMb2NrcyBub0NoYW5nZUFzcGVjdD0iMSIgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDwveGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgPHhkcjp4ZnJtPg0KICAgICAgICAgICAgPGE6b2ZmIHg9IjAiIHk9IjAiIC8+DQogICAgICAgICAgICA8YTpleHQgY3g9IjAiIGN5PSIwIiAvPg0KICAgICAgICAgIDwveGRyOnhmcm0+DQogICAgICAgICAgPGE6Z3JhcGhpYz4NCiAgICAgICAgICAgIDxhOmdyYXBoaWNEYXRhIHVyaT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL3NsaWNlciI+DQogICAgICAgICAgICAgIDxzbGU6c2xpY2VyIHhtbG5zOnNsZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL3NsaWNlciIgbmFtZT0iRGF0dW0gMiIgLz4NCiAgICAgICAgICAgIDwvYTpncmFwaGljRGF0YT4NCiAgICAgICAgICA8L2E6Z3JhcGhpYz4NCiAgICAgICAgPC94ZHI6Z3JhcGhpY0ZyYW1lPg0KICAgICAgPC9tYzpDaG9pY2U+DQogICAgICA8bWM6RmFsbGJhY2sgeG1sbnM9IiI+DQogICAgICAgIDx4ZHI6c3AgbWFjcm89IiIgdGV4dGxpbms9IiI+DQogICAgICAgICAgPHhkcjpudlNwUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSIwIiBuYW1lPSIiIC8+DQogICAgICAgICAgICA8eGRyOmNOdlNwUHI+DQogICAgICAgICAgICAgIDxhOnNwTG9ja3Mgbm9UZXh0RWRpdD0iMSIgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdlNwUHI+DQogICAgICAgICAgPC94ZHI6bnZTcFByPg0KICAgICAgICAgIDx4ZHI6c3BQcj4NCiAgICAgICAgICAgIDxhOnhmcm0+DQogICAgICAgICAgICAgIDxhOm9mZiB4PSIxIiB5PSIxNzA1ODQiIC8+DQogICAgICAgICAgICAgIDxhOmV4dCBjeD0iMTA3ODYzOSIgY3k9Ijk1MzI3MjUiIC8+DQogICAgICAgICAgICA8L2E6eGZybT4NCiAgICAgICAgICAgIDxhOnByc3RHZW9tIHByc3Q9InJlY3QiPg0KICAgICAgICAgICAgICA8YTphdkxzdCAvPg0KICAgICAgICAgICAgPC9hOnByc3RHZW9tPg0KICAgICAgICAgICAgPGE6c29saWRGaWxsPg0KICAgICAgICAgICAgICA8YTpwcnN0Q2xyIHZhbD0id2hpdGUiIC8+DQogICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPGE6bG4gdz0iMSI+DQogICAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgICA8YTpwcnN0Q2xyIHZhbD0iZ3JlZW4iIC8+DQogICAgICAgICAgICAgIDwvYTpzb2xpZEZpbGw+DQogICAgICAgICAgICA8L2E6bG4+DQogICAgICAgICAgPC94ZHI6c3BQcj4NCiAgICAgICAgICA8eGRyOnR4Qm9keT4NCiAgICAgICAgICAgIDxhOmJvZHlQciB2ZXJ0T3ZlcmZsb3c9ImNsaXAiIGhvcnpPdmVyZmxvdz0iY2xpcCIgLz4NCiAgICAgICAgICAgIDxhOmxzdFN0eWxlIC8+DQogICAgICAgICAgICA8YTpwPg0KICAgICAgICAgICAgICA8YTpyPg0KICAgICAgICAgICAgICAgIDxhOnJQciBsYW5nPSJzdi1TRSIgc3o9IjExMDAiIC8+DQogICAgICAgICAgICAgICAgPGE6dD5UaGlzIHNoYXBlIHJlcHJlc2VudHMgYSBzbGljZXIuIFNsaWNlcnMgYXJlIHN1cHBvcnRlZCBpbiBFeGNlbCAyMDEwIG9yIGxhdGVyLgoKSWYgdGhlIHNoYXBlIHdhcyBtb2RpZmllZCBpbiBhbiBlYXJsaWVyIHZlcnNpb24gb2YgRXhjZWwsIG9yIGlmIHRoZSB3b3JrYm9vayB3YXMgc2F2ZWQgaW4gRXhjZWwgMjAwMyBvciBlYXJsaWVyLCB0aGUgc2xpY2VyIGNhbm5vdCBiZSB1c2VkLjwvYTp0Pg0KICAgICAgICAgICAgICA8L2E6cj4NCiAgICAgICAgICAgIDwvYTpwPg0KICAgICAgICAgIDwveGRyOnR4Qm9keT4NCiAgICAgICAgPC94ZHI6c3A+DQogICAgICA8L21jOkZhbGxiYWNrPg0KICAgIDwvbWM6QWx0ZXJuYXRlQ29udGVudD4NCiAgICA8eGRyOmNsaWVudERhdGEgLz4NCiAgPC94ZHI6dHdvQ2VsbEFuY2hvcj4NCjwveGRyOndzRHI+UEsDBBQAAAAAAAAAIQAjmBoc4g4AAOIOAAAeAAAAeGwvcGl2b3RUYWJsZXMvcGl2b3RUYWJsZTMueG1s77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPHBpdm90VGFibGVEZWZpbml0aW9uIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIG5hbWU9IlBpdm90VGFibGU0IiBjYWNoZUlkPSIxOSIgYXBwbHlOdW1iZXJGb3JtYXRzPSIwIiBhcHBseUJvcmRlckZvcm1hdHM9IjAiIGFwcGx5Rm9udEZvcm1hdHM9IjAiIGFwcGx5UGF0dGVybkZvcm1hdHM9IjAiIGFwcGx5QWxpZ25tZW50Rm9ybWF0cz0iMCIgYXBwbHlXaWR0aEhlaWdodEZvcm1hdHM9IjEiIGRhdGFDYXB0aW9uPSJWYWx1ZXMiIGdyYW5kVG90YWxDYXB0aW9uPSJUb3RhbHQiIG1pc3NpbmdDYXB0aW9uPSIwIiB1cGRhdGVkVmVyc2lvbj0iNSIgbWluUmVmcmVzaGFibGVWZXJzaW9uPSIzIiB1c2VBdXRvRm9ybWF0dGluZz0iMSIgaXRlbVByaW50VGl0bGVzPSIxIiBjcmVhdGVkVmVyc2lvbj0iNSIgaW5kZW50PSIwIiBvdXRsaW5lPSIxIiBvdXRsaW5lRGF0YT0iMSIgbXVsdGlwbGVGaWVsZEZpbHRlcnM9IjAiIHJvd0hlYWRlckNhcHRpb249IkFudsOkbmRhcmUiIGNvbEhlYWRlckNhcHRpb249IlV0ZmFsbCI+DQogIDxsb2NhdGlvbiByZWY9IkMxOkU1IiBmaXJzdEhlYWRlclJvdz0iMSIgZmlyc3REYXRhUm93PSIyIiBmaXJzdERhdGFDb2w9IjEiIC8+DQogIDxwaXZvdEZpZWxkcyBjb3VudD0iNiI+DQogICAgPHBpdm90RmllbGQgbnVtRm10SWQ9IjE0IiBzaG93QWxsPSIwIiBkZWZhdWx0U3VidG90YWw9IjAiPg0KICAgICAgPGl0ZW1zIGNvdW50PSIxIj4NCiAgICAgICAgPGl0ZW0geD0iMCIgLz4NCiAgICAgIDwvaXRlbXM+DQogICAgPC9waXZvdEZpZWxkPg0KICAgIDxwaXZvdEZpZWxkIGF4aXM9ImF4aXNSb3ciIHNob3dBbGw9IjAiPg0KICAgICAgPGl0ZW1zIGNvdW50PSIyIj4NCiAgICAgICAgPGl0ZW0geD0iMCIgLz4NCiAgICAgICAgPGl0ZW0gdD0iZGVmYXVsdCIgLz4NCiAgICAgIDwvaXRlbXM+DQogICAgPC9waXZvdEZpZWxkPg0KICAgIDxwaXZvdEZpZWxkIGF4aXM9ImF4aXNSb3ciIHNob3dBbGw9IjAiPg0KICAgICAgPGl0ZW1zIGNvdW50PSIyIj4NCiAgICAgICAgPGl0ZW0geD0iMCIgLz4NCiAgICAgICAgPGl0ZW0gdD0iZGVmYXVsdCIgLz4NCiAgICAgIDwvaXRlbXM+DQogICAgPC9waXZvdEZpZWxkPg0KICAgIDxwaXZvdEZpZWxkIHNob3dBbGw9IjAiPg0KICAgICAgPGl0ZW1zIGNvdW50PSIyIj4NCiAgICAgICAgPGl0ZW0geD0iMCIgLz4NCiAgICAgICAgPGl0ZW0gdD0iZGVmYXVsdCIgLz4NCiAgICAgIDwvaXRlbXM+DQogICAgPC9waXZvdEZpZWxkPg0KICAgIDxwaXZvdEZpZWxkIGF4aXM9ImF4aXNDb2wiIHNob3dBbGw9IjAiPg0KICAgICAgPGl0ZW1zIGNvdW50PSIyIj4NCiAgICAgICAgPGl0ZW0geD0iMCIgLz4NCiAgICAgICAgPGl0ZW0gdD0iZGVmYXVsdCIgLz4NCiAgICAgIDwvaXRlbXM+DQogICAgPC9waXZvdEZpZWxkPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgbnVtRm10SWQ9IjEiIHNob3dBbGw9IjAiIC8+DQogIDwvcGl2b3RGaWVsZHM+DQogIDxyb3dGaWVsZHMgY291bnQ9IjIiPg0KICAgIDxmaWVsZCB4PSIxIiAvPg0KICAgIDxmaWVsZCB4PSIyIiAvPg0KICA8L3Jvd0ZpZWxkcz4NCiAgPHJvd0l0ZW1zIGNvdW50PSIzIj4NCiAgICA8aT4NCiAgICAgIDx4IC8+DQogICAgPC9pPg0KICAgIDxpIHI9IjEiPg0KICAgICAgPHggLz4NCiAgICA8L2k+DQogICAgPGkgdD0iZ3JhbmQiPg0KICAgICAgPHggLz4NCiAgICA8L2k+DQogIDwvcm93SXRlbXM+DQogIDxjb2xGaWVsZHMgY291bnQ9IjEiPg0KICAgIDxmaWVsZCB4PSI0IiAvPg0KICA8L2NvbEZpZWxkcz4NCiAgPGNvbEl0ZW1zIGNvdW50PSIyIj4NCiAgICA8aT4NCiAgICAgIDx4IC8+DQogICAgPC9pPg0KICAgIDxpIHQ9ImdyYW5kIj4NCiAgICAgIDx4IC8+DQogICAgPC9pPg0KICA8L2NvbEl0ZW1zPg0KICA8ZGF0YUZpZWxkcyBjb3VudD0iMSI+DQogICAgPGRhdGFGaWVsZCBuYW1lPSIgIiBmbGQ9IjUiIGJhc2VGaWVsZD0iMiIgYmFzZUl0ZW09IjIiIC8+DQogIDwvZGF0YUZpZWxkcz4NCiAgPGZvcm1hdHMgY291bnQ9IjciPg0KICAgIDxmb3JtYXQgZHhmSWQ9IjI3Ij4NCiAgICAgIDxwaXZvdEFyZWEgdHlwZT0iYWxsIiBkYXRhT25seT0iMCIgb3V0bGluZT0iMCIgZmllbGRQb3NpdGlvbj0iMCIgLz4NCiAgICA8L2Zvcm1hdD4NCiAgICA8Zm9ybWF0IGR4ZklkPSIyNiI+DQogICAgICA8cGl2b3RBcmVhIG91dGxpbmU9IjAiIGNvbGxhcHNlZExldmVsc0FyZVN1YnRvdGFscz0iMSIgZmllbGRQb3NpdGlvbj0iMCIgLz4NCiAgICA8L2Zvcm1hdD4NCiAgICA8Zm9ybWF0IGR4ZklkPSIyNSI+DQogICAgICA8cGl2b3RBcmVhIGRhdGFPbmx5PSIwIiBsYWJlbE9ubHk9IjEiIGZpZWxkUG9zaXRpb249IjAiPg0KICAgICAgICA8cmVmZXJlbmNlcyBjb3VudD0iMSI+DQogICAgICAgICAgPHJlZmVyZW5jZSBmaWVsZD0iMSIgY291bnQ9IjAiIC8+DQogICAgICAgIDwvcmVmZXJlbmNlcz4NCiAgICAgIDwvcGl2b3RBcmVhPg0KICAgIDwvZm9ybWF0Pg0KICAgIDxmb3JtYXQgZHhmSWQ9IjI0Ij4NCiAgICAgIDxwaXZvdEFyZWEgZGF0YU9ubHk9IjAiIGxhYmVsT25seT0iMSIgZ3JhbmRSb3c9IjEiIG91dGxpbmU9IjAiIGZpZWxkUG9zaXRpb249IjAiIC8+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMjMiPg0KICAgICAgPHBpdm90QXJlYSBkYXRhT25seT0iMCIgbGFiZWxPbmx5PSIxIiBmaWVsZFBvc2l0aW9uPSIwIj4NCiAgICAgICAgPHJlZmVyZW5jZXMgY291bnQ9IjIiPg0KICAgICAgICAgIDxyZWZlcmVuY2UgZmllbGQ9IjEiIGNvdW50PSIwIiBzZWxlY3RlZD0iMCIgLz4NCiAgICAgICAgICA8cmVmZXJlbmNlIGZpZWxkPSIyIiBjb3VudD0iMCIgLz4NCiAgICAgICAgPC9yZWZlcmVuY2VzPg0KICAgICAgPC9waXZvdEFyZWE+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMjIiPg0KICAgICAgPHBpdm90QXJlYSBkYXRhT25seT0iMCIgbGFiZWxPbmx5PSIxIiBmaWVsZFBvc2l0aW9uPSIwIj4NCiAgICAgICAgPHJlZmVyZW5jZXMgY291bnQ9IjEiPg0KICAgICAgICAgIDxyZWZlcmVuY2UgZmllbGQ9IjQiIGNvdW50PSIwIiAvPg0KICAgICAgICA8L3JlZmVyZW5jZXM+DQogICAgICA8L3Bpdm90QXJlYT4NCiAgICA8L2Zvcm1hdD4NCiAgICA8Zm9ybWF0IGR4ZklkPSIyMSI+DQogICAgICA8cGl2b3RBcmVhIGRhdGFPbmx5PSIwIiBsYWJlbE9ubHk9IjEiIGdyYW5kQ29sPSIxIiBvdXRsaW5lPSIwIiBmaWVsZFBvc2l0aW9uPSIwIiAvPg0KICAgIDwvZm9ybWF0Pg0KICA8L2Zvcm1hdHM+DQogIDxwaXZvdFRhYmxlU3R5bGVJbmZvIG5hbWU9IlBpdm90U3R5bGVNZWRpdW02IiBzaG93Um93SGVhZGVycz0iMSIgc2hvd0NvbEhlYWRlcnM9IjEiIHNob3dSb3dTdHJpcGVzPSIwIiBzaG93Q29sU3RyaXBlcz0iMCIgc2hvd0xhc3RDb2x1bW49IjEiIC8+DQogIDxleHRMc3Q+DQogICAgPGV4dCB1cmk9Ins5NjJFRjVEMS01Q0EyLTRjOTMtOEVGNC1EQkY1QzA1NDM5RDJ9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPg0KICAgICAgPHgxNDpwaXZvdFRhYmxlRGVmaW5pdGlvbiBoaWRlVmFsdWVzUm93PSIxIiB4bWxuczp4bT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZXhjZWwvMjAwNi9tYWluIiAvPg0KICAgIDwvZXh0Pg0KICA8L2V4dExzdD4NCjwvcGl2b3RUYWJsZURlZmluaXRpb24+UEsDBBQAAAAAAAAAIQBCVZsvRQEAAEUBAAApAAAAeGwvcGl2b3RUYWJsZXMvX3JlbHMvcGl2b3RUYWJsZTMueG1sLnJlbHM8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+PFJlbGF0aW9uc2hpcHMgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvcmVsYXRpb25zaGlwcyI+PFJlbGF0aW9uc2hpcCBJZD0icklkMSIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9waXZvdENhY2hlRGVmaW5pdGlvbiIgVGFyZ2V0PSIuLi9waXZvdENhY2hlL3Bpdm90Q2FjaGVEZWZpbml0aW9uMy54bWwiLz48L1JlbGF0aW9uc2hpcHM+UEsDBBQAAAAAAAAAIQBX71p+ewIAAHsCAAAWAAAAeGwvc2xpY2Vycy9zbGljZXIyLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxzbGljZXJzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIiB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIG1jOklnbm9yYWJsZT0ieCIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIj48c2xpY2VyIG5hbWU9IlByb2pla3QgMSIgY2FjaGU9IlNsaWNlcl9Qcm9qZWt0MSIgY2FwdGlvbj0iUHJvamVrdCIgc3R5bGU9IkRyZW1yZXBvcnRTbGljZXIiIHJvd0hlaWdodD0iMjQxMzAwIi8+PHNsaWNlciBuYW1lPSJTw6RsamFyZSAxIiBjYWNoZT0iU2xpY2VyX1PDpGxqYXJlMSIgY2FwdGlvbj0iU8OkbGphcmUiIHN0eWxlPSJEcmVtcmVwb3J0U2xpY2VyIiByb3dIZWlnaHQ9IjI0MTMwMCIvPjxzbGljZXIgbmFtZT0iRGF0dW0gMSIgY2FjaGU9IlNsaWNlcl9EYXR1bTEiIGNhcHRpb249IkRhdHVtIiBzdHlsZT0iRHJlbXJlcG9ydFNsaWNlciIgcm93SGVpZ2h0PSIyNDEzMDAiLz48L3NsaWNlcnM+UEsDBBQAAAAAAAAAIQDczso+yyAAAMsgAAAYAAAAeGwvZHJhd2luZ3MvZHJhd2luZzIueG1s77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPHhkcjp3c0RyIHhtbG5zOnhkcj0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL2RyYXdpbmdtbC8yMDA2L3NwcmVhZHNoZWV0RHJhd2luZyIgeG1sbnM6YT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL2RyYXdpbmdtbC8yMDA2L21haW4iPg0KICA8eGRyOnR3b0NlbGxBbmNob3IgZWRpdEFzPSJhYnNvbHV0ZSI+DQogICAgPHhkcjpmcm9tPg0KICAgICAgPHhkcjpjb2w+MTwveGRyOmNvbD4NCiAgICAgIDx4ZHI6Y29sT2ZmPjk4MTwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjE8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj4wPC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOmZyb20+DQogICAgPHhkcjp0bz4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj4yMzQ0NTgxPC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+MjM8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj4zNzY1MDwveGRyOnJvd09mZj4NCiAgICA8L3hkcjp0bz4NCiAgICA8bWM6QWx0ZXJuYXRlQ29udGVudCB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIHhtbG5zOmExND0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL21haW4iPg0KICAgICAgPG1jOkNob2ljZSBSZXF1aXJlcz0iYTE0Ij4NCiAgICAgICAgPHhkcjpncmFwaGljRnJhbWUgbWFjcm89IiI+DQogICAgICAgICAgPHhkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMiIgbmFtZT0iUHJvamVrdCAxIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICAgICAgPGE6Z3JhcGhpY0ZyYW1lTG9ja3Mgbm9DaGFuZ2VBc3BlY3Q9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8L3hkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDx4ZHI6eGZybT4NCiAgICAgICAgICAgIDxhOm9mZiB4PSIwIiB5PSIwIiAvPg0KICAgICAgICAgICAgPGE6ZXh0IGN4PSIwIiBjeT0iMCIgLz4NCiAgICAgICAgICA8L3hkcjp4ZnJtPg0KICAgICAgICAgIDxhOmdyYXBoaWM+DQogICAgICAgICAgICA8YTpncmFwaGljRGF0YSB1cmk9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiPg0KICAgICAgICAgICAgICA8c2xlOnNsaWNlciB4bWxuczpzbGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiIG5hbWU9IlByb2pla3QgMSIgLz4NCiAgICAgICAgICAgIDwvYTpncmFwaGljRGF0YT4NCiAgICAgICAgICA8L2E6Z3JhcGhpYz4NCiAgICAgICAgPC94ZHI6Z3JhcGhpY0ZyYW1lPg0KICAgICAgPC9tYzpDaG9pY2U+DQogICAgICA8bWM6RmFsbGJhY2sgeG1sbnM9IiI+DQogICAgICAgIDx4ZHI6c3AgbWFjcm89IiIgdGV4dGxpbms9IiI+DQogICAgICAgICAgPHhkcjpudlNwUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSIwIiBuYW1lPSIiIC8+DQogICAgICAgICAgICA8eGRyOmNOdlNwUHI+DQogICAgICAgICAgICAgIDxhOnNwTG9ja3Mgbm9UZXh0RWRpdD0iMSIgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdlNwUHI+DQogICAgICAgICAgPC94ZHI6bnZTcFByPg0KICAgICAgICAgIDx4ZHI6c3BQcj4NCiAgICAgICAgICAgIDxhOnhmcm0+DQogICAgICAgICAgICAgIDxhOm9mZiB4PSIxMDc3MzA2IiB5PSIxNjE5MjUiIC8+DQogICAgICAgICAgICAgIDxhOmV4dCBjeD0iMjM0MzYwMCIgY3k9IjM2MDAwMDAiIC8+DQogICAgICAgICAgICA8L2E6eGZybT4NCiAgICAgICAgICAgIDxhOnByc3RHZW9tIHByc3Q9InJlY3QiPg0KICAgICAgICAgICAgICA8YTphdkxzdCAvPg0KICAgICAgICAgICAgPC9hOnByc3RHZW9tPg0KICAgICAgICAgICAgPGE6c29saWRGaWxsPg0KICAgICAgICAgICAgICA8YTpwcnN0Q2xyIHZhbD0id2hpdGUiIC8+DQogICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPGE6bG4gdz0iMSI+DQogICAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgICA8YTpwcnN0Q2xyIHZhbD0iZ3JlZW4iIC8+DQogICAgICAgICAgICAgIDwvYTpzb2xpZEZpbGw+DQogICAgICAgICAgICA8L2E6bG4+DQogICAgICAgICAgPC94ZHI6c3BQcj4NCiAgICAgICAgICA8eGRyOnR4Qm9keT4NCiAgICAgICAgICAgIDxhOmJvZHlQciB2ZXJ0T3ZlcmZsb3c9ImNsaXAiIGhvcnpPdmVyZmxvdz0iY2xpcCIgLz4NCiAgICAgICAgICAgIDxhOmxzdFN0eWxlIC8+DQogICAgICAgICAgICA8YTpwPg0KICAgICAgICAgICAgICA8YTpyPg0KICAgICAgICAgICAgICAgIDxhOnJQciBsYW5nPSJzdi1TRSIgc3o9IjExMDAiIC8+DQogICAgICAgICAgICAgICAgPGE6dD5UaGlzIHNoYXBlIHJlcHJlc2VudHMgYSBzbGljZXIuIFNsaWNlcnMgYXJlIHN1cHBvcnRlZCBpbiBFeGNlbCAyMDEwIG9yIGxhdGVyLgoKSWYgdGhlIHNoYXBlIHdhcyBtb2RpZmllZCBpbiBhbiBlYXJsaWVyIHZlcnNpb24gb2YgRXhjZWwsIG9yIGlmIHRoZSB3b3JrYm9vayB3YXMgc2F2ZWQgaW4gRXhjZWwgMjAwMyBvciBlYXJsaWVyLCB0aGUgc2xpY2VyIGNhbm5vdCBiZSB1c2VkLjwvYTp0Pg0KICAgICAgICAgICAgICA8L2E6cj4NCiAgICAgICAgICAgIDwvYTpwPg0KICAgICAgICAgIDwveGRyOnR4Qm9keT4NCiAgICAgICAgPC94ZHI6c3A+DQogICAgICA8L21jOkZhbGxiYWNrPg0KICAgIDwvbWM6QWx0ZXJuYXRlQ29udGVudD4NCiAgICA8eGRyOmNsaWVudERhdGEgLz4NCiAgPC94ZHI6dHdvQ2VsbEFuY2hvcj4NCiAgPHhkcjp0d29DZWxsQW5jaG9yIGVkaXRBcz0iYWJzb2x1dGUiPg0KICAgIDx4ZHI6ZnJvbT4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj4yNTE8L3hkcjpjb2xPZmY+DQogICAgICA8eGRyOnJvdz4yMzwveGRyOnJvdz4NCiAgICAgIDx4ZHI6cm93T2ZmPjQyMTExPC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOmZyb20+DQogICAgPHhkcjp0bz4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj4yMzQzODUxPC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+NTY8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj45ODU4NjwveGRyOnJvd09mZj4NCiAgICA8L3hkcjp0bz4NCiAgICA8bWM6QWx0ZXJuYXRlQ29udGVudCB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIHhtbG5zOmExND0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL21haW4iPg0KICAgICAgPG1jOkNob2ljZSBSZXF1aXJlcz0iYTE0Ij4NCiAgICAgICAgPHhkcjpncmFwaGljRnJhbWUgbWFjcm89IiI+DQogICAgICAgICAgPHhkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMyIgbmFtZT0iU8OkbGphcmUgMSIgLz4NCiAgICAgICAgICAgIDx4ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICAgIDxhOmdyYXBoaWNGcmFtZUxvY2tzIG5vQ2hhbmdlQXNwZWN0PSIxIiAvPg0KICAgICAgICAgICAgPC94ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgPC94ZHI6bnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8eGRyOnhmcm0+DQogICAgICAgICAgICA8YTpvZmYgeD0iMCIgeT0iMCIgLz4NCiAgICAgICAgICAgIDxhOmV4dCBjeD0iMCIgY3k9IjAiIC8+DQogICAgICAgICAgPC94ZHI6eGZybT4NCiAgICAgICAgICA8YTpncmFwaGljPg0KICAgICAgICAgICAgPGE6Z3JhcGhpY0RhdGEgdXJpPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvc2xpY2VyIj4NCiAgICAgICAgICAgICAgPHNsZTpzbGljZXIgeG1sbnM6c2xlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvc2xpY2VyIiBuYW1lPSJTw6RsamFyZSAxIiAvPg0KICAgICAgICAgICAgPC9hOmdyYXBoaWNEYXRhPg0KICAgICAgICAgIDwvYTpncmFwaGljPg0KICAgICAgICA8L3hkcjpncmFwaGljRnJhbWU+DQogICAgICA8L21jOkNob2ljZT4NCiAgICAgIDxtYzpGYWxsYmFjayB4bWxucz0iIj4NCiAgICAgICAgPHhkcjpzcCBtYWNybz0iIiB0ZXh0bGluaz0iIj4NCiAgICAgICAgICA8eGRyOm52U3BQcj4NCiAgICAgICAgICAgIDx4ZHI6Y052UHIgaWQ9IjAiIG5hbWU9IiIgLz4NCiAgICAgICAgICAgIDx4ZHI6Y052U3BQcj4NCiAgICAgICAgICAgICAgPGE6c3BMb2NrcyBub1RleHRFZGl0PSIxIiAvPg0KICAgICAgICAgICAgPC94ZHI6Y052U3BQcj4NCiAgICAgICAgICA8L3hkcjpudlNwUHI+DQogICAgICAgICAgPHhkcjpzcFByPg0KICAgICAgICAgICAgPGE6eGZybT4NCiAgICAgICAgICAgICAgPGE6b2ZmIHg9IjEwNzY1NzYiIHk9IjM3NjYzODYiIC8+DQogICAgICAgICAgICAgIDxhOmV4dCBjeD0iMjM0MzYwMCIgY3k9IjU0MDAwMDAiIC8+DQogICAgICAgICAgICA8L2E6eGZybT4NCiAgICAgICAgICAgIDxhOnByc3RHZW9tIHByc3Q9InJlY3QiPg0KICAgICAgICAgICAgICA8YTphdkxzdCAvPg0KICAgICAgICAgICAgPC9hOnByc3RHZW9tPg0KICAgICAgICAgICAgPGE6c29saWRGaWxsPg0KICAgICAgICAgICAgICA8YTpwcnN0Q2xyIHZhbD0id2hpdGUiIC8+DQogICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPGE6bG4gdz0iMSI+DQogICAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgICA8YTpwcnN0Q2xyIHZhbD0iZ3JlZW4iIC8+DQogICAgICAgICAgICAgIDwvYTpzb2xpZEZpbGw+DQogICAgICAgICAgICA8L2E6bG4+DQogICAgICAgICAgPC94ZHI6c3BQcj4NCiAgICAgICAgICA8eGRyOnR4Qm9keT4NCiAgICAgICAgICAgIDxhOmJvZHlQciB2ZXJ0T3ZlcmZsb3c9ImNsaXAiIGhvcnpPdmVyZmxvdz0iY2xpcCIgLz4NCiAgICAgICAgICAgIDxhOmxzdFN0eWxlIC8+DQogICAgICAgICAgICA8YTpwPg0KICAgICAgICAgICAgICA8YTpyPg0KICAgICAgICAgICAgICAgIDxhOnJQciBsYW5nPSJzdi1TRSIgc3o9IjExMDAiIC8+DQogICAgICAgICAgICAgICAgPGE6dD5UaGlzIHNoYXBlIHJlcHJlc2VudHMgYSBzbGljZXIuIFNsaWNlcnMgYXJlIHN1cHBvcnRlZCBpbiBFeGNlbCAyMDEwIG9yIGxhdGVyLgoKSWYgdGhlIHNoYXBlIHdhcyBtb2RpZmllZCBpbiBhbiBlYXJsaWVyIHZlcnNpb24gb2YgRXhjZWwsIG9yIGlmIHRoZSB3b3JrYm9vayB3YXMgc2F2ZWQgaW4gRXhjZWwgMjAwMyBvciBlYXJsaWVyLCB0aGUgc2xpY2VyIGNhbm5vdCBiZSB1c2VkLjwvYTp0Pg0KICAgICAgICAgICAgICA8L2E6cj4NCiAgICAgICAgICAgIDwvYTpwPg0KICAgICAgICAgIDwveGRyOnR4Qm9keT4NCiAgICAgICAgPC94ZHI6c3A+DQogICAgICA8L21jOkZhbGxiYWNrPg0KICAgIDwvbWM6QWx0ZXJuYXRlQ29udGVudD4NCiAgICA8eGRyOmNsaWVudERhdGEgLz4NCiAgPC94ZHI6dHdvQ2VsbEFuY2hvcj4NCiAgPHhkcjp0d29DZWxsQW5jaG9yIGVkaXRBcz0iYWJzb2x1dGUiPg0KICAgIDx4ZHI6ZnJvbT4NCiAgICAgIDx4ZHI6Y29sPjA8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj4wPC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+MTwveGRyOnJvdz4NCiAgICAgIDx4ZHI6cm93T2ZmPjA8L3hkcjpyb3dPZmY+DQogICAgPC94ZHI6ZnJvbT4NCiAgICA8eGRyOnRvPg0KICAgICAgPHhkcjpjb2w+MTwveGRyOmNvbD4NCiAgICAgIDx4ZHI6Y29sT2ZmPjIxNzE8L3hkcjpjb2xPZmY+DQogICAgICA8eGRyOnJvdz41NjwveGRyOnJvdz4NCiAgICAgIDx4ZHI6cm93T2ZmPjk3NzI1PC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOnRvPg0KICAgIDxtYzpBbHRlcm5hdGVDb250ZW50IHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgeG1sbnM6YTE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvbWFpbiI+DQogICAgICA8bWM6Q2hvaWNlIFJlcXVpcmVzPSJhMTQiPg0KICAgICAgICA8eGRyOmdyYXBoaWNGcmFtZSBtYWNybz0iIj4NCiAgICAgICAgICA8eGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSI0IiBuYW1lPSJEYXR1bSAxIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICAgICAgPGE6Z3JhcGhpY0ZyYW1lTG9ja3Mgbm9DaGFuZ2VBc3BlY3Q9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8L3hkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDx4ZHI6eGZybT4NCiAgICAgICAgICAgIDxhOm9mZiB4PSIwIiB5PSIwIiAvPg0KICAgICAgICAgICAgPGE6ZXh0IGN4PSIwIiBjeT0iMCIgLz4NCiAgICAgICAgICA8L3hkcjp4ZnJtPg0KICAgICAgICAgIDxhOmdyYXBoaWM+DQogICAgICAgICAgICA8YTpncmFwaGljRGF0YSB1cmk9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiPg0KICAgICAgICAgICAgICA8c2xlOnNsaWNlciB4bWxuczpzbGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiIG5hbWU9IkRhdHVtIDEiIC8+DQogICAgICAgICAgICA8L2E6Z3JhcGhpY0RhdGE+DQogICAgICAgICAgPC9hOmdyYXBoaWM+DQogICAgICAgIDwveGRyOmdyYXBoaWNGcmFtZT4NCiAgICAgIDwvbWM6Q2hvaWNlPg0KICAgICAgPG1jOkZhbGxiYWNrIHhtbG5zPSIiPg0KICAgICAgICA8eGRyOnNwIG1hY3JvPSIiIHRleHRsaW5rPSIiPg0KICAgICAgICAgIDx4ZHI6bnZTcFByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMCIgbmFtZT0iIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZTcFByPg0KICAgICAgICAgICAgICA8YTpzcExvY2tzIG5vVGV4dEVkaXQ9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZTcFByPg0KICAgICAgICAgIDwveGRyOm52U3BQcj4NCiAgICAgICAgICA8eGRyOnNwUHI+DQogICAgICAgICAgICA8YTp4ZnJtPg0KICAgICAgICAgICAgICA8YTpvZmYgeD0iMCIgeT0iMTYxOTI1IiAvPg0KICAgICAgICAgICAgICA8YTpleHQgY3g9IjEwNzg0OTYiIGN5PSI5MDAzNjAwIiAvPg0KICAgICAgICAgICAgPC9hOnhmcm0+DQogICAgICAgICAgICA8YTpwcnN0R2VvbSBwcnN0PSJyZWN0Ij4NCiAgICAgICAgICAgICAgPGE6YXZMc3QgLz4NCiAgICAgICAgICAgIDwvYTpwcnN0R2VvbT4NCiAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9IndoaXRlIiAvPg0KICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDxhOmxuIHc9IjEiPg0KICAgICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9ImdyZWVuIiAvPg0KICAgICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPC9hOmxuPg0KICAgICAgICAgIDwveGRyOnNwUHI+DQogICAgICAgICAgPHhkcjp0eEJvZHk+DQogICAgICAgICAgICA8YTpib2R5UHIgdmVydE92ZXJmbG93PSJjbGlwIiBob3J6T3ZlcmZsb3c9ImNsaXAiIC8+DQogICAgICAgICAgICA8YTpsc3RTdHlsZSAvPg0KICAgICAgICAgICAgPGE6cD4NCiAgICAgICAgICAgICAgPGE6cj4NCiAgICAgICAgICAgICAgICA8YTpyUHIgbGFuZz0ic3YtU0UiIHN6PSIxMTAwIiAvPg0KICAgICAgICAgICAgICAgIDxhOnQ+VGhpcyBzaGFwZSByZXByZXNlbnRzIGEgc2xpY2VyLiBTbGljZXJzIGFyZSBzdXBwb3J0ZWQgaW4gRXhjZWwgMjAxMCBvciBsYXRlci4KCklmIHRoZSBzaGFwZSB3YXMgbW9kaWZpZWQgaW4gYW4gZWFybGllciB2ZXJzaW9uIG9mIEV4Y2VsLCBvciBpZiB0aGUgd29ya2Jvb2sgd2FzIHNhdmVkIGluIEV4Y2VsIDIwMDMgb3IgZWFybGllciwgdGhlIHNsaWNlciBjYW5ub3QgYmUgdXNlZC48L2E6dD4NCiAgICAgICAgICAgICAgPC9hOnI+DQogICAgICAgICAgICA8L2E6cD4NCiAgICAgICAgICA8L3hkcjp0eEJvZHk+DQogICAgICAgIDwveGRyOnNwPg0KICAgICAgPC9tYzpGYWxsYmFjaz4NCiAgICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogICAgPHhkcjpjbGllbnREYXRhIC8+DQogIDwveGRyOnR3b0NlbGxBbmNob3I+DQo8L3hkcjp3c0RyPlBLAwQUAAAAAAAAACEA2PthmIsXAACLFwAAHgAAAHhsL3Bpdm90VGFibGVzL3Bpdm90VGFibGUyLnhtbO+7vzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxwaXZvdFRhYmxlRGVmaW5pdGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIiBuYW1lPSJQaXZvdFRhYmxlMiIgY2FjaGVJZD0iMTIiIGFwcGx5TnVtYmVyRm9ybWF0cz0iMCIgYXBwbHlCb3JkZXJGb3JtYXRzPSIwIiBhcHBseUZvbnRGb3JtYXRzPSIwIiBhcHBseVBhdHRlcm5Gb3JtYXRzPSIwIiBhcHBseUFsaWdubWVudEZvcm1hdHM9IjAiIGFwcGx5V2lkdGhIZWlnaHRGb3JtYXRzPSIxIiBkYXRhQ2FwdGlvbj0iVmFsdWVzIiBncmFuZFRvdGFsQ2FwdGlvbj0iVG90YWx0IiBtaXNzaW5nQ2FwdGlvbj0iMCIgdXBkYXRlZFZlcnNpb249IjUiIG1pblJlZnJlc2hhYmxlVmVyc2lvbj0iMyIgdXNlQXV0b0Zvcm1hdHRpbmc9IjEiIGl0ZW1QcmludFRpdGxlcz0iMSIgY3JlYXRlZFZlcnNpb249IjUiIGluZGVudD0iMCIgb3V0bGluZT0iMSIgb3V0bGluZURhdGE9IjEiIG11bHRpcGxlRmllbGRGaWx0ZXJzPSIwIiByb3dIZWFkZXJDYXB0aW9uPSJBbnbDpG5kYXJlIj4NCiAgPGxvY2F0aW9uIHJlZj0iQzE6TzQiIGZpcnN0SGVhZGVyUm93PSIwIiBmaXJzdERhdGFSb3c9IjEiIGZpcnN0RGF0YUNvbD0iMSIgLz4NCiAgPHBpdm90RmllbGRzIGNvdW50PSIxNSI+DQogICAgPHBpdm90RmllbGQgYXhpcz0iYXhpc1JvdyIgc2hvd0FsbD0iMCI+DQogICAgICA8aXRlbXMgY291bnQ9IjIiPg0KICAgICAgICA8aXRlbSB4PSIwIiAvPg0KICAgICAgICA8aXRlbSB0PSJkZWZhdWx0IiAvPg0KICAgICAgPC9pdGVtcz4NCiAgICA8L3Bpdm90RmllbGQ+DQogICAgPHBpdm90RmllbGQgYXhpcz0iYXhpc1JvdyIgc2hvd0FsbD0iMCI+DQogICAgICA8aXRlbXMgY291bnQ9IjIiPg0KICAgICAgICA8aXRlbSB4PSIwIiAvPg0KICAgICAgICA8aXRlbSB0PSJkZWZhdWx0IiAvPg0KICAgICAgPC9pdGVtcz4NCiAgICA8L3Bpdm90RmllbGQ+DQogICAgPHBpdm90RmllbGQgbnVtRm10SWQ9IjIiIHNob3dBbGw9IjAiIGRlZmF1bHRTdWJ0b3RhbD0iMCI+DQogICAgICA8aXRlbXMgY291bnQ9IjEiPg0KICAgICAgICA8aXRlbSB4PSIwIiAvPg0KICAgICAgPC9pdGVtcz4NCiAgICA8L3Bpdm90RmllbGQ+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgc2hvd0FsbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIHNob3dBbGw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgc2hvd0FsbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIHNob3dBbGw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgc2hvd0FsbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIGRyYWdUb1Jvdz0iMCIgZHJhZ1RvQ29sPSIwIiBkcmFnVG9QYWdlPSIwIiBzaG93QWxsPSIwIiBkZWZhdWx0U3VidG90YWw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBkcmFnVG9Sb3c9IjAiIGRyYWdUb0NvbD0iMCIgZHJhZ1RvUGFnZT0iMCIgc2hvd0FsbD0iMCIgZGVmYXVsdFN1YnRvdGFsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgZHJhZ1RvUm93PSIwIiBkcmFnVG9Db2w9IjAiIGRyYWdUb1BhZ2U9IjAiIHNob3dBbGw9IjAiIGRlZmF1bHRTdWJ0b3RhbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIGRyYWdUb1Jvdz0iMCIgZHJhZ1RvQ29sPSIwIiBkcmFnVG9QYWdlPSIwIiBzaG93QWxsPSIwIiBkZWZhdWx0U3VidG90YWw9IjAiIC8+DQogIDwvcGl2b3RGaWVsZHM+DQogIDxyb3dGaWVsZHMgY291bnQ9IjIiPg0KICAgIDxmaWVsZCB4PSIwIiAvPg0KICAgIDxmaWVsZCB4PSIxIiAvPg0KICA8L3Jvd0ZpZWxkcz4NCiAgPHJvd0l0ZW1zIGNvdW50PSIzIj4NCiAgICA8aT4NCiAgICAgIDx4IC8+DQogICAgPC9pPg0KICAgIDxpIHI9IjEiPg0KICAgICAgPHggLz4NCiAgICA8L2k+DQogICAgPGkgdD0iZ3JhbmQiPg0KICAgICAgPHggLz4NCiAgICA8L2k+DQogIDwvcm93SXRlbXM+DQogIDxjb2xGaWVsZHMgY291bnQ9IjEiPg0KICAgIDxmaWVsZCB4PSItMiIgLz4NCiAgPC9jb2xGaWVsZHM+DQogIDxjb2xJdGVtcyBjb3VudD0iMTIiPg0KICAgIDxpPg0KICAgICAgPHggLz4NCiAgICA8L2k+DQogICAgPGkgaT0iMSI+DQogICAgICA8eCB2PSIxIiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSIyIj4NCiAgICAgIDx4IHY9IjIiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjMiPg0KICAgICAgPHggdj0iMyIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iNCI+DQogICAgICA8eCB2PSI0IiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSI1Ij4NCiAgICAgIDx4IHY9IjUiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjYiPg0KICAgICAgPHggdj0iNiIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iNyI+DQogICAgICA8eCB2PSI3IiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSI4Ij4NCiAgICAgIDx4IHY9IjgiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjkiPg0KICAgICAgPHggdj0iOSIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iMTAiPg0KICAgICAgPHggdj0iMTAiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjExIj4NCiAgICAgIDx4IHY9IjExIiAvPg0KICAgIDwvaT4NCiAgPC9jb2xJdGVtcz4NCiAgPGRhdGFGaWVsZHMgY291bnQ9IjEyIj4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IlJlZ2lzdHJlcmFkZSBzYW10YWwgIiBmbGQ9IjMiIGJhc2VGaWVsZD0iMCIgYmFzZUl0ZW09IjAiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSJSZWdpc3RyZXJhZGUgcHJvc3Bla3QgIiBmbGQ9IjQiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjEyIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iT3JkZXIgIiBmbGQ9IjUiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjEyIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iT3JkZXIgS29udGFrdGVyICIgZmxkPSI2IiBiYXNlRmllbGQ9IjEiIGJhc2VJdGVtPSIxMiIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IkhpdHJhdGUgJSAiIGZsZD0iMTEiIHN1YnRvdGFsPSJhdmVyYWdlIiBiYXNlRmllbGQ9IjAiIGJhc2VJdGVtPSIxIiBudW1GbXRJZD0iMTAiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSJTYW10YWwgLSBUZWwuU3ZhciAiIGZsZD0iMTIiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjkiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSIlIGxpdmUgc2FtdGFsICIgZmxkPSIxMyIgc3VidG90YWw9ImF2ZXJhZ2UiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjkiIG51bUZtdElkPSIxMCIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9Ik9yZGVyL2xpdmUgc2FtdGFsICIgZmxkPSIxNCIgc3VidG90YWw9ImF2ZXJhZ2UiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjkiIG51bUZtdElkPSIxMCIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IlRlbGVmb25zdmFyYXJlICIgZmxkPSI3IiBiYXNlRmllbGQ9IjEiIGJhc2VJdGVtPSIxMyIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IsOFdGVydXBwcmluZ25pbmdhciAiIGZsZD0iOCIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iMTMiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSJBbnRhbCBOZWogVGFjayAiIGZsZD0iOSIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iMTMiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSJBbnRhbCBCb3J0ZmFsbCAiIGZsZD0iMTAiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjEzIiAvPg0KICA8L2RhdGFGaWVsZHM+DQogIDxmb3JtYXRzIGNvdW50PSI3Ij4NCiAgICA8Zm9ybWF0IGR4ZklkPSIzNCI+DQogICAgICA8cGl2b3RBcmVhIHR5cGU9ImFsbCIgZGF0YU9ubHk9IjAiIG91dGxpbmU9IjAiIGZpZWxkUG9zaXRpb249IjAiIC8+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMzMiPg0KICAgICAgPHBpdm90QXJlYSBvdXRsaW5lPSIwIiBjb2xsYXBzZWRMZXZlbHNBcmVTdWJ0b3RhbHM9IjEiIGZpZWxkUG9zaXRpb249IjAiIC8+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMzIiPg0KICAgICAgPHBpdm90QXJlYSBmaWVsZD0iMCIgdHlwZT0iYnV0dG9uIiBkYXRhT25seT0iMCIgbGFiZWxPbmx5PSIxIiBvdXRsaW5lPSIwIiBheGlzPSJheGlzUm93IiBmaWVsZFBvc2l0aW9uPSIwIiAvPg0KICAgIDwvZm9ybWF0Pg0KICAgIDxmb3JtYXQgZHhmSWQ9IjMxIj4NCiAgICAgIDxwaXZvdEFyZWEgZGF0YU9ubHk9IjAiIGxhYmVsT25seT0iMSIgZmllbGRQb3NpdGlvbj0iMCI+DQogICAgICAgIDxyZWZlcmVuY2VzIGNvdW50PSIxIj4NCiAgICAgICAgICA8cmVmZXJlbmNlIGZpZWxkPSIwIiBjb3VudD0iMCIgLz4NCiAgICAgICAgPC9yZWZlcmVuY2VzPg0KICAgICAgPC9waXZvdEFyZWE+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMzAiPg0KICAgICAgPHBpdm90QXJlYSBkYXRhT25seT0iMCIgbGFiZWxPbmx5PSIxIiBncmFuZFJvdz0iMSIgb3V0bGluZT0iMCIgZmllbGRQb3NpdGlvbj0iMCIgLz4NCiAgICA8L2Zvcm1hdD4NCiAgICA8Zm9ybWF0IGR4ZklkPSIyOSI+DQogICAgICA8cGl2b3RBcmVhIGRhdGFPbmx5PSIwIiBsYWJlbE9ubHk9IjEiIGZpZWxkUG9zaXRpb249IjAiPg0KICAgICAgICA8cmVmZXJlbmNlcyBjb3VudD0iMiI+DQogICAgICAgICAgPHJlZmVyZW5jZSBmaWVsZD0iMCIgY291bnQ9IjAiIHNlbGVjdGVkPSIwIiAvPg0KICAgICAgICAgIDxyZWZlcmVuY2UgZmllbGQ9IjEiIGNvdW50PSIwIiAvPg0KICAgICAgICA8L3JlZmVyZW5jZXM+DQogICAgICA8L3Bpdm90QXJlYT4NCiAgICA8L2Zvcm1hdD4NCiAgICA8Zm9ybWF0IGR4ZklkPSIyOCI+DQogICAgICA8cGl2b3RBcmVhIGRhdGFPbmx5PSIwIiBsYWJlbE9ubHk9IjEiIG91dGxpbmU9IjAiIGZpZWxkUG9zaXRpb249IjAiPg0KICAgICAgICA8cmVmZXJlbmNlcyBjb3VudD0iMSI+DQogICAgICAgICAgPHJlZmVyZW5jZSBmaWVsZD0iNDI5NDk2NzI5NCIgY291bnQ9IjEyIj4NCiAgICAgICAgICAgIDx4IHY9IjAiIC8+DQogICAgICAgICAgICA8eCB2PSIxIiAvPg0KICAgICAgICAgICAgPHggdj0iMiIgLz4NCiAgICAgICAgICAgIDx4IHY9IjMiIC8+DQogICAgICAgICAgICA8eCB2PSI0IiAvPg0KICAgICAgICAgICAgPHggdj0iNSIgLz4NCiAgICAgICAgICAgIDx4IHY9IjYiIC8+DQogICAgICAgICAgICA8eCB2PSI3IiAvPg0KICAgICAgICAgICAgPHggdj0iOCIgLz4NCiAgICAgICAgICAgIDx4IHY9IjkiIC8+DQogICAgICAgICAgICA8eCB2PSIxMCIgLz4NCiAgICAgICAgICAgIDx4IHY9IjExIiAvPg0KICAgICAgICAgIDwvcmVmZXJlbmNlPg0KICAgICAgICA8L3JlZmVyZW5jZXM+DQogICAgICA8L3Bpdm90QXJlYT4NCiAgICA8L2Zvcm1hdD4NCiAgPC9mb3JtYXRzPg0KICA8cGl2b3RUYWJsZVN0eWxlSW5mbyBuYW1lPSJQaXZvdFN0eWxlTWVkaXVtNiIgc2hvd1Jvd0hlYWRlcnM9IjEiIHNob3dDb2xIZWFkZXJzPSIxIiBzaG93Um93U3RyaXBlcz0iMCIgc2hvd0NvbFN0cmlwZXM9IjAiIHNob3dMYXN0Q29sdW1uPSIxIiAvPg0KICA8ZXh0THN0Pg0KICAgIDxleHQgdXJpPSJ7OTYyRUY1RDEtNUNBMi00YzkzLThFRjQtREJGNUMwNTQzOUQyfSIgeG1sbnM6eDE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIj4NCiAgICAgIDx4MTQ6cGl2b3RUYWJsZURlZmluaXRpb24gaGlkZVZhbHVlc1Jvdz0iMSIgeG1sbnM6eG09Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2V4Y2VsLzIwMDYvbWFpbiIgLz4NCiAgICA8L2V4dD4NCiAgPC9leHRMc3Q+DQo8L3Bpdm90VGFibGVEZWZpbml0aW9uPlBLAwQUAAAAAAAAACEAZTC+rkUBAABFAQAAKQAAAHhsL3Bpdm90VGFibGVzL19yZWxzL3Bpdm90VGFibGUyLnhtbC5yZWxzPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxSZWxhdGlvbnNoaXBzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L3JlbGF0aW9uc2hpcHMiPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDEiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvcGl2b3RDYWNoZURlZmluaXRpb24iIFRhcmdldD0iLi4vcGl2b3RDYWNoZS9waXZvdENhY2hlRGVmaW5pdGlvbjIueG1sIi8+PC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAAAAACEA8rIYXXICAAByAgAAFgAAAHhsL3NsaWNlcnMvc2xpY2VyMS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8c2xpY2VycyB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvbWFpbiIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiI+PHNsaWNlciBuYW1lPSJQcm9qZWt0IiBjYWNoZT0iU2xpY2VyX1Byb2pla3QiIGNhcHRpb249IlByb2pla3QiIHN0eWxlPSJEcmVtcmVwb3J0U2xpY2VyIiByb3dIZWlnaHQ9IjI0MTMwMCIvPjxzbGljZXIgbmFtZT0iU8OkbGphcmUiIGNhY2hlPSJTbGljZXJfU8OkbGphcmUiIGNhcHRpb249IlPDpGxqYXJlIiBzdHlsZT0iRHJlbXJlcG9ydFNsaWNlciIgcm93SGVpZ2h0PSIyNDEzMDAiLz48c2xpY2VyIG5hbWU9IkRhdHVtIiBjYWNoZT0iU2xpY2VyX0RhdHVtIiBjYXB0aW9uPSJEYXR1bSIgc3R5bGU9IkRyZW1yZXBvcnRTbGljZXIiIHJvd0hlaWdodD0iMjQxMzAwIi8+PC9zbGljZXJzPlBLAwQUAAAAAAAAACEArGQ8asEgAADBIAAAGAAAAHhsL2RyYXdpbmdzL2RyYXdpbmcxLnhtbO+7vzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjx4ZHI6d3NEciB4bWxuczp4ZHI9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9kcmF3aW5nbWwvMjAwNi9zcHJlYWRzaGVldERyYXdpbmciIHhtbG5zOmE9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9kcmF3aW5nbWwvMjAwNi9tYWluIj4NCiAgPHhkcjp0d29DZWxsQW5jaG9yIGVkaXRBcz0iYWJzb2x1dGUiPg0KICAgIDx4ZHI6ZnJvbT4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj45MjExPC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+MTwveGRyOnJvdz4NCiAgICAgIDx4ZHI6cm93T2ZmPjA8L3hkcjpyb3dPZmY+DQogICAgPC94ZHI6ZnJvbT4NCiAgICA8eGRyOnRvPg0KICAgICAgPHhkcjpjb2w+MTwveGRyOmNvbD4NCiAgICAgIDx4ZHI6Y29sT2ZmPjIzNDkyMTE8L3hkcjpjb2xPZmY+DQogICAgICA8eGRyOnJvdz4yMzwveGRyOnJvdz4NCiAgICAgIDx4ZHI6cm93T2ZmPjM3NjUwPC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOnRvPg0KICAgIDxtYzpBbHRlcm5hdGVDb250ZW50IHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgeG1sbnM6YTE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvbWFpbiI+DQogICAgICA8bWM6Q2hvaWNlIFJlcXVpcmVzPSJhMTQiPg0KICAgICAgICA8eGRyOmdyYXBoaWNGcmFtZSBtYWNybz0iIj4NCiAgICAgICAgICA8eGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSIyIiBuYW1lPSJQcm9qZWt0IiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICAgICAgPGE6Z3JhcGhpY0ZyYW1lTG9ja3Mgbm9DaGFuZ2VBc3BlY3Q9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8L3hkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDx4ZHI6eGZybT4NCiAgICAgICAgICAgIDxhOm9mZiB4PSIwIiB5PSIwIiAvPg0KICAgICAgICAgICAgPGE6ZXh0IGN4PSIwIiBjeT0iMCIgLz4NCiAgICAgICAgICA8L3hkcjp4ZnJtPg0KICAgICAgICAgIDxhOmdyYXBoaWM+DQogICAgICAgICAgICA8YTpncmFwaGljRGF0YSB1cmk9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiPg0KICAgICAgICAgICAgICA8c2xlOnNsaWNlciB4bWxuczpzbGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2RyYXdpbmcvMjAxMC9zbGljZXIiIG5hbWU9IlByb2pla3QiIC8+DQogICAgICAgICAgICA8L2E6Z3JhcGhpY0RhdGE+DQogICAgICAgICAgPC9hOmdyYXBoaWM+DQogICAgICAgIDwveGRyOmdyYXBoaWNGcmFtZT4NCiAgICAgIDwvbWM6Q2hvaWNlPg0KICAgICAgPG1jOkZhbGxiYWNrIHhtbG5zPSIiPg0KICAgICAgICA8eGRyOnNwIG1hY3JvPSIiIHRleHRsaW5rPSIiPg0KICAgICAgICAgIDx4ZHI6bnZTcFByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMCIgbmFtZT0iIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZTcFByPg0KICAgICAgICAgICAgICA8YTpzcExvY2tzIG5vVGV4dEVkaXQ9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZTcFByPg0KICAgICAgICAgIDwveGRyOm52U3BQcj4NCiAgICAgICAgICA8eGRyOnNwUHI+DQogICAgICAgICAgICA8YTp4ZnJtPg0KICAgICAgICAgICAgICA8YTpvZmYgeD0iMTA4NTUzNiIgeT0iMTYxOTI1IiAvPg0KICAgICAgICAgICAgICA8YTpleHQgY3g9IjIzNDAwMDAiIGN5PSIzNjAwMDAwIiAvPg0KICAgICAgICAgICAgPC9hOnhmcm0+DQogICAgICAgICAgICA8YTpwcnN0R2VvbSBwcnN0PSJyZWN0Ij4NCiAgICAgICAgICAgICAgPGE6YXZMc3QgLz4NCiAgICAgICAgICAgIDwvYTpwcnN0R2VvbT4NCiAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9IndoaXRlIiAvPg0KICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDxhOmxuIHc9IjEiPg0KICAgICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9ImdyZWVuIiAvPg0KICAgICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPC9hOmxuPg0KICAgICAgICAgIDwveGRyOnNwUHI+DQogICAgICAgICAgPHhkcjp0eEJvZHk+DQogICAgICAgICAgICA8YTpib2R5UHIgdmVydE92ZXJmbG93PSJjbGlwIiBob3J6T3ZlcmZsb3c9ImNsaXAiIC8+DQogICAgICAgICAgICA8YTpsc3RTdHlsZSAvPg0KICAgICAgICAgICAgPGE6cD4NCiAgICAgICAgICAgICAgPGE6cj4NCiAgICAgICAgICAgICAgICA8YTpyUHIgbGFuZz0ic3YtU0UiIHN6PSIxMTAwIiAvPg0KICAgICAgICAgICAgICAgIDxhOnQ+VGhpcyBzaGFwZSByZXByZXNlbnRzIGEgc2xpY2VyLiBTbGljZXJzIGFyZSBzdXBwb3J0ZWQgaW4gRXhjZWwgMjAxMCBvciBsYXRlci4KCklmIHRoZSBzaGFwZSB3YXMgbW9kaWZpZWQgaW4gYW4gZWFybGllciB2ZXJzaW9uIG9mIEV4Y2VsLCBvciBpZiB0aGUgd29ya2Jvb2sgd2FzIHNhdmVkIGluIEV4Y2VsIDIwMDMgb3IgZWFybGllciwgdGhlIHNsaWNlciBjYW5ub3QgYmUgdXNlZC48L2E6dD4NCiAgICAgICAgICAgICAgPC9hOnI+DQogICAgICAgICAgICA8L2E6cD4NCiAgICAgICAgICA8L3hkcjp0eEJvZHk+DQogICAgICAgIDwveGRyOnNwPg0KICAgICAgPC9tYzpGYWxsYmFjaz4NCiAgICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogICAgPHhkcjpjbGllbnREYXRhIC8+DQogIDwveGRyOnR3b0NlbGxBbmNob3I+DQogIDx4ZHI6dHdvQ2VsbEFuY2hvciBlZGl0QXM9ImFic29sdXRlIj4NCiAgICA8eGRyOmZyb20+DQogICAgICA8eGRyOmNvbD4xPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+NTYxMDwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjIzPC94ZHI6cm93Pg0KICAgICAgPHhkcjpyb3dPZmY+NDA4MjM8L3hkcjpyb3dPZmY+DQogICAgPC94ZHI6ZnJvbT4NCiAgICA8eGRyOnRvPg0KICAgICAgPHhkcjpjb2w+MTwveGRyOmNvbD4NCiAgICAgIDx4ZHI6Y29sT2ZmPjIzNDkyMTA8L3hkcjpjb2xPZmY+DQogICAgICA8eGRyOnJvdz41NjwveGRyOnJvdz4NCiAgICAgIDx4ZHI6cm93T2ZmPjk3Mjk4PC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOnRvPg0KICAgIDxtYzpBbHRlcm5hdGVDb250ZW50IHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgeG1sbnM6YTE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvbWFpbiI+DQogICAgICA8bWM6Q2hvaWNlIFJlcXVpcmVzPSJhMTQiPg0KICAgICAgICA8eGRyOmdyYXBoaWNGcmFtZSBtYWNybz0iIj4NCiAgICAgICAgICA8eGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSI0IiBuYW1lPSJTw6RsamFyZSIgLz4NCiAgICAgICAgICAgIDx4ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgICAgIDxhOmdyYXBoaWNGcmFtZUxvY2tzIG5vQ2hhbmdlQXNwZWN0PSIxIiAvPg0KICAgICAgICAgICAgPC94ZHI6Y052R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgPC94ZHI6bnZHcmFwaGljRnJhbWVQcj4NCiAgICAgICAgICA8eGRyOnhmcm0+DQogICAgICAgICAgICA8YTpvZmYgeD0iMCIgeT0iMCIgLz4NCiAgICAgICAgICAgIDxhOmV4dCBjeD0iMCIgY3k9IjAiIC8+DQogICAgICAgICAgPC94ZHI6eGZybT4NCiAgICAgICAgICA8YTpncmFwaGljPg0KICAgICAgICAgICAgPGE6Z3JhcGhpY0RhdGEgdXJpPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvc2xpY2VyIj4NCiAgICAgICAgICAgICAgPHNsZTpzbGljZXIgeG1sbnM6c2xlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9kcmF3aW5nLzIwMTAvc2xpY2VyIiBuYW1lPSJTw6RsamFyZSIgLz4NCiAgICAgICAgICAgIDwvYTpncmFwaGljRGF0YT4NCiAgICAgICAgICA8L2E6Z3JhcGhpYz4NCiAgICAgICAgPC94ZHI6Z3JhcGhpY0ZyYW1lPg0KICAgICAgPC9tYzpDaG9pY2U+DQogICAgICA8bWM6RmFsbGJhY2sgeG1sbnM9IiI+DQogICAgICAgIDx4ZHI6c3AgbWFjcm89IiIgdGV4dGxpbms9IiI+DQogICAgICAgICAgPHhkcjpudlNwUHI+DQogICAgICAgICAgICA8eGRyOmNOdlByIGlkPSIwIiBuYW1lPSIiIC8+DQogICAgICAgICAgICA8eGRyOmNOdlNwUHI+DQogICAgICAgICAgICAgIDxhOnNwTG9ja3Mgbm9UZXh0RWRpdD0iMSIgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdlNwUHI+DQogICAgICAgICAgPC94ZHI6bnZTcFByPg0KICAgICAgICAgIDx4ZHI6c3BQcj4NCiAgICAgICAgICAgIDxhOnhmcm0+DQogICAgICAgICAgICAgIDxhOm9mZiB4PSIxMDgxOTM1IiB5PSIzNzY1MDk4IiAvPg0KICAgICAgICAgICAgICA8YTpleHQgY3g9IjIzNDM2MDAiIGN5PSI1NDAwMDAwIiAvPg0KICAgICAgICAgICAgPC9hOnhmcm0+DQogICAgICAgICAgICA8YTpwcnN0R2VvbSBwcnN0PSJyZWN0Ij4NCiAgICAgICAgICAgICAgPGE6YXZMc3QgLz4NCiAgICAgICAgICAgIDwvYTpwcnN0R2VvbT4NCiAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9IndoaXRlIiAvPg0KICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDxhOmxuIHc9IjEiPg0KICAgICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9ImdyZWVuIiAvPg0KICAgICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPC9hOmxuPg0KICAgICAgICAgIDwveGRyOnNwUHI+DQogICAgICAgICAgPHhkcjp0eEJvZHk+DQogICAgICAgICAgICA8YTpib2R5UHIgdmVydE92ZXJmbG93PSJjbGlwIiBob3J6T3ZlcmZsb3c9ImNsaXAiIC8+DQogICAgICAgICAgICA8YTpsc3RTdHlsZSAvPg0KICAgICAgICAgICAgPGE6cD4NCiAgICAgICAgICAgICAgPGE6cj4NCiAgICAgICAgICAgICAgICA8YTpyUHIgbGFuZz0ic3YtU0UiIHN6PSIxMTAwIiAvPg0KICAgICAgICAgICAgICAgIDxhOnQ+VGhpcyBzaGFwZSByZXByZXNlbnRzIGEgc2xpY2VyLiBTbGljZXJzIGFyZSBzdXBwb3J0ZWQgaW4gRXhjZWwgMjAxMCBvciBsYXRlci4KCklmIHRoZSBzaGFwZSB3YXMgbW9kaWZpZWQgaW4gYW4gZWFybGllciB2ZXJzaW9uIG9mIEV4Y2VsLCBvciBpZiB0aGUgd29ya2Jvb2sgd2FzIHNhdmVkIGluIEV4Y2VsIDIwMDMgb3IgZWFybGllciwgdGhlIHNsaWNlciBjYW5ub3QgYmUgdXNlZC48L2E6dD4NCiAgICAgICAgICAgICAgPC9hOnI+DQogICAgICAgICAgICA8L2E6cD4NCiAgICAgICAgICA8L3hkcjp0eEJvZHk+DQogICAgICAgIDwveGRyOnNwPg0KICAgICAgPC9tYzpGYWxsYmFjaz4NCiAgICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogICAgPHhkcjpjbGllbnREYXRhIC8+DQogIDwveGRyOnR3b0NlbGxBbmNob3I+DQogIDx4ZHI6dHdvQ2VsbEFuY2hvciBlZGl0QXM9ImFic29sdXRlIj4NCiAgICA8eGRyOmZyb20+DQogICAgICA8eGRyOmNvbD4wPC94ZHI6Y29sPg0KICAgICAgPHhkcjpjb2xPZmY+MTwveGRyOmNvbE9mZj4NCiAgICAgIDx4ZHI6cm93PjE8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj4wPC94ZHI6cm93T2ZmPg0KICAgIDwveGRyOmZyb20+DQogICAgPHhkcjp0bz4NCiAgICAgIDx4ZHI6Y29sPjE8L3hkcjpjb2w+DQogICAgICA8eGRyOmNvbE9mZj4yMzE1PC94ZHI6Y29sT2ZmPg0KICAgICAgPHhkcjpyb3c+NTY8L3hkcjpyb3c+DQogICAgICA8eGRyOnJvd09mZj45NzcyNTwveGRyOnJvd09mZj4NCiAgICA8L3hkcjp0bz4NCiAgICA8bWM6QWx0ZXJuYXRlQ29udGVudCB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIHhtbG5zOmExND0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL21haW4iPg0KICAgICAgPG1jOkNob2ljZSBSZXF1aXJlcz0iYTE0Ij4NCiAgICAgICAgPHhkcjpncmFwaGljRnJhbWUgbWFjcm89IiI+DQogICAgICAgICAgPHhkcjpudkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMyIgbmFtZT0iRGF0dW0iIC8+DQogICAgICAgICAgICA8eGRyOmNOdkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgICAgICA8YTpncmFwaGljRnJhbWVMb2NrcyBub0NoYW5nZUFzcGVjdD0iMSIgLz4NCiAgICAgICAgICAgIDwveGRyOmNOdkdyYXBoaWNGcmFtZVByPg0KICAgICAgICAgIDwveGRyOm52R3JhcGhpY0ZyYW1lUHI+DQogICAgICAgICAgPHhkcjp4ZnJtPg0KICAgICAgICAgICAgPGE6b2ZmIHg9IjAiIHk9IjAiIC8+DQogICAgICAgICAgICA8YTpleHQgY3g9IjAiIGN5PSIwIiAvPg0KICAgICAgICAgIDwveGRyOnhmcm0+DQogICAgICAgICAgPGE6Z3JhcGhpYz4NCiAgICAgICAgICAgIDxhOmdyYXBoaWNEYXRhIHVyaT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL3NsaWNlciI+DQogICAgICAgICAgICAgIDxzbGU6c2xpY2VyIHhtbG5zOnNsZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvZHJhd2luZy8yMDEwL3NsaWNlciIgbmFtZT0iRGF0dW0iIC8+DQogICAgICAgICAgICA8L2E6Z3JhcGhpY0RhdGE+DQogICAgICAgICAgPC9hOmdyYXBoaWM+DQogICAgICAgIDwveGRyOmdyYXBoaWNGcmFtZT4NCiAgICAgIDwvbWM6Q2hvaWNlPg0KICAgICAgPG1jOkZhbGxiYWNrIHhtbG5zPSIiPg0KICAgICAgICA8eGRyOnNwIG1hY3JvPSIiIHRleHRsaW5rPSIiPg0KICAgICAgICAgIDx4ZHI6bnZTcFByPg0KICAgICAgICAgICAgPHhkcjpjTnZQciBpZD0iMCIgbmFtZT0iIiAvPg0KICAgICAgICAgICAgPHhkcjpjTnZTcFByPg0KICAgICAgICAgICAgICA8YTpzcExvY2tzIG5vVGV4dEVkaXQ9IjEiIC8+DQogICAgICAgICAgICA8L3hkcjpjTnZTcFByPg0KICAgICAgICAgIDwveGRyOm52U3BQcj4NCiAgICAgICAgICA8eGRyOnNwUHI+DQogICAgICAgICAgICA8YTp4ZnJtPg0KICAgICAgICAgICAgICA8YTpvZmYgeD0iMSIgeT0iMTYxOTI1IiAvPg0KICAgICAgICAgICAgICA8YTpleHQgY3g9IjEwNzg2MzkiIGN5PSI5MDAzNjAwIiAvPg0KICAgICAgICAgICAgPC9hOnhmcm0+DQogICAgICAgICAgICA8YTpwcnN0R2VvbSBwcnN0PSJyZWN0Ij4NCiAgICAgICAgICAgICAgPGE6YXZMc3QgLz4NCiAgICAgICAgICAgIDwvYTpwcnN0R2VvbT4NCiAgICAgICAgICAgIDxhOnNvbGlkRmlsbD4NCiAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9IndoaXRlIiAvPg0KICAgICAgICAgICAgPC9hOnNvbGlkRmlsbD4NCiAgICAgICAgICAgIDxhOmxuIHc9IjEiPg0KICAgICAgICAgICAgICA8YTpzb2xpZEZpbGw+DQogICAgICAgICAgICAgICAgPGE6cHJzdENsciB2YWw9ImdyZWVuIiAvPg0KICAgICAgICAgICAgICA8L2E6c29saWRGaWxsPg0KICAgICAgICAgICAgPC9hOmxuPg0KICAgICAgICAgIDwveGRyOnNwUHI+DQogICAgICAgICAgPHhkcjp0eEJvZHk+DQogICAgICAgICAgICA8YTpib2R5UHIgdmVydE92ZXJmbG93PSJjbGlwIiBob3J6T3ZlcmZsb3c9ImNsaXAiIC8+DQogICAgICAgICAgICA8YTpsc3RTdHlsZSAvPg0KICAgICAgICAgICAgPGE6cD4NCiAgICAgICAgICAgICAgPGE6cj4NCiAgICAgICAgICAgICAgICA8YTpyUHIgbGFuZz0ic3YtU0UiIHN6PSIxMTAwIiAvPg0KICAgICAgICAgICAgICAgIDxhOnQ+VGhpcyBzaGFwZSByZXByZXNlbnRzIGEgc2xpY2VyLiBTbGljZXJzIGFyZSBzdXBwb3J0ZWQgaW4gRXhjZWwgMjAxMCBvciBsYXRlci4KCklmIHRoZSBzaGFwZSB3YXMgbW9kaWZpZWQgaW4gYW4gZWFybGllciB2ZXJzaW9uIG9mIEV4Y2VsLCBvciBpZiB0aGUgd29ya2Jvb2sgd2FzIHNhdmVkIGluIEV4Y2VsIDIwMDMgb3IgZWFybGllciwgdGhlIHNsaWNlciBjYW5ub3QgYmUgdXNlZC48L2E6dD4NCiAgICAgICAgICAgICAgPC9hOnI+DQogICAgICAgICAgICA8L2E6cD4NCiAgICAgICAgICA8L3hkcjp0eEJvZHk+DQogICAgICAgIDwveGRyOnNwPg0KICAgICAgPC9tYzpGYWxsYmFjaz4NCiAgICA8L21jOkFsdGVybmF0ZUNvbnRlbnQ+DQogICAgPHhkcjpjbGllbnREYXRhIC8+DQogIDwveGRyOnR3b0NlbGxBbmNob3I+DQo8L3hkcjp3c0RyPlBLAwQUAAAAAAAAACEA+oJojfQCAAD0AgAAFgAAAHhsL3NsaWNlcnMvc2xpY2VyMy54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8c2xpY2VycyB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvbWFpbiIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiI+PHNsaWNlciBuYW1lPSJVdGZhbGxzdHlwIiBjYWNoZT0iU2xpY2VyX1V0ZmFsbHN0eXAiIGNhcHRpb249IlV0ZmFsbHN0eXAiIHN0eWxlPSJEcmVtcmVwb3J0U2xpY2VyIiByb3dIZWlnaHQ9IjI0MTMwMCIvPjxzbGljZXIgbmFtZT0iUHJvamVrdCAyIiBjYWNoZT0iU2xpY2VyX1Byb2pla3QyIiBjYXB0aW9uPSJQcm9qZWt0IiBzdHlsZT0iRHJlbXJlcG9ydFNsaWNlciIgcm93SGVpZ2h0PSIyNDEzMDAiLz48c2xpY2VyIG5hbWU9IkFudsOkbmRhcmUiIGNhY2hlPSJTbGljZXJfQW52w6RuZGFyZSIgY2FwdGlvbj0iQW52w6RuZGFyZSIgc3R5bGU9IkRyZW1yZXBvcnRTbGljZXIiIHJvd0hlaWdodD0iMjQxMzAwIi8+PHNsaWNlciBuYW1lPSJEYXR1bSAyIiBjYWNoZT0iU2xpY2VyX0RhdHVtMiIgY2FwdGlvbj0iRGF0dW0iIHN0eWxlPSJEcmVtcmVwb3J0U2xpY2VyIiByb3dIZWlnaHQ9IjI0MTMwMCIvPjwvc2xpY2Vycz5QSwMEFAAAAAAAAAAhAPRRuacnAgAAJwIAABgAAAB4bC93b3Jrc2hlZXRzL3NoZWV0My54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+PHdvcmtzaGVldCB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIj48ZGltZW5zaW9uIHJlZj0iQTE6RjEiLz48c2hlZXREYXRhPjxyb3cgcj0iMSI+PGMgcj0iQTEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PkRhdHVtPC90PjwvaXM+PC9jPjxjIHI9IkIxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5Qcm9qZWt0PC90PjwvaXM+PC9jPjxjIHI9IkMxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5BbnbDpG5kYXJlPC90PjwvaXM+PC9jPjxjIHI9IkQxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5VdGZhbGxzdHlwPC90PjwvaXM+PC9jPjxjIHI9IkUxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5VdGZhbGw8L3Q+PC9pcz48L2M+PGMgcj0iRjEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PkFudGFsPC90PjwvaXM+PC9jPjwvcm93Pjwvc2hlZXREYXRhPjwvd29ya3NoZWV0PlBLAwQUAAAAAAAAACEAyn0X3oIDAACCAwAAGAAAAHhsL3dvcmtzaGVldHMvc2hlZXQyLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz48d29ya3NoZWV0IHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iPjxkaW1lbnNpb24gcmVmPSJBMTpLMSIvPjxzaGVldERhdGE+PHJvdyByPSIxIj48YyByPSJBMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+UHJvamVrdDwvdD48L2lzPjwvYz48YyByPSJCMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+U8OkbGphcmU8L3Q+PC9pcz48L2M+PGMgcj0iQzEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PkRhdHVtPC90PjwvaXM+PC9jPjxjIHI9IkQxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5SZWdpc3RyZXJhZGUgc2FtdGFsPC90PjwvaXM+PC9jPjxjIHI9IkUxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5SZWdpc3RyZXJhZGUgcHJvc3Bla3Q8L3Q+PC9pcz48L2M+PGMgcj0iRjEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0Pk9yZGVyPC90PjwvaXM+PC9jPjxjIHI9IkcxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5PcmRlciBLb250YWt0ZXI8L3Q+PC9pcz48L2M+PGMgcj0iSDEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PlRlbGVmb25zdmFyYXJlPC90PjwvaXM+PC9jPjxjIHI9IkkxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD7DhXRlcnVwcHJpbmduaW5nYXI8L3Q+PC9pcz48L2M+PGMgcj0iSjEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PkFudGFsIE5laiBUYWNrPC90PjwvaXM+PC9jPjxjIHI9IksxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5BbnRhbCBCb3J0ZmFsbDwvdD48L2lzPjwvYz48L3Jvdz48L3NoZWV0RGF0YT48L3dvcmtzaGVldD5QSwMEFAAAAAAAAAAhAE17cgNHHgAARx4AAB4AAAB4bC9waXZvdFRhYmxlcy9waXZvdFRhYmxlMS54bWzvu788P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8cGl2b3RUYWJsZURlZmluaXRpb24geG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgbmFtZT0iUGl2b3RUYWJsZTEiIGNhY2hlSWQ9IjciIGFwcGx5TnVtYmVyRm9ybWF0cz0iMCIgYXBwbHlCb3JkZXJGb3JtYXRzPSIwIiBhcHBseUZvbnRGb3JtYXRzPSIwIiBhcHBseVBhdHRlcm5Gb3JtYXRzPSIwIiBhcHBseUFsaWdubWVudEZvcm1hdHM9IjAiIGFwcGx5V2lkdGhIZWlnaHRGb3JtYXRzPSIxIiBkYXRhQ2FwdGlvbj0iVmFsdWVzIiBncmFuZFRvdGFsQ2FwdGlvbj0iVG90YWx0IiBtaXNzaW5nQ2FwdGlvbj0iMCIgdXBkYXRlZFZlcnNpb249IjUiIG1pblJlZnJlc2hhYmxlVmVyc2lvbj0iMyIgdXNlQXV0b0Zvcm1hdHRpbmc9IjEiIGl0ZW1QcmludFRpdGxlcz0iMSIgY3JlYXRlZFZlcnNpb249IjUiIGluZGVudD0iMCIgb3V0bGluZT0iMSIgb3V0bGluZURhdGE9IjEiIG11bHRpcGxlRmllbGRGaWx0ZXJzPSIwIiByb3dIZWFkZXJDYXB0aW9uPSJBbnbDpG5kYXJlIj4NCiAgPGxvY2F0aW9uIHJlZj0iQzE6VjQiIGZpcnN0SGVhZGVyUm93PSIwIiBmaXJzdERhdGFSb3c9IjEiIGZpcnN0RGF0YUNvbD0iMSIgLz4NCiAgPHBpdm90RmllbGRzIGNvdW50PSIyMiI+DQogICAgPHBpdm90RmllbGQgYXhpcz0iYXhpc1JvdyIgc2hvd0FsbD0iMCI+DQogICAgICA8aXRlbXMgY291bnQ9IjIiPg0KICAgICAgICA8aXRlbSB4PSIwIiAvPg0KICAgICAgICA8aXRlbSB0PSJkZWZhdWx0IiAvPg0KICAgICAgPC9pdGVtcz4NCiAgICA8L3Bpdm90RmllbGQ+DQogICAgPHBpdm90RmllbGQgYXhpcz0iYXhpc1JvdyIgc2hvd0FsbD0iMCI+DQogICAgICA8aXRlbXMgY291bnQ9IjIiPg0KICAgICAgICA8aXRlbSB4PSIwIiAvPg0KICAgICAgICA8aXRlbSB0PSJkZWZhdWx0IiAvPg0KICAgICAgPC9pdGVtcz4NCiAgICA8L3Bpdm90RmllbGQ+DQogICAgPHBpdm90RmllbGQgbnVtRm10SWQ9IjE2NCIgc2hvd0FsbD0iMCIgZGVmYXVsdFN1YnRvdGFsPSIwIj4NCiAgICAgIDxpdGVtcyBjb3VudD0iMSI+DQogICAgICAgIDxpdGVtIHg9IjAiIC8+DQogICAgICA8L2l0ZW1zPg0KICAgIDwvcGl2b3RGaWVsZD4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIHNob3dBbGw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgc2hvd0FsbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIHNob3dBbGw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgc2hvd0FsbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIHNob3dBbGw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiBkZWZhdWx0U3VidG90YWw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBzaG93QWxsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgc2hvd0FsbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIHNob3dBbGw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBkcmFnVG9Sb3c9IjAiIGRyYWdUb0NvbD0iMCIgZHJhZ1RvUGFnZT0iMCIgc2hvd0FsbD0iMCIgZGVmYXVsdFN1YnRvdGFsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgZHJhZ1RvUm93PSIwIiBkcmFnVG9Db2w9IjAiIGRyYWdUb1BhZ2U9IjAiIHNob3dBbGw9IjAiIGRlZmF1bHRTdWJ0b3RhbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIGRyYWdUb1Jvdz0iMCIgZHJhZ1RvQ29sPSIwIiBkcmFnVG9QYWdlPSIwIiBzaG93QWxsPSIwIiBkZWZhdWx0U3VidG90YWw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBkcmFnVG9Sb3c9IjAiIGRyYWdUb0NvbD0iMCIgZHJhZ1RvUGFnZT0iMCIgc2hvd0FsbD0iMCIgZGVmYXVsdFN1YnRvdGFsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgZHJhZ1RvUm93PSIwIiBkcmFnVG9Db2w9IjAiIGRyYWdUb1BhZ2U9IjAiIHNob3dBbGw9IjAiIGRlZmF1bHRTdWJ0b3RhbD0iMCIgLz4NCiAgICA8cGl2b3RGaWVsZCBkYXRhRmllbGQ9IjEiIGRyYWdUb1Jvdz0iMCIgZHJhZ1RvQ29sPSIwIiBkcmFnVG9QYWdlPSIwIiBzaG93QWxsPSIwIiBkZWZhdWx0U3VidG90YWw9IjAiIC8+DQogICAgPHBpdm90RmllbGQgZGF0YUZpZWxkPSIxIiBkcmFnVG9Sb3c9IjAiIGRyYWdUb0NvbD0iMCIgZHJhZ1RvUGFnZT0iMCIgc2hvd0FsbD0iMCIgZGVmYXVsdFN1YnRvdGFsPSIwIiAvPg0KICAgIDxwaXZvdEZpZWxkIGRhdGFGaWVsZD0iMSIgZHJhZ1RvUm93PSIwIiBkcmFnVG9Db2w9IjAiIGRyYWdUb1BhZ2U9IjAiIHNob3dBbGw9IjAiIGRlZmF1bHRTdWJ0b3RhbD0iMCIgLz4NCiAgPC9waXZvdEZpZWxkcz4NCiAgPHJvd0ZpZWxkcyBjb3VudD0iMiI+DQogICAgPGZpZWxkIHg9IjAiIC8+DQogICAgPGZpZWxkIHg9IjEiIC8+DQogIDwvcm93RmllbGRzPg0KICA8cm93SXRlbXMgY291bnQ9IjMiPg0KICAgIDxpPg0KICAgICAgPHggLz4NCiAgICA8L2k+DQogICAgPGkgcj0iMSI+DQogICAgICA8eCAvPg0KICAgIDwvaT4NCiAgICA8aSB0PSJncmFuZCI+DQogICAgICA8eCAvPg0KICAgIDwvaT4NCiAgPC9yb3dJdGVtcz4NCiAgPGNvbEZpZWxkcyBjb3VudD0iMSI+DQogICAgPGZpZWxkIHg9Ii0yIiAvPg0KICA8L2NvbEZpZWxkcz4NCiAgPGNvbEl0ZW1zIGNvdW50PSIxOSI+DQogICAgPGk+DQogICAgICA8eCAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSIxIj4NCiAgICAgIDx4IHY9IjEiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjIiPg0KICAgICAgPHggdj0iMiIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iMyI+DQogICAgICA8eCB2PSIzIiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSI0Ij4NCiAgICAgIDx4IHY9IjQiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjUiPg0KICAgICAgPHggdj0iNSIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iNiI+DQogICAgICA8eCB2PSI2IiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSI3Ij4NCiAgICAgIDx4IHY9IjciIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjgiPg0KICAgICAgPHggdj0iOCIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iOSI+DQogICAgICA8eCB2PSI5IiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSIxMCI+DQogICAgICA8eCB2PSIxMCIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iMTEiPg0KICAgICAgPHggdj0iMTEiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjEyIj4NCiAgICAgIDx4IHY9IjEyIiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSIxMyI+DQogICAgICA8eCB2PSIxMyIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iMTQiPg0KICAgICAgPHggdj0iMTQiIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjE1Ij4NCiAgICAgIDx4IHY9IjE1IiAvPg0KICAgIDwvaT4NCiAgICA8aSBpPSIxNiI+DQogICAgICA8eCB2PSIxNiIgLz4NCiAgICA8L2k+DQogICAgPGkgaT0iMTciPg0KICAgICAgPHggdj0iMTciIC8+DQogICAgPC9pPg0KICAgIDxpIGk9IjE4Ij4NCiAgICAgIDx4IHY9IjE4IiAvPg0KICAgIDwvaT4NCiAgPC9jb2xJdGVtcz4NCiAgPGRhdGFGaWVsZHMgY291bnQ9IjE5Ij4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IklubG9nZ2FkIHRpZCAiIGZsZD0iNiIgYmFzZUZpZWxkPSIwIiBiYXNlSXRlbT0iMSIgbnVtRm10SWQ9IjE2NSIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IkhhbnRlcmluZ3N0aWQgIiBmbGQ9IjUiIGJhc2VGaWVsZD0iMCIgYmFzZUl0ZW09IjEiIG51bUZtdElkPSIxNjUiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSIlICAiIGZsZD0iMTUiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjUiIG51bUZtdElkPSIxMCIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IlNhbXRhbHN0aWQgIiBmbGQ9IjciIGJhc2VGaWVsZD0iMCIgYmFzZUl0ZW09IjEiIG51bUZtdElkPSIxNjUiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSIlICAgICIgZmxkPSIxNyIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iNSIgbnVtRm10SWQ9IjEwIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iRWZ0ZXJiZWFyYmV0c3RpZCAiIGZsZD0iOCIgYmFzZUZpZWxkPSIwIiBiYXNlSXRlbT0iMSIgbnVtRm10SWQ9IjE2NSIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9IiUgICAgICIgZmxkPSIxOCIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iNSIgbnVtRm10SWQ9IjEwIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iVsOkbnRldGlkICIgZmxkPSI0IiBiYXNlRmllbGQ9IjAiIGJhc2VJdGVtPSIxIiBudW1GbXRJZD0iMTY1IiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iJSAgICIgZmxkPSIxNiIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iNSIgbnVtRm10SWQ9IjEwIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iT3ZlcmtzYW0gdGlkICIgZmxkPSIzIiBiYXNlRmllbGQ9IjAiIGJhc2VJdGVtPSIxIiBudW1GbXRJZD0iMTY1IiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iJSAiIGZsZD0iMTQiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjUiIG51bUZtdElkPSIxMCIgLz4NCiAgICA8ZGF0YUZpZWxkIG5hbWU9Ikdlbm9tc25pdHRsaWcgc2FtdGFsc3RpZCAiIGZsZD0iOSIgc3VidG90YWw9ImF2ZXJhZ2UiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjU5IiBudW1GbXRJZD0iMTY1IiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iL09yZGVyICIgZmxkPSIxMCIgc3VidG90YWw9ImF2ZXJhZ2UiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjU5IiBudW1GbXRJZD0iMTY1IiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iUmVnaXN0cmVyYWRlIHNhbXRhbCAiIGZsZD0iMTEiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjExIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iL2giIGZsZD0iMTkiIGJhc2VGaWVsZD0iMCIgYmFzZUl0ZW09IjAiIG51bUZtdElkPSIyIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iUmVnaXN0cmVyYWRlIHByb3NwZWt0ICIgZmxkPSIxMiIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iMTEiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSIvaCAiIGZsZD0iMjAiIGJhc2VGaWVsZD0iMSIgYmFzZUl0ZW09IjAiIG51bUZtdElkPSIyIiAvPg0KICAgIDxkYXRhRmllbGQgbmFtZT0iQXZicnV0aXQgc2FtdGFsc2JlZ8OkcmFuICIgZmxkPSIxMyIgYmFzZUZpZWxkPSIxIiBiYXNlSXRlbT0iMTEiIC8+DQogICAgPGRhdGFGaWVsZCBuYW1lPSIvaCAgIiBmbGQ9IjIxIiBiYXNlRmllbGQ9IjEiIGJhc2VJdGVtPSIwIiBudW1GbXRJZD0iMiIgLz4NCiAgPC9kYXRhRmllbGRzPg0KICA8Zm9ybWF0cyBjb3VudD0iNyI+DQogICAgPGZvcm1hdCBkeGZJZD0iNDEiPg0KICAgICAgPHBpdm90QXJlYSB0eXBlPSJhbGwiIGRhdGFPbmx5PSIwIiBvdXRsaW5lPSIwIiBmaWVsZFBvc2l0aW9uPSIwIiAvPg0KICAgIDwvZm9ybWF0Pg0KICAgIDxmb3JtYXQgZHhmSWQ9IjQwIj4NCiAgICAgIDxwaXZvdEFyZWEgb3V0bGluZT0iMCIgY29sbGFwc2VkTGV2ZWxzQXJlU3VidG90YWxzPSIxIiBmaWVsZFBvc2l0aW9uPSIwIiAvPg0KICAgIDwvZm9ybWF0Pg0KICAgIDxmb3JtYXQgZHhmSWQ9IjM5Ij4NCiAgICAgIDxwaXZvdEFyZWEgZmllbGQ9IjAiIHR5cGU9ImJ1dHRvbiIgZGF0YU9ubHk9IjAiIGxhYmVsT25seT0iMSIgb3V0bGluZT0iMCIgYXhpcz0iYXhpc1JvdyIgZmllbGRQb3NpdGlvbj0iMCIgLz4NCiAgICA8L2Zvcm1hdD4NCiAgICA8Zm9ybWF0IGR4ZklkPSIzOCI+DQogICAgICA8cGl2b3RBcmVhIGRhdGFPbmx5PSIwIiBsYWJlbE9ubHk9IjEiIGZpZWxkUG9zaXRpb249IjAiPg0KICAgICAgICA8cmVmZXJlbmNlcyBjb3VudD0iMSI+DQogICAgICAgICAgPHJlZmVyZW5jZSBmaWVsZD0iMCIgY291bnQ9IjAiIC8+DQogICAgICAgIDwvcmVmZXJlbmNlcz4NCiAgICAgIDwvcGl2b3RBcmVhPg0KICAgIDwvZm9ybWF0Pg0KICAgIDxmb3JtYXQgZHhmSWQ9IjM3Ij4NCiAgICAgIDxwaXZvdEFyZWEgZGF0YU9ubHk9IjAiIGxhYmVsT25seT0iMSIgZ3JhbmRSb3c9IjEiIG91dGxpbmU9IjAiIGZpZWxkUG9zaXRpb249IjAiIC8+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMzYiPg0KICAgICAgPHBpdm90QXJlYSBkYXRhT25seT0iMCIgbGFiZWxPbmx5PSIxIiBmaWVsZFBvc2l0aW9uPSIwIj4NCiAgICAgICAgPHJlZmVyZW5jZXMgY291bnQ9IjIiPg0KICAgICAgICAgIDxyZWZlcmVuY2UgZmllbGQ9IjAiIGNvdW50PSIwIiBzZWxlY3RlZD0iMCIgLz4NCiAgICAgICAgICA8cmVmZXJlbmNlIGZpZWxkPSIxIiBjb3VudD0iMCIgLz4NCiAgICAgICAgPC9yZWZlcmVuY2VzPg0KICAgICAgPC9waXZvdEFyZWE+DQogICAgPC9mb3JtYXQ+DQogICAgPGZvcm1hdCBkeGZJZD0iMzUiPg0KICAgICAgPHBpdm90QXJlYSBkYXRhT25seT0iMCIgbGFiZWxPbmx5PSIxIiBvdXRsaW5lPSIwIiBmaWVsZFBvc2l0aW9uPSIwIj4NCiAgICAgICAgPHJlZmVyZW5jZXMgY291bnQ9IjEiPg0KICAgICAgICAgIDxyZWZlcmVuY2UgZmllbGQ9IjQyOTQ5NjcyOTQiIGNvdW50PSIxOSI+DQogICAgICAgICAgICA8eCB2PSIwIiAvPg0KICAgICAgICAgICAgPHggdj0iMSIgLz4NCiAgICAgICAgICAgIDx4IHY9IjIiIC8+DQogICAgICAgICAgICA8eCB2PSIzIiAvPg0KICAgICAgICAgICAgPHggdj0iNCIgLz4NCiAgICAgICAgICAgIDx4IHY9IjUiIC8+DQogICAgICAgICAgICA8eCB2PSI2IiAvPg0KICAgICAgICAgICAgPHggdj0iNyIgLz4NCiAgICAgICAgICAgIDx4IHY9IjgiIC8+DQogICAgICAgICAgICA8eCB2PSI5IiAvPg0KICAgICAgICAgICAgPHggdj0iMTAiIC8+DQogICAgICAgICAgICA8eCB2PSIxMSIgLz4NCiAgICAgICAgICAgIDx4IHY9IjEyIiAvPg0KICAgICAgICAgICAgPHggdj0iMTMiIC8+DQogICAgICAgICAgICA8eCB2PSIxNCIgLz4NCiAgICAgICAgICAgIDx4IHY9IjE1IiAvPg0KICAgICAgICAgICAgPHggdj0iMTYiIC8+DQogICAgICAgICAgICA8eCB2PSIxNyIgLz4NCiAgICAgICAgICAgIDx4IHY9IjE4IiAvPg0KICAgICAgICAgIDwvcmVmZXJlbmNlPg0KICAgICAgICA8L3JlZmVyZW5jZXM+DQogICAgICA8L3Bpdm90QXJlYT4NCiAgICA8L2Zvcm1hdD4NCiAgPC9mb3JtYXRzPg0KICA8cGl2b3RUYWJsZVN0eWxlSW5mbyBuYW1lPSJQaXZvdFN0eWxlTWVkaXVtNiIgc2hvd1Jvd0hlYWRlcnM9IjEiIHNob3dDb2xIZWFkZXJzPSIxIiBzaG93Um93U3RyaXBlcz0iMCIgc2hvd0NvbFN0cmlwZXM9IjAiIHNob3dMYXN0Q29sdW1uPSIxIiAvPg0KICA8ZXh0THN0Pg0KICAgIDxleHQgdXJpPSJ7OTYyRUY1RDEtNUNBMi00YzkzLThFRjQtREJGNUMwNTQzOUQyfSIgeG1sbnM6eDE0PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIj4NCiAgICAgIDx4MTQ6cGl2b3RUYWJsZURlZmluaXRpb24gaGlkZVZhbHVlc1Jvdz0iMSIgeG1sbnM6eG09Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2V4Y2VsLzIwMDYvbWFpbiIgLz4NCiAgICA8L2V4dD4NCiAgPC9leHRMc3Q+DQo8L3Bpdm90VGFibGVEZWZpbml0aW9uPlBLAwQUAAAAAAAAACEATZmg9kUBAABFAQAAKQAAAHhsL3Bpdm90VGFibGVzL19yZWxzL3Bpdm90VGFibGUxLnhtbC5yZWxzPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxSZWxhdGlvbnNoaXBzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L3JlbGF0aW9uc2hpcHMiPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDEiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvcGl2b3RDYWNoZURlZmluaXRpb24iIFRhcmdldD0iLi4vcGl2b3RDYWNoZS9waXZvdENhY2hlRGVmaW5pdGlvbjEueG1sIi8+PC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAAAAACEARrcow14EAABeBAAAGAAAAHhsL3dvcmtzaGVldHMvc2hlZXQxLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz48d29ya3NoZWV0IHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iPjxkaW1lbnNpb24gcmVmPSJBMTpOMSIvPjxzaGVldERhdGE+PHJvdyByPSIxIj48YyByPSJBMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+UHJvamVrdDwvdD48L2lzPjwvYz48YyByPSJCMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+U8OkbGphcmU8L3Q+PC9pcz48L2M+PGMgcj0iQzEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PkRhdHVtPC90PjwvaXM+PC9jPjxjIHI9IkQxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5PdmVya3NhbSB0aWQ8L3Q+PC9pcz48L2M+PGMgcj0iRTEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0PlbDpG50ZXRpZDwvdD48L2lzPjwvYz48YyByPSJGMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+SGFudGVyaW5nc3RpZDwvdD48L2lzPjwvYz48YyByPSJHMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+SW5sb2dnYWQgdGlkPC90PjwvaXM+PC9jPjxjIHI9IkgxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5TYW10YWxzdGlkPC90PjwvaXM+PC9jPjxjIHI9IkkxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5FZnRlcmJlYXJiZXRzdGlkPC90PjwvaXM+PC9jPjxjIHI9IkoxIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5HZW5vbXNuaXR0bGlnIHNhbXRhbHN0aWQ8L3Q+PC9pcz48L2M+PGMgcj0iSzEiIHM9IjMiIHQ9ImlubGluZVN0ciI+PGlzPjx0Pkdlbm9tc25pdHRsaWcgc2FtdGFsc3RpZC9PcmRlcjwvdD48L2lzPjwvYz48YyByPSJMMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+UmVnaXN0cmVyYWRlIHNhbXRhbDwvdD48L2lzPjwvYz48YyByPSJNMSIgcz0iMyIgdD0iaW5saW5lU3RyIj48aXM+PHQ+UmVnaXN0cmVyYWRlIHByb3NwZWt0PC90PjwvaXM+PC9jPjxjIHI9Ik4xIiBzPSIzIiB0PSJpbmxpbmVTdHIiPjxpcz48dD5BdmJydXRpdCBzYW10YWxzYmVnw6RyYW48L3Q+PC9pcz48L2M+PC9yb3c+PC9zaGVldERhdGE+PC93b3Jrc2hlZXQ+UEsDBBQAAAAAAAAAIQAuDoH1fUsAAH1LAAANAAAAeGwvc3R5bGVzLnhtbO+7vzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxzdHlsZVNoZWV0IHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgbWM6SWdub3JhYmxlPSJ4MTRhYyIgeG1sbnM6eDE0YWM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L2FjIj4NCiAgPG51bUZtdHMgY291bnQ9IjIiPg0KICAgIDxudW1GbXQgbnVtRm10SWQ9IjE2NCIgZm9ybWF0Q29kZT0iWyQtRjQwMF1oOm1tOnNzXCBBTS9QTSIgLz4NCiAgICA8bnVtRm10IG51bUZtdElkPSIxNjUiIGZvcm1hdENvZGU9IltoXTptbTpzcztAIiAvPg0KICA8L251bUZtdHM+DQogIDxmb250cyBjb3VudD0iMiI+DQogICAgPGZvbnQ+DQogICAgICA8c3ogdmFsPSIxMSIgLz4NCiAgICAgIDxjb2xvciB0aGVtZT0iMSIgLz4NCiAgICAgIDxuYW1lIHZhbD0iQ2FsaWJyaSIgLz4NCiAgICAgIDxmYW1pbHkgdmFsPSIyIiAvPg0KICAgICAgPHNjaGVtZSB2YWw9Im1pbm9yIiAvPg0KICAgIDwvZm9udD4NCiAgICA8Zm9udD4NCiAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPGNvbG9yIHRoZW1lPSIxIiAvPg0KICAgICAgPG5hbWUgdmFsPSJDYWxpYnJpIiAvPg0KICAgICAgPGZhbWlseSB2YWw9IjIiIC8+DQogICAgICA8c2NoZW1lIHZhbD0ibWlub3IiIC8+DQogICAgPC9mb250Pg0KICA8L2ZvbnRzPg0KICA8ZmlsbHMgY291bnQ9IjMiPg0KICAgIDxmaWxsPg0KICAgICAgPHBhdHRlcm5GaWxsIHBhdHRlcm5UeXBlPSJub25lIiAvPg0KICAgIDwvZmlsbD4NCiAgICA8ZmlsbD4NCiAgICAgIDxwYXR0ZXJuRmlsbCBwYXR0ZXJuVHlwZT0iZ3JheTEyNSIgLz4NCiAgICA8L2ZpbGw+DQogICAgPGZpbGw+DQogICAgICA8cGF0dGVybkZpbGwgcGF0dGVyblR5cGU9InNvbGlkIj4NCiAgICAgICAgPGZnQ29sb3IgdGhlbWU9IjgiIHRpbnQ9Ii0wLjI0OTk3NzExMTExNzg5MyIgLz4NCiAgICAgICAgPGJnQ29sb3IgaW5kZXhlZD0iNjQiIC8+DQogICAgICA8L3BhdHRlcm5GaWxsPg0KICAgIDwvZmlsbD4NCiAgPC9maWxscz4NCiAgPGJvcmRlcnMgY291bnQ9IjEiPg0KICAgIDxib3JkZXI+DQogICAgICA8bGVmdCAvPg0KICAgICAgPHJpZ2h0IC8+DQogICAgICA8dG9wIC8+DQogICAgICA8Ym90dG9tIC8+DQogICAgICA8ZGlhZ29uYWwgLz4NCiAgICA8L2JvcmRlcj4NCiAgPC9ib3JkZXJzPg0KICA8Y2VsbFN0eWxlWGZzIGNvdW50PSIxIj4NCiAgICA8eGYgbnVtRm10SWQ9IjAiIGZvbnRJZD0iMCIgZmlsbElkPSIwIiBib3JkZXJJZD0iMCIgLz4NCiAgPC9jZWxsU3R5bGVYZnM+DQogIDxjZWxsWGZzIGNvdW50PSIxNCI+DQogICAgPHhmIG51bUZtdElkPSIwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIwIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIxNjQiIGFwcGx5TnVtYmVyRm9ybWF0PSIxIiBmb250SWQ9IjAiIGFwcGx5Rm9udD0iMSIgZmlsbElkPSIwIiBhcHBseUZpbGw9IjEiIGJvcmRlcklkPSIwIiBhcHBseUJvcmRlcj0iMSIgeGZJZD0iMCIgLz4NCiAgICA8eGYgbnVtRm10SWQ9IjIiIGFwcGx5TnVtYmVyRm9ybWF0PSIxIiBmb250SWQ9IjAiIGFwcGx5Rm9udD0iMSIgZmlsbElkPSIwIiBhcHBseUZpbGw9IjEiIGJvcmRlcklkPSIwIiBhcHBseUJvcmRlcj0iMSIgeGZJZD0iMCIgLz4NCiAgICA8eGYgbnVtRm10SWQ9IjAiIGFwcGx5TnVtYmVyRm9ybWF0PSIxIiBmb250SWQ9IjAiIGFwcGx5Rm9udD0iMSIgZmlsbElkPSIwIiBhcHBseUZpbGw9IjEiIGJvcmRlcklkPSIwIiBhcHBseUJvcmRlcj0iMSIgeGZJZD0iMCIgLz4NCiAgICA8eGYgbnVtRm10SWQ9IjE0IiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIwIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiPg0KICAgICAgPGFsaWdubWVudCBob3Jpem9udGFsPSJsZWZ0IiAvPg0KICAgIDwveGY+DQogICAgPHhmIG51bUZtdElkPSIxNjUiIGFwcGx5TnVtYmVyRm9ybWF0PSIxIiBmb250SWQ9IjEiIGFwcGx5Rm9udD0iMSIgZmlsbElkPSIwIiBhcHBseUZpbGw9IjEiIGJvcmRlcklkPSIwIiBhcHBseUJvcmRlcj0iMSIgeGZJZD0iMCIgLz4NCiAgICA8eGYgbnVtRm10SWQ9IjEwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIyIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiIC8+DQogICAgPHhmIG51bUZtdElkPSIwIiBhcHBseU51bWJlckZvcm1hdD0iMSIgZm9udElkPSIxIiBhcHBseUZvbnQ9IjEiIGZpbGxJZD0iMCIgYXBwbHlGaWxsPSIxIiBib3JkZXJJZD0iMCIgYXBwbHlCb3JkZXI9IjEiIHhmSWQ9IjAiPg0KICAgICAgPGFsaWdubWVudCBob3Jpem9udGFsPSJsZWZ0IiBpbmRlbnQ9IjEiIC8+DQogICAgPC94Zj4NCiAgICA8eGYgbnVtRm10SWQ9IjAiIGFwcGx5TnVtYmVyRm9ybWF0PSIxIiBmb250SWQ9IjEiIGFwcGx5Rm9udD0iMSIgZmlsbElkPSIyIiBhcHBseUZpbGw9IjEiIGJvcmRlcklkPSIwIiBhcHBseUJvcmRlcj0iMSIgeGZJZD0iMCIgLz4NCiAgPC9jZWxsWGZzPg0KICA8Y2VsbFN0eWxlcyBjb3VudD0iMSI+DQogICAgPGNlbGxTdHlsZSBuYW1lPSJOb3JtYWwiIHhmSWQ9IjAiIGJ1aWx0aW5JZD0iMCIgLz4NCiAgPC9jZWxsU3R5bGVzPg0KICA8ZHhmcyBjb3VudD0iNTYiPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8c3ogdmFsPSIxMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxzeiB2YWw9IjEwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPGIgLz4NCiAgICAgICAgPGkgLz4NCiAgICAgICAgPGNvbG9yIGluZGV4ZWQ9IjYzIiAvPg0KICAgICAgICA8bmFtZSB2YWw9IkNhbWJyaWEiIC8+DQogICAgICAgIDxzY2hlbWUgdmFsPSJub25lIiAvPg0KICAgICAgPC9mb250Pg0KICAgICAgPGJvcmRlcj4NCiAgICAgICAgPHRvcCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgPGNvbG9yIGluZGV4ZWQ9IjY0IiAvPg0KICAgICAgICA8L3RvcD4NCiAgICAgIDwvYm9yZGVyPg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPGIgLz4NCiAgICAgICAgPGkgdmFsPSIwIiAvPg0KICAgICAgICA8Y29sb3IgaW5kZXhlZD0iOSIgLz4NCiAgICAgICAgPG5hbWUgdmFsPSJDYW1icmlhIiAvPg0KICAgICAgICA8c2NoZW1lIHZhbD0ibm9uZSIgLz4NCiAgICAgIDwvZm9udD4NCiAgICAgIDxmaWxsPg0KICAgICAgICA8cGF0dGVybkZpbGw+DQogICAgICAgICAgPGJnQ29sb3IgaW5kZXhlZD0iMzAiIC8+DQogICAgICAgIDwvcGF0dGVybkZpbGw+DQogICAgICA8L2ZpbGw+DQogICAgICA8Ym9yZGVyPg0KICAgICAgICA8Ym90dG9tIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICA8Y29sb3IgaW5kZXhlZD0iNjQiIC8+DQogICAgICAgIDwvYm90dG9tPg0KICAgICAgPC9ib3JkZXI+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8YiAvPg0KICAgICAgICA8aSB2YWw9IjAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8YiAvPg0KICAgICAgICA8aSAvPg0KICAgICAgICA8Y29sb3IgaW5kZXhlZD0iNjMiIC8+DQogICAgICAgIDxuYW1lIHZhbD0iQ2FtYnJpYSIgLz4NCiAgICAgICAgPHNjaGVtZSB2YWw9Im5vbmUiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgICA8Ym9yZGVyPg0KICAgICAgICA8dG9wIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICA8Y29sb3IgaW5kZXhlZD0iNjQiIC8+DQogICAgICAgIDwvdG9wPg0KICAgICAgPC9ib3JkZXI+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8YiAvPg0KICAgICAgICA8aSB2YWw9IjAiIC8+DQogICAgICAgIDxjb2xvciBpbmRleGVkPSI5IiAvPg0KICAgICAgICA8bmFtZSB2YWw9IkNhbWJyaWEiIC8+DQogICAgICAgIDxzY2hlbWUgdmFsPSJub25lIiAvPg0KICAgICAgPC9mb250Pg0KICAgICAgPGZpbGw+DQogICAgICAgIDxwYXR0ZXJuRmlsbD4NCiAgICAgICAgICA8YmdDb2xvciBpbmRleGVkPSIzMCIgLz4NCiAgICAgICAgPC9wYXR0ZXJuRmlsbD4NCiAgICAgIDwvZmlsbD4NCiAgICAgIDxib3JkZXI+DQogICAgICAgIDxib3R0b20gc3R5bGU9InRoaW4iPg0KICAgICAgICAgIDxjb2xvciBpbmRleGVkPSI2NCIgLz4NCiAgICAgICAgPC9ib3R0b20+DQogICAgICA8L2JvcmRlcj4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxiIC8+DQogICAgICAgIDxpIHZhbD0iMCIgLz4NCiAgICAgIDwvZm9udD4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxiIC8+DQogICAgICAgIDxpIC8+DQogICAgICAgIDxjb2xvciBpbmRleGVkPSI2MyIgLz4NCiAgICAgICAgPG5hbWUgdmFsPSJDYW1icmlhIiAvPg0KICAgICAgICA8c2NoZW1lIHZhbD0ibm9uZSIgLz4NCiAgICAgIDwvZm9udD4NCiAgICAgIDxib3JkZXI+DQogICAgICAgIDx0b3Agc3R5bGU9InRoaW4iPg0KICAgICAgICAgIDxjb2xvciBpbmRleGVkPSI2NCIgLz4NCiAgICAgICAgPC90b3A+DQogICAgICA8L2JvcmRlcj4NCiAgICA8L2R4Zj4NCiAgICA8ZHhmPg0KICAgICAgPGZvbnQ+DQogICAgICAgIDxiIC8+DQogICAgICAgIDxpIHZhbD0iMCIgLz4NCiAgICAgICAgPGNvbG9yIGluZGV4ZWQ9IjkiIC8+DQogICAgICAgIDxuYW1lIHZhbD0iQ2FtYnJpYSIgLz4NCiAgICAgICAgPHNjaGVtZSB2YWw9Im5vbmUiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgICA8ZmlsbD4NCiAgICAgICAgPHBhdHRlcm5GaWxsPg0KICAgICAgICAgIDxiZ0NvbG9yIGluZGV4ZWQ9IjMwIiAvPg0KICAgICAgICA8L3BhdHRlcm5GaWxsPg0KICAgICAgPC9maWxsPg0KICAgICAgPGJvcmRlcj4NCiAgICAgICAgPGJvdHRvbSBzdHlsZT0idGhpbiI+DQogICAgICAgICAgPGNvbG9yIGluZGV4ZWQ9IjY0IiAvPg0KICAgICAgICA8L2JvdHRvbT4NCiAgICAgIDwvYm9yZGVyPg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPGIgLz4NCiAgICAgICAgPGkgdmFsPSIwIiAvPg0KICAgICAgPC9mb250Pg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPGIgLz4NCiAgICAgICAgPGkgLz4NCiAgICAgICAgPGNvbG9yIGluZGV4ZWQ9IjYzIiAvPg0KICAgICAgICA8bmFtZSB2YWw9IkNhbWJyaWEiIC8+DQogICAgICAgIDxzY2hlbWUgdmFsPSJub25lIiAvPg0KICAgICAgPC9mb250Pg0KICAgICAgPGJvcmRlcj4NCiAgICAgICAgPHRvcCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgPGNvbG9yIGluZGV4ZWQ9IjY0IiAvPg0KICAgICAgICA8L3RvcD4NCiAgICAgIDwvYm9yZGVyPg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPGIgLz4NCiAgICAgICAgPGkgdmFsPSIwIiAvPg0KICAgICAgICA8Y29sb3IgaW5kZXhlZD0iOSIgLz4NCiAgICAgICAgPG5hbWUgdmFsPSJDYW1icmlhIiAvPg0KICAgICAgICA8c2NoZW1lIHZhbD0ibm9uZSIgLz4NCiAgICAgIDwvZm9udD4NCiAgICAgIDxmaWxsPg0KICAgICAgICA8cGF0dGVybkZpbGw+DQogICAgICAgICAgPGJnQ29sb3IgaW5kZXhlZD0iMzAiIC8+DQogICAgICAgIDwvcGF0dGVybkZpbGw+DQogICAgICA8L2ZpbGw+DQogICAgICA8Ym9yZGVyPg0KICAgICAgICA8Ym90dG9tIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICA8Y29sb3IgaW5kZXhlZD0iNjQiIC8+DQogICAgICAgIDwvYm90dG9tPg0KICAgICAgPC9ib3JkZXI+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8YiAvPg0KICAgICAgICA8aSB2YWw9IjAiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgPC9keGY+DQogICAgPGR4Zj4NCiAgICAgIDxmb250Pg0KICAgICAgICA8YiAvPg0KICAgICAgICA8Y29sb3IgdGhlbWU9IjEiIC8+DQogICAgICA8L2ZvbnQ+DQogICAgICA8Ym9yZGVyPg0KICAgICAgICA8Ym90dG9tIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICA8Y29sb3IgdGhlbWU9IjQiIC8+DQogICAgICAgIDwvYm90dG9tPg0KICAgICAgICA8dmVydGljYWwgLz4NCiAgICAgICAgPGhvcml6b250YWwgLz4NCiAgICAgIDwvYm9yZGVyPg0KICAgIDwvZHhmPg0KICAgIDxkeGY+DQogICAgICA8Zm9udD4NCiAgICAgICAgPHN6IHZhbD0iMTAiIC8+DQogICAgICAgIDxjb2xvciB0aGVtZT0iMSIgLz4NCiAgICAgIDwvZm9udD4NCiAgICAgIDxib3JkZXI+DQogICAgICAgIDxsZWZ0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICA8Y29sb3IgdGhlbWU9IjQiIC8+DQogICAgICAgIDwvbGVmdD4NCiAgICAgICAgPHJpZ2h0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICA8Y29sb3IgdGhlbWU9IjQiIC8+DQogICAgICAgIDwvcmlnaHQ+DQogICAgICAgIDx0b3Agc3R5bGU9InRoaW4iPg0KICAgICAgICAgIDxjb2xvciB0aGVtZT0iNCIgLz4NCiAgICAgICAgPC90b3A+DQogICAgICAgIDxib3R0b20gc3R5bGU9InRoaW4iPg0KICAgICAgICAgIDxjb2xvciB0aGVtZT0iNCIgLz4NCiAgICAgICAgPC9ib3R0b20+DQogICAgICAgIDx2ZXJ0aWNhbCAvPg0KICAgICAgICA8aG9yaXpvbnRhbCAvPg0KICAgICAgPC9ib3JkZXI+DQogICAgPC9keGY+DQogIDwvZHhmcz4NCiAgPHRhYmxlU3R5bGVzIGNvdW50PSIxIiBkZWZhdWx0VGFibGVTdHlsZT0iVGFibGVTdHlsZU1lZGl1bTIiIGRlZmF1bHRQaXZvdFN0eWxlPSJQaXZvdFN0eWxlTGlnaHQxNiI+DQogICAgPHRhYmxlU3R5bGUgbmFtZT0iRHJlbXJlcG9ydFNsaWNlciIgcGl2b3Q9IjAiIHRhYmxlPSIwIiBjb3VudD0iMTAiPg0KICAgICAgPHRhYmxlU3R5bGVFbGVtZW50IHR5cGU9Indob2xlVGFibGUiIGR4ZklkPSI1NSIgLz4NCiAgICAgIDx0YWJsZVN0eWxlRWxlbWVudCB0eXBlPSJoZWFkZXJSb3ciIGR4ZklkPSI1NCIgLz4NCiAgICA8L3RhYmxlU3R5bGU+DQogIDwvdGFibGVTdHlsZXM+DQogIDxleHRMc3Q+DQogICAgPGV4dCB1cmk9Ins0NkY0MjFDQS0zMTJGLTY4MmYtM0REMi02MTY3NTIxOUI0MkR9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPg0KICAgICAgPHgxNDpkeGZzIGNvdW50PSI4Ij4NCiAgICAgICAgPGR4Zj4NCiAgICAgICAgICA8Zm9udD4NCiAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGMDAwMDAwIiAvPg0KICAgICAgICAgIDwvZm9udD4NCiAgICAgICAgICA8ZmlsbD4NCiAgICAgICAgICAgIDxncmFkaWVudEZpbGwgZGVncmVlPSI5MCI+DQogICAgICAgICAgICAgIDxzdG9wIHBvc2l0aW9uPSIwIj4NCiAgICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkY4RTE2MiIgLz4NCiAgICAgICAgICAgICAgPC9zdG9wPg0KICAgICAgICAgICAgICA8c3RvcCBwb3NpdGlvbj0iMSI+DQogICAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZGQ0Y3RTAiIC8+DQogICAgICAgICAgICAgIDwvc3RvcD4NCiAgICAgICAgICAgIDwvZ3JhZGllbnRGaWxsPg0KICAgICAgICAgIDwvZmlsbD4NCiAgICAgICAgICA8Ym9yZGVyPg0KICAgICAgICAgICAgPGxlZnQgc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvbGVmdD4NCiAgICAgICAgICAgIDxyaWdodCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGOTk5OTk5IiAvPg0KICAgICAgICAgICAgPC9yaWdodD4NCiAgICAgICAgICAgIDx0b3Agc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvdG9wPg0KICAgICAgICAgICAgPGJvdHRvbSBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGOTk5OTk5IiAvPg0KICAgICAgICAgICAgPC9ib3R0b20+DQogICAgICAgICAgICA8dmVydGljYWwgLz4NCiAgICAgICAgICAgIDxob3Jpem9udGFsIC8+DQogICAgICAgICAgPC9ib3JkZXI+DQogICAgICAgIDwvZHhmPg0KICAgICAgICA8ZHhmPg0KICAgICAgICAgIDxmb250Pg0KICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkYwMDAwMDAiIC8+DQogICAgICAgICAgPC9mb250Pg0KICAgICAgICAgIDxmaWxsPg0KICAgICAgICAgICAgPGdyYWRpZW50RmlsbCBkZWdyZWU9IjkwIj4NCiAgICAgICAgICAgICAgPHN0b3AgcG9zaXRpb249IjAiPg0KICAgICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGRjhFMTYyIiAvPg0KICAgICAgICAgICAgICA8L3N0b3A+DQogICAgICAgICAgICAgIDxzdG9wIHBvc2l0aW9uPSIxIj4NCiAgICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkZDRjdFMCIgLz4NCiAgICAgICAgICAgICAgPC9zdG9wPg0KICAgICAgICAgICAgPC9ncmFkaWVudEZpbGw+DQogICAgICAgICAgPC9maWxsPg0KICAgICAgICAgIDxib3JkZXI+DQogICAgICAgICAgICA8bGVmdCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGOTk5OTk5IiAvPg0KICAgICAgICAgICAgPC9sZWZ0Pg0KICAgICAgICAgICAgPHJpZ2h0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY5OTk5OTkiIC8+DQogICAgICAgICAgICA8L3JpZ2h0Pg0KICAgICAgICAgICAgPHRvcCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGOTk5OTk5IiAvPg0KICAgICAgICAgICAgPC90b3A+DQogICAgICAgICAgICA8Ym90dG9tIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY5OTk5OTkiIC8+DQogICAgICAgICAgICA8L2JvdHRvbT4NCiAgICAgICAgICAgIDx2ZXJ0aWNhbCAvPg0KICAgICAgICAgICAgPGhvcml6b250YWwgLz4NCiAgICAgICAgICA8L2JvcmRlcj4NCiAgICAgICAgPC9keGY+DQogICAgICAgIDxkeGY+DQogICAgICAgICAgPGZvbnQ+DQogICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjAwMDAwMCIgLz4NCiAgICAgICAgICA8L2ZvbnQ+DQogICAgICAgICAgPGZpbGw+DQogICAgICAgICAgICA8Z3JhZGllbnRGaWxsIGRlZ3JlZT0iOTAiPg0KICAgICAgICAgICAgICA8c3RvcCBwb3NpdGlvbj0iMCI+DQogICAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZGOEUxNjIiIC8+DQogICAgICAgICAgICAgIDwvc3RvcD4NCiAgICAgICAgICAgICAgPHN0b3AgcG9zaXRpb249IjEiPg0KICAgICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGRkNGN0UwIiAvPg0KICAgICAgICAgICAgICA8L3N0b3A+DQogICAgICAgICAgICA8L2dyYWRpZW50RmlsbD4NCiAgICAgICAgICA8L2ZpbGw+DQogICAgICAgICAgPGJvcmRlcj4NCiAgICAgICAgICAgIDxsZWZ0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY5OTk5OTkiIC8+DQogICAgICAgICAgICA8L2xlZnQ+DQogICAgICAgICAgICA8cmlnaHQgc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvcmlnaHQ+DQogICAgICAgICAgICA8dG9wIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY5OTk5OTkiIC8+DQogICAgICAgICAgICA8L3RvcD4NCiAgICAgICAgICAgIDxib3R0b20gc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvYm90dG9tPg0KICAgICAgICAgICAgPHZlcnRpY2FsIC8+DQogICAgICAgICAgICA8aG9yaXpvbnRhbCAvPg0KICAgICAgICAgIDwvYm9yZGVyPg0KICAgICAgICA8L2R4Zj4NCiAgICAgICAgPGR4Zj4NCiAgICAgICAgICA8Zm9udD4NCiAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGMDAwMDAwIiAvPg0KICAgICAgICAgIDwvZm9udD4NCiAgICAgICAgICA8ZmlsbD4NCiAgICAgICAgICAgIDxncmFkaWVudEZpbGwgZGVncmVlPSI5MCI+DQogICAgICAgICAgICAgIDxzdG9wIHBvc2l0aW9uPSIwIj4NCiAgICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkY4RTE2MiIgLz4NCiAgICAgICAgICAgICAgPC9zdG9wPg0KICAgICAgICAgICAgICA8c3RvcCBwb3NpdGlvbj0iMSI+DQogICAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZGQ0Y3RTAiIC8+DQogICAgICAgICAgICAgIDwvc3RvcD4NCiAgICAgICAgICAgIDwvZ3JhZGllbnRGaWxsPg0KICAgICAgICAgIDwvZmlsbD4NCiAgICAgICAgICA8Ym9yZGVyPg0KICAgICAgICAgICAgPGxlZnQgc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvbGVmdD4NCiAgICAgICAgICAgIDxyaWdodCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGOTk5OTk5IiAvPg0KICAgICAgICAgICAgPC9yaWdodD4NCiAgICAgICAgICAgIDx0b3Agc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvdG9wPg0KICAgICAgICAgICAgPGJvdHRvbSBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGOTk5OTk5IiAvPg0KICAgICAgICAgICAgPC9ib3R0b20+DQogICAgICAgICAgICA8dmVydGljYWwgLz4NCiAgICAgICAgICAgIDxob3Jpem9udGFsIC8+DQogICAgICAgICAgPC9ib3JkZXI+DQogICAgICAgIDwvZHhmPg0KICAgICAgICA8ZHhmPg0KICAgICAgICAgIDxmb250Pg0KICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY4MjgyODIiIC8+DQogICAgICAgICAgPC9mb250Pg0KICAgICAgICAgIDxmaWxsPg0KICAgICAgICAgICAgPHBhdHRlcm5GaWxsIHBhdHRlcm5UeXBlPSJzb2xpZCI+DQogICAgICAgICAgICAgIDxmZ0NvbG9yIHRoZW1lPSI0IiB0aW50PSIwLjc5OTk4MTY4ODg5NDMxNDQyIiAvPg0KICAgICAgICAgICAgICA8YmdDb2xvciB0aGVtZT0iNCIgdGludD0iMC43OTk5ODE2ODg4OTQzMTQ0MiIgLz4NCiAgICAgICAgICAgIDwvcGF0dGVybkZpbGw+DQogICAgICAgICAgPC9maWxsPg0KICAgICAgICAgIDxib3JkZXI+DQogICAgICAgICAgICA8bGVmdCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGQ0NDQ0NDIiAvPg0KICAgICAgICAgICAgPC9sZWZ0Pg0KICAgICAgICAgICAgPHJpZ2h0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZDQ0NDQ0MiIC8+DQogICAgICAgICAgICA8L3JpZ2h0Pg0KICAgICAgICAgICAgPHRvcCBzdHlsZT0idGhpbiI+DQogICAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGQ0NDQ0NDIiAvPg0KICAgICAgICAgICAgPC90b3A+DQogICAgICAgICAgICA8Ym90dG9tIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZDQ0NDQ0MiIC8+DQogICAgICAgICAgICA8L2JvdHRvbT4NCiAgICAgICAgICAgIDx2ZXJ0aWNhbCAvPg0KICAgICAgICAgICAgPGhvcml6b250YWwgLz4NCiAgICAgICAgICA8L2JvcmRlcj4NCiAgICAgICAgPC9keGY+DQogICAgICAgIDxkeGY+DQogICAgICAgICAgPGZvbnQ+DQogICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjAwMDAwMCIgLz4NCiAgICAgICAgICA8L2ZvbnQ+DQogICAgICAgICAgPGZpbGw+DQogICAgICAgICAgICA8cGF0dGVybkZpbGwgcGF0dGVyblR5cGU9InNvbGlkIj4NCiAgICAgICAgICAgICAgPGZnQ29sb3IgdGhlbWU9IjQiIHRpbnQ9IjAuNTk5OTkzODk2Mjk4MTA0ODUiIC8+DQogICAgICAgICAgICAgIDxiZ0NvbG9yIHRoZW1lPSI0IiB0aW50PSIwLjU5OTk5Mzg5NjI5ODEwNDg1IiAvPg0KICAgICAgICAgICAgPC9wYXR0ZXJuRmlsbD4NCiAgICAgICAgICA8L2ZpbGw+DQogICAgICAgICAgPGJvcmRlcj4NCiAgICAgICAgICAgIDxsZWZ0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY5OTk5OTkiIC8+DQogICAgICAgICAgICA8L2xlZnQ+DQogICAgICAgICAgICA8cmlnaHQgc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvcmlnaHQ+DQogICAgICAgICAgICA8dG9wIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkY5OTk5OTkiIC8+DQogICAgICAgICAgICA8L3RvcD4NCiAgICAgICAgICAgIDxib3R0b20gc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRjk5OTk5OSIgLz4NCiAgICAgICAgICAgIDwvYm90dG9tPg0KICAgICAgICAgICAgPHZlcnRpY2FsIC8+DQogICAgICAgICAgICA8aG9yaXpvbnRhbCAvPg0KICAgICAgICAgIDwvYm9yZGVyPg0KICAgICAgICA8L2R4Zj4NCiAgICAgICAgPGR4Zj4NCiAgICAgICAgICA8Zm9udD4NCiAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGODI4MjgyIiAvPg0KICAgICAgICAgIDwvZm9udD4NCiAgICAgICAgICA8ZmlsbD4NCiAgICAgICAgICAgIDxwYXR0ZXJuRmlsbCBwYXR0ZXJuVHlwZT0ic29saWQiPg0KICAgICAgICAgICAgICA8ZmdDb2xvciByZ2I9IkZGRkZGRkZGIiAvPg0KICAgICAgICAgICAgICA8YmdDb2xvciByZ2I9IkZGRkZGRkZGIiAvPg0KICAgICAgICAgICAgPC9wYXR0ZXJuRmlsbD4NCiAgICAgICAgICA8L2ZpbGw+DQogICAgICAgICAgPGJvcmRlcj4NCiAgICAgICAgICAgIDxsZWZ0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZFMEUwRTAiIC8+DQogICAgICAgICAgICA8L2xlZnQ+DQogICAgICAgICAgICA8cmlnaHQgc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkUwRTBFMCIgLz4NCiAgICAgICAgICAgIDwvcmlnaHQ+DQogICAgICAgICAgICA8dG9wIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZFMEUwRTAiIC8+DQogICAgICAgICAgICA8L3RvcD4NCiAgICAgICAgICAgIDxib3R0b20gc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkUwRTBFMCIgLz4NCiAgICAgICAgICAgIDwvYm90dG9tPg0KICAgICAgICAgICAgPHZlcnRpY2FsIC8+DQogICAgICAgICAgICA8aG9yaXpvbnRhbCAvPg0KICAgICAgICAgIDwvYm9yZGVyPg0KICAgICAgICA8L2R4Zj4NCiAgICAgICAgPGR4Zj4NCiAgICAgICAgICA8Zm9udD4NCiAgICAgICAgICAgIDxjb2xvciByZ2I9IkZGMDAwMDAwIiAvPg0KICAgICAgICAgIDwvZm9udD4NCiAgICAgICAgICA8ZmlsbD4NCiAgICAgICAgICAgIDxwYXR0ZXJuRmlsbCBwYXR0ZXJuVHlwZT0ic29saWQiPg0KICAgICAgICAgICAgICA8ZmdDb2xvciByZ2I9IkZGRkZGRkZGIiAvPg0KICAgICAgICAgICAgICA8YmdDb2xvciByZ2I9IkZGRkZGRkZGIiAvPg0KICAgICAgICAgICAgPC9wYXR0ZXJuRmlsbD4NCiAgICAgICAgICA8L2ZpbGw+DQogICAgICAgICAgPGJvcmRlcj4NCiAgICAgICAgICAgIDxsZWZ0IHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZDQ0NDQ0MiIC8+DQogICAgICAgICAgICA8L2xlZnQ+DQogICAgICAgICAgICA8cmlnaHQgc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkNDQ0NDQyIgLz4NCiAgICAgICAgICAgIDwvcmlnaHQ+DQogICAgICAgICAgICA8dG9wIHN0eWxlPSJ0aGluIj4NCiAgICAgICAgICAgICAgPGNvbG9yIHJnYj0iRkZDQ0NDQ0MiIC8+DQogICAgICAgICAgICA8L3RvcD4NCiAgICAgICAgICAgIDxib3R0b20gc3R5bGU9InRoaW4iPg0KICAgICAgICAgICAgICA8Y29sb3IgcmdiPSJGRkNDQ0NDQyIgLz4NCiAgICAgICAgICAgIDwvYm90dG9tPg0KICAgICAgICAgICAgPHZlcnRpY2FsIC8+DQogICAgICAgICAgICA8aG9yaXpvbnRhbCAvPg0KICAgICAgICAgIDwvYm9yZGVyPg0KICAgICAgICA8L2R4Zj4NCiAgICAgIDwveDE0OmR4ZnM+DQogICAgPC9leHQ+DQogICAgPGV4dCB1cmk9IntFQjc5REVGMi04MEI4LTQzZTUtOTVCRC01NENCRERGOTAyMEN9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPg0KICAgICAgPHgxNDpzbGljZXJTdHlsZXMgZGVmYXVsdFNsaWNlclN0eWxlPSJTbGljZXJTdHlsZUxpZ2h0MSI+DQogICAgICAgIDx4MTQ6c2xpY2VyU3R5bGUgbmFtZT0iRHJlbXJlcG9ydFNsaWNlciI+DQogICAgICAgICAgPHgxNDpzbGljZXJTdHlsZUVsZW1lbnRzPg0KICAgICAgICAgICAgPHgxNDpzbGljZXJTdHlsZUVsZW1lbnQgdHlwZT0idW5zZWxlY3RlZEl0ZW1XaXRoRGF0YSIgZHhmSWQ9IjciIC8+DQogICAgICAgICAgICA8eDE0OnNsaWNlclN0eWxlRWxlbWVudCB0eXBlPSJ1bnNlbGVjdGVkSXRlbVdpdGhOb0RhdGEiIGR4ZklkPSI2IiAvPg0KICAgICAgICAgICAgPHgxNDpzbGljZXJTdHlsZUVsZW1lbnQgdHlwZT0ic2VsZWN0ZWRJdGVtV2l0aERhdGEiIGR4ZklkPSI1IiAvPg0KICAgICAgICAgICAgPHgxNDpzbGljZXJTdHlsZUVsZW1lbnQgdHlwZT0ic2VsZWN0ZWRJdGVtV2l0aE5vRGF0YSIgZHhmSWQ9IjQiIC8+DQogICAgICAgICAgICA8eDE0OnNsaWNlclN0eWxlRWxlbWVudCB0eXBlPSJob3ZlcmVkVW5zZWxlY3RlZEl0ZW1XaXRoRGF0YSIgZHhmSWQ9IjMiIC8+DQogICAgICAgICAgICA8eDE0OnNsaWNlclN0eWxlRWxlbWVudCB0eXBlPSJob3ZlcmVkU2VsZWN0ZWRJdGVtV2l0aERhdGEiIGR4ZklkPSIyIiAvPg0KICAgICAgICAgICAgPHgxNDpzbGljZXJTdHlsZUVsZW1lbnQgdHlwZT0iaG92ZXJlZFVuc2VsZWN0ZWRJdGVtV2l0aE5vRGF0YSIgZHhmSWQ9IjEiIC8+DQogICAgICAgICAgICA8eDE0OnNsaWNlclN0eWxlRWxlbWVudCB0eXBlPSJob3ZlcmVkU2VsZWN0ZWRJdGVtV2l0aE5vRGF0YSIgZHhmSWQ9IjAiIC8+DQogICAgICAgICAgPC94MTQ6c2xpY2VyU3R5bGVFbGVtZW50cz4NCiAgICAgICAgPC94MTQ6c2xpY2VyU3R5bGU+DQogICAgICA8L3gxNDpzbGljZXJTdHlsZXM+DQogICAgPC9leHQ+DQogICAgPGV4dCB1cmk9Ins5MjYwQTUxMC1GMzAxLTQ2YTgtODYzNS1GNTEyRDY0QkU1RjV9IiB4bWxuczp4MTU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAxMC8xMS9tYWluIj4NCiAgICAgIDx4MTU6dGltZWxpbmVTdHlsZXMgZGVmYXVsdFRpbWVsaW5lU3R5bGU9IlRpbWVTbGljZXJTdHlsZUxpZ2h0MSIgLz4NCiAgICA8L2V4dD4NCiAgPC9leHRMc3Q+DQo8L3N0eWxlU2hlZXQ+UEsDBBQAAAAAAAAAIQD+u3FJ4gIAAOICAAAgAAAAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlMy54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8c2xpY2VyQ2FjaGVEZWZpbml0aW9uIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIiB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIG1jOklnbm9yYWJsZT0ieCIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIiBuYW1lPSJTbGljZXJfUHJvamVrdDEiIHNvdXJjZU5hbWU9IlByb2pla3QiPjxwaXZvdFRhYmxlcz48cGl2b3RUYWJsZSB0YWJJZD0iNSIgbmFtZT0iUGl2b3RUYWJsZTIiLz48L3Bpdm90VGFibGVzPjxkYXRhPjx0YWJ1bGFyIHBpdm90Q2FjaGVJZD0iNyIgc2hvd01pc3Npbmc9IjAiPjxpdGVtcyBjb3VudD0iMSI+PGkgeD0iMCIgcz0iMSIvPjwvaXRlbXM+PC90YWJ1bGFyPjwvZGF0YT48ZXh0THN0Pjx4OmV4dCB1cmk9Ins0NzA3MjJFMC1BQUNELTRDMTctOUNEQy0xN0VGNzY1REJDN0V9IiB4bWxuczp4MTU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAxMC8xMS9tYWluIj48eDE1OnNsaWNlckNhY2hlSGlkZUl0ZW1zV2l0aE5vRGF0YS8+PC94OmV4dD48L2V4dExzdD48L3NsaWNlckNhY2hlRGVmaW5pdGlvbj5QSwMEFAAAAAAAAAAhAJ+xbf3jAgAA4wIAACAAAAB4bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGUyLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxzbGljZXJDYWNoZURlZmluaXRpb24geG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iIHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgbWM6SWdub3JhYmxlPSJ4IiB4bWxuczp4PSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIG5hbWU9IlNsaWNlcl9Tw6RsamFyZSIgc291cmNlTmFtZT0iU8OkbGphcmUiPjxwaXZvdFRhYmxlcz48cGl2b3RUYWJsZSB0YWJJZD0iNCIgbmFtZT0iUGl2b3RUYWJsZTEiLz48L3Bpdm90VGFibGVzPjxkYXRhPjx0YWJ1bGFyIHBpdm90Q2FjaGVJZD0iNiIgc2hvd01pc3Npbmc9IjAiPjxpdGVtcyBjb3VudD0iMSI+PGkgeD0iMCIgcz0iMSIvPjwvaXRlbXM+PC90YWJ1bGFyPjwvZGF0YT48ZXh0THN0Pjx4OmV4dCB1cmk9Ins0NzA3MjJFMC1BQUNELTRDMTctOUNEQy0xN0VGNzY1REJDN0V9IiB4bWxuczp4MTU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAxMC8xMS9tYWluIj48eDE1OnNsaWNlckNhY2hlSGlkZUl0ZW1zV2l0aE5vRGF0YS8+PC94OmV4dD48L2V4dExzdD48L3NsaWNlckNhY2hlRGVmaW5pdGlvbj5QSwMEFAAAAAAAAAAhALlhKL7hAgAA4QIAACAAAAB4bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGUxLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxzbGljZXJDYWNoZURlZmluaXRpb24geG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iIHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgbWM6SWdub3JhYmxlPSJ4IiB4bWxuczp4PSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIG5hbWU9IlNsaWNlcl9Qcm9qZWt0IiBzb3VyY2VOYW1lPSJQcm9qZWt0Ij48cGl2b3RUYWJsZXM+PHBpdm90VGFibGUgdGFiSWQ9IjQiIG5hbWU9IlBpdm90VGFibGUxIi8+PC9waXZvdFRhYmxlcz48ZGF0YT48dGFidWxhciBwaXZvdENhY2hlSWQ9IjYiIHNob3dNaXNzaW5nPSIwIj48aXRlbXMgY291bnQ9IjEiPjxpIHg9IjAiIHM9IjEiLz48L2l0ZW1zPjwvdGFidWxhcj48L2RhdGE+PGV4dExzdD48eDpleHQgdXJpPSJ7NDcwNzIyRTAtQUFDRC00QzE3LTlDREMtMTdFRjc2NURCQzdFfSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+PHgxNTpzbGljZXJDYWNoZUhpZGVJdGVtc1dpdGhOb0RhdGEvPjwveDpleHQ+PC9leHRMc3Q+PC9zbGljZXJDYWNoZURlZmluaXRpb24+UEsDBBQAAAAAAAAAIQBLslhdOxMAADsTAAAYAAAAeGwvd29ya3NoZWV0cy9zaGVldDYueG1s77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pjx3b3Jrc2hlZXQgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgeG1sbnM6cj0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcyIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngxNGFjIiB4bWxuczp4MTRhYz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvYWMiPjxkaW1lbnNpb24gcmVmPSJBMTpFNSIgLz48c2hlZXRWaWV3cz48c2hlZXRWaWV3IHRvcExlZnRDZWxsPSJBMiIgem9vbVNjYWxlTm9ybWFsPSIxMDAiIHdvcmtib29rVmlld0lkPSIwIj48cGFuZSB5U3BsaXQ9IjEiIHRvcExlZnRDZWxsPSJBMyIgYWN0aXZlUGFuZT0iYm90dG9tTGVmdCIgc3RhdGU9ImZyb3plbiIgLz48c2VsZWN0aW9uIGFjdGl2ZUNlbGw9IkEyIiBzcXJlZj0iQTIiIC8+PHNlbGVjdGlvbiBwYW5lPSJib3R0b21MZWZ0IiBhY3RpdmVDZWxsPSJBMyIgc3FyZWY9IkEzIiAvPjwvc2hlZXRWaWV3Pjwvc2hlZXRWaWV3cz48c2hlZXRGb3JtYXRQciBkZWZhdWx0Um93SGVpZ2h0PSIxMi43NSIgeDE0YWM6ZHlEZXNjZW50PSIwLjIiIC8+PGNvbHM+PGNvbCBtaW49IjEiIG1heD0iMSIgd2lkdGg9IjE2LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyIiBtYXg9IjIiIHdpZHRoPSIzNS4yODUxNTYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIzIiBtYXg9IjMiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxMS43MTA5Mzc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjQiIG1heD0iNCIgYmVzdEZpdD0iMSIgd2lkdGg9IjcuNzEwOTM3NSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSI1IiBtYXg9IjUiIGJlc3RGaXQ9IjEiIHdpZHRoPSI1LjU3MDMxMjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iNiIgbWF4PSI2IiB3aWR0aD0iNi4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iNyIgbWF4PSI3IiB3aWR0aD0iMjIuMjg1MTU2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iOCIgbWF4PSI4IiB3aWR0aD0iNi40MjU3ODEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSI5IiBtYXg9IjkiIHdpZHRoPSIyMi4yODUxNTYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxMCIgbWF4PSIxMCIgd2lkdGg9IjIwLjI4NTE1NjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjExIiBtYXg9IjExIiBiZXN0Rml0PSIxIiB3aWR0aD0iMTAuNTcwMzEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxMiIgbWF4PSIxMiIgYmVzdEZpdD0iMSIgd2lkdGg9IjE3LjcxMDkzNzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTMiIG1heD0iMTMiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxNi4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTQiIG1heD0iMTQiIGJlc3RGaXQ9IjEiIHdpZHRoPSIyMS40MjU3ODEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxNSIgbWF4PSIxNSIgYmVzdEZpdD0iMSIgd2lkdGg9IjcuODU1NDY4NzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTYiIG1heD0iMTYiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxNS40MjU3ODEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxNyIgbWF4PSIxNyIgYmVzdEZpdD0iMSIgd2lkdGg9IjM4IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjE4IiBtYXg9IjE4IiBiZXN0Rml0PSIxIiB3aWR0aD0iMTMuMTQwNjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjE5IiBtYXg9IjE5IiB3aWR0aD0iMTkuNTcwMzEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMCIgbWF4PSIyMCIgd2lkdGg9IjYuNzEwOTM3NSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMSIgbWF4PSIyMSIgYmVzdEZpdD0iMSIgd2lkdGg9IjM0LjI4NTE1NjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjIyIiBtYXg9IjIyIiB3aWR0aD0iMTguNzEwOTM3NSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMyIgbWF4PSIyMyIgYmVzdEZpdD0iMSIgd2lkdGg9IjE4LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyNCIgbWF4PSIyNCIgYmVzdEZpdD0iMSIgd2lkdGg9IjM1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjI1IiBtYXg9IjI1IiB3aWR0aD0iMTMuMjg1MTU2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMjYiIG1heD0iMjYiIHdpZHRoPSIxMiIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyNyIgbWF4PSIyNyIgd2lkdGg9IjQiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMjgiIG1heD0iMjgiIHdpZHRoPSIxMC4yODUxNTYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyOSIgbWF4PSIyOSIgd2lkdGg9IjEwLjg1NTQ2ODc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjMwIiBtYXg9IjMwIiBiZXN0Rml0PSIxIiB3aWR0aD0iNDEuNzEwOTM3NSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIzMSIgbWF4PSIzMSIgYmVzdEZpdD0iMSIgd2lkdGg9IjcuMjg1MTU2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMzIiIG1heD0iMzIiIGJlc3RGaXQ9IjEiIHdpZHRoPSIzNC40MjU3ODEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIzMyIgbWF4PSIzMyIgYmVzdEZpdD0iMSIgd2lkdGg9IjI0LjcxMDkzNzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMzQiIG1heD0iMzQiIHdpZHRoPSIyNy4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMzUiIG1heD0iMzUiIGJlc3RGaXQ9IjEiIHdpZHRoPSIzOS4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMzYiIG1heD0iMzYiIGJlc3RGaXQ9IjEiIHdpZHRoPSI2LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIzNyIgbWF4PSIzNyIgYmVzdEZpdD0iMSIgd2lkdGg9IjI0LjcxMDkzNzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMzgiIG1heD0iMzgiIGJlc3RGaXQ9IjEiIHdpZHRoPSIyNy4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMzkiIG1heD0iMzkiIGJlc3RGaXQ9IjEiIHdpZHRoPSIzOS4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iNDAiIG1heD0iNDAiIGJlc3RGaXQ9IjEiIHdpZHRoPSIyOC4yODUxNTYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSI0MSIgbWF4PSI0MSIgYmVzdEZpdD0iMSIgd2lkdGg9IjExLjI4NTE1NjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjQyIiBtYXg9IjE2Mzg0IiB3aWR0aD0iOS4xNDA2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48L2NvbHM+PHNoZWV0RGF0YT48cm93IHI9IjEiIGh0PSIwIiBoaWRkZW49IjEiID48YyByPSJDMSIgcz0iNSIgdD0icyI+PHY+MjA8L3Y+PC9jPjxjIHI9IkQxIiBzPSI1IiB0PSJzIj48dj4yNjwvdj48L2M+PC9yb3c+PHJvdyByPSIyIiA+PGMgcj0iQTIiIHM9IjEzIiAvPjxjIHI9IkIyIiBzPSIxMyIgLz48YyByPSJDMiIgcz0iNSIgdD0icyI+PHY+MDwvdj48L2M+PGMgcj0iRDIiIHM9IjYiIHQ9InMiPjx2PjIwPC92PjwvYz48YyByPSJFMiIgcz0iNiIgdD0icyI+PHY+MjI8L3Y+PC9jPjwvcm93Pjxyb3cgcj0iMyIgPjxjIHI9IkMzIiBzPSI3IiB0PSJzIj48dj4yMDwvdj48L2M+PGMgcj0iRDMiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iRTMiIHM9IjEwIiA+PHY+MDwvdj48L2M+PC9yb3c+PHJvdyByPSI0IiA+PGMgcj0iQzQiIHM9IjEyIiB0PSJzIj48dj4yMDwvdj48L2M+PGMgcj0iRDQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iRTQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PC9yb3c+PHJvdyByPSI1IiA+PGMgcj0iQzUiIHM9IjciIHQ9InMiPjx2PjIyPC92PjwvYz48YyByPSJENSIgcz0iMTAiID48dj4wPC92PjwvYz48YyByPSJFNSIgcz0iMTAiID48dj4wPC92PjwvYz48L3Jvdz48L3NoZWV0RGF0YT48cGFnZU1hcmdpbnMgbGVmdD0iMC43IiByaWdodD0iMC43IiB0b3A9IjAuNzUiIGJvdHRvbT0iMC43NSIgaGVhZGVyPSIwLjMiIGZvb3Rlcj0iMC4zIiAvPjxoZWFkZXJGb290ZXIgLz48ZHJhd2luZyByOmlkPSJySWQyIiAvPjxleHRMc3Q+PGV4dCB1cmk9IntBODc2NUJBOS00NTZBLTRkYWItQjRGMy1BQ0Y4MzhDMTIxREV9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPjx4MTQ6c2xpY2VyTGlzdD48eDE0OnNsaWNlciByOmlkPSJySWQzIiAvPjwveDE0OnNsaWNlckxpc3Q+PC9leHQ+PC9leHRMc3Q+PC93b3Jrc2hlZXQ+UEsDBBQAAAAAAAAAIQCQOdNXPgIAAD4CAAAjAAAAeGwvd29ya3NoZWV0cy9fcmVscy9zaGVldDYueG1sLnJlbHM8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+PFJlbGF0aW9uc2hpcHMgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvcmVsYXRpb25zaGlwcyI+PFJlbGF0aW9uc2hpcCBJZD0icklkMyIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlciIgVGFyZ2V0PSIuLi9zbGljZXJzL3NsaWNlcjMueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMiIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9kcmF3aW5nIiBUYXJnZXQ9Ii4uL2RyYXdpbmdzL2RyYXdpbmczLnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDEiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvcGl2b3RUYWJsZSIgVGFyZ2V0PSIuLi9waXZvdFRhYmxlcy9waXZvdFRhYmxlMy54bWwiLz48L1JlbGF0aW9uc2hpcHM+UEsDBBQAAAAAAAAAIQAPoN0lFxMAABcTAAAYAAAAeGwvd29ya3NoZWV0cy9zaGVldDUueG1s77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pjx3b3Jrc2hlZXQgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgeG1sbnM6cj0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcyIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngxNGFjIiB4bWxuczp4MTRhYz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvYWMiPjxkaW1lbnNpb24gcmVmPSJBMTpPNCIgLz48c2hlZXRWaWV3cz48c2hlZXRWaWV3IHpvb21TY2FsZU5vcm1hbD0iMTAwIiB3b3JrYm9va1ZpZXdJZD0iMCI+PHBhbmUgeVNwbGl0PSIxIiB0b3BMZWZ0Q2VsbD0iQTIiIGFjdGl2ZVBhbmU9ImJvdHRvbUxlZnQiIHN0YXRlPSJmcm96ZW4iIC8+PHNlbGVjdGlvbiBwYW5lPSJib3R0b21MZWZ0IiBhY3RpdmVDZWxsPSJBMiIgc3FyZWY9IkEyIiAvPjwvc2hlZXRWaWV3Pjwvc2hlZXRWaWV3cz48c2hlZXRGb3JtYXRQciBkZWZhdWx0Um93SGVpZ2h0PSIxMi43NSIgeDE0YWM6ZHlEZXNjZW50PSIwLjIiIC8+PGNvbHM+PGNvbCBtaW49IjEiIG1heD0iMSIgd2lkdGg9IjE2LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyIiBtYXg9IjIiIHdpZHRoPSIzNS4yODUxNTYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIzIiBtYXg9IjMiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxMS43MTA5Mzc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjQiIG1heD0iNCIgYmVzdEZpdD0iMSIgd2lkdGg9IjE3LjQyNTc4MTI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjUiIG1heD0iNSIgYmVzdEZpdD0iMSIgd2lkdGg9IjE4Ljg1NTQ2ODc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjYiIG1heD0iNiIgYmVzdEZpdD0iMSIgd2lkdGg9IjYiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iNyIgbWF4PSI3IiBiZXN0Rml0PSIxIiB3aWR0aD0iMTQuMTQwNjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjgiIG1heD0iOCIgYmVzdEZpdD0iMSIgd2lkdGg9IjguNTcwMzEyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSI5IiBtYXg9IjkiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxNC41NzAzMTI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjEwIiBtYXg9IjEwIiBiZXN0Rml0PSIxIiB3aWR0aD0iMTEuODU1NDY4NzUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTEiIG1heD0iMTEiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxNS41NzAzMTI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjEyIiBtYXg9IjEyIiBiZXN0Rml0PSIxIiB3aWR0aD0iMTMuMjg1MTU2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTMiIG1heD0iMTMiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxNiIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxNCIgbWF4PSIxNCIgYmVzdEZpdD0iMSIgd2lkdGg9IjEyLjU3MDMxMjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTUiIG1heD0iMTUiIGJlc3RGaXQ9IjEiIHdpZHRoPSIxMiIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxNiIgbWF4PSIxNiIgd2lkdGg9IjM4LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIxNyIgbWF4PSIxNyIgd2lkdGg9IjIzLjI4NTE1NjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjE4IiBtYXg9IjE4IiBiZXN0Rml0PSIxIiB3aWR0aD0iMjkuNDI1NzgxMjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMTkiIG1heD0iMTkiIGJlc3RGaXQ9IjEiIHdpZHRoPSIzMS4yODUxNTYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMCIgbWF4PSIyMCIgYmVzdEZpdD0iMSIgd2lkdGg9IjI2LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjxjb2wgbWluPSIyMSIgbWF4PSIyMSIgYmVzdEZpdD0iMSIgd2lkdGg9IjIxLjg1NTQ2ODc1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjIyIiBtYXg9IjIyIiBiZXN0Rml0PSIxIiB3aWR0aD0iMzIuMTQwNjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjIzIiBtYXg9IjIzIiBiZXN0Rml0PSIxIiB3aWR0aD0iMzEiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMjQiIG1heD0iMjQiIHdpZHRoPSI3LjI4NTE1NjI1IiBjdXN0b21XaWR0aD0iMSIgc3R5bGU9IjYiIC8+PGNvbCBtaW49IjI1IiBtYXg9IjI1IiBiZXN0Rml0PSIxIiB3aWR0aD0iMTEuMjg1MTU2MjUiIGN1c3RvbVdpZHRoPSIxIiBzdHlsZT0iNiIgLz48Y29sIG1pbj0iMjYiIG1heD0iMTYzODQiIHdpZHRoPSI5LjE0MDYyNSIgY3VzdG9tV2lkdGg9IjEiIHN0eWxlPSI2IiAvPjwvY29scz48c2hlZXREYXRhPjxyb3cgcj0iMSIgPjxjIHI9IkExIiBzPSIxMyIgLz48YyByPSJCMSIgcz0iMTMiIC8+PGMgcj0iQzEiIHM9IjUiIHQ9InMiPjx2PjA8L3Y+PC9jPjxjIHI9IkQxIiBzPSI2IiB0PSJzIj48dj4xNDwvdj48L2M+PGMgcj0iRTEiIHM9IjYiIHQ9InMiPjx2PjE2PC92PjwvYz48YyByPSJGMSIgcz0iNiIgdD0icyI+PHY+MTA4PC92PjwvYz48YyByPSJHMSIgcz0iNiIgdD0icyI+PHY+MTA5PC92PjwvYz48YyByPSJIMSIgcz0iNiIgdD0icyI+PHY+MTEwPC92PjwvYz48YyByPSJJMSIgcz0iNiIgdD0icyI+PHY+MTExPC92PjwvYz48YyByPSJKMSIgcz0iNiIgdD0icyI+PHY+MTEyPC92PjwvYz48YyByPSJLMSIgcz0iNiIgdD0icyI+PHY+MTEzPC92PjwvYz48YyByPSJMMSIgcz0iNiIgdD0icyI+PHY+MTE0PC92PjwvYz48YyByPSJNMSIgcz0iNiIgdD0icyI+PHY+MTE1PC92PjwvYz48YyByPSJOMSIgcz0iNiIgdD0icyI+PHY+MTE2PC92PjwvYz48YyByPSJPMSIgcz0iNiIgdD0icyI+PHY+MTE3PC92PjwvYz48L3Jvdz48cm93IHI9IjIiID48YyByPSJDMiIgcz0iNyIgdD0icyI+PHY+MjA8L3Y+PC9jPjxjIHI9IkQyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkUyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkYyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkcyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkgyIiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iSTIiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iSjIiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJLMiIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9IkwyIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik0yIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik4yIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik8yIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjwvcm93Pjxyb3cgcj0iMyIgPjxjIHI9IkMzIiBzPSIxMiIgdD0icyI+PHY+MjA8L3Y+PC9jPjxjIHI9IkQzIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkUzIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkYzIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkczIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9IkgzIiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iSTMiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iSjMiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJLMyIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9IkwzIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik0zIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik4zIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjxjIHI9Ik8zIiBzPSIxMCIgPjx2PjA8L3Y+PC9jPjwvcm93Pjxyb3cgcj0iNCIgPjxjIHI9IkM0IiBzPSI3IiB0PSJzIj48dj4yMjwvdj48L2M+PGMgcj0iRDQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iRTQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iRjQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iRzQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iSDQiIHM9IjkiID48dj4wPC92PjwvYz48YyByPSJJNCIgcz0iMTAiID48dj4wPC92PjwvYz48YyByPSJKNCIgcz0iOSIgPjx2PjA8L3Y+PC9jPjxjIHI9Iks0IiBzPSI5IiA+PHY+MDwvdj48L2M+PGMgcj0iTDQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iTTQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iTjQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PGMgcj0iTzQiIHM9IjEwIiA+PHY+MDwvdj48L2M+PC9yb3c+PC9zaGVldERhdGE+PHBhZ2VNYXJnaW5zIGxlZnQ9IjAuNyIgcmlnaHQ9IjAuNyIgdG9wPSIwLjc1IiBib3R0b209IjAuNzUiIGhlYWRlcj0iMC4zIiBmb290ZXI9IjAuMyIgLz48cGFnZVNldHVwIHBhcGVyU2l6ZT0iOSIgb3JpZW50YXRpb249InBvcnRyYWl0IiAvPjxoZWFkZXJGb290ZXIgLz48ZHJhd2luZyByOmlkPSJySWQzIiAvPjxleHRMc3Q+PGV4dCB1cmk9IntBODc2NUJBOS00NTZBLTRkYWItQjRGMy1BQ0Y4MzhDMTIxREV9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPjx4MTQ6c2xpY2VyTGlzdD48eDE0OnNsaWNlciByOmlkPSJySWQ0IiAvPjwveDE0OnNsaWNlckxpc3Q+PC9leHQ+PC9leHRMc3Q+PC93b3Jrc2hlZXQ+UEsDBBQAAAAAAAAAIQC1fqv5PgIAAD4CAAAjAAAAeGwvd29ya3NoZWV0cy9fcmVscy9zaGVldDUueG1sLnJlbHM8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+PFJlbGF0aW9uc2hpcHMgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvcmVsYXRpb25zaGlwcyI+PFJlbGF0aW9uc2hpcCBJZD0icklkMyIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9kcmF3aW5nIiBUYXJnZXQ9Ii4uL2RyYXdpbmdzL2RyYXdpbmcyLnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDEiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvcGl2b3RUYWJsZSIgVGFyZ2V0PSIuLi9waXZvdFRhYmxlcy9waXZvdFRhYmxlMi54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQ0IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA3L3JlbGF0aW9uc2hpcHMvc2xpY2VyIiBUYXJnZXQ9Ii4uL3NsaWNlcnMvc2xpY2VyMi54bWwiLz48L1JlbGF0aW9uc2hpcHM+UEsDBBQAAAAAAAAAIQAnsTsm5wIAAOcCAAAgAAAAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlNS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8c2xpY2VyQ2FjaGVEZWZpbml0aW9uIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIiB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIG1jOklnbm9yYWJsZT0ieCIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIiBuYW1lPSJTbGljZXJfVXRmYWxsc3R5cCIgc291cmNlTmFtZT0iVXRmYWxsc3R5cCI+PHBpdm90VGFibGVzPjxwaXZvdFRhYmxlIHRhYklkPSI2IiBuYW1lPSJQaXZvdFRhYmxlNCIvPjwvcGl2b3RUYWJsZXM+PGRhdGE+PHRhYnVsYXIgcGl2b3RDYWNoZUlkPSI4IiBzaG93TWlzc2luZz0iMCI+PGl0ZW1zIGNvdW50PSIxIj48aSB4PSIwIiBzPSIxIi8+PC9pdGVtcz48L3RhYnVsYXI+PC9kYXRhPjxleHRMc3Q+PHg6ZXh0IHVyaT0iezQ3MDcyMkUwLUFBQ0QtNEMxNy05Q0RDLTE3RUY3NjVEQkM3RX0iIHhtbG5zOngxNT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDEwLzExL21haW4iPjx4MTU6c2xpY2VyQ2FjaGVIaWRlSXRlbXNXaXRoTm9EYXRhLz48L3g6ZXh0PjwvZXh0THN0Pjwvc2xpY2VyQ2FjaGVEZWZpbml0aW9uPlBLAwQUAAAAAAAAACEAo7nldeQCAADkAgAAIAAAAHhsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTQueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPHNsaWNlckNhY2hlRGVmaW5pdGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvbWFpbiIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgbmFtZT0iU2xpY2VyX1PDpGxqYXJlMSIgc291cmNlTmFtZT0iU8OkbGphcmUiPjxwaXZvdFRhYmxlcz48cGl2b3RUYWJsZSB0YWJJZD0iNSIgbmFtZT0iUGl2b3RUYWJsZTIiLz48L3Bpdm90VGFibGVzPjxkYXRhPjx0YWJ1bGFyIHBpdm90Q2FjaGVJZD0iNyIgc2hvd01pc3Npbmc9IjAiPjxpdGVtcyBjb3VudD0iMSI+PGkgeD0iMCIgcz0iMSIvPjwvaXRlbXM+PC90YWJ1bGFyPjwvZGF0YT48ZXh0THN0Pjx4OmV4dCB1cmk9Ins0NzA3MjJFMC1BQUNELTRDMTctOUNEQy0xN0VGNzY1REJDN0V9IiB4bWxuczp4MTU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAxMC8xMS9tYWluIj48eDE1OnNsaWNlckNhY2hlSGlkZUl0ZW1zV2l0aE5vRGF0YS8+PC94OmV4dD48L2V4dExzdD48L3NsaWNlckNhY2hlRGVmaW5pdGlvbj5QSwMEFAAAAAAAAAAhAEtXT27nAgAA5wIAACAAAAB4bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU3LnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxzbGljZXJDYWNoZURlZmluaXRpb24geG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iIHhtbG5zOm1jPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvbWFya3VwLWNvbXBhdGliaWxpdHkvMjAwNiIgbWM6SWdub3JhYmxlPSJ4IiB4bWxuczp4PSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvc3ByZWFkc2hlZXRtbC8yMDA2L21haW4iIG5hbWU9IlNsaWNlcl9BbnbDpG5kYXJlIiBzb3VyY2VOYW1lPSJBbnbDpG5kYXJlIj48cGl2b3RUYWJsZXM+PHBpdm90VGFibGUgdGFiSWQ9IjYiIG5hbWU9IlBpdm90VGFibGU0Ii8+PC9waXZvdFRhYmxlcz48ZGF0YT48dGFidWxhciBwaXZvdENhY2hlSWQ9IjgiIHNob3dNaXNzaW5nPSIwIj48aXRlbXMgY291bnQ9IjEiPjxpIHg9IjAiIHM9IjEiLz48L2l0ZW1zPjwvdGFidWxhcj48L2RhdGE+PGV4dExzdD48eDpleHQgdXJpPSJ7NDcwNzIyRTAtQUFDRC00QzE3LTlDREMtMTdFRjc2NURCQzdFfSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+PHgxNTpzbGljZXJDYWNoZUhpZGVJdGVtc1dpdGhOb0RhdGEvPjwveDpleHQ+PC9leHRMc3Q+PC9zbGljZXJDYWNoZURlZmluaXRpb24+UEsDBBQAAAAAAAAAIQCLgm5YjhoAAI4aAAATAAAAeGwvdGhlbWUvdGhlbWUxLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxhOnRoZW1lIHhtbG5zOmE9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9kcmF3aW5nbWwvMjAwNi9tYWluIiBuYW1lPSJPZmZpY2UgVGhlbWUiPjxhOnRoZW1lRWxlbWVudHM+PGE6Y2xyU2NoZW1lIG5hbWU9Ik9mZmljZSI+PGE6ZGsxPjxhOnN5c0NsciB2YWw9IndpbmRvd1RleHQiIGxhc3RDbHI9IjAwMDAwMCIvPjwvYTpkazE+PGE6bHQxPjxhOnN5c0NsciB2YWw9IndpbmRvdyIgbGFzdENscj0iRkZGRkZGIi8+PC9hOmx0MT48YTpkazI+PGE6c3JnYkNsciB2YWw9IjQ0NTQ2QSIvPjwvYTpkazI+PGE6bHQyPjxhOnNyZ2JDbHIgdmFsPSJFN0U2RTYiLz48L2E6bHQyPjxhOmFjY2VudDE+PGE6c3JnYkNsciB2YWw9IjVCOUJENSIvPjwvYTphY2NlbnQxPjxhOmFjY2VudDI+PGE6c3JnYkNsciB2YWw9IkVEN0QzMSIvPjwvYTphY2NlbnQyPjxhOmFjY2VudDM+PGE6c3JnYkNsciB2YWw9IkE1QTVBNSIvPjwvYTphY2NlbnQzPjxhOmFjY2VudDQ+PGE6c3JnYkNsciB2YWw9IkZGQzAwMCIvPjwvYTphY2NlbnQ0PjxhOmFjY2VudDU+PGE6c3JnYkNsciB2YWw9IjQ0NzJDNCIvPjwvYTphY2NlbnQ1PjxhOmFjY2VudDY+PGE6c3JnYkNsciB2YWw9IjcwQUQ0NyIvPjwvYTphY2NlbnQ2PjxhOmhsaW5rPjxhOnNyZ2JDbHIgdmFsPSIwNTYzQzEiLz48L2E6aGxpbms+PGE6Zm9sSGxpbms+PGE6c3JnYkNsciB2YWw9Ijk1NEY3MiIvPjwvYTpmb2xIbGluaz48L2E6Y2xyU2NoZW1lPjxhOmZvbnRTY2hlbWUgbmFtZT0iT2ZmaWNlIj48YTptYWpvckZvbnQ+PGE6bGF0aW4gdHlwZWZhY2U9IkNhbGlicmkgTGlnaHQiIHBhbm9zZT0iMDIwRjAzMDIwMjAyMDQwMzAyMDQiLz48YTplYSB0eXBlZmFjZT0iIi8+PGE6Y3MgdHlwZWZhY2U9IiIvPjxhOmZvbnQgc2NyaXB0PSJKcGFuIiB0eXBlZmFjZT0i77yt77yzIO+8sOOCtOOCt+ODg+OCryIvPjxhOmZvbnQgc2NyaXB0PSJIYW5nIiB0eXBlZmFjZT0i66eR7J2AIOqzoOuUlSIvPjxhOmZvbnQgc2NyaXB0PSJIYW5zIiB0eXBlZmFjZT0i5a6L5L2TIi8+PGE6Zm9udCBzY3JpcHQ9IkhhbnQiIHR5cGVmYWNlPSLmlrDntLDmmI7pq5QiLz48YTpmb250IHNjcmlwdD0iQXJhYiIgdHlwZWZhY2U9IlRpbWVzIE5ldyBSb21hbiIvPjxhOmZvbnQgc2NyaXB0PSJIZWJyIiB0eXBlZmFjZT0iVGltZXMgTmV3IFJvbWFuIi8+PGE6Zm9udCBzY3JpcHQ9IlRoYWkiIHR5cGVmYWNlPSJUYWhvbWEiLz48YTpmb250IHNjcmlwdD0iRXRoaSIgdHlwZWZhY2U9Ik55YWxhIi8+PGE6Zm9udCBzY3JpcHQ9IkJlbmciIHR5cGVmYWNlPSJWcmluZGEiLz48YTpmb250IHNjcmlwdD0iR3VqciIgdHlwZWZhY2U9IlNocnV0aSIvPjxhOmZvbnQgc2NyaXB0PSJLaG1yIiB0eXBlZmFjZT0iTW9vbEJvcmFuIi8+PGE6Zm9udCBzY3JpcHQ9IktuZGEiIHR5cGVmYWNlPSJUdW5nYSIvPjxhOmZvbnQgc2NyaXB0PSJHdXJ1IiB0eXBlZmFjZT0iUmFhdmkiLz48YTpmb250IHNjcmlwdD0iQ2FucyIgdHlwZWZhY2U9IkV1cGhlbWlhIi8+PGE6Zm9udCBzY3JpcHQ9IkNoZXIiIHR5cGVmYWNlPSJQbGFudGFnZW5ldCBDaGVyb2tlZSIvPjxhOmZvbnQgc2NyaXB0PSJZaWlpIiB0eXBlZmFjZT0iTWljcm9zb2Z0IFlpIEJhaXRpIi8+PGE6Zm9udCBzY3JpcHQ9IlRpYnQiIHR5cGVmYWNlPSJNaWNyb3NvZnQgSGltYWxheWEiLz48YTpmb250IHNjcmlwdD0iVGhhYSIgdHlwZWZhY2U9Ik1WIEJvbGkiLz48YTpmb250IHNjcmlwdD0iRGV2YSIgdHlwZWZhY2U9Ik1hbmdhbCIvPjxhOmZvbnQgc2NyaXB0PSJUZWx1IiB0eXBlZmFjZT0iR2F1dGFtaSIvPjxhOmZvbnQgc2NyaXB0PSJUYW1sIiB0eXBlZmFjZT0iTGF0aGEiLz48YTpmb250IHNjcmlwdD0iU3lyYyIgdHlwZWZhY2U9IkVzdHJhbmdlbG8gRWRlc3NhIi8+PGE6Zm9udCBzY3JpcHQ9Ik9yeWEiIHR5cGVmYWNlPSJLYWxpbmdhIi8+PGE6Zm9udCBzY3JpcHQ9Ik1seW0iIHR5cGVmYWNlPSJLYXJ0aWthIi8+PGE6Zm9udCBzY3JpcHQ9Ikxhb28iIHR5cGVmYWNlPSJEb2tDaGFtcGEiLz48YTpmb250IHNjcmlwdD0iU2luaCIgdHlwZWZhY2U9Iklza29vbGEgUG90YSIvPjxhOmZvbnQgc2NyaXB0PSJNb25nIiB0eXBlZmFjZT0iTW9uZ29saWFuIEJhaXRpIi8+PGE6Zm9udCBzY3JpcHQ9IlZpZXQiIHR5cGVmYWNlPSJUaW1lcyBOZXcgUm9tYW4iLz48YTpmb250IHNjcmlwdD0iVWlnaCIgdHlwZWZhY2U9Ik1pY3Jvc29mdCBVaWdodXIiLz48YTpmb250IHNjcmlwdD0iR2VvciIgdHlwZWZhY2U9IlN5bGZhZW4iLz48L2E6bWFqb3JGb250PjxhOm1pbm9yRm9udD48YTpsYXRpbiB0eXBlZmFjZT0iQ2FsaWJyaSIgcGFub3NlPSIwMjBGMDUwMjAyMDIwNDAzMDIwNCIvPjxhOmVhIHR5cGVmYWNlPSIiLz48YTpjcyB0eXBlZmFjZT0iIi8+PGE6Zm9udCBzY3JpcHQ9IkpwYW4iIHR5cGVmYWNlPSLvvK3vvLMg77yw44K044K344OD44KvIi8+PGE6Zm9udCBzY3JpcHQ9IkhhbmciIHR5cGVmYWNlPSLrp5HsnYAg6rOg65SVIi8+PGE6Zm9udCBzY3JpcHQ9IkhhbnMiIHR5cGVmYWNlPSLlrovkvZMiLz48YTpmb250IHNjcmlwdD0iSGFudCIgdHlwZWZhY2U9IuaWsOe0sOaYjumrlCIvPjxhOmZvbnQgc2NyaXB0PSJBcmFiIiB0eXBlZmFjZT0iQXJpYWwiLz48YTpmb250IHNjcmlwdD0iSGViciIgdHlwZWZhY2U9IkFyaWFsIi8+PGE6Zm9udCBzY3JpcHQ9IlRoYWkiIHR5cGVmYWNlPSJUYWhvbWEiLz48YTpmb250IHNjcmlwdD0iRXRoaSIgdHlwZWZhY2U9Ik55YWxhIi8+PGE6Zm9udCBzY3JpcHQ9IkJlbmciIHR5cGVmYWNlPSJWcmluZGEiLz48YTpmb250IHNjcmlwdD0iR3VqciIgdHlwZWZhY2U9IlNocnV0aSIvPjxhOmZvbnQgc2NyaXB0PSJLaG1yIiB0eXBlZmFjZT0iRGF1blBlbmgiLz48YTpmb250IHNjcmlwdD0iS25kYSIgdHlwZWZhY2U9IlR1bmdhIi8+PGE6Zm9udCBzY3JpcHQ9Ikd1cnUiIHR5cGVmYWNlPSJSYWF2aSIvPjxhOmZvbnQgc2NyaXB0PSJDYW5zIiB0eXBlZmFjZT0iRXVwaGVtaWEiLz48YTpmb250IHNjcmlwdD0iQ2hlciIgdHlwZWZhY2U9IlBsYW50YWdlbmV0IENoZXJva2VlIi8+PGE6Zm9udCBzY3JpcHQ9IllpaWkiIHR5cGVmYWNlPSJNaWNyb3NvZnQgWWkgQmFpdGkiLz48YTpmb250IHNjcmlwdD0iVGlidCIgdHlwZWZhY2U9Ik1pY3Jvc29mdCBIaW1hbGF5YSIvPjxhOmZvbnQgc2NyaXB0PSJUaGFhIiB0eXBlZmFjZT0iTVYgQm9saSIvPjxhOmZvbnQgc2NyaXB0PSJEZXZhIiB0eXBlZmFjZT0iTWFuZ2FsIi8+PGE6Zm9udCBzY3JpcHQ9IlRlbHUiIHR5cGVmYWNlPSJHYXV0YW1pIi8+PGE6Zm9udCBzY3JpcHQ9IlRhbWwiIHR5cGVmYWNlPSJMYXRoYSIvPjxhOmZvbnQgc2NyaXB0PSJTeXJjIiB0eXBlZmFjZT0iRXN0cmFuZ2VsbyBFZGVzc2EiLz48YTpmb250IHNjcmlwdD0iT3J5YSIgdHlwZWZhY2U9IkthbGluZ2EiLz48YTpmb250IHNjcmlwdD0iTWx5bSIgdHlwZWZhY2U9IkthcnRpa2EiLz48YTpmb250IHNjcmlwdD0iTGFvbyIgdHlwZWZhY2U9IkRva0NoYW1wYSIvPjxhOmZvbnQgc2NyaXB0PSJTaW5oIiB0eXBlZmFjZT0iSXNrb29sYSBQb3RhIi8+PGE6Zm9udCBzY3JpcHQ9Ik1vbmciIHR5cGVmYWNlPSJNb25nb2xpYW4gQmFpdGkiLz48YTpmb250IHNjcmlwdD0iVmlldCIgdHlwZWZhY2U9IkFyaWFsIi8+PGE6Zm9udCBzY3JpcHQ9IlVpZ2giIHR5cGVmYWNlPSJNaWNyb3NvZnQgVWlnaHVyIi8+PGE6Zm9udCBzY3JpcHQ9Ikdlb3IiIHR5cGVmYWNlPSJTeWxmYWVuIi8+PC9hOm1pbm9yRm9udD48L2E6Zm9udFNjaGVtZT48YTpmbXRTY2hlbWUgbmFtZT0iT2ZmaWNlIj48YTpmaWxsU3R5bGVMc3Q+PGE6c29saWRGaWxsPjxhOnNjaGVtZUNsciB2YWw9InBoQ2xyIi8+PC9hOnNvbGlkRmlsbD48YTpncmFkRmlsbCByb3RXaXRoU2hhcGU9IjEiPjxhOmdzTHN0PjxhOmdzIHBvcz0iMCI+PGE6c2NoZW1lQ2xyIHZhbD0icGhDbHIiPjxhOmx1bU1vZCB2YWw9IjExMDAwMCIvPjxhOnNhdE1vZCB2YWw9IjEwNTAwMCIvPjxhOnRpbnQgdmFsPSI2NzAwMCIvPjwvYTpzY2hlbWVDbHI+PC9hOmdzPjxhOmdzIHBvcz0iNTAwMDAiPjxhOnNjaGVtZUNsciB2YWw9InBoQ2xyIj48YTpsdW1Nb2QgdmFsPSIxMDUwMDAiLz48YTpzYXRNb2QgdmFsPSIxMDMwMDAiLz48YTp0aW50IHZhbD0iNzMwMDAiLz48L2E6c2NoZW1lQ2xyPjwvYTpncz48YTpncyBwb3M9IjEwMDAwMCI+PGE6c2NoZW1lQ2xyIHZhbD0icGhDbHIiPjxhOmx1bU1vZCB2YWw9IjEwNTAwMCIvPjxhOnNhdE1vZCB2YWw9IjEwOTAwMCIvPjxhOnRpbnQgdmFsPSI4MTAwMCIvPjwvYTpzY2hlbWVDbHI+PC9hOmdzPjwvYTpnc0xzdD48YTpsaW4gYW5nPSI1NDAwMDAwIiBzY2FsZWQ9IjAiLz48L2E6Z3JhZEZpbGw+PGE6Z3JhZEZpbGwgcm90V2l0aFNoYXBlPSIxIj48YTpnc0xzdD48YTpncyBwb3M9IjAiPjxhOnNjaGVtZUNsciB2YWw9InBoQ2xyIj48YTpzYXRNb2QgdmFsPSIxMDMwMDAiLz48YTpsdW1Nb2QgdmFsPSIxMDIwMDAiLz48YTp0aW50IHZhbD0iOTQwMDAiLz48L2E6c2NoZW1lQ2xyPjwvYTpncz48YTpncyBwb3M9IjUwMDAwIj48YTpzY2hlbWVDbHIgdmFsPSJwaENsciI+PGE6c2F0TW9kIHZhbD0iMTEwMDAwIi8+PGE6bHVtTW9kIHZhbD0iMTAwMDAwIi8+PGE6c2hhZGUgdmFsPSIxMDAwMDAiLz48L2E6c2NoZW1lQ2xyPjwvYTpncz48YTpncyBwb3M9IjEwMDAwMCI+PGE6c2NoZW1lQ2xyIHZhbD0icGhDbHIiPjxhOmx1bU1vZCB2YWw9Ijk5MDAwIi8+PGE6c2F0TW9kIHZhbD0iMTIwMDAwIi8+PGE6c2hhZGUgdmFsPSI3ODAwMCIvPjwvYTpzY2hlbWVDbHI+PC9hOmdzPjwvYTpnc0xzdD48YTpsaW4gYW5nPSI1NDAwMDAwIiBzY2FsZWQ9IjAiLz48L2E6Z3JhZEZpbGw+PC9hOmZpbGxTdHlsZUxzdD48YTpsblN0eWxlTHN0PjxhOmxuIHc9IjYzNTAiIGNhcD0iZmxhdCIgY21wZD0ic25nIiBhbGduPSJjdHIiPjxhOnNvbGlkRmlsbD48YTpzY2hlbWVDbHIgdmFsPSJwaENsciIvPjwvYTpzb2xpZEZpbGw+PGE6cHJzdERhc2ggdmFsPSJzb2xpZCIvPjxhOm1pdGVyIGxpbT0iODAwMDAwIi8+PC9hOmxuPjxhOmxuIHc9IjEyNzAwIiBjYXA9ImZsYXQiIGNtcGQ9InNuZyIgYWxnbj0iY3RyIj48YTpzb2xpZEZpbGw+PGE6c2NoZW1lQ2xyIHZhbD0icGhDbHIiLz48L2E6c29saWRGaWxsPjxhOnByc3REYXNoIHZhbD0ic29saWQiLz48YTptaXRlciBsaW09IjgwMDAwMCIvPjwvYTpsbj48YTpsbiB3PSIxOTA1MCIgY2FwPSJmbGF0IiBjbXBkPSJzbmciIGFsZ249ImN0ciI+PGE6c29saWRGaWxsPjxhOnNjaGVtZUNsciB2YWw9InBoQ2xyIi8+PC9hOnNvbGlkRmlsbD48YTpwcnN0RGFzaCB2YWw9InNvbGlkIi8+PGE6bWl0ZXIgbGltPSI4MDAwMDAiLz48L2E6bG4+PC9hOmxuU3R5bGVMc3Q+PGE6ZWZmZWN0U3R5bGVMc3Q+PGE6ZWZmZWN0U3R5bGU+PGE6ZWZmZWN0THN0Lz48L2E6ZWZmZWN0U3R5bGU+PGE6ZWZmZWN0U3R5bGU+PGE6ZWZmZWN0THN0Lz48L2E6ZWZmZWN0U3R5bGU+PGE6ZWZmZWN0U3R5bGU+PGE6ZWZmZWN0THN0PjxhOm91dGVyU2hkdyBibHVyUmFkPSI1NzE1MCIgZGlzdD0iMTkwNTAiIGRpcj0iNTQwMDAwMCIgYWxnbj0iY3RyIiByb3RXaXRoU2hhcGU9IjAiPjxhOnNyZ2JDbHIgdmFsPSIwMDAwMDAiPjxhOmFscGhhIHZhbD0iNjMwMDAiLz48L2E6c3JnYkNscj48L2E6b3V0ZXJTaGR3PjwvYTplZmZlY3RMc3Q+PC9hOmVmZmVjdFN0eWxlPjwvYTplZmZlY3RTdHlsZUxzdD48YTpiZ0ZpbGxTdHlsZUxzdD48YTpzb2xpZEZpbGw+PGE6c2NoZW1lQ2xyIHZhbD0icGhDbHIiLz48L2E6c29saWRGaWxsPjxhOnNvbGlkRmlsbD48YTpzY2hlbWVDbHIgdmFsPSJwaENsciI+PGE6dGludCB2YWw9Ijk1MDAwIi8+PGE6c2F0TW9kIHZhbD0iMTcwMDAwIi8+PC9hOnNjaGVtZUNscj48L2E6c29saWRGaWxsPjxhOmdyYWRGaWxsIHJvdFdpdGhTaGFwZT0iMSI+PGE6Z3NMc3Q+PGE6Z3MgcG9zPSIwIj48YTpzY2hlbWVDbHIgdmFsPSJwaENsciI+PGE6dGludCB2YWw9IjkzMDAwIi8+PGE6c2F0TW9kIHZhbD0iMTUwMDAwIi8+PGE6c2hhZGUgdmFsPSI5ODAwMCIvPjxhOmx1bU1vZCB2YWw9IjEwMjAwMCIvPjwvYTpzY2hlbWVDbHI+PC9hOmdzPjxhOmdzIHBvcz0iNTAwMDAiPjxhOnNjaGVtZUNsciB2YWw9InBoQ2xyIj48YTp0aW50IHZhbD0iOTgwMDAiLz48YTpzYXRNb2QgdmFsPSIxMzAwMDAiLz48YTpzaGFkZSB2YWw9IjkwMDAwIi8+PGE6bHVtTW9kIHZhbD0iMTAzMDAwIi8+PC9hOnNjaGVtZUNscj48L2E6Z3M+PGE6Z3MgcG9zPSIxMDAwMDAiPjxhOnNjaGVtZUNsciB2YWw9InBoQ2xyIj48YTpzaGFkZSB2YWw9IjYzMDAwIi8+PGE6c2F0TW9kIHZhbD0iMTIwMDAwIi8+PC9hOnNjaGVtZUNscj48L2E6Z3M+PC9hOmdzTHN0PjxhOmxpbiBhbmc9IjU0MDAwMDAiIHNjYWxlZD0iMCIvPjwvYTpncmFkRmlsbD48L2E6YmdGaWxsU3R5bGVMc3Q+PC9hOmZtdFNjaGVtZT48L2E6dGhlbWVFbGVtZW50cz48YTpvYmplY3REZWZhdWx0cy8+PGE6ZXh0cmFDbHJTY2hlbWVMc3QvPjxhOmV4dExzdD48YTpleHQgdXJpPSJ7MDVBNEMyNUMtMDg1RS00MzQwLTg1QTMtQTU1MzFFNTEwREIyfSI+PHRobTE1OnRoZW1lRmFtaWx5IHhtbG5zOnRobTE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS90aGVtZW1sLzIwMTIvbWFpbiIgbmFtZT0iT2ZmaWNlIFRoZW1lIiBpZD0iezYyRjkzOUI2LTkzQUYtNERCOC05QzZCLUQ2QzdERkRDNTg5Rn0iIHZpZD0iezRBM0M0NkU4LTYxQ0MtNDYwMy1BNTg5LTc0MjJBNDdBOEU0QX0iLz48L2E6ZXh0PjwvYTpleHRMc3Q+PC9hOnRoZW1lPlBLAwQUAAAAAAAAACEA4I8FguICAADiAgAAIAAAAHhsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTYueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPHNsaWNlckNhY2hlRGVmaW5pdGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvbWFpbiIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgbmFtZT0iU2xpY2VyX1Byb2pla3QyIiBzb3VyY2VOYW1lPSJQcm9qZWt0Ij48cGl2b3RUYWJsZXM+PHBpdm90VGFibGUgdGFiSWQ9IjYiIG5hbWU9IlBpdm90VGFibGU0Ii8+PC9waXZvdFRhYmxlcz48ZGF0YT48dGFidWxhciBwaXZvdENhY2hlSWQ9IjgiIHNob3dNaXNzaW5nPSIwIj48aXRlbXMgY291bnQ9IjEiPjxpIHg9IjAiIHM9IjEiLz48L2l0ZW1zPjwvdGFidWxhcj48L2RhdGE+PGV4dExzdD48eDpleHQgdXJpPSJ7NDcwNzIyRTAtQUFDRC00QzE3LTlDREMtMTdFRjc2NURCQzdFfSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+PHgxNTpzbGljZXJDYWNoZUhpZGVJdGVtc1dpdGhOb0RhdGEvPjwveDpleHQ+PC9leHRMc3Q+PC9zbGljZXJDYWNoZURlZmluaXRpb24+UEsDBBQAAAAAAAAAIQAAFA6rzgIAAM4CAAAgAAAAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlOS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8c2xpY2VyQ2FjaGVEZWZpbml0aW9uIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMDkvOS9tYWluIiB4bWxuczptYz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL21hcmt1cC1jb21wYXRpYmlsaXR5LzIwMDYiIG1jOklnbm9yYWJsZT0ieCIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIiBuYW1lPSJTbGljZXJfRGF0dW0xIiBzb3VyY2VOYW1lPSJEYXR1bSI+PHBpdm90VGFibGVzPjxwaXZvdFRhYmxlIHRhYklkPSI1IiBuYW1lPSJQaXZvdFRhYmxlMiIvPjwvcGl2b3RUYWJsZXM+PGRhdGE+PHRhYnVsYXIgcGl2b3RDYWNoZUlkPSI3Ij48aXRlbXMgY291bnQ9IjEiPjxpIHg9IjAiIHM9IjEiLz48L2l0ZW1zPjwvdGFidWxhcj48L2RhdGE+PGV4dExzdD48eDpleHQgdXJpPSJ7NDcwNzIyRTAtQUFDRC00QzE3LTlDREMtMTdFRjc2NURCQzdFfSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+PHgxNTpzbGljZXJDYWNoZUhpZGVJdGVtc1dpdGhOb0RhdGEvPjwveDpleHQ+PC9leHRMc3Q+PC9zbGljZXJDYWNoZURlZmluaXRpb24+UEsDBBQAAAAAAAAAIQCGNmI4zgIAAM4CAAAhAAAAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlMTAueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPHNsaWNlckNhY2hlRGVmaW5pdGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvbWFpbiIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgbmFtZT0iU2xpY2VyX0RhdHVtMiIgc291cmNlTmFtZT0iRGF0dW0iPjxwaXZvdFRhYmxlcz48cGl2b3RUYWJsZSB0YWJJZD0iNiIgbmFtZT0iUGl2b3RUYWJsZTQiLz48L3Bpdm90VGFibGVzPjxkYXRhPjx0YWJ1bGFyIHBpdm90Q2FjaGVJZD0iOCI+PGl0ZW1zIGNvdW50PSIxIj48aSB4PSIwIiBzPSIxIi8+PC9pdGVtcz48L3RhYnVsYXI+PC9kYXRhPjxleHRMc3Q+PHg6ZXh0IHVyaT0iezQ3MDcyMkUwLUFBQ0QtNEMxNy05Q0RDLTE3RUY3NjVEQkM3RX0iIHhtbG5zOngxNT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDEwLzExL21haW4iPjx4MTU6c2xpY2VyQ2FjaGVIaWRlSXRlbXNXaXRoTm9EYXRhLz48L3g6ZXh0PjwvZXh0THN0Pjwvc2xpY2VyQ2FjaGVEZWZpbml0aW9uPlBLAwQUAAAAAAAAACEAjds41s0CAADNAgAAIAAAAHhsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTgueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPHNsaWNlckNhY2hlRGVmaW5pdGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2Uvc3ByZWFkc2hlZXRtbC8yMDA5LzkvbWFpbiIgeG1sbnM6bWM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9tYXJrdXAtY29tcGF0aWJpbGl0eS8yMDA2IiBtYzpJZ25vcmFibGU9IngiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgbmFtZT0iU2xpY2VyX0RhdHVtIiBzb3VyY2VOYW1lPSJEYXR1bSI+PHBpdm90VGFibGVzPjxwaXZvdFRhYmxlIHRhYklkPSI0IiBuYW1lPSJQaXZvdFRhYmxlMSIvPjwvcGl2b3RUYWJsZXM+PGRhdGE+PHRhYnVsYXIgcGl2b3RDYWNoZUlkPSI2Ij48aXRlbXMgY291bnQ9IjEiPjxpIHg9IjAiIHM9IjEiLz48L2l0ZW1zPjwvdGFidWxhcj48L2RhdGE+PGV4dExzdD48eDpleHQgdXJpPSJ7NDcwNzIyRTAtQUFDRC00QzE3LTlDREMtMTdFRjc2NURCQzdFfSIgeG1sbnM6eDE1PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9zcHJlYWRzaGVldG1sLzIwMTAvMTEvbWFpbiI+PHgxNTpzbGljZXJDYWNoZUhpZGVJdGVtc1dpdGhOb0RhdGEvPjwveDpleHQ+PC9leHRMc3Q+PC9zbGljZXJDYWNoZURlZmluaXRpb24+UEsDBBQAAAAAAAAAIQDT8Pvi2wMAANsDAAAQAAAAZG9jUHJvcHMvYXBwLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxQcm9wZXJ0aWVzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9leHRlbmRlZC1wcm9wZXJ0aWVzIiB4bWxuczp2dD0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvZG9jUHJvcHNWVHlwZXMiPjxBcHBsaWNhdGlvbj5NaWNyb3NvZnQgRXhjZWw8L0FwcGxpY2F0aW9uPjxEb2NTZWN1cml0eT4wPC9Eb2NTZWN1cml0eT48U2NhbGVDcm9wPmZhbHNlPC9TY2FsZUNyb3A+PEhlYWRpbmdQYWlycz48dnQ6dmVjdG9yIHNpemU9IjIiIGJhc2VUeXBlPSJ2YXJpYW50Ij48dnQ6dmFyaWFudD48dnQ6bHBzdHI+V29ya3NoZWV0czwvdnQ6bHBzdHI+PC92dDp2YXJpYW50Pjx2dDp2YXJpYW50Pjx2dDppND42PC92dDppND48L3Z0OnZhcmlhbnQ+PC92dDp2ZWN0b3I+PC9IZWFkaW5nUGFpcnM+PFRpdGxlc09mUGFydHM+PHZ0OnZlY3RvciBzaXplPSI2IiBiYXNlVHlwZT0ibHBzdHIiPjx2dDpscHN0cj5EYXRhX0FnZW50c3RhdGlzdGlrPC92dDpscHN0cj48dnQ6bHBzdHI+RGF0YV9Qcm9qZWt0c3RhdGlzdGlrPC92dDpscHN0cj48dnQ6bHBzdHI+RGF0YV9VdGZhbGxzc3RhdGlzdGlrPC92dDpscHN0cj48dnQ6bHBzdHI+QWdlbnRzdGF0czwvdnQ6bHBzdHI+PHZ0Omxwc3RyPlByb2pla3RzdGF0czwvdnQ6bHBzdHI+PHZ0Omxwc3RyPlV0ZmFsbHNzdGF0czwvdnQ6bHBzdHI+PC92dDp2ZWN0b3I+PC9UaXRsZXNPZlBhcnRzPjxDb21wYW55PkxveHlzb2Z0PC9Db21wYW55PjxMaW5rc1VwVG9EYXRlPmZhbHNlPC9MaW5rc1VwVG9EYXRlPjxTaGFyZWREb2M+ZmFsc2U8L1NoYXJlZERvYz48SHlwZXJsaW5rc0NoYW5nZWQ+ZmFsc2U8L0h5cGVybGlua3NDaGFuZ2VkPjxBcHBWZXJzaW9uPjE1LjAzMDA8L0FwcFZlcnNpb24+PC9Qcm9wZXJ0aWVzPlBLAwQUAAAAAAAAACEApdNTwqsNAACrDQAAJwAAAHhsL3Bpdm90Q2FjaGUvcGl2b3RDYWNoZURlZmluaXRpb24xLnhtbO+7vzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiPz4NCjxwaXZvdENhY2hlRGVmaW5pdGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIiB4bWxuczpyPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzIiBzYXZlRGF0YT0iMCIgcmVmcmVzaE9uTG9hZD0iMSIgcmVmcmVzaGVkQnk9IlJpY2thcmQgTWFnbnVzc29uIiByZWZyZXNoZWREYXRlPSI0MjI5Ni41MDA4NTM1ODc5NjEiIGNyZWF0ZWRWZXJzaW9uPSI1IiByZWZyZXNoZWRWZXJzaW9uPSI1IiBtaW5SZWZyZXNoYWJsZVZlcnNpb249IjMiIHJlY29yZENvdW50PSIxIj4NCiAgPGNhY2hlU291cmNlIHR5cGU9IndvcmtzaGVldCI+DQogICAgPHdvcmtzaGVldFNvdXJjZSBuYW1lPSJEYXRhX0FnZW50c3RhdGlzdGlrX1RhYmxlIiAvPg0KICA8L2NhY2hlU291cmNlPg0KICA8Y2FjaGVGaWVsZHMgY291bnQ9IjIyIj4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJQcm9qZWt0IiBudW1GbXRJZD0iMCI+DQogICAgICA8c2hhcmVkSXRlbXMgY291bnQ9IjEiPg0KICAgICAgICA8cyB2PSIgIiAvPg0KICAgICAgPC9zaGFyZWRJdGVtcz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iU8OkbGphcmUiIG51bUZtdElkPSIwIj4NCiAgICAgIDxzaGFyZWRJdGVtcyBjb3VudD0iMSI+DQogICAgICAgIDxzIHY9IiAiIC8+DQogICAgICA8L3NoYXJlZEl0ZW1zPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJEYXR1bSIgbnVtRm10SWQ9IjE0Ij4NCiAgICAgIDxzaGFyZWRJdGVtcyBjb3VudD0iMSI+DQogICAgICAgIDxzIHY9IiAiIC8+DQogICAgICA8L3NoYXJlZEl0ZW1zPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJPdmVya3NhbSB0aWQiIG51bUZtdElkPSIxNjQiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IlbDpG50ZXRpZCIgbnVtRm10SWQ9IjE2NCI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iSGFudGVyaW5nc3RpZCIgbnVtRm10SWQ9IjE2NCI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iSW5sb2dnYWQgdGlkIiBudW1GbXRJZD0iMTY0Ij4NCiAgICAgIDxzaGFyZWRJdGVtcyAvPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJTYW10YWxzdGlkIiBudW1GbXRJZD0iMTY0Ij4NCiAgICAgIDxzaGFyZWRJdGVtcyAvPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJFZnRlcmJlYXJiZXRzdGlkIiBudW1GbXRJZD0iMTY0Ij4NCiAgICAgIDxzaGFyZWRJdGVtcyAvPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJHZW5vbXNuaXR0bGlnIHNhbXRhbHN0aWQiIG51bUZtdElkPSIxNjQiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9Ikdlbm9tc25pdHRsaWcgc2FtdGFsc3RpZC9PcmRlciIgbnVtRm10SWQ9IjE2NCI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iUmVnaXN0cmVyYWRlIHNhbXRhbCIgbnVtRm10SWQ9IjIiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IlJlZ2lzdHJlcmFkZSBwcm9zcGVrdCIgbnVtRm10SWQ9IjIiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IkF2YnJ1dGl0IHNhbXRhbHNiZWfDpHJhbiIgbnVtRm10SWQ9IjIiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9Ik92ZXJrc2FtIHRpZCAlIiBudW1GbXRJZD0iMCIgZm9ybXVsYT0iSUYoJ092ZXJrc2FtIHRpZCc9MCwwLCdPdmVya3NhbSB0aWQnLydJbmxvZ2dhZCB0aWQnKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJIYW50ZXJpbmdzdGlkICUiIG51bUZtdElkPSIwIiBmb3JtdWxhPSJJRihIYW50ZXJpbmdzdGlkPTAsMCxIYW50ZXJpbmdzdGlkLydJbmxvZ2dhZCB0aWQnKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJWw6RudGV0aWQgJSIgbnVtRm10SWQ9IjAiIGZvcm11bGE9IklGKFbDpG50ZXRpZD0wLDAsVsOkbnRldGlkLydJbmxvZ2dhZCB0aWQnKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJTYW10YWxzdGlkICUiIG51bUZtdElkPSIwIiBmb3JtdWxhPSJJRihTYW10YWxzdGlkPTAsMCxTYW10YWxzdGlkLydJbmxvZ2dhZCB0aWQnKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJFZnRlcmJlYXJiZXRuaW5nc3RpZCAlIiBudW1GbXRJZD0iMCIgZm9ybXVsYT0iSUYoRWZ0ZXJiZWFyYmV0c3RpZD0wLDAsRWZ0ZXJiZWFyYmV0c3RpZC8nSW5sb2dnYWQgdGlkJykiIGRhdGFiYXNlRmllbGQ9IjAiIC8+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iUmVnaXN0cmVyYWRlIHNhbXRhbC9oIiBudW1GbXRJZD0iMCIgZm9ybXVsYT0iSUYoJ1JlZ2lzdHJlcmFkZSBzYW10YWwnPTAsMCwnUmVnaXN0cmVyYWRlIHNhbXRhbCcvKCdJbmxvZ2dhZCB0aWQnKiAyNCkpIiBkYXRhYmFzZUZpZWxkPSIwIiAvPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IlJlZ2lzdHJlcmFkZSBwcm9zcGVrdC9oIiBudW1GbXRJZD0iMCIgZm9ybXVsYT0iSUYoJ1JlZ2lzdHJlcmFkZSBwcm9zcGVrdCc9MCwwLCdSZWdpc3RyZXJhZGUgcHJvc3Bla3QnLygnSW5sb2dnYWQgdGlkJyogMjQpKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJBdmJydXRpdCBzYW10YWxzYmVnw6RyYW4vaCIgbnVtRm10SWQ9IjAiIGZvcm11bGE9IklGKCdBdmJydXRpdCBzYW10YWxzYmVnw6RyYW4nPTAsMCwnQXZicnV0aXQgc2FtdGFsc2JlZ8OkcmFuJy8oJ0lubG9nZ2FkIHRpZCcqIDI0KSkiIGRhdGFiYXNlRmllbGQ9IjAiIC8+DQogIDwvY2FjaGVGaWVsZHM+DQogIDxleHRMc3Q+DQogICAgPGV4dCB1cmk9Ins3MjVBRTJBRS05NDkxLTQ4YmUtQjJCNC00RUI5NzRGQzMwODR9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPg0KICAgICAgPHgxNDpwaXZvdENhY2hlRGVmaW5pdGlvbiBwaXZvdENhY2hlSWQ9IjYiIC8+DQogICAgPC9leHQ+DQogIDwvZXh0THN0Pg0KPC9waXZvdENhY2hlRGVmaW5pdGlvbj5QSwMEFAAAAAAAAAAhAEsf1WJfCgAAXwoAACcAAAB4bC9waXZvdENhY2hlL3Bpdm90Q2FjaGVEZWZpbml0aW9uMi54bWzvu788P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8cGl2b3RDYWNoZURlZmluaXRpb24geG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgeG1sbnM6cj0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcyIgc2F2ZURhdGE9IjAiIHJlZnJlc2hPbkxvYWQ9IjEiIHJlZnJlc2hlZEJ5PSJSaWNrYXJkIE1hZ251c3NvbiIgcmVmcmVzaGVkRGF0ZT0iNDIyOTYuNTAwODUzNzAzNzA3IiBjcmVhdGVkVmVyc2lvbj0iNSIgcmVmcmVzaGVkVmVyc2lvbj0iNSIgbWluUmVmcmVzaGFibGVWZXJzaW9uPSIzIiByZWNvcmRDb3VudD0iMSI+DQogIDxjYWNoZVNvdXJjZSB0eXBlPSJ3b3Jrc2hlZXQiPg0KICAgIDx3b3Jrc2hlZXRTb3VyY2UgbmFtZT0iRGF0YV9Qcm9qZWt0c3RhdGlzdGlrX1RhYmxlIiAvPg0KICA8L2NhY2hlU291cmNlPg0KICA8Y2FjaGVGaWVsZHMgY291bnQ9IjE1Ij4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJQcm9qZWt0IiBudW1GbXRJZD0iMCI+DQogICAgICA8c2hhcmVkSXRlbXMgY291bnQ9IjEiPg0KICAgICAgICA8cyB2PSIgIiAvPg0KICAgICAgPC9zaGFyZWRJdGVtcz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iU8OkbGphcmUiIG51bUZtdElkPSIyIj4NCiAgICAgIDxzaGFyZWRJdGVtcyBjb3VudD0iMSI+DQogICAgICAgIDxzIHY9IiAiIC8+DQogICAgICA8L3NoYXJlZEl0ZW1zPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJEYXR1bSIgbnVtRm10SWQ9IjE0Ij4NCiAgICAgIDxzaGFyZWRJdGVtcyBjb3VudD0iMSI+DQogICAgICAgIDxzIHY9IiAiIC8+DQogICAgICA8L3NoYXJlZEl0ZW1zPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJSZWdpc3RyZXJhZGUgc2FtdGFsIiBudW1GbXRJZD0iMiI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iUmVnaXN0cmVyYWRlIHByb3NwZWt0IiBudW1GbXRJZD0iMiI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iT3JkZXIiIG51bUZtdElkPSIyIj4NCiAgICAgIDxzaGFyZWRJdGVtcyAvPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJPcmRlciBLb250YWt0ZXIiIG51bUZtdElkPSIyIj4NCiAgICAgIDxzaGFyZWRJdGVtcyAvPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJUZWxlZm9uc3ZhcmFyZSIgbnVtRm10SWQ9IjIiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IsOFdGVydXBwcmluZ25pbmdhciIgbnVtRm10SWQ9IjIiPg0KICAgICAgPHNoYXJlZEl0ZW1zIC8+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IkFudGFsIE5laiBUYWNrIiBudW1GbXRJZD0iMiI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iQW50YWwgQm9ydGZhbGwiIG51bUZtdElkPSIyIj4NCiAgICAgIDxzaGFyZWRJdGVtcyAvPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJIaXRyYXRlICUiIG51bUZtdElkPSIwIiBmb3JtdWxhPSJJRignQW50YWwgTmVqIFRhY2snKydPcmRlciBLb250YWt0ZXInPTAsMCwnT3JkZXIgS29udGFrdGVyJy8gKCdBbnRhbCBOZWogVGFjaycrJ09yZGVyIEtvbnRha3RlcicpKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJTYW10YWwgLSBUZWwuU3ZhciIgbnVtRm10SWQ9IjAiIGZvcm11bGE9IklGKCdSZWdpc3RyZXJhZGUgc2FtdGFsJz0wLDAsJ1JlZ2lzdHJlcmFkZSBzYW10YWwnLVRlbGVmb25zdmFyYXJlKSIgZGF0YWJhc2VGaWVsZD0iMCIgLz4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSIlIGxpdmUgc2FtdGFsIiBudW1GbXRJZD0iMCIgZm9ybXVsYT0iIElGKCdSZWdpc3RyZXJhZGUgc2FtdGFsJz0wLDAsKCdSZWdpc3RyZXJhZGUgc2FtdGFsJy1UZWxlZm9uc3ZhcmFyZSkgLydSZWdpc3RyZXJhZGUgc2FtdGFsJykiIGRhdGFiYXNlRmllbGQ9IjAiIC8+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iT3JkZXIvbGl2ZSBzYW10YWwiIG51bUZtdElkPSIwIiBmb3JtdWxhPSJJRignUmVnaXN0cmVyYWRlIHNhbXRhbCc9MCwwLCdPcmRlciBLb250YWt0ZXInLydSZWdpc3RyZXJhZGUgc2FtdGFsJykiIGRhdGFiYXNlRmllbGQ9IjAiIC8+DQogIDwvY2FjaGVGaWVsZHM+DQogIDxleHRMc3Q+DQogICAgPGV4dCB1cmk9Ins3MjVBRTJBRS05NDkxLTQ4YmUtQjJCNC00RUI5NzRGQzMwODR9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPg0KICAgICAgPHgxNDpwaXZvdENhY2hlRGVmaW5pdGlvbiBwaXZvdENhY2hlSWQ9IjciIC8+DQogICAgPC9leHQ+DQogIDwvZXh0THN0Pg0KPC9waXZvdENhY2hlRGVmaW5pdGlvbj5QSwMEFAAAAAAAAAAhAK6BZ845BgAAOQYAACcAAAB4bC9waXZvdENhY2hlL3Bpdm90Q2FjaGVEZWZpbml0aW9uMy54bWzvu788P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8cGl2b3RDYWNoZURlZmluaXRpb24geG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9zcHJlYWRzaGVldG1sLzIwMDYvbWFpbiIgeG1sbnM6cj0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcyIgc2F2ZURhdGE9IjAiIHJlZnJlc2hPbkxvYWQ9IjEiIHJlZnJlc2hlZEJ5PSJSaWNrYXJkIE1hZ251c3NvbiIgcmVmcmVzaGVkRGF0ZT0iNDIyOTYuNTAwODUzODE5NDQ1IiBjcmVhdGVkVmVyc2lvbj0iNSIgcmVmcmVzaGVkVmVyc2lvbj0iNSIgbWluUmVmcmVzaGFibGVWZXJzaW9uPSIzIiByZWNvcmRDb3VudD0iMSI+DQogIDxjYWNoZVNvdXJjZSB0eXBlPSJ3b3Jrc2hlZXQiPg0KICAgIDx3b3Jrc2hlZXRTb3VyY2UgbmFtZT0iRGF0YV9VdGZhbGxzc3RhdGlzdGlrX1RhYmxlIiAvPg0KICA8L2NhY2hlU291cmNlPg0KICA8Y2FjaGVGaWVsZHMgY291bnQ9IjYiPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IkRhdHVtIiBudW1GbXRJZD0iMTQiPg0KICAgICAgPHNoYXJlZEl0ZW1zIGNvdW50PSIxIj4NCiAgICAgICAgPHMgdj0iICIgLz4NCiAgICAgIDwvc2hhcmVkSXRlbXM+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IlByb2pla3QiIG51bUZtdElkPSIwIj4NCiAgICAgIDxzaGFyZWRJdGVtcyBjb3VudD0iMSI+DQogICAgICAgIDxzIHY9IiAiIC8+DQogICAgICA8L3NoYXJlZEl0ZW1zPg0KICAgIDwvY2FjaGVGaWVsZD4NCiAgICA8Y2FjaGVGaWVsZCBuYW1lPSJBbnbDpG5kYXJlIiBudW1GbXRJZD0iMCI+DQogICAgICA8c2hhcmVkSXRlbXMgY291bnQ9IjEiPg0KICAgICAgICA8cyB2PSIgIiAvPg0KICAgICAgPC9zaGFyZWRJdGVtcz4NCiAgICA8L2NhY2hlRmllbGQ+DQogICAgPGNhY2hlRmllbGQgbmFtZT0iVXRmYWxsc3R5cCIgbnVtRm10SWQ9IjAiPg0KICAgICAgPHNoYXJlZEl0ZW1zIGNvdW50PSIxIj4NCiAgICAgICAgPHMgdj0iICIgLz4NCiAgICAgIDwvc2hhcmVkSXRlbXM+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IlV0ZmFsbCIgbnVtRm10SWQ9IjAiPg0KICAgICAgPHNoYXJlZEl0ZW1zIGNvdW50PSIxIj4NCiAgICAgICAgPHMgdj0iICIgLz4NCiAgICAgIDwvc2hhcmVkSXRlbXM+DQogICAgPC9jYWNoZUZpZWxkPg0KICAgIDxjYWNoZUZpZWxkIG5hbWU9IkFudGFsIiBudW1GbXRJZD0iMiI+DQogICAgICA8c2hhcmVkSXRlbXMgLz4NCiAgICA8L2NhY2hlRmllbGQ+DQogIDwvY2FjaGVGaWVsZHM+DQogIDxleHRMc3Q+DQogICAgPGV4dCB1cmk9Ins3MjVBRTJBRS05NDkxLTQ4YmUtQjJCNC00RUI5NzRGQzMwODR9IiB4bWxuczp4MTQ9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL3NwcmVhZHNoZWV0bWwvMjAwOS85L21haW4iPg0KICAgICAgPHgxNDpwaXZvdENhY2hlRGVmaW5pdGlvbiBwaXZvdENhY2hlSWQ9IjgiIC8+DQogICAgPC9leHQ+DQogIDwvZXh0THN0Pg0KPC9waXZvdENhY2hlRGVmaW5pdGlvbj5QSwMEFAAAAAAAAAAhAP////9DAQAAQwEAABAAAABbdHJhc2hdLzAwMDMuZGF0/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQSwMEFAAAAAAAAAAhANd5tdEPFAAADxQAABQAAAB4bC9zaGFyZWRTdHJpbmdzLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJ5ZXMiID8+PHNzdCB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3NwcmVhZHNoZWV0bWwvMjAwNi9tYWluIiBjb3VudD0iMTE4IiB1bmlxdWVDb3VudD0iMTE4Ij48c2k+PHQ+QW52w6RuZGFyZTwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj5JbmxvZ2dhZCB0aWQgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPkhhbnRlcmluZ3N0aWQgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPiUgIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj5TYW10YWxzdGlkIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj4lICAgIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj5FZnRlcmJlYXJiZXRzdGlkIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj4lICAgICA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+VsOkbnRldGlkIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj4lICAgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPk92ZXJrc2FtIHRpZCA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+JSA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+R2Vub21zbml0dGxpZyBzYW10YWxzdGlkIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj4vT3JkZXIgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPlJlZ2lzdHJlcmFkZSBzYW10YWwgPC90Pjwvc2k+PHNpPjx0Pi9oPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPlJlZ2lzdHJlcmFkZSBwcm9zcGVrdCA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+L2ggPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPkF2YnJ1dGl0IHNhbXRhbHNiZWfDpHJhbiA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+L2ggIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj4gPC90Pjwvc2k+PHNpPjx0PiNESVYvMCE8L3Q+PC9zaT48c2k+PHQ+VG90YWx0PC90Pjwvc2k+PHNpPjx0PkRhdHVtPC90Pjwvc2k+PHNpPjx0PlByb2pla3Q8L3Q+PC9zaT48c2k+PHQ+VXRmYWxsc3R5cDwvdD48L3NpPjxzaT48dD5VdGZhbGw8L3Q+PC9zaT48c2k+PHQ+QW50YWw8L3Q+PC9zaT48c2k+PHQ+UmVsaW54IC0gSGFsZWJvcF9SZWJpbmQ8L3Q+PC9zaT48c2k+PHQ+RGFuaWVsIENlZGVycXVpc3QgKHMxMDNfZGFuY2VkXzczMyBJRDogMTM0NTgpPC90Pjwvc2k+PHNpPjx0Pk9yZGVyPC90Pjwvc2k+PHNpPjx0PkphIHRhY2shPC90Pjwvc2k+PHNpPjx0PkJvcnRmYWxsPC90Pjwvc2k+PHNpPjx0Pk5laiBidW5kZW4gdGlsbCBhbm5hbiBsZXZlcmFudMO2ciAxMi0yNCBtw6VuYWRlciBOZWo8L3Q+PC9zaT48c2k+PHQ+TmVqIHJlZGFuIGbDtnJsw6RuZ3Q8L3Q+PC9zaT48c2k+PHQ+RWogT3JkZXI8L3Q+PC9zaT48c2k+PHQ+TmVqIGVqIGJlc2x1dHNmYXR0YXJlPC90Pjwvc2k+PHNpPjx0Pk5laiB2aWxsIGVqIGJpbmRhIHNpZzwvdD48L3NpPjxzaT48dD5OZWogdmlsbCBlaiBseXNzbmEgcMOlIGVyYmp1ZGFuZGV0PC90Pjwvc2k+PHNpPjx0Pk1pbyBMaW5qb3MgKHMxMDNfTWlvTGluXzY1NiBJRDogMTMwNDgpPC90Pjwvc2k+PHNpPjx0Pk5laiBlaiBzcHLDpWtiYXI8L3Q+PC9zaT48c2k+PHQ+THVjYXMgT25lZ8OlcmQgT25lZ8OlcmQgKHMxMDNfbHVjb25lXzc0NiBJRDogMTM1MDIpPC90Pjwvc2k+PHNpPjx0Pk5laiBidW5kZW4gdGlsbCBhbm5hbiBsZXZlcmFudMO2ciA2LTEyIG3DpW5hZGVyPC90Pjwvc2k+PHNpPjx0PkVoaSBLaW95YW1lIChzMTAzX2VoaUBwb3N0LmNvbSBJRDogMTM4MDApPC90Pjwvc2k+PHNpPjx0Pk1lbHZpbiBCZXJnIChzMTAzX21lbGxlYmVyZzE0QGdtYWlsLmNvbSBJRDogMTM4MTYpPC90Pjwvc2k+PHNpPjx0Pk5laiBmZWwgdGVsZWZvbm51bW1lcjwvdD48L3NpPjxzaT48dD5BeGVsIENhcmzDqW4gKHMxMDNfbGlkaW5nby4xODEzMUBnbWFpbC5jb20gSUQ6IDEzODIzKTwvdD48L3NpPjxzaT48dD5HcmVlbnBlYWNlPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPk9za2FyICBXZW5uYmVyZyAoczEwM19vc2thcjc4d2JlcmdAZ21haWwuY29tIElEOiAxMzg5Nyk8L3Q+PC9zaT48c2k+PHQ+Qm9ydGZhbGwsIGF2bGlkZW48L3Q+PC9zaT48c2k+PHQ+Qm9ydGZhbGwsIGZlbCBudW1tZXI8L3Q+PC9zaT48c2k+PHQ+Qm9ydGZhbGwsIGJlZmludGxpZyBtw6VuYWRzZ2l2YXJlPC90Pjwvc2k+PHNpPjx0Pk5laiB0YWNrPC90Pjwvc2k+PHNpPjx0Pk5laiB0YWNrLCBnZXIgZW5nw6VuZ3Nnw6V2b3I8L3Q+PC9zaT48c2k+PHQ+TmVqIHRhY2ssIGdlciByZWRhbiB0aWxsIGFuZHJhIG9yZ2FuaXNhdGlvbmVyPC90Pjwvc2k+PHNpPjx0Pk5laiB0YWNrLCBoYXIgaW50ZSByw6VkIFBlbnNpb27DpHI8L3Q+PC9zaT48c2k+PHQ+TmVqIHRhY2ssIGhhciBpbnRlIHLDpWQgZsO2ciB0aWxsZsOkbGxldDwvdD48L3NpPjxzaT48dD5OZWogdGFjaywgaGFyIGludGUgc3ZlbnNrIGJhbms8L3Q+PC9zaT48c2k+PHQ+TGVvIErDtm5zc29uIChzMTAzX2xlb2pvbnNzb24xNEBnbWFpbC5jb20gSUQ6IDEzODQ5KTwvdD48L3NpPjxzaT48dD5LYXNwZXIgTWFkZXN0YW0gKHMxMDNfa21hZGVzdGFtQGdtYWlsLmNvbSBJRDogMTM5MTEpPC90Pjwvc2k+PHNpPjx0Pk5laiB0YWNrLCBoYXIgaW50ZSByw6VkPC90Pjwvc2k+PHNpPjx0Pk5laiB0YWNrLCBtaXNzbsO2amQgbWVkIEdQPC90Pjwvc2k+PHNpPjx0PkZyaWRhIFRvcm5ldXMgKHMxMDNfZnJpZGF0b3JuZXVzQGhvdG1haWwuY29tIElEOiAxMzk2Nyk8L3Q+PC9zaT48c2k+PHQ+TmVqIHRhY2ssIHZpbGwgaW50ZSBoYSBzYW10YWw8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+UGllcnJlICBXYWxsYmVyZyAoczEwM19waWVycmV3OTU5QGdtYWlsLmNvbSBJRDogMTM5NzIpPC90Pjwvc2k+PHNpPjx0Pk5laiB0YWNrLCBoYXIgaW50ZSByw6VkIFN0dWRlbnQ8L3Q+PC9zaT48c2k+PHQ+Qm9ydGZhbGwsIHVuZGVyIDE4PC90Pjwvc2k+PHNpPjx0PllvdXNzZWYgSGFkZGFtIChzMTAzX2hhZGRhbXlvdXNzZWYwQGdtYWlsLmNvbSBJRDogMTM5MDYpPC90Pjwvc2k+PHNpPjx0Pk5laiBmw7ZyZXRhZ3NrdW5kPC90Pjwvc2k+PHNpPjx0PkJvcnRmYWxsIG1heCBhbnRhbCByaW5nZsO2cnPDtmsgZ2pvcmRhIGVsbGVyIG9naWx0aWc8L3Q+PC9zaT48c2k+PHQ+TmVqIG9jaCB2aWxsIHV0dHJ5Y2tsaWdlbiBibGkgc3DDpHJyYWQgZsO2ciBmbGVyIHDDpXI8L3Q+PC9zaT48c2k+PHQ+TmVqIGhhciByZWRhbiBibGl2aXQga29udGFrdGFkIGFuZ8OlZW5kZSBIYWxlYm9wIHNlPC90Pjwvc2k+PHNpPjx0Pk5laiBkeXJ0PC90Pjwvc2k+PHNpPjx0PkpvZWwgV2luYmVyZyAoczEwM19qdjk1NTE5MDBAZ21haWwuY29tIElEOiAxMzkxMCk8L3Q+PC9zaT48c2k+PHQ+QWRhbSBCdXJzdHLDtm0gKHMxMDNfQWRhbS5zLmJ1cnN0cm9tQGdtYWlsLmNvbSBJRDogMTM5MDMpPC90Pjwvc2k+PHNpPjx0Pk5laiBnb2QgbWFuPC90Pjwvc2k+PHNpPjx0Pk5laiBhdnNsYWcga3JlZGl0a29udHJvbGw8L3Q+PC9zaT48c2k+PHQ+Qm9ydGZhbGwsIGdvZCBtYW48L3Q+PC9zaT48c2k+PHQ+R3JlZW5wZWFjZSBVcGdyYWRlPC90Pjwvc2k+PHNpPjx0PlPDpGxqYXJlPC90Pjwvc2k+PHNpPjx0PlJlZ2lzdHJlcmFkZSBzYW10YWw8L3Q+PC9zaT48c2k+PHQ+UmVnaXN0cmVyYWRlIHByb3NwZWt0PC90Pjwvc2k+PHNpPjx0Pk9yZGVyIEtvbnRha3RlcjwvdD48L3NpPjxzaT48dD5UZWxlZm9uc3ZhcmFyZTwvdD48L3NpPjxzaT48dD7DhXRlcnVwcHJpbmduaW5nYXI8L3Q+PC9zaT48c2k+PHQ+QW50YWwgTmVqIFRhY2s8L3Q+PC9zaT48c2k+PHQ+QW50YWwgQm9ydGZhbGw8L3Q+PC9zaT48c2k+PHQ+MjAyNi0wNS0wNDwvdD48L3NpPjxzaT48dD4yMDI2LTA1LTA1PC90Pjwvc2k+PHNpPjx0PkFtcmVmPC90Pjwvc2k+PHNpPjx0PjIwMjYtMDUtMDY8L3Q+PC9zaT48c2k+PHQ+MjAyNi0wNS0wNzwvdD48L3NpPjxzaT48dD4yMDI2LTA1LTA4PC90Pjwvc2k+PHNpPjx0PjIwMjYtMDUtMTE8L3Q+PC9zaT48c2k+PHQ+MjAyNi0wNS0xMjwvdD48L3NpPjxzaT48dD4yMDI2LTA1LTEzPC90Pjwvc2k+PHNpPjx0PjIwMjYtMDUtMTU8L3Q+PC9zaT48c2k+PHQ+MjAyNi0wNS0xODwvdD48L3NpPjxzaT48dD4yMDI2LTA1LTE5PC90Pjwvc2k+PHNpPjx0Pk92ZXJrc2FtIHRpZDwvdD48L3NpPjxzaT48dD5Ww6RudGV0aWQ8L3Q+PC9zaT48c2k+PHQ+SGFudGVyaW5nc3RpZDwvdD48L3NpPjxzaT48dD5JbmxvZ2dhZCB0aWQ8L3Q+PC9zaT48c2k+PHQ+U2FtdGFsc3RpZDwvdD48L3NpPjxzaT48dD5FZnRlcmJlYXJiZXRzdGlkPC90Pjwvc2k+PHNpPjx0Pkdlbm9tc25pdHRsaWcgc2FtdGFsc3RpZDwvdD48L3NpPjxzaT48dD5HZW5vbXNuaXR0bGlnIHNhbXRhbHN0aWQvT3JkZXI8L3Q+PC9zaT48c2k+PHQ+QXZicnV0aXQgc2FtdGFsc2JlZ8OkcmFuPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPk9yZGVyIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj5PcmRlciBLb250YWt0ZXIgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPkhpdHJhdGUgJSA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+U2FtdGFsIC0gVGVsLlN2YXIgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPiUgbGl2ZSBzYW10YWwgPC90Pjwvc2k+PHNpPjx0IHhtbDpzcGFjZT0icHJlc2VydmUiPk9yZGVyL2xpdmUgc2FtdGFsIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj5UZWxlZm9uc3ZhcmFyZSA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+w4V0ZXJ1cHByaW5nbmluZ2FyIDwvdD48L3NpPjxzaT48dCB4bWw6c3BhY2U9InByZXNlcnZlIj5BbnRhbCBOZWogVGFjayA8L3Q+PC9zaT48c2k+PHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+QW50YWwgQm9ydGZhbGwgPC90Pjwvc2k+PC9zc3Q+UEsDBBQAAAAAAAAAIQDpoUZutjgAALY4AAATAAAAY3VzdG9tWG1sL2l0ZW0xLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9InV0Zi04Ij8+PGN0OmNvbnRlbnRUeXBlU2NoZW1hIGN0Ol89IiIgbWE6Xz0iIiBtYTpjb250ZW50VHlwZU5hbWU9ImRva3VtZW50IiBtYTpjb250ZW50VHlwZUlEPSIweDAxMDEwMEZGQjg0MjlFNjlEMTQ2NDFCMUYzOEQ3NDUzNzY2QkU3IiBtYTpjb250ZW50VHlwZVZlcnNpb249IjE3IiBtYTpjb250ZW50VHlwZURlc2NyaXB0aW9uPSJTa2FwYSBldHQgbnl0dCBkb2t1bWVudC4iIG1hOmNvbnRlbnRUeXBlU2NvcGU9IiIgbWE6dmVyc2lvbklEPSI2YWE1NGNiNDk0OGIyM2UyMjExZWJlNmEyZjI1OTYxNCIgeG1sbnM6Y3Q9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDYvbWV0YWRhdGEvY29udGVudFR5cGUiIHhtbG5zOm1hPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA2L21ldGFkYXRhL3Byb3BlcnRpZXMvbWV0YUF0dHJpYnV0ZXMiPg0KPHhzZDpzY2hlbWEgdGFyZ2V0TmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA2L21ldGFkYXRhL3Byb3BlcnRpZXMiIG1hOnJvb3Q9InRydWUiIG1hOmZpZWxkc0lEPSI0MTE5ZjVkNGY0MDIwM2Y1NjU2NjViZjQ0MTcyY2VlYyIgbnMyOl89IiIgbnMzOl89IiIgeG1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpwPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA2L21ldGFkYXRhL3Byb3BlcnRpZXMiIHhtbG5zOm5zMj0iOGNjNmY0YTAtY2E1Yi00NmVlLWE2ZTYtNjM3YzE1NmZkNWNiIiB4bWxuczpuczM9IjhjMDI2NmQ2LTczZTMtNGIwNy1iY2QxLTBiM2NjOGRhNTI3MiI+DQo8eHNkOmltcG9ydCBuYW1lc3BhY2U9IjhjYzZmNGEwLWNhNWItNDZlZS1hNmU2LTYzN2MxNTZmZDVjYiIvPg0KPHhzZDppbXBvcnQgbmFtZXNwYWNlPSI4YzAyNjZkNi03M2UzLTRiMDctYmNkMS0wYjNjYzhkYTUyNzIiLz4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJwcm9wZXJ0aWVzIj4NCjx4c2Q6Y29tcGxleFR5cGU+DQo8eHNkOnNlcXVlbmNlPg0KPHhzZDplbGVtZW50IG5hbWU9ImRvY3VtZW50TWFuYWdlbWVudCI+DQo8eHNkOmNvbXBsZXhUeXBlPg0KPHhzZDphbGw+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6TWVkaWFTZXJ2aWNlTWV0YWRhdGEiIG1pbk9jY3Vycz0iMCIvPg0KPHhzZDplbGVtZW50IHJlZj0ibnMyOk1lZGlhU2VydmljZUZhc3RNZXRhZGF0YSIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6TWVkaWFTZXJ2aWNlQXV0b0tleVBvaW50cyIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6TWVkaWFTZXJ2aWNlS2V5UG9pbnRzIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCByZWY9Im5zMjpNZWRpYVNlcnZpY2VBdXRvVGFncyIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6TWVkaWFTZXJ2aWNlT0NSIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCByZWY9Im5zMjpNZWRpYVNlcnZpY2VHZW5lcmF0aW9uVGltZSIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6TWVkaWFTZXJ2aWNlRXZlbnRIYXNoQ29kZSIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6TWVkaWFTZXJ2aWNlRGF0ZVRha2VuIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCByZWY9Im5zMjpNZWRpYUxlbmd0aEluU2Vjb25kcyIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczM6U2hhcmVkV2l0aFVzZXJzIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCByZWY9Im5zMzpTaGFyZWRXaXRoRGV0YWlscyIgbWluT2NjdXJzPSIwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJuczI6bGNmNzZmMTU1Y2VkNGRkY2I0MDk3MTM0ZmYzYzMzMmYiIG1pbk9jY3Vycz0iMCIvPg0KPHhzZDplbGVtZW50IHJlZj0ibnMzOlRheENhdGNoQWxsIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCByZWY9Im5zMjpNZWRpYVNlcnZpY2VPYmplY3REZXRlY3RvclZlcnNpb25zIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCByZWY9Im5zMjpNZWRpYVNlcnZpY2VTZWFyY2hQcm9wZXJ0aWVzIiBtaW5PY2N1cnM9IjAiLz4NCjwveHNkOmFsbD4NCjwveHNkOmNvbXBsZXhUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjwveHNkOnNlcXVlbmNlPg0KPC94c2Q6Y29tcGxleFR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPC94c2Q6c2NoZW1hPg0KPHhzZDpzY2hlbWEgdGFyZ2V0TmFtZXNwYWNlPSI4Y2M2ZjRhMC1jYTViLTQ2ZWUtYTZlNi02MzdjMTU2ZmQ1Y2IiIGVsZW1lbnRGb3JtRGVmYXVsdD0icXVhbGlmaWVkIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOmRtcz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNi9kb2N1bWVudE1hbmFnZW1lbnQvdHlwZXMiIHhtbG5zOnBjPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9pbmZvcGF0aC8yMDA3L1BhcnRuZXJDb250cm9scyI+DQo8eHNkOmltcG9ydCBuYW1lc3BhY2U9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDYvZG9jdW1lbnRNYW5hZ2VtZW50L3R5cGVzIi8+DQo8eHNkOmltcG9ydCBuYW1lc3BhY2U9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2luZm9wYXRoLzIwMDcvUGFydG5lckNvbnRyb2xzIi8+DQo8eHNkOmVsZW1lbnQgbmFtZT0iTWVkaWFTZXJ2aWNlTWV0YWRhdGEiIG1hOmluZGV4PSI4IiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9Ik1lZGlhU2VydmljZU1ldGFkYXRhIiBtYTpoaWRkZW49InRydWUiIG1hOmludGVybmFsTmFtZT0iTWVkaWFTZXJ2aWNlTWV0YWRhdGEiIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6c2ltcGxlVHlwZT4NCjx4c2Q6cmVzdHJpY3Rpb24gYmFzZT0iZG1zOk5vdGUiLz4NCjwveHNkOnNpbXBsZVR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPHhzZDplbGVtZW50IG5hbWU9Ik1lZGlhU2VydmljZUZhc3RNZXRhZGF0YSIgbWE6aW5kZXg9IjkiIG5pbGxhYmxlPSJ0cnVlIiBtYTpkaXNwbGF5TmFtZT0iTWVkaWFTZXJ2aWNlRmFzdE1ldGFkYXRhIiBtYTpoaWRkZW49InRydWUiIG1hOmludGVybmFsTmFtZT0iTWVkaWFTZXJ2aWNlRmFzdE1ldGFkYXRhIiBtYTpyZWFkT25seT0idHJ1ZSI+DQo8eHNkOnNpbXBsZVR5cGU+DQo8eHNkOnJlc3RyaWN0aW9uIGJhc2U9ImRtczpOb3RlIi8+DQo8L3hzZDpzaW1wbGVUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJNZWRpYVNlcnZpY2VBdXRvS2V5UG9pbnRzIiBtYTppbmRleD0iMTAiIG5pbGxhYmxlPSJ0cnVlIiBtYTpkaXNwbGF5TmFtZT0iTWVkaWFTZXJ2aWNlQXV0b0tleVBvaW50cyIgbWE6aGlkZGVuPSJ0cnVlIiBtYTppbnRlcm5hbE5hbWU9Ik1lZGlhU2VydmljZUF1dG9LZXlQb2ludHMiIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6c2ltcGxlVHlwZT4NCjx4c2Q6cmVzdHJpY3Rpb24gYmFzZT0iZG1zOk5vdGUiLz4NCjwveHNkOnNpbXBsZVR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPHhzZDplbGVtZW50IG5hbWU9Ik1lZGlhU2VydmljZUtleVBvaW50cyIgbWE6aW5kZXg9IjExIiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9IktleVBvaW50cyIgbWE6aW50ZXJuYWxOYW1lPSJNZWRpYVNlcnZpY2VLZXlQb2ludHMiIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6c2ltcGxlVHlwZT4NCjx4c2Q6cmVzdHJpY3Rpb24gYmFzZT0iZG1zOk5vdGUiPg0KPHhzZDptYXhMZW5ndGggdmFsdWU9IjI1NSIvPg0KPC94c2Q6cmVzdHJpY3Rpb24+DQo8L3hzZDpzaW1wbGVUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJNZWRpYVNlcnZpY2VBdXRvVGFncyIgbWE6aW5kZXg9IjEyIiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9IlRhZ3MiIG1hOmludGVybmFsTmFtZT0iTWVkaWFTZXJ2aWNlQXV0b1RhZ3MiIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6c2ltcGxlVHlwZT4NCjx4c2Q6cmVzdHJpY3Rpb24gYmFzZT0iZG1zOlRleHQiLz4NCjwveHNkOnNpbXBsZVR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPHhzZDplbGVtZW50IG5hbWU9Ik1lZGlhU2VydmljZU9DUiIgbWE6aW5kZXg9IjEzIiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9IkV4dHJhY3RlZCBUZXh0IiBtYTppbnRlcm5hbE5hbWU9Ik1lZGlhU2VydmljZU9DUiIgbWE6cmVhZE9ubHk9InRydWUiPg0KPHhzZDpzaW1wbGVUeXBlPg0KPHhzZDpyZXN0cmljdGlvbiBiYXNlPSJkbXM6Tm90ZSI+DQo8eHNkOm1heExlbmd0aCB2YWx1ZT0iMjU1Ii8+DQo8L3hzZDpyZXN0cmljdGlvbj4NCjwveHNkOnNpbXBsZVR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPHhzZDplbGVtZW50IG5hbWU9Ik1lZGlhU2VydmljZUdlbmVyYXRpb25UaW1lIiBtYTppbmRleD0iMTQiIG5pbGxhYmxlPSJ0cnVlIiBtYTpkaXNwbGF5TmFtZT0iTWVkaWFTZXJ2aWNlR2VuZXJhdGlvblRpbWUiIG1hOmhpZGRlbj0idHJ1ZSIgbWE6aW50ZXJuYWxOYW1lPSJNZWRpYVNlcnZpY2VHZW5lcmF0aW9uVGltZSIgbWE6cmVhZE9ubHk9InRydWUiPg0KPHhzZDpzaW1wbGVUeXBlPg0KPHhzZDpyZXN0cmljdGlvbiBiYXNlPSJkbXM6VGV4dCIvPg0KPC94c2Q6c2ltcGxlVHlwZT4NCjwveHNkOmVsZW1lbnQ+DQo8eHNkOmVsZW1lbnQgbmFtZT0iTWVkaWFTZXJ2aWNlRXZlbnRIYXNoQ29kZSIgbWE6aW5kZXg9IjE1IiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9Ik1lZGlhU2VydmljZUV2ZW50SGFzaENvZGUiIG1hOmhpZGRlbj0idHJ1ZSIgbWE6aW50ZXJuYWxOYW1lPSJNZWRpYVNlcnZpY2VFdmVudEhhc2hDb2RlIiBtYTpyZWFkT25seT0idHJ1ZSI+DQo8eHNkOnNpbXBsZVR5cGU+DQo8eHNkOnJlc3RyaWN0aW9uIGJhc2U9ImRtczpUZXh0Ii8+DQo8L3hzZDpzaW1wbGVUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJNZWRpYVNlcnZpY2VEYXRlVGFrZW4iIG1hOmluZGV4PSIxNiIgbmlsbGFibGU9InRydWUiIG1hOmRpc3BsYXlOYW1lPSJNZWRpYVNlcnZpY2VEYXRlVGFrZW4iIG1hOmhpZGRlbj0idHJ1ZSIgbWE6aW50ZXJuYWxOYW1lPSJNZWRpYVNlcnZpY2VEYXRlVGFrZW4iIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6c2ltcGxlVHlwZT4NCjx4c2Q6cmVzdHJpY3Rpb24gYmFzZT0iZG1zOlRleHQiLz4NCjwveHNkOnNpbXBsZVR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPHhzZDplbGVtZW50IG5hbWU9Ik1lZGlhTGVuZ3RoSW5TZWNvbmRzIiBtYTppbmRleD0iMTciIG5pbGxhYmxlPSJ0cnVlIiBtYTpkaXNwbGF5TmFtZT0iTWVkaWFMZW5ndGhJblNlY29uZHMiIG1hOmhpZGRlbj0idHJ1ZSIgbWE6aW50ZXJuYWxOYW1lPSJNZWRpYUxlbmd0aEluU2Vjb25kcyIgbWE6cmVhZE9ubHk9InRydWUiPg0KPHhzZDpzaW1wbGVUeXBlPg0KPHhzZDpyZXN0cmljdGlvbiBiYXNlPSJkbXM6VW5rbm93biIvPg0KPC94c2Q6c2ltcGxlVHlwZT4NCjwveHNkOmVsZW1lbnQ+DQo8eHNkOmVsZW1lbnQgbmFtZT0ibGNmNzZmMTU1Y2VkNGRkY2I0MDk3MTM0ZmYzYzMzMmYiIG1hOmluZGV4PSIyMSIgbmlsbGFibGU9InRydWUiIG1hOnRheG9ub215PSJ0cnVlIiBtYTppbnRlcm5hbE5hbWU9ImxjZjc2ZjE1NWNlZDRkZGNiNDA5NzEzNGZmM2MzMzJmIiBtYTp0YXhvbm9teUZpZWxkTmFtZT0iTWVkaWFTZXJ2aWNlSW1hZ2VUYWdzIiBtYTpkaXNwbGF5TmFtZT0iQmlsZG1hcmtlcmluZ2FyIiBtYTpyZWFkT25seT0iZmFsc2UiIG1hOmZpZWxkSWQ9Ins1Y2Y3NmYxNS01Y2VkLTRkZGMtYjQwOS03MTM0ZmYzYzMzMmZ9IiBtYTp0YXhvbm9teU11bHRpPSJ0cnVlIiBtYTpzc3BJZD0iNjMxM2U3MmQtMzY0NC00MDM2LWFmYmItN2RjOTZjMGY1MTE0IiBtYTp0ZXJtU2V0SWQ9IjA5ODE0Y2QzLTU2OGUtZmU5MC05ODE0LThkNjIxZmY4ZmI4NCIgbWE6YW5jaG9ySWQ9ImZiYTU0ZmIzLWMzZTEtZmU4MS1hNzc2LWNhNGI2OTE0OGM0ZCIgbWE6b3Blbj0idHJ1ZSIgbWE6aXNLZXl3b3JkPSJmYWxzZSI+DQo8eHNkOmNvbXBsZXhUeXBlPg0KPHhzZDpzZXF1ZW5jZT4NCjx4c2Q6ZWxlbWVudCByZWY9InBjOlRlcm1zIiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0iMSI+PC94c2Q6ZWxlbWVudD4NCjwveHNkOnNlcXVlbmNlPg0KPC94c2Q6Y29tcGxleFR5cGU+DQo8L3hzZDplbGVtZW50Pg0KPHhzZDplbGVtZW50IG5hbWU9Ik1lZGlhU2VydmljZU9iamVjdERldGVjdG9yVmVyc2lvbnMiIG1hOmluZGV4PSIyMyIgbmlsbGFibGU9InRydWUiIG1hOmRpc3BsYXlOYW1lPSJNZWRpYVNlcnZpY2VPYmplY3REZXRlY3RvclZlcnNpb25zIiBtYTpoaWRkZW49InRydWUiIG1hOmluZGV4ZWQ9InRydWUiIG1hOmludGVybmFsTmFtZT0iTWVkaWFTZXJ2aWNlT2JqZWN0RGV0ZWN0b3JWZXJzaW9ucyIgbWE6cmVhZE9ubHk9InRydWUiPg0KPHhzZDpzaW1wbGVUeXBlPg0KPHhzZDpyZXN0cmljdGlvbiBiYXNlPSJkbXM6VGV4dCIvPg0KPC94c2Q6c2ltcGxlVHlwZT4NCjwveHNkOmVsZW1lbnQ+DQo8eHNkOmVsZW1lbnQgbmFtZT0iTWVkaWFTZXJ2aWNlU2VhcmNoUHJvcGVydGllcyIgbWE6aW5kZXg9IjI0IiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9Ik1lZGlhU2VydmljZVNlYXJjaFByb3BlcnRpZXMiIG1hOmhpZGRlbj0idHJ1ZSIgbWE6aW50ZXJuYWxOYW1lPSJNZWRpYVNlcnZpY2VTZWFyY2hQcm9wZXJ0aWVzIiBtYTpyZWFkT25seT0idHJ1ZSI+DQo8eHNkOnNpbXBsZVR5cGU+DQo8eHNkOnJlc3RyaWN0aW9uIGJhc2U9ImRtczpOb3RlIi8+DQo8L3hzZDpzaW1wbGVUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjwveHNkOnNjaGVtYT4NCjx4c2Q6c2NoZW1hIHRhcmdldE5hbWVzcGFjZT0iOGMwMjY2ZDYtNzNlMy00YjA3LWJjZDEtMGIzY2M4ZGE1MjcyIiBlbGVtZW50Rm9ybURlZmF1bHQ9InF1YWxpZmllZCIgeG1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpkbXM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDYvZG9jdW1lbnRNYW5hZ2VtZW50L3R5cGVzIiB4bWxuczpwYz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvaW5mb3BhdGgvMjAwNy9QYXJ0bmVyQ29udHJvbHMiPg0KPHhzZDppbXBvcnQgbmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA2L2RvY3VtZW50TWFuYWdlbWVudC90eXBlcyIvPg0KPHhzZDppbXBvcnQgbmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS9pbmZvcGF0aC8yMDA3L1BhcnRuZXJDb250cm9scyIvPg0KPHhzZDplbGVtZW50IG5hbWU9IlNoYXJlZFdpdGhVc2VycyIgbWE6aW5kZXg9IjE4IiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9IkRlbGF0IG1lZCIgbWE6aW50ZXJuYWxOYW1lPSJTaGFyZWRXaXRoVXNlcnMiIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6Y29tcGxleFR5cGU+DQo8eHNkOmNvbXBsZXhDb250ZW50Pg0KPHhzZDpleHRlbnNpb24gYmFzZT0iZG1zOlVzZXJNdWx0aSI+DQo8eHNkOnNlcXVlbmNlPg0KPHhzZDplbGVtZW50IG5hbWU9IlVzZXJJbmZvIiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0idW5ib3VuZGVkIj4NCjx4c2Q6Y29tcGxleFR5cGU+DQo8eHNkOnNlcXVlbmNlPg0KPHhzZDplbGVtZW50IG5hbWU9IkRpc3BsYXlOYW1lIiB0eXBlPSJ4c2Q6c3RyaW5nIiBtaW5PY2N1cnM9IjAiLz4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJBY2NvdW50SWQiIHR5cGU9ImRtczpVc2VySWQiIG1pbk9jY3Vycz0iMCIgbmlsbGFibGU9InRydWUiLz4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJBY2NvdW50VHlwZSIgdHlwZT0ieHNkOnN0cmluZyIgbWluT2NjdXJzPSIwIi8+DQo8L3hzZDpzZXF1ZW5jZT4NCjwveHNkOmNvbXBsZXhUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjwveHNkOnNlcXVlbmNlPg0KPC94c2Q6ZXh0ZW5zaW9uPg0KPC94c2Q6Y29tcGxleENvbnRlbnQ+DQo8L3hzZDpjb21wbGV4VHlwZT4NCjwveHNkOmVsZW1lbnQ+DQo8eHNkOmVsZW1lbnQgbmFtZT0iU2hhcmVkV2l0aERldGFpbHMiIG1hOmluZGV4PSIxOSIgbmlsbGFibGU9InRydWUiIG1hOmRpc3BsYXlOYW1lPSJEZWxhdCBtZWQgaW5mb3JtYXRpb24iIG1hOmludGVybmFsTmFtZT0iU2hhcmVkV2l0aERldGFpbHMiIG1hOnJlYWRPbmx5PSJ0cnVlIj4NCjx4c2Q6c2ltcGxlVHlwZT4NCjx4c2Q6cmVzdHJpY3Rpb24gYmFzZT0iZG1zOk5vdGUiPg0KPHhzZDptYXhMZW5ndGggdmFsdWU9IjI1NSIvPg0KPC94c2Q6cmVzdHJpY3Rpb24+DQo8L3hzZDpzaW1wbGVUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJUYXhDYXRjaEFsbCIgbWE6aW5kZXg9IjIyIiBuaWxsYWJsZT0idHJ1ZSIgbWE6ZGlzcGxheU5hbWU9IlRheG9ub215IENhdGNoIEFsbCBDb2x1bW4iIG1hOmhpZGRlbj0idHJ1ZSIgbWE6bGlzdD0ie2Q0MDJlM2YyLTRhYWEtNDIwNS1hMzllLTJlMWUxODE4ZTYwY30iIG1hOmludGVybmFsTmFtZT0iVGF4Q2F0Y2hBbGwiIG1hOnNob3dGaWVsZD0iQ2F0Y2hBbGxEYXRhIiBtYTp3ZWI9IjhjMDI2NmQ2LTczZTMtNGIwNy1iY2QxLTBiM2NjOGRhNTI3MiI+DQo8eHNkOmNvbXBsZXhUeXBlPg0KPHhzZDpjb21wbGV4Q29udGVudD4NCjx4c2Q6ZXh0ZW5zaW9uIGJhc2U9ImRtczpNdWx0aUNob2ljZUxvb2t1cCI+DQo8eHNkOnNlcXVlbmNlPg0KPHhzZDplbGVtZW50IG5hbWU9IlZhbHVlIiB0eXBlPSJkbXM6TG9va3VwIiBtYXhPY2N1cnM9InVuYm91bmRlZCIgbWluT2NjdXJzPSIwIiBuaWxsYWJsZT0idHJ1ZSIvPg0KPC94c2Q6c2VxdWVuY2U+DQo8L3hzZDpleHRlbnNpb24+DQo8L3hzZDpjb21wbGV4Q29udGVudD4NCjwveHNkOmNvbXBsZXhUeXBlPg0KPC94c2Q6ZWxlbWVudD4NCjwveHNkOnNjaGVtYT4NCjx4c2Q6c2NoZW1hIHRhcmdldE5hbWVzcGFjZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3BhY2thZ2UvMjAwNi9tZXRhZGF0YS9jb3JlLXByb3BlcnRpZXMiIGVsZW1lbnRGb3JtRGVmYXVsdD0icXVhbGlmaWVkIiBhdHRyaWJ1dGVGb3JtRGVmYXVsdD0idW5xdWFsaWZpZWQiIGJsb2NrRGVmYXVsdD0iI2FsbCIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvbWV0YWRhdGEvY29yZS1wcm9wZXJ0aWVzIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOmRjdGVybXM9Imh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy8iIHhtbG5zOm9kb2M9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vaW50ZXJuYWwvb2JkIj4NCjx4c2Q6aW1wb3J0IG5hbWVzcGFjZT0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHNjaGVtYUxvY2F0aW9uPSJodHRwOi8vZHVibGluY29yZS5vcmcvc2NoZW1hcy94bWxzL3FkYy8yMDAzLzA0LzAyL2RjLnhzZCIvPg0KPHhzZDppbXBvcnQgbmFtZXNwYWNlPSJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvIiBzY2hlbWFMb2NhdGlvbj0iaHR0cDovL2R1YmxpbmNvcmUub3JnL3NjaGVtYXMveG1scy9xZGMvMjAwMy8wNC8wMi9kY3Rlcm1zLnhzZCIvPg0KPHhzZDplbGVtZW50IG5hbWU9ImNvcmVQcm9wZXJ0aWVzIiB0eXBlPSJDVF9jb3JlUHJvcGVydGllcyIvPg0KPHhzZDpjb21wbGV4VHlwZSBuYW1lPSJDVF9jb3JlUHJvcGVydGllcyI+DQo8eHNkOmFsbD4NCjx4c2Q6ZWxlbWVudCByZWY9ImRjOmNyZWF0b3IiIG1pbk9jY3Vycz0iMCIgbWF4T2NjdXJzPSIxIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJkY3Rlcm1zOmNyZWF0ZWQiIG1pbk9jY3Vycz0iMCIgbWF4T2NjdXJzPSIxIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJkYzppZGVudGlmaWVyIiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0iMSIvPg0KPHhzZDplbGVtZW50IG5hbWU9ImNvbnRlbnRUeXBlIiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0iMSIgdHlwZT0ieHNkOnN0cmluZyIgbWE6aW5kZXg9IjAiIG1hOmRpc3BsYXlOYW1lPSJJbm5laMOlbGxzdHlwIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJkYzp0aXRsZSIgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9IjEiIG1hOmluZGV4PSI0IiBtYTpkaXNwbGF5TmFtZT0iUnVicmlrIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJkYzpzdWJqZWN0IiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0iMSIvPg0KPHhzZDplbGVtZW50IHJlZj0iZGM6ZGVzY3JpcHRpb24iIG1pbk9jY3Vycz0iMCIgbWF4T2NjdXJzPSIxIi8+DQo8eHNkOmVsZW1lbnQgbmFtZT0ia2V5d29yZHMiIG1pbk9jY3Vycz0iMCIgbWF4T2NjdXJzPSIxIiB0eXBlPSJ4c2Q6c3RyaW5nIi8+DQo8eHNkOmVsZW1lbnQgcmVmPSJkYzpsYW5ndWFnZSIgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9IjEiLz4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJjYXRlZ29yeSIgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9IjEiIHR5cGU9InhzZDpzdHJpbmciLz4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJ2ZXJzaW9uIiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0iMSIgdHlwZT0ieHNkOnN0cmluZyIvPg0KPHhzZDplbGVtZW50IG5hbWU9InJldmlzaW9uIiBtaW5PY2N1cnM9IjAiIG1heE9jY3Vycz0iMSIgdHlwZT0ieHNkOnN0cmluZyI+DQo8eHNkOmFubm90YXRpb24+DQo8eHNkOmRvY3VtZW50YXRpb24+DQogICAgICAgICAgICAgICAgICAgICAgICBUaGlzIHZhbHVlIGluZGljYXRlcyB0aGUgbnVtYmVyIG9mIHNhdmVzIG9yIHJldmlzaW9ucy4gVGhlIGFwcGxpY2F0aW9uIGlzIHJlc3BvbnNpYmxlIGZvciB1cGRhdGluZyB0aGlzIHZhbHVlIGFmdGVyIGVhY2ggcmV2aXNpb24uDQogICAgICAgICAgICAgICAgICAgIDwveHNkOmRvY3VtZW50YXRpb24+DQo8L3hzZDphbm5vdGF0aW9uPg0KPC94c2Q6ZWxlbWVudD4NCjx4c2Q6ZWxlbWVudCBuYW1lPSJsYXN0TW9kaWZpZWRCeSIgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9IjEiIHR5cGU9InhzZDpzdHJpbmciLz4NCjx4c2Q6ZWxlbWVudCByZWY9ImRjdGVybXM6bW9kaWZpZWQiIG1pbk9jY3Vycz0iMCIgbWF4T2NjdXJzPSIxIi8+DQo8eHNkOmVsZW1lbnQgbmFtZT0iY29udGVudFN0YXR1cyIgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9IjEiIHR5cGU9InhzZDpzdHJpbmciLz4NCjwveHNkOmFsbD4NCjwveHNkOmNvbXBsZXhUeXBlPg0KPC94c2Q6c2NoZW1hPg0KPHhzOnNjaGVtYSB0YXJnZXROYW1lc3BhY2U9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlL2luZm9wYXRoLzIwMDcvUGFydG5lckNvbnRyb2xzIiBlbGVtZW50Rm9ybURlZmF1bHQ9InF1YWxpZmllZCIgYXR0cmlidXRlRm9ybURlZmF1bHQ9InVucXVhbGlmaWVkIiB4bWxuczpwYz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvaW5mb3BhdGgvMjAwNy9QYXJ0bmVyQ29udHJvbHMiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+DQo8eHM6ZWxlbWVudCBuYW1lPSJQZXJzb24iPg0KPHhzOmNvbXBsZXhUeXBlPg0KPHhzOnNlcXVlbmNlPg0KPHhzOmVsZW1lbnQgcmVmPSJwYzpEaXNwbGF5TmFtZSIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkFjY291bnRJZCIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkFjY291bnRUeXBlIiBtaW5PY2N1cnM9IjAiPjwveHM6ZWxlbWVudD4NCjwveHM6c2VxdWVuY2U+DQo8L3hzOmNvbXBsZXhUeXBlPg0KPC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgbmFtZT0iRGlzcGxheU5hbWUiIHR5cGU9InhzOnN0cmluZyI+PC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgbmFtZT0iQWNjb3VudElkIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IG5hbWU9IkFjY291bnRUeXBlIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IG5hbWU9IkJEQ0Fzc29jaWF0ZWRFbnRpdHkiPg0KPHhzOmNvbXBsZXhUeXBlPg0KPHhzOnNlcXVlbmNlPg0KPHhzOmVsZW1lbnQgcmVmPSJwYzpCRENFbnRpdHkiIG1pbk9jY3Vycz0iMCIgbWF4T2NjdXJzPSJ1bmJvdW5kZWQiPjwveHM6ZWxlbWVudD4NCjwveHM6c2VxdWVuY2U+DQo8eHM6YXR0cmlidXRlIHJlZj0icGM6RW50aXR5TmFtZXNwYWNlIj48L3hzOmF0dHJpYnV0ZT4NCjx4czphdHRyaWJ1dGUgcmVmPSJwYzpFbnRpdHlOYW1lIj48L3hzOmF0dHJpYnV0ZT4NCjx4czphdHRyaWJ1dGUgcmVmPSJwYzpTeXN0ZW1JbnN0YW5jZU5hbWUiPjwveHM6YXR0cmlidXRlPg0KPHhzOmF0dHJpYnV0ZSByZWY9InBjOkFzc29jaWF0aW9uTmFtZSI+PC94czphdHRyaWJ1dGU+DQo8L3hzOmNvbXBsZXhUeXBlPg0KPC94czplbGVtZW50Pg0KPHhzOmF0dHJpYnV0ZSBuYW1lPSJFbnRpdHlOYW1lc3BhY2UiIHR5cGU9InhzOnN0cmluZyI+PC94czphdHRyaWJ1dGU+DQo8eHM6YXR0cmlidXRlIG5hbWU9IkVudGl0eU5hbWUiIHR5cGU9InhzOnN0cmluZyI+PC94czphdHRyaWJ1dGU+DQo8eHM6YXR0cmlidXRlIG5hbWU9IlN5c3RlbUluc3RhbmNlTmFtZSIgdHlwZT0ieHM6c3RyaW5nIj48L3hzOmF0dHJpYnV0ZT4NCjx4czphdHRyaWJ1dGUgbmFtZT0iQXNzb2NpYXRpb25OYW1lIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6YXR0cmlidXRlPg0KPHhzOmVsZW1lbnQgbmFtZT0iQkRDRW50aXR5Ij4NCjx4czpjb21wbGV4VHlwZT4NCjx4czpzZXF1ZW5jZT4NCjx4czplbGVtZW50IHJlZj0icGM6RW50aXR5RGlzcGxheU5hbWUiIG1pbk9jY3Vycz0iMCI+PC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgcmVmPSJwYzpFbnRpdHlJbnN0YW5jZVJlZmVyZW5jZSIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkVudGl0eUlkMSIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkVudGl0eUlkMiIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkVudGl0eUlkMyIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkVudGl0eUlkNCIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCByZWY9InBjOkVudGl0eUlkNSIgbWluT2NjdXJzPSIwIj48L3hzOmVsZW1lbnQ+DQo8L3hzOnNlcXVlbmNlPg0KPC94czpjb21wbGV4VHlwZT4NCjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IG5hbWU9IkVudGl0eURpc3BsYXlOYW1lIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IG5hbWU9IkVudGl0eUluc3RhbmNlUmVmZXJlbmNlIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IG5hbWU9IkVudGl0eUlkMSIgdHlwZT0ieHM6c3RyaW5nIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCBuYW1lPSJFbnRpdHlJZDIiIHR5cGU9InhzOnN0cmluZyI+PC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgbmFtZT0iRW50aXR5SWQzIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IG5hbWU9IkVudGl0eUlkNCIgdHlwZT0ieHM6c3RyaW5nIj48L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCBuYW1lPSJFbnRpdHlJZDUiIHR5cGU9InhzOnN0cmluZyI+PC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgbmFtZT0iVGVybXMiPg0KPHhzOmNvbXBsZXhUeXBlPg0KPHhzOnNlcXVlbmNlPg0KPHhzOmVsZW1lbnQgcmVmPSJwYzpUZXJtSW5mbyIgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9InVuYm91bmRlZCI+PC94czplbGVtZW50Pg0KPC94czpzZXF1ZW5jZT4NCjwveHM6Y29tcGxleFR5cGU+DQo8L3hzOmVsZW1lbnQ+DQo8eHM6ZWxlbWVudCBuYW1lPSJUZXJtSW5mbyI+DQo8eHM6Y29tcGxleFR5cGU+DQo8eHM6c2VxdWVuY2U+DQo8eHM6ZWxlbWVudCByZWY9InBjOlRlcm1OYW1lIiBtaW5PY2N1cnM9IjAiPjwveHM6ZWxlbWVudD4NCjx4czplbGVtZW50IHJlZj0icGM6VGVybUlkIiBtaW5PY2N1cnM9IjAiPjwveHM6ZWxlbWVudD4NCjwveHM6c2VxdWVuY2U+DQo8L3hzOmNvbXBsZXhUeXBlPg0KPC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgbmFtZT0iVGVybU5hbWUiIHR5cGU9InhzOnN0cmluZyI+PC94czplbGVtZW50Pg0KPHhzOmVsZW1lbnQgbmFtZT0iVGVybUlkIiB0eXBlPSJ4czpzdHJpbmciPjwveHM6ZWxlbWVudD4NCjwveHM6c2NoZW1hPg0KPC9jdDpjb250ZW50VHlwZVNjaGVtYT5QSwMEFAAAAAAAAAAhAL1/wPfJAAAAyQAAABgAAABjdXN0b21YbWwvaXRlbVByb3BzMS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ibm8iPz4NCjxkczpkYXRhc3RvcmVJdGVtIGRzOml0ZW1JRD0iezY5QzdDRDUyLTM3OTktNERFQi04NTFCLURBNDVFNEJGRjE1Nn0iIHhtbG5zOmRzPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9jdXN0b21YbWwiLz5QSwMEFAAAAAAAAAAhAL2EYiPbAAAA2wAAABMAAABjdXN0b21YbWwvaXRlbTIueG1sPD9tc28tY29udGVudFR5cGU/PjxGb3JtVGVtcGxhdGVzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3NoYXJlcG9pbnQvdjMvY29udGVudHR5cGUvZm9ybXMiPjxEaXNwbGF5PkRvY3VtZW50TGlicmFyeUZvcm08L0Rpc3BsYXk+PEVkaXQ+RG9jdW1lbnRMaWJyYXJ5Rm9ybTwvRWRpdD48TmV3PkRvY3VtZW50TGlicmFyeUZvcm08L05ldz48L0Zvcm1UZW1wbGF0ZXM+UEsDBBQAAAAAAAAAIQB9eGcbyQAAAMkAAAAYAAAAY3VzdG9tWG1sL2l0ZW1Qcm9wczIueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8ZHM6ZGF0YXN0b3JlSXRlbSBkczppdGVtSUQ9InsxQTQ0MjdGQy00MjRCLTRBNjMtQkUyMC05ODJGNzQxNTNBOTZ9IiB4bWxuczpkcz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvY3VzdG9tWG1sIi8+UEsDBBQAAAAAAAAAIQB0Pzl6KAEAACgBAAAeAAAAY3VzdG9tWG1sL19yZWxzL2l0ZW0xLnhtbC5yZWxzPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPFJlbGF0aW9uc2hpcHMgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvcmVsYXRpb25zaGlwcyI+PFJlbGF0aW9uc2hpcCBJZD0icklkMSIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9jdXN0b21YbWxQcm9wcyIgVGFyZ2V0PSJpdGVtUHJvcHMxLnhtbCIvPjwvUmVsYXRpb25zaGlwcz5QSwMEFAAAAAAAAAAhAFyWJyIoAQAAKAEAAB4AAABjdXN0b21YbWwvX3JlbHMvaXRlbTIueG1sLnJlbHM8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8UmVsYXRpb25zaGlwcyB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3BhY2thZ2UvMjAwNi9yZWxhdGlvbnNoaXBzIj48UmVsYXRpb25zaGlwIElkPSJySWQxIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL2N1c3RvbVhtbFByb3BzIiBUYXJnZXQ9Iml0ZW1Qcm9wczIueG1sIi8+PC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAAAAACEA/////2MCAABjAgAAEAAAAFt0cmFzaF0vMDAwNC5kYXT/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBLAwQUAAAAAAAAACEAwE7ej48OAACPDgAAGgAAAHhsL19yZWxzL3dvcmtib29rLnhtbC5yZWxzPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPFJlbGF0aW9uc2hpcHMgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvcmVsYXRpb25zaGlwcyI+PFJlbGF0aW9uc2hpcCBJZD0icklkOCIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9waXZvdENhY2hlRGVmaW5pdGlvbiIgVGFyZ2V0PSJwaXZvdENhY2hlL3Bpdm90Q2FjaGVEZWZpbml0aW9uMi54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxMyIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlckNhY2hlIiBUYXJnZXQ9InNsaWNlckNhY2hlcy9zbGljZXJDYWNoZTQueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMTgiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDcvcmVsYXRpb25zaGlwcy9zbGljZXJDYWNoZSIgVGFyZ2V0PSJzbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU5LnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDMiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvd29ya3NoZWV0IiBUYXJnZXQ9IndvcmtzaGVldHMvc2hlZXQzLnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDIxIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3N0eWxlcyIgVGFyZ2V0PSJzdHlsZXMueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkNyIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9waXZvdENhY2hlRGVmaW5pdGlvbiIgVGFyZ2V0PSJwaXZvdENhY2hlL3Bpdm90Q2FjaGVEZWZpbml0aW9uMS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxMiIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlckNhY2hlIiBUYXJnZXQ9InNsaWNlckNhY2hlcy9zbGljZXJDYWNoZTMueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMTciIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDcvcmVsYXRpb25zaGlwcy9zbGljZXJDYWNoZSIgVGFyZ2V0PSJzbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU4LnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDI1IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL2N1c3RvbVhtbCIgVGFyZ2V0PSIuLi9jdXN0b21YbWwvaXRlbTMueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMiIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy93b3Jrc2hlZXQiIFRhcmdldD0id29ya3NoZWV0cy9zaGVldDIueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMTYiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDcvcmVsYXRpb25zaGlwcy9zbGljZXJDYWNoZSIgVGFyZ2V0PSJzbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU3LnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDIwIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3RoZW1lIiBUYXJnZXQ9InRoZW1lL3RoZW1lMS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3dvcmtzaGVldCIgVGFyZ2V0PSJ3b3Jrc2hlZXRzL3NoZWV0MS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQ2IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3dvcmtzaGVldCIgVGFyZ2V0PSJ3b3Jrc2hlZXRzL3NoZWV0Ni54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxMSIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlckNhY2hlIiBUYXJnZXQ9InNsaWNlckNhY2hlcy9zbGljZXJDYWNoZTIueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMjQiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvY3VzdG9tWG1sIiBUYXJnZXQ9Ii4uL2N1c3RvbVhtbC9pdGVtMi54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQ1IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3dvcmtzaGVldCIgVGFyZ2V0PSJ3b3Jrc2hlZXRzL3NoZWV0NS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxNSIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlckNhY2hlIiBUYXJnZXQ9InNsaWNlckNhY2hlcy9zbGljZXJDYWNoZTYueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMjMiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L3JlbGF0aW9uc2hpcHMvY3VzdG9tWG1sIiBUYXJnZXQ9Ii4uL2N1c3RvbVhtbC9pdGVtMS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQxMCIgVHlwZT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNy9yZWxhdGlvbnNoaXBzL3NsaWNlckNhY2hlIiBUYXJnZXQ9InNsaWNlckNhY2hlcy9zbGljZXJDYWNoZTEueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMTkiIFR5cGU9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMDcvcmVsYXRpb25zaGlwcy9zbGljZXJDYWNoZSIgVGFyZ2V0PSJzbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGUxMC54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQ0IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3dvcmtzaGVldCIgVGFyZ2V0PSJ3b3Jrc2hlZXRzL3NoZWV0NC54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQ5IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL3Bpdm90Q2FjaGVEZWZpbml0aW9uIiBUYXJnZXQ9InBpdm90Q2FjaGUvcGl2b3RDYWNoZURlZmluaXRpb24zLnhtbCIvPjxSZWxhdGlvbnNoaXAgSWQ9InJJZDE0IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA3L3JlbGF0aW9uc2hpcHMvc2xpY2VyQ2FjaGUiIFRhcmdldD0ic2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlNS54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQyMiIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9zaGFyZWRTdHJpbmdzIiBUYXJnZXQ9InNoYXJlZFN0cmluZ3MueG1sIi8+PC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAAAAACEAmp/u5EsCAABLAgAAEwAAAGN1c3RvbVhtbC9pdGVtMy54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtOCI/PjxwOnByb3BlcnRpZXMgeG1sbnM6cD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNi9tZXRhZGF0YS9wcm9wZXJ0aWVzIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczpwYz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvaW5mb3BhdGgvMjAwNy9QYXJ0bmVyQ29udHJvbHMiPjxkb2N1bWVudE1hbmFnZW1lbnQ+PGxjZjc2ZjE1NWNlZDRkZGNiNDA5NzEzNGZmM2MzMzJmIHhtbG5zPSI4Y2M2ZjRhMC1jYTViLTQ2ZWUtYTZlNi02MzdjMTU2ZmQ1Y2IiPjxUZXJtcyB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvaW5mb3BhdGgvMjAwNy9QYXJ0bmVyQ29udHJvbHMiPjwvVGVybXM+PC9sY2Y3NmYxNTVjZWQ0ZGRjYjQwOTcxMzRmZjNjMzMyZj48VGF4Q2F0Y2hBbGwgeG1sbnM9IjhjMDI2NmQ2LTczZTMtNGIwNy1iY2QxLTBiM2NjOGRhNTI3MiIgeHNpOm5pbD0idHJ1ZSIvPjwvZG9jdW1lbnRNYW5hZ2VtZW50PjwvcDpwcm9wZXJ0aWVzPlBLAwQUAAAAAAAAACEAaYfda8kAAADJAAAAGAAAAGN1c3RvbVhtbC9pdGVtUHJvcHMzLnhtbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBzdGFuZGFsb25lPSJubyI/Pg0KPGRzOmRhdGFzdG9yZUl0ZW0gZHM6aXRlbUlEPSJ7QUE5REZFNjgtNzEwNy00ODRELTkwQTAtNjEzRUFGMDk3MDE1fSIgeG1sbnM6ZHM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9vZmZpY2VEb2N1bWVudC8yMDA2L2N1c3RvbVhtbCIvPlBLAwQUAAAAAAAAACEA8+ZUIJIBAACSAQAAEwAAAGRvY1Byb3BzL2N1c3RvbS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8UHJvcGVydGllcyB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvY3VzdG9tLXByb3BlcnRpZXMiIHhtbG5zOnZ0PSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9kb2NQcm9wc1ZUeXBlcyI+PHByb3BlcnR5IGZtdGlkPSJ7RDVDREQ1MDUtMkU5Qy0xMDFCLTkzOTctMDgwMDJCMkNGOUFFfSIgcGlkPSIyIiBuYW1lPSJDb250ZW50VHlwZUlkIj48dnQ6bHB3c3RyPjB4MDEwMTAwRkZCODQyOUU2OUQxNDY0MUIxRjM4RDc0NTM3NjZCRTc8L3Z0Omxwd3N0cj48L3Byb3BlcnR5PjwvUHJvcGVydGllcz5QSwMEFAAAAAAAAAAhAHvzAqMoAQAAKAEAAB4AAABjdXN0b21YbWwvX3JlbHMvaXRlbTMueG1sLnJlbHM8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8UmVsYXRpb25zaGlwcyB4bWxucz0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL3BhY2thZ2UvMjAwNi9yZWxhdGlvbnNoaXBzIj48UmVsYXRpb25zaGlwIElkPSJySWQxIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL2N1c3RvbVhtbFByb3BzIiBUYXJnZXQ9Iml0ZW1Qcm9wczMueG1sIi8+PC9SZWxhdGlvbnNoaXBzPlBLAwQUAAAAAAAAACEAE16+Zd8CAADfAgAACwAAAF9yZWxzLy5yZWxzPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPFJlbGF0aW9uc2hpcHMgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvcmVsYXRpb25zaGlwcyI+PFJlbGF0aW9uc2hpcCBJZD0icklkMyIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9leHRlbmRlZC1wcm9wZXJ0aWVzIiBUYXJnZXQ9ImRvY1Byb3BzL2FwcC54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQyIiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L3JlbGF0aW9uc2hpcHMvbWV0YWRhdGEvY29yZS1wcm9wZXJ0aWVzIiBUYXJnZXQ9ImRvY1Byb3BzL2NvcmUueG1sIi8+PFJlbGF0aW9uc2hpcCBJZD0icklkMSIgVHlwZT0iaHR0cDovL3NjaGVtYXMub3BlbnhtbGZvcm1hdHMub3JnL29mZmljZURvY3VtZW50LzIwMDYvcmVsYXRpb25zaGlwcy9vZmZpY2VEb2N1bWVudCIgVGFyZ2V0PSJ4bC93b3JrYm9vay54bWwiLz48UmVsYXRpb25zaGlwIElkPSJySWQ0IiBUeXBlPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvb2ZmaWNlRG9jdW1lbnQvMjAwNi9yZWxhdGlvbnNoaXBzL2N1c3RvbS1wcm9wZXJ0aWVzIiBUYXJnZXQ9ImRvY1Byb3BzL2N1c3RvbS54bWwiLz48L1JlbGF0aW9uc2hpcHM+UEsDBBQAAAAAAAAAIQCnop4ccwIAAHMCAAARAAAAZG9jUHJvcHMvY29yZS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8Y3A6Y29yZVByb3BlcnRpZXMgeG1sbnM6Y3A9Imh0dHA6Ly9zY2hlbWFzLm9wZW54bWxmb3JtYXRzLm9yZy9wYWNrYWdlLzIwMDYvbWV0YWRhdGEvY29yZS1wcm9wZXJ0aWVzIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOmRjdGVybXM9Imh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy8iIHhtbG5zOmRjbWl0eXBlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIj48ZGM6Y3JlYXRvcj5SaWNrYXJkIE1hZ251c3NvbjwvZGM6Y3JlYXRvcj48Y3A6bGFzdE1vZGlmaWVkQnk+Umlja2FyZCBNYWdudXNzb248L2NwOmxhc3RNb2RpZmllZEJ5PjxkY3Rlcm1zOmNyZWF0ZWQgeHNpOnR5cGU9ImRjdGVybXM6VzNDRFRGIj4yMDE0LTA1LTA4VDA5OjQ2OjQwWjwvZGN0ZXJtczpjcmVhdGVkPjxkY3Rlcm1zOm1vZGlmaWVkIHhzaTp0eXBlPSJkY3Rlcm1zOlczQ0RURiI+MjAxNS0xMC0xOVQxMDowMjowN1o8L2RjdGVybXM6bW9kaWZpZWQ+PC9jcDpjb3JlUHJvcGVydGllcz5QSwMEFAAAAAAAAAAhAO6oqgQnFAAAJxQAABMAAABbQ29udGVudF9UeXBlc10ueG1sPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pg0KPFR5cGVzIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5vcGVueG1sZm9ybWF0cy5vcmcvcGFja2FnZS8yMDA2L2NvbnRlbnQtdHlwZXMiPjxEZWZhdWx0IEV4dGVuc2lvbj0iYmluIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwucHJpbnRlclNldHRpbmdzIi8+PERlZmF1bHQgRXh0ZW5zaW9uPSJyZWxzIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLXBhY2thZ2UucmVsYXRpb25zaGlwcyt4bWwiLz48RGVmYXVsdCBFeHRlbnNpb249InhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3dvcmtib29rLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5zcHJlYWRzaGVldG1sLnNoZWV0Lm1haW4reG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvd29ya3NoZWV0cy9zaGVldDQueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwud29ya3NoZWV0K3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL2RyYXdpbmdzL2RyYXdpbmczLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5kcmF3aW5nK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3Bpdm90VGFibGVzL3Bpdm90VGFibGUzLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5zcHJlYWRzaGVldG1sLnBpdm90VGFibGUreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvc2xpY2Vycy9zbGljZXIyLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5tcy1leGNlbC5zbGljZXIreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvZHJhd2luZ3MvZHJhd2luZzIueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LmRyYXdpbmcreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvcGl2b3RUYWJsZXMvcGl2b3RUYWJsZTIueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwucGl2b3RUYWJsZSt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9zbGljZXJzL3NsaWNlcjEueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm1zLWV4Y2VsLnNsaWNlcit4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9kcmF3aW5ncy9kcmF3aW5nMS54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQuZHJhd2luZyt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9zbGljZXJzL3NsaWNlcjMueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm1zLWV4Y2VsLnNsaWNlcit4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC93b3Jrc2hlZXRzL3NoZWV0My54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQuc3ByZWFkc2hlZXRtbC53b3Jrc2hlZXQreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvd29ya3NoZWV0cy9zaGVldDIueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwud29ya3NoZWV0K3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3Bpdm90VGFibGVzL3Bpdm90VGFibGUxLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5zcHJlYWRzaGVldG1sLnBpdm90VGFibGUreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvd29ya3NoZWV0cy9zaGVldDEueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwud29ya3NoZWV0K3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3N0eWxlcy54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQuc3ByZWFkc2hlZXRtbC5zdHlsZXMreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlMy54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQubXMtZXhjZWwuc2xpY2VyQ2FjaGUreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlMi54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQubXMtZXhjZWwuc2xpY2VyQ2FjaGUreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlMS54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQubXMtZXhjZWwuc2xpY2VyQ2FjaGUreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvd29ya3NoZWV0cy9zaGVldDYueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwud29ya3NoZWV0K3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3dvcmtzaGVldHMvc2hlZXQ1LnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5zcHJlYWRzaGVldG1sLndvcmtzaGVldCt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU1LnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5tcy1leGNlbC5zbGljZXJDYWNoZSt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU0LnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5tcy1leGNlbC5zbGljZXJDYWNoZSt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU3LnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5tcy1leGNlbC5zbGljZXJDYWNoZSt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC90aGVtZS90aGVtZTEueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnRoZW1lK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTYueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm1zLWV4Y2VsLnNsaWNlckNhY2hlK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTkueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm1zLWV4Y2VsLnNsaWNlckNhY2hlK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL3hsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTEwLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5tcy1leGNlbC5zbGljZXJDYWNoZSt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii94bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU4LnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5tcy1leGNlbC5zbGljZXJDYWNoZSt4bWwiLz48T3ZlcnJpZGUgUGFydE5hbWU9Ii9kb2NQcm9wcy9hcHAueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LmV4dGVuZGVkLXByb3BlcnRpZXMreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvcGl2b3RDYWNoZS9waXZvdENhY2hlRGVmaW5pdGlvbjEueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwucGl2b3RDYWNoZURlZmluaXRpb24reG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvcGl2b3RDYWNoZS9waXZvdENhY2hlRGVmaW5pdGlvbjIueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwucGl2b3RDYWNoZURlZmluaXRpb24reG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvcGl2b3RDYWNoZS9waXZvdENhY2hlRGVmaW5pdGlvbjMueG1sIiBDb250ZW50VHlwZT0iYXBwbGljYXRpb24vdm5kLm9wZW54bWxmb3JtYXRzLW9mZmljZWRvY3VtZW50LnNwcmVhZHNoZWV0bWwucGl2b3RDYWNoZURlZmluaXRpb24reG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIvZG9jUHJvcHMvY29yZS54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtcGFja2FnZS5jb3JlLXByb3BlcnRpZXMreG1sIi8+PE92ZXJyaWRlIFBhcnROYW1lPSIveGwvc2hhcmVkU3RyaW5ncy54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQuc3ByZWFkc2hlZXRtbC5zaGFyZWRTdHJpbmdzK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL2N1c3RvbVhtbC9pdGVtUHJvcHMxLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5jdXN0b21YbWxQcm9wZXJ0aWVzK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL2N1c3RvbVhtbC9pdGVtUHJvcHMyLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5jdXN0b21YbWxQcm9wZXJ0aWVzK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL2N1c3RvbVhtbC9pdGVtUHJvcHMzLnhtbCIgQ29udGVudFR5cGU9ImFwcGxpY2F0aW9uL3ZuZC5vcGVueG1sZm9ybWF0cy1vZmZpY2Vkb2N1bWVudC5jdXN0b21YbWxQcm9wZXJ0aWVzK3htbCIvPjxPdmVycmlkZSBQYXJ0TmFtZT0iL2RvY1Byb3BzL2N1c3RvbS54bWwiIENvbnRlbnRUeXBlPSJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQuY3VzdG9tLXByb3BlcnRpZXMreG1sIi8+PC9UeXBlcz5QSwECFAMUAAAAAAAAACEA/////xwCAAAcAgAAEAAAAAAAAAAAAAAAgAEAAAAAW3RyYXNoXS8wMDAwLmRhdFBLAQIUAxQAAAAAAAAAIQD/////4QAAAOEAAAAQAAAAAAAAAAAAAACAAUoCAABbdHJhc2hdLzAwMDIuZGF0UEsBAhQDFAAAAAAAAAAhAKyb9gZLDwAASw8AAA8AAAAAAAAAAAAAAIABWQMAAHhsL3dvcmtib29rLnhtbFBLAQIUAxQAAAAAAAAAIQD/////ogEAAKIBAAAQAAAAAAAAAAAAAACAAdESAABbdHJhc2hdLzAwMDEuZGF0UEsBAhQDFAAAAAAAAAAhAHP0QRy6FQAAuhUAABgAAAAAAAAAAAAAAIABoRQAAHhsL3dvcmtzaGVldHMvc2hlZXQ0LnhtbFBLAQIUAxQAAAAAAAAAIQBcllovPgIAAD4CAAAjAAAAAAAAAAAAAACAAZEqAAB4bC93b3Jrc2hlZXRzL19yZWxzL3NoZWV0NC54bWwucmVsc1BLAQIUAxQAAAAAAAAAIQDY1Qp2WysAAFsrAAAYAAAAAAAAAAAAAACAARAtAAB4bC9kcmF3aW5ncy9kcmF3aW5nMy54bWxQSwECFAMUAAAAAAAAACEAI5gaHOIOAADiDgAAHgAAAAAAAAAAAAAAgAGhWAAAeGwvcGl2b3RUYWJsZXMvcGl2b3RUYWJsZTMueG1sUEsBAhQDFAAAAAAAAAAhAEJVmy9FAQAARQEAACkAAAAAAAAAAAAAAIABv2cAAHhsL3Bpdm90VGFibGVzL19yZWxzL3Bpdm90VGFibGUzLnhtbC5yZWxzUEsBAhQDFAAAAAAAAAAhAFfvWn57AgAAewIAABYAAAAAAAAAAAAAAIABS2kAAHhsL3NsaWNlcnMvc2xpY2VyMi54bWxQSwECFAMUAAAAAAAAACEA3M7KPssgAADLIAAAGAAAAAAAAAAAAAAAgAH6awAAeGwvZHJhd2luZ3MvZHJhd2luZzIueG1sUEsBAhQDFAAAAAAAAAAhANj7YZiLFwAAixcAAB4AAAAAAAAAAAAAAIAB+4wAAHhsL3Bpdm90VGFibGVzL3Bpdm90VGFibGUyLnhtbFBLAQIUAxQAAAAAAAAAIQBlML6uRQEAAEUBAAApAAAAAAAAAAAAAACAAcKkAAB4bC9waXZvdFRhYmxlcy9fcmVscy9waXZvdFRhYmxlMi54bWwucmVsc1BLAQIUAxQAAAAAAAAAIQDyshhdcgIAAHICAAAWAAAAAAAAAAAAAACAAU6mAAB4bC9zbGljZXJzL3NsaWNlcjEueG1sUEsBAhQDFAAAAAAAAAAhAKxkPGrBIAAAwSAAABgAAAAAAAAAAAAAAIAB9KgAAHhsL2RyYXdpbmdzL2RyYXdpbmcxLnhtbFBLAQIUAxQAAAAAAAAAIQD6gmiN9AIAAPQCAAAWAAAAAAAAAAAAAACAAevJAAB4bC9zbGljZXJzL3NsaWNlcjMueG1sUEsBAhQDFAAAAAAAAAAhAPRRuacnAgAAJwIAABgAAAAAAAAAAAAAAIABE80AAHhsL3dvcmtzaGVldHMvc2hlZXQzLnhtbFBLAQIUAxQAAAAAAAAAIQDKfRfeggMAAIIDAAAYAAAAAAAAAAAAAACAAXDPAAB4bC93b3Jrc2hlZXRzL3NoZWV0Mi54bWxQSwECFAMUAAAAAAAAACEATXtyA0ceAABHHgAAHgAAAAAAAAAAAAAAgAEo0wAAeGwvcGl2b3RUYWJsZXMvcGl2b3RUYWJsZTEueG1sUEsBAhQDFAAAAAAAAAAhAE2ZoPZFAQAARQEAACkAAAAAAAAAAAAAAIABq/EAAHhsL3Bpdm90VGFibGVzL19yZWxzL3Bpdm90VGFibGUxLnhtbC5yZWxzUEsBAhQDFAAAAAAAAAAhAEa3KMNeBAAAXgQAABgAAAAAAAAAAAAAAIABN/MAAHhsL3dvcmtzaGVldHMvc2hlZXQxLnhtbFBLAQIUAxQAAAAAAAAAIQAuDoH1fUsAAH1LAAANAAAAAAAAAAAAAACAAcv3AAB4bC9zdHlsZXMueG1sUEsBAhQDFAAAAAAAAAAhAP67cUniAgAA4gIAACAAAAAAAAAAAAAAAIABc0MBAHhsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTMueG1sUEsBAhQDFAAAAAAAAAAhAJ+xbf3jAgAA4wIAACAAAAAAAAAAAAAAAIABk0YBAHhsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTIueG1sUEsBAhQDFAAAAAAAAAAhALlhKL7hAgAA4QIAACAAAAAAAAAAAAAAAIABtEkBAHhsL3NsaWNlckNhY2hlcy9zbGljZXJDYWNoZTEueG1sUEsBAhQDFAAAAAAAAAAhAEuyWF07EwAAOxMAABgAAAAAAAAAAAAAAIAB00wBAHhsL3dvcmtzaGVldHMvc2hlZXQ2LnhtbFBLAQIUAxQAAAAAAAAAIQCQOdNXPgIAAD4CAAAjAAAAAAAAAAAAAACAAURgAQB4bC93b3Jrc2hlZXRzL19yZWxzL3NoZWV0Ni54bWwucmVsc1BLAQIUAxQAAAAAAAAAIQAPoN0lFxMAABcTAAAYAAAAAAAAAAAAAACAAcNiAQB4bC93b3Jrc2hlZXRzL3NoZWV0NS54bWxQSwECFAMUAAAAAAAAACEAtX6r+T4CAAA+AgAAIwAAAAAAAAAAAAAAgAEQdgEAeGwvd29ya3NoZWV0cy9fcmVscy9zaGVldDUueG1sLnJlbHNQSwECFAMUAAAAAAAAACEAJ7E7JucCAADnAgAAIAAAAAAAAAAAAAAAgAGPeAEAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlNS54bWxQSwECFAMUAAAAAAAAACEAo7nldeQCAADkAgAAIAAAAAAAAAAAAAAAgAG0ewEAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlNC54bWxQSwECFAMUAAAAAAAAACEAS1dPbucCAADnAgAAIAAAAAAAAAAAAAAAgAHWfgEAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlNy54bWxQSwECFAMUAAAAAAAAACEAi4JuWI4aAACOGgAAEwAAAAAAAAAAAAAAgAH7gQEAeGwvdGhlbWUvdGhlbWUxLnhtbFBLAQIUAxQAAAAAAAAAIQDgjwWC4gIAAOICAAAgAAAAAAAAAAAAAACAAbqcAQB4bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU2LnhtbFBLAQIUAxQAAAAAAAAAIQAAFA6rzgIAAM4CAAAgAAAAAAAAAAAAAACAAdqfAQB4bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGU5LnhtbFBLAQIUAxQAAAAAAAAAIQCGNmI4zgIAAM4CAAAhAAAAAAAAAAAAAACAAeaiAQB4bC9zbGljZXJDYWNoZXMvc2xpY2VyQ2FjaGUxMC54bWxQSwECFAMUAAAAAAAAACEAjds41s0CAADNAgAAIAAAAAAAAAAAAAAAgAHzpQEAeGwvc2xpY2VyQ2FjaGVzL3NsaWNlckNhY2hlOC54bWxQSwECFAMUAAAAAAAAACEA0/D74tsDAADbAwAAEAAAAAAAAAAAAAAAgAH+qAEAZG9jUHJvcHMvYXBwLnhtbFBLAQIUAxQAAAAAAAAAIQCl01PCqw0AAKsNAAAnAAAAAAAAAAAAAACAAQetAQB4bC9waXZvdENhY2hlL3Bpdm90Q2FjaGVEZWZpbml0aW9uMS54bWxQSwECFAMUAAAAAAAAACEASx/VYl8KAABfCgAAJwAAAAAAAAAAAAAAgAH3ugEAeGwvcGl2b3RDYWNoZS9waXZvdENhY2hlRGVmaW5pdGlvbjIueG1sUEsBAhQDFAAAAAAAAAAhAK6BZ845BgAAOQYAACcAAAAAAAAAAAAAAIABm8UBAHhsL3Bpdm90Q2FjaGUvcGl2b3RDYWNoZURlZmluaXRpb24zLnhtbFBLAQIUAxQAAAAAAAAAIQD/////QwEAAEMBAAAQAAAAAAAAAAAAAACAARnMAQBbdHJhc2hdLzAwMDMuZGF0UEsBAhQDFAAAAAAAAAAhANd5tdEPFAAADxQAABQAAAAAAAAAAAAAAIABis0BAHhsL3NoYXJlZFN0cmluZ3MueG1sUEsBAhQDFAAAAAAAAAAhAOmhRm62OAAAtjgAABMAAAAAAAAAAAAAAIABy+EBAGN1c3RvbVhtbC9pdGVtMS54bWxQSwECFAMUAAAAAAAAACEAvX/A98kAAADJAAAAGAAAAAAAAAAAAAAAgAGyGgIAY3VzdG9tWG1sL2l0ZW1Qcm9wczEueG1sUEsBAhQDFAAAAAAAAAAhAL2EYiPbAAAA2wAAABMAAAAAAAAAAAAAAIABsRsCAGN1c3RvbVhtbC9pdGVtMi54bWxQSwECFAMUAAAAAAAAACEAfXhnG8kAAADJAAAAGAAAAAAAAAAAAAAAgAG9HAIAY3VzdG9tWG1sL2l0ZW1Qcm9wczIueG1sUEsBAhQDFAAAAAAAAAAhAHQ/OXooAQAAKAEAAB4AAAAAAAAAAAAAAIABvB0CAGN1c3RvbVhtbC9fcmVscy9pdGVtMS54bWwucmVsc1BLAQIUAxQAAAAAAAAAIQBcliciKAEAACgBAAAeAAAAAAAAAAAAAACAASAfAgBjdXN0b21YbWwvX3JlbHMvaXRlbTIueG1sLnJlbHNQSwECFAMUAAAAAAAAACEA/////2MCAABjAgAAEAAAAAAAAAAAAAAAgAGEIAIAW3RyYXNoXS8wMDA0LmRhdFBLAQIUAxQAAAAAAAAAIQDATt6Pjw4AAI8OAAAaAAAAAAAAAAAAAACAARUjAgB4bC9fcmVscy93b3JrYm9vay54bWwucmVsc1BLAQIUAxQAAAAAAAAAIQCan+7kSwIAAEsCAAATAAAAAAAAAAAAAACAAdwxAgBjdXN0b21YbWwvaXRlbTMueG1sUEsBAhQDFAAAAAAAAAAhAGmH3WvJAAAAyQAAABgAAAAAAAAAAAAAAIABWDQCAGN1c3RvbVhtbC9pdGVtUHJvcHMzLnhtbFBLAQIUAxQAAAAAAAAAIQDz5lQgkgEAAJIBAAATAAAAAAAAAAAAAACAAVc1AgBkb2NQcm9wcy9jdXN0b20ueG1sUEsBAhQDFAAAAAAAAAAhAHvzAqMoAQAAKAEAAB4AAAAAAAAAAAAAAIABGjcCAGN1c3RvbVhtbC9fcmVscy9pdGVtMy54bWwucmVsc1BLAQIUAxQAAAAAAAAAIQATXr5l3wIAAN8CAAALAAAAAAAAAAAAAACAAX44AgBfcmVscy8ucmVsc1BLAQIUAxQAAAAAAAAAIQCnop4ccwIAAHMCAAARAAAAAAAAAAAAAACAAYY7AgBkb2NQcm9wcy9jb3JlLnhtbFBLAQIUAxQAAAAAAAAAIQDuqKoEJxQAACcUAAATAAAAAAAAAAAAAACAASg+AgBbQ29udGVudF9UeXBlc10ueG1sUEsFBgAAAAA6ADoAVBAAAIBSAgAAAA==';\n      /* __TTV_TPL_END__ */\n      function download(filename, blob) {\n        var url = URL.createObjectURL(blob);\n        var a = document.createElement('a');\n        a.href = url;\n        a.download = filename;\n        document.body.appendChild(a);\n        a.click();\n        a.remove();\n        setTimeout(function () { URL.revokeObjectURL(url); }, 10000);\n      }\n      function exportRange(exp) {\n        var now = new Date();\n        var key = exp.range;\n        if (key === 'custom') {\n          var re = /^\\d{4}-\\d{2}-\\d{2}$/;\n          if (!re.test(exp.customFrom || '') || !re.test(exp.customTo || '')) return null;\n          var from = new Date(exp.customFrom + 'T00:00:00').getTime();\n          var to = new Date(exp.customTo + 'T23:59:59.999').getTime();\n          if (isNaN(from) || isNaN(to) || to < from) return null;\n          return { from: from, to: to, label: exp.customFrom + ' – ' + exp.customTo };\n        }\n        if (key === 'thisMonth') {\n          return { from: new Date(now.getFullYear(), now.getMonth(), 1).getTime(), to: now.getTime(), label: 'denna månad' };\n        }\n        if (key === 'lastMonth') {\n          return {\n            from: new Date(now.getFullYear(), now.getMonth() - 1, 1).getTime(),\n            to: new Date(now.getFullYear(), now.getMonth(), 1).getTime() - 1,\n            label: 'förra månaden'\n          };\n        }\n        return { from: startOfDay(new Date(now.getTime() - 29 * 86400000)).getTime(), to: now.getTime(), label: 'senaste 30 dagarna' };\n      }\n      function hours2(ms) { return Math.round(ms / 3600000 * 100) / 100; }\n      /* Same wall-clock minute must never count twice even with several open tabs:\n         user_tabs rows are per TAB and can overlap, but issue_1238 active time is per USER\n         (backend keeps a single open session per user). Cap each user's per-day project\n         split so it sums to the deduplicated per-user total, scaling proportionally. */\n      function normalizeActivePerUserDay(agg, dedupRows, targetNames, range) {\n        if (!dedupRows || !dedupRows.length) return;\n        var cap = {};\n        dedupRows.forEach(function (row) {\n          if (!(row.userId in targetNames)) return;\n          var t = new Date(row.intervalStart).getTime();\n          if (!(t >= range.from && t <= range.to)) return;\n          var k = dayKey(row.intervalStart) + '\\u0001' + targetNames[row.userId];\n          cap[k] = (cap[k] || 0) + (row.activeDuration || 0);\n        });\n        var sums = {};\n        Object.keys(agg).forEach(function (k) {\n          var pp = k.split('\\u0001');\n          var du = pp[0] + '\\u0001' + pp[2];\n          sums[du] = (sums[du] || 0) + (agg[k].active || 0);\n        });\n        Object.keys(agg).forEach(function (k) {\n          var pp = k.split('\\u0001');\n          var du = pp[0] + '\\u0001' + pp[2];\n          var c = cap[du], total = sums[du];\n          if (c != null && total > c && total > 0) {\n            agg[k].active = agg[k].active * (c / total);\n          }\n        });\n      }\n\n      /* Export via Viktors rapportmall: inject fresh data into the embedded template's\n         hidden data sheets — pivots/slicers/format stay byte-identical to the original. */\n      function runTemplateExport(exp, targets, range) {\n        state.exporting = true;\n        state.exportProgress = { done: 0, total: 6, label: 'Exporterar Loxy (' + range.label + ')…' };\n        state.exportModal = null;\n        renderModal();\n        toastProgress();\n        var fromIso = new Date(range.from).toISOString();\n        var toIso = new Date(range.to).toISOString();\n        var method = exp.method === 'span' ? 'span' : 'sum';\n        var deductMs = (method === 'span' ? (exp.deductMin != null ? exp.deductMin : 60) : 0) * 60000;\n        var capEarly = /^(\\d{1,2}):(\\d{2})$/.exec(exp.capEarly || '');\n        var capLate = /^(\\d{1,2}):(\\d{2})$/.exec(exp.capLate || '');\n        var hasCaps = Boolean(capEarly || capLate);\n        function capWin(dk) {\n          var base = new Date(dk + 'T00:00:00');\n          var lo = range.from, hi = range.to + 1;\n          if (capEarly) { var a = new Date(base); a.setHours(Number(capEarly[1]), Number(capEarly[2]), 0, 0); lo = a.getTime(); }\n          if (capLate) { var b = new Date(base); b.setHours(Number(capLate[1]), Number(capLate[2]), 0, 0); hi = b.getTime(); }\n          return [lo, hi];\n        }\n        /* clip a [start,end] span carrying a duration to the day's cap window */\n        function clipDur(startMs, endMs, dur, dk) {\n          if (!hasCaps) return dur;\n          var w = capWin(dk);\n          var cs = Math.max(startMs, w[0]), ce = Math.min(endMs, w[1]);\n          if (ce <= cs) return 0;\n          var span = endMs - startMs;\n          return span > 0 ? dur * ((ce - cs) / span) : dur;\n        }\n        function bucketInWin(iso) {\n          if (!hasCaps) return true;\n          var t = new Date(iso).getTime();\n          var w = capWin(dayKey(iso));\n          return t >= w[0] && t < w[1];\n        }\n        /* hour buckets when clipping/span needs precision; 30-min dial buckets align :00/:30 caps */\n        var tabInterval = (hasCaps || method === 'span') ? 'hour' : 'day';\n        var dialParams = { from: fromIso, to: toIso };\n        if (hasCaps) dialParams.fixedIntervalMillis = 1800000; else dialParams.fixedIntervalType = 'hour';\n        Promise.all([\n          trackStep(api('/users/statistics-v1/own/user_tabs', { from: fromIso, to: toIso, fixedIntervalType: tabInterval }).catch(function () { return []; })),\n          trackStep(api('/dial/statistics-v1/own/issue_1238_2', dialParams).catch(function () { return []; })),\n          trackStep(api('/dial/tags-v1').catch(function () { return []; })),\n          trackStep(api('/dial/call-tag-groups-v1').catch(function () { return []; })),\n          trackStep(api('/users/statistics-v1/own/issue_1238_active_user_time', { from: fromIso, to: toIso, fixedIntervalType: 'hour' }).catch(function () { return []; }))\n        ]).then(function (loaded) {\n          if (state.exportProgress) { state.exportProgress.label = 'Bygger Loxy-rapport…'; toastProgress(); }\n          var tabRows = loaded[0] || [];\n          var dRows = loaded[1] || [];\n          var tags = (loaded[2] && loaded[2].tags) || loaded[2] || [];\n          var groups = loaded[3] || [];\n          var tagName = {}, tagGroup = {};\n          tags.forEach(function (tg) { tagName[tg.id] = tg.name; });\n          groups.forEach(function (g) {\n            (g.tagIds || []).forEach(function (tid) { tagGroup[tid] = g.name; });\n          });\n          var tn = {};\n          targets.forEach(function (u) { tn[u.id] = u.fullName || u.username || ''; });\n          function inRange(iso) {\n            var t = new Date(iso).getTime();\n            return t >= range.from && t <= range.to;\n          }\n          function projOf(row) { return row.projectId ? projectById(row.projectId).name : '(Inget projekt)'; }\n          function serial(dk) {\n            var pp = dk.split('-');\n            return Math.round((Date.UTC(Number(pp[0]), Number(pp[1]) - 1, Number(pp[2])) - Date.UTC(1899, 11, 30)) / 86400000);\n          }\n          var DAY = 86400000;\n\n          /* per (day, proj, user): active ms + call stats; plus outcome counts */\n          var agent = {}, utfall = {};\n          function agentRow(day, proj, user) {\n            var k = [day, proj, user].join('\\u0001');\n            return agent[k] || (agent[k] = { active: 0, dur: 0, count: 0, nejtack: 0, bortfall: 0 });\n          }\n          tabRows.forEach(function (row) {\n            if (!(row.userId in tn) || !inRange(row.intervalStart)) return;\n            var dkT = dayKey(row.intervalStart);\n            var sT = new Date(row.startAt || row.intervalStart).getTime();\n            var eT = new Date(row.endAt || row.intervalEnd || row.intervalStart).getTime();\n            var clipped = clipDur(sT, eT, row.activeDuration || 0, dkT);\n            if (clipped > 0) agentRow(dkT, projOf(row), tn[row.userId]).active += clipped;\n          });\n          dRows.forEach(function (row) {\n            if (!(row.userId in tn) || !inRange(row.intervalStart) || !bucketInWin(row.intervalStart)) return;\n            var day = dayKey(row.intervalStart), proj = projOf(row), user = tn[row.userId];\n            var a = agentRow(day, proj, user);\n            a.count += row.count || 0;\n            a.dur += row.totalDuration || 0;\n            var grp = row.tagId ? (tagGroup[row.tagId] || 'Övrigt') : 'Ej utvärderat';\n            if (/nej\\s*tack/i.test(grp)) a.nejtack += row.count || 0;\n            if (/bortfall/i.test(grp)) a.bortfall += row.count || 0;\n            var uf = row.tagId ? (tagName[row.tagId] || 'Okänt utfall') : '(Inget utfall)';\n            var uk = [day, proj, user, grp, uf].join('\\u0001');\n            utfall[uk] = (utfall[uk] || 0) + (row.count || 0);\n          });\n\n          /* per-user reference (single session per user server-side = no double counting), clipped to caps */\n          var refRows = loaded[4] || [];\n          var userDay = {};   /* day+user -> { active, first, last } from the deduped source */\n          refRows.forEach(function (row) {\n            if (!(row.userId in tn) || !inRange(row.intervalStart)) return;\n            var dkR = dayKey(row.intervalStart);\n            var sR = new Date(row.startAt || row.intervalStart).getTime();\n            var eR = new Date(row.endAt || row.intervalEnd || row.intervalStart).getTime();\n            var w = capWin(dkR);\n            var cs = Math.max(sR, w[0]), ce = Math.min(eR, w[1]);\n            if (ce <= cs) return;\n            var kR = dkR + '\\u0001' + tn[row.userId];\n            var cur = userDay[kR] || (userDay[kR] = { active: 0, first: cs, last: ce });\n            cur.active += clipDur(sR, eR, row.activeDuration || 0, dkR);\n            if (cs < cur.first) cur.first = cs;\n            if (ce > cur.last) cur.last = ce;\n          });\n          if (method === 'span') {\n            /* Inloggad tid = first-to-last activity minus break deduction, distributed\n               over the day's projects proportionally to the user_tabs split */\n            var weights = {};\n            Object.keys(agent).forEach(function (k) {\n              var pp = k.split('\\u0001');\n              var du = pp[0] + '\\u0001' + pp[2];\n              weights[du] = (weights[du] || 0) + (agent[k].active || 0);\n            });\n            Object.keys(agent).forEach(function (k) {\n              var pp = k.split('\\u0001');\n              var du = pp[0] + '\\u0001' + pp[2];\n              var ud = userDay[du];\n              var net = ud ? Math.max(0, (ud.last - ud.first) - deductMs) : 0;\n              var w2 = weights[du];\n              agent[k].active = (w2 > 0 && ud) ? net * (agent[k].active / w2) : (ud ? net : 0);\n            });\n          } else {\n            var capMap = {};\n            Object.keys(userDay).forEach(function (k) { capMap[k] = userDay[k].active; });\n            var sums2 = {};\n            Object.keys(agent).forEach(function (k) {\n              var pp = k.split('\\u0001');\n              var du = pp[0] + '\\u0001' + pp[2];\n              sums2[du] = (sums2[du] || 0) + (agent[k].active || 0);\n            });\n            Object.keys(agent).forEach(function (k) {\n              var pp = k.split('\\u0001');\n              var du = pp[0] + '\\u0001' + pp[2];\n              var c2 = capMap[du], t2 = sums2[du];\n              if (c2 != null && t2 > c2 && t2 > 0) agent[k].active = agent[k].active * (c2 / t2);\n            });\n          }\n          var agentRows = [], projRows = [];\n          Object.keys(agent).sort().forEach(function (k) {\n            var pp = k.split('\\u0001');\n            var a = agent[k];\n            var sn = serial(pp[0]);\n            agentRows.push([pp[1], pp[2], sn, 0, 0, 0,\n              a.active / DAY, a.dur / DAY, 0,\n              a.count ? (a.dur / a.count) / DAY : 0, 0,\n              a.count, 0, 0]);\n            projRows.push([pp[1], pp[2], sn, a.count, 0, 0, 0, 0, 0, a.nejtack, a.bortfall]);\n          });\n          var utfallRows = [];\n          Object.keys(utfall).sort().forEach(function (k) {\n            var pp = k.split('\\u0001');\n            utfallRows.push([serial(pp[0]), pp[1], pp[2], pp[3], pp[4], utfall[k]]);\n          });\n\n          var H1 = ['Projekt', 'Säljare', 'Datum', 'Overksam tid', 'Väntetid', 'Hanteringstid',\n            'Inloggad tid', 'Samtalstid', 'Efterbearbetstid', 'Genomsnittlig samtalstid',\n            'Genomsnittlig samtalstid/Order', 'Registrerade samtal', 'Registrerade prospekt',\n            'Avbrutit samtalsbegäran'];\n          var H2 = ['Projekt', 'Säljare', 'Datum', 'Registrerade samtal', 'Registrerade prospekt',\n            'Order', 'Order Kontakter', 'Telefonsvarare', 'Återuppringningar',\n            'Antal Nej Tack', 'Antal Bortfall'];\n          var H3 = ['Datum', 'Projekt', 'Användare', 'Utfallstyp', 'Utfall', 'Antal'];\n          var reps = {};\n          reps['xl/worksheets/sheet1.xml'] = PXL.tplSheetXml(H1, agentRows, ['s', 's', 'd', 't', 't', 't', 't', 't', 't', 't', 't', 'n', 'n', 'n']);\n          reps['xl/worksheets/sheet2.xml'] = PXL.tplSheetXml(H2, projRows, ['s', 's', 'd', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n']);\n          reps['xl/worksheets/sheet3.xml'] = PXL.tplSheetXml(H3, utfallRows, ['d', 's', 's', 's', 's', 'n']);\n          var bytes = PXL.injectTemplate(TPL_B64, reps);\n          var blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });\n          var who = exp.mode === 'all' ? 'alla användare' :\n            targets.length > 1 ? targets.length + ' användare' :\n            (targets[0].fullName || targets[0].username || 'användare');\n          var safe = String('Tidrapport ' + who).replace(/[^a-zA-Z0-9åäöÅÄÖ _-]+/g, '').trim();\n          download(safe + ' ' + new Date().toISOString().slice(0, 10) + '.xlsx', blob);\n          state.exporting = false;\n          state.exportProgress = null;\n          var totActive = 0;\n          Object.keys(agent).forEach(function (k) { totActive += agent[k].active; });\n          toast('Exporterade Loxy: ' + fmtHm(totActive) + ' tim aktiv tid (' + range.label + ').');\n        }).catch(function (err) {\n          console.error('ttv template export', err);\n          state.exporting = false;\n          state.exportProgress = null;\n          toast('Kunde inte skapa Excel-filen.', true);\n        });\n      }\n\n      function runExport() {\n        var exp = state.exportModal;\n        if (!exp || exp.busy) return;\n        if (state.exporting) { toast('En export pågår redan – vänta tills den är klar.', true); return; }\n        /* capture custom date inputs before anything re-renders */\n        var cf = el('ttv-exp-from'), ct = el('ttv-exp-to');\n        if (cf) exp.customFrom = cf.value || '';\n        if (ct) exp.customTo = ct.value || '';\n        var range = exportRange(exp);\n        if (!range) { toast('Ange ett giltigt datumintervall (från och till).', true); return; }\n        var targets;\n        if (exp.mode === 'self') targets = [state.me];\n        else if (exp.mode === 'user') {\n          var pool = (state.users || []).concat(state.removedUsers || []);\n          targets = pool.filter(function (u) { return (exp.userIds || []).indexOf(u.id) !== -1; });\n          if (!targets.length) { toast('Välj minst en användare först.', true); return; }\n        } else {\n          targets = (state.users || []).slice();\n          if (exp.includeRemoved) targets = targets.concat(state.removedUsers || []);\n        }\n        if (exp.format === 'mall') { runTemplateExport(exp, targets, range); return; }\n        var wantManual = exp.data !== 'smart';\n        var wantSmart = exp.data === 'smart' || exp.data === 'both';\n        var method = exp.method === 'span' ? 'span' : 'sum';\n        /* capture the break deduction + time caps before the modal re-renders */\n        var deductEl = el('ttv-exp-deduct');\n        if (deductEl) { var dv = parseInt(deductEl.value, 10); exp.deductMin = isNaN(dv) ? 0 : Math.max(0, Math.min(600, dv)); }\n        var capEarlyEl = el('ttv-exp-cap-early'); if (capEarlyEl) exp.capEarly = capEarlyEl.value || '';\n        var capLateEl = el('ttv-exp-cap-late'); if (capLateEl) exp.capLate = capLateEl.value || '';\n        var deductMs = (method === 'span' ? (exp.deductMin != null ? exp.deductMin : 60) : 0) * 60000;\n        var capEarly = /^(\\d{1,2}):(\\d{2})$/.exec(exp.capEarly || '');\n        var capLate = /^(\\d{1,2}):(\\d{2})$/.exec(exp.capLate || '');\n        /* per-day cap timestamps (local); null = no cap */\n        function dayCap(dk, m) {\n          if (!m) return null;\n          var d = new Date(dk + 'T00:00:00'); d.setHours(Number(m[1]), Number(m[2]), 0, 0);\n          return d.getTime();\n        }\n        /* all inputs are captured — close the modal and run in the background */\n        var wantManualPre = exp.data !== 'smart';\n        var wantSmartPre = exp.data === 'smart' || exp.data === 'both';\n        var wantDialPre = exp.outcomes || exp.projTime;\n        var totalSteps = (wantSmartPre ? 1 : 0) + (wantDialPre ? 3 : 0) +\n          (exp.projTime ? 1 : 0) + ((exp.projTime && !wantSmartPre) ? 1 : 0) + 1;\n        state.exporting = true;\n        state.exportProgress = { done: 0, total: totalSteps, label: 'Exporterar (' + range.label + ')…' };\n        state.exportModal = null;\n        renderModal();\n        toastProgress();\n        /* For the span method we need to pin down the day's first/last activity, so query\n           fine 5-minute intervals (bounds the resolution to 5 min even without startAt/endAt).\n           The sum method is additive, so coarse hourly buckets are enough and far lighter. */\n        var smartIntervalMs = method === 'span' ? 5 * 60000 : 60 * 60000;\n        var pSmart = wantSmart ?\n          trackStep(api('/users/statistics-v1/own/issue_1238_active_user_time', {\n            from: new Date(range.from).toISOString(),\n            to: new Date(range.to).toISOString(),\n            fixedIntervalMillis: smartIntervalMs\n          }).catch(function () { return []; })) :\n          Promise.resolve(null);\n        /* Samtalsutfall: per-tag call stats + tag names + tag groups (\"Utfallstyp\") */\n        var wantDial = exp.outcomes || exp.projTime;\n        var pOutcomes = wantDial ?\n          Promise.all([\n            trackStep(api('/dial/statistics-v1/own/issue_1238_2', {\n              from: new Date(range.from).toISOString(),\n              to: new Date(range.to).toISOString(),\n              fixedIntervalType: 'hour'\n            }).catch(function () { return []; })),\n            trackStep(api('/dial/tags-v1').catch(function () { return []; })),\n            trackStep(api('/dial/call-tag-groups-v1').catch(function () { return []; }))\n          ]) :\n          Promise.resolve(null);\n        /* user_tabs = active time split per project (per Slack: Johan built this for time reports;\n           heavy aggregation — day interval is enough and much lighter) */\n        var pTabs = exp.projTime ?\n          trackStep(api('/users/statistics-v1/own/user_tabs', {\n            from: new Date(range.from).toISOString(),\n            to: new Date(range.to).toISOString(),\n            fixedIntervalType: 'day'\n          }).catch(function () { return []; })) :\n          Promise.resolve(null);\n        /* dedup reference: per-USER active time (backend keeps ONE active session per user,\n           so this can never double count) — used to cap the per-tab project split */\n        var pDedup = (exp.projTime && !wantSmart) ?\n          trackStep(api('/users/statistics-v1/own/issue_1238_active_user_time', {\n            from: new Date(range.from).toISOString(),\n            to: new Date(range.to).toISOString(),\n            fixedIntervalType: 'hour'\n          }).catch(function () { return []; })) :\n          Promise.resolve(null);\n        Promise.all([pSmart, pOutcomes, pTabs, pDedup]).then(function (loaded) {\n          if (state.exportProgress) { state.exportProgress.label = 'Bygger Excel-fil…'; toastProgress(); }\n          var smartRows = loaded[0];\n          var outcomesData = loaded[1];\n          var tabRows = loaded[2];\n          var dedupRows = smartRows || loaded[3];\n          var dataSheets = [], pivots = [];\n          var totals = 0;\n          if (wantManual) {\n            var detailRows = [];\n            var sumAgg = {};                /* day+user+project -> ms (sum of shifts) */\n            targets.forEach(function (u) {\n              var name = u.fullName || u.username || '';\n              entriesOf(u).forEach(function (e) {\n                var st = new Date(e.startAt).getTime();\n                if (!(st >= range.from && st <= range.to)) return;\n                var dk = dayKey(e.startAt);\n                var proj = projectById(e.projectId || null).name;\n                detailRows.push([dk, name, proj, fmtTime(e.startAt), fmtTime(e.stopAt), hours2(entryMs(e))]);\n                var k = dk + '\\u0001' + name + '\\u0001' + proj;\n                sumAgg[k] = (sumAgg[k] || 0) + entryMs(e);\n              });\n            });\n            var daily = [['Datum', 'Användare', 'Projekt', 'Timmar']];\n            Object.keys(sumAgg).sort().forEach(function (k) {\n              var parts = k.split('\\u0001');\n              totals += sumAgg[k];\n              daily.push([parts[0], parts[1], parts[2], hours2(sumAgg[k])]);\n            });\n            detailRows.sort(function (a, b) {\n              return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 :\n                a[1] < b[1] ? -1 : a[1] > b[1] ? 1 :\n                a[3] < b[3] ? -1 : a[3] > b[3] ? 1 : 0;\n            });\n            var detail = [['Datum', 'Användare', 'Projekt', 'Starttid', 'Sluttid', 'Timmar']].concat(detailRows);\n            dataSheets.push({ name: 'Data_Tidrapport', aoa: daily });\n            dataSheets.push({ name: 'Data_Pass', aoa: detail });\n            pivots.push({ name: 'Tidstats', source: 'Data_Tidrapport', header: daily[0], rows: ['Användare', 'Datum'], value: 'Timmar', dataName: 'Summa av Timmar' });\n          }\n          if (wantSmart) {\n            /* hourly activity buckets (issue_1238, each has startAt/endAt) → Swedish local days */\n            var targetNames = {};\n            targets.forEach(function (u) { targetNames[u.id] = u.fullName || u.username || ''; });\n            var smartDay = {};   /* day+user -> { first, last, sum } */\n            (smartRows || []).forEach(function (row) {\n              if (!(row.userId in targetNames)) return;\n              var ist = new Date(row.intervalStart).getTime();\n              if (!(ist >= range.from && ist <= range.to)) return;\n              var dk = dayKey(row.intervalStart);\n              var first = row.startAt ? new Date(row.startAt).getTime() : ist;\n              var last = row.endAt ? new Date(row.endAt).getTime() : (ist + (row.activeDuration || 0));\n              /* clip the bucket to the optional Tidigaste/Sista-tid caps for this day */\n              var earliest = dayCap(dk, capEarly), latest = dayCap(dk, capLate);\n              var cf = (earliest != null && first < earliest) ? earliest : first;\n              var cl = (latest != null && last > latest) ? latest : last;\n              if (cl <= cf) return;   /* bucket entirely outside the cap window */\n              var frac = (last > first) ? (cl - cf) / (last - first) : 1;\n              var sk = dk + '\\u0001' + targetNames[row.userId];\n              var cur = smartDay[sk] || (smartDay[sk] = { first: cf, last: cl, sum: 0 });\n              if (cf < cur.first) cur.first = cf;\n              if (cl > cur.last) cur.last = cl;\n              cur.sum += (row.activeDuration || 0) * frac;\n            });\n            var smartTotal = 0;\n            var smartSheet;\n            if (method === 'span') {\n              smartSheet = [['Datum', 'Användare', 'Första', 'Sista', 'Avdrag (min)', 'Aktiva timmar']];\n              Object.keys(smartDay).sort().forEach(function (sk) {\n                var parts = sk.split('\\u0001');\n                var s = smartDay[sk];\n                var net = Math.max(0, (s.last - s.first) - deductMs);\n                smartTotal += net;\n                smartSheet.push([parts[0], parts[1], fmtTime(new Date(s.first)), fmtTime(new Date(s.last)), Math.round(deductMs / 60000), hours2(net)]);\n              });\n            } else {\n              smartSheet = [['Datum', 'Användare', 'Aktiva timmar']];\n              Object.keys(smartDay).sort().forEach(function (sk) {\n                var parts = sk.split('\\u0001');\n                smartTotal += smartDay[sk].sum;\n                smartSheet.push([parts[0], parts[1], hours2(smartDay[sk].sum)]);\n              });\n            }\n            if (!wantManual) totals = smartTotal;\n            dataSheets.push({ name: 'Data_Aktivtid', aoa: smartSheet });\n            pivots.push({ name: 'Aktivstats', source: 'Data_Aktivtid', header: smartSheet[0], rows: ['Användare', 'Datum'], value: 'Aktiva timmar', dataName: 'Summa av aktiva timmar' });\n          }\n          if (outcomesData) {\n            var oRows = outcomesData[0] || [];\n            var oTags = (outcomesData[1] && outcomesData[1].tags) || outcomesData[1] || [];\n            var oGroups = outcomesData[2] || [];\n            var tagNames = {}, tagGroup = {};\n            oTags.forEach(function (tg) { tagNames[tg.id] = tg.name; });\n            oGroups.forEach(function (g) {\n              (g.tagIds || []).forEach(function (tid) { tagGroup[tid] = g.name; });\n            });\n            var oTargetNames = {};\n            targets.forEach(function (u) { oTargetNames[u.id] = u.fullName || u.username || ''; });\n            /* Samtalsutfall — 1:1 with the template's Data_Utfallsstatistik:\n               Datum, Projekt, Användare, Utfallstyp (tag group), Utfall (tag), Antal */\n            if (exp.outcomes) {\n              var oAgg = {};\n              oRows.forEach(function (row) {\n                if (!(row.userId in oTargetNames)) return;\n                var ot = new Date(row.intervalStart).getTime();\n                if (!(ot >= range.from && ot <= range.to)) return;\n                var utfall = row.tagId ? (tagNames[row.tagId] || 'Okänt utfall') : '(Inget utfall)';\n                var typ = row.tagId ? (tagGroup[row.tagId] || 'Övrigt') : 'Ej utvärderat';\n                var projName = row.projectId ? projectById(row.projectId).name : '(Inget projekt)';\n                var ok = [dayKey(row.intervalStart), projName, oTargetNames[row.userId], typ, utfall].join('\\u0001');\n                oAgg[ok] = (oAgg[ok] || 0) + (row.count || 0);\n              });\n              var outfallSheet = [['Datum', 'Projekt', 'Användare', 'Utfallstyp', 'Utfall', 'Antal']];\n              Object.keys(oAgg).sort().forEach(function (ok) {\n                var parts = ok.split('\\u0001');\n                outfallSheet.push([parts[0], parts[1], parts[2], parts[3], parts[4], oAgg[ok]]);\n              });\n              dataSheets.push({ name: 'Data_Utfall', aoa: outfallSheet });\n              pivots.push({ name: 'Utfallsstats', source: 'Data_Utfall', header: outfallSheet[0], rows: ['Utfall', 'Användare'], value: 'Antal', dataName: 'Antal utfall' });\n            }\n            /* Projekttid — aktiv tid per projekt (user_tabs) + samtal/samtalstid (dial stats) */\n            if (exp.projTime) {\n              var pAgg = {};\n              function projRow(day, projectId, userName) {\n                var pn = projectId ? projectById(projectId).name : '(Inget projekt)';\n                var pk = [day, pn, userName].join('\\u0001');\n                return pAgg[pk] || (pAgg[pk] = { active: 0, count: 0, dur: 0, conn: 0, connDur: 0 });\n              }\n              (tabRows || []).forEach(function (row) {\n                if (!(row.userId in oTargetNames)) return;\n                var tt = new Date(row.intervalStart).getTime();\n                if (!(tt >= range.from && tt <= range.to)) return;\n                projRow(dayKey(row.intervalStart), row.projectId, oTargetNames[row.userId]).active += row.activeDuration || 0;\n              });\n              oRows.forEach(function (row) {\n                if (!(row.userId in oTargetNames)) return;\n                var pt = new Date(row.intervalStart).getTime();\n                if (!(pt >= range.from && pt <= range.to)) return;\n                var cur = projRow(dayKey(row.intervalStart), row.projectId, oTargetNames[row.userId]);\n                cur.count += row.count || 0;\n                cur.dur += row.totalDuration || 0;\n                cur.conn += row.connectedCount || 0;\n                cur.connDur += row.connectedDuration || 0;\n              });\n              normalizeActivePerUserDay(pAgg, dedupRows, oTargetNames, range);\n              var projTimeSheet = [['Datum', 'Projekt', 'Användare', 'Aktiv tid (tim)', 'Samtal', 'Samtalstid (tim)', 'Kopplade', 'Kopplad tid (tim)']];\n              Object.keys(pAgg).sort().forEach(function (pk) {\n                var parts = pk.split('\\u0001');\n                var c = pAgg[pk];\n                projTimeSheet.push([parts[0], parts[1], parts[2], hours2(c.active), c.count, hours2(c.dur), c.conn, hours2(c.connDur)]);\n              });\n              dataSheets.push({ name: 'Data_Projekttid', aoa: projTimeSheet });\n              pivots.push({ name: 'Projektstats', source: 'Data_Projekttid', header: projTimeSheet[0], rows: ['Projekt', 'Användare'], value: 'Aktiv tid (tim)', dataName: 'Aktiv tid (tim)' });\n            }\n          }\n          var bytes = PXL.build(dataSheets, pivots);\n          var blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });\n          var who = exp.mode === 'all' ? 'alla användare' :\n            targets.length > 1 ? targets.length + ' användare' :\n            (targets[0].fullName || targets[0].username || 'användare');\n          var safe = String('Tidrapport ' + who).replace(/[^a-zA-Z0-9åäöÅÄÖ _-]+/g, '').trim();\n          download(safe + ' ' + new Date().toISOString().slice(0, 10) + '.xlsx', blob);\n          state.exporting = false;\n          state.exportProgress = null;\n          toast('Exporterade ' + fmtHm(totals) + ' tim (' + range.label + ').');\n        }).catch(function (err) {\n          console.error('ttv export', err);\n          state.exporting = false;\n          state.exportProgress = null;\n          toast('Kunde inte skapa Excel-filen.', true);\n        });\n      }\n\n      /* ---------------- rendering ---------------- */\n      function renderDate() {\n        var d = el('ttv-date');\n        if (!d) return;\n        try {\n          d.textContent = new Date().toLocaleDateString('sv-SE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n        } catch (e) { d.textContent = new Date().toLocaleDateString(); }\n      }\n\n      function renderSelf() {\n        var me = state.me;\n        var a = me ? activeOf(me) : null;\n        /* the dialer is \"off\" for me when my own call rights are paused — can't clock in */\n        var dialerOffForMe = state.loaded && !a && Boolean(suspensionOf(me));\n\n        var btn = el('ttv-bigbtn');\n        if (btn) {\n          btn.disabled = !state.loaded || state.busy || dialerOffForMe;\n          btn.className = 'ttv-bigbtn' + (a ? ' ttv-stop' : '');\n          btn.setAttribute('data-ttv-act', a ? 'stop' : 'start');\n          btn.title = dialerOffForMe ? 'Dialern är inte aktiv — du kan inte stämpla in just nu' : '';\n          btn.innerHTML = state.busy ?\n            '<div class=\"ttv-spinner\" style=\"border-top-color:#fff;\"></div>' :\n            (a ? '<i class=\"fas fa-stop\"></i><span>Stoppa</span>' : '<i class=\"fas fa-play\"></i><span>Starta</span>');\n        }\n\n        var clock = el('ttv-clock');\n        if (clock) {\n          clock.className = 'ttv-clock' + (a ? ' ttv-running' : '');\n          clock.textContent = a ? fmtClock(Date.now() - new Date(a.startedAt).getTime()) : '00:00:00';\n        }\n        var sub = el('ttv-clock-sub');\n        if (sub) {\n          if (a) {\n            sub.innerHTML = 'Pågår på <b>' + esc(projectById(a.projectId).name) + '</b> · startade ' + esc(fmtTime(a.startedAt));\n          } else if (dialerOffForMe) {\n            sub.innerHTML = '<i class=\"fas fa-phone-slash\" style=\"margin-right:5px;color:var(--ttv-danger);\"></i>Dialern är inte aktiv – du kan inte stämpla in just nu';\n          } else {\n            sub.textContent = state.loaded ? 'Ingen tid pågår' : 'Laddar…';\n          }\n        }\n\n        var wrap = el('ttv-projects');\n        if (wrap && state.loaded) {\n          var current = a ? (a.projectId || null) : selectedProjectId();\n          var list = (state.projects || []).slice();\n          var html = '';\n          if (!list.length) {\n            html += chipHtml(NO_PROJECT, current === null, a);\n          } else {\n            list.forEach(function (p) { html += chipHtml(p, current === p.id, a); });\n          }\n          wrap.innerHTML = html;\n        }\n      }\n      function chipHtml(p, isActive, running) {\n        return '<button class=\"ttv-proj-chip' + (isActive ? ' ttv-active' : '') + '\" data-ttv-proj=\"' + esc(p.id || '') + '\"' +\n          ' title=\"' + (running && !isActive ? 'Byt projekt (avslutar pågående pass och startar nytt)' : 'Välj projekt') + '\">' +\n          '<span class=\"ttv-proj-dot\" style=\"background:' + esc(projColor(p.id)) + ';\"></span>' +\n          esc(p.name) + '</button>';\n      }\n\n      function renderToday() {\n        var container = el('ttv-today');\n        var totalEl = el('ttv-today-total');\n        if (!container || !state.loaded) return;\n        var me = state.me;\n        var dk = dayKey(new Date());\n        var entries = entriesForDay(me, dk);\n        var a = activeOf(me);\n        var html = '';\n        entries.forEach(function (e) {\n          html += '<div class=\"ttv-entry\">' +\n            '<span class=\"ttv-entry-time\">' + esc(fmtTime(e.startAt)) + '–' + esc(fmtTime(e.stopAt)) + '</span>' +\n            '<span class=\"ttv-entry-proj\"><span class=\"ttv-proj-dot\" style=\"background:' + esc(projColor(e.projectId || null)) + ';\"></span>' +\n            esc(projectById(e.projectId).name) + '</span>' +\n            '<span class=\"ttv-entry-dur\">' + esc(fmtDur(entryMs(e))) + '</span></div>';\n        });\n        if (a && dayKey(a.startedAt) === dk) {\n          html += '<div class=\"ttv-entry ttv-live\">' +\n            '<span class=\"ttv-entry-time\">' + esc(fmtTime(a.startedAt)) + '–pågår</span>' +\n            '<span class=\"ttv-entry-proj\"><span class=\"ttv-proj-dot\" style=\"background:' + esc(projColor(a.projectId || null)) + ';\"></span>' +\n            esc(projectById(a.projectId).name) + '</span>' +\n            '<span class=\"ttv-entry-dur\" data-ttv-live-dur=\"' + new Date(a.startedAt).getTime() + '\">' +\n            esc(fmtDur(Date.now() - new Date(a.startedAt).getTime())) + '</span></div>';\n        }\n        container.innerHTML = html || '<div class=\"ttv-empty\">Ingen tid rapporterad idag ännu. Tryck på startknappen!</div>';\n        if (totalEl) totalEl.textContent = 'Totalt idag: ' + fmtHm(totalForDay(me, dk)) + ' tim';\n      }\n\n      function renderHistory() {\n        var container = el('ttv-history');\n        var btn = el('ttv-hist-btn');\n        if (btn) btn.textContent = state.historyOpen ? 'Dölj historik' : 'Visa full historik';\n        if (!container) return;\n        if (!state.historyOpen || !state.loaded) { container.innerHTML = ''; return; }\n\n        var me = state.me;\n        var days = [];\n        var today = startOfDay(new Date());\n        for (var i = 29; i >= 0; i--) days.push(new Date(today.getTime() - i * 86400000));\n\n        var perDay = {}, perProj = {}, total = 0, activeDays = 0;\n        entriesOf(me).forEach(function (e) {\n          var ms = entryMs(e);\n          var dk = dayKey(e.startAt);\n          if (new Date(e.startAt) < new Date(today.getTime() - 29 * 86400000)) return;\n          perDay[dk] = (perDay[dk] || 0) + ms;\n          var pk = e.projectId || '';\n          perProj[pk] = (perProj[pk] || 0) + ms;\n          total += ms;\n        });\n        var a = activeOf(me);\n        if (a) {\n          var ams = Date.now() - new Date(a.startedAt).getTime();\n          perDay[dayKey(a.startedAt)] = (perDay[dayKey(a.startedAt)] || 0) + ams;\n          perProj[a.projectId || ''] = (perProj[a.projectId || ''] || 0) + ams;\n          total += ams;\n        }\n        Object.keys(perDay).forEach(function (k) { if (perDay[k] > 60000) activeDays++; });\n\n        var max = 0;\n        days.forEach(function (d) { max = Math.max(max, perDay[dayKey(d)] || 0); });\n\n        var bars = '<div class=\"ttv-chart\">';\n        days.forEach(function (d, idx) {\n          var v = perDay[dayKey(d)] || 0;\n          var pct = max ? Math.max(2, Math.round(v / max * 100)) : 2;\n          var isToday = dayKey(d) === dayKey(new Date());\n          var label = (idx % 5 === 0 || isToday) ? d.getDate() + '/' + (d.getMonth() + 1) : '';\n          bars += '<div class=\"ttv-bar-col' + (isToday ? ' ttv-today-col' : '') + '\" title=\"' +\n            esc(d.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' })) + ': ' + esc(fmtHm(v)) + ' tim\">' +\n            '<div class=\"ttv-bar\" style=\"height:' + pct + '%\"></div>' +\n            '<div class=\"ttv-bar-day\">' + esc(label) + '</div></div>';\n        });\n        bars += '</div>';\n\n        /* donut */\n        var projKeys = Object.keys(perProj).filter(function (k) { return perProj[k] > 0; })\n          .sort(function (x, y) { return perProj[y] - perProj[x]; });\n        var donut = '';\n        if (total > 0 && projKeys.length) {\n          var R = 52, C = 2 * Math.PI * R;\n          var offset = 0;\n          var segs = '';\n          projKeys.forEach(function (k) {\n            var frac = perProj[k] / total;\n            var len = frac * C;\n            segs += '<circle r=\"' + R + '\" cx=\"70\" cy=\"70\" fill=\"transparent\" stroke=\"' + esc(projColor(k || null)) + '\"' +\n              ' stroke-width=\"17\" stroke-dasharray=\"' + len.toFixed(2) + ' ' + (C - len).toFixed(2) + '\"' +\n              ' stroke-dashoffset=\"' + (-offset).toFixed(2) + '\" transform=\"rotate(-90 70 70)\"></circle>';\n            offset += len;\n          });\n          donut = '<svg width=\"140\" height=\"140\" viewBox=\"0 0 140 140\" role=\"img\" aria-label=\"Fördelning per projekt\">' + segs +\n            '<text x=\"70\" y=\"66\" text-anchor=\"middle\" font-size=\"17\" font-weight=\"800\" fill=\"currentColor\">' + esc(fmtHm(total)) + '</text>' +\n            '<text x=\"70\" y=\"84\" text-anchor=\"middle\" font-size=\"10\" fill=\"currentColor\" opacity=\"0.6\">tim / 30 dgr</text></svg>';\n          donut += '<div class=\"ttv-legend\">';\n          projKeys.forEach(function (k) {\n            var pctTxt = Math.round(perProj[k] / total * 100);\n            donut += '<div class=\"ttv-legend-row\"><span class=\"ttv-proj-dot\" style=\"background:' + esc(projColor(k || null)) + ';\"></span>' +\n              '<span class=\"ttv-legend-name\">' + esc(projectById(k || null).name) + '</span>' +\n              '<span class=\"ttv-legend-val\">' + esc(fmtHm(perProj[k])) + ' tim · ' + pctTxt + '%</span></div>';\n          });\n          donut += '</div>';\n        } else {\n          donut = '<div class=\"ttv-empty\">Ingen tid rapporterad de senaste 30 dagarna.</div>';\n        }\n\n        container.innerHTML =\n          '<div class=\"ttv-hist-kpis\">' +\n          '<div class=\"ttv-kpi\"><div class=\"ttv-kpi-num\">' + esc(fmtHm(total)) + '</div><div class=\"ttv-kpi-label\">Timmar senaste 30 dagarna</div></div>' +\n          '<div class=\"ttv-kpi\"><div class=\"ttv-kpi-num\">' + activeDays + '</div><div class=\"ttv-kpi-label\">Dagar med rapporterad tid</div></div>' +\n          '<div class=\"ttv-kpi\"><div class=\"ttv-kpi-num\">' + esc(activeDays ? fmtHm(total / activeDays) : '0:00') + '</div><div class=\"ttv-kpi-label\">Snitt per aktiv dag</div></div>' +\n          '</div>' +\n          '<div class=\"ttv-hist-grid\"><div>' + bars + '</div><div class=\"ttv-donut-wrap\">' + donut + '</div></div>';\n      }\n\n      function renderAdmin() {\n        var section = el('ttv-admin');\n        if (!section) return;\n        /* visibility is owned by renderMode (tab switch); here we only fill content and modals */\n        if (!state.adminAllowed || !state.loaded) { section.style.display = 'none'; renderModal(); return; }\n        if (state.tab !== 'admin') { section.style.display = 'none'; renderModal(); return; }\n        section.style.display = '';\n\n        var seg = el('ttv-mode-seg');\n        if (seg) {\n          var segHtml = '<span class=\"ttv-seg\">';\n          [['manual', 'Manuell'], ['smart', 'Smart tracking'], ['both', 'Båda']].forEach(function (m) {\n            segHtml += '<button class=\"' + (state.trackMode === m[0] ? 'ttv-active' : '') + '\" data-ttv-mode=\"' + m[0] + '\"' +\n              ' title=\"' + (m[0] === 'manual' ? 'Visa stämpelklockans tid' : m[0] === 'smart' ? 'Visa SaleSys inbyggda aktivitetsmätning' : 'Visa båda KPI:erna sida vid sida') + '\">' +\n              m[1] + '</button>';\n          });\n          seg.innerHTML = segHtml + '</span>';\n        }\n\n        var bar = el('ttv-admin-toolbar');\n        if (bar && state.users) {\n          var sel = selectedIds();\n          var allSelected = sel.length && sel.length === state.users.length;\n          var barHtml = '';\n          if (sel.length) {\n            barHtml = '<div class=\"ttv-bulkbar\">' +\n              '<button class=\"ttv-check' + (allSelected ? ' ttv-on' : '') + '\" data-ttv-sel-all=\"1\" title=\"' + (allSelected ? 'Avmarkera alla' : 'Markera alla') + '\"><i class=\"fas fa-check\"></i></button>' +\n              '<span class=\"ttv-bulkbar-count\">' + sel.length + ' markerade</span>' +\n              (state.canManageDialer ?\n                '<button class=\"ttv-bulk-btn\" data-ttv-bulk=\"suspend\" title=\"Pausa samtalsrättigheter för alla markerade\"><i class=\"fas fa-phone-slash\"></i>Pausa rättigheter</button>' +\n                '<button class=\"ttv-bulk-btn\" data-ttv-bulk=\"restore\" title=\"Återställ pausade rättigheter för alla markerade\"><i class=\"fas fa-phone\"></i>Återställ</button>' : '') +\n              (state.canEditTime ?\n                '<button class=\"ttv-bulk-btn\" data-ttv-bulk=\"stop\" title=\"Stoppa pågående timers för alla markerade\"><i class=\"fas fa-stop\"></i>Stoppa timer</button>' : '') +\n              (state.canLogOutUsers ?\n                '<button class=\"ttv-bulk-btn' + (state.armedBulkKick ? ' ttv-armed' : '') + '\" data-ttv-bulk=\"kick\" title=\"' +\n                (state.armedBulkKick ? 'Klicka igen för att logga ut alla markerade' : 'Logga ut alla markerade (stoppar timers)') +\n                '\"><i class=\"fas fa-sign-out-alt\"></i>' + (state.armedBulkKick ? 'Bekräfta utloggning' : 'Logga ut') + '</button>' : '') +\n              (state.canExportTeam ?\n                '<button class=\"ttv-bulk-btn\" data-ttv-bulk=\"export\" title=\"Exportera tidrapport för alla markerade\"><i class=\"fas fa-file-excel\"></i>Exportera</button>' : '') +\n              '<span class=\"ttv-spacer\" style=\"flex:1;\"></span>' +\n              '<button class=\"ttv-selall\" data-ttv-sel-clear=\"1\">Rensa markering</button>' +\n              '</div>';\n          } else {\n            barHtml = '<div style=\"display:flex;align-items:center;gap:8px;margin-bottom:8px;\">' +\n              '<button class=\"ttv-check\" data-ttv-sel-all=\"1\" title=\"Markera alla\"><i class=\"fas fa-check\"></i></button>' +\n              '<span style=\"font-size:12px;color:var(--ttv-muted-fg);\">Markera flera för att pausa rättigheter, logga ut eller exportera i klump.</span>' +\n              '</div>';\n          }\n          bar.innerHTML = barHtml;\n        }\n\n        var listEl = el('ttv-admin-users');\n        if (listEl && state.users) {\n          var dk = dayKey(new Date());\n          /* own row must render from fresh state.me — the users-v1 list copy lags own writes */\n          var sorted = state.users.map(function (u) {\n            return u.id === state.me.id ? state.me : u;\n          }).sort(function (x, y) {\n            var ax = activeOf(x) ? 0 : 1, ay = activeOf(y) ? 0 : 1;\n            if (ax !== ay) return ax - ay;\n            return String(x.fullName || x.username).localeCompare(String(y.fullName || y.username), 'sv');\n          });\n          var html = '';\n          sorted.forEach(function (u) {\n            var a = activeOf(u);\n            var susp = suspensionOf(u);\n            var stale = a && (Date.now() - new Date(a.startedAt).getTime() > STALE_AFTER_MS);\n            var initials = String(u.fullName || u.username || '?').split(/\\s+/).map(function (w) { return w.charAt(0); }).slice(0, 2).join('').toUpperCase();\n            var mode = state.trackMode;\n            var presence = mode !== 'manual' ? smartPresence(u.id) : null;\n            var status;\n            if (a) {\n              status = '<span class=\"ttv-status-dot ttv-on\"></span>' + esc(projectById(a.projectId).name) +\n                ' · <span data-ttv-live-dur=\"' + new Date(a.startedAt).getTime() + '\">' + esc(fmtDur(Date.now() - new Date(a.startedAt).getTime())) + '</span>';\n            } else if (presence && presence.isActive) {\n              status = '<span class=\"ttv-status-dot ttv-on\"></span>Aktiv i SaleSys' +\n                (presence.projectId ? ' · ' + esc(projectById(presence.projectId).name) : '');\n            } else if (presence && presence.isOnline) {\n              status = '<span class=\"ttv-status-dot\"></span>Inloggad (inaktiv)';\n            } else {\n              status = '<span class=\"ttv-status-dot\"></span>' + (mode === 'smart' ? 'Inte inloggad' : 'Ingen aktiv tid');\n            }\n            var kpis = '';\n            var manualKpi = esc(fmtHm(totalForDay(u, dk)));\n            var smartKpi = esc(fmtHm((state.smart && state.smart.today && state.smart.today[u.id]) || 0));\n            if (mode === 'manual') {\n              kpis = '<span class=\"ttv-user-today\" title=\"Stämplad tid idag\">' + manualKpi + '</span>';\n            } else if (mode === 'smart') {\n              kpis = '<span class=\"ttv-user-today\" title=\"Aktiv tid i SaleSys idag (Smart tracking)\" style=\"color:var(--ttv-warn);\"><i class=\"fas fa-bolt\" style=\"font-size:10px;margin-right:4px;opacity:0.8;\"></i>' + smartKpi + '</span>';\n            } else {\n              kpis = '<span class=\"ttv-kpi-pair\">' +\n                '<span class=\"ttv-kpi-mini\" title=\"Stämplad tid idag (manuell)\"><i class=\"fas fa-stopwatch\"></i>' + manualKpi + '</span>' +\n                '<span class=\"ttv-kpi-mini ttv-smart-kpi\" title=\"Aktiv tid i SaleSys idag (Smart tracking)\"><i class=\"fas fa-bolt\"></i>' + smartKpi + '</span>' +\n                '</span>';\n            }\n            html += '<div class=\"ttv-user-row' + (state.edit && state.edit.userId === u.id ? ' ttv-selected' : '') + '\">' +\n              '<button class=\"ttv-check' + (state.selected[u.id] ? ' ttv-on' : '') + '\" data-ttv-sel=\"' + esc(u.id) + '\" title=\"Markera\"><i class=\"fas fa-check\"></i></button>' +\n              '<div class=\"ttv-avatar\">' + esc(initials) + '</div>' +\n              '<div class=\"ttv-user-main\">' +\n              '<div class=\"ttv-user-name\">' + esc(u.fullName || u.username) + (u.id === state.me.id ? ' <span style=\"color:var(--ttv-muted-fg);font-weight:400;\">(du)</span>' : '') + '</div>' +\n              '<div class=\"ttv-user-status\">' + status + '</div></div>' +\n              (susp ? '<span class=\"ttv-badge\" title=\"Samtalsrättigheter pausade ' + esc(fmtTime(susp.suspendedAt)) + ' av ' + esc(susp.byName || '') + '\"><i class=\"fas fa-phone-slash\"></i> Pausad</span>' : '') +\n              (stale ? '<span class=\"ttv-badge\" title=\"Timern har varit igång ovanligt länge.\"><i class=\"fas fa-exclamation-triangle\"></i></span>' : '') +\n              kpis +\n              (state.canEditTime ?\n                '<button class=\"ttv-iconbtn\" data-ttv-admin-edit=\"' + esc(u.id) + '\" title=\"Redigera tider\"><i class=\"fas fa-pen\"></i></button>' : '') +\n              (state.canManageDialer && u.type !== 'admin' && u.id !== state.me.id ?\n                '<button class=\"ttv-iconbtn' + (susp ? ' ttv-warn-on' : '') + '\" data-ttv-admin-suspend=\"' + esc(u.id) + '\" title=\"' +\n                (susp ? 'Återställ samtalsrättigheter' : 'Pausa samtalsrättigheter') + '\"><i class=\"fas ' + (susp ? 'fa-phone-slash' : 'fa-phone') + '\"></i></button>' : '') +\n              (state.canLogOutUsers && u.id !== state.me.id ?\n                '<button class=\"ttv-iconbtn ttv-danger' + (state.armedKick && state.armedKick.userId === u.id ? ' ttv-armed' : '') + '\"' +\n                ' data-ttv-admin-kick=\"' + esc(u.id) + '\" title=\"' +\n                (state.armedKick && state.armedKick.userId === u.id ? 'Klicka igen för att logga ut användaren' : 'Logga ut användaren (stoppar timern)') +\n                '\"><i class=\"fas fa-sign-out-alt\"></i></button>' : '') +\n              '</div>';\n          });\n          listEl.innerHTML = html || '<div class=\"ttv-empty\">Inga användare.</div>';\n        }\n\n        renderEditor();\n        renderModal();\n      }\n\n      function renderEditor() {\n        var mount = el('ttv-admin-editor');\n        if (!mount) return;\n        var edit = state.edit;\n        if (!edit) {\n          mount.innerHTML = '<div class=\"ttv-empty\" style=\"padding: 20px 10px;\">' +\n            '<i class=\"fas fa-pen\" style=\"margin-right: 7px; opacity: 0.5;\"></i>' +\n            'Välj en användare med pennan för att se och redigera tider i efterhand.</div>';\n          return;\n        }\n        var user = editUser();\n        if (!user) { state.edit = null; mount.innerHTML = ''; return; }\n        var a = activeOf(user);\n        var initials = String(user.fullName || user.username || '?').split(/\\s+/).map(function (w) { return w.charAt(0); }).slice(0, 2).join('').toUpperCase();\n\n        var minDate = dayKey(new Date(Date.now() - GC_KEEP_MS + 86400000));\n        var html = '<div class=\"ttv-editor-head\">' +\n          '<div class=\"ttv-avatar\">' + esc(initials) + '</div>' +\n          '<div class=\"ttv-editor-name\">' + esc(user.fullName || user.username) + '<div style=\"font-size:11px;font-weight:400;color:var(--ttv-muted-fg);\">Redigera tider i efterhand – välj dag</div></div>' +\n          '<input type=\"date\" class=\"ttv-date-input\" id=\"ttv-edit-date\" value=\"' + esc(edit.date) + '\" max=\"' + esc(dayKey(new Date())) + '\" min=\"' + esc(minDate) + '\" title=\"Välj vilken dag du vill justera\">' +\n          '<button class=\"ttv-iconbtn\" data-ttv-edit-close=\"1\" title=\"Stäng\"><i class=\"fas fa-times\"></i></button>' +\n          '</div>';\n\n        if (a) {\n          html += '<div class=\"ttv-running-banner\"><i class=\"fas fa-circle\"></i>' +\n            'Timer pågår på ' + esc(projectById(a.projectId).name) + ' sedan ' + esc(fmtTime(a.startedAt)) +\n            '<span class=\"ttv-spacer\"></span>' +\n            '<button class=\"ttv-btn ttv-btn-sm\" data-ttv-admin-stop=\"' + esc(user.id) + '\">Stoppa nu</button></div>';\n        }\n\n        var projList = visibleProjects(user);\n        if (!projList.length) projList = [NO_PROJECT];\n        edit.rows.forEach(function (row, idx) {\n          var d = rowToDates(row, edit.date);\n          var chips = '';\n          projList.forEach(function (p) {\n            var isOn = (row.projectId || null) === (p.id || null);\n            chips += '<button class=\"ttv-mini-chip' + (isOn ? ' ttv-active' : '') + '\" data-ttv-row-proj=\"' + idx + ':' + esc(p.id || '') + '\">' +\n              '<span class=\"ttv-proj-dot\" style=\"background:' + esc(projColor(p.id)) + ';\"></span>' + esc(p.name) + '</button>';\n          });\n          html += '<div class=\"ttv-erow\">' +\n            '<input type=\"time\" class=\"ttv-time-input' + (row.start && !/^\\d{1,2}:\\d{2}$/.test(row.start) ? ' ttv-invalid' : '') + '\" value=\"' + esc(row.start) + '\" data-ttv-row-start=\"' + idx + '\">' +\n            '<span class=\"ttv-erow-arrow\"><i class=\"fas fa-arrow-right\"></i></span>' +\n            '<input type=\"time\" class=\"ttv-time-input\" value=\"' + esc(row.stop) + '\" data-ttv-row-stop=\"' + idx + '\">' +\n            '<span class=\"ttv-erow-dur\">' + (d ? esc(fmtDur(new Date(d.stopAt) - new Date(d.startAt))) : '<span style=\"color:var(--ttv-danger);\">ogiltig</span>') + '</span>' +\n            '<button class=\"ttv-iconbtn ttv-danger\" data-ttv-row-del=\"' + idx + '\" title=\"Ta bort pass\"><i class=\"fas fa-trash\"></i></button>' +\n            '<div class=\"ttv-erow-projs\">' + chips + '</div>' +\n            '</div>';\n        });\n        if (!edit.rows.length) {\n          html += '<div class=\"ttv-empty\">Inga pass denna dag.</div>';\n        }\n        html += '<div style=\"margin-top: 10px;\"><button class=\"ttv-btn ttv-btn-sm\" data-ttv-row-add=\"1\"><i class=\"fas fa-plus\" style=\"margin-right: 6px;\"></i>Lägg till pass</button></div>';\n        html += '<div class=\"ttv-editor-actions\">' +\n          '<button class=\"ttv-btn ttv-primary\" data-ttv-edit-save=\"1\"' + (edit.dirty ? '' : ' disabled') + '>Spara ändringar</button>' +\n          '<button class=\"ttv-btn\" data-ttv-edit-close=\"1\">Avbryt</button>' +\n          '</div>';\n        mount.innerHTML = html;\n      }\n\n      function infoModalHtml() {\n        return '<div class=\"ttv-overlay\" data-ttv-modal-bg=\"1\"><div class=\"ttv-modal\">' +\n          '<div class=\"ttv-modal-title\">Smart tracking – så fungerar det</div>' +\n          '<div class=\"ttv-modal-sub\">SaleSys inbyggda aktivitetsmätning, helt automatisk.</div>' +\n          '<ul class=\"ttv-info-list\">' +\n          '<li><i class=\"fas fa-heartbeat\"></i><span>Medan SaleSys är öppet skickar webbläsaren en puls var 15:e sekund.</span></li>' +\n          '<li><i class=\"fas fa-mouse-pointer\"></i><span>En puls räknas som <b>aktiv</b> bara om du har rört musen eller tangentbordet sedan förra pulsen. Att bara ha fliken öppen räknas inte.</span></li>' +\n          '<li><i class=\"fas fa-bolt\"></i><span><b>Aktiv tid</b> är summan av alla aktiva pulser. Ingen startknapp behövs — och den kan aldrig glömmas på.</span></li>' +\n          '<li><i class=\"fas fa-stopwatch\"></i><span><b>Manuell tid</b> (stämpelklockan i den här vyn) visar vad användaren avser att arbeta med. Smart tracking visar faktisk aktivitet i systemet. Läget <b>Båda</b> visar KPI:erna sida vid sida — skillnaden mellan dem är ofta det intressanta.</span></li>' +\n          '<li><i class=\"fas fa-lock\"></i><span>Smart tracking kan inte pausas eller redigeras i efterhand — den mäter verklig aktivitet.</span></li>' +\n          '</ul>' +\n          '<div class=\"ttv-modal-actions\"><button class=\"ttv-btn ttv-primary\" data-ttv-modal-close=\"1\">Jag förstår</button></div>' +\n          '</div></div>';\n      }\n\n      function bulkSuspendModalHtml(userIds) {\n        var eligible = [], skipped = [];\n        userIds.forEach(function (id) {\n          var u = (state.users || []).filter(function (x) { return x.id === id; })[0];\n          if (!u) return;\n          if (u.id === state.me.id) { skipped.push({ u: u, why: 'du själv' }); return; }\n          if (u.type === 'admin') { skipped.push({ u: u, why: 'administratör' }); return; }\n          if (suspensionOf(u)) { skipped.push({ u: u, why: 'redan pausad' }); return; }\n          var sources = callGrantingSources(u);\n          if (!sources.length) { skipped.push({ u: u, why: 'inga samtalsrättigheter' }); return; }\n          eligible.push({ u: u, sources: sources });\n        });\n        var html = '<div class=\"ttv-overlay\" data-ttv-modal-bg=\"1\"><div class=\"ttv-modal\">' +\n          '<div class=\"ttv-modal-title\">Pausa samtalsrättigheter</div>' +\n          '<div class=\"ttv-modal-sub\">' + userIds.length + ' markerade användare</div>';\n        if (eligible.length) {\n          html += '<div class=\"ttv-modal-note\">Team/roller som ger rätt att ringa tas bort tillfälligt och kan återställas <b>exakt</b>. Användarna loggas samtidigt ut så att ändringen slår igenom direkt.</div>';\n          eligible.forEach(function (e) {\n            html += '<div class=\"ttv-bulk-user-row\"><i class=\"fas fa-phone-slash\" style=\"color:var(--ttv-warn);\"></i>' +\n              '<span>' + esc(e.u.fullName || e.u.username) + '</span>' +\n              '<span class=\"ttv-bulk-user-what\">' + e.sources.map(function (s) {\n                return esc(s.name) + ' (' + (s.kind === 'role' ? 'roll' : 'team') + ')';\n              }).join(', ') + '</span></div>';\n          });\n        } else {\n          html += '<div class=\"ttv-modal-note\">Ingen av de markerade användarna har samtalsrättigheter som kan pausas.</div>';\n        }\n        skipped.forEach(function (s) {\n          html += '<div class=\"ttv-bulk-user-row ttv-skipped\"><i class=\"fas fa-minus-circle\"></i>' +\n            '<span>' + esc(s.u.fullName || s.u.username) + '</span>' +\n            '<span class=\"ttv-bulk-user-what\">hoppas över — ' + esc(s.why) + '</span></div>';\n        });\n        html += '<div class=\"ttv-modal-actions\">' +\n          (eligible.length ?\n            '<button class=\"ttv-btn ttv-danger-btn\" data-ttv-suspend-confirm=\"bulk\">Pausa för ' + eligible.length + ' användare</button>' : '') +\n          '<button class=\"ttv-btn\" data-ttv-modal-close=\"1\">Avbryt</button></div>' +\n          '</div></div>';\n        return html;\n      }\n\n      function renderModal() {\n        var mount = el('ttv-modal-mount');\n        if (!mount) return;\n        if (state.infoModal) { mount.innerHTML = infoModalHtml(); return; }\n        if (state.exportModal) { mount.innerHTML = exportModalHtml(); return; }\n        if (!state.suspendModal) { mount.innerHTML = ''; return; }\n        var modalIds = state.suspendModal.userIds || [];\n        if (modalIds.length > 1) { mount.innerHTML = bulkSuspendModalHtml(modalIds); return; }\n        var user = (state.users || []).filter(function (u) { return u.id === modalIds[0]; })[0];\n        if (!user) { mount.innerHTML = ''; return; }\n        var susp = suspensionOf(user);\n        var html = '<div class=\"ttv-overlay\" data-ttv-modal-bg=\"1\"><div class=\"ttv-modal\">';\n        if (susp) {\n          html += '<div class=\"ttv-modal-title\">Återställ samtalsrättigheter</div>' +\n            '<div class=\"ttv-modal-sub\">' + esc(user.fullName || user.username) + '</div>';\n          html += '<div class=\"ttv-modal-note\"><i class=\"fas fa-info-circle\" style=\"margin-right:6px;\"></i>' +\n            'Pausad ' + esc(new Date(susp.suspendedAt).toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })) +\n            (susp.byName ? ' av ' + esc(susp.byName) : '') + '. Följande togs bort tillfälligt och läggs tillbaka exakt som innan:</div>';\n          (susp.removed || []).forEach(function (s) {\n            html += '<div class=\"ttv-source-row\"><i class=\"fas ' + (s.kind === 'role' ? 'fa-id-badge' : 'fa-users') + '\"></i>' +\n              '<div style=\"flex:1;\"><div>' + esc(s.name) + '</div><div class=\"ttv-source-kind\">' +\n              (s.kind === 'role' ? 'Roll' : s.kind === 'team' ? 'Team' : 'Handlett team') + '</div></div></div>';\n          });\n          html += '<div class=\"ttv-modal-actions\">' +\n            '<button class=\"ttv-btn ttv-primary\" data-ttv-restore=\"' + esc(user.id) + '\">Återställ rättigheter</button>' +\n            '<button class=\"ttv-btn\" data-ttv-modal-close=\"1\">Avbryt</button></div>';\n        } else {\n          var sources = callGrantingSources(user);\n          html += '<div class=\"ttv-modal-title\">Pausa samtalsrättigheter</div>' +\n            '<div class=\"ttv-modal-sub\">' + esc(user.fullName || user.username) + '</div>';\n          if (!sources.length) {\n            html += '<div class=\"ttv-modal-note\">Användaren har inga team eller roller som ger rättigheten att ringa (' + esc(CALL_RIGHT) + '). Inget att pausa.</div>' +\n              '<div class=\"ttv-modal-actions\"><button class=\"ttv-btn\" data-ttv-modal-close=\"1\">Stäng</button></div>';\n          } else {\n            html += '<div class=\"ttv-modal-note\">Rättigheten att ringa kommer från följande team/roller. De tas bort från användaren tillfälligt och kan återställas <b>exakt</b> med ett klick. Användaren loggas samtidigt ut så att ändringen slår igenom direkt' + (state.canUpdateUsers ? ' och en eventuell timer stoppas' : '') + '.</div>';\n            sources.forEach(function (s) {\n              html += '<div class=\"ttv-source-row\"><i class=\"fas ' + (s.kind === 'role' ? 'fa-id-badge' : 'fa-users') + '\"></i>' +\n                '<div style=\"flex:1;\"><div>' + esc(s.name) + '</div><div class=\"ttv-source-kind\">' +\n                (s.kind === 'role' ? 'Roll' : s.kind === 'team' ? 'Team — användaren tas tillfälligt ur teamet' : 'Handlett team — tas tillfälligt bort') + '</div></div></div>';\n            });\n            if (sources.some(function (s) { return s.kind === 'team'; })) {\n              html += '<div class=\"ttv-modal-note\" style=\"color:var(--ttv-warn);\"><i class=\"fas fa-exclamation-triangle\" style=\"margin-right:6px;\"></i>' +\n                'Obs: när användaren tas ur sitt team försvinner även teamets övriga rättigheter tills du återställer.</div>';\n            }\n            html += '<div class=\"ttv-modal-actions\">' +\n              '<button class=\"ttv-btn ttv-danger-btn\" data-ttv-suspend-confirm=\"' + esc(user.id) + '\">Pausa rättigheter</button>' +\n              '<button class=\"ttv-btn\" data-ttv-modal-close=\"1\">Avbryt</button></div>';\n          }\n        }\n        html += '</div></div>';\n        mount.innerHTML = html;\n      }\n\n      function exportModalHtml() {\n        var exp = state.exportModal;\n        var html = '<div class=\"ttv-overlay\" data-ttv-modal-bg=\"1\"><div class=\"ttv-modal\">' +\n          '<div class=\"ttv-modal-title\">Exportera tidrapport</div>' +\n          '<div class=\"ttv-modal-sub\">Excel-fil (.xlsx) med pivottabeller (som er mall) + datablad med tid per dag och pass i detalj.</div>';\n\n        if (exp.mode !== 'self') {\n          html += '<div class=\"ttv-step-label\"><span class=\"ttv-step-num\">1</span> Vad vill du exportera?</div>' +\n            '<div class=\"ttv-preset-grid\">' +\n            '<button class=\"ttv-preset-card' + (exp.mode === 'all' ? ' ttv-active' : '') + '\" data-ttv-exp-mode=\"all\">' +\n            '<i class=\"fas fa-users\"></i><span class=\"ttv-preset-name\">' + (state.isSuperAdmin ? 'Alla användare' : 'Hela teamet') + '</span>' +\n            '<span class=\"ttv-preset-desc\">' + (state.isSuperAdmin ? 'Hela organisationens rapporterade tid i en fil.' : 'Ditt teams rapporterade tid i en fil.') + '</span></button>' +\n            '<button class=\"ttv-preset-card' + (exp.mode === 'user' ? ' ttv-active' : '') + '\" data-ttv-exp-mode=\"user\">' +\n            '<i class=\"fas fa-user-check\"></i><span class=\"ttv-preset-name\">Utvalda användare</span>' +\n            '<span class=\"ttv-preset-desc\">Tidrapport för en eller flera användare.</span></button>' +\n            '</div>';\n          /* the \"borttagna\"-toggle applies to BOTH modes: in Utvalda it reveals removed\n             users in the picker; in Alla/Hela teamet it includes them in the export */\n          if ((state.removedUsers || []).length) {\n            html += '<label class=\"ttv-check-row\" style=\"margin-top:8px;\">' +\n              '<button class=\"ttv-check' + (exp.includeRemoved ? ' ttv-on' : '') + '\" data-ttv-exp-removed=\"1\" title=\"Ta med borttagna (avstängda) användare\"><i class=\"fas fa-check\"></i></button>' +\n              '<span>' + (exp.mode === 'all' ? 'Ta med borttagna användare' : 'Visa borttagna användare') + '</span></label>';\n          }\n          if (exp.mode === 'user') {\n            var pickPool = (state.users || []).slice();\n            if (exp.includeRemoved) pickPool = pickPool.concat(state.removedUsers || []);\n            html += '<div class=\"ttv-exp-users\" style=\"margin-top: 8px;\">';\n            pickPool.sort(function (x, y) {\n              return String(x.fullName || x.username).localeCompare(String(y.fullName || y.username), 'sv');\n            }).forEach(function (u) {\n              var on = (exp.userIds || []).indexOf(u.id) !== -1;\n              var removed = isRemovedUser(u);\n              html += '<button class=\"ttv-mini-chip' + (on ? ' ttv-active' : '') + (removed ? ' ttv-chip-removed' : '') + '\" data-ttv-exp-user=\"' + esc(u.id) + '\"' +\n                (removed ? ' title=\"Borttagen användare\"' : '') + '>' +\n                esc(u.fullName || u.username) + (removed ? ' <span style=\"opacity:.65;font-size:10px;\">(borttagen)</span>' : '') + '</button>';\n            });\n            html += '</div>';\n          }\n        }\n\n        var step = (exp.mode === 'self') ? 0 : 1;\n        step++;\n        html += '<div class=\"ttv-step-label\"><span class=\"ttv-step-num\">' + step + '</span> Format</div>' +\n          '<div class=\"ttv-preset-grid\">' +\n          '<button class=\"ttv-preset-card' + (exp.format !== 'mall' ? ' ttv-active' : '') + '\" data-ttv-exp-format=\"pxl\">' +\n          '<i class=\"fas fa-table\"></i><span class=\"ttv-preset-name\">SaleSys-arbetsbok</span>' +\n          '<span class=\"ttv-preset-desc\">Pivottabeller byggda från dina val nedan.</span></button>' +\n          '<button class=\"ttv-preset-card' + (exp.format === 'mall' ? ' ttv-active' : '') + '\" data-ttv-exp-format=\"mall\">' +\n          '<i class=\"fas fa-file-invoice\"></i><span class=\"ttv-preset-name\">Loxy</span>' +\n          '<span class=\"ttv-preset-desc\">Exakt kopia av Loxy-rapportmallen (Agentstats, Projektstats, Utfallsstats med slicers) fylld med periodens data.</span></button>' +\n          '</div>';\n        step++;\n        html += '<div class=\"ttv-step-label\"><span class=\"ttv-step-num\">' + step + '</span> Period</div>' +\n          '<div class=\"ttv-range-chips\">';\n        [['last30', 'Senaste 30 dagarna'], ['thisMonth', 'Denna månad'], ['lastMonth', 'Förra månaden'], ['custom', 'Eget intervall']].forEach(function (r) {\n          html += '<button class=\"ttv-proj-chip' + (exp.range === r[0] ? ' ttv-active' : '') + '\" data-ttv-exp-range=\"' + r[0] + '\">' + r[1] + '</button>';\n        });\n        html += '</div>';\n        if (exp.range === 'custom') {\n          var maxDate = dayKey(new Date());\n          var minDate = dayKey(new Date(Date.now() - GC_KEEP_MS + 86400000));\n          html += '<div style=\"display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap;\">' +\n            '<input type=\"date\" class=\"ttv-date-input\" id=\"ttv-exp-from\" value=\"' + esc(exp.customFrom || '') + '\" min=\"' + esc(minDate) + '\" max=\"' + esc(maxDate) + '\" title=\"Från och med\">' +\n            '<span style=\"font-size:12.5px;color:var(--ttv-muted-fg);\">–</span>' +\n            '<input type=\"date\" class=\"ttv-date-input\" id=\"ttv-exp-to\" value=\"' + esc(exp.customTo || '') + '\" min=\"' + esc(minDate) + '\" max=\"' + esc(maxDate) + '\" title=\"Till och med\">' +\n            '<span style=\"font-size:12.5px;color:var(--ttv-muted-fg);\">från – till (hela dagar)</span></div>';\n        }\n        if (exp.format === 'mall') {\n          html += '<div class=\"ttv-modal-note\"><i class=\"fas fa-info-circle\" style=\"margin-right:6px;\"></i>' +\n            'Loxy fylls med: Agentstatistik (Inloggad tid = aktiv tid, samtalstid, snittsamtal, antal samtal), ' +\n            'Projektstatistik (samtal, Nej tack, Bortfall) och Utfallsstatistik (alla samtalsutfall). ' +\n            'Kolumner som saknar källa i SaleSys sätts till 0. Beräkning och kapning nedan påverkar Inloggad tid ' +\n            'samt vilka samtal som räknas.</div>';\n        }\n        if (exp.format !== 'mall' && exp.mode !== 'self' && state.adminAllowed) {\n          step++;\n          html += '<div class=\"ttv-step-label\"><span class=\"ttv-step-num\">' + step + '</span> Data</div>' +\n            '<div class=\"ttv-range-chips\">';\n          [['manual', 'Manuell tid'], ['smart', 'Smart tracking'], ['both', 'Båda']].forEach(function (d) {\n            html += '<button class=\"ttv-proj-chip' + (exp.data === d[0] ? ' ttv-active' : '') + '\" data-ttv-exp-data=\"' + d[0] + '\">' + d[1] + '</button>';\n          });\n          html += '</div>';\n        }\n        if (exp.format === 'mall' || exp.data !== 'manual') {\n          step++;\n          html += '<div class=\"ttv-step-label\"><span class=\"ttv-step-num\">' + step + '</span> Beräkning (aktiv tid)</div>' +\n            '<div class=\"ttv-preset-grid\">' +\n            '<button class=\"ttv-preset-card' + (exp.method !== 'span' ? ' ttv-active' : '') + '\" data-ttv-exp-method=\"sum\">' +\n            '<i class=\"fas fa-bolt\"></i><span class=\"ttv-preset-name\">Faktisk aktiv tid</span>' +\n            '<span class=\"ttv-preset-desc\">Summan av all uppmätt aktivitet under dagen.</span></button>' +\n            '<button class=\"ttv-preset-card' + (exp.method === 'span' ? ' ttv-active' : '') + '\" data-ttv-exp-method=\"span\">' +\n            '<i class=\"fas fa-arrows-left-right\"></i><span class=\"ttv-preset-name\">Första–sista aktivitet</span>' +\n            '<span class=\"ttv-preset-desc\">Hela tiden från dagens första till sista aktivitet, minus rast.</span></button>' +\n            '</div>';\n          if (exp.method === 'span') {\n            html += '<div style=\"display:flex;align-items:center;gap:10px;margin-top:10px;\">' +\n              '<label style=\"font-size:13px;font-weight:700;\">Tid att ta bort per dag</label>' +\n              '<input type=\"number\" min=\"0\" max=\"600\" step=\"5\" class=\"ttv-time-input\" style=\"width:70px;\" value=\"' + esc(String(exp.deductMin != null ? exp.deductMin : 60)) + '\" id=\"ttv-exp-deduct\">' +\n              '<span style=\"font-size:12.5px;color:var(--ttv-muted-fg);\">minuter (rast/lunch)</span></div>';\n          }\n          html += '<div style=\"display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap;\">' +\n            '<label style=\"font-size:13px;font-weight:700;\">Kapa till</label>' +\n            '<input type=\"time\" class=\"ttv-time-input\" style=\"width:96px;\" value=\"' + esc(exp.capEarly || '') + '\" id=\"ttv-exp-cap-early\" title=\"Tidigaste tid – aktivitet tidigare räknas från denna tid\">' +\n            '<span style=\"font-size:12.5px;color:var(--ttv-muted-fg);\">–</span>' +\n            '<input type=\"time\" class=\"ttv-time-input\" style=\"width:96px;\" value=\"' + esc(exp.capLate || '') + '\" id=\"ttv-exp-cap-late\" title=\"Sista tid – aktivitet senare kapas till denna tid\">' +\n            '<span style=\"font-size:12.5px;color:var(--ttv-muted-fg);\">tidigaste / sista tid (valfritt)</span></div>';\n        }\n        if (exp.format !== 'mall') {\n        step++;\n        html += '<div class=\"ttv-step-label\"><span class=\"ttv-step-num\">' + step + '</span> Tillägg</div>' +\n          '<div class=\"ttv-range-chips\">' +\n          '<button class=\"ttv-proj-chip' + (exp.outcomes ? ' ttv-active' : '') + '\" data-ttv-exp-outcomes=\"1\" title=\"Lägger till bladet Data_Utfall + pivottabellen Utfallsstats (samtalsutfall per dag, projekt och användare)\">' +\n          '<i class=\"fas ' + (exp.outcomes ? 'fa-check' : 'fa-phone') + '\" style=\"margin-right:6px;font-size:11px;\"></i>Samtalsutfall</button>' +\n          '<button class=\"ttv-proj-chip' + (exp.projTime ? ' ttv-active' : '') + '\" data-ttv-exp-projtime=\"1\" title=\"Lägger till bladet Data_Projekttid + pivottabellen Projektstats (aktiv tid per projekt via user_tabs, plus samtal och samtalstid per projekt, dag och användare)\">' +\n          '<i class=\"fas ' + (exp.projTime ? 'fa-check' : 'fa-briefcase') + '\" style=\"margin-right:6px;font-size:11px;\"></i>Projekttid</button>' +\n          '</div>';\n        }\n        html +=\n          '<div class=\"ttv-modal-note\"><i class=\"fas fa-info-circle\" style=\"margin-right:6px;\"></i>Tidsdata sparas cirka 180 dagar bakåt i tiden.</div>' +\n          '<div class=\"ttv-modal-actions\">' +\n          '<button class=\"ttv-btn ttv-primary\" data-ttv-exp-run=\"1\"' + (exp.busy ? ' disabled' : '') + '>' +\n          (exp.busy ? 'Skapar fil…' : '<i class=\"fas fa-file-excel\" style=\"margin-right:7px;\"></i>Ladda ner .xlsx') + '</button>' +\n          '<button class=\"ttv-btn\" data-ttv-modal-close=\"1\">Avbryt</button>' +\n          '</div></div></div>';\n        return html;\n      }\n\n      /* ---------------- tabs + view mode ---------------- */\n      function renderTabs() {\n        var tabsEl = el('ttv-tabs');\n        if (!tabsEl) return;\n        if (!state.adminAllowed) { tabsEl.style.display = 'none'; return; }\n        tabsEl.style.display = '';\n        var tab = state.tab;\n        tabsEl.innerHTML =\n          '<button class=\"' + (tab === 'seller' ? 'ttv-active' : '') + '\" data-ttv-tab=\"seller\">' +\n          '<i class=\"fas fa-user-clock\"></i> Säljare</button>' +\n          '<button class=\"' + (tab === 'admin' ? 'ttv-active' : '') + '\" data-ttv-tab=\"admin\">' +\n          '<i class=\"fas fa-users-cog\"></i> Admin</button>';\n      }\n      function renderMode() {\n        var adminTab = state.adminAllowed && state.tab === 'admin';\n        var seller = el('ttv-seller-controls');\n        var dialer = el('ttv-dialer-panel');\n        var sellerRight = el('ttv-seller-right');\n        var adminRight = el('ttv-admin-right');\n        var admin = el('ttv-admin');\n        if (seller) seller.style.display = adminTab ? 'none' : 'contents';\n        if (dialer) dialer.style.display = adminTab ? 'contents' : 'none';\n        if (sellerRight) sellerRight.style.display = adminTab ? 'none' : '';\n        if (adminRight) adminRight.style.display = adminTab ? '' : 'none';\n        if (admin) admin.style.display = adminTab ? '' : 'none';\n        if (adminTab) { renderDialerPanel(); renderDialerLog(); }\n      }\n      function dialerIsStopped() {\n        return dialerSuspendedUsers().length > 0 && dialerActiveUsers().length === 0;\n      }\n      function renderDialerPanel() {\n        var panel = el('ttv-dialer-panel');\n        if (!panel || !(state.adminAllowed && state.tab === 'admin')) return;\n        var activeUsers = dialerActiveUsers();\n        var suspended = dialerSuspendedUsers();\n        var total = activeUsers.length + suspended.length;\n        var isStopped = dialerIsStopped();\n        var armed = Boolean(state.armedStopDialer);\n        /* Mirrors the seller card exactly: big readout + one big round toggle + label.\n           Company-wide: active count in the \"clock\" slot, one play/stop button. */\n        var scopeWord = state.isSuperAdmin ? 'användare' : 'i teamet';\n        var sub = isStopped\n          ? 'Dialern är stoppad — ' + suspended.length + (suspended.length === 1 ? (' användare pausad') : (' användare pausade'))\n          : activeUsers.length + ' av ' + total + ' ' + (state.isSuperAdmin ? 'användare kan ringa' : 'i teamet kan ringa');\n        var btnLabel = isStopped ? 'Aktivera' : (armed ? 'Bekräfta' : 'Stoppa');\n        var btnDisabled = state.dialerBusy || !state.canManageDialer ||\n          (isStopped ? suspended.length === 0 : activeUsers.length === 0);\n        var html =\n          '<div class=\"ttv-clock' + (isStopped ? '' : ' ttv-running') + '\">' + activeUsers.length + '</div>' +\n          '<div class=\"ttv-clock-sub\">' + esc(sub) + '</div>' +\n          '<button class=\"ttv-bigbtn' + (isStopped ? '' : ' ttv-stop') + '\" data-ttv-dialer=\"toggle\"' + (btnDisabled ? ' disabled' : '') + '>' +\n          (state.dialerBusy ? '<div class=\"ttv-spinner\" style=\"border-top-color:#fff;\"></div>' :\n            '<i class=\"fas ' + (isStopped ? 'fa-phone-volume' : 'fa-phone-slash') + '\"></i><span>' + btnLabel + '</span>') +\n          '</button>' +\n          '<div class=\"ttv-proj-label\">' + (state.isSuperAdmin ? 'Företagets dialer' : 'Teamets dialer') + '</div>' +\n          '<div class=\"ttv-projects\">' +\n          '<span class=\"ttv-proj-chip\" style=\"cursor:default;\"><span class=\"ttv-proj-dot\" style=\"background:var(--ttv-ok);\"></span>' + activeUsers.length + ' kan ringa</span>' +\n          '<span class=\"ttv-proj-chip\" style=\"cursor:default;\"><span class=\"ttv-proj-dot\" style=\"background:var(--ttv-danger);\"></span>' + suspended.length + ' pausade</span>' +\n          '</div>';\n        panel.innerHTML = html;\n      }\n\n      function dialerLogEntries() {\n        /* aggregate ttDialerLog across all users (own reference is freshest) */\n        var all = [];\n        (state.users || []).map(function (u) {\n          return u.id === state.me.id ? state.me : u;\n        }).forEach(function (u) {\n          var log = refOf(u).ttDialerLog;\n          if (Array.isArray(log)) all = all.concat(log);\n        });\n        /* if me not in users list (non-admin can't happen here), include own */\n        if (!(state.users || []).some(function (u) { return u.id === state.me.id; })) {\n          var mine = refOf(state.me).ttDialerLog;\n          if (Array.isArray(mine)) all = all.concat(mine);\n        }\n        var seen = {};\n        return all.filter(function (e) {\n          if (!e || !e.id || seen[e.id]) return false;\n          seen[e.id] = true;\n          return true;\n        }).sort(function (a, b) { return new Date(b.at) - new Date(a.at); });\n      }\n      function renderDialerLog() {\n        var mount = el('ttv-admin-right');\n        if (!mount || !(state.adminAllowed && state.tab === 'admin')) return;\n        var entries = dialerLogEntries();\n        var rows = '';\n        entries.slice(0, 40).forEach(function (e) {\n          var meta = {\n            stop: { icon: 'fa-phone-slash', color: 'var(--ttv-danger)', label: 'Stoppade dialern' },\n            activate: { icon: 'fa-phone-volume', color: 'var(--ttv-ok)', label: 'Aktiverade dialern' },\n            suspend: { icon: 'fa-phone-slash', color: 'var(--ttv-warn)', label: 'Pausade rättigheter' },\n            restore: { icon: 'fa-phone', color: 'var(--ttv-ok)', label: 'Återställde rättigheter' }\n          }[e.action] || { icon: 'fa-circle', color: 'var(--ttv-muted-fg)', label: e.action };\n          var when = '';\n          try { when = new Date(e.at).toLocaleString('sv-SE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); } catch (x) {}\n          rows += '<div class=\"ttv-log-row\">' +\n            '<span class=\"ttv-log-icon\" style=\"color:' + meta.color + ';background:color-mix(in srgb,' + meta.color + ' 14%,transparent);\"><i class=\"fas ' + meta.icon + '\"></i></span>' +\n            '<div class=\"ttv-log-body\">' +\n            '<div class=\"ttv-log-title\">' + esc(meta.label) + (e.count ? ' <span class=\"ttv-log-count\">' + e.count + (e.count === 1 ? ' användare' : ' användare') + '</span>' : '') + '</div>' +\n            '<div class=\"ttv-log-meta\">' + esc(e.byName || 'Admin') + (when ? ' · ' + esc(when) : '') + '</div>' +\n            '</div></div>';\n        });\n        mount.innerHTML =\n          '<div class=\"ttv-card\">' +\n          '<div class=\"ttv-card-title\"><i class=\"fas fa-history\"></i> Dialer-logg' +\n          '<span class=\"ttv-spacer\"></span>' +\n          '<span style=\"font-size:11.5px;font-weight:400;color:var(--ttv-muted-fg);\">pauser &amp; aktiveringar</span></div>' +\n          (rows || '<div class=\"ttv-empty\">Inga händelser ännu. Starta eller stoppa dialern för att logga.</div>') +\n          '</div>';\n      }\n\n      function renderAll() {\n        var root = el('ttv-root');\n        if (!root) return;\n        root.setAttribute('data-ttv-rendered', '1');\n        renderDate();\n        renderTabs();\n        renderMode();\n        renderSelf();\n        renderToday();\n        renderHistory();\n        renderAdmin();\n      }\n\n      /* ---------------- ticking ---------------- */\n      function tick() {\n        var root = el('ttv-root');\n        if (!root) return;\n        /* re-probe contrast every tick — the app applies the (dark) theme by rewriting a\n           <head> style AFTER our init, so the initial probe can miss it on refresh. Idempotent. */\n        applyContrast();\n        /* the app re-renders the view DOM after init — detect and re-render everything */\n        if (state.loaded && !root.getAttribute('data-ttv-rendered')) { renderAll(); return; }\n\n        var a = state.me ? activeOf(state.me) : null;\n        var clock = el('ttv-clock');\n        if (clock && a) clock.textContent = fmtClock(Date.now() - new Date(a.startedAt).getTime());\n\n        /* live durations in lists */\n        var live = document.querySelectorAll('[data-ttv-live-dur]');\n        for (var i = 0; i < live.length; i++) {\n          var t0 = Number(live[i].getAttribute('data-ttv-live-dur'));\n          if (t0) live[i].textContent = fmtDur(Date.now() - t0);\n        }\n\n        /* disarm kick confirmations after 4 s */\n        if (state.armedKick && Date.now() - state.armedKick.at > 4000) {\n          state.armedKick = null;\n          renderAdmin();\n        }\n        if (state.armedBulkKick && Date.now() - state.armedBulkKick > 4000) {\n          state.armedBulkKick = null;\n          renderAdmin();\n        }\n        if (state.armedStopDialer && Date.now() - state.armedStopDialer > 4000) {\n          state.armedStopDialer = null;\n          renderDialerPanel();\n        }\n        /* app re-renders can wipe the toast mount while an export runs — restore it */\n        if (state.exporting) {\n          var tm = el('ttv-toast-mount');\n          if (tm && !tm.firstChild) toastProgress();\n        }\n      }\n\n      /* ---------------- events (document-level delegation) ---------------- */\n      function inRoot(node) {\n        var root = el('ttv-root');\n        return root && node && root.contains(node);\n      }\n      function closest(target, sel) {\n        return target && target.closest ? target.closest(sel) : null;\n      }\n\n      function onClick(event) {\n        var t = event.target;\n        var hit;\n\n        hit = closest(t, '[data-ttv-tab]');\n        if (hit && inRoot(hit)) {\n          var nextTab = hit.getAttribute('data-ttv-tab');\n          if (nextTab !== state.tab) {\n            state.tab = nextTab;\n            state.armedStopDialer = null;\n            renderAll();\n            if (nextTab === 'admin') loadSmart().then(function () { renderAdmin(); });\n          }\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-dialer]');\n        if (hit && inRoot(hit)) {\n          if (state.dialerBusy) return;\n          if (dialerIsStopped()) {\n            activateDialer();\n          } else if (state.armedStopDialer) {\n            stopDialer();\n          } else {\n            state.armedStopDialer = Date.now();\n            toast('Klicka igen för att stoppa dialern och logga ut alla användare.');\n            renderDialerPanel();\n          }\n          return;\n        }\n\n        hit = closest(t, '#ttv-bigbtn');\n        if (hit && inRoot(hit)) {\n          if (state.busy || !state.loaded) return;\n          if (hit.getAttribute('data-ttv-act') === 'stop') stopTimer();\n          else startTimer();\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-proj]');\n        if (hit && inRoot(hit)) {\n          if (state.busy || !state.loaded) return;\n          pickProject(hit.getAttribute('data-ttv-proj') || null);\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-hist-toggle]');\n        if (hit && inRoot(hit)) {\n          state.historyOpen = !state.historyOpen;\n          renderHistory();\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-admin-edit]');\n        if (hit && inRoot(hit)) { openEditor(hit.getAttribute('data-ttv-admin-edit')); return; }\n\n        hit = closest(t, '[data-ttv-sel]');\n        if (hit && inRoot(hit)) {\n          var sid = hit.getAttribute('data-ttv-sel');\n          if (state.selected[sid]) delete state.selected[sid];\n          else state.selected[sid] = true;\n          renderAdmin();\n          return;\n        }\n        hit = closest(t, '[data-ttv-sel-all]');\n        if (hit && inRoot(hit)) {\n          var everyone = (state.users || []).map(function (u) { return u.id; });\n          var allOn = everyone.length && everyone.every(function (id) { return state.selected[id]; });\n          state.selected = {};\n          if (!allOn) everyone.forEach(function (id) { state.selected[id] = true; });\n          renderAdmin();\n          return;\n        }\n        hit = closest(t, '[data-ttv-sel-clear]');\n        if (hit && inRoot(hit)) {\n          state.selected = {};\n          state.armedBulkKick = null;\n          renderAdmin();\n          return;\n        }\n        hit = closest(t, '[data-ttv-bulk]');\n        if (hit && inRoot(hit)) {\n          var bulkIds = selectedIds();\n          if (!bulkIds.length) return;\n          var action = hit.getAttribute('data-ttv-bulk');\n          if (action === 'suspend') {\n            state.suspendModal = { userIds: bulkIds };\n            renderModal();\n          } else if (action === 'restore') {\n            restoreUsers(bulkIds);\n          } else if (action === 'stop') {\n            bulkStopTimers(bulkIds);\n          } else if (action === 'kick') {\n            if (state.armedBulkKick) {\n              state.armedBulkKick = null;\n              bulkKick(bulkIds.filter(function (id) { return id !== state.me.id; }));\n            } else {\n              state.armedBulkKick = Date.now();\n              toast('Klicka en gång till för att logga ut ' + bulkIds.length + ' användare.');\n              renderAdmin();\n            }\n          } else if (action === 'export') {\n            state.exportModal = {\n              mode: 'user', userIds: bulkIds, range: 'last30',\n              data: state.trackMode || 'manual', method: 'sum', deductMin: 60, capEarly: '', capLate: '', outcomes: false, projTime: false, format: 'pxl', customFrom: '', customTo: '', includeRemoved: false, busy: false\n            };\n            state.suspendModal = null;\n            renderModal();\n          }\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-admin-suspend]');\n        if (hit && inRoot(hit)) {\n          state.suspendModal = { userIds: [hit.getAttribute('data-ttv-admin-suspend')] };\n          renderModal();\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-admin-kick]');\n        if (hit && inRoot(hit)) {\n          var kid = hit.getAttribute('data-ttv-admin-kick');\n          if (state.armedKick && state.armedKick.userId === kid) {\n            state.armedKick = null;\n            kickUser(kid);\n          } else {\n            state.armedKick = { userId: kid, at: Date.now() };\n            toast('Klicka en gång till för att logga ut användaren.');\n            renderAdmin();\n          }\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-admin-stop]');\n        if (hit && inRoot(hit)) { stopUserTimer(hit.getAttribute('data-ttv-admin-stop')); return; }\n\n        hit = closest(t, '[data-ttv-row-del]');\n        if (hit && inRoot(hit) && state.edit) {\n          state.edit.rows.splice(Number(hit.getAttribute('data-ttv-row-del')), 1);\n          state.edit.dirty = true;\n          renderEditor();\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-row-add]');\n        if (hit && inRoot(hit) && state.edit) {\n          var user = editUser();\n          var projs = user ? visibleProjects(user) : [];\n          state.edit.rows.push({ id: null, projectId: projs.length ? projs[0].id : null, start: '08:00', stop: '12:00' });\n          state.edit.dirty = true;\n          renderEditor();\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-row-proj]');\n        if (hit && inRoot(hit) && state.edit) {\n          var parts = hit.getAttribute('data-ttv-row-proj').split(':');\n          var row = state.edit.rows[Number(parts[0])];\n          if (row) { row.projectId = parts[1] || null; state.edit.dirty = true; renderEditor(); }\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-edit-save]');\n        if (hit && inRoot(hit)) { saveEdit(); return; }\n\n        hit = closest(t, '[data-ttv-edit-close]');\n        if (hit && inRoot(hit)) { state.edit = null; renderAdmin(); return; }\n\n        hit = closest(t, '[data-ttv-mode]');\n        if (hit && inRoot(hit)) {\n          var newMode = hit.getAttribute('data-ttv-mode');\n          if (newMode !== state.trackMode) {\n            state.trackMode = newMode;\n            renderAdmin();\n            loadSmart().then(function () { renderAdmin(); });\n            writePrefs({ trackMode: newMode });\n          }\n          return;\n        }\n        hit = closest(t, '[data-ttv-smart-info]');\n        if (hit && inRoot(hit)) {\n          state.infoModal = true;\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-export-open]');\n        if (hit && inRoot(hit)) {\n          var wantSelf = hit.getAttribute('data-ttv-export-open') === 'self' || !state.canExportTeam;\n          state.exportModal = {\n            mode: wantSelf ? 'self' : 'all', userIds: selectedIds(), range: 'last30',\n            data: wantSelf ? 'manual' : (state.trackMode || 'manual'), method: 'sum', deductMin: 60, capEarly: '', capLate: '', outcomes: false, projTime: false, format: 'pxl', customFrom: '', customTo: '', includeRemoved: false, busy: false\n          };\n          state.suspendModal = null;\n          state.infoModal = false;\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-mode]');\n        if (hit && state.exportModal) {\n          state.exportModal.mode = hit.getAttribute('data-ttv-exp-mode');\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-removed]');\n        if (hit && state.exportModal) {\n          var em = state.exportModal;\n          em.includeRemoved = !em.includeRemoved;\n          if (!em.includeRemoved) {\n            /* dropping the toggle also deselects any removed users that just disappeared */\n            var removedIds = (state.removedUsers || []).map(function (u) { return u.id; });\n            em.userIds = (em.userIds || []).filter(function (id) { return removedIds.indexOf(id) === -1; });\n          }\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-user]');\n        if (hit && state.exportModal) {\n          var expUid = hit.getAttribute('data-ttv-exp-user');\n          var list = state.exportModal.userIds || (state.exportModal.userIds = []);\n          var at = list.indexOf(expUid);\n          if (at === -1) list.push(expUid); else list.splice(at, 1);\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-range]');\n        if (hit && state.exportModal) {\n          state.exportModal.range = hit.getAttribute('data-ttv-exp-range');\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-data]');\n        if (hit && state.exportModal) {\n          state.exportModal.data = hit.getAttribute('data-ttv-exp-data');\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-method]');\n        if (hit && state.exportModal) {\n          state.exportModal.method = hit.getAttribute('data-ttv-exp-method');\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-outcomes]');\n        if (hit && state.exportModal) {\n          state.exportModal.outcomes = !state.exportModal.outcomes;\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-projtime]');\n        if (hit && state.exportModal) {\n          state.exportModal.projTime = !state.exportModal.projTime;\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-format]');\n        if (hit && state.exportModal) {\n          state.exportModal.format = hit.getAttribute('data-ttv-exp-format');\n          renderModal();\n          return;\n        }\n        hit = closest(t, '[data-ttv-exp-run]');\n        if (hit && state.exportModal) { runExport(); return; }\n\n        hit = closest(t, '[data-ttv-suspend-confirm]');\n        if (hit) {\n          var confArg = hit.getAttribute('data-ttv-suspend-confirm');\n          suspendUsers(confArg === 'bulk' ?\n            (state.suspendModal && state.suspendModal.userIds || []) : [confArg]);\n          return;\n        }\n\n        hit = closest(t, '[data-ttv-restore]');\n        if (hit) { restoreUsers([hit.getAttribute('data-ttv-restore')]); return; }\n\n        hit = closest(t, '[data-ttv-modal-close]');\n        if (hit) { state.suspendModal = null; state.exportModal = null; state.infoModal = false; renderModal(); return; }\n\n        /* click on overlay background closes the modal */\n        if (t && t.getAttribute && t.getAttribute('data-ttv-modal-bg')) {\n          state.suspendModal = null;\n          state.exportModal = null;\n          state.infoModal = false;\n          renderModal();\n        }\n      }\n\n      function onChange(event) {\n        var t = event.target;\n        if (t.id === 'ttv-exp-deduct' && state.exportModal) {\n          var v = parseInt(t.value, 10);\n          state.exportModal.deductMin = isNaN(v) ? 0 : Math.max(0, Math.min(600, v));\n          return;\n        }\n        if (t.id === 'ttv-exp-cap-early' && state.exportModal) { state.exportModal.capEarly = t.value || ''; return; }\n        if (t.id === 'ttv-exp-cap-late' && state.exportModal) { state.exportModal.capLate = t.value || ''; return; }\n        if (t.id === 'ttv-exp-from' && state.exportModal) { state.exportModal.customFrom = t.value || ''; return; }\n        if (t.id === 'ttv-exp-to' && state.exportModal) { state.exportModal.customTo = t.value || ''; return; }\n        if (!inRoot(t) || !state.edit) return;\n        if (t.id === 'ttv-edit-date') {\n          var user = editUser();\n          if (user) {\n            state.edit.date = t.value || dayKey(new Date());\n            state.edit.rows = rowsForDay(user, state.edit.date);\n            state.edit.dirty = false;\n            renderEditor();\n          }\n          return;\n        }\n        var startIdx = t.getAttribute('data-ttv-row-start');\n        var stopIdx = t.getAttribute('data-ttv-row-stop');\n        if (startIdx != null && state.edit.rows[Number(startIdx)]) {\n          state.edit.rows[Number(startIdx)].start = t.value;\n          state.edit.dirty = true;\n          renderEditor();\n        } else if (stopIdx != null && state.edit.rows[Number(stopIdx)]) {\n          state.edit.rows[Number(stopIdx)].stop = t.value;\n          state.edit.dirty = true;\n          renderEditor();\n        }\n      }\n\n      function onKeyDown(event) {\n        if (event.key === 'Escape' && (state.suspendModal || state.exportModal || state.infoModal)) {\n          state.suspendModal = null;\n          state.exportModal = null;\n          state.infoModal = false;\n          renderModal();\n        }\n      }\n\n      /* ---------------- contrast probe (text on primary) ---------------- */\n      var lastPrimaryFg = null;\n      function applyContrast() {\n        var root = el('ttv-root');\n        if (!root) return;\n        var probe = document.createElement('div');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var rgb = (getComputedStyle(probe).color.match(/\\d+/g) || ['21', '101', '192']).map(Number);\n        probe.remove();\n        var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n        var fg = lum > 0.6 ? '#1b2430' : '#ffffff';\n        if (fg !== lastPrimaryFg) {\n          lastPrimaryFg = fg;\n          root.style.setProperty('--ttv-primary-fg', fg);\n        }\n      }\n      var themeObserver = new MutationObserver(applyContrast);\n      themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      themeObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n      if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n      applyContrast();\n\n      /* refresh promptly when the tab regains focus — catches dialer re-activation faster */\n      function onVisibility() {\n        if (!document.hidden && state.loaded && el('ttv-root')) refresh();\n      }\n\n      document.addEventListener('click', onClick);\n      document.addEventListener('change', onChange);\n      document.addEventListener('keydown', onKeyDown);\n      document.addEventListener('visibilitychange', onVisibility);\n\n      var tickTimer = setInterval(tick, 500);\n      var pollTimer = setInterval(function () {\n        if (!document.hidden && el('ttv-root')) refresh();\n      }, POLL_MS);\n\n      window.__ttvCleanup = function () {\n        themeObserver.disconnect();\n        clearInterval(tickTimer);\n        clearInterval(pollTimer);\n        if (state.toastTimer) clearTimeout(state.toastTimer);\n        document.removeEventListener('click', onClick);\n        document.removeEventListener('change', onChange);\n        document.removeEventListener('keydown', onKeyDown);\n        document.removeEventListener('visibilitychange', onVisibility);\n      };\n\n      boot();\n    })();\n  </script>\n</div>","iconName":"stopwatch","iconColor":"#00897B","settingsHtml":"","description":"","rights":[{"right":"viewTeamTime","name":"Se teamets tider","description":"Visar Admin-fliken med teamets tider, närvaro och smart tracking."},{"right":"editTime","name":"Redigera tider","description":"Redigera teamets tidsstämplar retroaktivt och stoppa pågående timers. Kräver även plattformsrättigheten Uppdatera användare."},{"right":"manageDialer","name":"Styra dialern","description":"Pausa och återställa samtalsrättigheter för teamet samt använda teamets dialer-knapp. Kräver även Uppdatera användare."},{"right":"exportTeam","name":"Exportera teamets tider","description":"Exportera tidrapporter för teamets användare."}],"components":[]},{"id":"6a55ee69c215b87561e20f2e","name":"Filtrerad lista","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"navigation","position":1,"label":"Lista"}],"html":"<div id=\"ksl-root\">\n  <style>\n    #ksl-root {\n      --ksl-bg: var(--color-background-2, #fafafa);\n      --ksl-card: var(--color-background-1, #ffffff);\n      --ksl-secondary: var(--color-background-3, #eeeeee);\n      --ksl-fg: var(--color-text-1, #212121);\n      --ksl-muted-fg: var(--color-text-3, #757575);\n      --ksl-primary: var(--color-primary, #1565C0);\n      --ksl-primary-fg: #ffffff;\n      --ksl-danger: var(--color-danger, #E53935);\n      --ksl-green: #2E7D32;\n      --ksl-hover: var(--color-element-hover, rgba(20, 20, 20, 0.04));\n      --ksl-border: color-mix(in srgb, var(--ksl-fg) 13%, transparent);\n      --ksl-border-soft: color-mix(in srgb, var(--ksl-fg) 8%, transparent);\n      --ksl-radius: 8px;\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--ksl-fg);\n      display: block;\n      background: var(--ksl-bg);\n    }\n    #ksl-root * { box-sizing: border-box; }\n    #ksl-root button { font-family: inherit; cursor: pointer; }\n    #ksl-root input { font-family: inherit; font-size: 14px; color: var(--ksl-fg); }\n\n    .ksl-wrap { max-width: 1180px; margin: 0 auto; padding: 4px 20px 100px; }\n\n    /* header */\n    .ksl-head { display: flex; align-items: center; gap: 14px; padding: 10px 2px 16px; flex-wrap: wrap; }\n    .ksl-title { font-size: 22px; font-weight: 800; letter-spacing: -0.01em; margin: 0; display: flex; align-items: center; gap: 10px; }\n    .ksl-title .ksl-count {\n      font-size: 13px; font-weight: 700; color: var(--ksl-muted-fg);\n      background: var(--ksl-secondary); border-radius: 999px; padding: 3px 11px;\n    }\n    .ksl-head-spacer { flex: 1; }\n    .ksl-search {\n      position: relative; width: 260px; max-width: 42vw;\n    }\n    .ksl-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: var(--ksl-muted-fg); pointer-events: none; }\n    .ksl-search input {\n      width: 100%; padding: 9px 12px 9px 34px; border: 1px solid var(--ksl-border);\n      border-radius: var(--ksl-radius); outline: none; background: var(--ksl-card);\n    }\n    .ksl-search input:focus { border-color: var(--ksl-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ksl-primary) 16%, transparent); }\n    .ksl-iconbtn {\n      background: var(--ksl-card); border: 1px solid var(--ksl-border); border-radius: var(--ksl-radius);\n      width: 38px; height: 38px; display: inline-flex; align-items: center; justify-content: center; color: var(--ksl-muted-fg);\n    }\n    .ksl-iconbtn:hover { background: var(--ksl-hover); color: var(--ksl-fg); }\n    .ksl-iconbtn.ksl-spinning svg { animation: ksl-spin 0.9s linear infinite; }\n\n    /* filter summary chips */\n    .ksl-filterbar { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 2px 14px; }\n    .ksl-fchip {\n      display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600;\n      color: var(--ksl-muted-fg); background: var(--ksl-card); border: 1px solid var(--ksl-border-soft);\n      border-radius: 999px; padding: 4px 11px;\n    }\n    .ksl-fchip svg { width: 12px; height: 12px; opacity: 0.75; }\n\n    /* table */\n    .ksl-tablecard { background: var(--ksl-card); border: 1px solid var(--ksl-border-soft); border-radius: 12px; overflow: hidden; }\n    .ksl-table { width: 100%; border-collapse: collapse; }\n    .ksl-table thead th {\n      text-align: left; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;\n      color: var(--ksl-muted-fg); padding: 12px 14px; border-bottom: 1px solid var(--ksl-border-soft);\n      white-space: nowrap; background: var(--ksl-card); position: sticky; top: 0; z-index: 2;\n    }\n    .ksl-table thead th.ksl-num { text-align: right; }\n    .ksl-row { border-bottom: 1px solid var(--ksl-border-soft); cursor: pointer; transition: background 0.08s; }\n    .ksl-row:last-child { border-bottom: none; }\n    .ksl-row:hover { background: var(--ksl-hover); }\n    .ksl-row.ksl-open { background: color-mix(in srgb, var(--ksl-primary) 5%, transparent); }\n    .ksl-row td { padding: 12px 14px; font-size: 14px; vertical-align: middle; }\n    .ksl-row td.ksl-num { text-align: right; font-variant-numeric: tabular-nums; color: var(--ksl-muted-fg); }\n    .ksl-primarycell { font-weight: 700; }\n    .ksl-sub { font-size: 12px; color: var(--ksl-muted-fg); font-weight: 500; }\n    .ksl-muted { color: var(--ksl-muted-fg); }\n    .ksl-nowrap { white-space: nowrap; }\n\n    /* avatar for seller */\n    .ksl-seller { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }\n    .ksl-av {\n      width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;\n      font-size: 11px; font-weight: 700; color: #fff;\n    }\n\n    /* tag / status chips */\n    .ksl-tags { display: inline-flex; flex-wrap: wrap; gap: 4px; }\n    .ksl-tag { font-size: 11px; font-weight: 700; border-radius: 5px; padding: 2px 7px; white-space: nowrap; }\n    .ksl-status { font-size: 11px; font-weight: 700; border-radius: 999px; padding: 3px 10px; white-space: nowrap; display: inline-block; }\n\n    /* recording button */\n    .ksl-play {\n      display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--ksl-border);\n      background: var(--ksl-card); border-radius: 999px; padding: 4px 12px 4px 9px; font-size: 12px; font-weight: 700; color: var(--ksl-fg);\n    }\n    .ksl-play:hover { background: var(--ksl-hover); }\n    .ksl-play svg { width: 12px; height: 12px; color: var(--ksl-primary); }\n\n    /* expanded detail */\n    .ksl-detail td { padding: 0; background: var(--ksl-bg); }\n    .ksl-detail-inner { padding: 16px 18px; border-bottom: 1px solid var(--ksl-border-soft); }\n    .ksl-detail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px 22px; }\n    .ksl-dl { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n    .ksl-dl dt { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--ksl-muted-fg); }\n    .ksl-dl dd { margin: 0; font-size: 14px; word-break: break-word; }\n    .ksl-detail-section { margin-top: 14px; }\n    .ksl-detail-section h4 { margin: 0 0 8px; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.03em; color: var(--ksl-muted-fg); }\n    .ksl-prodrow { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; padding: 4px 0; border-bottom: 1px dashed var(--ksl-border-soft); }\n    .ksl-prodrow:last-child { border-bottom: none; }\n    .ksl-audio { width: 100%; margin-top: 8px; }\n    .ksl-linkbtn {\n      display: inline-flex; align-items: center; gap: 6px; text-decoration: none; font-size: 13px; font-weight: 700;\n      color: var(--ksl-primary); border: 1px solid var(--ksl-border); border-radius: var(--ksl-radius); padding: 6px 12px; background: var(--ksl-card);\n    }\n    .ksl-linkbtn:hover { background: var(--ksl-hover); }\n    .ksl-linkbtn svg { width: 13px; height: 13px; }\n\n    /* states */\n    .ksl-loading, .ksl-empty, .ksl-error {\n      display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;\n      padding: 70px 20px; text-align: center; color: var(--ksl-muted-fg);\n    }\n    .ksl-empty svg, .ksl-error svg { width: 42px; height: 42px; opacity: 0.5; }\n    .ksl-empty-title { font-size: 16px; font-weight: 700; color: var(--ksl-fg); }\n    .ksl-error { color: var(--ksl-danger); }\n    .ksl-spin { animation: ksl-spin 0.9s linear infinite; }\n    @keyframes ksl-spin { to { transform: rotate(360deg); } }\n\n    /* load more */\n    .ksl-more { display: flex; justify-content: center; padding: 18px 0 4px; }\n    .ksl-more button {\n      background: var(--ksl-card); border: 1px solid var(--ksl-border); border-radius: var(--ksl-radius);\n      padding: 9px 20px; font-size: 13px; font-weight: 700; color: var(--ksl-fg);\n    }\n    .ksl-more button:hover { background: var(--ksl-hover); }\n    .ksl-more button:disabled { opacity: 0.6; cursor: default; }\n\n    .ksl-notconfig {\n      background: var(--ksl-card); border: 1px dashed var(--ksl-border); border-radius: 12px;\n      padding: 40px 24px; text-align: center; color: var(--ksl-muted-fg); max-width: 560px; margin: 30px auto;\n    }\n    .ksl-notconfig h3 { margin: 0 0 8px; color: var(--ksl-fg); font-size: 18px; }\n    .ksl-notconfig p { margin: 6px 0; font-size: 14px; line-height: 1.5; }\n    .ksl-notconfig .ksl-steps { text-align: left; display: inline-block; margin-top: 10px; font-size: 14px; }\n\n    @media (max-width: 720px) {\n      .ksl-hide-sm { display: none; }\n      .ksl-wrap { padding: 4px 12px 100px; }\n    }\n  </style>\n\n  <div id=\"ksl-mount\">\n    <div class=\"ksl-loading\"><svg class=\"ksl-spin\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Laddar...</div>\n  </div>\n\n  <script>\n  (function () {\n    'use strict';\n    var API = 'https://app.salesys.se/api';\n\n    if (window.__kslCleanup) { try { window.__kslCleanup(); } catch (e) { /* ignore */ } window.__kslCleanup = null; }\n\n    /* ---------- tiny utils ---------- */\n    function el(id) { return document.getElementById(id); }\n    function esc(s) {\n      return String(s == null ? '' : s).replace(/[&<>\"']/g, function (c) {\n        return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c];\n      });\n    }\n    function asArray(r, key) { return Array.isArray(r) ? r : ((r && r[key]) || []); }\n\n    function getToken() {\n      var cookies = document.cookie.split(';').reduce(function (o, c) {\n        var p = /(.+?)=(.+)/.exec(c.trim()); if (p) o[p[1]] = p[2]; return o;\n      }, {});\n      var raw = cookies['s2_utoken'];\n      if (!raw) return null;\n      try { return decodeURIComponent(raw); } catch (e) { return raw; }\n    }\n\n    function api(method, path, params, body) {\n      var qs = '';\n      if (params) {\n        var pairs = [];\n        Object.keys(params).forEach(function (k) {\n          var v = params[k];\n          if (v === undefined || v === null || v === '') return;\n          if (Array.isArray(v)) { if (!v.length) return; v = v.join(','); }\n          pairs.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));\n        });\n        if (pairs.length) qs = '?' + pairs.join('&');\n      }\n      var opts = { method: method || 'GET', headers: { 'Authorization': 'Bearer ' + getToken() } };\n      if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }\n      return fetch(API + path + qs, opts).then(function (res) {\n        if (!res.ok) { var err = new Error('API ' + res.status + ' ' + path); err.status = res.status; throw err; }\n        return res.text().then(function (t) { try { return JSON.parse(t); } catch (e) { return null; } });\n      });\n    }\n\n    /* ---------- formatting ---------- */\n    function fmtDate(v) {\n      if (!v) return '';\n      var d = new Date(v), now = new Date();\n      var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n      if (sameDay) return d.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });\n      if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' });\n      return d.toLocaleDateString('sv-SE');\n    }\n    function fmtDateFull(v) {\n      if (!v) return '';\n      return new Date(v).toLocaleString('sv-SE', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });\n    }\n    function fmtPhone(p) {\n      if (!p) return '';\n      p = String(p);\n      if (p.indexOf('+46') === 0) p = '0' + p.substring(3);\n      if (/^07\\d{8}$/.test(p)) return p.substring(0, 3) + '-' + p.substring(3, 6) + ' ' + p.substring(6, 8) + ' ' + p.substring(8);\n      return p;\n    }\n    function fmtMoney(n) {\n      if (n == null || isNaN(n)) return '';\n      return Math.round(n).toLocaleString('sv-SE') + ' kr';\n    }\n    function fmtDuration(ms) {\n      if (!ms) return '0:00';\n      var s = Math.round(ms / 1000), m = Math.floor(s / 60);\n      return m + ':' + ('0' + (s % 60)).slice(-2);\n    }\n\n    var AV_COLORS = ['#1565C0', '#00897B', '#5E35B1', '#EF6C00', '#C2185B', '#00838F', '#2E7D32', '#6D4C41', '#546E7A', '#AD1457'];\n    function avatarColor(id) {\n      var h = 0, s = String(id || '');\n      for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;\n      return AV_COLORS[h % AV_COLORS.length];\n    }\n    function initials(name) {\n      var parts = String(name || '?').trim().split(/\\s+/);\n      return ((parts[0] || '')[0] || '?').toUpperCase() + (parts.length > 1 ? (parts[parts.length - 1][0] || '').toUpperCase() : '');\n    }\n\n    var TAG_COLORS = {\n      red: '#E53935', pink: '#D81B60', purple: '#8E24AA', 'deep-purple': '#5E35B1', indigo: '#3949AB',\n      blue: '#1E88E5', 'light-blue': '#039BE5', cyan: '#00ACC1', teal: '#00897B', green: '#43A047',\n      'light-green': '#7CB342', lime: '#C0CA33', yellow: '#FDD835', amber: '#FFB300', orange: '#FB8C00',\n      'deep-orange': '#F4511E', brown: '#6D4C41', grey: '#757575', 'blue-grey': '#546E7A'\n    };\n    function tagColor(c) { return TAG_COLORS[c] || (typeof c === 'string' && c[0] === '#' ? c : '#607D8B'); }\n\n    /* ---------- state ---------- */\n    var state = {\n      viewId: null,\n      cfg: null,\n      loading: true,\n      error: null,\n      items: [],\n      total: null,\n      offset: 0,\n      pageSize: 50,\n      hasMore: false,\n      loadingMore: false,\n      search: '',\n      open: {},          /* id -> true */\n      ref: {             /* lookup maps, loaded lazily per entity */\n        users: {}, orderTags: {}, orderFields: {}, callTags: {}, templates: {}, projects: {}\n      },\n      recordingUrls: {}  /* callId -> objectURL */\n    };\n\n    /* ---------- config ---------- */\n    function parseViewId() {\n      var m = /\\/views\\/([a-f0-9]{24})/i.exec(location.pathname) || /\\/views\\/([a-f0-9]{24})/i.exec(location.href);\n      return m ? m[1] : null;\n    }\n    var ENTITY_META = {\n      orders: { title: 'Ordrar', icon: 'cart', empty: 'Inga ordrar matchar filtret.' },\n      offers: { title: 'Avtal', icon: 'file', empty: 'Inga avtal matchar filtret.' },\n      calls: { title: 'Samtal', icon: 'phone', empty: 'Inga samtal matchar filtret.' }\n    };\n    function defaultCfg() { return { configured: false, entity: 'orders' }; }\n    function normCfg(ps) {\n      var c = {\n        configured: true,\n        entity: (ps.entity === 'offers' || ps.entity === 'calls') ? ps.entity : 'orders',\n        title: ps.title || '',\n        scope: ps.scope || 'all',\n        dateRange: ps.dateRange || 'all',\n        projectIds: ps.projectIds || [],\n        isTest: ps.isTest === true ? 'any' : (ps.isTest === 'any' ? 'any' : false),\n        pageSize: ps.pageSize || 50,\n        showSearch: ps.showSearch !== false,\n        order: ps.order || {},\n        offer: ps.offer || {},\n        call: ps.call || {}\n      };\n      return c;\n    }\n\n    function loadConfig() {\n      state.viewId = parseViewId();\n      if (!state.viewId) { state.cfg = defaultCfg(); return Promise.resolve(); }\n      return api('GET', '/users/views-v1').then(function (views) {\n        var list = asArray(views, 'views'), mine = null;\n        for (var i = 0; i < list.length; i++) if (list[i].id === state.viewId) { mine = list[i]; break; }\n        var ps = mine && mine.presetState;\n        state.cfg = (ps && ps.configured && ps.entity) ? normCfg(ps) : defaultCfg();\n      }).catch(function () { state.cfg = defaultCfg(); });\n    }\n\n    /* ---------- reference data ---------- */\n    function indexById(list, map) { (list || []).forEach(function (x) { if (x && x.id) map[x.id] = x; }); return map; }\n    function loadReference() {\n      var c = state.cfg, jobs = [\n        api('GET', '/users/users-v1').then(function (r) { indexById(asArray(r, 'users'), state.ref.users); }).catch(function () {})\n      ];\n      if (c.entity === 'orders') {\n        jobs.push(api('GET', '/orders/tags-v1').then(function (r) { indexById(asArray(r, 'tags'), state.ref.orderTags); }).catch(function () {}));\n        jobs.push(api('GET', '/orders/fields-v1').then(function (r) { indexById(asArray(r, 'fields'), state.ref.orderFields); }).catch(function () {}));\n      } else if (c.entity === 'calls') {\n        jobs.push(api('GET', '/dial/tags-v1').then(function (r) { indexById(asArray(r, 'tags'), state.ref.callTags); }).catch(function () {}));\n      } else if (c.entity === 'offers') {\n        jobs.push(api('GET', '/offers/offer-templates-v2').then(function (r) { indexById(asArray(r, 'templates'), state.ref.templates); }).catch(function () {}));\n      }\n      return Promise.all(jobs);\n    }\n\n    function userName(id) { var u = state.ref.users[id]; return (u && (u.fullName || u.alias || u.name)) || ''; }\n\n    /* ---------- date range ---------- */\n    function dateRange(key) {\n      if (!key || key === 'all') return {};\n      var now = new Date(), start = new Date(now);\n      start.setHours(0, 0, 0, 0);\n      if (key === 'today') { /* start already today 00:00 */ }\n      else if (key === 'yesterday') { start.setDate(start.getDate() - 1); return { from: start, to: new Date(start.getTime() + 86400000) }; }\n      else if (key === 'thisWeek') { var d = (start.getDay() + 6) % 7; start.setDate(start.getDate() - d); }\n      else if (key === 'thisMonth') { start.setDate(1); }\n      else if (key === 'thisYear') { start.setMonth(0, 1); }\n      else if (key === 'last7') { start.setDate(start.getDate() - 7); }\n      else if (key === 'last30') { start.setDate(start.getDate() - 30); }\n      return { from: start, to: now };\n    }\n\n    /* ---------- query building ---------- */\n    function scopeUserIds() {\n      var c = state.cfg;\n      if (c.scope === 'me') { return [state.me && state.me.id].filter(Boolean); }\n      return null;\n    }\n    function scopeTeamIds() {\n      var c = state.cfg;\n      if (c.scope === 'team') { return (state.me && state.me.teamId) ? [state.me.teamId] : null; }\n      return null;\n    }\n\n    function buildParams() {\n      var c = state.cfg, r = dateRange(c.dateRange), p = {\n        count: c.pageSize, offset: state.offset\n      };\n      var uids = scopeUserIds(), tids = scopeTeamIds();\n      if (uids) p.userIds = uids;\n      if (tids) p.teamIds = tids;\n      if (c.projectIds && c.projectIds.length) p.projectIds = c.projectIds;\n\n      if (c.entity === 'orders') {\n        var o = c.order || {};\n        if (r.from) p.from = r.from.toISOString();\n        if (r.to) p.to = r.to.toISOString();\n        p.isTest = c.isTest;\n        if (o.anyTagId && o.anyTagId.length) p.anyTagId = o.anyTagId;\n        if (o.allTagIds && o.allTagIds.length) p.allTagIds = o.allTagIds;\n        if (o.notAnyTagId && o.notAnyTagId.length) p.notAnyTagId = o.notAnyTagId;\n        p.sortBy = o.sortBy || 'serialId';\n        p.sortOrder = o.sortOrder || 'desc';\n        if (state.search) p.s = state.search;\n      } else if (c.entity === 'offers') {\n        var of = c.offer || {};\n        if (r.from) p.from = r.from.toISOString();\n        if (r.to) p.to = r.to.toISOString();\n        p.isTest = c.isTest;\n        if (of.statuses && of.statuses.length) p.statuses = of.statuses;\n        if (of.templateIds && of.templateIds.length) p.templateIds = of.templateIds;\n        p.dateType = of.dateType || 'created';\n        p.sortBy = of.sortBy || 'date';\n        p.sortOrder = of.sortOrder || 'desc';\n        if (state.search) p.s = state.search;\n      } else if (c.entity === 'calls') {\n        var cl = c.call || {};\n        if (r.from) p.after = r.from.toISOString();\n        if (r.to) p.before = r.to.toISOString();\n        p.connectionState = cl.connectionState || 'any';\n        if (cl.callTagIds && cl.callTagIds.length) p.anyTagId = cl.callTagIds;\n        if (cl.direction === 'inbound') p.isInbound = true;\n        else if (cl.direction === 'outbound') p.isInbound = false;\n        if (cl.isRecordingAvailable) p.isRecordingAvailable = true;\n        if (cl.voicemail === true) p.voicemail = true;\n        else if (cl.voicemail === false) p.voicemail = false;\n        p.sortBy = cl.sortBy || 'connectingAt';\n        p.sortOrder = cl.sortOrder || 'desc';\n        if (state.search) p.s = state.search;\n      }\n      return p;\n    }\n\n    var LIST_ENDPOINT = { orders: '/orders/orders-v3', offers: '/offers/offers-v2', calls: '/dial/calls-v1' };\n    var COUNT_ENDPOINT = { orders: '/orders/orders-v2/count', offers: '/offers/offers-v1/count', calls: '/dial/calls-v1/count' };\n    var RESULT_KEY = { orders: 'orders', offers: 'offers', calls: 'calls' };\n\n    function fetchPage() {\n      var c = state.cfg, p = buildParams();\n      return api('GET', LIST_ENDPOINT[c.entity], p).then(function (r) { return asArray(r, RESULT_KEY[c.entity]); });\n    }\n    function fetchCount() {\n      var c = state.cfg, p = buildParams();\n      delete p.count; delete p.offset; delete p.sortBy; delete p.sortOrder;\n      return api('GET', COUNT_ENDPOINT[c.entity], p).then(function (r) { return r && typeof r.count === 'number' ? r.count : null; }).catch(function () { return null; });\n    }\n\n    /* ---------- boot / load ---------- */\n    function boot() {\n      state.loading = true; render();\n      Promise.all([\n        api('GET', '/users/users-v1/me').catch(function () { return null; }),\n        loadConfig()\n      ]).then(function (r) {\n        state.me = r[0];\n        if (!state.cfg || !state.cfg.configured) { state.loading = false; render(); return; }\n        state.pageSize = state.cfg.pageSize;\n        return loadReference().then(function () { return reload(); });\n      }).catch(function (err) {\n        state.loading = false;\n        state.error = (err && err.status === 403) ? 'Du saknar behörighet att läsa denna data.' : 'Kunde inte ladda. Försök igen.';\n        render();\n      });\n    }\n\n    function reload() {\n      state.offset = 0; state.items = []; state.open = {}; state.loading = true; render();\n      return Promise.all([fetchPage(), fetchCount()]).then(function (r) {\n        state.items = r[0]; state.total = r[1];\n        state.offset = state.items.length;\n        state.hasMore = state.items.length >= state.pageSize;\n        state.loading = false; state.error = null; render();\n      }).catch(function (err) {\n        state.loading = false;\n        state.error = (err && err.status === 403) ? 'Du saknar behörighet att läsa denna data.' : 'Kunde inte ladda listan.';\n        render();\n      });\n    }\n\n    function loadMore() {\n      if (state.loadingMore || !state.hasMore) return;\n      state.loadingMore = true; render();\n      fetchPage().then(function (rows) {\n        state.items = state.items.concat(rows);\n        state.offset = state.items.length;\n        state.hasMore = rows.length >= state.pageSize;\n        state.loadingMore = false; render();\n      }).catch(function () { state.loadingMore = false; state.hasMore = false; render(); });\n    }\n\n    /* ---------- order helpers ---------- */\n    var NAME_LABELS = ['fullständigt namn', 'namn', 'förnamn', 'kontakt', 'kund'];\n    var COMPANY_LABELS = ['företagsnamn', 'företag', 'bolag'];\n    function fieldVal(fields, fieldId) {\n      for (var i = 0; i < (fields || []).length; i++) if (fields[i].fieldId === fieldId) return fields[i].value;\n      return null;\n    }\n    function fieldValByLabel(fields, labels) {\n      for (var i = 0; i < (fields || []).length; i++) {\n        var f = state.ref.orderFields[fields[i].fieldId];\n        if (f && labels.indexOf(String(f.label || '').toLowerCase()) !== -1 && fields[i].value) return fields[i].value;\n      }\n      return null;\n    }\n    function orderName(order) {\n      var first = fieldValByLabel(order.fields, ['förnamn']);\n      var last = fieldValByLabel(order.fields, ['efternamn']);\n      if (first || last) return [first, last].filter(Boolean).join(' ');\n      return fieldValByLabel(order.fields, NAME_LABELS) || fieldValByLabel(order.fields, COMPANY_LABELS) || ('Order #' + order.serialId);\n    }\n    function orderSum(order) {\n      var s = 0, has = false;\n      (order.products || []).forEach(function (p) { if (typeof p.price === 'number') { has = true; s += p.price * (p.quantity || 1); } });\n      return has ? s : null;\n    }\n\n    /* ---------- rendering: columns ---------- */\n    function sellerCell(userId) {\n      var name = userName(userId);\n      if (!name) return '<span class=\"ksl-muted\">—</span>';\n      return '<span class=\"ksl-seller\"><span class=\"ksl-av\" style=\"background:' + avatarColor(userId) + '\">' + esc(initials(name)) + '</span>' + esc(name) + '</span>';\n    }\n    function tagsCell(tagIds, map) {\n      if (!tagIds || !tagIds.length) return '';\n      return '<span class=\"ksl-tags\">' + tagIds.map(function (id) {\n        var t = map[id]; if (!t) return '';\n        var col = tagColor(t.color);\n        return '<span class=\"ksl-tag\" style=\"background:' + col + '22;color:' + col + '\">' + esc(t.name) + '</span>';\n      }).join('') + '</span>';\n    }\n\n    var OFFER_STATUS = {\n      pending: { label: 'Väntar', color: '#FB8C00' },\n      distributed: { label: 'Skickat', color: '#1E88E5' },\n      read: { label: 'Läst', color: '#00ACC1' },\n      signed: { label: 'Signerat', color: '#2E7D32' },\n      canceled: { label: 'Avbrutet', color: '#757575' },\n      expired: { label: 'Utgånget', color: '#E53935' }\n    };\n    function offerStatusChip(status) {\n      var s = OFFER_STATUS[status] || { label: status || '—', color: '#607D8B' };\n      return '<span class=\"ksl-status\" style=\"background:' + s.color + '1f;color:' + s.color + '\">' + esc(s.label) + '</span>';\n    }\n\n    function headings() {\n      var c = state.cfg;\n      if (c.entity === 'orders') {\n        var extra = (c.order.columnFieldIds || []).map(function (id) {\n          var f = state.ref.orderFields[id]; return '<th class=\"ksl-hide-sm\">' + esc(f ? f.label : 'Fält') + '</th>';\n        }).join('');\n        return '<th class=\"ksl-num\">Nr</th><th>Kund</th>' + extra + '<th class=\"ksl-hide-sm\">Taggar</th><th>Säljare</th><th class=\"ksl-nowrap\">Datum</th>';\n      }\n      if (c.entity === 'offers') {\n        return '<th class=\"ksl-num\">Nr</th><th>Mottagare</th><th class=\"ksl-hide-sm\">Mall</th><th class=\"ksl-num ksl-hide-sm\">Belopp</th><th>Status</th><th>Säljare</th><th class=\"ksl-nowrap\">Datum</th>';\n      }\n      return '<th class=\"ksl-num\">Nr</th><th>Kontakt</th><th class=\"ksl-hide-sm\">Resultat</th><th class=\"ksl-num ksl-hide-sm\">Längd</th><th class=\"ksl-hide-sm\">Inspelning</th><th>Säljare</th><th class=\"ksl-nowrap\">Datum</th>';\n    }\n\n    function rowCells(item) {\n      var c = state.cfg;\n      if (c.entity === 'orders') {\n        var extra = (c.order.columnFieldIds || []).map(function (id) {\n          return '<td class=\"ksl-hide-sm\">' + esc(fieldVal(item.fields, id) || '') + '</td>';\n        }).join('');\n        return '<td class=\"ksl-num\">' + esc(item.serialId) + '</td>' +\n          '<td class=\"ksl-primarycell\">' + esc(orderName(item)) + '</td>' + extra +\n          '<td class=\"ksl-hide-sm\">' + tagsCell(item.tagIds, state.ref.orderTags) + '</td>' +\n          '<td>' + sellerCell(item.userId) + '</td>' +\n          '<td class=\"ksl-sub ksl-nowrap\">' + esc(fmtDate(item.date)) + '</td>';\n      }\n      if (c.entity === 'offers') {\n        var rec = item.recipient || {};\n        var who = rec.name || fmtPhone(rec.sms) || rec.email || ('Avtal #' + item.serialId);\n        var sub = rec.name && (rec.sms || rec.email) ? '<div class=\"ksl-sub\">' + esc(fmtPhone(rec.sms) || rec.email) + '</div>' : '';\n        var tmpl = state.ref.templates[item.templateId];\n        var sum = orderSum(item);\n        return '<td class=\"ksl-num\">' + esc(item.serialId) + '</td>' +\n          '<td class=\"ksl-primarycell\">' + esc(who) + sub + '</td>' +\n          '<td class=\"ksl-hide-sm ksl-sub\">' + esc(tmpl ? (tmpl.name || tmpl.title || '') : '') + '</td>' +\n          '<td class=\"ksl-num ksl-hide-sm\">' + esc(sum != null ? fmtMoney(sum) : '') + '</td>' +\n          '<td>' + offerStatusChip(item.status) + '</td>' +\n          '<td>' + sellerCell(item.userId) + '</td>' +\n          '<td class=\"ksl-sub ksl-nowrap\">' + esc(fmtDate(item.date)) + '</td>';\n      }\n      /* calls */\n      var phone = fmtPhone(item.recipient);\n      var play = item.isRecordingAvailable\n        ? '<button class=\"ksl-play\" data-play=\"' + esc(item.id) + '\" onclick=\"event.stopPropagation();\"><svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"></path></svg>Spela</button>'\n        : '<span class=\"ksl-muted\">—</span>';\n      return '<td class=\"ksl-num\">' + esc(item.serialId || '') + '</td>' +\n        '<td class=\"ksl-primarycell ksl-nowrap\">' + esc(phone) + '</td>' +\n        '<td class=\"ksl-hide-sm\">' + tagsCell(item.tagIds, state.ref.callTags) + '</td>' +\n        '<td class=\"ksl-num ksl-hide-sm ksl-sub\">' + esc(item.isConnected ? fmtDuration(item.connectedDuration) : '–') + '</td>' +\n        '<td class=\"ksl-hide-sm\">' + play + '</td>' +\n        '<td>' + sellerCell(item.userId) + '</td>' +\n        '<td class=\"ksl-sub ksl-nowrap\">' + esc(fmtDate(item.connectingAt || item.date)) + '</td>';\n    }\n\n    /* ---------- rendering: expanded detail ---------- */\n    function dl(term, val) {\n      if (val == null || val === '') return '';\n      return '<div class=\"ksl-dl\"><dt>' + esc(term) + '</dt><dd>' + val + '</dd></div>';\n    }\n    function detailHtml(item) {\n      var c = state.cfg, colSpan = headings().split('<th').length - 1;\n      var inner = '';\n      if (c.entity === 'orders') {\n        var grid = (item.fields || []).map(function (f) {\n          var fd = state.ref.orderFields[f.fieldId];\n          return dl(fd ? fd.label : 'Fält', esc(f.value));\n        }).join('');\n        inner += '<div class=\"ksl-detail-grid\">' + grid + dl('Datum', esc(fmtDateFull(item.date))) + '</div>';\n        if ((item.products || []).length) {\n          inner += '<div class=\"ksl-detail-section\"><h4>Produkter</h4>' + item.products.map(function (p) {\n            return '<div class=\"ksl-prodrow\"><span>' + esc(p.productName || '') + (p.quantity > 1 ? ' × ' + p.quantity : '') + '</span><span>' + esc(p.price != null ? fmtMoney(p.price * (p.quantity || 1)) : '') + '</span></div>';\n          }).join('') + '</div>';\n        }\n        if ((item.comments || []).length) {\n          inner += '<div class=\"ksl-detail-section\"><h4>Kommentarer</h4>' + item.comments.map(function (cm) {\n            return '<div class=\"ksl-prodrow\"><span>' + esc(cm.message || cm.text || '') + '</span><span class=\"ksl-sub\">' + esc(fmtDate(cm.createdAt || cm.date)) + '</span></div>';\n          }).join('') + '</div>';\n        }\n      } else if (c.entity === 'offers') {\n        var rec = item.recipient || {};\n        inner += '<div class=\"ksl-detail-grid\">' +\n          dl('Mottagare', esc(rec.name)) + dl('Telefon', esc(fmtPhone(rec.sms))) + dl('E-post', esc(rec.email)) +\n          dl('Status', offerStatusChip(item.status)) +\n          dl('Skapat', esc(fmtDateFull(item.date))) +\n          dl('Uppdaterat', esc(fmtDateFull(item.statusUpdatedAt))) +\n          (item.signature && item.signature.signedAt ? dl('Signerat', esc(fmtDateFull(item.signature.signedAt)) + (item.signature.signeeName ? ' av ' + esc(item.signature.signeeName) : '')) : '') +\n          '</div>';\n        if ((item.products || []).length) {\n          inner += '<div class=\"ksl-detail-section\"><h4>Produkter</h4>' + item.products.map(function (p) {\n            return '<div class=\"ksl-prodrow\"><span>' + esc(p.productName || p.name || '') + (p.quantity > 1 ? ' × ' + p.quantity : '') + '</span><span>' + esc(p.price != null ? fmtMoney(p.price * (p.quantity || 1)) : '') + '</span></div>';\n          }).join('') + '</div>';\n        }\n        var doc = (item.signature && item.signature.signedDocument) || item.unsignedDocument;\n        if (doc && doc.url) {\n          inner += '<div class=\"ksl-detail-section\"><a class=\"ksl-linkbtn\" target=\"_blank\" rel=\"noopener\" href=\"' + esc(fileProxy(doc.url)) + '\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline></svg>Öppna dokument</a></div>';\n        }\n      } else {\n        inner += '<div class=\"ksl-detail-grid\">' +\n          dl('Nummer', esc(fmtPhone(item.recipient))) +\n          dl('Riktning', item.isInbound ? 'Inkommande' : 'Utgående') +\n          dl('Ansluten', item.isConnected ? 'Ja' : 'Nej') +\n          dl('Samtalslängd', esc(fmtDuration(item.connectedDuration))) +\n          dl('Totaltid', esc(fmtDuration(item.totalDuration))) +\n          dl('Tidpunkt', esc(fmtDateFull(item.connectingAt || item.date))) +\n          dl('Resultat', tagsCell(item.tagIds, state.ref.callTags) || '—') +\n          '</div>';\n        if (item.isRecordingAvailable) {\n          inner += '<div class=\"ksl-detail-section\" id=\"ksl-rec-' + esc(item.id) + '\"><h4>Inspelning</h4><button class=\"ksl-linkbtn\" data-play=\"' + esc(item.id) + '\"><svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"></path></svg>Ladda inspelning</button></div>';\n        }\n      }\n      return '<tr class=\"ksl-detail\" data-detail=\"' + esc(item.id) + '\"><td colspan=\"' + colSpan + '\"><div class=\"ksl-detail-inner\">' + inner + '</div></td></tr>';\n    }\n\n    function fileProxy(uri) {\n      try { return API + '/users/files-v1' + new URL(uri).pathname; } catch (e) { return uri; }\n    }\n\n    /* ---------- filter summary ---------- */\n    function filterChips() {\n      var c = state.cfg, chips = [];\n      var DR = { today: 'Idag', yesterday: 'Igår', thisWeek: 'Denna vecka', thisMonth: 'Denna månad', thisYear: 'I år', last7: 'Senaste 7 dagar', last30: 'Senaste 30 dagar' };\n      if (c.dateRange && c.dateRange !== 'all') chips.push({ icon: 'cal', text: DR[c.dateRange] || c.dateRange });\n      if (c.scope === 'me') chips.push({ icon: 'user', text: 'Endast mina' });\n      else if (c.scope === 'team') chips.push({ icon: 'user', text: 'Mitt team' });\n      if (c.projectIds && c.projectIds.length) chips.push({ icon: 'tag', text: c.projectIds.length + ' projekt' });\n      if (c.entity === 'orders') {\n        var oc = (c.order.anyTagId || []).length + (c.order.allTagIds || []).length;\n        if (oc) chips.push({ icon: 'tag', text: oc + ' tagg' + (oc > 1 ? 'ar' : '') });\n      } else if (c.entity === 'offers') {\n        if ((c.offer.statuses || []).length) chips.push({ icon: 'tag', text: c.offer.statuses.map(function (s) { return (OFFER_STATUS[s] || {}).label || s; }).join(', ') });\n      } else if (c.entity === 'calls') {\n        if (c.call.connectionState && c.call.connectionState !== 'any') chips.push({ icon: 'phone', text: c.call.connectionState === 'connected' ? 'Besvarade' : 'Ej besvarade' });\n        if ((c.call.callTagIds || []).length) chips.push({ icon: 'tag', text: c.call.callTagIds.length + ' resultat' });\n        if (c.call.isRecordingAvailable) chips.push({ icon: 'phone', text: 'Med inspelning' });\n      }\n      var ICONS = {\n        cal: '<path d=\"M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z\"/>',\n        user: '<path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>',\n        tag: '<path d=\"M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z\"/><line x1=\"7\" y1=\"7\" x2=\"7.01\" y2=\"7\"/>',\n        phone: '<path d=\"M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.98.36 1.94.7 2.86a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.22-1.27a2 2 0 0 1 2.11-.45c.92.34 1.88.57 2.86.7A2 2 0 0 1 22 16.92z\"/>'\n      };\n      return chips.map(function (ch) {\n        return '<span class=\"ksl-fchip\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' + ICONS[ch.icon] + '</svg>' + esc(ch.text) + '</span>';\n      }).join('');\n    }\n\n    /* ---------- main render ---------- */\n    function render() {\n      var mount = el('ksl-mount'); if (!mount) return;\n      var c = state.cfg;\n\n      if (state.loading && !state.items.length) {\n        mount.innerHTML = '<div class=\"ksl-wrap\"><div class=\"ksl-loading\"><svg class=\"ksl-spin\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path></svg> Laddar...</div></div>';\n        applyContrast(); return;\n      }\n      if (!c || !c.configured) {\n        mount.innerHTML = '<div class=\"ksl-wrap\"><div class=\"ksl-notconfig\"><h3>Listvyn är inte konfigurerad ännu</h3>' +\n          '<p>Öppna <strong>Inställningar &rsaquo; Vyer</strong>, välj denna vy och klicka på <strong>Inställningsvy</strong>.</p>' +\n          '<div class=\"ksl-steps\">Där väljer du:<br>1. Vilken data vyn ska visa (ordrar, avtal eller samtal)<br>2. Vilka filter som ska gälla<br>3. Vilka kolumner som ska visas</div></div></div>';\n        applyContrast(); return;\n      }\n\n      var meta = ENTITY_META[c.entity];\n      var title = c.title || meta.title;\n      var countBadge = state.total != null ? '<span class=\"ksl-count\">' + state.total + '</span>' : '';\n\n      var body;\n      if (state.error) {\n        body = '<div class=\"ksl-error\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/></svg>' + esc(state.error) + '</div>';\n      } else if (!state.items.length) {\n        body = '<div class=\"ksl-tablecard\"><div class=\"ksl-empty\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"><path d=\"M9 11l3 3L22 4\"/><path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/></svg><div class=\"ksl-empty-title\">Inget att visa</div><div>' + esc(meta.empty) + '</div></div></div>';\n      } else {\n        var rows = state.items.map(function (item) {\n          var isOpen = state.open[item.id];\n          var tr = '<tr class=\"ksl-row' + (isOpen ? ' ksl-open' : '') + '\" data-row=\"' + esc(item.id) + '\">' + rowCells(item) + '</tr>';\n          return tr + (isOpen ? detailHtml(item) : '');\n        }).join('');\n        body = '<div class=\"ksl-tablecard\"><table class=\"ksl-table\"><thead><tr>' + headings() + '</tr></thead><tbody>' + rows + '</tbody></table></div>';\n        if (state.hasMore) {\n          body += '<div class=\"ksl-more\"><button id=\"ksl-more\"' + (state.loadingMore ? ' disabled' : '') + '>' + (state.loadingMore ? 'Laddar...' : 'Visa fler') + '</button></div>';\n        }\n      }\n\n      var fchips = filterChips();\n      mount.innerHTML = '<div class=\"ksl-wrap\">' +\n        '<div class=\"ksl-head\">' +\n          '<h1 class=\"ksl-title\">' + esc(title) + countBadge + '</h1>' +\n          '<div class=\"ksl-head-spacer\"></div>' +\n          (c.showSearch ? '<div class=\"ksl-search\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg><input id=\"ksl-search\" placeholder=\"Sök...\" value=\"' + esc(state.search) + '\"></div>' : '') +\n          '<button class=\"ksl-iconbtn' + (state.loading ? ' ksl-spinning' : '') + '\" id=\"ksl-refresh\" title=\"Uppdatera\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M23 4v6h-6M1 20v-6h6\"/><path d=\"M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15\"/></svg></button>' +\n        '</div>' +\n        (fchips ? '<div class=\"ksl-filterbar\">' + fchips + '</div>' : '') +\n        body +\n      '</div>';\n      applyContrast();\n    }\n\n    /* ---------- recording playback ---------- */\n    function playRecording(callId, btn) {\n      if (state.recordingUrls[callId]) { insertAudio(callId, state.recordingUrls[callId], btn); return; }\n      if (btn) { btn.disabled = true; btn.innerHTML = 'Laddar...'; }\n      fetch(API + '/dial/calls-v1/' + callId + '/recording.wav', { headers: { 'Authorization': 'Bearer ' + getToken() } })\n        .then(function (res) { if (!res.ok) throw new Error('rec ' + res.status); return res.blob(); })\n        .then(function (blob) {\n          var url = URL.createObjectURL(blob.type === 'audio/x-wav' || blob.type === 'audio/wav' ? blob : new Blob([blob], { type: 'audio/wav' }));\n          state.recordingUrls[callId] = url; insertAudio(callId, url, btn);\n        })\n        .catch(function () { if (btn) { btn.disabled = false; btn.innerHTML = 'Kunde inte ladda'; } });\n    }\n    function insertAudio(callId, url, btn) {\n      var host = el('ksl-rec-' + callId);\n      if (host) {\n        if (host.querySelector('audio')) return;\n        var a = document.createElement('audio'); a.controls = true; a.autoplay = true; a.className = 'ksl-audio'; a.src = url;\n        host.appendChild(a);\n        if (btn && btn.parentNode === host) btn.style.display = 'none';\n      } else if (btn) {\n        var a2 = document.createElement('audio'); a2.controls = true; a2.autoplay = true; a2.src = url;\n        btn.parentNode.replaceChild(a2, btn);\n      }\n    }\n\n    /* ---------- contrast probe (mandatory) ---------- */\n    function applyContrast() {\n      var root = el('ksl-root'); if (!root) return;\n      try {\n        var probe = document.createElement('span');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var col = getComputedStyle(probe).color; root.removeChild(probe);\n        var m = /(\\d+),\\s*(\\d+),\\s*(\\d+)/.exec(col);\n        if (m) {\n          var lum = (0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3]) / 255;\n          root.style.setProperty('--ksl-primary-fg', lum > 0.62 ? '#1a1a1a' : '#ffffff');\n        }\n      } catch (e) { /* ignore */ }\n    }\n\n    /* ---------- events ---------- */\n    function onClick(e) {\n      var root = el('ksl-root'); if (!root || !root.contains(e.target)) return;\n      var playBtn = e.target.closest && e.target.closest('[data-play]');\n      if (playBtn) { e.stopPropagation(); playRecording(playBtn.getAttribute('data-play'), playBtn); return; }\n      if (e.target.closest && e.target.closest('#ksl-refresh')) { reload(); return; }\n      if (e.target.closest && e.target.closest('#ksl-more')) { loadMore(); return; }\n      var row = e.target.closest && e.target.closest('[data-row]');\n      if (row) {\n        var id = row.getAttribute('data-row');\n        state.open[id] = !state.open[id];\n        render();\n      }\n    }\n    var searchTimer = null;\n    function onInput(e) {\n      if (e.target && e.target.id === 'ksl-search') {\n        state.search = e.target.value;\n        if (searchTimer) clearTimeout(searchTimer);\n        searchTimer = setTimeout(function () { reload(); }, 450);\n      }\n    }\n\n    document.addEventListener('click', onClick, true);\n    document.addEventListener('input', onInput, true);\n\n    var headObserver = null;\n    try { headObserver = new MutationObserver(function () { applyContrast(); }); headObserver.observe(document.head, { childList: true, subtree: true }); } catch (e) { /* ignore */ }\n\n    window.__kslCleanup = function () {\n      document.removeEventListener('click', onClick, true);\n      document.removeEventListener('input', onInput, true);\n      if (headObserver) { try { headObserver.disconnect(); } catch (e) {} }\n      Object.keys(state.recordingUrls).forEach(function (k) { try { URL.revokeObjectURL(state.recordingUrls[k]); } catch (e) {} });\n    };\n\n    boot();\n  })();\n  </script>\n</div>","iconName":"list","iconColor":"#455A64","settingsHtml":"<div id=\"ksls-root\">\n  <style>\n    #ksls-root {\n      --s-bg: var(--color-background-2, #fafafa);\n      --s-card: var(--color-background-1, #ffffff);\n      --s-fg: var(--color-text-1, #212121);\n      --s-muted: var(--color-text-3, #757575);\n      --s-primary: var(--color-primary, #1565C0);\n      --s-primary-fg: #ffffff;\n      --s-border: color-mix(in srgb, var(--s-fg) 14%, transparent);\n      --s-border-soft: color-mix(in srgb, var(--s-fg) 8%, transparent);\n      --s-hover: var(--color-element-hover, rgba(20, 20, 20, 0.04));\n      font-family: var(--font-family-default, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n      color: var(--s-fg);\n      display: block;\n    }\n    #ksls-root * { box-sizing: border-box; }\n    #ksls-root button { font-family: inherit; cursor: pointer; }\n    #ksls-root input { font-family: inherit; font-size: 14px; color: var(--s-fg); }\n\n    .ksls-wrap { display: flex; flex-direction: column; gap: 26px; }\n\n    .ksls-step { display: flex; flex-direction: column; gap: 12px; }\n    .ksls-step-head { display: flex; align-items: baseline; gap: 10px; }\n    .ksls-step-num {\n      width: 22px; height: 22px; flex-shrink: 0; border-radius: 50%; background: var(--s-primary); color: var(--s-primary-fg);\n      font-size: 12px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center;\n    }\n    .ksls-step-title { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }\n    .ksls-step-desc { font-size: 12.5px; color: var(--s-muted); margin-left: 32px; margin-top: -6px; }\n    .ksls-body { margin-left: 32px; display: flex; flex-direction: column; gap: 12px; }\n\n    /* entity cards */\n    .ksls-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n    .ksls-card {\n      border: 1.5px solid var(--s-border-soft); border-radius: 10px; background: var(--s-card);\n      padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center;\n      transition: border-color 0.1s, background 0.1s;\n    }\n    .ksls-card:hover { background: var(--s-hover); }\n    .ksls-card.on { border-color: var(--s-primary); background: color-mix(in srgb, var(--s-primary) 7%, transparent); }\n    .ksls-card svg { width: 26px; height: 26px; color: var(--s-primary); }\n    .ksls-card-title { font-size: 14px; font-weight: 800; }\n    .ksls-card-sub { font-size: 11.5px; color: var(--s-muted); line-height: 1.35; }\n\n    /* segmented */\n    .ksls-seg { display: inline-flex; background: var(--s-bg); border: 1px solid var(--s-border-soft); border-radius: 9px; padding: 3px; gap: 3px; flex-wrap: wrap; }\n    .ksls-seg button {\n      border: none; background: transparent; color: var(--s-muted); font-size: 13px; font-weight: 700;\n      padding: 6px 14px; border-radius: 6px;\n    }\n    .ksls-seg button.on { background: var(--s-card); color: var(--s-fg); box-shadow: 0 1px 2px rgba(0,0,0,0.08); }\n\n    /* chips */\n    .ksls-chips { display: flex; flex-wrap: wrap; gap: 7px; }\n    .ksls-chip {\n      border: 1.5px solid var(--s-border-soft); background: var(--s-card); color: var(--s-fg);\n      font-size: 12.5px; font-weight: 700; border-radius: 999px; padding: 6px 13px; display: inline-flex; align-items: center; gap: 6px;\n    }\n    .ksls-chip:hover { background: var(--s-hover); }\n    .ksls-chip.on { border-color: var(--s-primary); background: color-mix(in srgb, var(--s-primary) 12%, transparent); color: color-mix(in srgb, var(--s-primary) 72%, var(--s-fg)); }\n    .ksls-chip .ksls-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }\n    .ksls-chip .ksls-check { width: 13px; height: 13px; display: none; }\n    .ksls-chip.on .ksls-check { display: inline-block; }\n    .ksls-empty-note { font-size: 12.5px; color: var(--s-muted); font-style: italic; }\n\n    /* text input */\n    .ksls-input {\n      width: 100%; max-width: 420px; padding: 9px 12px; border: 1px solid var(--s-border);\n      border-radius: 8px; background: var(--s-card); outline: none;\n    }\n    .ksls-input:focus { border-color: var(--s-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--s-primary) 16%, transparent); }\n\n    /* toggle row */\n    .ksls-toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }\n    .ksls-switch { width: 40px; height: 23px; border-radius: 999px; background: var(--s-border); position: relative; transition: background 0.12s; flex-shrink: 0; }\n    .ksls-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px; border-radius: 50%; background: #fff; transition: transform 0.12s; box-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n    .ksls-toggle.on .ksls-switch { background: var(--s-primary); }\n    .ksls-toggle.on .ksls-switch::after { transform: translateX(17px); }\n    .ksls-toggle-label { font-size: 13.5px; font-weight: 600; }\n\n    .ksls-hint {\n      display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; color: var(--s-muted);\n      background: color-mix(in srgb, var(--s-primary) 6%, transparent); border-radius: 9px; padding: 11px 13px; line-height: 1.45;\n    }\n    .ksls-hint svg { width: 15px; height: 15px; flex-shrink: 0; margin-top: 1px; color: var(--s-primary); }\n    .ksls-sub-label { font-size: 12px; font-weight: 700; color: var(--s-muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 2px; }\n    .ksls-divider { height: 1px; background: var(--s-border-soft); margin: 2px 0; }\n  </style>\n\n  <div id=\"ksls-mount\"></div>\n\n  <script>\n  (function () {\n    'use strict';\n    var API = 'https://app.salesys.se/api';\n\n    if (window.__kslSetCleanup) { try { window.__kslSetCleanup(); } catch (e) {} window.__kslSetCleanup = null; }\n\n    function esc(s) { return String(s == null ? '' : s).replace(/[&<>\"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]; }); }\n    function asArray(r, key) { return Array.isArray(r) ? r : ((r && r[key]) || []); }\n    function getToken() {\n      var ck = document.cookie.split(';').reduce(function (o, c) { var p = /(.+?)=(.+)/.exec(c.trim()); if (p) o[p[1]] = p[2]; return o; }, {});\n      var raw = ck['s2_utoken']; if (!raw) return null;\n      try { return decodeURIComponent(raw); } catch (e) { return raw; }\n    }\n    function api(path) {\n      return fetch(API + path, { headers: { 'Authorization': 'Bearer ' + getToken() } })\n        .then(function (r) { if (!r.ok) throw new Error('API ' + r.status); return r.json(); });\n    }\n\n    var TAG_COLORS = { red: '#E53935', pink: '#D81B60', purple: '#8E24AA', 'deep-purple': '#5E35B1', indigo: '#3949AB', blue: '#1E88E5', 'light-blue': '#039BE5', cyan: '#00ACC1', teal: '#00897B', green: '#43A047', 'light-green': '#7CB342', lime: '#C0CA33', yellow: '#FDD835', amber: '#FFB300', orange: '#FB8C00', 'deep-orange': '#F4511E', brown: '#6D4C41', grey: '#757575', 'blue-grey': '#546E7A' };\n    function tagColor(c) { return TAG_COLORS[c] || (typeof c === 'string' && c[0] === '#' ? c : '#90A4AE'); }\n\n    /* ---------- config ---------- */\n    function defaults() {\n      return {\n        configured: true, entity: 'orders', title: '', scope: 'all', dateRange: 'all',\n        projectIds: [], isTest: false, pageSize: 50, showSearch: true,\n        order: { anyTagId: [], notAnyTagId: [], columnFieldIds: [], sortBy: 'serialId', sortOrder: 'desc' },\n        offer: { statuses: [], templateIds: [], dateType: 'created', sortBy: 'date', sortOrder: 'desc' },\n        call: { connectionState: 'any', direction: 'any', callTagIds: [], isRecordingAvailable: false, voicemail: null, sortBy: 'connectingAt', sortOrder: 'desc' }\n      };\n    }\n    function readInitial() {\n      var d = defaults();\n      /* In the settings pane, window.form.settings holds the persisted presetState\n         (window.form.custom/setState is transient and does NOT persist). */\n      var cur = (window.form && (window.form.settings || window.form.custom)) || {};\n      try {\n        d.entity = (cur.entity === 'offers' || cur.entity === 'calls') ? cur.entity : (cur.entity || 'orders');\n        if (typeof cur.title === 'string') d.title = cur.title;\n        if (cur.scope) d.scope = cur.scope;\n        if (cur.dateRange) d.dateRange = cur.dateRange;\n        if (Array.isArray(cur.projectIds)) d.projectIds = cur.projectIds.slice();\n        if (cur.isTest === 'any' || cur.isTest === true) d.isTest = 'any';\n        if (cur.pageSize) d.pageSize = cur.pageSize;\n        if (typeof cur.showSearch === 'boolean') d.showSearch = cur.showSearch;\n        ['order', 'offer', 'call'].forEach(function (k) { if (cur[k] && typeof cur[k] === 'object') { Object.keys(cur[k]).forEach(function (kk) { d[k][kk] = cur[k][kk]; }); } });\n      } catch (e) { /* ignore */ }\n      return d;\n    }\n\n    var cfg = readInitial();\n    var ref = { orderTags: [], orderFields: [], callTags: [], templates: [], projects: [] };\n\n    function push() {\n      cfg.configured = true;\n      if (!window.form) return;\n      var snapshot = JSON.parse(JSON.stringify(cfg));\n      /* Persisted config lives in window.form.settings, written via setSettings.\n         The admin's \"Spara\" button in the modal commits it to the database. */\n      try {\n        if (typeof window.form.setSettings === 'function') { window.form.setSettings(function () { return snapshot; }); return; }\n        if (typeof window.form.setState === 'function') { window.form.setState(function () { return snapshot; }); }\n      } catch (e) { console.error('lista settings save failed', e); }\n    }\n\n    /* ---------- component builders (return HTML) ---------- */\n    function checkSvg() { return '<svg class=\"ksls-check\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>'; }\n\n    function segmented(name, value, options) {\n      return '<div class=\"ksls-seg\" data-seg=\"' + name + '\">' + options.map(function (o) {\n        return '<button data-val=\"' + esc(o.value) + '\" class=\"' + (String(value) === String(o.value) ? 'on' : '') + '\">' + esc(o.label) + '</button>';\n      }).join('') + '</div>';\n    }\n    function toggle(name, on, label) {\n      return '<label class=\"ksls-toggle' + (on ? ' on' : '') + '\" data-toggle=\"' + name + '\"><span class=\"ksls-switch\"></span><span class=\"ksls-toggle-label\">' + esc(label) + '</span></label>';\n    }\n    function chipGroup(name, selected, items) {\n      if (!items.length) return '<div class=\"ksls-empty-note\">Inga alternativ tillgängliga.</div>';\n      selected = selected || [];\n      return '<div class=\"ksls-chips\" data-chips=\"' + name + '\">' + items.map(function (it) {\n        var on = selected.indexOf(it.value) !== -1;\n        var dot = it.color ? '<span class=\"ksls-dot\" style=\"background:' + it.color + '\"></span>' : '';\n        return '<button class=\"ksls-chip' + (on ? ' on' : '') + '\" data-val=\"' + esc(it.value) + '\">' + dot + checkSvg() + esc(it.label) + '</button>';\n      }).join('') + '</div>';\n    }\n\n    function step(num, title, desc, bodyHtml) {\n      return '<div class=\"ksls-step\"><div class=\"ksls-step-head\"><span class=\"ksls-step-num\">' + num + '</span><span class=\"ksls-step-title\">' + esc(title) + '</span></div>' +\n        (desc ? '<div class=\"ksls-step-desc\">' + esc(desc) + '</div>' : '') +\n        '<div class=\"ksls-body\">' + bodyHtml + '</div></div>';\n    }\n\n    var ENTITY_CARDS = [\n      { value: 'orders', title: 'Ordrar', sub: 'Lagda ordrar', icon: '<path d=\"M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z\"/><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"/><path d=\"M16 10a4 4 0 0 1-8 0\"/>' },\n      { value: 'offers', title: 'Avtal', sub: 'Skickade avtal', icon: '<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><polyline points=\"14 2 14 8 20 8\"/><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"/><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"/>' },\n      { value: 'calls', title: 'Samtal', sub: 'Ring- & samtalslogg', icon: '<path d=\"M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.98.36 1.94.7 2.86a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.22-1.27a2 2 0 0 1 2.11-.45c.92.34 1.88.57 2.86.7A2 2 0 0 1 22 16.92z\"/>' }\n    ];\n    var DATE_OPTS = [\n      { value: 'all', label: 'Alla' }, { value: 'today', label: 'Idag' }, { value: 'yesterday', label: 'Igår' },\n      { value: 'thisWeek', label: 'Denna vecka' }, { value: 'thisMonth', label: 'Denna månad' },\n      { value: 'thisYear', label: 'I år' }, { value: 'last7', label: 'Senaste 7 dgr' }, { value: 'last30', label: 'Senaste 30 dgr' }\n    ];\n    var OFFER_STATUSES = [\n      { value: 'pending', label: 'Väntar', color: '#FB8C00' }, { value: 'distributed', label: 'Skickat', color: '#1E88E5' },\n      { value: 'read', label: 'Läst', color: '#00ACC1' }, { value: 'signed', label: 'Signerat', color: '#2E7D32' },\n      { value: 'canceled', label: 'Avbrutet', color: '#757575' }, { value: 'expired', label: 'Utgånget', color: '#E53935' }\n    ];\n\n    /* ---------- render ---------- */\n    function render() {\n      var mount = document.getElementById('ksls-mount'); if (!mount) return;\n      var meta = { orders: 'Ordrar', offers: 'Avtal', calls: 'Samtal' }[cfg.entity];\n      var parts = [];\n\n      /* 1. entity */\n      parts.push(step(1, 'Vilken data ska visas?', 'Välj vad listan ska hämta.',\n        '<div class=\"ksls-cards\" data-cards=\"entity\">' + ENTITY_CARDS.map(function (c) {\n          return '<button class=\"ksls-card' + (cfg.entity === c.value ? ' on' : '') + '\" data-val=\"' + c.value + '\">' +\n            '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' + c.icon + '</svg>' +\n            '<span class=\"ksls-card-title\">' + c.title + '</span><span class=\"ksls-card-sub\">' + c.sub + '</span></button>';\n        }).join('') + '</div>'));\n\n      /* 2. title */\n      parts.push(step(2, 'Rubrik', 'Lämna tomt för att använda \"' + meta + '\".',\n        '<input class=\"ksls-input\" data-input=\"title\" placeholder=\"' + esc(meta) + '\" value=\"' + esc(cfg.title) + '\">'));\n\n      /* 3. scope */\n      parts.push(step(3, 'Vems poster?', null,\n        segmented('scope', cfg.scope, [{ value: 'all', label: 'Alla' }, { value: 'me', label: 'Endast mina' }, { value: 'team', label: 'Mitt team' }])));\n\n      /* 4. date */\n      parts.push(step(4, 'Tidsperiod', null, segmented('dateRange', cfg.dateRange, DATE_OPTS)));\n\n      /* 5. projects (optional) */\n      if (ref.projects.length) {\n        parts.push(step(5, 'Projekt', 'Tomt = alla projekt.',\n          chipGroup('projectIds', cfg.projectIds, ref.projects.map(function (p) { return { value: p.id, label: p.name }; }))));\n      }\n\n      var n = ref.projects.length ? 6 : 5;\n\n      /* 6. entity-specific filters */\n      if (cfg.entity === 'orders') {\n        var oBody =\n          '<div class=\"ksls-sub-label\">Har någon av dessa taggar</div>' +\n          chipGroup('order.anyTagId', cfg.order.anyTagId, ref.orderTags.map(function (t) { return { value: t.id, label: t.name, color: tagColor(t.color) }; })) +\n          '<div class=\"ksls-divider\"></div>' +\n          '<div class=\"ksls-sub-label\">Får inte ha dessa taggar</div>' +\n          chipGroup('order.notAnyTagId', cfg.order.notAnyTagId, ref.orderTags.map(function (t) { return { value: t.id, label: t.name, color: tagColor(t.color) }; }));\n        parts.push(step(n, 'Filtrera ordrar', 'Välj vilka taggar som avgör vad som visas.', oBody));\n        parts.push(step(n + 1, 'Kolumner', 'Vilka orderfält ska visas som kolumner i listan.',\n          chipGroup('order.columnFieldIds', cfg.order.columnFieldIds, ref.orderFields.map(function (f) { return { value: f.id, label: f.label }; }))));\n      } else if (cfg.entity === 'offers') {\n        parts.push(step(n, 'Filtrera avtal', 'Välj status och mallar. Tomt = alla.',\n          '<div class=\"ksls-sub-label\">Status</div>' + chipGroup('offer.statuses', cfg.offer.statuses, OFFER_STATUSES) +\n          (ref.templates.length ? '<div class=\"ksls-divider\"></div><div class=\"ksls-sub-label\">Mallar</div>' + chipGroup('offer.templateIds', cfg.offer.templateIds, ref.templates.map(function (t) { return { value: t.id, label: t.name || t.title || 'Mall' }; })) : '')));\n      } else {\n        var cBody =\n          '<div class=\"ksls-sub-label\">Anslutning</div>' +\n          segmented('call.connectionState', cfg.call.connectionState, [{ value: 'any', label: 'Alla' }, { value: 'connected', label: 'Besvarade' }, { value: 'disconnected', label: 'Ej besvarade' }]) +\n          '<div class=\"ksls-sub-label\" style=\"margin-top:8px\">Riktning</div>' +\n          segmented('call.direction', cfg.call.direction, [{ value: 'any', label: 'Alla' }, { value: 'outbound', label: 'Utgående' }, { value: 'inbound', label: 'Inkommande' }]) +\n          (ref.callTags.length ? '<div class=\"ksls-divider\"></div><div class=\"ksls-sub-label\">Resultat (taggar)</div>' + chipGroup('call.callTagIds', cfg.call.callTagIds, ref.callTags.map(function (t) { return { value: t.id, label: t.name, color: tagColor(t.color) }; })) : '') +\n          '<div class=\"ksls-divider\"></div>' + toggle('call.isRecordingAvailable', !!cfg.call.isRecordingAvailable, 'Endast samtal med inspelning');\n        parts.push(step(n, 'Filtrera samtal', null, cBody));\n      }\n\n      var last = n + (cfg.entity === 'orders' ? 2 : 1);\n\n      /* display options */\n      parts.push(step(last, 'Visning', null,\n        '<div class=\"ksls-sub-label\">Antal per sida</div>' +\n        segmented('pageSize', cfg.pageSize, [{ value: 25, label: '25' }, { value: 50, label: '50' }, { value: 100, label: '100' }]) +\n        '<div class=\"ksls-divider\"></div>' +\n        toggle('showSearch', cfg.showSearch, 'Visa sökruta') +\n        '<div class=\"ksls-divider\"></div>' +\n        (cfg.entity !== 'calls' ? toggle('isTest', cfg.isTest === 'any', 'Inkludera testposter') : '')));\n\n      parts.push('<div class=\"ksls-hint\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"/></svg><span>Klicka på <strong>Spara</strong> längst ner för att spara inställningarna. Vyn läser dem automatiskt nästa gång den öppnas.</span></div>');\n\n      mount.innerHTML = '<div class=\"ksls-wrap\">' + parts.join('') + '</div>';\n      applyContrast();\n    }\n\n    /* ---------- helpers to update nested cfg ---------- */\n    function getPath(name) { var p = name.split('.'); return p.length === 1 ? cfg : cfg[p[0]]; }\n    function keyOf(name) { var p = name.split('.'); return p[p.length - 1]; }\n    function setScalar(name, val) { getPath(name)[keyOf(name)] = val; push(); }\n    function toggleInArray(name, val) {\n      var obj = getPath(name), k = keyOf(name), arr = obj[k] || (obj[k] = []);\n      var i = arr.indexOf(val); if (i === -1) arr.push(val); else arr.splice(i, 1); push();\n    }\n\n    /* ---------- events ---------- */\n    function onClick(e) {\n      var root = document.getElementById('ksls-root'); if (!root || !root.contains(e.target)) return;\n\n      var card = e.target.closest && e.target.closest('[data-cards] [data-val]');\n      if (card) { cfg.entity = card.getAttribute('data-val'); push(); render(); return; }\n\n      var seg = e.target.closest && e.target.closest('.ksls-seg [data-val]');\n      if (seg) {\n        var segName = seg.parentNode.getAttribute('data-seg');\n        var v = seg.getAttribute('data-val');\n        if (segName === 'pageSize') v = parseInt(v, 10);\n        setScalar(segName, v); render(); return;\n      }\n\n      var chip = e.target.closest && e.target.closest('.ksls-chips .ksls-chip');\n      if (chip) { toggleInArray(chip.parentNode.getAttribute('data-chips'), chip.getAttribute('data-val')); chip.classList.toggle('on'); return; }\n\n      var tog = e.target.closest && e.target.closest('[data-toggle]');\n      if (tog) {\n        e.preventDefault();\n        var tName = tog.getAttribute('data-toggle');\n        if (tName === 'isTest') { cfg.isTest = cfg.isTest === 'any' ? false : 'any'; }\n        else { setScalar(tName, !getPath(tName)[keyOf(tName)]); }\n        tog.classList.toggle('on'); push(); return;\n      }\n    }\n    function onInput(e) {\n      if (e.target && e.target.getAttribute && e.target.getAttribute('data-input') === 'title') { cfg.title = e.target.value; push(); }\n    }\n    document.addEventListener('click', onClick, true);\n    document.addEventListener('input', onInput, true);\n\n    /* ---------- contrast probe ---------- */\n    function applyContrast() {\n      var root = document.getElementById('ksls-root'); if (!root) return;\n      try {\n        var probe = document.createElement('span');\n        probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--color-primary,#1565C0)';\n        root.appendChild(probe);\n        var col = getComputedStyle(probe).color; root.removeChild(probe);\n        var m = /(\\d+),\\s*(\\d+),\\s*(\\d+)/.exec(col);\n        if (m) { var lum = (0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3]) / 255; root.style.setProperty('--s-primary-fg', lum > 0.62 ? '#1a1a1a' : '#ffffff'); }\n      } catch (e) {}\n    }\n    var headObs = null;\n    try { headObs = new MutationObserver(applyContrast); headObs.observe(document.head, { childList: true, subtree: true }); } catch (e) {}\n\n    window.__kslSetCleanup = function () {\n      document.removeEventListener('click', onClick, true);\n      document.removeEventListener('input', onInput, true);\n      if (headObs) { try { headObs.disconnect(); } catch (e) {} }\n    };\n\n    /* ---------- load reference then render ---------- */\n    render();\n    Promise.all([\n      api('/users/projects-v1').then(function (r) { ref.projects = asArray(r, 'projects'); }).catch(function () {}),\n      api('/orders/tags-v1').then(function (r) { ref.orderTags = asArray(r, 'tags'); }).catch(function () {}),\n      api('/orders/fields-v1').then(function (r) { ref.orderFields = asArray(r, 'fields'); }).catch(function () {}),\n      api('/dial/tags-v1').then(function (r) { ref.callTags = asArray(r, 'tags'); }).catch(function () {}),\n      api('/offers/offer-templates-v2').then(function (r) { ref.templates = asArray(r, 'templates'); }).catch(function () {})\n    ]).then(function () { push(); render(); });\n  })();\n  </script>\n</div>","description":"","rights":[],"components":[]},{"id":"6a5f33de69c918341e9a3127","name":"Stängtider","isRemoved":false,"isViewPreviewEnabled":true,"entityGroupIds":["69a306434e630d4dc2855b7e"],"locations":[{"type":"settings","position":null}],"html":"<style>\n  .hours-item {\n    box-sizing: border-box;\n    padding: 24px;\n    background: var(--color-background-1);\n    border: 1px solid var(--color-background-3);\n    border-radius: 16px;\n    display: flex;\n    flex-direction: column;\n    gap: 24px;\n    width: 440px;\n  }\n\n  .hours-item {\n    width: 360px;\n    transition: opacity 150ms ease-out;\n  }\n\n  .hours-item.partially-invisible {\n    opacity: 0.35;\n    pointer-events: none;\n  }\n\n  .hours-item.invisible {\n    opacity: 0;\n    pointer-events: none;\n  }\n\n  .hours-editor {\n    display: flex;\n    flex-direction: column;\n    gap: 24px;\n  }\n</style>\n\n<div style=\"position: relative;\">\n  <SettingsList>\n    {{#each settings.rules}}\n    <option value=\"{}\" icon=\"clock\" iconType=\"regular\" label=\"{{label}}\" tooltip=\"Redigera\" {{#ifEq\n      ../custom.selectedRule.id id}} selected {{/ifEq}}\n      onClick=\"window.form.setState({ selectedRule: window.form.settings.rules.find(rule => rule.id === '{{id}}') }); window.openEditor();\">\n      <!-- Testkommentar -->\n      {{#each summary}}\n      {{this}}<br />\n      {{/each}}\n    </option>\n    {{/each}}\n  </SettingsList>\n\n  <div class=\"hours-editor\" modal>\n    {{#with custom.selectedRule}}\n    <div style=\"display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px;\">\n      <Field label=\"Från kl.\" value=\"{{afterTime}}\" type=\"time\" containerStyle=\"width: 100%\" style=\"font-size: 18px\"\n        onchange=\"window.onTimeChange('{{id}}', value, true)\">\n      </Field>\n\n      <div>\n        <Field label=\"Till kl.\" value=\"{{beforeTime}}\" type=\"time\" containerStyle=\"width: 100%\" style=\"font-size: 18px\"\n          onchange=\"window.onTimeChange('{{id}}', value, false)\">\n        </Field>\n\n        {{#if beforeHour}}\n        {{#ifLt beforeHour afterHour}}\n        <div style=\"font-size: 13px; color: var(--color-text-3); margin-top: 8px; padding-left: 8px;\">Nästa dag</div>\n        {{/ifLt}}\n        {{/if}}\n      </div>\n    </div>\n\n    {{#ifEq weekdays.length 0}}\n    <div style=\"text-align: center; font-size: 14px; color: var(--color-text-3); margin-top: 8px;\">\n      <i class=\"fas fa-exclamation-triangle\"></i>&nbsp;\n      Du måste välja en eller flera veckodagar.\n    </div>\n    {{/ifEq}}\n\n    <div style=\"display: flex; flex-direction: row; justify-content: space-between;\">\n      {{#each ../custom.weekdays}}\n      <Tooltip delay=\"false\" tooltip=\"{{lookup ../../custom.weekdaysLong @index}}\">\n        <SecondaryButton {{#ifIncludes ../weekdays @index}} weight=\"primary\" {{/ifIncludes}}\n          style=\"width: 42px; height: 42px; border-radius: 50%; padding: 0;\" textStyle=\"padding: 0\"\n          onClick=\"window.onRuleChange('{{../id}}', rule => ({ weekdays: window.toggle(rule.weekdays, {{@index}}) }))\">\n          {{this}}\n        </SecondaryButton>\n      </Tooltip>\n      {{/each}}\n    </div>\n\n    <Transition boolLock=\"{{isDateVisible}}\">\n      {{#either afterDate beforeDate isDateVisible}}\n      <div style=\"display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px;\">\n        <Field label=\"Från datum\" type=\"date\" value=\"{{formatDate afterDate 'YYYY-MM-DD'}}\"\n          max=\"{{formatDate beforeDate 'YYYY-MM-DD'}}\" containerStyle=\"width: 100%\"\n          onChange=\"window.onRuleChange('{{id}}', { afterDate: value })\"></Field>\n\n        <Field label=\"Till datum\" type=\"date\" value=\"{{formatDate beforeDate 'YYYY-MM-DD'}}\"\n          min=\"{{formatDate afterDate 'YYYY-MM-DD'}}\" containerStyle=\"width: 100%\"\n          onChange=\"window.onRuleChange('{{id}}', { beforeDate: value })\"></Field>\n      </div>\n      {{else}}\n      <SecondaryButton icon=\"calendar\" size=\"compact\" style=\"width: 100%\"\n        onClick=\"window.onRuleChange('{{id}}', { isDateVisible: true })\">\n        Välj specifika datum\n      </SecondaryButton>\n      {{/either}}\n    </Transition>\n\n    {{#ifEq phoneNumbers.length 0}}\n    <div style=\"text-align: center; font-size: 14px; color: var(--color-text-3); margin-top: 8px;\">\n      <i class=\"fas fa-exclamation-triangle\"></i>&nbsp;\n      Du måste välja ett nummer.\n    </div>\n    {{/ifEq}}\n\n    <Transition boolLock=\"{{isPhoneNumbersVisible}}\">\n      {{#if isPhoneNumbersVisible}}\n\n      <Field label=\"Gäller telefonnummer\" select-type=\"switches\" switches-auto-sort containerStyle=\"width: 100%\">\n        {{#each ../custom.phoneNumbers}}\n        <option value=\"{{id}}\" {{#ifIncludes ../phoneNumbers phoneNumber}} selected {{/ifIncludes}}\n          onChange=\"window.onRuleChange('{{../id}}', rule => ({ phoneNumbers: window.toggle(rule.phoneNumbers, '{{phoneNumber}}') }))\">\n          {{phoneNumber}}\n        </option>\n        {{/each}}\n      </Field>\n\n      {{else}}\n\n      <SecondaryButton size=\"compact\" style=\"width: 100%\" icon=\"pen\"\n        onClick=\"window.onRuleChange('{{id}}', { isPhoneNumbersVisible: true })\">\n        {{#ifGt phoneNumbers.length 1}}\n        {{phoneNumbers.length}} nummer\n        {{else}}\n\n        {{#ifGt phoneNumbers.length 0}}\n        {{lookup phoneNumbers 0}}\n        {{else}}\n        Välj nummer\n        {{/ifGt}}\n\n        {{/ifGt}}\n      </SecondaryButton>\n\n      {{/if}}\n    </Transition>\n\n    <div style=\"text-align: center; font-size: 14px; color: var(--color-text-3); margin-top: 8px;\">\n      När någon ringer in under denna tid...\n    </div>\n\n    <div style=\"display: flex; flex-direction: column; gap: 16px;\">\n      <Transition boolLock=\"{{isAudioVisible}}\">\n        <CheckboxField label=\"Spela ljud\" {{#if isAudioVisible}} checked {{/if}}\n          onChange=\"window.onRuleChange('{{id}}', rule => ({ isAudioVisible: checked, audioUrl: checked ? rule.audioUrl : null }))\">\n        </CheckboxField>\n\n        {{#if isAudioVisible}}\n        <Transition boolLock=\"{{isAudioUrlVisible}}\">\n          <div\n            style=\"padding-top: 12px; padding-left: 36px; box-sizing: border-box; padding-bottom: 8px; display: flex; flex-direction: column; gap: 16px\">\n            <SecondaryButton\n              icon=\"file-audio\"\n              size=\"compact\"\n              style=\"width: 100%\"\n              onClick=\"document.getElementById('audio-{{'id'}}').click()\"\n              {{#if isUploadingAudio}} disabled showTaskIndicator {{/if}}>\n              Välj ljudfil\n            </SecondaryButton>\n\n            <input\n              type=\"file\"\n              class=\"hidden\"\n              onChange=\"window.onAudioFileChange('{{id}}', event)\"\n              accept=\"audio/wav\"\n              id=\"audio-{{'id'}}\">\n\n            <audio src=\"{{audioUrl}}\" controls style=\"width: 100%\"></audio>\n          </div>\n        </Transition>\n        {{/if}}\n      </Transition>\n\n      {{#if isAudioVisible}}\n      <div style=\"font-size: 14px; color: var(--color-text-3);\">Och sen</div>\n      {{/if}}\n\n      <Transition lock=\"{{#ifEq type 'destination'}} 1 {{else}} 2 {{/ifEq}}\">\n        <CheckboxField label=\"Vidarekoppla till nummer\" {{#ifEq type 'destination' }} checked {{/ifEq}}\n          onChange=\"if (checked) { window.onRuleChange('{{id}}', { type: 'destination' }) }\" type=\"radio\">\n        </CheckboxField>\n\n        {{#ifEq type 'destination'}}\n        <div\n          style=\"padding-top: 12px; padding-left: 36px; box-sizing: border-box; padding-bottom: 8px; display: flex; flex-direction: column; gap: 8px\">\n          <Field label=\"Nummer\" onChange=\"window.onRuleChange('{{id}}', { destination: value })\" maxLength=\"16\"\n            containerStyle=\"width: 100%;\" value=\"{{destination}}\" placeholder=\"0770710658\">\n          </Field>\n        </div>\n        {{/ifEq}}\n      </Transition>\n\n      <CheckboxField label=\"Lägg på\" {{#ifEq type 'hangup' }} checked {{/ifEq}}\n        onChange=\"if (checked) { window.onRuleChange('{{id}}', { type: 'hangup' }) }\" type=\"radio\"></CheckboxField>\n    </div>\n    {{/with}}\n  </div>\n</div>\n\n<script>\n  (function () {\n\n    window.createId = function () {\n      return Date.now().toString();\n    }\n\n    window.applyUpdate = function (update, original) {\n      if (typeof update === 'function') {\n        update = update(original);\n      }\n\n      return { ...original, ...update };\n    }\n\n    window.toggle = function (array, element) {\n      if (!Array.isArray(array)) {\n        return [element];\n      }\n\n      if (!array.includes(element)) {\n        return [...array, element];\n      }\n\n      return array.filter(e => e !== element);\n    }\n\n    window.unique = function (array) {\n      return new Set(array);\n    }\n\n    window.push = function (array, element) {\n      if (!Array.isArray(array)) {\n        return [element];\n      }\n\n      array.push(element);\n      return array;\n    }\n\n    window.formatTime = function (hour, minute) {\n      minute = minute || 0;\n\n      return String(hour).padStart(2, \"0\") + \":\" + String(minute).padStart(2, \"0\");\n    }\n\n    window.addRule = function () {\n      const rule = {\n        id: window.createId(),\n        weekdays: [0, 1, 2, 3, 4],\n        phoneNumbers: [],\n        type: 'destination',\n        isPhoneNumbersVisible: true,\n      };\n\n      window.form.setState(state => ({\n        selectedRule: rule\n      }));\n\n      window.openEditor();\n    }\n\n    window.openEditor = function () {\n      window.form.openModal('.hours-editor', {\n        title: \"Vilken tid har ni stängt?\",\n        onHidden: () => {\n          window.form.setState({ selectedRule: null });\n        },\n        separated: {\n          weight: \"danger\",\n          children: \"Ta bort\",\n          onClick: () => {\n            if (window.confirm('Vill du ta bort tiden?')) {\n              return window.removeRule(window.form.custom.selectedRule.id);\n            }\n          }\n        },\n        go: {\n          children: \"Spara\",\n          onClick: () => {\n            return window.saveRule();\n          }\n        },\n        cancel: {}\n      });\n    }\n\n    window.onRuleChange = function (ruleId, update) {\n      window.form.setState(state => {\n        if (state.selectedRule && state.selectedRule.id === ruleId) {\n          return {\n            selectedRule: { ...window.applyUpdate(update, state.selectedRule), isChanged: true }\n          };\n        }\n\n        return {};\n      });\n    }\n\n    window.onAudioFileChange = async function (ruleId, event) {\n      if (!event.currentTarget.files || !event.currentTarget.files.length) {\n        return;\n      }\n\n      window.onRuleChange(ruleId, { isUploadingAudio: true });\n\n      try {\n        const formData = new FormData();\n        formData.append('public', 'true');\n        formData.append('usage', 'Används för vyn Öppettider');\n        formData.append('file', event.currentTarget.files[0]);\n\n        const response = await window.fetch(\n          new URL(\"/api/users/files-v1\", document.location.origin),\n          {\n            method: \"POST\",\n            body: formData,\n            headers: {\n              \"Authorization\": \"Bearer \" + window.getAccessToken(),\n            }\n          }\n        );\n\n        const result = await response.json();\n        if (!response.ok) {\n          throw result;\n        }\n\n        window.onRuleChange(ruleId, { audioUrl: result.url });\n\n        window.form.notify(\"Laddat upp ljudfil\", \"ok\");\n\n      } catch (error) {\n        console.error(\"Error uploading file\", error);\n        window.form.notify(error.localizedMessage || \"Något gick snett\");\n      }\n      \n      window.onRuleChange(ruleId, { isUploadingAudio: false });\n    }\n\n    window.onAudioUrlChange = function (ruleId, audioUrl) {\n      try {\n        const url = new URL(audioUrl);\n        window.onRuleChange(ruleId, { audioUrl });\n\n      } catch (error) {\n        //  Ej giltig URL, spara inte, och uppdatera inte spelaren.\n      }\n    }\n\n    window.onTimeChange = function (ruleId, value, isAfter) {\n      const parts = value.split(':').map(e => parseInt(e));\n\n      let update = {\n        beforeHour: parts[0],\n        beforeMinute: parts[1]\n      };\n\n      if (isAfter) {\n        update = {\n          afterHour: parts[0],\n          afterMinute: parts[1]\n        };\n      }\n\n      window.onRuleChange(ruleId, update);\n    }\n\n    window.getAccessToken = function () {\n      const cookies = document.cookie.split(';').reduce((object, cookie) => {\n        const regex = /(.+)=(.+)/;\n        const parts = regex.exec(cookie.trim());\n        if (!parts || !parts.length) {\n          console.warn(\"Unrecognized cookie %s\", cookie, regex);\n          return object;\n        }\n\n\n        object[parts[1]] = parts[2];\n        return object;\n      }, {});\n\n\n      return cookies[\"s2_utoken\"];\n    }\n\n    window.updateViewSettings = async function (update) {\n      if (window.form.preview) {\n        alert(\"Du kan inte göra ändringar vid förhandsgranskning.\");\n        throw new Error(\"Preview\");\n      }\n\n      //  Spara vyns inställningar, webhooken kollar på detta.\n      const viewId = window.form.view.id;\n\n      //  Hämta nuvarande inställningar om det har ändrats under tiden.\n      const latestView = await window.fetch(new URL(\"/api/users/views-v1/\" + viewId, document.location.origin), {\n        headers: {\n          \"Authorization\": \"Bearer \" + window.getAccessToken(),\n          \"Content-type\": \"application/json\",\n        }\n      }).then(res => {\n        if (!res.ok) {\n          return res.text().then(text => { throw new Error(text); })\n        }\n        return res.json()\n      });\n\n      await window.fetch(new URL(\"/api/users/views-v1/\" + window.form.view.id, document.location.origin), {\n        method: \"PUT\",\n        headers: {\n          \"Authorization\": \"Bearer \" + window.getAccessToken(),\n          \"Content-type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          presetState: window.applyUpdate(update, latestView.presetState)\n        })\n      });\n    }\n\n    window.removeRule = async function (ruleId) {\n      window.form.setState({ isRemovingRule: true });\n\n      try {\n        await window.updateViewSettings(settings => {\n          settings.rules = settings.rules || [];\n          settings.rules = settings.rules.filter(rule => rule.id !== ruleId);\n\n          window.form.setSettings({ rules: settings.rules });\n\n          return { rules: settings.rules };\n        });\n\n        window.form.notify(\"Tagit bort tid\", \"ok\");\n\n        window.form.setState({\n          isRemovingRule: false,\n        });\n\n        window.form.closeModal();\n\n      } catch (error) {\n        console.error(error);\n        alert(\"Något gick snett. Prova igen senare.\");\n      }\n\n      window.form.setState({\n        isRemovingRule: false,\n      });\n    }\n\n    //  Uppdaterar webhook på valda telefonnummer och sparar vyinställningar.\n    window.saveRule = async function () {\n      let rule = window.form.custom.selectedRule;\n      const ruleId = rule.id;\n\n      rule = {\n        ...rule,\n        isSaved: true,  //  Används för att inte spamma \"lägg till\"-knappen.\n        //  Spara inte denna.\n        isChanged: null,\n        isDateVisible: null,\n        isPhoneNumbersVisible: null,\n      };\n\n      window.form.setState({ isSavingRule: true });\n\n      try {\n        for (const phoneNumber of rule.phoneNumbers) {\n          const phoneNumberId = window.form.custom.phoneNumbers.find(element => element.phoneNumber === phoneNumber).id;\n\n          const webhookUrl = new URL(\"https://salesys.se/api/action-presets/opening_hours_routing/webhook.php\");\n          webhookUrl.searchParams.set('viewId', window.form.view.id);\n\n          //  Lägg till webhook så att inkommande samtal hanteras.\n          await window.fetch(new URL(\"/api/dial/phone-numbers-v1/\" + phoneNumberId, document.location.origin), {\n            method: \"PUT\",\n            headers: {\n              \"Authorization\": \"Bearer \" + window.getAccessToken(),\n              \"Content-type\": \"application/json\",\n            },\n            body: JSON.stringify({\n              inboundConfiguration: {\n                ...(phoneNumber.inboundConfiguration || {}),\n                enabled: true,\n                webhookAuth: true,\n                url: webhookUrl.href,\n              }\n            })\n          });\n        }\n\n        await window.updateViewSettings(settings => {\n          settings.rules = settings.rules || [];\n          settings.rules = [\n            ...settings.rules.filter(r => r.id !== ruleId),\n            rule,\n          ];\n\n          window.form.setSettings({ rules: settings.rules });\n\n          return { rules: settings.rules };\n        });\n\n        window.form.notify(\"Sparat regel\", \"ok\");\n\n        window.form.closeModal();\n\n      } catch (error) {\n        console.error(error);\n        alert(\"Något gick snett. Prova igen senare.\");\n      }\n\n      window.form.setState({\n        isSavingRule: false\n      });\n    }\n\n    window.refreshPhoneNumbers = async function () {\n      window.form.setState({ isRefreshingPhoneNumbers: true });\n\n      try {\n        const phoneNumbers = await window.fetch(new URL(\"/api/dial/phone-numbers-v1\", document.location.origin), {\n          headers: {\n            \"Authorization\": \"Bearer \" + getAccessToken()\n          }\n        })\n          .then(response => response.json());\n\n        window.form.setState({\n          phoneNumbers: phoneNumbers.filter(phoneNumber => phoneNumber.status === \"ready\"),\n          error: null\n        });\n\n      } catch (error) {\n        window.form.setState({ error });\n      }\n\n      window.form.setState({ isRefreshingPhoneNumbers: false });\n    }\n\n    window.refreshPhoneNumbers();\n\n    window.form.setState({\n      weekdays: [\"Må\", \"Ti\", \"On\", \"To\", \"Fr\", \"Lö\", \"Sö\"],\n      weekdaysLong: [\"Måndag\", \"Tisdag\", \"Onsdag\", \"Torsdag\", \"Fredag\", \"Lördag\", \"Söndag\"],\n    });\n\n    window.form.addHook('set button', function () {\n      window.form.setButton('add', {\n        icon: 'plus',\n        weight: 'primary',\n        children: 'Lägg till',\n        onClick: () => window.addRule(),\n        disabled: Boolean(window.form.settings.rules && window.form.settings.rules.find(rule => !rule.isSaved))\n      });\n    });\n\n    window.form.addHook('set subtitle', function () {\n      const rules = window.form.settings.rules || [];\n\n      window.form.setSubtitle(!rules.length ? \"Inga inlagda tider än.\" : null)\n    });\n\n    window.form.addHook('set rule time', function () {\n      if (!window.form.settings.rules) {\n        return;\n      }\n\n      //  Sparar en property som vi sätter på input type=\"time\"\n      //  Undviker detta:\n      //  The specified value \"16:3\" does not conform to the required format\n      window.form.setSettings(settings => {\n        rules: settings.rules.map(rule => {\n          rule.afterTime = null;\n\n          if (typeof rule.afterHour === 'number') {\n            const minute = rule.afterMinute || 0;\n            rule.afterTime = String(rule.afterHour).padStart(2, '0') + ':' +\n              String(minute).padStart(2, '0');\n          }\n\n          if (typeof rule.beforeHour === 'number') {\n            const minute = rule.beforeMinute || 0;\n            rule.beforeTime = String(rule.beforeHour).padStart(2, '0') + ':' +\n              String(minute).padStart(2, '0');\n          }\n\n          return rule;\n        })\n      });\n    });\n\n    window.form.addHook('set rule error', function () {\n      if (!window.form.settings.rules) {\n        return;\n      }\n\n      window.form.setSettings(settings => ({\n        rules: settings.rules.map(rule => {\n          rule.error = null;\n\n          if (!rule.phoneNumbers.length) {\n            rule.error = \"Välj ett nummer\";\n          }\n\n          return rule;\n        }),\n      }));\n    })\n\n    window.form.addHook('set rule summary', function () {\n      if (!window.form.settings.rules) {\n        return;\n      }\n\n      window.form.setSettings(settings => ({\n        rules: settings.rules.map(rule => {\n          let summary = [];\n          let label = \"Stängt\";\n\n          //  veckodag\n          const weekdayNames = [\"mån\", \"tis\", \"ons\", \"tor\", \"fre\", \"lör\", \"sön\"];\n\n          const weekdays = [...rule.weekdays];\n          weekdays.sort();\n\n          if (JSON.stringify(weekdays) === JSON.stringify([0, 1, 2, 3, 4])) {\n            summary.push('Mån - fre');\n\n          } else if (weekdays.length) {\n            summary.push(weekdayNames.filter((name, index) => rule.weekdays.includes(index)).join(', '));\n\n          } else {\n            summary.push('Gäller ej (ingen veckodag valt)')\n          }\n\n\n          //  klockan\n          if (typeof rule.afterHour === 'number' && typeof rule.beforeHour === 'number') {\n            label += \" \" + (\n              window.formatTime(rule.afterHour, rule.afterMinute) + \" - \" +\n              window.formatTime(rule.beforeHour, rule.beforeMinute)\n            );\n\n          } else if (typeof rule.afterHour === 'number') {\n            label += \" \" + (\n              window.formatTime(rule.afterHour, rule.afterMinute) + \"—\"\n            );\n\n          } else if (typeof rule.beforeHour === 'number') {\n            label += \" \" + (\n              \"—\" + window.formatTime(rule.beforeHour, rule.beforeMinute)\n            );\n          }\n\n          //  nummer\n          if (rule.phoneNumbers.length) {\n            summary.push(rule.phoneNumbers.join(', '));\n          }\n\n          //  åtgärder\n          if (rule.audioUrl) {\n            summary.push('Spela upp ljud');\n          }\n\n          if (rule.type === 'destination' && rule.destination) {\n            summary.push('Vidarekoppla: ' + rule.destination);\n\n          } else if (rule.type === 'hangup') {\n            summary.push('Lägg på');\n          }\n\n          rule.label = label;\n          rule.summary = summary;\n          return rule;\n        }),\n      }));\n    });\n  })();\n</script>","iconName":"calendar","iconColor":null,"settingsHtml":"","description":"Ställ in vilka tider ni har stängt, vidarekoppla samtal till ett annat nummer, och mer.","rights":[],"components":[]},{"id":"6a719ef9739e1e0a723a9799","name":"Webhooks","isRemoved":true,"isViewPreviewEnabled":true,"entityGroupIds":[],"locations":[{"type":"settings","position":null}],"html":"<script>\n  //  Översätt events här. Bara att fylla på.\n  window.eventTranslations = {\n    \"after-order-work-executed-v1\": \"Orderarbete exekverat\",\n    \"bankid-session-status-updated-v1\": \"Avtal BankID upddaterat\",\n    \"calendar-event-deleted-v1\": \"Händelse raderad\",\n    \"calendar-event-updated-v1\": \"Händelse uppdaterad\",\n    \"call-tag-removed-v1\": \"Samtalstagg raderad\",\n    \"contact-updated-v1\": \"Kontakt uppdaterad\",\n    \"contacts-deleted-v1\": \"Kontakter raderade\",\n    \"dial-group-contacts-added-v1\": \"Kontakt lagd i lista\",\n    \"dial-group-contacts-removed-v1\": \"Kontakt borttagen från lista\",\n    \"dial-group-created-v1\": \"Ringlista skapad\",\n    \"dial-group-deleted-v1\": \"Ringlista raderad\",\n    \"dial-group-updated-v1\": \"Ringlista uppdaterad\",\n    \"document-ready-v1\": \"Dokument färdigt\",\n    \"exclude-list-deleted-v1\": \"Raderat spärrlista\",\n    \"exclude-list-strings-added-v1\": \"Term tillagd\",\n    \"exclude-list-strings-removed-v1\": \"Term borttagen\",\n    \"exclude-list-strings-updated-v1\": \"Term ändrad\",\n    \"external-call-created-v1\": \"\",\n    \"feature-enabled-v1\": \"Funktion aktiverad\",\n    \"import-task-status-updated-v1\": \"Import uppdaterad\",\n    \"inbound-sms-received-v1\": \"SMS mottaget\",\n    \"offer-created-v1\": \"Avtal skapat\",\n    \"offer-message-created-v1\": \"Avtalsutskick skapat\",\n    \"offer-recipient-added-v1\": \"Avtalsmottagare tillagd\",\n    \"offer-status-updated-v1\": \"Avtalstatus uppdaterat\",\n    \"offer-template-created-v1\": \"Avtalsmall skapad\",\n    \"offer-template-updated-v1\": \"Avtalsmall uppdaterad\",\n    \"order-created-v1\": \"Order skapad\",\n    \"order-deleted-v1\": \"Order raderad\",\n    \"order-tag-deleted-v1\": \"Ordertagg raderad\",\n    \"order-updated-v1\": \"Order uppdaterad\",\n    \"outbound-sms-sent-v1\": \"SMS skickat\",\n    \"phone-number-status-updated-v1\": \"Status ändrat\",\n    \"user-created-v1\": \"Användare skapad\",\n    \"user-logged-out-v1\": \"Användare loggat ut\",\n    \"user-password-updated-v1\": \"Ändrat lösenord\",\n  };\n\n  //  Events grupperas utifrån prefix. Om ingen match så blir det \"Övrigt\".\n  window.eventPrefixes = {\n    \"call-tag\": \"Inställningar\",\n    \"order-tag\": \"Inställningar\",\n\n    //  Exempelvis calendar-event-updated-v1\n    \"contact\": \"Kontakt\",\n    \"calendar\": \"Kalender\",\n    \"user\": \"Användare\",\n    \"offer\": \"Avtal\",\n    \"document\": \"Dokument\",\n    \"order\": \"Order\",\n    \"after-order-work\": \"Order\",\n    \"dial-group\": \"Ringlistor\",\n    \"lead-lists\": \"Ringlistor\",\n    \"exclude-list\": \"Spärrlistor\",\n    \"phone-number\": \"Telefonnummer\",\n  };\n\n  window.insertOptions = [\n    {\n      text: \"Ordernummer\",\n      data: { _path: \"$.serialId\" },  //  Hur datan faktiskt tas fram.\n      group: \"Order\", //  Frivillig rubrik för att gruppera alternativ, exempelvis \"Orderinformation\".\n\n      //  Frivilligt, vilka events som detta gäller för.\n      eventPrefix: [\"order\"],\n    },\n    ...window.form.resources.orderFields.map(field => ({\n      text: field.label,\n      data: { _field: field.id },\n      group: \"Orderinformation\",\n      eventPrefix: [\"order\", \"offer\"]\n    })),\n    {\n      text: \"Användarens epost\",\n      data: { _path: \"$.email\" },\n      group: \"Användare\",\n      eventPrefix: [\"user-\"],\n    },\n    {\n      text: \"Användarens namn\",\n      data: { _path: \"$.fullName\" },\n      group: \"Användare\",\n      eventPrefix: [\"user-\"],\n    },\n  ];\n\n  //  Returnerar namnet på en ikon som tillhör en åtgärd.\n  //  https://fontawesome.com/v5/search?q=window&ic=free-collection\n  window.getActionIcon = function(id, name) {\n    //  Lägg till specifika ikoner här. Exempelvis:\n    //  if (id === \"Skicka_SMS\") { return \"mobile-alt\"; }\n\n    if (id === \"http\") { return \"code\"; }\n\n\n    //  Gissa utifrån namnet.\n    return window.window.guessActionIcon(id, name);\n  }\n  \n  window.guessActionIcon = function(id, name) {\n    //  Lägg inte till någon ikon för ett specifikt ID här!\n\n    name = name || id.split(/[_\\-]/).join(' ');\n\n    const tokens = name.split(' ').map(token => token.toLowerCase().replace(/[^\\w]/g, ''));\n\n    if (tokens.includes(\"epost\") || tokens.includes(\"email\") || tokens.includes(\"eposta\") || tokens.includes(\"mail\") || tokens.includes(\"maila\")) {\n      return \"envelope\";\n    \n    } if (tokens.includes(\"sms\")) {\n      return \"mobile-alt\";\n    \n    } else if (tokens.includes(\"skicka\")) {\n      return \"paper-plane\";\n    \n    } else if (tokens.includes(\"notis\") || tokens.includes(\"notifiera\") || tokens.includes(\"avvisera\") || tokens.includes(\"meddela\")) {\n      return \"bell\";\n    }\n\n    return \"wrench\";\n  }\n\n\n</script>\n\n<style>\n  .webhook-section {\n    padding: 16px;\n    box-sizing: border-box;\n    border: 1px solid var(--color-background-3);\n    border-radius: 8px;\n    display: flex;\n    flex-direction: column;\n    gap: 24px;\n  }\n</style>\n\n<div style=\"display: flex; flex-direction: column; gap: 48px;\">\n  <SettingsList>\n    {{#each custom.webhooks}}\n    <option id=\"{{id}}\"\n      icon=\"{{either reference.icon 'wrench'}}\"\n      label=\"{{either reference.name 'Webhook'}}\"\n      description=\"{{reference.description}}\"\n      onClick=\"window.openEditor(window.form.custom.webhooks.find(webhook => webhook.id === '{{id}}'))\"\n      {{#if ../custom.isRefreshingWebhooks}} disabled {{/if}}\n      {{#ifNot ../custom.webhookEvents}} disabled {{/ifNot}}>\n\n      {{descriptionShort}}\n\n      {{#if automaticDisableReason}}\n      \n      <attribute icon=\"exclamation-triangle\">\n        Har blivit inaktiverad automatiskt. Du behöver klicka redigera och aktivera den igen.\n\n        Anledning: {{automaticDisableReason}}\n      </attribute>\n\n      {{else}}\n        {{#ifNot isEnabled}}\n        <attribute icon=\"ban\">\n          Inaktiverad\n        </attribute>\n        {{/ifNot}}\n      {{/if}}\n      \n      {{#if automaticRetryDisableReason}}\n      <attribute icon=\"exclamation-triangle\">\n        Automatisk omförsök vid fel har blivit inaktiverat automatiskt.\n\n        Anledning: {{automaticRetryDisableReason}}\n      </attribute>\n      {{/if}}\n    </option>\n    {{/each}}\n  </SettingsList>\n\n  <div class=\"webhook-editor\" modal>\n    <Transition\n      lock=\"{{#if custom.isPreviewOpen}} 1 {{/if}} {{#if custom.previewResult}} 1 {{/if}}\"\n      overflowHiddenDuringTransition=\"true\">\n\n      <div style=\"display: flex; flex-direction: column; gap: 24px; min-height: 240px; position: relative;\">\n      {{#if custom.isPreviewOpen}}\n        <!-- Förhandsgranska, som med orderarbeten/samtalsarbeten -->\n        <h2 style=\"display: flex; flex-direction: row; align-items: center; gap: 6px; margin: 0;\">\n          <Tooltip delay=\"false\" tooltip=\"Tillbaka\">\n            <SecondaryButton icon=\"chevron-left\"\n              onClick=\"window.form.setState({ isPreviewOpen: false })\"\n              iconSize=\"medium\">\n            </SecondaryButton>\n          </Tooltip>\n          Förhandsgranska\n        </h2>\n\n        {{#if custom.previewResult}}\n          {{#each custom.previewFields}}\n            <Field\n              label=\"{{name}}\"\n              value=\"{{value}}\"\n              containerStyle=\"width: 100%\"\n              multiline\n              maxRows=\"6\"\n              readOnly>\n            </Field>\n          {{/each}}\n\n          <Field label=\"Data som kan infogas\"\n            value=\"{{custom.previewResult.sourceJson}}\"\n            containerStyle=\"width: 100%\"\n            inputContainerStyle=\"background-color: var(--color-background-1); \"\n            style=\"font-family: monospace; font-size: 13px; line-height: 150%;\"\n            multiline\n            maxRows=\"40\"\n            readOnly>\n          </Field>\n        {{else}}\n          {{#if custom.previewError}}\n            <div style=\"text-align: center; font-size: 14px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); line-height: 150%; width: 100%; box-sizing: border-box; padding: 0 24px;\">\n              {{either custom.previewError.localizedMessage \"Något gick snett\"}}\n            </div>\n\n          {{else}}\n            <!-- Laddar förhandsgranskning. Visa inbyggd SaleSys-spinner -->\n            <div class=\"task-indicator large animated\"\n              style=\"position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);\">\n              <div data-spinner></div>\n            </div>\n          {{/if}}\n\n        {{/if}}\n\n      {{else}}\n      <!-- Redigera webhook. -->\n        <div style=\"display: flex; flex-direction: column; gap: 8px;\">\n          <Field placeholder=\"Ange namn\" containerStyle=\"width: 100%\" style=\"font-size: 32px; font-weight: 300;\"\n            value=\"{{custom.selectedWebhook.reference.name}}\" onchange=\"window.onWebhookReferenceChange({ name: value })\">\n          </Field>\n\n          {{#if custom.selectedWebhook.id}}\n          <div style=\"text-align: center; font-size: 14px;\">\n            ID: {{custom.selectedWebhook.id}}\n          </div>\n          {{/if}}\n        </div>\n        \n        <Field\n          label=\"Beskrivning\"\n          placeholder=\"Beskriv denna webhook\"\n          containerStyle=\"width: 100%\" \n          multiline\n          maxRows=\"3\"\n          maxLength=\"500\"\n          value=\"{{custom.selectedWebhook.reference.description}}\"\n          onchange=\"window.onWebhookReferenceChange({ description: value })\">\n        </Field>\n\n        <Transition lock=\"{{#if custom.selectedWebhook.reference.actionId}} 1 {{else}} 0 {{/if}} {{#if custom.isWebhookAdvancedOpen}} 1 {{else}} 0 {{/if}}\">\n          <div style=\"display: flex; flex-direction: column; gap: 16px;\">\n            <CheckboxField label=\"Aktiv\"\n              {{#if custom.selectedWebhook.isEnabled}} checked {{/if}}\n              onChange=\"window.onWebhookChange({ isEnabled: checked })\"\n              sub=\"Bocka ur för att inaktivera denna webhook.\">\n            </CheckboxField>\n            \n            {{#ifNot custom.selectedWebhook.reference.actionId}}\n              {{#if custom.isWebhookAdvancedOpen}}\n\n              <CheckboxField label=\"Automatiska omförsök vid fel\"\n                {{#if custom.selectedWebhook.retry}} checked {{/if}}\n                onChange=\"window.onWebhookChange({ retry: checked })\"\n                sub=\"Försöker igen automatiskt efter en stund vid fel\">\n              </CheckboxField>\n              \n              <CheckboxField label=\"Autentisera anrop\"\n                {{#if custom.selectedWebhook.auth}} checked {{/if}}\n                onChange=\"window.onWebhookChange({ auth: checked })\"\n                sub=\"Systemet skickar med Authorization-header\">\n              </CheckboxField>\n              \n              <CheckboxField label=\"Spara händelser\"\n                {{#if custom.selectedWebhook.isExecutionLogEnabled}} checked {{/if}}\n                onChange=\"window.onWebhookChange({ isExecutionLogEnabled: checked })\"\n                sub=\"Sparar en händelse för varje anrop\">\n              </CheckboxField>\n              \n              {{else}}\n\n              <SecondaryButton icon=\"chevron-down\" style=\"width: 100%\" size=\"compact\"\n                onClick=\"window.form.setState({ isWebhookAdvancedOpen: true })\">\n                Visa avancerade inställningar\n              </SecondaryButton>\n\n              {{/if}}\n\n            {{/ifNot}}\n          </div>\n        </Transition>\n\n        <div class=\"webhook-section\">\n          <Field label=\"Välj händelse\" select-type=\"dropdown\" containerStyle=\"width: 100%\"\n            value=\"{{custom.selectedWebhookEventGroup.label}}\"\n            onChange=\"window.form.setState(state => ({ selectedWebhookEventGroup: state.webhookEventGroups.find(group => group.label === value) }))\">\n            {{#each custom.webhookEventGroups}}\n            <option>{{label}}</option>\n            {{/each}}\n          </Field>\n\n          <div style=\"display: flex; flex-direction: column;\">\n            <Field label=\"Händelser ({{custom.selectedWebhookEventGroup.label}})\" select-type=\"switches\" containerStyle=\"width: 100%;\">\n              {{#each custom.selectedWebhookEventGroup.events}}\n              <option {{#ifIncludes ../custom.selectedWebhook.eventNames event}} selected {{/ifIncludes}} value=\"{{event}}\"\n                onChange=\"window.onWebhookChange(webhook => ({ eventNames: ['{{event}}'] }))\">\n                {{translation}}\n              </option>\n              {{/each}}\n            </Field>\n\n            <Transition boolLock=\"{{custom.selectedWebhook.reference.actionId}}\">\n            {{#if custom.selectedWebhook.reference.actionId}}\n              <div style=\"padding-top: 24px\">\n              {{> Filter}}\n              </div>\n            {{/if}}\n            </Transition>\n          </div>\n        </div>\n\n        <div class=\"webhook-section\" style=\"margin-top: 16px;\">\n          <Field label=\"Välj åtgärd\" value=\"{{either custom.selectedWebhook.reference.actionId 'default'}}\"\n            onChange=\"window.getPreviewFields = null; window.onWebhookReferenceChange({ actionId: value === 'default' ? null : value });\"\n            containerStyle=\"width: 100%;\">\n\n            <option value=\"default\">ingen / egen</option>\n\n            {{#each custom.actions}}\n            <option value=\"{{id}}\" icon=\"{{either icon 'wrench'}}\">{{either name id}}</option>\n            {{/each}}\n\n          </Field>\n\n          {{#if custom.selectedWebhook.reference.actionId}}\n\n          <div class=\"webhook-action-editor\"\n            onFocusCapture=\"if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') { window.webhooksLastFocusedElement = event.target; }\">\n            {{{custom.selectedAction.html}}}\n          </div>\n\n          <div style=\"display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;\">\n            <SecondaryButton\n              icon=\"plus\"\n              style=\"width: 100%\"\n              size=\"compact\"\n              position=\"right\"\n              {{#ifNot custom.insertOptions.length}} disabled {{/ifNot}}>\n              Infoga\n\n              {{#each custom.insertOptions}}\n              <option onClick=\"window.insertText('[[{{text}}]]'.toLowerCase())\" group=\"{{group}}\" search=\"{{search}}\">\n                {{text}}\n              </option>\n              {{/each}}\n            </SecondaryButton>\n            \n            <SecondaryButton\n              icon=\"eye\"\n              style=\"width: 100%\"\n              size=\"compact\"\n              onclick=\"window.form.setState({ isPreviewOpen: true }); window.refreshPreview(window.form.custom.selectedWebhook.eventNames[0]);\">\n              Förhandsgranska\n            </SecondaryButton>\n          </div>\n\n          {{else}}\n\n          <!-- URL som default. -->\n          <Field label=\"URL\" value=\"{{custom.selectedWebhook.url}}\"\n            onChange=\"window.onWebhookChange({ url: value })\"\n            containerStyle=\"width: 100%\"\n            placeholder=\"https://example.org/webhook\">\n          </Field>\n\n          {{/if}}\n        </div>\n\n      {{/if}}\n      </div>\n    </Transition>\n    \n  </div>\n\n  {{> Events}}\n</div>\n\n<script>\n  //  GUI för webhooks.\n  //  Detta är inställningar för webhooks, och dessa sparas i webhook.reference och\n  //  hanteras av ett generiskt php-skript.\n\n\n  (function () {\n    // Generisk endpoint som hanterar webhooks via den här vyns inställningar.\n    const webhookUrl = \"https://salesys.se/api/action-presets/webhooks/webhook.php\";\n\n    window.getAccessToken = function () {\n      const cookies = document.cookie.split(';').reduce((object, cookie) => {\n        const regex = /(.+)=(.+)/;\n        const parts = regex.exec(cookie.trim());\n        if (!parts || !parts.length) {\n          console.warn(\"Unrecognized cookie %s\", cookie, regex);\n          return object;\n        }\n\n\n        object[parts[1]] = parts[2];\n        return object;\n      }, {});\n\n\n      return cookies[\"s2_utoken\"];\n    }\n\n    window.push = function (array, element) {\n      if (!Array.isArray(array)) {\n        return [element];\n      }\n\n      return [...array, element];\n    }\n\n    window.filter = function (array, element, predicate) {\n      if (!Array.isArray(array)) {\n        return [];\n      }\n\n      if (!predicate) {\n        predicate = (e => e !== element);\n      }\n\n      return array.filter(predicate);\n    }\n\n    window.applyUpdate = function (update, original) {\n      if (typeof update === 'function') {\n        update = update(original);\n      }\n\n      return { ...original, ...update };\n    }\n\n    window.toggle = function (array, element) {\n      if (!Array.isArray(array)) {\n        return [element];\n      }\n\n      return array.includes(element) ?\n        array.filter(e => e !== element) :\n        [...array, element];\n    }\n\n\n    const headers = {\n      \"Authorization\": \"Bearer \" + window.getAccessToken(),\n      \"Content-type\": \"application/json\"\n    };\n\n    //  Grupperar och översätter events.\n    window.listGroupedWebhookEvents = function (eventNames) {\n      return Object.entries(\n        eventNames.reduce((object, event) => {\n          let label = \"\";\n\n          for (const prefix in window.eventPrefixes) {\n            if (event.startsWith(prefix)) {\n              label = window.eventPrefixes[prefix];\n              break;\n            }\n          }\n\n          if (label) {\n          } else if (event.includes('sms-')) {\n            label = \"SMS\";\n\n          } else if (event.includes('call')) {\n            label = \"Samtal\";\n\n          } else {\n            label = \"Övrigt\";\n          }\n\n          let translation = window.eventTranslations[event];\n\n          if (!translation) {\n            //  Default översättning:\n            //  \"user-logged-out-v1\" => \"user logged out\"\n            translation = event.replace(/-v\\d$/, '')\n              .replace(/-/g, ' ');\n\n            //  \"user logged out\" => \"User logged out\"\n            translation = translation.slice(0, 1).toUpperCase() + translation.slice(1);\n          }\n\n          object[label] = object[label] || [];\n          object[label].push({ event, translation })\n\n          return object;\n        }, {})\n      ).map(([label, events]) => ({\n        label,\n        events: events.sort((a, b) => a.translation.localeCompare(b.translation))\n      })).sort((a, b) => {\n        if (a.label === \"Övrigt\") {\n          //  Övrigt ska vara längst nere.\n          return 1;\n        }\n\n        if (b.label === \"Övrigt\") {\n          return -1;\n        }\n\n        return a.label.localeCompare(b.label);\n      });\n    }\n\n    //  Hämtar events som man kan välja mellan.\n    window.refreshWebhookEvents = async function () {\n      try {\n        const response = await window.fetch(\n          new URL(\"/api/events/webhooks-v1/events\", document.location.origin),\n          { headers }\n        );\n\n        const result = await response.json();\n\n        if (!response.ok) {\n          throw result;\n        }\n\n        window.form.setState({\n          webhookEvents: result.allowedEvents,\n\n          //  Grupperade och översatta, de som syns i listan.\n          webhookEventGroups: window.listGroupedWebhookEvents(result.allowedEvents),\n        });\n\n      } catch (error) {\n        console.error(\"Error refreshing webhook events\", error);\n        window.form.notify(error.localizedMessage || \"Något gick snett\");\n      }\n    }\n\n    //  Hämtar webhooks som vi ska lista.\n    window.refreshWebhooks = async function () {\n      window.form.setState({ isRefreshingWebhooks: true });\n\n      try {\n        const response = await window.fetch(\n          new URL(\"/api/events/webhooks-v1\", document.location.origin),\n          { headers }\n        );\n\n        const result = await response.json();\n\n        if (!response.ok) {\n          throw result;\n        }\n\n        window.form.setState({\n          webhooks: result.map(webhook => {\n            //  eventNames är lite enklare att jobba med än .events.name.\n            webhook.eventNames = webhook.events.map(event => event.name);\n            webhook.descriptionShort = webhook.reference.description &&\n              webhook.reference.description.length > 28 ?\n              webhook.reference.description.slice(0, 28) + '...' :\n                webhook.reference.description;\n\n            return webhook;\n          })\n        });\n\n        window.form.setSubtitle(result.length ? null : \"Inga webhooks än.\");\n\n      } catch (error) {\n        console.error(\"Error refreshing webhooks\", error);\n        window.form.notify(error.localizedMessage || \"Något gick snett\");\n      }\n\n      window.form.setState({ isRefreshingWebhooks: false });\n    }\n\n    window.refreshWebhookEvents();\n    window.refreshWebhooks();\n\n    window.form.setState({\n      actions: window.form.resources.viewPreset.components\n      .filter(component => component.id.toLowerCase() !== 'filter' &&\n        component.id.toLowerCase() !== 'events')\n      .map(component => ({\n        ...component,\n        icon: window.getActionIcon(component.id, component.name)\n      }))\n    });\n\n    window.saveWebhook = async function () {\n      const webhook = window.form.custom.selectedWebhook;\n      const action = window.form.custom.selectedAction || { icon: \"wrench\" };\n      const actionId = webhook.reference.actionId;\n      const settings = webhook.reference[actionId] || {};\n\n      //  Vad vi alltid ska uppdatera \"reference\" till. Forcerade värden för \"reference\".\n      const referenceUpdate = {\n        //  Lägg till data som ska sparas i reference här.\n        name: webhook.reference.name,\n        description: webhook.reference.description,\n        actionId: actionId,\n        icon: action.icon,\n        delayValue: webhook.reference.delayValue,\n        delayUnit: webhook.reference.delayUnit,\n        delayId: webhook.reference.delayId,\n        [actionId]: webhook.reference[actionId],\n\n        //  Saken (ordern, användaren, osv) som eventet berör.\n        //  Skriptet använder denna för att kunna stämma av med filtret.\n        //  Detta funkar tack vare Webhook.parseReference.\n        //  Datan kommer från getWebhookEventSourceCallback i s2-events.\n        entity: { _path: \"$\" },\n      };\n\n      try {\n        let response;\n        \n        webhook.events = webhook.eventNames.map(name => ({ name: name }));\n\n        if (actionId) {\n          webhook.auth = true;\n          webhook.isExecutionLogEnabled = true;\n          webhook.parseReference = window.getParseReferenceOptions();\n          webhook.authRights = settings.authRights || [];  // Sätts av den andra vymallen.\n          webhook.url = webhookUrl;\n          \n        } else {\n          //  Vanlig webhook med URL (ej konfigurerad via denna vy).\n        }\n\n        if (webhook.id) {\n          //  Uppdatera webhook.\n          response = await window.fetch(\n            new URL(\"/api/events/webhooks-v1/\" + webhook.id, document.location.origin),\n            {\n              headers,\n              method: \"PUT\",\n              //  TODO: skicka bara med reference.actionId och reference[reference.actionId]\n              body: JSON.stringify({\n                ...webhook,\n\n                //  reference slås ihop på servern.\n                reference: referenceUpdate,\n              }),\n            }\n          );\n\n        } else {\n          //  Skapa webhook.\n          response = await window.fetch(\n            new URL(\"/api/events/webhooks-v1\", document.location.origin),\n            {\n              headers,\n              method: \"POST\",\n              body: JSON.stringify({\n                ...webhook,\n                reference: { ...referenceUpdate, ...webhook.reference }\n              }),\n            }\n          );\n        }\n\n        if (!response.ok) {\n          throw new Error(await response.text());\n        }\n\n        //  Annars kommer den fråga om du vill stänga modalen.\n        window.checkWebhookChangesBeforeClose = false;\n\n        window.refreshWebhooks();\n\n        //  TODO: bättre text\n        window.form.notify(\"Sparat\", \"ok\");\n\n        return true;\n\n      } catch (error) {\n        console.error(\"Error saving webhook %s\", webhook.id, error);\n        window.form.notify(error.localizedMessage || \"Något gick snett\");\n      }\n    }\n\n    window.getWebhookDefaultName = function (webhook) {\n      return \"Ett bra namn\";\n    }\n\n    window.newWebhook = function () {\n      return {\n        isEnabled: true,\n        events: [],\n        eventNames: [],\n        reference: {\n          name: \"\",\n        },\n      };\n    }\n\n    window.openEditor = function (webhook) {\n      window.checkWebhookChangesBeforeClose = true;\n\n      window.form.setState(state => {\n        state.selectedWebhook = webhook;\n        state.isWebhookAdvancedOpen = false;\n\n        //  Ta fram vad vi ska visa i dropdownen över typer av händelser.\n        state.selectedWebhookEventGroup = state.webhookEventGroups.find(group =>\n          group.events.find(event => webhook.eventNames.includes(event.event))) || state.webhookEventGroups[0];\n\n        return state;\n      });\n\n      window.form.openModal('.webhook-editor', {\n        go: {\n          type: \"ok\",\n          children: \"Spara\",\n          onClick: () => window.saveWebhook(),\n        },\n        size: 'medium',\n        cancel: {},\n        onClose: () => {\n          \n          if (!window.checkWebhookChangesBeforeClose) {\n            return;\n          }\n\n          //  Kolla om webhooken har ändrats, isåfall fråga om rutan ska stängas.\n          const selectedWebhook = window.form.custom.selectedWebhook;\n          const initial = window.form.custom.webhooks.find(webhook => webhook.id === selectedWebhook.id);\n\n          if (JSON.stringify(selectedWebhook) !== JSON.stringify(initial) && \n            !window.confirm(\"Vill du stänga redigeraren?\")) {\n            return false; //  Stäng inte modalen.\n          }\n          \n          window.form.setState({ isPreviewOpen: false });\n        },\n        onHidden: () => {\n          //  .selectedWebhook är hur vi vet att .webhook-editor är öppen.\n          window.form.setState({ selectedWebhook: null });\n        }\n      });\n    }\n\n    //  Ändrar selectedWebhook\n    window.onWebhookChange = function (update) {\n      window.form.setState(state => {\n        if (state.selectedWebhook) {\n          return { selectedWebhook: window.applyUpdate(update, state.selectedWebhook) };\n        }\n      })\n    }\n\n    //  Ändrar selectedWebhook.reference\n    window.onWebhookReferenceChange = function (update) {\n      window.onWebhookChange(webhook => ({\n        reference: window.applyUpdate(update, webhook.reference)\n      }));\n    }\n\n    //  Ändrar selectedWebhook.reference[actionId]\n    window.onWebhookSettingsChange = function (update) {\n      window.onWebhookChange(webhook => {\n        const actionId = webhook.reference.actionId;\n        if (!actionId) {\n          return {};\n        }\n\n        return {\n          reference: {\n            ...webhook.reference,\n            //  Skilj på inställningar utifrån ID.\n            [actionId]: applyUpdate(update, webhook.reference[actionId] || {})\n          },\n        };\n      });\n    }\n\n    //  Returnerar nuvarande åtgärd.\n    window.getSelectedActionId = () => {\n      const webhook = window.form.custom.selectedWebhook;\n      return webhook ? webhook.reference.actionId : null;\n    }\n\n    //  Returnerar vad vi ska spara som webhook.parseReference.\n    window.getParseReferenceOptions = () => {\n      const webhook = window.form.custom.selectedWebhook;\n      const actionId = window.getSelectedActionId();\n        \n      if (actionId && window.form.custom.insertOptions) {\n        //  T.ex. { \"säljare\": { _text: \"[[user.firstName]]\" }, ... }\n        return {\n          ...webhook.parseReference || {},\n          defaultTextVariables:\n            window.form.custom.insertOptions.reduce((object, option) => {\n              object[option.text.toLowerCase()] = option.data;\n              return object;\n            }, {}),\n        };\n      }\n\n      return webhook.parseReference || {};\n    }\n\n    //  Likadant API och implementation som window.form.setValidationMessage.\n    //  Annorlunda namn för att inte förvirra med window.form.setValidationMessage samt för att\n    //  förtydliga att det är per action.\n    window.setActionValidation = function(id, message) {\n      if (arguments.length !== 2) {\n        //  Så man inte råkar skriva setValidationMessage(\"Vänligen ange epost\")\n        throw new Error(\"You must pass an id and a message to setValidationMessage, for example setValidationMessage('email', 'Enter a valid email address')\");\n      }\n      \n      const actionId = window.getSelectedActionId();\n      if (!actionId) {\n        return;\n      }\n\n      window.form.setState(state => {\n        state.validationMessageObject[actionId] = \n          state.validationMessageObject[actionId] || {};\n        \n        state.validationMessageObject[actionId][id] = message;\n\n        return state;\n      });\n    }\n\n    //  Likadant API och implementation som window.form.removeValidationMessage.\n    window.removeActionValidation = function(id) {\n      if (typeof id !== 'string') {\n        throw new Error(\"Invalid id oassed to removeValidationMessage: \" + JSON.stringify(id));\n      }\n      \n      const actionId = window.getSelectedActionId();\n      if (!actionId) {\n        return;\n      }\n\n      window.form.setState(state => {\n        if (state.validationMessageObject[actionId]) {\n          state.validationMessageObject[actionId][id] = null;\n        }\n        \n        return state;\n      });\n    }\n\n    //  Infogar text i senaste iklickad textruta.\n    window.insertText = function(text) {\n      let element = window.webhooksLastFocusedElement ||\n        document.querySelector('input[default-insert-target], textarea[default-insert-target], [default-insert-target] input, [default-insert-target] textarea');\n\n      if (element && !element.isConnected) {\n        element = null;\n      }\n\n      if (!element) {\n        throw new Error(\"No element to insert text to. Click on an element, or add an element: <Field default-insert-target=\\\"\\\" ...>\");\n      }\n\n      let start = element.selectionStart;\n      \n      if (!window.webhooksLastFocusedElement) {\n        //  Om man inte har klickat på något element så är selectionStart=0, men vi ska infoga texten sist.\n        start = element.value.length;\n      }\n\n      if (start >= element.value.length && !text.endsWith(' ')) {\n        //  Anta att det ska vara ett mellanrum mellan den infogade texten.\n        text = ' ' + text;\n      }\n\n      element.value = element.value.substring(0, start) + text +\n        element.value.substring(start);\n\n      //  Kalla onChange från React, som läser av .value och uppdaterar state.\n      //  Denna funktionen är inbyggd i textfält.\n      element.onChange_original();\n    }\n\n    //  Hämtar förhandsgranskningsdata.\n    window.refreshPreview = async function(eventName) {\n      window.form.setState({ previewResult: null, previewError: null, previewFields: null });\n\n      const webhook = window.form.custom.selectedWebhook;\n      const actionId = window.getSelectedActionId();\n\n      try {\n        const url = new URL(\"/api/events/webhooks-v1/preview\", document.location.origin);\n        //  TODO: kunna välja flera event names.\n        url.searchParams.set('eventName', eventName);\n\n        const response = await fetch(url, {\n          headers,\n          method: \"POST\",\n          body: JSON.stringify({\n            object: webhook.reference,\n            options: window.getParseReferenceOptions()\n          })\n        });\n\n        const result = await response.json();\n\n        if (result.code === \"202608210718\") {\n          //  Vald händelse har inte hänt än.\n          result.localizedMessage = \"För att kunna förhandsgranska måste vald händelse ha inträffat på kontot inom de 3 senaste månaderna.\";\n        }\n\n        if (!response.ok) {\n          throw result;\n        }\n\n        //  Din komponent (åtgärd) kan ändra window.getPreviewFields för att\n        //  visa relevant information i förhandsgranskning.\n        if (window.getPreviewFields) {\n          const previewFields = window.getPreviewFields(result.object[actionId]);\n\n          if (!Array.isArray(previewFields)) {\n            throw new Error(\"getPreviewFields must return an array, returned: \" + JSON.stringify(previewFields));\n          }\n\n          window.form.setState({\n            previewFields,\n          });\n        }\n\n        window.form.setState({\n          previewResult: {\n            ...result,\n            sourceJson: JSON.stringify(result.source, null, 2),\n          }\n        });\n\n      } catch (error) {\n        console.error(\"Error refreshing preview\", error);\n        window.form.setState({ previewError: error });\n      }\n    }\n\n    window.form.addHook('set buttons', function () {\n      //  Visa spinner medan vi laddar in webhooks.\n      window.form.setBusy(window.form.custom.isRefreshingWebhooks);\n\n      window.form.setButton('add webhook', {\n        children: 'Lägg till',\n        icon: 'plus',\n        weight: 'primary',\n        disabled: !window.form.custom.webhookEvents,\n        onClick: () => {\n          window.openEditor(window.newWebhook());\n        },\n      });\n\n      if (window.form.custom.isDocsLinkVisible) {\n        window.form.setButton('docs', {\n          children: 'Dokumentation om denna vy',\n          size: 'compact',\n          icon: 'external-link-alt',\n          externalLinkTo: 'https://docs.google.com/document/d/1RzZRjFOP-aInqU1KCpqGdWOxEY7v-Vmw_D4xq_krSpA/edit?usp=sharing'\n        });\n      }\n    });\n\n    window.form.addHook('set webhook .settings', function () {\n      window.form.setState(state => {\n        state.webhooks = state.webhooks.map(webhook => {\n          //  Det kallas \"settings\" på vissa ställen - onWebhookSettingsChange\n          //  och för enkelhetens skull kan vi hänvisa till de relevanta inställningarna (vid actionId).\n          webhook.settings = webhook.reference[webhook.reference.actionId] || {};\n          return webhook;\n        });\n\n        if (state.selectedWebhook) {\n          state.selectedWebhook.settings =\n            state.selectedWebhook.reference[state.selectedWebhook.reference.actionId] || {};\n        }\n\n        return state;\n      });\n    });\n\n    window.form.addHook('set selectedAction', function() {\n      const webhook = window.form.custom.selectedWebhook;\n\n      if (!webhook || !webhook.reference.actionId) {\n        window.form.setState({\n          selectedAction: null\n        });\n\n        return;\n      }\n\n      window.form.setState(state => ({\n        selectedAction: state.actions.find(action => action.id === webhook.reference.actionId)\n      }));\n    });\n\n    window.form.addHook('set insertOptions', function() {\n      const webhook = window.form.custom.selectedWebhook;\n\n      if (!webhook) {\n        return;\n      }\n\n      window.form.setState(state => ({\n        insertOptions: window.insertOptions\n          .filter(option => {\n            //  Filtrera utifrån eventPrefix.\n            return !option.eventPrefix || !option.eventPrefix.length ||\n              option.eventPrefix.find(prefix =>\n                webhook.eventNames.find(name =>\n                name.startsWith(prefix.toLowerCase())));\n          })\n          .map(option => {\n            //  Vad som är sökbart i dropdown.\n            option.search = option.text;\n\n            return option;\n          })\n      }));\n    });\n\n    window.form.addHook('update editor', function() {\n      const webhook = window.form.custom.selectedWebhook;\n      if (!webhook) {\n        return;\n      }\n\n      const actionId = webhook.reference.actionId;\n\n      //  Valideringsmeddelanden är per åtgärd.\n      const validationMessages = Object.values(window.form.custom.validationMessageObject[actionId] || {})\n        .filter(message => message && typeof message === 'string');\n\n      window.form.setModal((modal, selector) => {\n        if (selector !== '.webhook-editor') {\n          return;\n        }\n\n        modal.go.disabled = validationMessages.length > 0;\n        modal.go.onClickCapture = () => {\n          if (validationMessages.length) {\n            window.form.notify(validationMessages[0]);\n          }\n        };\n\n        return modal;\n      })\n    });\n\n    window.form.setState(state => ({\n      validationMessageObject: state.validationMessageObject || {},\n      isDocsLinkVisible: location.hostname.includes('admin.')\n    }));\n\n  })();\n</script>","iconName":"project-diagram","iconColor":"#D50000","settingsHtml":"","description":"Gör något automatiskt när något händer i SaleSys.","rights":[],"components":[{"_id":"6a85b2eeaa26084906ce2e35","id":"Filter","name":null,"html":"<script>\n  //  Lista på alla möjliga filter, och vilka events som är relevanta för.\n  //  När du lägger till ett filter måste du implementera filtreringslogiken i salesys.se/api/action-presets/webhooks/.filter.php\n  window.eventFilterDefinitions = [\n    {\n      //  webhook.settings.filter kommer ha denna property \"userIds\".\n      property: \"userIds\",\n\n      //  Namn på filtret.\n      name: \"Användare\",\n\n      //  Om man kan markera flera saker i listan.\n      multiple: true,\n\n      //  Objekt man kan välja mellan. Måste vara en lista över { id: string; name: string; }\n      list: window.form.resources.users.filter(user => user.active && !user.suspension)\n        .map(user => ({ id: user.id, name: user.fullName }))\n    },\n    {\n      property: \"teamIds\",\n      name: \"Team\",\n      multiple: true,\n      list: window.form.resources.teams.map(team => ({ id: team.id, name: team.name }))\n    },\n    {\n      property: \"projectIds\",\n      name: \"Projekt\",\n      multiple: true,\n      list: window.form.resources.projects.map(project => ({ id: project.id, name: project.name }))\n    },\n    {\n      property: \"anyOrderTagId\",\n      name: \"Taggar\",\n      multiple: true,\n\n      //  Man kan välja \"en av\", \"alla\", \"ej en av\" eller \"ej alla\"\n      anyAndAll: true,\n\n      //  Vilka events som filtret är relevant för.\n      eventPrefix: [\"order\", \"after-order-work\"],\n      list: window.form.resources.orderTags.map(tag => ({ id: tag.id, name: tag.name }))\n    },\n    {\n      property: \"callTagIds\",\n      name: \"Taggar\",\n      multiple: true,\n      eventPrefix: [\"call\"],\n      list: window.form.resources.callTags.map(tag => ({ id: tag.id, name: tag.name }))\n    }\n  ];\n\n  window.eventDelayDefinitions = [\n    {\n      //  Din PHP-fil ska ha samma namn som id här.\n      id: \"afterMidnight\",\n      //  Beskrivning som visas i dropdown.\n      name: \"efter midnatt\"\n    },\n    {\n      id: \"beforeMidnight\",\n      name: \"innan midnatt\"\n    }\n  ];\n\n</script>\n\n<div style=\"display: flex; flex-direction: column;\">\n  <div className=\"label\">Filter</div>\n  <div style=\"display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;\">\n    {{#each custom.visibleFilters}}\n    <SecondaryButton\n      {{#if isActive}} weight=\"primary\" icon=\"pen\" {{else}} weight=\"simple\" icon=\"plus\" {{/if}}\n      size=\"compact\"\n      onClick=\"window.onSelectFilter('{{property}}')\">\n      {{name}}\n    </SecondaryButton>\n    {{/each}}\n    \n    <SecondaryButton\n      {{#if custom.isDateFilterActive}} weight=\"primary\" {{else}} weight=\"simple\" {{/if}}\n      icon=\"calendar\"\n      size=\"compact\"\n      onClick=\"window.form.setState({ selectedFilter: null, selectedFilterSection: 'datetime' })\">\n      Tid\n    </SecondaryButton>\n  </div>\n\n  <Transition lock=\"{{custom.selectedFilter.property}} {{custom.selectedFilterSection}}\">\n    {{#ifEq custom.selectedFilterSection \"datetime\"}}\n    <div style=\"display: flex; flex-direction: row; gap: 8px; padding-top: 24px;\">\n      <div style=\"position: relative; top: 8px; white-space: nowrap;\">Utför</div>\n      <div style=\"display: flex; flex-direction: column; gap: 16px; flex-grow: 1;\">\n        <div style=\"display: flex; flex-direction: row; gap: 8px\">\n          <Field label=\"\" type=\"number\"\n            value=\"{{either custom.selectedWebhook.reference.delayValue 0}}\"\n            onChange=\"window.onWebhookReferenceChange({ delayValue: value })\"\n            max=\"100\"\n            min=\"0\"\n            containerStyle=\"width: 20%\">\n          </Field>\n          \n          <Field label=\"\"\n            value=\"{{either custom.selectedWebhook.reference.delayUnit \"minute\"}}\"\n            onChange=\"window.onWebhookReferenceChange({ delayUnit: value })\"\n            containerStyle=\"flex-grow: 1\">\n            <option value=\"minute\">minuter</option>\n            <option value=\"hour\">timmar</option>\n            <option value=\"day\">dagar</option>\n            <option value=\"week\">veckor</option>\n          </Field>\n        </div>\n\n        <Field label=\"\"\n          containerStyle=\"width: 100%\"\n          value=\"{{either custom.selectedWebhook.reference.delayId \"afterEvent\"}}\"\n          onChange=\"window.onWebhookReferenceChange({ delayId: value })\">\n          <option value=\"afterEvent\">efter händelsen sker</option>\n          {{#each custom.eventDelayDefinitions}}\n            <option value=\"{{id}}\">{{name}}</option>\n          {{/each}}\n        </Field>\n      </div>\n    </div>\n\n    {{else}}\n      {{#if custom.selectedFilter}}\n        <Field\n          label=\"Filter - {{custom.selectedFilter.name}}\"\n          select-type=\"switches\"\n          containerStyle=\"width: 100%; padding-top: 24px;\">\n\n          {{#each custom.selectedFilter.list}}\n          <option\n            value=\"{{id}}\"\n            {{#ifIncludes (lookup ../custom.selectedWebhook.settings.filter ../custom.selectedFilter.property) id}} selected {{/ifIncludes}}\n            onClick=\"window.onFilterListItemClick('{{id}}')\">\n            {{name}}\n          </option>\n          {{/each}}\n\n        </Field>\n      {{/if}}\n    {{/ifEq}}\n  </Transition>\n</div>\n\n<script>\n  window.onSelectFilter = function(property) {\n    window.form.setState(state => {\n      state.selectedFilterSection = null;\n\n      if (state.selectedFilter && state.selectedFilter.property === property) {\n        state.selectedFilter = null;\n      }\n\n      state.selectedFilter = window.eventFilterDefinitions.find(def => def.property === property);\n      return state;\n    });\n  }\n\n  window.onFilterListItemClick = function(value) {\n    const selectedFilter = window.form.custom.selectedFilter;\n    const property = selectedFilter.property;\n\n    window.onWebhookSettingsChange(settings => {\n      settings.filter = settings.filter || {};\n\n      if (!Array.isArray(settings.filter[property])) {\n        settings.filter[property] = [];\n      }\n\n      if (settings.filter[property].includes(value)) {\n        //  Ta bort.\n        settings.filter[property] = settings.filter[property].filter(v => v !== value);\n      \n      } else if (selectedFilter.multiple) {\n        //  Lägg till.\n        settings.filter[property].push(value);\n\n      } else {\n        //  Lägg till.\n        settings.filter[property] = [value];\n      }\n\n      return settings;  \n    });\n  }\n  \n  window.form.addHook('set selectedFilter', function () {\n    window.form.setState(state => {\n      if (state.selectedFilter && !state.visibleFilters.find(filter => filter.property === state.selectedFilter.property)) {\n        //  Filtret finns inte längre att välja.\n        return { selectedFilter: null };\n      }\n\n      return { };\n    })\n  })\n\n  window.form.addHook('set visibleFilters', function () {\n    const webhook = window.form.custom.selectedWebhook;\n\n    if (!webhook) {\n      return;\n    }\n\n    const filters = window.eventFilterDefinitions;\n\n    window.form.setState(state => ({\n      visibleFilters: window.eventFilterDefinitions\n        .filter(definition => {\n          if (!definition.list || !definition.list.length) {\n            return false;\n          }\n\n          return !definition.eventPrefix || webhook.eventNames\n            .find(name => definition.eventPrefix.find(prefix => name.startsWith(prefix)))\n        })\n        .map(definition => {\n          if (webhook.settings.filter) {\n            const filterValue = webhook.settings.filter[definition.property];\n            definition.isActive = filterValue && filterValue.length > 0;\n          }\n\n          return definition;\n        })\n    }));\n  });\n\n  window.form.setState({\n    eventDelayDefinitions: window.eventDelayDefinitions\n  });\n\n  window.form.addHook('set isDateFilterActive', function() {\n    const webhook = window.form.custom.selectedWebhook;\n\n    if (!webhook) {\n      return;\n    }\n\n    window.form.setState({\n      isDateFilterActive: \n        Boolean(webhook.reference.delayValue && webhook.reference.delayValue !== \"0\") ||\n        (webhook.reference.delayId && webhook.reference.delayId !== \"afterEvent\")\n    });\n  });\n</script>","organizationIds":[]},{"_id":"6a85b2eeaa26084906ce2e36","id":"Events","name":"Händelser","html":"<style>\n  .event-list {\n    display: flex;\n    flex-direction: row;\n    gap: 18px 24px;\n    flex-wrap: wrap;\n  }\n\n  .event-list-item {\n    display: flex;\n    flex-direction: row;\n    gap: 24px;\n    align-items: center;\n    border: 1px solid var(--color-background-3);\n    border-radius: 8px;\n    padding: 16px;\n    width: 280px;\n  }\n\n  .event-list-item > div {\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n  }\n</style>\n\n<div style=\"display: flex; flex-direction: column; gap: 8px;\">\n  <div>\n    <h2 style=\"margin: 0; display: flex; flex-direction: row; gap: 8px; align-items: center;\">\n      Senaste händelserna\n\n      <Tooltip tooltip=\"Visar händelser från samtliga webhooks, även egna integrationer. Uppdateras automatiskt.\" delay=\"false\">\n        <div class=\"far fa-question-circle\" style=\"font-size: 15px; position: relative; top: 3px; color: var(--color-text-3)\"></div>\n      </Tooltip>\n\n      <div style=\"display: flex; flex-direction: row; gap: 8px; margin-left: 16px;\">\n        <!-- Filter för händelser. -->\n        <SecondaryButton\n          size=\"compact\"\n          style=\"max-width: 240px\"\n          onClick=\"window.openExecutionFilters()\"\n          {{#if custom.executionsFilter.date.active}} weight=\"primary\" icon=\"pen\" {{else}} icon=\"plus\" {{/if}}>\n          {{custom.executionsFilter.date.label}}\n        </SecondaryButton>\n\n        <SecondaryButton\n          size=\"compact\"\n          style=\"max-width: 240px\"\n          onClick=\"window.openExecutionFilters()\"\n          {{#if custom.executionsFilter.webhooks.active}} weight=\"primary\" icon=\"pen\" {{else}} icon=\"plus\" {{/if}}>\n          {{custom.executionsFilter.webhooks.label}}\n        </SecondaryButton>\n      </div>\n    </h2>\n    <!-- Inbyggd spinner i Salesys. -->\n    <div\n      style=\"display: flex; flex-direction: align-items: center; font-size: 15px; color: var(--color-text-3)\">\n      {{#ifEq custom.executions.length 0}}\n        {{#if custom.anyExecutionFilterActive}}\n        Inga händelser som matchar filtret.\n        {{else}}\n        Inga händelser än.\n        {{/if}}\n      {{/ifEq}}\n\n      <div className=\"task-indicator tiny animated\"\n        style=\"{{#ifNot custom.isExecutionSpinnerVisible}} visibility: hidden {{/ifNot}}\">\n        <div data-spinner></div>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"event-list\">\n    {{#each custom.executions}}\n    <Tooltip {{#if error}} tooltip=\"{{either error.localizedMessage error.cause.localizedMessage 'Något gick snett'}}\" {{/if}} className=\"event-list-item\">\n\n      {{#if error}}\n      <div\n        style=\"font-size: 15px; width: 14px; overflow: visible; color: var(--color-danger);\"\n        class=\"fas fa-exclamation-triangle\">\n      </div>\n      {{else}}\n      <div\n        style=\"font-size: 15px; width: 14px; overflow: visible; color: var(--color-accent);\"\n        class=\"fas fa-{{either webhook.reference.icon 'wrench'}}\">\n      </div>\n      {{/if}}\n\n      <div style=\"display: flex; flex-direction: column; flex-grow: 1;\">\n        <div style=\"font-size: 15px; color: var(--color-text-2); font-weight: 400;\">\n          {{webhook.reference.name}}\n        </div>\n        <div>\n          <Tooltip tooltip=\"{{formatDate date \"D MMM YYYY HH:mm\"}}\"\n            style=\"font-size: 14px; color: var(--color-text-3); font-weight: 400;\">\n            {{simpleDate}}\n          </Tooltip>\n        </div>\n      </div>\n\n      {{#if hasPresentation}}\n      <div style=\"display: flex; align-items: center; justify-content: center;\">\n        <Tooltip delay=\"false\" tooltip=\"Visa mer information om händelsen.\">\n          <SecondaryButton icon=\"ellipsis-v\" onClick=\"window.openExecution('{{id}}', { subtitle: '{{formatDate date \"D MMM YYYY HH:mm\"}}' })\">\n          </SecondaryButton>\n        </Tooltip>\n      </div>\n      {{/if}}\n    </Tooltip>\n    {{/each}}\n  </div>\n</div>\n\n<div class=\"execution-editor\" style=\"display: flex; flex-direction: column; gap: 24px;\" modal>\n\n  {{#if custom.selectedExecution.result.text}}\n  <div style=\"font-size: 14px; color: var(--color-text-3); text-align: center;\">\n    {{custom.selectedExecution.result.text}}\n  </div>\n  {{/if}}\n  \n  {{#each custom.selectedExecution.fields}}\n  <Field containerStyle=\"width: 100%;\" label=\"{{name}}\" value=\"{{value}}\" readonly>\n  </Field>\n  {{/each}}\n\n</div>\n\n<div class=\"execution-filters\" style=\"display: flex; flex-direction: column; gap: 24px;\" modal>\n  <div style=\"display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px\">\n    <Field\n      containerStyle=\"width: 100%;\"\n      label=\"Från\"\n      type=\"date\"\n      value=\"{{custom.executionQuery.after}}\"\n      onChange=\"window.onExecutionQueryChange({ after: value })\">\n    </Field>\n\n    <Field\n      containerStyle=\"width: 100%;\"\n      label=\"Till\"\n      type=\"date\"\n      value=\"{{custom.executionQuery.before}}\"\n      onChange=\"window.onExecutionQueryChange({ before: value })\">\n    </Field>\n  </div>\n\n  <Field\n    containerStyle=\"width: 100%\"\n    label=\"Webhooks\"\n    select-type=\"switches\">\n    \n    {{#each custom.webhooks}}\n    <option\n      {{#ifIncludes ../custom.executionQuery.webhookIds id}} selected {{/ifIncludes}}\n      onChange=\"window.onExecutionQueryChange(query => ({ webhookIds: window.toggle(query.webhookIds, '{{id}}') }))\"\n      value=\"{{id}}\">\n      {{either reference.name \"Webhook\"}}\n    </option>\n    {{/each}}\n  \n  </Field>\n</div>\n\n<script id=\"webhook-executions-js\">\n\n  window.toSimpleDate = function(date) {\n    let millis = Date.now() - date.getTime();\n    let seconds = Math.floor(millis / 1000);\n    let minutes = Math.floor(seconds / 60);\n    let hours = Math.floor(minutes / 60);\n    let days = Math.floor(hours / 24);\n    let weeks = Math.floor(days / 7);\n    let months = Math.floor(weeks / 4);\n    \n    if (millis < 0) {\n      return \"\";\n    }\n\n    if (months === 1) {\n      return `${months} månad sen`;\n    \n    } else if (months > 1) {\n      return `${months} månader sen`;\n    \n    } else if (weeks === 1) {\n      return `${weeks} vecka sen`;\n    \n    } else if (weeks > 1) {\n      return `${weeks} veckor sen`;\n\n    } else if (days === 1) {\n      return `${days} dag sen`;\n    \n    } else if (days > 1) {\n      return `${days} dagar sen`;\n    \n    } else if (hours === 1) {\n      return `${hours} timme sen`;\n    \n    } else if (hours > 1) {\n      return `${hours} timmar sen`;\n    \n    } else if (minutes === 1) {\n      return `${minutes} minut sen`;\n    \n    } else if (minutes > 1) {\n      return `${minutes} minuter sen`;\n\n    } else if (seconds > 30) {\n      return `${seconds} sekunder sen`;\n    }\n\n    return `Nu precis`;\n  }\n\n  window.refreshExecutions = async function () {\n    window.form.setState(state =>{\n      state.isRefreshingExecutions = true;\n      state.isExecutionSpinnerVisible = !state.hasRefreshedExecutionsOnce;\n      return state;\n    });\n\n    const count = 100;\n\n    try {\n      const url = new URL(\"/api/events/webhook-executions-v1\", document.location.origin);\n      url.searchParams.set('count', count);\n      url.searchParams.set('result.isSkipped', 'false');  //  Filtera bort \n\n      const query = window.form.custom.executionQuery;\n\n      for (const name in query) {\n        if (!query[name]) {\n          continue;\n        }\n        \n        if (Array.isArray(query[name]) && !query[name].length) {\n          continue;\n        }\n\n        if (Array.isArray(query[name])) {\n          url.searchParams.set(name, query[name].join(','));\n\n        } else {\n          url.searchParams.set(name, String(query[name]));\n        }\n      }\n\n      //  Sålänge filtret inte ändrats eller vi hämtar händelser för första gången så ska vi bara hämta nya händelser.\n      if (window.form.custom.hasRefreshedExecutionsOnce && window.form.custom.executions.length) {\n        const latest = window.form.custom.executions.reduce((latest, exec) =>\n          exec.date > latest ? exec.date : latest,\n          window.form.custom.executions[0].date\n        );\n        \n        url.searchParams.set('after', latest.toISOString());        \n      }\n\n      const response = await window.fetch(\n        url,\n        {\n          headers: {\n            \"Authorization\": \"Bearer \" + window.getAccessToken(),\n            \"Content-type\": \"application/json\"\n          },\n          signal: window.autoRefreshExecutionsController ?\n            window.autoRefreshExecutionsController.signal : null,\n        }\n      );\n\n      const result = await response.json();\n\n      if (!response.ok) {\n        throw result;\n      }\n\n      const executions = result.filter(execution => {\n        //  Filtrera bort som hoppat över.\n        if (execution.result && execution.result.isSkipped) {\n          return false;\n        }\n\n        return true;\n\n      }).map(execution => {\n        execution.result = execution.result || {};\n        \n        //  result.webhook är historiska webhooken.\n        execution.webhook = execution.result.webhook ||\n          window.form.custom.webhooks.find(webhook =>\n            webhook.id === execution.webhookId);\n\n        execution.date = new Date(execution.date);\n        execution.simpleDate = window.toSimpleDate(execution.date);\n\n        if (execution.result) {\n          /*\n          Exempel på result.fields:\n          {\n            \"Från\": \"JOHAN\",\n            \"Till\": \"0721521129\",\n          }\n          */\n\n          //  Ange result.text för ytterligare text:\n          /*\n          fields: {\n            \"Från\": \"JOHAN\",\n            \"Till\": \"0721521129\",\n          },\n          text: \"SMS:et skickades via Telnect\"\n          */\n\n          execution.fields = Object.entries(execution.result.fields || {})\n            .map(([name, value]) => ({ name, value }));\n\n          execution.hasPresentation = execution.fields.length > 0;\n        }\n\n        if (/^4|5/.test(String(execution.status))) {\n          execution.error = { localizedMessage: \"Något gick snett\" };\n        }\n\n        return execution;\n      });\n\n      window.form.setState(state => {\n        if (state.hasRefreshedExecutionsOnce) {\n          const executionIds = executions.map(execution => execution.id);\n          state.executions = state.executions.filter(execution => !executionIds.includes(execution.id)); // Ta bort dubletter.\n\n          //  Vi har hämtat fler eller de senaste, lägg på dom.\n          executions.forEach(execution => state.executions.push(execution));\n        \n        } else {\n          state.executions = executions;\n        }\n\n        state.executions = state.executions.map(execution => {\n          execution.simpleDate = window.toSimpleDate(execution.date);\n          return execution;\n        });\n\n        state.executions.sort((a, b) => b.date.getTime() - a.date.getTime()).slice(0, count);\n\n        return state;\n      });\n\n    } catch (error) {\n      console.error(\"Error refreshing webhook executions\", error);\n\n      if (error.name === \"AbortError\") {\n        //  AbortError betyder att den inte hann bli färdig innan vi hämtar händelser på nytt.\n        return;\n      }\n\n      window.form.notify(error.localizedMessage || \"Något gick snett\");\n    }\n\n    window.form.setState(state => {\n      state.isRefreshingExecutions = false;\n      state.hasRefreshedExecutionsOnce = true;\n      state.isExecutionSpinnerVisible = false;\n\n      return state;\n    });\n  }\n\n  window.openExecution = function(id, options) {\n    const execution = window.form.custom.executions.find(execution => execution.id === id);\n    if (!execution) {\n      console.log(\"Unknown execution\", id);\n      return;\n    }\n\n    options = options || {};\n\n    window.form.setState({ selectedExecution: execution });\n\n    window.form.openModal('.execution-editor', {\n      title: execution.webhook.reference.name,\n      size: 'medium',\n      cancel: {},\n      onHidden: () => window.form.setState({ selectedExecution: null }),\n      ...options,\n    });\n  }\n\n  window.autoRefreshExecutions = function () {\n    if (!document.getElementById('webhook-executions-js').isConnected) {\n      return;\n    }\n\n    if (window.autoRefreshExecutionsController) {\n      //  Avbryt förra inläsningen (vid ändring av filter)\n      window.autoRefreshExecutionsController.abort();\n    }\n\n    window.autoRefreshExecutionsController = new AbortController();\n    clearTimeout(window.autoRefreshExecutionsId);\n\n    if (document.hidden) {\n      window.autoRefreshExecutionsId = setTimeout(window.autoRefreshExecutions, 500);\n      return;\n    }\n\n    return window.refreshExecutions().finally(() => {\n      window.autoRefreshExecutionsId = setTimeout(window.autoRefreshExecutions, 5500)\n    });\n  }\n\n  //  Vänta på att alla funktioner är på plats.\n  setTimeout(() => {\n    window.autoRefreshExecutions()\n    }, 150);\n\n  window.openExecutionFilters = function() {\n    window.form.openModal('.execution-filters', {\n      title: 'Filtrera händelser',\n      size: 'medium',\n      hideBackdrop: true,\n      cancel: {},\n      separated: {\n        weight: 'danger',\n        children: 'Rensa',\n        onClick: () => {\n          window.form.setState({ executionQuery: {} });\n          return true;\n        }\n      },\n    });\n  }\n\n  window.onExecutionQueryChange = function(update) {\n    window.form.setState(state => {\n      state.executionQuery = window.applyUpdate(update, state.executionQuery || {});\n      state.hasRefreshedExecutionsOnce = false; //  Visa spinner igen.\n\n      setTimeout(() => window.autoRefreshExecutions(), 150);\n\n      return state;\n    });\n  }\n\n  window.form.addHook('set filters', function() {\n    //  Uppdaterar executionFilters som hämtar från executionQuery.\n    //  executionFilters är mer visuell information för knappar och executionQuery är vad vi skickar till API.\n    window.form.setState(state => {\n      const query = state.executionQuery = state.executionQuery || {};\n      query.webhookIds = query.webhookIds || [];\n\n      state.executionsFilter = state.executionsFilter || {};\n      const date = state.executionsFilter.date = state.executionsFilter.date || {};\n      const webhooks = state.executionsFilter.webhooks = state.executionsFilter.webhooks || {};\n\n      //  Om filtret är aktivt eller ej.\n      date.active = Boolean(query.after || query.before);\n      //  Knapptexten\n      if (query.after && query.before) {\n        date.label = window.form.formatDate(query.after, 'D MMM') + ' - ' + window.form.formatDate(query.before, 'D MMM');\n      \n      } else if (query.after || query.before) {\n        date.label = window.form.formatDate(query.after || query.before, 'D MMM');\n\n      } else {\n        //  Ej aktiv.\n        date.label = 'Datum';\n      }\n\n      webhooks.active = query.webhookIds.length > 0;\n      webhooks.label = 'Webhooks';\n\n      const selectedWebhooks = webhooks.active ?\n        window.form.custom.webhooks.filter(webhook => query.webhookIds.includes(webhook.id)) : [];\n\n      if (selectedWebhooks.length) {\n        webhooks.label = selectedWebhooks.map(webhook => webhook.reference.name || \"Webhook\").join(', ');\n      }\n\n      //  Om du lägger till fler filter måste du kolla .active här.\n      state.anyExecutionFilterActive = date.active || webhooks.active;\n\n      return state;\n    });\n  });\n</script>","organizationIds":[]},{"_id":"6a85b2eeaa26084906ce2e37","id":"Skicka_SMS","name":"Skicka SMS","html":"<div style=\"display: flex; flex-direction: column; gap: 24px;\">\n  <div style=\"display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px\">\n    <Field\n      label=\"Avsändare\"\n      containerStyle=\"width: 100%\"\n      value=\"{{custom.selectedWebhook.settings.from}}\"\n      onChange=\"window.onWebhookSettingsChange({ from: value })\"></Field>\n\n    <Field\n      label=\"Till\"\n      containerStyle=\"width: 100%\"\n      value=\"{{custom.selectedWebhook.settings.to}}\"\n      onChange=\"window.onWebhookSettingsChange({ to: value })\">\n    </Field>\n  </div>\n\n  <Field\n    label=\"Meddelande\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.text}}\"\n    rows=\"2\"\n    maxRows=\"8\"\n    multiline=\"true\"\n    placeholder=\"Skriv ett meddelande\"\n    onChange=\"window.onWebhookSettingsChange({ text: value })\"\n    default-insert-target></Field>\n</div>\n\n<script>\n  //  Ditt komponent-ID.\n  window.form.addHook('Skicka_SMS', () => {\n\n    //  Sätt valideringsmeddelanden.\n    //  Måste kolla nuvarande action!\n    if (window.getSelectedActionId() !== 'Skicka_SMS') {\n      return;\n    }\n    \n    //  Konfiguration för detta verktyg.\n    window.onWebhookSettingsChange({\n      //  En 'anonym' access token skickas till php.\n      //  Här är vilka rättigheter som den anonyma användaren ska ha. \n      authRights: ['sms.sendConversation']\n    });\n\n    //  Ställ in vilka fält som visas när man klickar förhandsgranska.\n    window.getPreviewFields = result => [\n      { name: \"Avsändare\", value: result.from },\n      { name: \"Till\", value: result.to },\n      { name: \"Text\", value: result.text },\n    ];\n\n    const settings = window.form.custom.selectedWebhook.settings;\n\n    if (!settings.from) {\n      window.setActionValidation('from', 'Ange avsändare');\n      \n    } else {\n      window.removeActionValidation('from');\n    }\n\n    if (!settings.to) {\n      window.setActionValidation('to', 'Ange mottagare');\n      \n    } else {\n      window.removeActionValidation('to');\n    }\n\n    if (!settings.text) {\n      window.setActionValidation('text', 'Ange meddelande');\n      \n    } else {\n      window.removeActionValidation('text');\n    }\n  });\n</script>","organizationIds":[]},{"_id":"6a86f2f68ede5138d3a81f98","id":"Skicka_epost","name":"Skicka e-post","html":"<script>\n  //  Konfiguration för detta verktyg.\n</script>\n\n<div style=\"display: flex; flex-direction: column; gap: 24px;\">\n  <Field\n    label=\"Avsändare namn\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.senderName}}\"\n    onChange=\"window.onWebhookSettingsChange({ senderName: value })\"></Field>\n\n  <Field\n    label=\"Avsändare adress\"\n    containerStyle=\"width: 100%\"\n    placeholder=\"info@exempel.se\"\n    value=\"{{custom.selectedWebhook.settings.senderAddress}}\"\n    onChange=\"window.onWebhookSettingsChange({ senderAddress: value })\">\n  </Field>\n\n  <div style=\"text-align: center; font-size: 14px; position: relative; top: -16px;\">\n    Observera att domänet behöver vara verifierat av SaleSys kundtjänst.\n  </div>\n\n  <Field\n    label=\"Till\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.to}}\"\n    onChange=\"window.onWebhookSettingsChange({ to: value })\">\n  </Field>\n\n  <div style=\"text-align: center; font-size: 14px; position: relative; top: -16px;\">\n    Ange flera adresser kommaseparerat.\n  </div>\n\n  <Field\n    label=\"Ämnesrad\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.subject}}\"\n    onChange=\"window.onWebhookSettingsChange({ subject: value })\">\n  </Field>\n\n  <Field\n    label=\"Meddelande\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.body}}\"\n    rows=\"3\"\n    maxRows=\"8\"\n    multiline=\"true\"\n    placeholder=\"Skriv ett meddelande\"\n    onChange=\"window.onWebhookSettingsChange({ body: value })\"\n    default-insert-target></Field>\n</div>\n\n<script>\n  window.form.addHook('Skicka_epost set validation', () => {\n    if (window.getSelectedActionId() !== 'Skicka_epost') {\n      return;\n    }\n\n    const settings = window.form.custom.selectedWebhook.settings;\n\n    window.setActionValidation('sender', settings.senderAddress && settings.senderName ? null : 'Ange avsändare namn och adress');\n    window.setActionValidation('subject', settings.subject ? null : 'Ange ämnesrad');\n    window.setActionValidation('body', settings.body ? null : 'Ange meddelande');\n    window.setActionValidation('to', settings.to ? null : 'Ange mottagare');\n  });\n</script>","organizationIds":[]},{"_id":"6a870aa50472491704da034d","id":"http","name":"Integration","html":"<div style=\"display: flex; flex-direction: column; gap: 24px;\">\n  <Field\n    label=\"URL\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.url}}\"\n    placeholder=\"https://example.org/webhook\"\n    onChange=\"window.onWebhookSettingsChange({ url: value })\"\n    default-insert-target></Field>\n</div>\n\n<script>\n  window.form.addHook('http set validation', () => {\n    if (window.getSelectedActionId() !== 'http') {\n      return;\n    }\n\n    const settings = window.form.custom.selectedWebhook.settings;\n\n    window.setActionValidation('url', settings.url ? null : 'Ange en URL');\n  });\n</script>","organizationIds":[]},{"_id":"6a88341111260e054aca34ba","id":"Skicka_notifikation","name":"Skicka notifikation","html":"<div style=\"display: flex; flex-direction: column; gap: 24px;\">\n  <Field\n    label=\"Till\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.to}}\"\n    onChange=\"window.onWebhookSettingsChange({ to: value })\">\n  {{#each resources.users}}\n    <option value=\"{{id}}\">{{fullName}}</option>\n  {{/each}}\n  </Field>\n\n  <Field\n    label=\"Titel\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.title}}\"\n    placeholder=\"Ange titel på notifikationen\"\n    onChange=\"window.onWebhookSettingsChange({ title: value })\"\n    default-insert-target></Field>\n\n  <Field\n    label=\"Meddelande\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.text}}\"\n    placeholder=\"Skriv ett meddelande\"\n    onChange=\"window.onWebhookSettingsChange({ text: value })\"\n    default-insert-target></Field>\n\n  <Field\n    label=\"Länk (frivilligt)\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.link}}\"\n    onChange=\"window.onWebhookSettingsChange({ link: value })\">\n  </Field>\n\n<Field\n    prefix=\"https://\"\n    label=\"Ikon\"\n    containerStyle=\"width: 100%\"\n    value=\"{{custom.selectedWebhook.settings.icon}}\"\n    onChange=\"window.onWebhookSettingsChange({ icon: value })\">\n      <option selected value=\"envelope\">Brev</option>\n          <option value=\"star\">Stjärna</option>\n          <option value=\"magic\">Magi</option>\n          <option value=\"exclamation-triangle\">Varning</option>\n          <option value=\"check\">Bock</option>\n    </Field>\n\n<CheckboxField label=\"Visa konfetti\"\n  {{#if custom.selectedWebhook.settings.showConfetti}} checked {{/if}}\n  onChange=\"window.onWebhookSettingsChange({ showConfetti: checked })\">\n</CheckboxField>\n</div>\n\n<script>\n\n  window.form.addHook(\"handleUpdateLink\", () => {\n    const link = window.form.custom.selectedWebhook.settings.link;\n  \n    if (!link) {\n      window.form.setState({ linkText: null })\n      return;\n    }\n  \n    let linkText = link;\n  \n    if (!link.startsWith(\"https://\") && !link.startsWith(\"http://\")) {\n      linkText = \"https://\" + link;\n    }\n  \n    window.onWebhookSettingsChange({ linkText })\n  });\n\n  //  Ditt komponent-ID.\n  window.form.addHook('Skicka_notifikation', () => {\n\n    //  Sätt valideringsmeddelanden.\n    //  Måste kolla nuvarande action!\n    if (window.getSelectedActionId() !== 'Skicka_notifikation') {\n      return;\n    }\n\n    //  Konfiguration för detta verktyg.\n    window.onWebhookSettingsChange({\n      //  En 'anonym' access token skickas till php.\n      //  Här är vilka rättigheter som den anonyma användaren ska ha.\n      authRights: ['users.createUserMessage']\n    });\n\n    //  Ställ in vilka fält som visas när man klickar förhandsgranska.\n    window.getPreviewFields = result => [\n      { name: \"Till\", value: result.to },\n      { name: \"Titel\", value: result.title },\n      { name: \"Text\", value: result.text },\n      { name: \"Länk\", value: result.linkText },\n      { name: \"Ikon\", value: result.icon },\n      { name: \"Visa konfetti\", value: result.showConfetti === true ? \"Ja\" : \"Nej\" },\n    ];\n\n    const settings = window.form.custom.selectedWebhook.settings;\n\n    if (!settings.to) {\n      window.setActionValidation('to', 'Välj en mottagare');\n    } else {\n      window.removeActionValidation('to');\n    }\n\n    if (!settings.title) {\n      window.setActionValidation('title', 'Ange titel');\n    } else {\n      window.removeActionValidation('title');\n    }\n\n    if (!settings.text) {\n      window.setActionValidation('text', 'Ange meddelande');\n    } else {\n      window.removeActionValidation('text');\n    }\n\n  });\n</script>","organizationIds":[]}]}]