commits
tags
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FORTIS</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;font-family:'Helvetica Neue',Arial,sans-serif}
:root{
--accent:#E85D26;
--accent2:#2A6AE8;
--accent-light:#FEF0EB;
--bg:#F2F1ED;
--card:#FFFFFF;
--text:#141414;
--muted:#888880;
--border:rgba(0,0,0,0.09);
--nav:#FFFFFF;
--header:#141414;
--header-text:#FFFFFF;
--outer:#E8E6E0;
--nav-h:66px;
--radius:14px;
--radius-sm:10px;
}
html,body{height:100%;background:var(--outer)}
#root{max-width:420px;margin:0 auto;min-height:100vh;background:var(--bg);position:relative;overflow:hidden;box-shadow:0 0 40px rgba(0,0,0,0.15);transition:background 0.3s}
/* ── SPLASH ─────────────────────────────── */
#splash{position:absolute;inset:0;background:#0F0F0F;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100}
.splash-ring{position:relative;width:140px;height:140px;margin-bottom:32px}
.splash-ring svg{animation:spin 3s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.splash-logo{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
.splash-name{font-size:32px;font-weight:800;letter-spacing:0.12em;color:#FFF;margin-bottom:2px}
.splash-sub{font-size:10px;letter-spacing:0.25em;color:#666;text-transform:uppercase}
.splash-progress{position:absolute;bottom:48px;left:48px;right:48px;height:2px;background:#222;border-radius:1px;overflow:hidden}
.splash-fill{height:100%;background:var(--accent);border-radius:1px;width:0;transition:width 0.1s linear}
/* ── ONBOARDING ──────────────────────────── */
#onboarding{position:absolute;inset:0;background:#FFF;overflow-y:auto;display:none}
.ob-header{padding:56px 28px 24px;background:#FFF}
.ob-steps{display:flex;gap:6px;margin-bottom:28px}
.ob-step{height:3px;flex:1;border-radius:2px;background:#EAEAEA;transition:background 0.3s}
.ob-step.done{background:var(--accent)}
.ob-title{font-size:26px;font-weight:800;color:var(--text);line-height:1.2;margin-bottom:6px}
.ob-desc{font-size:14px;color:var(--muted);line-height:1.5}
.ob-body{padding:0 28px 120px}
.field{margin-bottom:18px}
.field label{display:block;font-size:12px;font-weight:600;letter-spacing:0.06em;color:var(--muted);text-transform:uppercase;margin-bottom:7px}
.field input,.field select{width:100%;padding:14px 16px;border:1.5px solid #E8E8E8;border-radius:var(--radius-sm);font-size:16px;color:var(--text);background:#FFF;-webkit-appearance:none;outline:none;transition:border-color 0.2s}
.field input:focus,.field select:focus{border-color:var(--accent)}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.radio-group{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.radio-opt{padding:12px 8px;border:1.5px solid #E8E8E8;border-radius:var(--radius-sm);text-align:center;font-size:13px;cursor:pointer;transition:all 0.2s;color:var(--text)}
.radio-opt.active{border-color:var(--accent);background:var(--accent-light);color:var(--accent);font-weight:600}
.macro-preview{background:#F7F7F5;border-radius:var(--radius);padding:20px;margin-top:8px}
.macro-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #EAEAEA}
.macro-row:last-child{border-bottom:none}
.macro-name{font-size:14px;color:var(--muted)}
.macro-val{font-size:18px;font-weight:700;color:var(--text)}
.macro-unit{font-size:12px;color:var(--muted);font-weight:400}
.ob-footer{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:420px;padding:16px 28px 32px;background:#FFF;border-top:1px solid #EAEAEA}
.btn-primary{width:100%;padding:17px;background:var(--accent);color:#FFF;font-size:16px;font-weight:700;border:none;border-radius:var(--radius);cursor:pointer;letter-spacing:0.02em;transition:opacity 0.2s}
.btn-primary:active{opacity:0.85}
.btn-ghost{width:100%;padding:13px;background:none;color:var(--muted);font-size:14px;border:none;cursor:pointer;margin-top:8px}
.done-screen{text-align:center;padding:48px 28px}
.done-icon{width:80px;height:80px;background:var(--accent-light);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 24px;font-size:36px}
.done-title{font-size:28px;font-weight:800;color:var(--text);margin-bottom:8px}
.done-sub{font-size:15px;color:var(--muted);line-height:1.6}
/* ── MAIN APP ────────────────────────────── */
#app{position:absolute;inset:0;display:none;flex-direction:column}
.screen{flex:1;overflow-y:auto;padding-bottom:calc(var(--nav-h) + 16px)}
/* ── BOTTOM NAV ─────────────────────────── */
.bottom-nav{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:420px;height:var(--nav-h);background:var(--nav);border-top:1px solid var(--border);display:flex;align-items:center;z-index:50;padding:0 8px}
.nav-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:8px 4px;cursor:pointer;border:none;background:none;transition:color 0.2s}
.nav-icon{font-size:20px;line-height:1}
.nav-label{font-size:10px;letter-spacing:0.04em;font-weight:500;color:var(--muted)}
.nav-item.active .nav-label{color:var(--accent)}
.nav-item.active .nav-icon{filter:none}
/* ── HOME / DASHBOARD ────────────────────── */
.home-header{padding:52px 22px 20px;background:var(--header)}
.greeting-sub{font-size:12px;color:#888;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:4px}
.greeting{font-size:26px;font-weight:800;color:#FFF;line-height:1.2}
.streak-pill{display:inline-flex;align-items:center;gap:5px;background:#2A2A2A;border-radius:20px;padding:5px 12px;margin-top:12px;font-size:12px;color:#FFF;font-weight:600}
.home-content{padding:18px 16px}
.section-title{font-size:12px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin-bottom:10px}
.card{background:var(--card);border-radius:var(--radius);padding:18px;margin-bottom:14px;border:1px solid var(--border)}
/* Calorie Ring */
.calorie-card{display:flex;align-items:center;gap:20px}
.ring-wrap{position:relative;flex-shrink:0}
.ring-wrap svg{transform:rotate(-90deg)}
.ring-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
.ring-num{font-size:22px;font-weight:800;color:var(--text)}
.ring-label{font-size:10px;color:var(--muted);letter-spacing:0.06em;text-transform:uppercase}
.ring-macros{flex:1}
.rm-row{margin-bottom:10px}
.rm-label{display:flex;justify-content:space-between;margin-bottom:4px}
.rm-name{font-size:12px;color:var(--muted)}
.rm-val{font-size:12px;font-weight:600;color:var(--text)}
.rm-bar{height:5px;background:#F0F0EE;border-radius:3px;overflow:hidden}
.rm-fill{height:100%;border-radius:3px}
/* Water */
.water-card .section-title{margin-bottom:14px}
.water-row{display:flex;align-items:center;gap:12px}
.water-drops{display:flex;gap:5px;flex:1;flex-wrap:wrap}
.drop{width:22px;height:22px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);background:#E8F4FD;border:1.5px solid #C8E6F8;transition:background 0.2s}
.drop.filled{background:#2A6AE8;border-color:#2A6AE8}
.water-btn{padding:8px 14px;border:1.5px solid #2A6AE8;border-radius:8px;color:#2A6AE8;font-size:13px;font-weight:600;background:none;cursor:pointer}
/* Today's workout */
.workout-card-inner{display:flex;justify-content:space-between;align-items:flex-start}
.workout-name{font-size:18px;font-weight:800;color:var(--text);margin-bottom:4px}
.workout-meta{font-size:13px;color:var(--muted)}
.muscle-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
.badge{padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
.badge-orange{background:var(--accent-light);color:var(--accent)}
.badge-blue{background:#EBF0FE;color:#2A6AE8}
.badge-green{background:#EAFAEE;color:#22A647}
.btn-start{padding:10px 18px;background:var(--accent);color:#FFF;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap}
.btn-start:active{opacity:0.85}
.no-workout{text-align:center;padding:12px 0;color:var(--muted);font-size:14px}
/* ── SCHEDULE ────────────────────────────── */
.sched-header{padding:52px 22px 0;background:var(--header)}
.sched-title{font-size:24px;font-weight:800;color:#FFF;margin-bottom:18px}
.week-strip{display:flex;gap:4px;overflow-x:auto;padding-bottom:16px;scrollbar-width:none}
.week-strip::-webkit-scrollbar{display:none}
.day-pill{flex-shrink:0;width:46px;height:62px;border-radius:12px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;cursor:pointer;background:rgba(255,255,255,0.1);border:1.5px solid transparent;transition:all 0.2s}
.day-pill.active{background:#FFF;border-color:#FFF}
.day-name{font-size:10px;letter-spacing:0.06em;color:rgba(255,255,255,0.6);font-weight:600;text-transform:uppercase}
.day-num{font-size:18px;font-weight:800;color:rgba(255,255,255,0.9)}
.day-pill.active .day-name{color:var(--muted)}
.day-pill.active .day-num{color:var(--text)}
.day-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}
.day-pill.active .day-dot{background:var(--accent)}
.sched-body{padding:18px 16px}
.day-title{font-size:20px;font-weight:800;color:var(--text);margin-bottom:14px}
.workout-block{background:var(--card);border-radius:var(--radius);overflow:hidden;border:1px solid var(--border);margin-bottom:12px}
.wb-header{padding:14px 16px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}
.wb-name{font-size:16px;font-weight:700;color:var(--text)}
.wb-time{font-size:12px;color:var(--muted)}
.wb-exercises{padding:12px 16px}
.wb-ex{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:14px;color:var(--text)}
.wb-ex-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0}
.wb-ex-detail{color:var(--muted);font-size:12px;margin-left:auto}
.wb-footer{padding:12px 16px;border-top:1px solid var(--border)}
.btn-start-full{width:100%;padding:13px;background:var(--accent);color:#FFF;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
.add-workout-btn{width:100%;padding:14px;background:none;border:2px dashed #DDDDD8;border-radius:var(--radius);color:var(--muted);font-size:14px;cursor:pointer;margin-top:4px;transition:all 0.2s}
.add-workout-btn:hover{border-color:var(--accent);color:var(--accent)}
/* ── LIBRARY ─────────────────────────────── */
.lib-header{padding:52px 16px 16px;background:var(--header)}
.lib-title{font-size:24px;font-weight:800;color:#FFF;margin-bottom:14px}
.search-wrap{position:relative}
.search-input{width:100%;padding:13px 16px 13px 44px;border-radius:var(--radius-sm);border:none;font-size:15px;background:#FFF;color:var(--text);outline:none}
.search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#AAA;font-size:16px}
.filter-btn{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--accent);font-size:13px;font-weight:700;display:flex;align-items:center;gap:3px}
.filter-badge{background:var(--accent);color:#FFF;border-radius:10px;padding:1px 6px;font-size:10px;font-weight:700}
.chips-row{display:flex;gap:8px;overflow-x:auto;padding:14px 16px 4px;scrollbar-width:none;background:var(--header)}
.chips-row::-webkit-scrollbar{display:none}
.chip{flex-shrink:0;padding:7px 14px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;border:1.5px solid rgba(255,255,255,0.2);color:rgba(255,255,255,0.8);background:transparent;transition:all 0.2s}
.chip.active{background:var(--accent);border-color:var(--accent);color:#FFF}
.lib-body{padding:14px 16px}
.ex-card{background:var(--card);border-radius:var(--radius-sm);padding:14px 16px;display:flex;align-items:center;gap:14px;margin-bottom:10px;border:1px solid var(--border);cursor:pointer;transition:transform 0.1s}
.ex-card:active{transform:scale(0.99)}
.ex-icon{width:44px;height:44px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
.ex-info{flex:1;min-width:0}
.ex-name{font-size:15px;font-weight:700;color:var(--text);margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ex-meta{font-size:12px;color:var(--muted)}
.ex-equip{font-size:11px;padding:3px 8px;border-radius:20px;background:#F0F0EE;color:var(--muted);font-weight:600;flex-shrink:0}
/* ── NUTRITION STUB ──────────────────────── */
.nutr-header{padding:52px 22px 20px;background:var(--header)}
.nutr-title{font-size:24px;font-weight:800;color:#FFF;margin-bottom:4px}
.nutr-date{font-size:13px;color:rgba(255,255,255,0.5)}
.meal-card{background:var(--card);border-radius:var(--radius);border:1px solid var(--border);margin-bottom:12px;overflow:hidden}
.meal-header{padding:14px 16px;display:flex;justify-content:space-between;align-items:center}
.meal-name{font-size:15px;font-weight:700;color:var(--text)}
.meal-cal{font-size:13px;color:var(--muted)}
.meal-add{padding:4px 12px;border:1.5px solid var(--accent);border-radius:8px;color:var(--accent);font-size:12px;font-weight:700;background:none;cursor:pointer}
.meal-empty{padding:12px 16px;font-size:13px;color:var(--muted);border-top:1px solid var(--border)}
.food-item{display:flex;align-items:center;gap:8px;padding:10px 16px;border-top:1px solid var(--border)}
.food-item-name{font-size:14px;font-weight:600;color:var(--text)}
.food-item-detail{font-size:11px;color:var(--muted);margin-top:1px}
.food-item-macros{font-size:11px;color:var(--muted);white-space:nowrap;margin-left:auto}
.food-item-del{background:none;border:none;color:var(--muted);font-size:16px;cursor:pointer;padding:0 0 0 8px;line-height:1;flex-shrink:0}
.nutr-summary{background:var(--card);border-radius:var(--radius);border:1px solid var(--border);padding:16px;margin-bottom:16px}
.nutr-cal-row{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px}
.nutr-cal-num{font-size:28px;font-weight:800;color:var(--text)}
.nutr-cal-goal{font-size:13px;color:var(--muted)}
.food-search-item{padding:12px 0;border-bottom:1px solid var(--border);cursor:pointer;display:flex;justify-content:space-between;align-items:center}
.food-search-item:last-child{border-bottom:none}
.food-search-item:active{opacity:0.7}
.food-search-name{font-size:14px;font-weight:600;color:var(--text)}
.food-search-meta{font-size:12px;color:var(--muted);margin-top:2px}
.food-amount-section{background:var(--bg);border-radius:var(--radius-sm);padding:14px;margin:12px 0}
.food-amount-preview{font-size:13px;color:var(--muted);margin-top:8px;line-height:1.6}
/* ── PROFILE ─────────────────────────────── */
.prof-header{padding:52px 22px 28px;background:var(--text);text-align:center}
.prof-avatar{width:72px;height:72px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;margin:0 auto 14px;font-size:26px;font-weight:800;color:#FFF}
.prof-name{font-size:22px;font-weight:800;color:#FFF;margin-bottom:4px}
.prof-goal{font-size:13px;color:rgba(255,255,255,0.5)}
.prof-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:18px 16px}
.ps-card{background:var(--card);border-radius:var(--radius-sm);padding:14px 12px;text-align:center;border:1px solid var(--border)}
.ps-num{font-size:22px;font-weight:800;color:var(--text)}
.ps-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-top:2px}
.settings-section{padding:0 16px 12px}
/* ── ACHIEVEMENT TOAST ───────────────────── */
#achievementToast{position:absolute;top:60px;left:50%;transform:translateX(-50%) translateY(-20px);background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,0.18);z-index:500;width:260px;opacity:0;pointer-events:none;transition:opacity 0.3s,transform 0.3s}
#achievementToast.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* ── ACHIEVEMENT GRID ────────────────────── */
.ach-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-bottom:20px}
.ach-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;display:flex;gap:10px;align-items:center}
.ach-card.locked{opacity:0.38}
.ach-icon{font-size:26px;flex-shrink:0}
.ach-name{font-size:13px;font-weight:700;color:var(--text);line-height:1.2}
.ach-desc{font-size:11px;color:var(--muted);margin-top:2px;line-height:1.3}
.theme-row{display:flex;gap:10px;margin-bottom:16px}
.theme-swatch{flex:1;height:52px;border-radius:var(--radius-sm);cursor:pointer;border:2px solid transparent;overflow:hidden;transition:border-color 0.2s,transform 0.15s;position:relative}
.theme-swatch:active{transform:scale(0.95)}
.theme-swatch.active{border-color:var(--accent)}
.theme-swatch-top{height:60%;width:100%}
.theme-swatch-bot{height:40%;width:100%}
.theme-swatch-check{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,0.5);opacity:0}
.theme-swatch.active .theme-swatch-check{opacity:1}
.theme-label{text-align:center;font-size:10px;color:var(--muted);margin-top:4px;font-weight:500}
.settings-item{display:flex;justify-content:space-between;align-items:center;padding:15px 16px;background:var(--card);margin-bottom:2px;cursor:pointer;border:1px solid var(--border)}
.settings-item:first-child{border-radius:var(--radius) var(--radius) 0 0}
.settings-item:last-child{border-radius:0 0 var(--radius) var(--radius)}
.si-label{font-size:15px;color:var(--text)}
.si-val{font-size:14px;color:var(--muted)}
/* ── ACTIVE WORKOUT ──────────────────────── */
#active-workout{position:absolute;inset:0;background:#0F0F0F;display:none;flex-direction:column;z-index:200}
.aw-header{padding:52px 22px 20px;display:flex;justify-content:space-between;align-items:flex-start}
.aw-name{font-size:22px;font-weight:800;color:#FFF}
.aw-timer{font-size:14px;color:#888;margin-top:4px}
.aw-close{background:rgba(255,255,255,0.1);border:none;color:#FFF;width:36px;height:36px;border-radius:50%;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.aw-body{flex:1;overflow-y:auto;padding:0 16px 16px}
.aw-ex-card{background:#1A1A1A;border-radius:var(--radius);padding:16px;margin-bottom:12px}
.aw-ex-name{font-size:17px;font-weight:700;color:#FFF;margin-bottom:12px}
.aw-sets{display:flex;flex-direction:column;gap:8px}
.aw-set-row{display:grid;grid-template-columns:28px 1fr 1fr auto;gap:8px;align-items:center}
.set-num{font-size:12px;color:#666;font-weight:700;text-align:center}
.set-input{padding:10px 12px;background:#2A2A2A;border:1px solid #333;border-radius:8px;color:#FFF;font-size:16px;text-align:center;width:100%;outline:none}
.set-input:focus{border-color:var(--accent)}
.set-done{width:34px;height:34px;border-radius:8px;border:1.5px solid #444;background:none;color:#666;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.set-done.completed{background:var(--accent);border-color:var(--accent);color:#FFF}
.aw-footer{padding:16px;border-top:1px solid #222}
.btn-finish{width:100%;padding:16px;background:#FFF;color:#0F0F0F;border:none;border-radius:var(--radius);font-size:16px;font-weight:800;cursor:pointer}
/* ── MODAL ──────────────────────────────── */
.modal-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:none;align-items:flex-end}
.modal-overlay.show{display:flex}
.modal-sheet{background:#FFF;border-radius:var(--radius) var(--radius) 0 0;width:100%;padding:24px 22px 40px;max-height:85vh;overflow-y:auto}
.modal-handle{width:36px;height:4px;background:#DDD;border-radius:2px;margin:0 auto 20px}
.modal-title{font-size:20px;font-weight:800;color:var(--text);margin-bottom:20px}
/* ── FILTER MODAL ──────────────────────── */
.filter-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.5);z-index:400;display:none;align-items:flex-end}
.filter-overlay.show{display:flex}
.create-ex-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.5);z-index:450;display:none;align-items:flex-end}
.create-ex-overlay.show{display:flex}
.create-ex-sheet{background:var(--card);border-radius:var(--radius) var(--radius) 0 0;width:100%;padding:24px 22px 40px;max-height:90vh;overflow-y:auto}
.create-ex-sheet .field{margin-bottom:12px}
.create-ex-sheet .field label{display:block;font-size:11px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
.create-ex-sheet .field input,.create-ex-sheet .field select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:15px;color:var(--text);background:var(--bg);box-sizing:border-box}
.ce-instruction-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}
.ce-instruction-row input{flex:1;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:14px;color:var(--text);background:var(--bg)}
.ce-instruction-row button{width:32px;height:32px;background:#FEE2E2;border:none;border-radius:var(--radius-sm);color:#E53E3E;font-size:16px;cursor:pointer;flex-shrink:0}
.lib-fab{position:sticky;bottom:16px;left:50%;transform:translateX(-50%);display:block;margin:16px auto 0;padding:13px 28px;background:var(--accent);color:#FFF;font-size:15px;font-weight:700;border:none;border-radius:999px;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,0.18);width:max-content}
.ex-card-custom{border:1.5px solid var(--accent);}
.filter-sheet{background:#FFF;border-radius:var(--radius) var(--radius) 0 0;width:100%;padding:24px 22px 40px;max-height:80vh;overflow-y:auto}
.filter-section-title{font-size:11px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin:18px 0 10px}
.filter-section-title:first-child{margin-top:0}
.filter-chip-row{display:flex;flex-wrap:wrap;gap:8px}
.filter-chip{padding:8px 14px;border:1.5px solid #E8E8E8;border-radius:20px;font-size:13px;cursor:pointer;transition:all 0.2s;color:var(--text);background:#FFF}
.filter-chip.active{border-color:var(--accent);background:var(--accent-light);color:var(--accent);font-weight:600}
.filter-actions{display:flex;gap:10px;margin-top:24px}
.filter-reset{flex:1;padding:14px;background:#F5F5F3;color:var(--text);font-size:15px;font-weight:600;border:none;border-radius:var(--radius);cursor:pointer}
.filter-apply{flex:2;padding:14px;background:var(--accent);color:#FFF;font-size:15px;font-weight:700;border:none;border-radius:var(--radius);cursor:pointer}
/* ── REST TIMER ─────────────────────────── */
.rest-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.85);z-index:250;display:none;flex-direction:column;align-items:center;justify-content:center}
.rest-overlay.show{display:flex}
.rest-num{font-size:80px;font-weight:800;color:#FFF;font-variant-numeric:tabular-nums}
.rest-label{font-size:14px;color:#888;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:32px}
.rest-skip{padding:12px 32px;border:2px solid rgba(255,255,255,0.3);border-radius:12px;background:none;color:#FFF;font-size:14px;font-weight:700;cursor:pointer}
</style>
</head>
<body>
<div id="root">
<div id="achievementToast"></div>
<!-- SPLASH -->
<div id="splash">
<div class="splash-ring">
<svg width="140" height="140" viewBox="0 0 140 140">
<circle cx="70" cy="70" r="60" fill="none" stroke="#1A1A1A" stroke-width="3"/>
<circle cx="70" cy="70" r="60" fill="none" stroke="#E85D26" stroke-width="3"
stroke-dasharray="377" stroke-dashoffset="188" stroke-linecap="round"/>
</svg>
<div class="splash-logo">
<div class="splash-name">FORTIS</div>
<div class="splash-sub">Träningsapp</div>
</div>
</div>
<div class="splash-progress"><div class="splash-fill" id="splashFill"></div></div>
</div>
<!-- ONBOARDING -->
<div id="onboarding">
<div class="ob-header">
<div class="ob-steps" id="obSteps"></div>
<div class="ob-title" id="obTitle"></div>
<div class="ob-desc" id="obDesc"></div>
</div>
<div class="ob-body" id="obBody"></div>
<div class="ob-footer" id="obFooter"></div>
</div>
<!-- MAIN APP -->
<div id="app">
<!-- Home -->
<div class="screen" id="tabHome">
<div class="home-header">
<div class="greeting-sub">God morgon</div>
<div class="greeting" id="homeGreeting">Jens</div>
<div class="streak-pill">🔥 <span id="streakCount">0</span> dagars streak</div>
</div>
<div class="home-content">
<div class="section-title">Kalorier idag</div>
<div class="card calorie-card">
<div class="ring-wrap">
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" fill="none" stroke="#F0F0EE" stroke-width="8"/>
<circle id="calRing" cx="50" cy="50" r="42" fill="none" stroke="#E85D26" stroke-width="8"
stroke-dasharray="264" stroke-dashoffset="264" stroke-linecap="round"
style="transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset 0.8s ease"/>
</svg>
<div class="ring-center">
<div class="ring-num" id="calsLeft">—</div>
<div class="ring-label">kvar</div>
</div>
</div>
<div class="ring-macros">
<div class="rm-row">
<div class="rm-label"><span class="rm-name">Protein</span><span class="rm-val" id="mProtein">0<span class="rm-unit">g</span></span></div>
<div class="rm-bar"><div class="rm-fill" id="mProteinBar" style="background:#E85D26;width:0%"></div></div>
</div>
<div class="rm-row">
<div class="rm-label"><span class="rm-name">Kolhydrater</span><span class="rm-val" id="mCarbs">0<span class="rm-unit">g</span></span></div>
<div class="rm-bar"><div class="rm-fill" id="mCarbsBar" style="background:#2A6AE8;width:0%"></div></div>
</div>
<div class="rm-row">
<div class="rm-label"><span class="rm-name">Fett</span><span class="rm-val" id="mFat">0<span class="rm-unit">g</span></span></div>
<div class="rm-bar"><div class="rm-fill" id="mFatBar" style="background:#22A647;width:0%"></div></div>
</div>
</div>
</div>
<div class="section-title">Vatten</div>
<div class="card">
<div class="water-row">
<div class="water-drops" id="waterDrops"></div>
<button class="water-btn" onclick="addWater()">+2,5 dl</button>
</div>
<div style="font-size:12px;color:var(--muted);margin-top:10px" id="waterLabel">0 / 2500 ml</div>
</div>
<div class="section-title">Dagens pass</div>
<div class="card" id="todayWorkoutCard"></div>
</div>
</div>
<!-- Schedule -->
<div class="screen" id="tabSchedule" style="display:none">
<div class="sched-header">
<div class="sched-title">Schema</div>
<div class="week-strip" id="weekStrip"></div>
</div>
<div class="sched-body" id="schedBody"></div>
</div>
<!-- Library -->
<div class="screen" id="tabLibrary" style="display:none">
<div class="lib-header">
<div class="lib-title">Övningsbibliotek</div>
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input class="search-input" type="text" placeholder="Sök övning..." id="libSearch" oninput="filterExercises()">
<button class="filter-btn" onclick="toggleFilterModal()">Filter <span class="filter-badge" id="filterBadge" style="display:none">0</span></button>
</div>
</div>
<div class="chips-row" id="equipChips"></div>
<div class="lib-body" id="libBody"></div>
<button class="lib-fab" onclick="openCreateExercise()">+ Ny övning</button>
</div>
<!-- Nutrition -->
<div class="screen" id="tabNutrition" style="display:none">
<div class="nutr-header">
<div class="nutr-title">Kost</div>
<div class="nutr-date" id="nutrDate"></div>
</div>
<div style="padding:18px 16px">
<div class="nutr-summary" id="nutrSummary"></div>
<div class="section-title" style="margin-bottom:10px">Måltider</div>
<div id="mealCards"></div>
<span id="nutrProtGoal" style="display:none"></span>
<span id="nutrCarbGoal" style="display:none"></span>
<span id="nutrFatGoal" style="display:none"></span>
</div>
</div>
<!-- Profile -->
<div class="screen" id="tabProfile" style="display:none">
<div class="prof-header">
<div class="prof-avatar" id="profAvatar">J</div>
<div class="prof-name" id="profName">—</div>
<div class="prof-goal" id="profGoalText">—</div>
</div>
<div class="prof-stats">
<div class="ps-card"><div class="ps-num" id="psTotal">0</div><div class="ps-label">Pass totalt</div></div>
<div class="ps-card"><div class="ps-num" id="psStreak">0</div><div class="ps-label">Streak</div></div>
<div class="ps-card"><div class="ps-num" id="psWeight">—</div><div class="ps-label">Kg</div></div>
</div>
<div class="settings-section">
<div class="section-title" style="padding:0 0 10px">Achievements</div>
<div class="ach-grid" id="achGrid"></div>
<div class="section-title" style="padding:0 0 10px">Inställningar</div>
<div id="settingsItems"></div>
</div>
<div style="padding:0 16px 20px">
<button onclick="resetApp()" style="width:100%;padding:14px;border:1.5px solid #E0E0E0;border-radius:var(--radius);background:none;color:#CC3333;font-size:14px;font-weight:600;cursor:pointer">Återställ app</button>
</div>
</div>
<nav class="bottom-nav">
<button class="nav-item active" onclick="switchTab('Home')" id="navHome">
<span class="nav-icon">🏠</span><span class="nav-label">Hem</span>
</button>
<button class="nav-item" onclick="switchTab('Schedule')" id="navSchedule">
<span class="nav-icon">📅</span><span class="nav-label">Schema</span>
</button>
<button class="nav-item" onclick="switchTab('Library')" id="navLibrary">
<span class="nav-icon">📚</span><span class="nav-label">Bibliotek</span>
</button>
<button class="nav-item" onclick="switchTab('Nutrition')" id="navNutrition">
<span class="nav-icon">🍎</span><span class="nav-label">Kost</span>
</button>
<button class="nav-item" onclick="switchTab('Profile')" id="navProfile">
<span class="nav-icon">👤</span><span class="nav-label">Profil</span>
</button>
</nav>
</div>
<!-- Active Workout View -->
<div id="active-workout">
<div class="aw-header">
<div>
<div class="aw-name" id="awName">Pass</div>
<div class="aw-timer" id="awTimer">00:00</div>
</div>
<button class="aw-close" onclick="closeWorkout()">✕</button>
</div>
<div class="aw-body" id="awBody"></div>
<div class="aw-footer">
<button class="btn-finish" onclick="finishWorkout()">Avsluta pass</button>
</div>
</div>
<!-- Rest Timer Overlay -->
<div class="rest-overlay" id="restOverlay">
<div class="rest-label">Vila</div>
<div class="rest-num" id="restNum">60</div>
<button class="rest-skip" onclick="skipRest()">Hoppa över</button>
</div>
<!-- Modal (Exercise detail / Add workout) -->
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
<div class="modal-sheet" id="modalSheet">
<div class="modal-handle"></div>
<div class="modal-title" id="modalTitle"></div>
<div id="modalBody"></div>
</div>
</div>
<div class="filter-overlay" id="filterOverlay" onclick="closeFilterOverlay(event)">
<div class="filter-sheet">
<div class="modal-handle"></div>
<div class="modal-title">Filter</div>
<div class="filter-section-title">Muskelgrupp</div>
<div class="filter-chip-row" id="fmMuscles"></div>
<div class="filter-section-title">Svårighet</div>
<div class="filter-chip-row" id="fmDiffs"></div>
<div class="filter-actions">
<button class="filter-reset" onclick="resetFilters()">Återställ</button>
<button class="filter-apply" onclick="applyFilters()">Visa resultat</button>
</div>
</div>
</div>
<div class="create-ex-overlay" id="createExOverlay" onclick="closeCreateExercise(event)">
<div class="create-ex-sheet">
<div class="modal-handle"></div>
<div class="modal-title">Skapa övning</div>
<div class="field"><label>Namn</label><input type="text" id="ceName" placeholder="Övningens namn"></div>
<div class="field"><label>Ikon (emoji)</label><input type="text" id="ceIcon" placeholder="💪" maxlength="2" style="width:64px"></div>
<div class="field"><label>Muskelgrupp</label>
<select id="ceMuscle">
<option>Bröst</option><option>Rygg</option><option>Ben</option><option>Axlar</option>
<option>Armar</option><option>Core</option><option>Helkropp</option><option>Kondition</option>
</select>
</div>
<div class="field"><label>Sekundära muskler</label><input type="text" id="ceSecondary" placeholder="t.ex. Triceps, Axlar"></div>
<div class="field"><label>Utrustning</label>
<select id="ceEquip">
<option>Kroppsvikt</option><option>Hantel</option><option>Kettlebell</option><option>Skivstång</option>
<option>Kabelmaskin</option><option>Maskin</option><option>Motståndsband</option><option>Löpband</option>
</select>
</div>
<div class="field"><label>Svårighet</label>
<select id="ceDiff"><option>Nybörjare</option><option>Medel</option><option>Avancerad</option></select>
</div>
<div class="field"><label>Typ</label>
<select id="ceType"><option>Styrka</option><option>Kondition</option><option>Rörlighet</option></select>
</div>
<div class="filter-section-title" style="margin-top:16px">Instruktioner</div>
<div id="ceInstructions"></div>
<button class="btn-ghost" style="margin-top:8px;font-size:14px" onclick="addInstruction()">+ Lägg till steg</button>
<div style="display:flex;gap:10px;margin-top:20px">
<button class="filter-reset" onclick="closeCreateExercise()">Avbryt</button>
<button class="filter-apply" onclick="saveCustomExercise()">Spara övning</button>
</div>
</div>
</div>
<script>
// ── DATA ─────────────────────────────────────────────────
const EXERCISES=[
// ── BRÖST ──────────────────────────────────────────────
{id:'pushup',name:'Armhävningar',muscle:'Bröst',secondary:'Triceps, Axlar',equip:'Kroppsvikt',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Lägg händerna axelbrett isär på golvet','Håll kroppen rak från huvud till häl','Sänk bröstet mot golvet','Pressa upp till startläget']},
{id:'bench_press',name:'Bänkpress',muscle:'Bröst',secondary:'Triceps, Axlar',equip:'Skivstång',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Ligg på bänken med fötterna i golvet','Grip stången lite bredare än axelbredd','Sänk stången till bröstet','Pressa upp explosivt']},
{id:'incline_bench_press',name:'Incline Bench Press',muscle:'Bröst',secondary:'Triceps, Axlar',equip:'Skivstång',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Ställ bänken på 30–45°','Grip stången axelbrett','Sänk stången mot övre bröstet','Pressa upp och in']},
{id:'db_bench_press',name:'Dumbbell Bench Press',muscle:'Bröst',secondary:'Triceps, Axlar',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på bänk med en hantel i varje hand','Hantelerna vid sidan av bröstet','Pressa rakt upp','Sänk kontrollerat']},
{id:'db_incline_press',name:'Dumbbell Incline Press',muscle:'Bröst',secondary:'Axlar, Triceps',equip:'Hantel',icon:'💪',diff:'Medel',type:'Styrka',instructions:['Bänk på 30–45°, hantel i varje hand','Hantelerna vid övre bröstet','Pressa upp','Sänk kontrollerat till startläge']},
{id:'db_floor_press',name:'Dumbbell Floor Press',muscle:'Bröst',secondary:'Triceps, Axlar',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på golvet med knäna böjda','Hantel i varje hand vid sidan av bröstet','Pressa rakt upp','Sänk tills armbågarna nuddar golvet']},
{id:'db_incline_fly',name:'Dumbbell Incline Fly',muscle:'Bröst',secondary:'Axlar',equip:'Hantel',icon:'🦅',diff:'Medel',type:'Styrka',instructions:['Bänk på 30–45°, hantel i varje hand','Armarna nästan raka, lätt böjda i armbågen','Sänk hantelerna i en vid båge','Pressa ihop framför bröstet']},
{id:'cable_fly',name:'Cable Fly',muscle:'Bröst',secondary:'Axlar',equip:'Kabelmaskin',icon:'🔗',diff:'Nybörjare',type:'Styrka',instructions:['Ställ kabler i brösthöjd','Stå i mitten, ett steg framåt','Dra handtagen framåt och ihop','Kontrollera tillbaka']},
{id:'push_up_diamond',name:'Diamond Push-Up',muscle:'Bröst',secondary:'Triceps',equip:'Kroppsvikt',icon:'💎',diff:'Medel',type:'Styrka',instructions:['Händerna tätt ihop, tummar och pekfingrar bildar en romb','Kroppen rak','Sänk bröstet mot händerna','Pressa upp']},
{id:'machine_chest_press',name:'Machine Chest Press',muscle:'Bröst',secondary:'Triceps, Axlar',equip:'Maskin',icon:'🔩',diff:'Nybörjare',type:'Styrka',instructions:['Justera sätet, händtagen vid bröstnivå','Grip handtagen, fötterna i golvet','Pressa rakt framåt','Kontrollera tillbaka']},
// ── RYGG ───────────────────────────────────────────────
{id:'pullup',name:'Chins (underhand)',muscle:'Rygg',secondary:'Biceps',equip:'Kroppsvikt',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Grip stången underhands, axelbrett','Häng med raka armar','Dra dig upp tills hakan är över stången','Sänk kontrollerat']},
{id:'pull_up',name:'Pull-Up (overhand)',muscle:'Rygg',secondary:'Biceps, Axlar',equip:'Kroppsvikt',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Grip stången överhands, lite bredare än axelbredd','Häng med raka armar','Dra dig upp tills bröstet nuddar stången','Sänk kontrollerat']},
{id:'inverted_row',name:'Inverted Row',muscle:'Rygg',secondary:'Biceps, Core',equip:'Kroppsvikt',icon:'🔄',diff:'Nybörjare',type:'Styrka',instructions:['Ligg under en stång i höfthöjd','Grip stången, kroppen rak','Dra bröstet upp mot stången','Sänk kontrollerat']},
{id:'deadlift',name:'Marklyft',muscle:'Rygg',secondary:'Ben, Rumpa',equip:'Skivstång',icon:'🏋️',diff:'Avancerad',type:'Styrka',instructions:['Stå med stången över fotmitt','Böj i höft och knä, rygg rak','Grip stången axelbrett','Pressa marken ifrån dig, höfterna framåt']},
{id:'lat_pulldown',name:'Latsdrag',muscle:'Rygg',secondary:'Biceps',equip:'Kabelmaskin',icon:'🔗',diff:'Nybörjare',type:'Styrka',instructions:['Sitt vid maskinen och greppa stången brett','Dra ner stången till övre bröstet','Håll armbågarna nere och bakåt','Kontrollera uppgången']},
{id:'cable_row',name:'Kabeldrag sittande',muscle:'Rygg',secondary:'Biceps, Axlar',equip:'Kabelmaskin',icon:'🔗',diff:'Nybörjare',type:'Styrka',instructions:['Sitt med fötterna mot plattformen','Håll ryggen rak','Dra handtaget mot magen','Pressa ihop skulderbladen']},
{id:'db_bent_row',name:'Dumbbell Bent Over Row',muscle:'Rygg',secondary:'Biceps, Axlar',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Böj dig framåt i höften, ryggen rak','Hantel i varje hand, armarna hängande','Dra hantelerna mot höfterna','Pressa skulderbladen ihop']},
{id:'barbell_row',name:'Barbell Bent Over Row',muscle:'Rygg',secondary:'Biceps, Axlar',equip:'Skivstång',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Böj dig 45° framåt, ryggen rak','Grip stången axelbrett underhands','Dra stången mot naveln','Sänk kontrollerat']},
{id:'db_pullover',name:'Dumbbell Pullover',muscle:'Rygg',secondary:'Bröst, Triceps',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Ligg tvärs över bänk, överkropp plant','Håll en hantel med båda händer ovan bröstet','Sänk hanteln bakom huvudet i en båge','Dra tillbaka upp']},
{id:'superman',name:'Superman',muscle:'Rygg',secondary:'Rumpa',equip:'Kroppsvikt',icon:'🦸',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på mage, armar sträckta framåt','Lyft armar och ben från golvet simultant','Håll 2–3 sek i toppläget','Sänk kontrollerat']},
{id:'seated_row_machine',name:'Seated Row Machine',muscle:'Rygg',secondary:'Biceps, Axlar',equip:'Maskin',icon:'🔩',diff:'Nybörjare',type:'Styrka',instructions:['Sitt i maskinen, bröst mot stödplattan','Grip handtagen','Dra handtagen mot kroppen','Kontrollera tillbaka']},
// ── BEN ────────────────────────────────────────────────
{id:'squat',name:'Knäböj (kroppsvikt)',muscle:'Ben',secondary:'Rumpa, Core',equip:'Kroppsvikt',icon:'🦵',diff:'Nybörjare',type:'Styrka',instructions:['Stå axelbrett med fötterna','Sänk höfterna bakåt och nedåt','Knäna följer tårnas riktning','Pressa upp genom hälarna']},
{id:'barbell_squat',name:'Barbell Squat',muscle:'Ben',secondary:'Rumpa, Core',equip:'Skivstång',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Stång på övre ryggen, axelbrett fötter','Bröst upprätt, ryggen neutral','Sänk höfterna nedåt och bakåt','Pressa upp explosivt']},
{id:'goblet_squat',name:'Goblet Squat',muscle:'Ben',secondary:'Core, Rumpa',equip:'Kettlebell',icon:'🫙',diff:'Nybörjare',type:'Styrka',instructions:['Håll kettlebell mot bröstet','Fötterna bredare än axelbrett','Böj knäna och sänk dig djupt','Pressa upp med bröst upprätt']},
{id:'lunges',name:'Utfall',muscle:'Ben',secondary:'Rumpa, Core',equip:'Kroppsvikt',icon:'🦵',diff:'Nybörjare',type:'Styrka',instructions:['Stå med fötterna ihop','Ta ett stort steg framåt','Böj båda knäna till 90°','Pressa upp och byt ben']},
{id:'bulgarian_split_squat',name:'Bulgarian Split Squat',muscle:'Ben',secondary:'Rumpa, Core',equip:'Kroppsvikt',icon:'🦵',diff:'Medel',type:'Styrka',instructions:['Bakre foten på en bänk','Fram-foten ett stort steg framåt','Sänk bakre knäet mot golvet','Pressa upp via framfoten']},
{id:'leg_press',name:'Benpress',muscle:'Ben',secondary:'Rumpa',equip:'Maskin',icon:'🦵',diff:'Nybörjare',type:'Styrka',instructions:['Sitt i maskinen med fötterna axelbrett','Böj knäna till 90°','Pressa plattan ifrån dig','Sträck inte ut knäna helt']},
{id:'leg_extension',name:'Leg Extension',muscle:'Ben',secondary:'',equip:'Maskin',icon:'🦵',diff:'Nybörjare',type:'Styrka',instructions:['Sitt i maskinen, underben mot kudden','Håll ryggen mot ryggstödet','Sträck ut benen till horisontellt','Sänk kontrollerat']},
{id:'leg_curl',name:'Leg Curl',muscle:'Ben',secondary:'',equip:'Maskin',icon:'🦵',diff:'Nybörjare',type:'Styrka',instructions:['Ligg i maskinen (eller sitt), vaden mot kudden','Böj knäna och dra hälarna mot rumpan','Håll i toppläget en sekund','Sänk kontrollerat']},
{id:'kb_swing',name:'Kettlebell Swing',muscle:'Ben',secondary:'Rygg, Rumpa',equip:'Kettlebell',icon:'⚡',diff:'Medel',type:'Styrka',instructions:['Stå axelbrett, kettlebell mellan benen','Böj i höften, svinga bakåt','Explosiv höftrörelse framåt, svinga upp till brösthöjd','Kontrollera pendeln tillbaka']},
{id:'kb_sumo_deadlift',name:'Kettlebell Sumo Deadlift',muscle:'Ben',secondary:'Rygg, Rumpa',equip:'Kettlebell',icon:'🫙',diff:'Nybörjare',type:'Styrka',instructions:['Brett fotläge, tår utåt','Kettlebell mellan fötterna','Höft och knä böjt, rygg rak','Pressa upp, sträck höft och knä simultant']},
{id:'db_front_squat',name:'Dumbbell Front Squat',muscle:'Ben',secondary:'Core, Rumpa',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Håll hantel i varje hand på axlarna','Fötterna axelbrett','Sänk rakt ner, bröst upprätt','Pressa upp']},
{id:'db_hip_thrust',name:'Dumbbell Hip Thrust',muscle:'Ben',secondary:'Core',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Övre rygg mot bänk, fötter i golvet','Hantel på höfterna','Sänk rumpan mot golvet','Pressa höfterna rakt upp, kläm rumpan']},
{id:'db_rdl',name:'Romanian Deadlift (DB)',muscle:'Ben',secondary:'Rygg, Rumpa',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Stå med hantel i varje hand framför låren','Böj i höften, hantelerna glider längs benen','Känn sträcket i baklåren','Pressa höfterna framåt till startläget']},
{id:'calf_raise',name:'Standing Calf Raise',muscle:'Ben',secondary:'',equip:'Kroppsvikt',icon:'🦶',diff:'Nybörjare',type:'Styrka',instructions:['Stå på kanten av ett steg','Sänk hälarna under stegnivå','Pressa upp på tårna','Håll i toppläget en sekund']},
{id:'step_ups',name:'Step-Ups',muscle:'Ben',secondary:'Rumpa, Core',equip:'Kroppsvikt',icon:'🦵',diff:'Nybörjare',type:'Styrka',instructions:['Stå framför en stabil yta (bänk/steg)','Kliv upp med ett ben','Ta med dig det andra benet','Kliv ner kontrollerat och byt']},
{id:'box_squat',name:'Box Squat',muscle:'Ben',secondary:'Rumpa, Core',equip:'Kroppsvikt',icon:'📦',diff:'Nybörjare',type:'Styrka',instructions:['Stå framför en låg bänk eller box','Axelbrett fotläge','Sänk dig tills du nuddar boxen','Pressa upp utan att gunga']},
{id:'pistol_squat',name:'Pistol Squat',muscle:'Ben',secondary:'Core, Rumpa',equip:'Kroppsvikt',icon:'🦵',diff:'Avancerad',type:'Styrka',instructions:['Stå på ett ben, det andra rakt framåt','Armarna ut för balans','Sänk på ett ben så djupt du kan','Pressa upp']},
{id:'glute_bridge',name:'Glute Bridge',muscle:'Ben',secondary:'Core',equip:'Kroppsvikt',icon:'🌉',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på rygg, knäna böjda, fötter i golvet','Pressa höfterna rakt upp','Kläm rumpan hårt i toppläget','Sänk kontrollerat']},
// ── AXLAR ──────────────────────────────────────────────
{id:'shoulder_press',name:'Dumbbell Shoulder Press',muscle:'Axlar',secondary:'Triceps',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Sitt eller stå med hantel i varje hand','Hantelerna vid öronen','Pressa rakt upp','Sänk kontrollerat till öronnivå']},
{id:'overhead_press',name:'Overhead Press (Barbell)',muscle:'Axlar',secondary:'Triceps, Core',equip:'Skivstång',icon:'🏋️',diff:'Medel',type:'Styrka',instructions:['Stå med stången vid brösthöjd','Grip axelbrett','Pressa rakt upp tills armarna är raka','Sänk kontrollerat till startläge']},
{id:'lateral_raise',name:'Lateral Raise',muscle:'Axlar',secondary:'',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Stå med hantel i varje hand längs sidan','Lätt böjda armar','Lyft hantelerna till sidan upp till axelhöjd','Sänk kontrollerat']},
{id:'front_raise',name:'Front Raise',muscle:'Axlar',secondary:'',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Stå med hantel i varje hand','Lyft en hantel i taget rakt framåt till axelhöjd','Håll armbågen lätt böjd','Sänk kontrollerat och byt arm']},
{id:'rear_delt_fly',name:'Rear Delt Fly',muscle:'Axlar',secondary:'Rygg',equip:'Hantel',icon:'🦅',diff:'Nybörjare',type:'Styrka',instructions:['Böj dig 90° framåt, rygg rak','Hantel i varje hand hängande','Lyft hantelerna ut till sidan','Kläm skulderbladen ihop i toppen']},
{id:'face_pull',name:'Face Pull',muscle:'Axlar',secondary:'Rygg',equip:'Kabelmaskin',icon:'🔗',diff:'Nybörjare',type:'Styrka',instructions:['Ställ kabeln i ögonhöjd','Dra mot ansiktet med armbågarna högt','Pressa skulderbladen ihop','Kontrollera tillbaka']},
{id:'band_pull_apart',name:'Band Pull-Apart',muscle:'Axlar',secondary:'Rygg',equip:'Motståndsband',icon:'🔴',diff:'Nybörjare',type:'Styrka',instructions:['Håll bandet med båda händer framför bröstet, axelbrett','Sträck ut armarna','Dra bandet isär tills det nuddar bröstet','Kontrollera tillbaka']},
{id:'handstand_pushup',name:'Handstand Push-Up',muscle:'Axlar',secondary:'Triceps, Core',equip:'Kroppsvikt',icon:'🤸',diff:'Avancerad',type:'Styrka',instructions:['Stå på händer mot en vägg','Armarna raka, lägg händerna axelbrett','Böj armbågarna, sänk huvudet mot golvet','Pressa upp till startläge']},
// ── ARMAR ──────────────────────────────────────────────
{id:'dumbbell_curl',name:'Dumbbell Bicep Curl',muscle:'Armar',secondary:'Underarmar',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Stå med en hantel i varje hand','Armarna längs sidan','Curl upp mot axeln','Sänk kontrollerat']},
{id:'hammer_curl',name:'Hammer Curl',muscle:'Armar',secondary:'Underarmar',equip:'Hantel',icon:'🔨',diff:'Nybörjare',type:'Styrka',instructions:['Hantel i varje hand, neutralt grepp (tummen upp)','Håll armbågarna stilla längs sidan','Curl upp till axeln','Sänk kontrollerat']},
{id:'barbell_curl',name:'Barbell Curl',muscle:'Armar',secondary:'Underarmar',equip:'Skivstång',icon:'🏋️',diff:'Nybörjare',type:'Styrka',instructions:['Grip stången underhands, axelbrett','Armbågarna stilla vid sidan','Curl upp till axlarna','Sänk kontrollerat']},
{id:'zottman_curl',name:'Zottman Curl',muscle:'Armar',secondary:'Underarmar',equip:'Hantel',icon:'🔄',diff:'Medel',type:'Styrka',instructions:['Curl upp med underhands grepp','Rotera handleden i toppen (tummen nedåt)','Sänk med överhands grepp','Rotera tillbaka i botten']},
{id:'tricep_dips',name:'Dips',muscle:'Armar',secondary:'Bröst, Axlar',equip:'Kroppsvikt',icon:'💪',diff:'Medel',type:'Styrka',instructions:['Grip parallella stänger','Sänk kroppen ned tills armbågarna är 90°','Pressa upp','Luta dig inte för mycket framåt']},
{id:'cable_tricep_pushdown',name:'Cable Tricep Pushdown',muscle:'Armar',secondary:'',equip:'Kabelmaskin',icon:'🔗',diff:'Nybörjare',type:'Styrka',instructions:['Stå vid kabelmaskin, handtaget i brösthöjd','Armbågarna tätt mot sidan','Tryck handtaget rakt ner tills armarna är raka','Kontrollera tillbaka']},
{id:'db_tricep_kickback',name:'Dumbbell Tricep Kickback',muscle:'Armar',secondary:'',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Böj framåt, armbågen i 90°','Sträck ut armen bakåt tills den är rak','Håll armbågen stilla','Böj tillbaka kontrollerat']},
{id:'skull_crusher',name:'Skull Crusher',muscle:'Armar',secondary:'',equip:'Hantel',icon:'💀',diff:'Medel',type:'Styrka',instructions:['Ligg på bänk, hantel i varje hand ovan bröstet','Armbågarna pekande uppåt','Böj armbågarna, sänk hantelerna mot tinningarna','Pressa upp']},
{id:'overhead_tricep_ext',name:'Overhead Tricep Extension',muscle:'Armar',secondary:'',equip:'Hantel',icon:'💪',diff:'Nybörjare',type:'Styrka',instructions:['Stå eller sitt, håll en hantel med båda händer ovanför huvudet','Armbågarna tätt vid öronen','Böj armbågarna, sänk hanteln bakom huvudet','Pressa upp']},
// ── CORE ───────────────────────────────────────────────
{id:'plank',name:'Plank',muscle:'Core',secondary:'Axlar, Rygg',equip:'Kroppsvikt',icon:'🧘',diff:'Nybörjare',type:'Styrka',isTimer:true,instructions:['Lägg dig i push-up-position','Vila på underarmarna','Håll kroppen helt rak','Andas kontrollerat']},
{id:'situp',name:'Situps',muscle:'Core',secondary:'Höftböjare',equip:'Kroppsvikt',icon:'🧘',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på rygg med knäna böjda','Händerna bakom huvudet','Lyft överkroppen mot knäna','Sänk kontrollerat']},
{id:'crunches',name:'Crunches',muscle:'Core',secondary:'',equip:'Kroppsvikt',icon:'🧘',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på rygg, knäna böjda','Händerna vid tinningarna','Curl övre ryggen upp mot knäna','Sänk kontrollerat']},
{id:'russian_twist',name:'Russian Twist',muscle:'Core',secondary:'Obliques',equip:'Kroppsvikt',icon:'🌀',diff:'Nybörjare',type:'Styrka',instructions:['Sitt med knäna böjda och fötterna lätt lyfta','Luta dig lite bakåt','Rotera överkroppen sida till sida','Håll core spänt']},
{id:'knee_raise',name:'Hanging Knee Raise',muscle:'Core',secondary:'Höftböjare',equip:'Kroppsvikt',icon:'🧘',diff:'Medel',type:'Styrka',instructions:['Häng i en pull-up stång','Håll kroppen stilla','Lyft knäna mot bröstet','Sänk kontrollerat utan att gunga']},
{id:'leg_raise_lying',name:'Lying Leg Raise',muscle:'Core',secondary:'Höftböjare',equip:'Kroppsvikt',icon:'🧘',diff:'Medel',type:'Styrka',instructions:['Ligg på rygg, händerna under rumpan','Benen raka, lätt böjda','Lyft benen till 90°','Sänk utan att nudda golvet']},
{id:'plank_shoulder_tap',name:'Plank Shoulder Tap',muscle:'Core',secondary:'Axlar, Rumpa',equip:'Kroppsvikt',icon:'🧘',diff:'Medel',type:'Styrka',instructions:['Push-up position, händer under axlarna','Håll höfterna stilla','Lyft höger hand, nudda vänster axel','Byt sida, håll balansen']},
{id:'bicycle_crunch',name:'Bicycle Crunch',muscle:'Core',secondary:'Obliques',equip:'Kroppsvikt',icon:'🚴',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på rygg, händerna vid öronen','Lyft axlarna från golvet','Dra ena knäet mot bröstet, rotera mot det','Byt sida i ett cyklande rörelsemönster']},
{id:'dead_bug',name:'Dead Bug',muscle:'Core',secondary:'',equip:'Kroppsvikt',icon:'🐛',diff:'Nybörjare',type:'Styrka',instructions:['Ligg på rygg, armar raka upp, knäna böjda 90°','Håll ryggen tryckt mot golvet','Sänk höger arm och vänster ben simultant','Dra tillbaka och byt sida']},
{id:'plank_jacks',name:'Plank Jacks',muscle:'Core',secondary:'Ben, Axlar',equip:'Kroppsvikt',icon:'⚡',diff:'Medel',type:'Kondition',isTimer:true,instructions:['Push-up position','Hoppa fötterna isär och ihop (som jumping jacks)','Håll höfterna nere','Håll ett jämnt tempo']},
{id:'ab_wheel',name:'Ab Wheel Rollout',muscle:'Core',secondary:'Axlar, Rygg',equip:'Kroppsvikt',icon:'🌀',diff:'Avancerad',type:'Styrka',instructions:['Knäna i golvet, håll ab-hjulet','Rulla ut långsamt tills kroppen är nästan parallell med golvet','Håll core hårt spänt','Dra tillbaka med magen']},
// ── KONDITION ──────────────────────────────────────────
{id:'burpee',name:'Burpees',muscle:'Helkropp',secondary:'Core, Ben, Bröst',equip:'Kroppsvikt',icon:'⚡',diff:'Medel',type:'Kondition',instructions:['Stå rakt','Hoppa ner i push-up-position','Gör en armhävning','Hoppa upp med händerna ovan huvud']},
{id:'jumping_jacks',name:'Jumping Jacks',muscle:'Kondition',secondary:'Ben, Axlar',equip:'Kroppsvikt',icon:'⭐',diff:'Nybörjare',type:'Kondition',isTimer:true,instructions:['Stå med fötterna ihop, armar längs sidan','Hoppa ut fötterna och lyft armarna till sidan','Klappa händerna ovan huvud','Hoppa tillbaka till startläget']},
{id:'high_knees',name:'High Knees',muscle:'Kondition',secondary:'Core, Ben',equip:'Kroppsvikt',icon:'🏃',diff:'Nybörjare',type:'Kondition',isTimer:true,instructions:['Stå rakt','Spring på stället med höga knän','Knäna upp till höfthöjd','Håll ett snabbt tempo']},
{id:'mountain_climbers',name:'Mountain Climbers',muscle:'Kondition',secondary:'Core, Ben',equip:'Kroppsvikt',icon:'🧗',diff:'Nybörjare',type:'Kondition',isTimer:true,instructions:['Push-up position','Dra ett knä snabbt mot bröstet','Byt ben explosivt','Håll höfterna nere och jämn takt']},
{id:'jump_squat',name:'Jump Squat',muscle:'Kondition',secondary:'Ben, Rumpa',equip:'Kroppsvikt',icon:'⚡',diff:'Medel',type:'Kondition',instructions:['Axelbrett fotläge','Sänk ner i knäböj','Hoppa explosivt upp','Landa mjukt och gå direkt ner i nästa rep']},
{id:'box_jump',name:'Box Jump',muscle:'Kondition',secondary:'Ben, Rumpa',equip:'Kroppsvikt',icon:'📦',diff:'Medel',type:'Kondition',instructions:['Stå framför en box eller bänk','Böj knäna lätt','Hoppa explosivt upp på boxen','Kliv ner kontrollerat']},
{id:'treadmill',name:'Treadmill',muscle:'Kondition',secondary:'Ben',equip:'Löpband',icon:'🏃',diff:'Nybörjare',type:'Kondition',isTimer:true,instructions:['Ställ önskad hastighet','Håll upprätt hållning','Titta rakt fram','Anpassa fart efter syfte (gång/löpning/intervall)']},
{id:'stationary_bike',name:'Stationary Bike',muscle:'Kondition',secondary:'Ben',equip:'Maskin',icon:'🚴',diff:'Nybörjare',type:'Kondition',isTimer:true,instructions:['Justera sadelhöjd, benen nästan raka vid botten','Håll upprätt hållning','Pedala med jämnt motstånd','Anpassa intensitet efter syfte']},
];
const EQUIP_CHIPS=['Kroppsvikt','Hantel','Kettlebell','Skivstång','Kabelmaskin','Maskin','Motståndsband','Löpband'];
const MUSCLE_FILTER=['Alla','Bröst','Rygg','Ben','Axlar','Armar','Core','Helkropp','Kondition'];
// ── FOODS (per 100g) ──────────────────────────────────────
const FOODS=[
// Kött & Fisk
{id:'chicken_breast', name:'Kycklingfilé', cal:165, prot:31, carbs:0, fat:3.6},
{id:'ground_beef', name:'Nötfärs (10%)', cal:152, prot:20, carbs:0, fat:8},
{id:'salmon', name:'Lax', cal:208, prot:20, carbs:0, fat:13},
{id:'tuna_canned', name:'Tonfisk (konserv)', cal:116, prot:26, carbs:0, fat:1},
{id:'egg', name:'Ägg', cal:155, prot:13, carbs:1.1, fat:11},
{id:'shrimp', name:'Räkor', cal:99, prot:21, carbs:0, fat:1.1},
// Mejeri
{id:'milk', name:'Mjölk (3%)', cal:61, prot:3.2, carbs:4.7, fat:3},
{id:'greek_yogurt', name:'Grekisk yoghurt (0%)', cal:59, prot:10, carbs:3.6, fat:0.4},
{id:'cottage_cheese', name:'Keso', cal:72, prot:12, carbs:2.7, fat:1},
{id:'cheddar', name:'Ost (28%)', cal:280, prot:25, carbs:0, fat:20},
{id:'quark', name:'Kvarg', cal:66, prot:10.5,carbs:3.8, fat:0.2},
// Spannmål
{id:'oats', name:'Havregryn', cal:389, prot:17, carbs:66, fat:7},
{id:'rice_cooked', name:'Ris (kokt)', cal:130, prot:2.7, carbs:28, fat:0.3},
{id:'pasta_cooked', name:'Pasta (kokt)', cal:131, prot:5, carbs:25, fat:1.1},
{id:'white_bread', name:'Vitt bröd', cal:265, prot:9, carbs:49, fat:3.2},
{id:'rye_bread', name:'Rågbröd', cal:259, prot:9, carbs:45, fat:3.3},
{id:'tortilla', name:'Tortillabröd', cal:303, prot:8, carbs:52, fat:6.7},
// Grönsaker
{id:'broccoli', name:'Broccoli', cal:34, prot:2.8, carbs:7, fat:0.4},
{id:'spinach', name:'Spenat', cal:23, prot:2.9, carbs:3.6, fat:0.4},
{id:'potato', name:'Potatis (kokt)', cal:87, prot:2.1, carbs:19, fat:0.1},
{id:'sweet_potato', name:'Sötpotatis', cal:86, prot:1.6, carbs:20, fat:0.1},
// Frukt
{id:'banana', name:'Banan', cal:89, prot:1.1, carbs:23, fat:0.3},
{id:'apple', name:'Äpple', cal:52, prot:0.3, carbs:14, fat:0.2},
{id:'blueberry', name:'Blåbär', cal:57, prot:0.7, carbs:14, fat:0.3},
// Nötter & övrigt
{id:'almonds', name:'Mandlar', cal:579, prot:21, carbs:22, fat:50},
{id:'peanut_butter', name:'Jordnötssmör', cal:588, prot:25, carbs:20, fat:50},
{id:'olive_oil', name:'Olivolja', cal:884, prot:0, carbs:0, fat:100},
{id:'avocado', name:'Avokado', cal:160, prot:2, carbs:9, fat:15},
{id:'protein_powder', name:'Proteinpulver (vanilj)', cal:390, prot:75, carbs:10, fat:5},
{id:'kidney_beans', name:'Kidneybönor (kokt)', cal:127, prot:8.7, carbs:22, fat:0.5},
];
// ── THEMES ────────────────────────────────────────────────
const THEMES={
fortis:{
name:'Fortis',
accent:'#E85D26', accent2:'#2A6AE8', accentLight:'#FEF0EB',
bg:'#F2F1ED', card:'#FFFFFF', text:'#141414', muted:'#888880',
border:'rgba(0,0,0,0.09)', nav:'#FFFFFF', header:'#141414',
headerText:'#FFFFFF', outer:'#E8E6E0',
},
dark:{
name:'Dark',
accent:'#E85D26', accent2:'#4A8AFF', accentLight:'#2A1A14',
bg:'#121212', card:'#1E1E1E', text:'#F0F0EC', muted:'#777770',
border:'rgba(255,255,255,0.08)', nav:'#1A1A1A', header:'#0A0A0A',
headerText:'#F0F0EC', outer:'#090909',
},
midnight:{
name:'Midnight',
accent:'#5B8BFF', accent2:'#E85D26', accentLight:'#131B2E',
bg:'#0D1117', card:'#161B22', text:'#E6EDF3', muted:'#7D8590',
border:'rgba(255,255,255,0.08)', nav:'#161B22', header:'#0D1117',
headerText:'#E6EDF3', outer:'#080B10',
},
forest:{
name:'Forest',
accent:'#2D9E5F', accent2:'#2A6AE8', accentLight:'#E6F7ED',
bg:'#F0F4F0', card:'#FFFFFF', text:'#1A2E1A', muted:'#6A7A6A',
border:'rgba(0,0,0,0.08)', nav:'#FFFFFF', header:'#1A2E1A',
headerText:'#FFFFFF', outer:'#E4EAE4',
},
stone:{
name:'Stone',
accent:'#6366F1', accent2:'#EC4899', accentLight:'#EDEDFD',
bg:'#F5F5F4', card:'#FFFFFF', text:'#1C1917', muted:'#78716C',
border:'rgba(0,0,0,0.08)', nav:'#FFFFFF', header:'#1C1917',
headerText:'#FFFFFF', outer:'#E7E5E4',
},
};
function applyTheme(id){
const t=THEMES[id]||THEMES.fortis;
const r=document.documentElement.style;
r.setProperty('--accent',t.accent);
r.setProperty('--accent2',t.accent2);
r.setProperty('--accent-light',t.accentLight);
r.setProperty('--bg',t.bg);
r.setProperty('--card',t.card);
r.setProperty('--text',t.text);
r.setProperty('--muted',t.muted);
r.setProperty('--border',t.border);
r.setProperty('--nav',t.nav);
r.setProperty('--header',t.header);
r.setProperty('--header-text',t.headerText);
r.setProperty('--outer',t.outer);
document.body.style.background=t.outer;
ST.activeTheme=id;
}
async function setTheme(id){
applyTheme(id);
await save('fortis_theme',id);
renderProfile();
}
// ── ACHIEVEMENTS ──────────────────────────────────────────
const ACHIEVEMENT_DEFS=[
{id:'first_workout', icon:'🏋️', name:'Första passet', desc:'Slutför ditt första pass'},
{id:'workouts_10', icon:'💪', name:'10 pass', desc:'Slutför 10 pass totalt'},
{id:'workouts_50', icon:'🏆', name:'50 pass', desc:'Slutför 50 pass totalt'},
{id:'workouts_100', icon:'🥇', name:'100 pass', desc:'Slutför 100 pass totalt'},
{id:'streak_3', icon:'🔥', name:'3 i rad', desc:'3 dagars träningsstreak'},
{id:'streak_7', icon:'🔥', name:'Vecka på raken', desc:'7 dagars träningsstreak'},
{id:'streak_30', icon:'🔥', name:'Månadsstreak', desc:'30 dagars träningsstreak'},
{id:'early_bird', icon:'🌅', name:'Tidig fågel', desc:'Logga ett pass före 08:00'},
{id:'variety', icon:'🌟', name:'Allrounder', desc:'Träna 5 olika muskelgrupper'},
{id:'marathon', icon:'⏱️', name:'Uthållighet', desc:'Slutför ett pass längre än 60 min'},
];
function calcBestStreak(){
if(!ST.workoutLog.length)return 0;
const dates=[...new Set(ST.workoutLog.map(l=>l.date))].sort();
let best=1,cur=1;
for(let i=1;i<dates.length;i++){
const diff=(new Date(dates[i])-new Date(dates[i-1]))/86400000;
if(diff===1){cur++;best=Math.max(best,cur);}else cur=1;
}
return best;
}
function evalAchievements(){
const unlocked=new Set(ST.achievements||[]);
const toUnlock=[];
const total=ST.workoutLog.length;
const streak=calcStreak();
const best=calcBestStreak();
const check=(id,cond)=>{if(cond&&!unlocked.has(id))toUnlock.push(id);};
check('first_workout', total>=1);
check('workouts_10', total>=10);
check('workouts_50', total>=50);
check('workouts_100', total>=100);
check('streak_3', streak>=3||best>=3);
check('streak_7', streak>=7||best>=7);
check('streak_30', streak>=30||best>=30);
check('variety', (()=>{
const muscles=new Set();
for(const entry of ST.workoutLog){
const w=ST.workouts.find(x=>x.id===entry.workoutId);
if(!w)continue;
w.exercises.forEach(e=>{const ex=getExercises().find(x=>x.id===e.exerciseId);if(ex)muscles.add(ex.muscle);});
}
return muscles.size>=5;
})());
check('early_bird', ST.workoutLog.some(e=>{
const h=new Date(e.id).getHours();return h<8;
}));
check('marathon', ST.workoutLog.some(e=>e.durationMin>=60));
return toUnlock;
}
async function checkAchievements(){
const newOnes=evalAchievements();
if(!newOnes.length)return;
ST.achievements=[...(ST.achievements||[]),...newOnes];
await save('fortis_achievements',ST.achievements);
for(const id of newOnes){
const def=ACHIEVEMENT_DEFS.find(a=>a.id===id);
if(def)showAchievementToast(def);
await new Promise(r=>setTimeout(r,2200));
}
}
function showAchievementToast(def){
const el=document.getElementById('achievementToast');
el.innerHTML=`<div style="font-size:28px;margin-bottom:4px">${def.icon}</div>
<div style="font-size:11px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent);margin-bottom:2px">Achievement unlocked</div>
<div style="font-size:15px;font-weight:800;color:var(--text)">${def.name}</div>
<div style="font-size:12px;color:var(--muted);margin-top:2px">${def.desc}</div>`;
el.classList.add('show');
setTimeout(()=>el.classList.remove('show'),2000);
}
// ── STATE ─────────────────────────────────────────────────
let ST={
profile:null,
activeTheme:'fortis',
obStep:1,
obData:{name:'',gender:'male',age:'',height:'',weight:'',goalWeight:'',goal:'lose',activity:'moderate'},
macroGoals:{calories:2200,protein:165,carbs:220,fat:73},
waterToday:0,
waterGoal:2500,
activeTab:'Home',
selectedDay:new Date().getDay()==0?0:new Date().getDay()-1,
workouts:[],
workoutLog:[],
achievements:[],
foodLog:[],
_addFoodMeal:null,
_addFoodSelected:null,
activeWorkout:null,
awStartTime:null,
awTimerInterval:null,
restInterval:null,
equipFilter:[],
muscleFilter:'Alla',
diffFilter:'Alla',
searchQ:'',
customExercises:[],
_createExInstructions:[],
};
// ── STORAGE ───────────────────────────────────────────────
async function load(key,def=null){
if(!window.storage)return def;
try{const r=await window.storage.get(key);return r?JSON.parse(r.value):def;}catch{return def;}
}
async function save(key,val){
if(!window.storage)return;
try{await window.storage.set(key,JSON.stringify(val));}catch{}
}
// ── INIT ──────────────────────────────────────────────────
async function init(){
ST.profile=await load('fortis_profile');
ST.waterToday=await load('fortis_water_'+today(),0);
ST.workouts=await load('fortis_workouts',defaultWorkouts());
ST.workoutLog=await load('fortis_workoutlog',[]);
ST.achievements=await load('fortis_achievements',[]);
ST.foodLog=await load('fortis_foodlog',[]);
ST.customExercises=await load('fortis_custom_exercises',[]);
const savedTheme=await load('fortis_theme','fortis');
applyTheme(savedTheme);
startSplash();
}
function today(){return new Date().toISOString().slice(0,10);}
function defaultWorkouts(){
return[
{id:'w1',name:'Hemmapass',day:0,exercises:[
{exerciseId:'situp',sets:3,reps:30,weight:0,restSec:60},
{exerciseId:'pushup',sets:3,reps:20,weight:0,restSec:60},
{exerciseId:'squat',sets:3,reps:20,weight:20,restSec:90},
{exerciseId:'plank',sets:1,durationSec:60,weight:0,restSec:60},
{exerciseId:'dumbbell_curl',sets:3,reps:15,weight:12,restSec:60},
]},
{id:'w2',name:'Konditionspass',day:2,exercises:[
{exerciseId:'burpee',sets:3,reps:12,weight:0,restSec:90},
{exerciseId:'lunges',sets:3,reps:20,weight:0,restSec:60},
{exerciseId:'russian_twist',sets:3,reps:30,weight:0,restSec:45},
]},
{id:'w3',name:'Rygg & Biceps',day:4,exercises:[
{exerciseId:'pullup',sets:3,reps:8,weight:0,restSec:120},
{exerciseId:'cable_row',sets:3,reps:12,weight:40,restSec:90},
{exerciseId:'dumbbell_curl',sets:3,reps:12,weight:14,restSec:60},
]},
];
}
// ── SPLASH ────────────────────────────────────────────────
function startSplash(){
document.getElementById('splash').style.display='flex';
const fill=document.getElementById('splashFill');
let p=0;
const iv=setInterval(()=>{
p+=100/30;fill.style.width=Math.min(p,100)+'%';
if(p>=100){clearInterval(iv);endSplash();}
},100);
}
function endSplash(){
document.getElementById('splash').style.opacity='0';
document.getElementById('splash').style.transition='opacity 0.4s';
setTimeout(()=>{
document.getElementById('splash').style.display='none';
if(ST.profile){showApp();}else{showOnboarding();}
},400);
}
// ── ONBOARDING ────────────────────────────────────────────
const OB=[
{title:'Välkommen till FORTIS',desc:'Din personliga träningskompis. Sätt upp mål, följ din progress och bli starkare — varje dag.',btn:'Kom igång'},
{title:'Om dig',desc:'Vi behöver lite grundläggande info för att anpassa appen till dig.',btn:'Nästa'},
{title:'Ditt mål',desc:'Berätta vad du vill uppnå och hur aktiv du är till vardags.',btn:'Beräkna mål'},
{title:'Kalori- och makromål',desc:'Baserat på din info har vi räknat ut ditt dagliga mål. Du kan justera det manuellt.',btn:'Acceptera'},
{title:'Allt klart!',desc:'',btn:'Starta FORTIS'},
];
function showOnboarding(){
document.getElementById('onboarding').style.display='block';
renderOB();
}
function renderOB(){
const s=ST.obStep;
document.getElementById('obSteps').innerHTML=OB.map((_,i)=>`<div class="ob-step${i<s?' done':''}"></div>`).join('');
document.getElementById('obTitle').textContent=OB[s-1].title;
document.getElementById('obDesc').textContent=OB[s-1].desc;
const backBtn=s>1&&s<5?'<button class="btn-ghost" onclick="obBack()">Tillbaka</button>':'';
const skipBtn=s===1?'<button class="btn-ghost" onclick="skipOnboarding()">Hoppa över</button>':'';
document.getElementById('obFooter').innerHTML=`<button class="btn-primary" onclick="obNext()">${OB[s-1].btn}</button>${backBtn}${skipBtn}`;
const body=document.getElementById('obBody');
if(s===1){body.innerHTML=`<div style="text-align:center;padding:32px 0"><div style="font-size:64px;margin-bottom:16px">💪</div><p style="font-size:15px;color:var(--muted);line-height:1.7">Logga pass · Spåra kost · Se din progress</p></div>`;}
else if(s===2){body.innerHTML=`
<div class="field"><label>Namn</label><input type="text" value="${ST.obData.name}" placeholder="Ditt namn" oninput="ST.obData.name=this.value"></div>
<div class="field"><label>Kön</label><div class="radio-group">
<div class="radio-opt ${ST.obData.gender==='male'?'active':''}" onclick="setOBField('gender','male',this,'radio-opt')">Man</div>
<div class="radio-opt ${ST.obData.gender==='female'?'active':''}" onclick="setOBField('gender','female',this,'radio-opt')">Kvinna</div>
<div class="radio-opt ${ST.obData.gender==='other'?'active':''}" onclick="setOBField('gender','other',this,'radio-opt')">Annat</div>
</div></div>
<div class="field"><label>Ålder</label><input type="number" value="${ST.obData.age}" placeholder="år" oninput="ST.obData.age=this.value" min="10" max="99"></div>`;}
else if(s===3){body.innerHTML=`
<div class="field-row">
<div class="field"><label>Längd (cm)</label><input type="number" value="${ST.obData.height}" placeholder="cm" oninput="ST.obData.height=this.value" min="140" max="220"></div>
<div class="field"><label>Vikt (kg)</label><input type="number" value="${ST.obData.weight}" placeholder="kg" oninput="ST.obData.weight=this.value" min="40" max="200"></div>
</div>
<div class="field"><label>Målvikt (kg)</label><input type="number" value="${ST.obData.goalWeight}" placeholder="kg" oninput="ST.obData.goalWeight=this.value"></div>
<div class="field"><label>Mål</label><div class="radio-group">
<div class="radio-opt ${ST.obData.goal==='lose'?'active':''}" onclick="setOBField('goal','lose',this,'radio-opt')">Gå ner</div>
<div class="radio-opt ${ST.obData.goal==='maintain'?'active':''}" onclick="setOBField('goal','maintain',this,'radio-opt')">Hålla vikt</div>
<div class="radio-opt ${ST.obData.goal==='gain'?'active':''}" onclick="setOBField('goal','gain',this,'radio-opt')">Bygga muskler</div>
</div></div>
<div class="field"><label>Aktivitetsnivå</label><select onchange="ST.obData.activity=this.value">
<option value="sedentary" ${ST.obData.activity==='sedentary'?'selected':''}>Stillasittande (lite/ingen träning)</option>
<option value="light" ${ST.obData.activity==='light'?'selected':''}>Lätt aktiv (1–3 dagar/vecka)</option>
<option value="moderate" ${ST.obData.activity==='moderate'?'selected':''}>Måttligt aktiv (3–5 dagar/vecka)</option>
<option value="very_active" ${ST.obData.activity==='very_active'?'selected':''}>Mycket aktiv (6–7 dagar/vecka)</option>
</select></div>`;}
else if(s===4){
calcMacros();
body.innerHTML=`
<div class="macro-preview">
<div class="macro-row"><span class="macro-name">Kalorier</span><span class="macro-val">${ST.macroGoals.calories}<span class="macro-unit"> kcal</span></span></div>
<div class="macro-row"><span class="macro-name">Protein</span><span class="macro-val">${ST.macroGoals.protein}<span class="macro-unit"> g</span></span></div>
<div class="macro-row"><span class="macro-name">Kolhydrater</span><span class="macro-val">${ST.macroGoals.carbs}<span class="macro-unit"> g</span></span></div>
<div class="macro-row"><span class="macro-name">Fett</span><span class="macro-val">${ST.macroGoals.fat}<span class="macro-unit"> g</span></span></div>
</div>
<p style="font-size:13px;color:var(--muted);margin-top:14px;line-height:1.6">Beräknat med Mifflin-St Jeor-formeln. Du kan justera detta när som helst under Profil → Inställningar.</p>`;}
else if(s===5){body.innerHTML=`<div class="done-screen"><div class="done-icon">🚀</div><div class="done-title">Dags att köra, ${ST.obData.name||'champ'}!</div><div class="done-sub">Ditt program är klart. Börja med att kolla in ditt schema och hoppa in i ditt första pass.</div></div>`;}
}
function setOBField(field,val,el,cls){
ST.obData[field]=val;
document.querySelectorAll('.'+cls).forEach(e=>e.classList.remove('active'));
el.classList.add('active');
}
function calcMacros(){
const d=ST.obData;
const w=parseFloat(d.weight)||80,h=parseFloat(d.height)||175,a=parseInt(d.age)||30;
let bmr=d.gender==='female'?(10*w+6.25*h-5*a-161):(10*w+6.25*h-5*a+5);
const mult={sedentary:1.2,light:1.375,moderate:1.55,very_active:1.725}[d.activity]||1.55;
let tdee=Math.round(bmr*mult);
if(d.goal==='lose')tdee-=400;
if(d.goal==='gain')tdee+=300;
ST.macroGoals={
calories:tdee,
protein:Math.round(w*2.2),
carbs:Math.round((tdee*0.4)/4),
fat:Math.round((tdee*0.3)/9),
};
}
function obNext(){
if(ST.obStep===5){saveProfile();showApp();return;}
ST.obStep++;renderOB();
}
function obBack(){ST.obStep--;renderOB();}
async function skipOnboarding(){
ST.obData={name:'',gender:'male',age:25,height:175,weight:75,goalWeight:75,goal:'maintain',activity:'moderate'};
ST.macroGoals={calories:2200,protein:165,carbs:220,fat:73};
await saveProfile();
showApp();
}
async function saveProfile(){
ST.profile={...ST.obData,macroGoals:ST.macroGoals,createdAt:new Date().toISOString()};
await save('fortis_profile',ST.profile);
await save('fortis_macrogoals',ST.macroGoals);
document.getElementById('onboarding').style.display='none';
}
// ── MAIN APP ──────────────────────────────────────────────
function showApp(){
document.getElementById('onboarding').style.display='none';
const app=document.getElementById('app');
app.style.display='flex';
if(ST.profile){
const m=ST.profile.macroGoals||ST.macroGoals;
ST.macroGoals=m;
}
renderHome();
renderScheduleSetup();
renderLibrary();
renderNutrition();
renderProfile();
}
function switchTab(tab){
['Home','Schedule','Library','Nutrition','Profile'].forEach(t=>{
document.getElementById('tab'+t).style.display=t===tab?'':'none';
document.getElementById('nav'+t).classList.toggle('active',t===tab);
});
ST.activeTab=tab;
}
// ── HOME ──────────────────────────────────────────────────
function renderHome(){
const p=ST.profile||{name:'Jens'};
document.getElementById('homeGreeting').textContent=p.name||'Jens';
document.getElementById('streakCount').textContent=calcStreak();
renderCalorieRing(0,ST.macroGoals.calories);
renderMacros(0,0,0);
renderWater();
renderTodayWorkout();
}
function renderCalorieRing(consumed,goal){
const left=Math.max(0,goal-consumed);
document.getElementById('calsLeft').textContent=left;
const pct=Math.min(consumed/goal,1);
const circ=264;
document.getElementById('calRing').style.strokeDashoffset=circ-(circ*pct);
}
function renderMacros(p,c,f){
document.getElementById('mProtein').innerHTML=`${p}<span class="rm-unit">g</span>`;
document.getElementById('mCarbs').innerHTML=`${c}<span class="rm-unit">g</span>`;
document.getElementById('mFat').innerHTML=`${f}<span class="rm-unit">g</span>`;
document.getElementById('mProteinBar').style.width=Math.min(p/ST.macroGoals.protein*100,100)+'%';
document.getElementById('mCarbsBar').style.width=Math.min(c/ST.macroGoals.carbs*100,100)+'%';
document.getElementById('mFatBar').style.width=Math.min(f/ST.macroGoals.fat*100,100)+'%';
}
function renderWater(){
const portions=10;
const filled=Math.round((ST.waterToday/ST.waterGoal)*portions);
document.getElementById('waterDrops').innerHTML=Array.from({length:portions},(_,i)=>`<div class="drop ${i<filled?'filled':''}"></div>`).join('');
document.getElementById('waterLabel').textContent=`${ST.waterToday} / ${ST.waterGoal} ml`;
}
async function addWater(){
ST.waterToday=Math.min(ST.waterToday+250,ST.waterGoal);
await save('fortis_water_'+today(),ST.waterToday);
renderWater();
}
function renderTodayWorkout(){
const dow=new Date().getDay()==0?6:new Date().getDay()-1;
const w=ST.workouts.find(x=>x.day===dow);
const card=document.getElementById('todayWorkoutCard');
if(!w){card.innerHTML=`<div class="no-workout">Ingen träning schemalagd idag 😴<br><span style="font-size:12px">Lägg till ett pass i Schema</span></div>`;return;}
const exs=w.exercises.map(e=>getExercises().find(x=>x.id===e.exerciseId)).filter(Boolean);
const muscles=[...new Set(exs.map(e=>e.muscle))];
card.innerHTML=`
<div class="workout-card-inner">
<div style="flex:1;min-width:0">
<div class="workout-name">${w.name}</div>
<div class="workout-meta">${w.exercises.length} övningar · ~${w.exercises.length*8} min</div>
<div class="muscle-badges">${muscles.map((m,i)=>`<span class="badge ${['badge-orange','badge-blue','badge-green'][i%3]}">${m}</span>`).join('')}</div>
</div>
<button class="btn-start" onclick="startWorkout('${w.id}')">Starta</button>
</div>`;
}
function calcStreak(){
if(!ST.workoutLog.length)return 0;
let s=0;const d=new Date();
while(true){
const key=d.toISOString().slice(0,10);
if(ST.workoutLog.some(l=>l.date===key)){s++;d.setDate(d.getDate()-1);}
else break;
}
return s;
}
// ── SCHEDULE ─────────────────────────────────────────────
function renderScheduleSetup(){
const days=['Mån','Tis','Ons','Tor','Fre','Lör','Sön'];
const todayDow=new Date().getDay()==0?6:new Date().getDay()-1;
const strip=document.getElementById('weekStrip');
strip.innerHTML=days.map((d,i)=>{
const hasW=ST.workouts.some(w=>w.day===i);
return`<div class="day-pill ${i===ST.selectedDay?'active':''}" onclick="selectDay(${i})">
<div class="day-name">${d}</div>
<div class="day-num" style="${i===todayDow?'color:#E85D26':''}">${i+1}</div>
${hasW?'<div class="day-dot"></div>':'<div style="width:4px;height:4px"></div>'}
</div>`;
}).join('');
renderDaySchedule();
}
function selectDay(i){
ST.selectedDay=i;renderScheduleSetup();}
function renderDaySchedule(){
const days=['Måndag','Tisdag','Onsdag','Torsdag','Fredag','Lördag','Söndag'];
const body=document.getElementById('schedBody');
const ws=ST.workouts.filter(w=>w.day===ST.selectedDay);
let html=`<div class="day-title">${days[ST.selectedDay]}</div>`;
ws.forEach(w=>{
const exs=w.exercises.map(e=>({...e,ex:getExercises().find(x=>x.id===e.exerciseId)})).filter(e=>e.ex);
html+=`<div class="workout-block">
<div class="wb-header"><div class="wb-name">${w.name}</div><div class="wb-time">${w.exercises.length} övn · ~${w.exercises.length*8} min</div></div>
<div class="wb-exercises">
${exs.map(e=>`<div class="wb-ex"><div class="wb-ex-dot"></div>${e.ex.name}<div class="wb-ex-detail">${e.sets}×${e.reps||e.durationSec+'s'}${e.weight?' @ '+e.weight+'kg':''}</div></div>`).join('')}
</div>
<div class="wb-footer"><button class="btn-start-full" onclick="startWorkout('${w.id}')">Starta pass</button></div>
</div>`;
});
html+=`<button class="add-workout-btn" onclick="showAddWorkout()">+ Lägg till pass</button>`;
body.innerHTML=html;
}
function showAddWorkout(){
document.getElementById('modalTitle').textContent='Nytt pass';
document.getElementById('modalBody').innerHTML=`
<div class="field"><label>Passnamn</label><input type="text" id="newWName" placeholder="t.ex. Benstyrka"></div>
<p style="font-size:13px;color:var(--muted);margin-bottom:16px">Övningar läggs till från biblioteket efter att passet skapats.</p>
<button class="btn-primary" onclick="createWorkout()">Skapa pass</button>`;
document.getElementById('modalOverlay').classList.add('show');
}
function createWorkout(){
const name=document.getElementById('newWName').value.trim();
if(!name)return;
const w={id:'w'+Date.now(),name,day:ST.selectedDay,exercises:[]};
ST.workouts.push(w);
save('fortis_workouts',ST.workouts);
closeModal();
renderScheduleSetup();
}
// ── LIBRARY ───────────────────────────────────────────────
function getExercises(){return[...EXERCISES,...ST.customExercises];}
function renderLibrary(){
const chips=document.getElementById('equipChips');
chips.innerHTML=EQUIP_CHIPS.map(c=>`<div class="chip ${ST.equipFilter.includes(c)?'active':''}" onclick="toggleChip('${c}')">${c}</div>`).join('');
updateFilterBadge();
filterExercises();
}
function toggleChip(c){
ST.equipFilter=ST.equipFilter.includes(c)?ST.equipFilter.filter(x=>x!==c):[...ST.equipFilter,c];
renderLibrary();
}
function updateFilterBadge(){
const count=(ST.muscleFilter!=='Alla'?1:0)+(ST.diffFilter!=='Alla'?1:0);
const badge=document.getElementById('filterBadge');
if(!badge)return;
if(count>0){badge.textContent=count;badge.style.display='inline';}
else{badge.style.display='none';}
}
function filterExercises(){
ST.searchQ=document.getElementById('libSearch')?.value.toLowerCase()||'';
let exs=getExercises();
if(ST.equipFilter.length)exs=exs.filter(e=>ST.equipFilter.includes(e.equip));
if(ST.muscleFilter!=='Alla')exs=exs.filter(e=>e.muscle===ST.muscleFilter);
if(ST.diffFilter!=='Alla')exs=exs.filter(e=>e.diff===ST.diffFilter);
if(ST.searchQ)exs=exs.filter(e=>e.name.toLowerCase().includes(ST.searchQ));
const colors={'Kroppsvikt':'#FEF0EB','Hantel':'#EBF0FE','Kettlebell':'#EAFAEE','Skivstång':'#FFF8E6','Kabelmaskin':'#F3EBFE','Maskin':'#F0F0EE'};
document.getElementById('libBody').innerHTML=exs.length?
exs.map(e=>`<div class="ex-card${e.isCustom?' ex-card-custom':''}" onclick="showExercise('${e.id}')">
<div class="ex-icon" style="background:${colors[e.equip]||'#F5F5F3'}">${e.icon||'⚡'}</div>
<div class="ex-info">
<div class="ex-name">${e.name}${e.isCustom?'<span style="font-size:10px;font-weight:700;color:var(--accent);margin-left:6px">EGEN</span>':''}</div>
<div class="ex-meta">${e.muscle} · ${e.diff}</div>
</div>
<div class="ex-equip">${e.equip}</div>
</div>`).join('')
:'<div style="text-align:center;padding:32px;color:var(--muted)">Inga övningar hittades</div>';
}
function showExercise(id){
const e=getExercises().find(x=>x.id===id);if(!e)return;
document.getElementById('modalTitle').textContent=e.name;
document.getElementById('modalBody').innerHTML=`
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<span class="badge badge-orange">${e.muscle}</span>
<span class="badge badge-blue">${e.equip}</span>
<span class="badge" style="background:var(--bg);color:var(--muted)">${e.diff}</span>
</div>
<div style="font-size:13px;color:var(--muted);margin-bottom:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em">Sekundära muskler</div>
<div style="font-size:14px;color:var(--text);margin-bottom:16px">${e.secondary||'—'}</div>
<div style="font-size:13px;color:var(--muted);margin-bottom:8px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em">Utförande</div>
${e.instructions.map((s,i)=>`<div style="display:flex;gap:10px;margin-bottom:8px"><div style="width:22px;height:22px;border-radius:50%;background:var(--accent);color:#FFF;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0">${i+1}</div><div style="font-size:14px;color:var(--text);line-height:1.5">${s}</div></div>`).join('')}
<button class="btn-primary" style="margin-top:20px" onclick="showProgression('${id}')">📈 Se progression</button>
${e.isCustom?`<button class="btn-ghost" style="color:#E53E3E;border-color:#E53E3E;margin-top:8px" onclick="deleteCustomExercise('${id}')">🗑 Ta bort övning</button>`:''}
<button class="btn-ghost" onclick="closeModal()">Stäng</button>`;
document.getElementById('modalOverlay').classList.add('show');
}
function getExerciseProgression(exId){
const points=[];
const sorted=[...ST.workoutLog].sort((a,b)=>a.date<b.date?-1:1);
for(const entry of sorted){
const workout=ST.workouts.find(w=>w.id===entry.workoutId);
if(!workout)continue;
const exIdx=workout.exercises.findIndex(e=>e.exerciseId===exId);
if(exIdx===-1)continue;
const sets=(entry.exercises||[])[exIdx];
if(!sets||!sets.length)continue;
const done=sets.filter(s=>s.done);
if(!done.length)continue;
const maxW=Math.max(...done.map(s=>s.weight||0));
const maxR=Math.max(...done.map(s=>s.reps||0));
const vol=done.reduce((sum,s)=>sum+(s.reps||0)*(s.weight||0),0);
points.push({date:entry.date,maxW,maxR,vol,sets:done.length});
}
return points;
}
function buildProgressionSVG(points,isBodyweight){
const W=300,H=140,PL=36,PR=8,PT=10,PB=28;
const PW=W-PL-PR,PH=H-PT-PB;
const vals=points.map(p=>isBodyweight?p.maxR:p.maxW||p.maxR);
const maxV=Math.max(...vals,1)*1.15;
const prIdx=vals.indexOf(Math.max(...vals));
const xs=points.map((_,i)=>PL+(points.length>1?i/(points.length-1)*PW:PW/2));
const ys=vals.map(v=>PT+PH-(v/maxV)*PH);
const line=xs.map((x,i)=>`${i?'L':'M'}${x.toFixed(1)},${ys[i].toFixed(1)}`).join('');
const area=`${line}L${xs[xs.length-1].toFixed(1)},${PT+PH}L${xs[0].toFixed(1)},${PT+PH}Z`;
const grid=[0.25,0.5,0.75,1].map(f=>{
const y=PT+PH-f*PH;
return`<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="var(--border)" stroke-width="1"/>
<text x="${PL-4}" y="${(y+3.5).toFixed(1)}" text-anchor="end" font-size="9" fill="var(--muted)">${Math.round(f*maxV)}</text>`;
}).join('');
const step=Math.ceil(points.length/5);
const xlabels=points.map((p,i)=>{
if(i%step!==0&&i!==points.length-1)return'';
return`<text x="${xs[i].toFixed(1)}" y="${PT+PH+16}" text-anchor="middle" font-size="8" fill="var(--muted)">${p.date.slice(5)}</text>`;
}).join('');
const dots=xs.map((x,i)=>{
const pr=i===prIdx;
return`<circle cx="${x.toFixed(1)}" cy="${ys[i].toFixed(1)}" r="${pr?5.5:3.5}" fill="${pr?'var(--accent)':'var(--accent2)'}" stroke="white" stroke-width="1.5"/>`;
}).join('');
return`<svg viewBox="0 0 ${W} ${H}" style="width:100%;overflow:visible">
<defs><linearGradient id="ag" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--accent)" stop-opacity="0.18"/>
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
</linearGradient></defs>
${grid}
<line x1="${PL}" y1="${PT}" x2="${PL}" y2="${PT+PH}" stroke="var(--border)" stroke-width="1"/>
<line x1="${PL}" y1="${PT+PH}" x2="${W-PR}" y2="${PT+PH}" stroke="var(--border)" stroke-width="1"/>
${xlabels}
<path d="${area}" fill="url(#ag)"/>
<path d="${line}" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
${dots}
</svg>`;
}
function showProgression(id){
const e=getExercises().find(x=>x.id===id);if(!e)return;
const points=getExerciseProgression(id);
const isBodyweight=e.equip==='Kroppsvikt'||e.isTimer;
const unit=isBodyweight?'reps':'kg';
const prVal=points.length?Math.max(...points.map(p=>isBodyweight?p.maxR:p.maxW||p.maxR)):0;
const lastVal=points.length?(isBodyweight?points[points.length-1].maxR:points[points.length-1].maxW||0):0;
document.getElementById('modalTitle').textContent=e.name;
document.getElementById('modalBody').innerHTML=`
<button onclick="showExercise('${id}')" style="background:none;border:none;color:var(--accent);font-size:13px;font-weight:600;cursor:pointer;padding:0;margin-bottom:16px">← Info</button>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:20px">
<div style="background:var(--bg);border-radius:var(--radius-sm);padding:12px;text-align:center">
<div style="font-size:20px;font-weight:800;color:var(--accent)">${prVal||'—'}</div>
<div style="font-size:10px;color:var(--muted);margin-top:2px">PR (${unit})</div>
</div>
<div style="background:var(--bg);border-radius:var(--radius-sm);padding:12px;text-align:center">
<div style="font-size:20px;font-weight:800;color:var(--text)">${points.length}</div>
<div style="font-size:10px;color:var(--muted);margin-top:2px">Pass</div>
</div>
<div style="background:var(--bg);border-radius:var(--radius-sm);padding:12px;text-align:center">
<div style="font-size:20px;font-weight:800;color:var(--text)">${lastVal||'—'}</div>
<div style="font-size:10px;color:var(--muted);margin-top:2px">Senast (${unit})</div>
</div>
</div>
<div style="font-size:11px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin-bottom:10px">${isBodyweight?'Max reps per pass':'Max vikt per pass (kg)'}</div>
${points.length>=2
? buildProgressionSVG(points,isBodyweight)
: `<div style="background:var(--bg);border-radius:var(--radius);padding:32px 16px;text-align:center;color:var(--muted);font-size:14px">${points.length===1?'Logga minst 2 pass för att se graf':'Inga loggade pass ännu'}</div>`}
<button class="btn-ghost" onclick="closeModal()" style="margin-top:16px">Stäng</button>`;
}
// ── NUTRITION ─────────────────────────────────────────────
function todayFoodLog(){return ST.foodLog.filter(e=>e.date===today());}
function foodMacros(f,g){
const r=g/100;
return{cal:Math.round(f.cal*r),prot:Math.round(f.prot*r*10)/10,carbs:Math.round(f.carbs*r*10)/10,fat:Math.round(f.fat*r*10)/10};
}
function renderNutrition(){
const d=new Date();
document.getElementById('nutrDate').textContent=d.toLocaleDateString('sv-SE',{weekday:'long',day:'numeric',month:'long'});
const log=todayFoodLog();
const g=ST.macroGoals;
const totals=log.reduce((s,e)=>({cal:s.cal+e.cal,prot:s.prot+e.prot,carbs:s.carbs+e.carbs,fat:s.fat+e.fat}),{cal:0,prot:0,carbs:0,fat:0});
const pctW=(v,max)=>Math.min(v/max*100,100).toFixed(1)+'%';
document.getElementById('nutrProtGoal').textContent=g.protein;
document.getElementById('nutrCarbGoal').textContent=g.carbs;
document.getElementById('nutrFatGoal').textContent=g.fat;
document.getElementById('nutrSummary').innerHTML=`
<div class="nutr-cal-row">
<div><div class="nutr-cal-num">${Math.round(totals.cal)}</div><div style="font-size:11px;color:var(--muted);letter-spacing:0.06em;text-transform:uppercase">kcal idag</div></div>
<div style="text-align:right"><div style="font-size:13px;font-weight:700;color:var(--text)">${g.calories-Math.round(totals.cal)}</div><div style="font-size:11px;color:var(--muted)">kcal kvar</div></div>
</div>
${[['Protein','#E85D26',totals.prot,g.protein,'g'],['Kolhydrater','#2A6AE8',totals.carbs,g.carbs,'g'],['Fett','#22A647',totals.fat,g.fat,'g']].map(([n,c,v,max])=>`
<div class="rm-row" style="margin-bottom:10px">
<div class="rm-label"><span class="rm-name">${n}</span><span class="rm-val">${v} / ${max}${''}</span></div>
<div class="rm-bar"><div class="rm-fill" style="background:${c};width:${pctW(v,max)}"></div></div>
</div>`).join('')}`;
const MEAL_IDS=[['breakfast','Frukost'],['lunch','Lunch'],['dinner','Middag'],['snack','Mellanmål']];
document.getElementById('mealCards').innerHTML=MEAL_IDS.map(([id,label])=>{
const items=log.filter(e=>e.meal===id);
const mCal=items.reduce((s,e)=>s+e.cal,0);
return`<div class="meal-card">
<div class="meal-header">
<span class="meal-name">${label}</span>
<div style="display:flex;align-items:center;gap:10px">
<span class="meal-cal">${mCal?Math.round(mCal)+' kcal':''}</span>
<button class="meal-add" onclick="openAddFood('${id}')">+ Lägg till</button>
</div>
</div>
${items.length?items.map(e=>`
<div class="food-item">
<div style="flex:1">
<div class="food-item-name">${e.foodName}</div>
<div class="food-item-detail">${e.amountG}g · ${Math.round(e.cal)} kcal</div>
</div>
<div class="food-item-macros">P${e.prot}·K${e.carbs}·F${e.fat}</div>
<button class="food-item-del" onclick="removeFoodEntry('${e.id}')">✕</button>
</div>`).join('')
:`<div class="meal-empty">Inga livsmedel tillagda</div>`}
</div>`;
}).join('');
}
function openAddFood(meal){
ST._addFoodMeal=meal;
ST._addFoodSelected=null;
const labels={breakfast:'Frukost',lunch:'Lunch',dinner:'Middag',snack:'Mellanmål'};
document.getElementById('modalTitle').textContent='Lägg till — '+labels[meal];
document.getElementById('modalBody').innerHTML=`
<input id="foodSearchInput" type="text" placeholder="Sök livsmedel..." oninput="renderFoodSearch(this.value)"
style="width:100%;padding:12px 14px;border:1.5px solid var(--border);border-radius:var(--radius-sm);font-size:15px;color:var(--text);background:var(--bg);outline:none;margin-bottom:4px">
<div id="foodSearchResults"></div>
<div id="foodAmountSection" style="display:none">
<div class="food-amount-section">
<div style="font-size:14px;font-weight:700;color:var(--text);margin-bottom:10px" id="foodSelectedName"></div>
<label style="font-size:12px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--muted)">Mängd (gram)</label>
<input id="foodAmountInput" type="number" value="100" min="1" max="2000"
style="width:100%;padding:12px;border:1.5px solid var(--border);border-radius:var(--radius-sm);font-size:20px;font-weight:700;color:var(--text);background:var(--card);text-align:center;margin-top:6px;outline:none"
oninput="updateFoodPreview()">
<div class="food-amount-preview" id="foodPreview"></div>
</div>
<button class="btn-primary" onclick="confirmAddFood()">Lägg till i logg</button>
</div>`;
renderFoodSearch('');
document.getElementById('modalOverlay').classList.add('show');
setTimeout(()=>document.getElementById('foodSearchInput')?.focus(),100);
}
function renderFoodSearch(q){
const lower=q.toLowerCase();
const results=FOODS.filter(f=>!q||f.name.toLowerCase().includes(lower)).slice(0,12);
document.getElementById('foodSearchResults').innerHTML=results.map(f=>`
<div class="food-search-item" onclick="selectFood('${f.id}')">
<div>
<div class="food-search-name">${f.name}</div>
<div class="food-search-meta">per 100g: ${f.cal} kcal · P${f.prot}g · K${f.carbs}g · F${f.fat}g</div>
</div>
<div style="font-size:18px;color:var(--muted)">›</div>
</div>`).join('')||'<div style="padding:20px;text-align:center;color:var(--muted)">Inga träffar</div>';
}
function selectFood(id){
ST._addFoodSelected=FOODS.find(f=>f.id===id);
document.getElementById('foodAmountSection').style.display='block';
document.getElementById('foodSelectedName').textContent=ST._addFoodSelected.name;
document.getElementById('foodSearchResults').style.display='none';
document.getElementById('foodSearchInput').style.display='none';
updateFoodPreview();
}
function updateFoodPreview(){
const f=ST._addFoodSelected;if(!f)return;
const g=parseFloat(document.getElementById('foodAmountInput').value)||100;
const m=foodMacros(f,g);
document.getElementById('foodPreview').innerHTML=`<b>${m.cal} kcal</b> · Protein ${m.prot}g · Kolhydrater ${m.carbs}g · Fett ${m.fat}g`;
}
async function confirmAddFood(){
const f=ST._addFoodSelected;if(!f)return;
const g=parseFloat(document.getElementById('foodAmountInput').value)||100;
const m=foodMacros(f,g);
const entry={id:'fl'+Date.now(),date:today(),meal:ST._addFoodMeal,foodId:f.id,foodName:f.name,amountG:g,cal:m.cal,prot:m.prot,carbs:m.carbs,fat:m.fat};
ST.foodLog.push(entry);
await save('fortis_foodlog',ST.foodLog);
document.getElementById('modalOverlay').classList.remove('show');
renderNutrition();
}
async function removeFoodEntry(id){
ST.foodLog=ST.foodLog.filter(e=>e.id!==id);
await save('fortis_foodlog',ST.foodLog);
renderNutrition();
}
// ── PROFILE ───────────────────────────────────────────────
function renderProfile(){
const p=ST.profile;
if(!p)return;
const initials=(p.name||'?').split(' ').map(n=>n[0]).join('').toUpperCase().slice(0,2);
document.getElementById('profAvatar').textContent=initials;
document.getElementById('profName').textContent=p.name||'—';
const goals={lose:'Gå ner i vikt',maintain:'Hålla vikt',gain:'Bygga muskler'};
document.getElementById('profGoalText').textContent=`Mål: ${goals[p.goal]||'—'} · ${p.weight||'—'} kg`;
document.getElementById('psTotal').textContent=ST.workoutLog.length;
document.getElementById('psStreak').textContent=calcStreak();
document.getElementById('psWeight').textContent=p.weight||'—';
const unlocked=new Set(ST.achievements||[]);
document.getElementById('achGrid').innerHTML=ACHIEVEMENT_DEFS.map(a=>`
<div class="ach-card ${unlocked.has(a.id)?'':'locked'}">
<div class="ach-icon">${a.icon}</div>
<div><div class="ach-name">${a.name}</div><div class="ach-desc">${a.desc}</div></div>
</div>`).join('');
const g=ST.macroGoals;
const themeHTML=`
<div class="section-title" style="padding:0 0 10px;margin-top:20px">Tema</div>
<div class="theme-row">
${Object.entries(THEMES).map(([id,t])=>`
<div style="flex:1">
<div class="theme-swatch ${ST.activeTheme===id?'active':''}" onclick="setTheme('${id}')">
<div class="theme-swatch-top" style="background:${t.bg}"></div>
<div class="theme-swatch-bot" style="background:${t.accent}"></div>
<div class="theme-swatch-check">✓</div>
</div>
<div class="theme-label">${t.name}</div>
</div>`).join('')}
</div>`;
document.getElementById('settingsItems').innerHTML=themeHTML+[
{label:'Kalorimål',val:g.calories+' kcal'},
{label:'Proteinmål',val:g.protein+'g'},
{label:'Längd',val:(p.height||'—')+'cm'},
{label:'Målvikt',val:(p.goalWeight||'—')+'kg'},
{label:'Vattenmål',val:ST.waterGoal+'ml'},
].map(i=>`<div class="settings-item"><span class="si-label">${i.label}</span><span class="si-val">${i.val}</span></div>`).join('');
}
// ── ACTIVE WORKOUT ────────────────────────────────────────
function startWorkout(wid){
const w=ST.workouts.find(x=>x.id===wid);if(!w)return;
ST.activeWorkout={...w,setStates:w.exercises.map(e=>Array.from({length:e.sets},()=>({done:false,reps:e.reps||0,weight:e.weight||0,dur:e.durationSec||0})))};
ST.awStartTime=Date.now();
document.getElementById('awName').textContent=w.name;
document.getElementById('active-workout').style.display='flex';
ST.awTimerInterval=setInterval(()=>{
const s=Math.floor((Date.now()-ST.awStartTime)/1000);
document.getElementById('awTimer').textContent=`${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
},1000);
renderActiveWorkout();
}
function renderActiveWorkout(){
const aw=ST.activeWorkout;
document.getElementById('awBody').innerHTML=aw.exercises.map((e,ei)=>{
const ex=getExercises().find(x=>x.id===e.exerciseId);if(!ex)return'';
const isTimer=!!e.durationSec;
return`<div class="aw-ex-card">
<div class="aw-ex-name">${ex.icon} ${ex.name}</div>
<div class="aw-sets">
<div class="aw-set-row" style="opacity:0.5;font-size:11px;letter-spacing:0.06em;margin-bottom:2px">
<div></div><div style="text-align:center;color:#666">${isTimer?'Sek':'Reps'}</div>
<div style="text-align:center;color:#666">${isTimer?'':' Kg'}</div><div></div>
</div>
${aw.setStates[ei].map((set,si)=>`
<div class="aw-set-row">
<div class="set-num">${si+1}</div>
<input class="set-input" type="number" value="${isTimer?set.dur:set.reps}" min="0" onchange="updateSet(${ei},${si},'${isTimer?'dur':'reps'}',this.value)">
${isTimer?`<div style="text-align:center;color:#444;font-size:13px">sek</div>`:
`<input class="set-input" type="number" value="${set.weight}" step="0.5" min="0" onchange="updateSet(${ei},${si},'weight',this.value)">`}
<button class="set-done ${set.done?'completed':''}" onclick="toggleSet(${ei},${si})">✓</button>
</div>`).join('')}
</div>
</div>`;
}).join('');
}
function updateSet(ei,si,field,val){ST.activeWorkout.setStates[ei][si][field]=parseFloat(val)||0;}
function toggleSet(ei,si){
const set=ST.activeWorkout.setStates[ei][si];
set.done=!set.done;
if(set.done){
const restSec=ST.activeWorkout.exercises[ei]?.restSec||60;
startRest(restSec);
}
renderActiveWorkout();
}
function startRest(secs){
let t=secs;
document.getElementById('restNum').textContent=t;
document.getElementById('restOverlay').classList.add('show');
clearInterval(ST.restInterval);
ST.restInterval=setInterval(()=>{
t--;document.getElementById('restNum').textContent=t;
if(t<=0){clearInterval(ST.restInterval);document.getElementById('restOverlay').classList.remove('show');}
},1000);
}
function skipRest(){clearInterval(ST.restInterval);document.getElementById('restOverlay').classList.remove('show');}
async function finishWorkout(){
clearInterval(ST.awTimerInterval);
const dur=Math.floor((Date.now()-ST.awStartTime)/1000/60);
const entry={id:Date.now(),date:today(),workoutId:ST.activeWorkout.id,workoutName:ST.activeWorkout.name,durationMin:dur,exercises:ST.activeWorkout.setStates};
ST.workoutLog.unshift(entry);
await save('fortis_workoutlog',ST.workoutLog);
document.getElementById('active-workout').style.display='none';
document.getElementById('streakCount').textContent=calcStreak();
document.getElementById('psTotal').textContent=ST.workoutLog.length;
document.getElementById('psStreak').textContent=calcStreak();
renderTodayWorkout();
alert(`Pass klart! 💪 Varade ${dur} minuter.`);
checkAchievements();
}
function closeWorkout(){
if(confirm('Avsluta passet utan att spara?')){
clearInterval(ST.awTimerInterval);clearInterval(ST.restInterval);
document.getElementById('active-workout').style.display='none';
document.getElementById('restOverlay').classList.remove('show');
}
}
// ── MODAL ─────────────────────────────────────────────────
function closeModal(e){if(!e||e.target===document.getElementById('modalOverlay'))document.getElementById('modalOverlay').classList.remove('show');}
function toggleFilterModal(){
const overlay=document.getElementById('filterOverlay');
const muscles=['Alla','Bröst','Rygg','Ben','Axlar','Armar','Core','Helkropp','Kondition'];
const diffs=['Alla','Nybörjare','Medel','Avancerad'];
overlay.querySelector('#fmMuscles').innerHTML=muscles.map(m=>
`<div class="filter-chip ${ST.muscleFilter===m?'active':''}" onclick="setMuscleFilter('${m}')">${m}</div>`
).join('');
overlay.querySelector('#fmDiffs').innerHTML=diffs.map(d=>
`<div class="filter-chip ${ST.diffFilter===d?'active':''}" onclick="setDiffFilter('${d}')">${d}</div>`
).join('');
overlay.classList.add('show');
}
function setMuscleFilter(m){
ST.muscleFilter=m;
document.querySelectorAll('#fmMuscles .filter-chip').forEach(c=>c.classList.toggle('active',c.textContent===m));
}
function setDiffFilter(d){
ST.diffFilter=d;
document.querySelectorAll('#fmDiffs .filter-chip').forEach(c=>c.classList.toggle('active',c.textContent===d));
}
function applyFilters(){
document.getElementById('filterOverlay').classList.remove('show');
updateFilterBadge();
filterExercises();
}
function resetFilters(){
ST.muscleFilter='Alla';
ST.diffFilter='Alla';
document.querySelectorAll('#fmMuscles .filter-chip').forEach(c=>c.classList.toggle('active',c.textContent==='Alla'));
document.querySelectorAll('#fmDiffs .filter-chip').forEach(c=>c.classList.toggle('active',c.textContent==='Alla'));
}
function closeFilterOverlay(e){
if(e.target===document.getElementById('filterOverlay'))applyFilters();
}
// ── CUSTOM EXERCISES ──────────────────────────────────────
function openCreateExercise(){
ST._createExInstructions=[''];
document.getElementById('ceName').value='';
document.getElementById('ceIcon').value='';
document.getElementById('ceSecondary').value='';
renderCEInstructions();
document.getElementById('createExOverlay').classList.add('show');
}
function closeCreateExercise(e){
if(!e||e.target===document.getElementById('createExOverlay'))
document.getElementById('createExOverlay').classList.remove('show');
}
function renderCEInstructions(){
document.getElementById('ceInstructions').innerHTML=ST._createExInstructions.map((s,i)=>`
<div class="ce-instruction-row">
<input type="text" value="${s.replace(/"/g,'"')}" placeholder="Steg ${i+1}..." oninput="ST._createExInstructions[${i}]=this.value">
<button onclick="removeInstruction(${i})">✕</button>
</div>`).join('');
}
function addInstruction(){
ST._createExInstructions.push('');
renderCEInstructions();
}
function removeInstruction(i){
ST._createExInstructions.splice(i,1);
if(!ST._createExInstructions.length)ST._createExInstructions=[''];
renderCEInstructions();
}
async function saveCustomExercise(){
const name=document.getElementById('ceName').value.trim();
if(!name){alert('Ange ett namn.');return;}
const icon=document.getElementById('ceIcon').value.trim()||'⚡';
const muscle=document.getElementById('ceMuscle').value;
const secondary=document.getElementById('ceSecondary').value.trim();
const equip=document.getElementById('ceEquip').value;
const diff=document.getElementById('ceDiff').value;
const type=document.getElementById('ceType').value;
const instructions=ST._createExInstructions.filter(s=>s.trim());
const ex={id:'custom_'+Date.now(),name,muscle,secondary,equip,icon,diff,type,instructions,isCustom:true};
ST.customExercises.push(ex);
await save('fortis_custom_exercises',ST.customExercises);
closeCreateExercise();
renderLibrary();
}
async function deleteCustomExercise(id){
if(!confirm('Ta bort övningen? Den försvinner från biblioteket.'))return;
ST.customExercises=ST.customExercises.filter(e=>e.id!==id);
await save('fortis_custom_exercises',ST.customExercises);
closeModal();
renderLibrary();
}
// ── RESET ─────────────────────────────────────────────────
async function resetApp(){
if(!confirm('Återställ all data? Detta kan inte ångras.'))return;
if(window.storage){
try{await window.storage.delete('fortis_profile');}catch{}
try{await window.storage.delete('fortis_workouts');}catch{}
try{await window.storage.delete('fortis_workoutlog');}catch{}
try{await window.storage.delete('fortis_water_'+today());}catch{}
}
location.reload();
}
// ── START ─────────────────────────────────────────────────
init();
</script>
</div>
</body>
</html>