Files
holiday-2026/index.html
T
Till Heidrich f622dc0619 feat: Großes v3-Update – Sterne-Rating, Ranking, Kommentare, eigene Ideen gleichberechtigt
 Sterne-Bewertungs-System (1-5 pro Person, Till + Lea unabhängig)
🏆 Dynamische Ranking-Liste mit Top-3-Badges (Gold/Silber/Bronze)
💬 Kommentare unter jeder Karte (aufklappbar, eigene löschbar)
👤 User-Profil-Switch (Till ↔ Lea) – Header-Pill mit Modal
💜 Eigene Ideen sind jetzt vollwertige Karten (im Ideen-Tab gemischt)
✏️ Eigene Ideen bearbeitbar + löschbar (eigener Tab)
📊 Sortierung: Ranking / Status / Preis / Name / Eigene zuerst
📈 Live-Stats: Ideen, Bewertet, Kommentare, Tage bis Reise
🎨 Bottom-Navigation mit 5 Tabs + Badge für eigene Ideen
📋 JSON-Export aller Daten für Codex
🔧 Daten-driven: destinations.json + dynamisches Rendering
📱 Mobile-optimiert mit Safe-Area-Insets

Datenstruktur:
- localStorage.f24_ratings, f24_comments, f24_own, f24_user
- 18 built-in Destinations aus data/destinations.json
- Gleiche Render-Pipeline für built-in und own
2026-05-26 14:03:55 +00:00

1150 lines
73 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1">
<title>Fredi24 Reise-Planer</title>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
:root{
--bg:#F7F3ED;--bg2:#EDE8DF;--card:#FFFFFF;--border:#E4DDD4;
--coral:#E55A35;--coral-dark:#C4482A;--coral-light:#FCEAE4;
--gold:#D4A020;--gold-light:#FDF4DC;
--teal:#1F6B5E;--teal-light:#D4EDE8;
--blue:#3B7EC8;--blue-light:#DAEAF8;
--purple:#7B4DBA;--purple-light:#EDE0FA;
--text:#18181B;--muted:#78716C;--subtle:#A8A29E;
--navy:#0D1B2A;
--r:12px;--r-sm:8px;--r-lg:16px;
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-top: env(safe-area-inset-top, 0px);
}
html{-webkit-text-size-adjust:100%;}
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:14px;min-height:100vh;padding-bottom:calc(72px + var(--safe-bottom));}
/* GATE */
#gate{position:fixed;inset:0;background:var(--navy);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;}
.gate-box{background:#fff;border-radius:20px;padding:36px 28px;text-align:center;max-width:320px;width:100%;}
.gate-paw{font-size:52px;margin-bottom:8px;}
.gate-brand{font-family:'Bricolage Grotesque',sans-serif;font-size:28px;font-weight:800;color:var(--coral);letter-spacing:-0.04em;}
.gate-sub{font-size:13px;color:var(--muted);margin:4px 0 24px;}
.gate-input{width:100%;padding:12px 16px;border:2px solid var(--border);border-radius:var(--r);font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border .2s;margin-bottom:10px;background:var(--bg);}
.gate-input:focus{border-color:var(--coral);}
.gate-btn{width:100%;padding:13px;background:var(--coral);color:#fff;border:none;border-radius:var(--r);font-size:14px;font-weight:700;cursor:pointer;font-family:'Outfit',sans-serif;transition:background .2s;letter-spacing:.01em;}
.gate-btn:hover{background:var(--coral-dark);}
.gate-err{color:var(--coral);font-size:12px;margin-top:8px;display:none;font-weight:500;}
/* PROFILE PICKER */
.profile-pick{display:flex;gap:8px;margin-bottom:12px;justify-content:center;}
.profile-opt{flex:1;padding:8px 6px;border:2px solid var(--border);border-radius:var(--r);background:var(--card);cursor:pointer;text-align:center;font-size:12px;font-weight:600;color:var(--muted);transition:all .2s;}
.profile-opt.active{border-color:var(--coral);background:var(--coral-light);color:var(--coral);}
.profile-opt .emoji{font-size:22px;display:block;margin-bottom:2px;}
/* HEADER */
.header{background:var(--navy);color:#fff;padding:calc(12px + var(--safe-top)) 16px 12px;position:sticky;top:0;z-index:100;}
.header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.brand{display:flex;align-items:center;gap:6px;}
.brand-logo{font-size:22px;}
.brand-name{font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:800;color:#fff;letter-spacing:-0.04em;}
.brand-name span{color:var(--coral);}
.brand-sub{font-size:10px;color:rgba(255,255,255,0.5);margin-top:-2px;}
.user-pill{font-size:11px;color:rgba(255,255,255,0.85);background:var(--coral);padding:4px 10px;border-radius:20px;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:4px;}
.user-pill:hover{background:var(--coral-dark);}
.header-pills{display:flex;gap:5px;flex-wrap:wrap;margin-top:9px;}
.hp{background:rgba(255,255,255,0.1);border-radius:20px;padding:3px 9px;font-size:11px;font-weight:500;color:rgba(255,255,255,0.75);white-space:nowrap;}
.hp.budget{background:rgba(212,160,32,0.3);color:#FFD966;}
.budget-indicator{margin-top:8px;display:flex;align-items:center;gap:8px;}
.budget-track{flex:1;height:3px;background:rgba(255,255,255,0.15);border-radius:2px;overflow:hidden;}
.budget-fill{height:100%;background:linear-gradient(90deg,#2D7A6A,#D4A020);border-radius:2px;}
.budget-label{font-size:10px;color:rgba(255,255,255,0.5);}
/* BOTTOM NAV */
.bottom-nav{position:fixed;bottom:0;left:0;right:0;z-index:200;background:#fff;border-top:1px solid var(--border);display:flex;padding-bottom:var(--safe-bottom);box-shadow:0 -4px 24px rgba(0,0,0,0.08);}
.bnav-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 4px;cursor:pointer;border:none;background:none;color:var(--muted);transition:color .15s;font-size:9px;font-weight:600;font-family:'Outfit',sans-serif;letter-spacing:.03em;position:relative;}
.bnav-item .icon{font-size:20px;line-height:1;}
.bnav-item.active{color:var(--coral);}
.bnav-item .nbadge{position:absolute;top:4px;right:25%;background:var(--coral);color:#fff;border-radius:10px;padding:1px 5px;font-size:9px;font-weight:700;min-width:14px;text-align:center;}
/* CONTENT */
.tab-content{display:none;padding:12px 12px 0;}
.tab-content.active{display:block;}
/* STATS DASHBOARD */
.stats-bar{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:12px;}
.stat-card{background:var(--card);border-radius:var(--r);padding:8px 6px;text-align:center;border:1px solid var(--border);}
.stat-num{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;color:var(--coral);line-height:1;letter-spacing:-0.02em;}
.stat-lbl{font-size:9px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-top:2px;}
/* RECO HERO */
.reco-hero{background:linear-gradient(135deg,var(--teal),#1A4B3F);border-radius:var(--r-lg);padding:16px;margin-bottom:14px;color:#fff;position:relative;overflow:hidden;}
.reco-hero::before{content:'🏆';position:absolute;right:-10px;top:-15px;font-size:90px;opacity:0.08;}
.reco-label{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,0.6);margin-bottom:4px;}
.reco-title{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;line-height:1.1;margin-bottom:6px;letter-spacing:-0.02em;}
.reco-why{font-size:12px;color:rgba(255,255,255,0.75);margin-bottom:12px;line-height:1.5;}
.reco-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.reco-price{font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:800;color:#FFD966;}
.reco-tag{background:rgba(255,255,255,0.15);border-radius:20px;padding:3px 10px;font-size:11px;font-weight:600;}
.reco-btn{margin-left:auto;background:#fff;color:var(--teal);border:none;border-radius:var(--r);padding:8px 14px;font-size:12px;font-weight:700;cursor:pointer;font-family:'Outfit',sans-serif;}
/* SECTION */
.section-head{display:flex;align-items:center;justify-content:space-between;margin:14px 0 10px;}
.section-title{font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:800;letter-spacing:-0.02em;}
.section-action{font-size:11px;color:var(--coral);font-weight:700;cursor:pointer;background:none;border:none;}
/* FILTER ROW */
.filter-row{display:flex;gap:6px;overflow-x:auto;padding-bottom:2px;margin-bottom:10px;scrollbar-width:none;}
.filter-row::-webkit-scrollbar{display:none;}
.chip{padding:5px 12px;border-radius:20px;border:1.5px solid var(--border);background:var(--card);font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;color:var(--muted);transition:all .15s;flex-shrink:0;}
.chip.active{background:var(--coral);border-color:var(--coral);color:#fff;}
/* SORT BAR */
.sort-bar{display:flex;align-items:center;gap:8px;margin-bottom:10px;padding:8px 12px;background:var(--card);border-radius:var(--r);border:1px solid var(--border);}
.sort-label{font-size:11px;color:var(--muted);font-weight:600;}
.sort-select{flex:1;background:none;border:none;font-family:'Outfit',sans-serif;font-size:12px;font-weight:600;color:var(--text);outline:none;cursor:pointer;}
/* DEST GRID */
.dest-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(310px,1fr));gap:10px;}
@media(max-width:660px){.dest-grid{grid-template-columns:1fr;}}
.dest-card{background:var(--card);border-radius:var(--r-lg);overflow:hidden;border:1.5px solid var(--border);transition:transform .15s,box-shadow .15s;position:relative;}
.dest-card:hover{transform:translateY(-1px);box-shadow:0 8px 32px rgba(0,0,0,0.1);}
.dest-card.dim{opacity:.7;}
.dest-card.own{border-color:var(--purple);border-style:dashed;}
.rank-badge{position:absolute;top:10px;right:10px;background:var(--gold);color:#fff;font-family:'Bricolage Grotesque',sans-serif;font-size:11px;font-weight:800;padding:3px 9px;border-radius:20px;z-index:2;display:none;}
.rank-badge.show{display:block;}
.rank-badge.top1{background:#FFD700;color:#7a5c00;}
.rank-badge.top2{background:#C0C0C0;color:#5a5a5a;}
.rank-badge.top3{background:#CD7F32;color:#4a2810;}
.card-hdr{padding:12px 14px 8px;display:flex;align-items:flex-start;gap:10px;}
.dest-em{font-size:30px;flex-shrink:0;line-height:1;}
.dest-meta{flex:1;min-width:0;}
.dest-name{font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:800;line-height:1.15;letter-spacing:-0.02em;}
.dest-sub{font-size:11px;color:var(--muted);margin-top:1px;}
.status-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:4px;}
.sbadge{padding:2px 8px;border-radius:20px;font-size:9px;font-weight:800;letter-spacing:.06em;color:#fff;white-space:nowrap;text-transform:uppercase;}
.s-TOP{background:var(--teal);}
.s-OK{background:var(--gold);}
.s-NEW{background:var(--purple);}
.s-SPÄTER{background:var(--blue);}
.s-MAYBE{background:var(--muted);}
.s-NEIN{background:#DC2626;}
.s-OWN{background:var(--purple);}
.card-price{padding:8px 14px;background:linear-gradient(90deg,var(--bg),var(--card));display:flex;align-items:center;gap:8px;}
.price-main{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;color:var(--text);}
.price-range{font-size:10px;color:var(--muted);}
.price-badge{margin-left:auto;font-size:10px;padding:2px 8px;border-radius:10px;font-weight:700;}
.pb-cheap{background:var(--teal-light);color:var(--teal);}
.pb-mid{background:var(--gold-light);color:var(--gold);}
.pb-pricey{background:var(--coral-light);color:var(--coral);}
.card-scores{display:flex;gap:8px;padding:6px 14px;background:var(--bg);}
.sc{flex:1;}
.sc-lbl{font-size:9px;color:var(--muted);font-weight:700;margin-bottom:2px;text-transform:uppercase;letter-spacing:.05em;}
.sc-bar{height:3px;background:var(--border);border-radius:2px;overflow:hidden;}
.sc-fill{height:100%;border-radius:2px;}
.sc-f{background:var(--teal);}
.sc-l{background:var(--coral);}
.sc-m{background:var(--gold);}
.card-stats{display:flex;border-top:1px solid var(--bg);border-bottom:1px solid var(--bg);}
.cstat{flex:1;text-align:center;padding:7px 4px;border-right:1px solid var(--bg);}
.cstat:last-child{border-right:none;}
.cstat-val{font-size:11px;font-weight:700;color:var(--text);display:block;}
.cstat-key{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;}
.card-body{padding:10px 14px 12px;}
.card-highlight{font-size:12px;color:var(--teal);font-weight:600;margin-bottom:7px;font-style:italic;}
.pros-cons{display:grid;grid-template-columns:1fr 1fr;gap:4px;}
.pro-list li,.con-list li{list-style:none;font-size:11px;line-height:1.6;padding-left:13px;position:relative;}
.pro-list li::before{content:"✓";position:absolute;left:0;color:var(--teal);font-weight:700;}
.con-list li::before{content:"✗";position:absolute;left:0;color:var(--coral);font-weight:700;}
.card-note{margin-top:8px;padding:6px 9px;background:var(--bg);border-radius:6px;font-size:11px;color:var(--muted);border-left:2px solid var(--gold);}
/* STAR RATING */
.rating-bar{display:flex;align-items:center;padding:10px 14px;border-top:1px solid var(--bg);background:linear-gradient(180deg,var(--bg) 0%,var(--card) 100%);}
.rating-section{flex:1;}
.rating-row{display:flex;align-items:center;gap:6px;margin-bottom:4px;}
.rating-row:last-child{margin-bottom:0;}
.rating-who{font-size:10px;font-weight:700;color:var(--muted);width:36px;text-transform:uppercase;letter-spacing:.04em;}
.stars{display:flex;gap:2px;}
.star{font-size:18px;cursor:pointer;color:#E4DDD4;transition:all .15s;background:none;border:none;padding:0 1px;font-family:Arial,sans-serif;line-height:1;}
.star.filled{color:#FFB800;}
.star:hover{transform:scale(1.15);}
.star.hover-preview{color:#FFD966;}
.rating-summary{margin-left:8px;text-align:center;}
.avg-num{font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:800;color:var(--coral);line-height:1;letter-spacing:-0.02em;}
.avg-lbl{font-size:9px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-top:2px;}
/* COMMENT SECTION */
.comments-section{border-top:1px solid var(--bg);padding:0;}
.comments-toggle{width:100%;padding:8px 14px;background:none;border:none;display:flex;align-items:center;gap:6px;font-family:'Outfit',sans-serif;font-size:11px;font-weight:700;color:var(--coral);cursor:pointer;text-align:left;}
.comments-toggle .arr{transition:transform .2s;}
.comments-toggle.open .arr{transform:rotate(90deg);}
.comments-toggle .ccount{margin-left:auto;color:var(--muted);font-weight:600;}
.comments-body{display:none;padding:0 14px 12px;}
.comments-body.open{display:block;}
.comment{background:var(--bg);border-radius:var(--r-sm);padding:8px 10px;margin-bottom:6px;border-left:3px solid var(--coral);}
.comment.till{border-color:var(--teal);}
.comment.lea{border-color:var(--coral);}
.comment-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;}
.comment-who{font-size:11px;font-weight:700;color:var(--text);}
.comment-time{font-size:10px;color:var(--muted);}
.comment-text{font-size:12px;line-height:1.5;color:var(--text);}
.comment-del{background:none;border:none;color:var(--muted);font-size:14px;cursor:pointer;padding:0 4px;margin-left:4px;}
.comment-empty{font-size:11px;color:var(--muted);font-style:italic;padding:6px 0;}
.comment-input-row{display:flex;gap:6px;margin-top:8px;}
.comment-input{flex:1;padding:8px 10px;border:1.5px solid var(--border);border-radius:var(--r-sm);font-family:'Outfit',sans-serif;font-size:12px;outline:none;background:var(--bg);}
.comment-input:focus{border-color:var(--coral);}
.comment-btn{padding:8px 12px;background:var(--coral);color:#fff;border:none;border-radius:var(--r-sm);font-weight:700;font-size:11px;cursor:pointer;font-family:'Outfit',sans-serif;}
/* MODAL */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:500;display:none;align-items:flex-end;justify-content:center;padding:0;}
.modal-overlay.open{display:flex;}
.modal{background:#fff;border-radius:20px 20px 0 0;padding:20px;max-height:90vh;overflow-y:auto;width:100%;max-width:540px;}
.modal-handle{width:36px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 14px;}
.modal-title{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;margin-bottom:14px;letter-spacing:-0.02em;}
.form-group{margin-bottom:12px;}
.form-label{font-size:12px;font-weight:600;color:var(--muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em;}
.form-input,.form-select,.form-textarea{width:100%;padding:10px 12px;border:1.5px solid var(--border);border-radius:var(--r-sm);font-size:13px;font-family:'Outfit',sans-serif;outline:none;background:var(--bg);transition:border .2s;color:var(--text);}
.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--coral);}
.form-textarea{resize:none;min-height:70px;}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.modal-btn{width:100%;padding:13px;background:var(--coral);color:#fff;border:none;border-radius:var(--r);font-size:14px;font-weight:700;cursor:pointer;font-family:'Outfit',sans-serif;margin-top:4px;}
.modal-btn-secondary{background:var(--bg);color:var(--text);border:1.5px solid var(--border);}
.modal-close{float:right;background:none;border:none;font-size:20px;cursor:pointer;color:var(--muted);padding:0 4px;}
/* EMOJI PICKER */
.emoji-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:4px;background:var(--bg);padding:8px;border-radius:var(--r-sm);max-height:120px;overflow-y:auto;}
.emoji-pick{background:none;border:none;font-size:20px;cursor:pointer;padding:4px;border-radius:6px;transition:background .15s;}
.emoji-pick:hover{background:rgba(0,0,0,0.05);}
.emoji-pick.selected{background:var(--coral-light);}
/* RANKING */
.rank-row{display:grid;grid-template-columns:30px 1fr auto auto;gap:10px;align-items:center;padding:10px 14px;background:var(--card);border-radius:var(--r);margin-bottom:5px;border:1px solid var(--border);}
.rank-num{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;color:var(--coral);text-align:center;letter-spacing:-0.02em;}
.rank-emoji{font-size:20px;margin-right:8px;}
.rank-name{font-weight:700;font-size:13px;display:flex;align-items:center;}
.rank-meta{font-size:10px;color:var(--muted);}
.rank-score{display:flex;align-items:center;gap:4px;font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:800;color:var(--gold);}
.rank-stars{font-size:14px;line-height:1;}
.rank-price{font-family:'Bricolage Grotesque',sans-serif;font-size:13px;font-weight:700;color:var(--teal);}
/* HISTORY */
.hist-year{font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:800;color:var(--coral);margin:16px 0 8px;letter-spacing:-0.03em;}
.hist-item{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;background:var(--card);border-radius:var(--r-sm);margin-bottom:5px;border:1px solid var(--border);}
.hist-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:4px;}
.hotel .hist-dot{background:var(--teal);}
.cruise .hist-dot{background:var(--coral);}
.ferienhaus .hist-dot{background:var(--gold);}
.airbnb .hist-dot{background:var(--purple);}
.family .hist-dot{background:var(--blue);}
.hist-em{font-size:18px;flex-shrink:0;}
.hist-body{flex:1;}
.hist-dest-name{font-weight:700;font-size:13px;}
.hist-loc{font-size:11px;color:var(--muted);}
.hist-tags{display:flex;gap:4px;margin-top:3px;flex-wrap:wrap;}
.htag{font-size:9px;padding:1px 7px;border-radius:10px;font-weight:700;}
.htag-hotel{background:var(--teal-light);color:var(--teal);}
.htag-cruise{background:var(--coral-light);color:var(--coral);}
.htag-fh{background:var(--gold-light);color:var(--gold);}
.htag-airbnb{background:var(--purple-light);color:var(--purple);}
.htag-family{background:var(--blue-light);color:var(--blue);}
.hist-pers{font-size:10px;color:var(--muted);margin-left:auto;white-space:nowrap;flex-shrink:0;}
/* INFO TAB */
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
@media(max-width:540px){.info-grid{grid-template-columns:1fr;}}
.info-card{background:var(--card);border-radius:var(--r-lg);padding:14px;border:1.5px solid var(--border);}
.info-card-title{font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:800;margin-bottom:8px;letter-spacing:-0.02em;}
.info-list li{list-style:none;font-size:11px;line-height:1.8;padding-left:13px;position:relative;}
.info-list li::before{content:"→";position:absolute;left:0;color:var(--coral);}
.info-list li.excl::before{content:"✗";color:#DC2626;}
.info-full{grid-column:1/-1;}
.step-row{display:flex;gap:10px;align-items:flex-start;padding:9px 0;border-bottom:1px solid var(--bg);}
.step-row:last-child{border-bottom:none;}
.step-num{width:22px;height:22px;border-radius:50%;background:var(--coral);color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.step-text{font-size:11px;line-height:1.6;}
.step-text strong{color:var(--teal);}
.age-card{grid-column:1/-1;background:var(--navy);border-radius:var(--r-lg);padding:16px;color:#fff;}
.age-card-title{font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:800;margin-bottom:10px;color:rgba(255,255,255,0.9);}
.age-row{display:flex;gap:10px;align-items:flex-start;margin-bottom:10px;}
.age-badge{background:var(--coral);color:#fff;border-radius:8px;padding:4px 10px;font-size:11px;font-weight:800;white-space:nowrap;flex-shrink:0;}
.age-body{font-size:11px;color:rgba(255,255,255,0.65);line-height:1.65;}
.age-body strong{color:#fff;}
/* FAB */
.fab{position:fixed;right:16px;bottom:calc(80px + var(--safe-bottom));background:var(--coral);color:#fff;border:none;border-radius:50%;width:56px;height:56px;font-size:26px;cursor:pointer;box-shadow:0 4px 20px rgba(229,90,53,0.4);z-index:150;display:flex;align-items:center;justify-content:center;transition:transform .15s;}
.fab:hover{transform:scale(1.05);}
/* TOAST */
.toast{position:fixed;bottom:calc(90px + var(--safe-bottom));left:50%;transform:translateX(-50%) translateY(20px);background:var(--navy);color:#fff;padding:10px 18px;border-radius:30px;font-size:12px;font-weight:600;opacity:0;transition:all .3s;z-index:400;pointer-events:none;}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
/* OWN BADGE */
.own-marker{position:absolute;top:0;left:0;background:var(--purple);color:#fff;padding:3px 10px;font-size:9px;font-weight:800;letter-spacing:.05em;border-bottom-right-radius:8px;text-transform:uppercase;z-index:1;}
/* EMPTY */
.empty{text-align:center;padding:40px 20px;}
.empty-icon{font-size:48px;margin-bottom:12px;}
.empty-title{font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:800;margin-bottom:4px;}
.empty-sub{font-size:12px;color:var(--muted);}
</style>
</head>
<body>
<!-- GATE -->
<div id="gate">
<div class="gate-box">
<div class="gate-paw">🐾</div>
<div class="gate-brand">Fredi24</div>
<div class="gate-sub">Unser Reise-Planer · Till & Lea</div>
<input class="gate-input" type="password" id="pw" placeholder="Passwort" onkeydown="if(event.key==='Enter')auth()">
<button class="gate-btn" onclick="auth()">Einloggen</button>
<div class="gate-err" id="pw-err">Nicht korrekt versuch's nochmal 🙅</div>
</div>
</div>
<!-- HEADER -->
<div class="header">
<div class="header-row">
<div class="brand">
<span class="brand-logo">🐾</span>
<div>
<div class="brand-name">Fredi<span>24</span></div>
<div class="brand-sub">Unser Reise-Planer</div>
</div>
</div>
<div class="user-pill" onclick="switchUser()" id="user-pill">👤 <span id="user-name">Till</span></div>
</div>
<div class="header-pills">
<span class="hp">✈️ HAM / HAJ · Direktflug</span>
<span class="hp">⭐⭐⭐⭐ Mind. 4★</span>
<span class="hp budget">💶 Max. 3.600 €</span>
<span class="hp">📅 ab 13. Juli 2026</span>
</div>
<div class="budget-indicator">
<div class="budget-track"><div class="budget-fill" style="width:78%"></div></div>
<span class="budget-label">2.8003.600 € Range</span>
</div>
</div>
<!-- FAB -->
<button class="fab" onclick="openSubmit()" title="Eigene Idee einreichen"></button>
<!-- TOAST -->
<div class="toast" id="toast">✓ Gespeichert</div>
<!-- SUBMIT MODAL -->
<div class="modal-overlay" id="submit-modal" onclick="if(event.target===this)closeModal('submit-modal')">
<div class="modal">
<div class="modal-handle"></div>
<button class="modal-close" onclick="closeModal('submit-modal')"></button>
<div class="modal-title">💡 Eigene Idee einreichen</div>
<input type="hidden" id="edit-id" value="">
<div class="form-row">
<div class="form-group">
<label class="form-label">Emoji</label>
<input class="form-input" id="s-emoji" placeholder="🏖️" maxlength="4" value="🌍" type="text">
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" id="s-status">
<option value="TOP">⭐ TOP</option>
<option value="OK">👍 OK</option>
<option value="MAYBE">🤔 Vielleicht</option>
<option value="SPÄTER">⏳ Später</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Destination *</label>
<input class="form-input" id="s-dest" placeholder="z.B. Ibiza Nordküste" type="text">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Land / Region</label>
<input class="form-input" id="s-land" placeholder="Spanien" type="text">
</div>
<div class="form-group">
<label class="form-label">Budget (€)</label>
<input class="form-input" id="s-budget" placeholder="3000" type="number">
</div>
</div>
<div class="form-group">
<label class="form-label">Subtext (kurze Beschreibung)</label>
<input class="form-input" id="s-sub" placeholder="z.B. AI Resort · Familien-Hotel" type="text">
</div>
<div class="form-group">
<label class="form-label">Warum interessant? (Begründung)</label>
<textarea class="form-textarea" id="s-why" placeholder="Was macht das Ziel spannend für euch?"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Vorgeschlagen von</label>
<select class="form-select" id="s-who">
<option>Till</option>
<option>Lea</option>
<option>Beide</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Flugzeit (ca.)</label>
<input class="form-input" id="s-flight" placeholder="3h 30" type="text">
</div>
</div>
<button class="modal-btn" onclick="submitIdea()" id="submit-btn">Idee einreichen 🚀</button>
</div>
</div>
<!-- USER SWITCH MODAL -->
<div class="modal-overlay" id="user-modal" onclick="if(event.target===this)closeModal('user-modal')">
<div class="modal">
<div class="modal-handle"></div>
<div class="modal-title">👤 Wer bewertet gerade?</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;">Deine Sterne und Kommentare werden unter dem gewählten Profil gespeichert.</p>
<div class="profile-pick">
<div class="profile-opt" onclick="setUser('Till')" id="opt-till">
<span class="emoji">👨‍💼</span>
Till
</div>
<div class="profile-opt" onclick="setUser('Lea')" id="opt-lea">
<span class="emoji">👩‍🦰</span>
Lea
</div>
</div>
</div>
</div>
<!-- ============= IDEEN TAB ============= -->
<div id="tab-ideen" class="tab-content active">
<div class="stats-bar">
<div class="stat-card">
<div class="stat-num" id="stat-total">18</div>
<div class="stat-lbl">Ideen</div>
</div>
<div class="stat-card">
<div class="stat-num" id="stat-rated">0</div>
<div class="stat-lbl">Bewertet</div>
</div>
<div class="stat-card">
<div class="stat-num" id="stat-comments">0</div>
<div class="stat-lbl">Kommentare</div>
</div>
<div class="stat-card">
<div class="stat-num" id="stat-days"></div>
<div class="stat-lbl">Tage bis Reise</div>
</div>
</div>
<div class="reco-hero">
<div class="reco-label">🏆 Top-Empfehlung Sommer 2026</div>
<div class="reco-title">Sardinien direkt <br>sofort buchbar</div>
<div class="reco-why">Direktflug Hamburg 2:20h · EU-Kindersitz-Standard · Neuland für euch · Lea sagt ja · Felix liebt Sandstrände · Im Budget.</div>
<div class="reco-row">
<span class="reco-price">~3.150 €</span>
<span class="reco-tag">✈️ 2h 20</span>
<span class="reco-tag">🏖️ 4★</span>
<button class="reco-btn" onclick="scrollToCard('sardinien')">Details →</button>
</div>
</div>
<div class="sort-bar">
<span class="sort-label">📊 Sortierung:</span>
<select class="sort-select" id="sort-select" onchange="renderCards()">
<option value="ranking">⭐ Bewertung (höchste zuerst)</option>
<option value="status">📌 Status (TOP → SPÄTER)</option>
<option value="price-asc">💰 Preis (günstig → teuer)</option>
<option value="price-desc">💰 Preis (teuer → günstig)</option>
<option value="name">🔤 Name A-Z</option>
<option value="newest">🆕 Eigene Ideen zuerst</option>
</select>
</div>
<div class="filter-row">
<button class="chip active" onclick="filterStatus('ALL',this)">Alle</button>
<button class="chip" onclick="filterStatus('TOP',this)">⭐ TOP</button>
<button class="chip" onclick="filterStatus('OK',this)">👍 OK</button>
<button class="chip" onclick="filterStatus('NEW',this)">🆕 Neu</button>
<button class="chip" onclick="filterStatus('MAYBE',this)">🤔 Vielleicht</button>
<button class="chip" onclick="filterStatus('SPÄTER',this)">⏳ Später</button>
<button class="chip" onclick="filterStatus('OWN',this)">💜 Eigene</button>
</div>
<div class="dest-grid" id="dg"></div>
</div>
<!-- ============= RANKING TAB ============= -->
<div id="tab-ranking" class="tab-content">
<div class="section-head">
<span class="section-title">🏆 Ranking-Liste</span>
<button class="section-action" onclick="exportData()">📋 Export</button>
</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;">Sortiert nach Durchschnitt aller Bewertungen. Stimmt mehr ab → Liste wird dynamischer.</p>
<div id="ranking-list"></div>
</div>
<!-- ============= EIGENE IDEEN TAB ============= -->
<div id="tab-eigene" class="tab-content">
<div class="section-head">
<span class="section-title">💜 Eigene Vorschläge</span>
<button class="section-action" onclick="openSubmit()">+ Neu</button>
</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;">Hier landen alle Ideen, die ihr selbst eingereicht habt. Werden gleichberechtigt im Ideen-Dashboard angezeigt.</p>
<div class="dest-grid" id="own-list"></div>
</div>
<!-- ============= ARCHIV TAB ============= -->
<div id="tab-archiv" class="tab-content">
<div class="filter-row">
<button class="chip active" onclick="filterHistory('ALL',this)">Alle</button>
<button class="chip" onclick="filterHistory('hotel',this)">🏨 Hotel</button>
<button class="chip" onclick="filterHistory('cruise',this)">🚢 Kreuzfahrt</button>
<button class="chip" onclick="filterHistory('ferienhaus',this)">🏡 Ferienhaus</button>
<button class="chip" onclick="filterHistory('airbnb',this)">🏠 Airbnb</button>
<button class="chip" onclick="filterHistory('family',this)">👨‍👩‍👦 Familie</button>
</div>
<div id="hl"></div>
</div>
<!-- ============= INFO TAB ============= -->
<div id="tab-info" class="tab-content">
<div class="info-grid">
<div class="info-card">
<div class="info-card-title">❌ Ausschlussliste</div>
<ul class="info-list">
<li class="excl">Türkei, Ägypten (Lea)</li>
<li class="excl">Dänemark im Sommer (Lea)</li>
<li class="excl">Kroatien (überfüllt)</li>
<li class="excl">Griechenland (Kindersitz-Problem)</li>
<li class="excl">AIDA Norwegen (2024 gemacht)</li>
<li class="excl">Kein Umsteigeflug</li>
<li class="excl">Nur HAM oder HAJ als Abflug</li>
<li class="excl">Unter 4 Sterne</li>
<li class="excl">P&V Cap d'Agde / Houlgate</li>
</ul>
</div>
<div class="info-card">
<div class="info-card-title">✅ Reiseprofil</div>
<ul class="info-list">
<li>Direktflug HAM/HAJ</li>
<li>Mind. 4★ oder Glamping-Village</li>
<li>Pool + täglich Mietwagen</li>
<li>Felix: Kinderclub, Strand</li>
<li>EU-Kindersitz Pflicht</li>
<li>Lea: HP/AI bevorzugt</li>
<li>Till: Abwechslung + Fotografie</li>
<li>Caddy = Dienstwagen HHGmbH</li>
<li>Frieda bleibt zuhause</li>
</ul>
</div>
<div class="age-card">
<div class="age-card-title">📅 Felix-Alterskurve</div>
<div class="age-row"><div class="age-badge">2026<br>3,5J</div><div class="age-body"><strong>Jetzt:</strong> Strand, Pool, Kinderclub ab 3J, Wasserpark. Max. 6h Flug.</div></div>
<div class="age-row"><div class="age-badge">2027<br>4J</div><div class="age-body"><strong>Bald:</strong> + Burgen (Gruyères!), Astrid Lindgrens Welt, Tierparks.</div></div>
<div class="age-row"><div class="age-badge">2028-29<br>5-6J</div><div class="age-body"><strong>Madeira-Fenster:</strong> + Levadas, Schweden Schären, Norwegen Natur.</div></div>
<div class="age-row"><div class="age-badge">2030+<br>7J+</div><div class="age-body"><strong>Träume:</strong> + Bali, Japan, Lappland Nordlichter.</div></div>
</div>
<div class="info-card info-full">
<div class="info-card-title">⚡ Nächste Schritte</div>
<div class="step-row"><div class="step-num">1</div><div class="step-text"><strong>AIDA prüfen:</strong> aida.de Route PMI07307 Sardinien-Route 13.16. Juli? Kinder 35% Rabatt.</div></div>
<div class="step-row"><div class="step-num">2</div><div class="step-text"><strong>Glamping ASAP:</strong> Union Lido/Cavallino, Punta Lunga/Vieste. acsi.eu + pitchup.com.</div></div>
<div class="step-row"><div class="step-num">3</div><div class="step-text"><strong>Sterne vergeben:</strong> Beide voten unabhängig. Ranking-Tab zeigt Match.</div></div>
<div class="step-row"><div class="step-num">4</div><div class="step-text"><strong>Diskutieren:</strong> Kommentare unter jeder Karte. Lea kann ihre Bedenken festhalten, Till seine Punkte.</div></div>
<div class="step-row"><div class="step-num">5</div><div class="step-text"><strong>Entscheiden + buchen:</strong> Wenn beide ⭐⭐⭐⭐+ = Match. Sonst weiter abwägen.</div></div>
</div>
</div>
</div>
<!-- BOTTOM NAV -->
<div class="bottom-nav">
<button class="bnav-item active" onclick="navTo('ideen',this)">
<span class="icon">🗺️</span><span>Ideen</span>
</button>
<button class="bnav-item" onclick="navTo('ranking',this)">
<span class="icon">🏆</span><span>Ranking</span>
</button>
<button class="bnav-item" onclick="navTo('eigene',this)">
<span class="icon">💜</span><span>Eigene</span>
<span class="nbadge" id="own-badge" style="display:none">0</span>
</button>
<button class="bnav-item" onclick="navTo('archiv',this)">
<span class="icon">📖</span><span>Archiv</span>
</button>
<button class="bnav-item" onclick="navTo('info',this)">
<span class="icon"></span><span>Info</span>
</button>
</div>
<script>
// ============= DESTINATIONS DATA =============
const DESTINATIONS_BUILTIN = [{"id": "sardinien", "emoji": "🏖️", "name": "Sardinien", "country": "🇮🇹 Italien", "sub": "Nordsardinien · Hotel 4★ · Mietwagen", "status": "TOP", "isRecommendation": true, "price": 3150, "priceLabel": "für 3 Pers. 7N", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 100, "scoreLea": 80, "scoreCar": 100, "flight": "2h 20", "from": "HAM→OLB", "climate": "3035°C", "available": "ab 13.Jul ✓", "highlight": "Karibik-Strände · EU-Kindersitz-Standard · Neuland", "pros": ["Direktflug HAMOlbia 2:20h", "Traumstrände (La Pelosa, Principe)", "Lea hat Sardinien okayed", "Sehr kinderfreundlich"], "cons": ["3035°C im Juli heiß", "Kein AI = Eigenplanung", "Hochsaison teuer + voll"], "note": "Hotels: Club Hotel Baja Sardinia (4★), Sporting Beach Hotel. Nordsardinien bevorzugen.", "felixMinAge": 0, "season": ["6", "7", "8", "9"]}, {"id": "aida-kreuzfahrt", "emoji": "🚢", "name": "AIDAcosma Kreuzfahrt", "country": "🇮🇹 Mittelmeer", "sub": "Korsika · Sardinien · Rom · Marseille · Barcelona", "status": "TOP", "price": 3320, "priceLabel": "Schiff + Flug HAM→PMI", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 100, "scoreLea": 100, "scoreCar": 20, "flight": "2h", "from": "HAM→PMI", "climate": "2528°C", "available": "Prüfen!", "highlight": "Kreuzfahrt-Wunsch erfüllt · Lea muss nichts planen · Vollpension", "pros": ["Korsika + Sardinien + Rom kombiniert", "Felix Kids-Club 36J + Rutschen", "AIDA kennt ihr seit 2 Reisen", "Kinder 35% Rabatt"], "cons": ["Korsika-Route: 4.Juli oder 15.Aug", "18.Juli = Gibraltar (weniger gut)", "Kein echtes Mietwagen-Feeling"], "note": "⚠️ aida.de Route PMI07307 direkt prüfen: Sardinien-Route 13.16. Juli verfügbar?", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "algarve", "emoji": "🌊", "name": "Algarve", "country": "🇵🇹 Portugal", "sub": "Carvoeiro · Lagos · Atlantik-Klippenküste", "status": "TOP", "price": 2950, "priceLabel": "günstigste TOP-Option", "priceBadge": "Budget-Tipp 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 100, "scoreLea": 65, "scoreCar": 100, "flight": "3h 30", "from": "HAM→FAO", "climate": "2528°C", "available": "ab 13.Jul ✓", "highlight": "Mildestes Klima für Felix · Spektakuläre Mietwagen-Touren", "pros": ["2528°C ideal für Kleinkind", "Cabo de São Vicente, Lagos, Sagres", "Carvoeiro, Praia da Luz ruhig", "Günstigstes Budget aller TOPs"], "cons": ["Lea etwas zögerlich (neu)", "Weniger AI-Auswahl als Kanaren"], "note": "HP statt AI ihr esst täglich unterwegs bei Mietwagen-Touren.", "felixMinAge": 0, "season": ["4", "5", "6", "7", "8", "9", "10"]}, {"id": "glamping", "emoji": "🏕️", "name": "Glamping Village", "country": "🇮🇹 Italien", "sub": "Bungalow direkt am Meer · Pool · Kinderanimation", "status": "NEW", "price": 2500, "priceLabel": "günstigste Option!", "priceBadge": "Preis-Tipp 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 100, "scoreLea": 70, "scoreCar": 80, "flight": "2h", "from": "HAM→VCE", "climate": "2833°C", "available": "ASAP!", "highlight": "Tipp vom Meister: Bungalow am Meer mit Pool unterschätzt!", "pros": ["Direkter Meerzugang + eigener Pool", "Kinderclub, Animation, Strand", "3050% günstiger als Hotel", "Absolutes Neuland"], "cons": ["Kein 4★-Hotelkomfort", "Qualität je nach Anlage variabel", "Juli-Plätze schnell weg"], "note": "Top: Union Lido/Cavallino (Venetien), Punta Lunga/Vieste (Gargano). Buchung: acsi.eu · pitchup.com", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9"]}, {"id": "schweiz", "emoji": "🏔️", "name": "Schweiz + Norditalien", "country": "🇨🇭 Schweiz", "sub": "Fribourg · Gruyères · Lago Maggiore", "status": "OK", "price": 2000, "priceLabel": "günstigster Kurztrip", "priceBadge": "Budget-Winner 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 60, "scoreLea": 90, "scoreCar": 80, "flight": "860 km", "from": "Caddy*", "climate": "2228°C", "available": "Flexibel", "highlight": "Lea würde es sofort machen · Freunde besuchen · Château de Gruyères", "pros": ["Lea will Schweiz ausdrücklich", "Château de Gruyères Felix findet Burgen spannend!", "Tagestour Lago Maggiore (2,5h)", "Freunde bei Fribourg = ggf. günstig"], "cons": ["860 km Caddy (Dienstwagen)", "Schweizer Hotels teuer", "Kein klassischer Strandurlaub"], "note": "*Caddy ist Dienstwagen HHGmbH keine persönlichen Kosten. Alternativ: Flug HAM→GVA + Mietwagen.", "felixMinAge": 3, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "cote-azur", "emoji": "🇫🇷", "name": "Côte d'Azur", "country": "🇫🇷 Frankreich", "sub": "Nizza · Antibes · Familien-Feriendorf", "status": "OK", "price": 3000, "priceLabel": "Flug + Hotel/Feriendorf", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 80, "scoreLea": 85, "scoreCar": 90, "flight": "2h", "from": "HAM→NCE", "climate": "2833°C", "available": "ab 13.Jul ✓", "highlight": "Lea würde sofort Frankreich machen · Charmant", "pros": ["Lea mag Frankreich", "Nizza direkt ab Hamburg 2h", "Traumküste + Mietwagen (Monaco, Cannes)", "Familienfreundliche Feriendörfer"], "cons": ["Juli sehr teuer an der Côte d'Azur", "Wenig AI-Hotels"], "note": "Feriendorf-Tipp: Center Parcs Les Lacs de l'Escapade (Var) oder Antibes direkt.", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9"]}, {"id": "taurito", "emoji": "🌴", "name": "Taurito Princess", "country": "🇪🇸 Gran Canaria", "sub": "TUI KIDS CLUB · All Inclusive", "status": "OK", "price": 3100, "priceLabel": "AI Pauschal inkl. Flug", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 100, "scoreLea": 100, "scoreCar": 60, "flight": "5h", "from": "HAM→LPA", "climate": "2528°C", "available": "ab 13.Jul ✓", "highlight": "Lea's ausdrücklicher Favorit · Felix Mini-Club ab 3J", "pros": ["Lea will es", "Felix Mini-Club + Kinderpool", "AI-Vollversorgung", "Wasserpark 5 Min entfernt"], "cons": ["4. Kanaren-Reise in 4 Jahren", "Till will was anderes", "AI-Bunker ohne echte Touren"], "note": "Link: c.24.de/pr/F8F2FX · Kinderfestpreis (212J) MaiSep 2026.", "felixMinAge": 3, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "fuerteventura", "emoji": "🏜️", "name": "Fuerteventura", "country": "🇪🇸 Kanaren", "sub": "Aldiana / Robinson · Ruhiger als GC", "status": "OK", "price": 3200, "priceLabel": "AI-Clubhotel", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 90, "scoreLea": 80, "scoreCar": 60, "flight": "4h 30", "from": "HAM→FUE", "climate": "2428°C", "available": "ab 13.Jul ✓", "highlight": "Ruhigste Kanareninseln · Riesige Sandstrände", "pros": ["Weniger touristisch als GC", "Aldiana/Robinson Club-Konzept", "Riesige flache Strände für Felix"], "cons": ["Kanaren = nochmal dasselbe", "Windig (Kitesurf-Destination)"], "felixMinAge": 0, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "menorca", "emoji": "🏝️", "name": "Menorca", "country": "🇪🇸 Balearen", "sub": "Ruhigste Baleareninsel · Cala Galdana", "status": "OK", "price": 2900, "priceLabel": "HP 4★ + Flug", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 85, "scoreLea": 75, "scoreCar": 90, "flight": "2h 30", "from": "HAM→MAH", "climate": "2833°C", "available": "ab 13.Jul ✓", "highlight": "Ruhiger als Mallorca · Türkises Wasser", "pros": ["Ruhiger als Mallorca + Ibiza", "Biosphärenreservat sauber", "Schöne Buchten mit Mietwagen"], "cons": ["Weniger Kinderclub-Angebot", "Sehr heiß im Juli"], "felixMinAge": 0, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "kapverden", "emoji": "🌍", "name": "Kapverden", "country": "🌍 Atlantik", "sub": "Sal · Boa Vista · AI-Resort · Exotisch", "status": "OK", "price": 3400, "priceLabel": "AI Direktflug", "priceBadge": "Knapp am Budget", "priceBadgeClass": "pb-pricey", "scoreFelix": 80, "scoreLea": 75, "scoreCar": 40, "flight": "6h", "from": "HAM→SID", "climate": "2730°C", "available": "ok", "highlight": "Exotisch ohne langen Flug · Kilometer-Strände", "pros": ["Direktflug 6h kein Umsteigen", "Riesige flache Strände (Felix-Gold)", "RIU/Iberostar All Inclusive", "Echtes Neuland"], "cons": ["Knapp am Budget", "Wenig Mietwagen-Sightseeing", "Windige Insel"], "felixMinAge": 2, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "malta", "emoji": "⛪", "name": "Malta", "country": "🇲🇹 Malta", "sub": "Gozo · St. Julian's · Familienhotel 4★", "status": "OK", "price": 2400, "priceLabel": "günstig für Europa", "priceBadge": "Preis-Tipp 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 75, "scoreLea": 70, "scoreCar": 75, "flight": "3h", "from": "HAM→MLA", "climate": "3034°C", "available": "ab 13.Jul ✓", "highlight": "Kleinste Insel Europas · EU · Günstig · Englisch", "pros": ["Direktflug 3h, günstig", "EU-Standards, kein Visum", "Gozo für Mietwagen-Touren"], "cons": ["Kaum Sandstrand (Felsküste)", "Sehr heiß + touristisch im Juli"], "felixMinAge": 0, "season": ["4", "5", "6", "7", "8", "9", "10"]}, {"id": "lanzarote", "emoji": "🌋", "name": "Lanzarote", "country": "🇪🇸 Kanaren", "sub": "Vulkaninsel · Puerto del Carmen", "status": "OK", "price": 2800, "priceLabel": "HP + Flug", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 80, "scoreLea": 70, "scoreCar": 80, "flight": "4h 45", "from": "HAM→ACE", "climate": "2528°C", "available": "ab 13.Jul ✓", "highlight": "Ruhigste Kanareninseln · Vulkan-Landschaft · César Manrique", "pros": ["Ruhiger als GC/Teneriffa", "Timanfaya Vulkan per Mietwagen", "Passat-Klima: 2528°C"], "cons": ["Kanaren = schon dreimal", "Weniger Kinderclub-Auswahl"], "felixMinAge": 0, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "costa-sol", "emoji": "🌞", "name": "Costa del Sol / Málaga", "country": "🇪🇸 Andalusien", "sub": "Nerja · Marbella · Direktflug HAM", "status": "OK", "price": 2700, "priceLabel": "HP + Direktflug", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 80, "scoreLea": 70, "scoreCar": 90, "flight": "3h 20", "from": "HAM→AGP", "climate": "3238°C", "available": "ab 13.Jul ✓", "highlight": "Günstig · Andalusien Mietwagen-Touren", "pros": ["Direktflug HAM→Málaga 3h20", "Nerja (schön + ruhig)", "Mietwagen: Granada, Alhambra, Ronda"], "cons": ["3238°C im Juli sehr heiß", "Viele Touristenstädte"], "felixMinAge": 0, "season": ["4", "5", "6", "9", "10"]}, {"id": "msc-kreuzfahrt", "emoji": "🛳️", "name": "MSC / TUI Cruises", "country": "🚢 Mittelmeer", "sub": "Alternative zu AIDA", "status": "MAYBE", "price": 3000, "priceLabel": "inkl. Flug", "priceBadge": "Im Budget", "priceBadgeClass": "pb-mid", "scoreFelix": 90, "scoreLea": 80, "scoreCar": 20, "flight": "2h", "from": "Flug FCO/VCE", "climate": "2530°C", "available": "Prüfen", "highlight": "Alternative Reederei · Evtl. günstigere Routen", "pros": ["MSC Grandiosa familienfreundlich", "Evtl. günstigere Kabinen", "Mein Schiff deutschsprachig"], "cons": ["Kein AIDA-Erfahrungsvorteil", "Kinderprogramm variiert"], "felixMinAge": 0, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "korsika", "emoji": "⛰️", "name": "Korsika direkt", "country": "🇫🇷 Frankreich", "sub": "Palombaggia · Porto-Vecchio · Bonifacio", "status": "MAYBE", "price": 3200, "priceLabel": "Ferienwohnung + Flug", "priceBadge": "Im Budget", "priceBadgeClass": "pb-mid", "scoreFelix": 70, "scoreLea": 70, "scoreCar": 100, "flight": "via Paris", "from": "Umsteigen!", "climate": "2832°C", "available": "Hochpreis", "highlight": "Schönste Strände Europas · Lea hat Korsika okayed", "pros": ["Palombaggia: traumhaft", "Lea okay für Korsika"], "cons": ["Kein Direktflug ab HAM/HAJ", "Hochpreisinsel im Sommer"], "note": "Besser als AIDA-Stopp in Ajaccio als Kreuzfahrt-Erlebnis.", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9"]}, {"id": "madeira", "emoji": "🌺", "name": "Madeira", "country": "🇵🇹 Portugal", "sub": "Dritter Anlauf · Levadas · Porto Moniz", "status": "SPÄTER", "price": 3000, "priceLabel": "Flug + Hotel HP", "priceBadge": "Im Budget", "priceBadgeClass": "pb-mid", "scoreFelix": 40, "scoreLea": 80, "scoreCar": 100, "flight": "4h 30", "from": "HAM→FNC", "climate": "2225°C", "available": "2028?", "highlight": "Lea: \"wenn Felix älter ist\" · 2× Corona-Storno · 3. Anlauf!", "pros": ["2225°C mildestes Klima", "Mietwagen-Paradies (Levadas)", "Vertraut 2× geplant gewesen"], "cons": ["Kaum Sandstrand für Felix", "Lea: explizit \"warten bis Felix größer\""], "note": "📅 Ab Felix 5J+ (ca. 2028/29)", "felixMinAge": 5, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "schweden", "emoji": "🦌", "name": "Schweden", "country": "🇸🇪 Schweden", "sub": "Bohuslän · Astrid Lindgrens Welt", "status": "SPÄTER", "price": 2800, "priceLabel": "Ferienhäuser + Mietwagen", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 85, "scoreLea": 50, "scoreCar": 100, "flight": "1h 20", "from": "HAM→GOT", "climate": "1824°C", "available": "2027", "highlight": "Till's Herzensthema · Astrid Lindgrens Welt!", "pros": ["Astrid Lindgrens Welt (Felix mit 4J+)", "Direktflug HAM→Göteborg 1:20h", "Natur pur: Seen, Wälder, Schären"], "cons": ["Lea: \"nicht meine Vorstellung von Sommerurlaub\"", "Wetter wechselhaft"], "note": "📅 Ab Felix 4J (ca. 2027) Astrid Lindgrens Welt!", "felixMinAge": 4, "season": ["6", "7", "8"]}, {"id": "bali", "emoji": "🌴", "name": "Bali", "country": "🇮🇩 Indonesien", "sub": "Jenny's Verbindung · Ubud · Seminyak", "status": "SPÄTER", "price": 6000, "priceLabel": "Flug + Villa", "priceBadge": "Über Budget", "priceBadgeClass": "pb-pricey", "scoreFelix": 50, "scoreLea": 95, "scoreCar": 80, "flight": "~14h", "from": "Umsteigen", "climate": "2832°C", "available": "2030+", "highlight": "Lea's Freundin Jenny hat Mann aus Bali", "pros": ["Traumziel für beide", "Günstige Luxusvillen mit Pool", "Jenny als Insider-Kontakt"], "cons": ["Langer Flug + Zeitzone (+6h)", "Gesundheit/Impfungen", "Aktuell zu teuer"], "note": "📅 Ab Felix 78J (ca. 2030)", "felixMinAge": 7, "season": ["4", "5", "6", "7", "8", "9"]}];
// ============= HISTORY DATA =============
const HISTORY = [
{year:2015,emoji:"🇬🇧",dest:"Zimmer in London (Bermondsey)",loc:"London, UK · Airbnb bei Gordon",type:"airbnb",pers:"Till + Bruder Jasper",date:"26.31. Aug. · 5N"},
{year:2016,emoji:"🌲",dest:"A-Frame Cabin in Sulzfeld",loc:"Feriendorf Sulzfeld, Deutschland",type:"airbnb",pers:"Till solo",date:"5.10. Sept. · 5N"},
{year:2016,emoji:"🇵🇹",dest:"Porto Zimmer in Baixa",loc:"Porto, Portugal",type:"airbnb",pers:"Till solo",date:"26.29. Sept. · 3N"},
{year:2016,emoji:"🇳🇱",dest:"Kortenhoef (Niederlande)",loc:"Kortenhoef, NL",type:"airbnb",pers:"Till solo",date:"1.3. Okt. · 2N"},
{year:2017,emoji:"🇵🇹",dest:"Lissabon (Anjos)",loc:"Lisboa, Portugal",type:"airbnb",pers:"Till solo",date:"26.28. Sept. · 2N"},
{year:2017,emoji:"🌺",dest:"Funchal, Madeira (8N)",loc:"Funchal, Madeira",type:"airbnb",pers:"Till solo",date:"28. Sept.6. Okt. · 8N"},
{year:2017,emoji:"🍷",dest:"Vila Nova de Gaia / Porto",loc:"Porto / Douro, Portugal",type:"airbnb",pers:"Till solo",date:"6.10. Okt. · 4N"},
{year:2018,emoji:"🇸🇪",dest:"Sunne, Värmland Hochzeit",loc:"Sunne, Schweden · Till schwärmt seitdem",type:"family",pers:"Till + Familie",date:"Sommer"},
{year:2019,emoji:"🇬🇧",dest:"Greater London (Battersea)",loc:"London, UK · bei Jilly",type:"airbnb",pers:"Till + Lea",date:"22.25. März · 3N"},
{year:2019,emoji:"🌴",dest:"Vanilla Garden · Teneriffa",loc:"Playa de las Americas · 4★ HP",type:"hotel",pers:"Till + Lea",date:"31.0707.08 · 7N · #41629995"},
{year:2019,emoji:"🇩🇰",dest:"Oksbøl Ferienhaus",loc:"Oksbøl, Dänemark",type:"airbnb",pers:"Till + Lea",date:"29.Okt.2.Nov. · 4N"},
{year:2020,emoji:"🚤",dest:"Waterside Cabin Vinkeveen",loc:"Vinkeveen (nahe Amsterdam), NL",type:"airbnb",pers:"Till + Lea",date:"22.29. Aug. · 7N"},
{year:2021,emoji:"🌴",dest:"Radisson Blu Gran Canaria",loc:"Patalavaca · 5★ HP",type:"hotel",pers:"Till + Lea",date:"21.28.07 · 7N · #47201487"},
{year:2022,emoji:"🌴",dest:"Dreams Jardin Tropical",loc:"Costa Adeje · Teneriffa · 4★ HP · 3.276 €",type:"hotel",pers:"Till + Lea",date:"08.15.07 · 7N · #12852050"},
{year:2023,emoji:"🚢",dest:"AIDAblu Adria-Kreuzfahrt",loc:"Korfu · Triest · Zadar · Kotor · Dubrovnik · Bari",type:"cruise",pers:"Till+Lea+Felix+Astrid",date:"13.20.08 · 7N · 5.557 € · #14997795"},
{year:2023,emoji:"🏨",dest:"Acharavi Beach Hotel · Korfu",loc:"Acharavi, GR · 546 €",type:"hotel",pers:"Till+Lea+Felix",date:"20.22.08 · 2N · #410637840"},
{year:2023,emoji:"🏡",dest:"MarinaPark Bad Nederrijn",loc:"Maurik, NL",type:"ferienhaus",pers:"Familie",date:"01.05.10 · 4N"},
{year:2024,emoji:"🏡",dest:"EuroParcs De Utrechtse Heuvelrug",loc:"Maarn, NL · 422 €",type:"ferienhaus",pers:"Familie",date:"23.28.03 · 5N"},
{year:2024,emoji:"🚢",dest:"AIDAprima Norwegen",loc:"HAM→Ålesund→Geiranger→Bergen→HAM",type:"cruise",pers:"Familie",date:"15.22.06 · 7N · 1.998 € · #15808257"},
{year:2024,emoji:"🏡",dest:"Sonne und Strand, Følle Strand",loc:"Rønde, Dänemark",type:"ferienhaus",pers:"Familie",date:"14.21.09 · 7N"},
{year:2025,emoji:"🏡",dest:"Residence Berger Duinen",loc:"Bergen aan Zee, NL",type:"ferienhaus",pers:"Familie",date:"24.28.03 · 4N"},
{year:2025,emoji:"🏡",dest:"De Strabrechtse Vennen",loc:"Niederlande · Haus E8",type:"ferienhaus",pers:"Familie",date:"14.21.07 · 7N"},
{year:2025,emoji:"🏡",dest:"EuroParcs Esonstad",loc:"Niederlande",type:"ferienhaus",pers:"Familie",date:"27.31.10 · 4N"},
{year:2026,emoji:"🚤",dest:"De Reeuwijkse Plassen",loc:"NL · 4-Pers.-Wasserhaus #191",type:"ferienhaus",pers:"Familie",date:"30.0303.04 · 4N"},
];
// ============= AUTH =============
(function(){if(sessionStorage.getItem('f24')==='1')document.getElementById('gate').style.display='none';})();
function auth(){
if(document.getElementById('pw').value==='frediurlaub26'){
document.getElementById('gate').style.display='none';
sessionStorage.setItem('f24','1');
init();
} else {
document.getElementById('pw-err').style.display='block';
document.getElementById('pw').style.borderColor='var(--coral)';
setTimeout(()=>document.getElementById('pw').style.borderColor='',1500);
}
}
// ============= USER PROFILE =============
function getCurrentUser(){return localStorage.getItem('f24_user')||'Till';}
function setUser(u){
localStorage.setItem('f24_user',u);
document.getElementById('user-name').textContent=u;
document.getElementById('opt-till').classList.toggle('active',u==='Till');
document.getElementById('opt-lea').classList.toggle('active',u==='Lea');
closeModal('user-modal');
renderCards();
showToast('👤 Profil: '+u);
}
function switchUser(){
const cur=getCurrentUser();
document.getElementById('opt-till').classList.toggle('active',cur==='Till');
document.getElementById('opt-lea').classList.toggle('active',cur==='Lea');
document.getElementById('user-modal').classList.add('open');
}
// ============= NAV =============
function navTo(tab,btn){
document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.bnav-item').forEach(b=>b.classList.remove('active'));
document.getElementById('tab-'+tab).classList.add('active');
btn.classList.add('active');
if(tab==='ranking')renderRanking();
if(tab==='eigene')renderOwn();
if(tab==='archiv')renderHistory();
}
// ============= DATA ACCESS =============
function getOwn(){return JSON.parse(localStorage.getItem('f24_own')||'[]');}
function saveOwn(arr){localStorage.setItem('f24_own',JSON.stringify(arr));}
function getRatings(){return JSON.parse(localStorage.getItem('f24_ratings')||'{}');}
function saveRatings(r){localStorage.setItem('f24_ratings',JSON.stringify(r));}
function getComments(){return JSON.parse(localStorage.getItem('f24_comments')||'{}');}
function saveComments(c){localStorage.setItem('f24_comments',JSON.stringify(c));}
function getAllDestinations(){
return [...DESTINATIONS_BUILTIN, ...getOwn()];
}
// ============= STAR RATING =============
function setRating(destId,user,stars){
const r=getRatings();
if(!r[destId])r[destId]={};
r[destId][user]=r[destId][user]===stars?0:stars;
saveRatings(r);
renderCards();
if(document.getElementById('tab-ranking').classList.contains('active'))renderRanking();
updateStats();
showToast(stars+' Sterne von '+user);
}
function getAvgRating(destId){
const r=getRatings()[destId]||{};
const vals=Object.values(r).filter(v=>v>0);
return vals.length?(vals.reduce((a,b)=>a+b,0)/vals.length):0;
}
function renderStars(destId){
const r=getRatings()[destId]||{};
const avg=getAvgRating(destId);
return `<div class="rating-section">
<div class="rating-row">
<span class="rating-who">Till</span>
<div class="stars">${[1,2,3,4,5].map(s=>`<button class="star ${(r.Till||0)>=s?'filled':''}" onclick="setRating('${destId}','Till',${s})">★</button>`).join('')}</div>
</div>
<div class="rating-row">
<span class="rating-who">Lea</span>
<div class="stars">${[1,2,3,4,5].map(s=>`<button class="star ${(r.Lea||0)>=s?'filled':''}" onclick="setRating('${destId}','Lea',${s})">★</button>`).join('')}</div>
</div>
</div>
<div class="rating-summary">
<div class="avg-num">${avg.toFixed(1)}</div>
<div class="avg-lbl">Ø Sterne</div>
</div>`;
}
// ============= COMMENTS =============
function toggleComments(destId){
const body=document.getElementById('cm-body-'+destId);
const tog=document.getElementById('cm-tog-'+destId);
body.classList.toggle('open');
tog.classList.toggle('open');
}
function addComment(destId){
const inp=document.getElementById('cm-inp-'+destId);
const txt=inp.value.trim();
if(!txt)return;
const c=getComments();
if(!c[destId])c[destId]=[];
c[destId].push({
user:getCurrentUser(),
text:txt,
ts:Date.now()
});
saveComments(c);
inp.value='';
renderCards();
updateStats();
setTimeout(()=>{
const body=document.getElementById('cm-body-'+destId);
const tog=document.getElementById('cm-tog-'+destId);
if(body){body.classList.add('open');tog.classList.add('open');}
},50);
showToast('💬 Kommentar gepostet');
}
function deleteComment(destId,idx){
if(!confirm('Diesen Kommentar löschen?'))return;
const c=getComments();
c[destId].splice(idx,1);
saveComments(c);
renderCards();
}
function formatTime(ts){
const d=new Date(ts);
const now=new Date();
const diff=now-d;
if(diff<60000)return'gerade';
if(diff<3600000)return Math.floor(diff/60000)+' Min';
if(diff<86400000)return Math.floor(diff/3600000)+' Std';
return d.toLocaleDateString('de',{day:'2-digit',month:'short'});
}
function renderComments(destId){
const c=getComments()[destId]||[];
const cur=getCurrentUser();
return `<button class="comments-toggle" id="cm-tog-${destId}" onclick="toggleComments('${destId}')">
<span class="arr">▸</span>
💬 Kommentare
<span class="ccount">${c.length}</span>
</button>
<div class="comments-body" id="cm-body-${destId}">
${c.length===0?'<div class="comment-empty">Noch keine Kommentare. Macht den Anfang!</div>':c.map((cm,i)=>`
<div class="comment ${cm.user.toLowerCase()}">
<div class="comment-head">
<span class="comment-who">${cm.user}</span>
<div>
<span class="comment-time">${formatTime(cm.ts)}</span>
${cm.user===cur?`<button class="comment-del" onclick="deleteComment('${destId}',${i})" title="Löschen">×</button>`:''}
</div>
</div>
<div class="comment-text">${escapeHtml(cm.text)}</div>
</div>
`).join('')}
<div class="comment-input-row">
<input class="comment-input" id="cm-inp-${destId}" placeholder="Schreib was als ${cur}..." onkeydown="if(event.key==='Enter')addComment('${destId}')">
<button class="comment-btn" onclick="addComment('${destId}')">Posten</button>
</div>
</div>`;
}
function escapeHtml(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);}
// ============= RENDER CARDS =============
let currentFilter='ALL';
function filterStatus(status,btn){
document.querySelectorAll('#tab-ideen .filter-row .chip').forEach(p=>p.classList.remove('active'));
btn.classList.add('active');
currentFilter=status;
renderCards();
}
function renderCards(){
const all=getAllDestinations();
const sort=document.getElementById('sort-select').value;
let filtered=all;
if(currentFilter==='OWN'){
filtered=all.filter(d=>d.isOwn);
} else if(currentFilter!=='ALL'){
filtered=all.filter(d=>d.status===currentFilter);
}
// Sort
if(sort==='ranking'){
filtered.sort((a,b)=>{
const ra=getAvgRating(a.id),rb=getAvgRating(b.id);
if(rb!==ra)return rb-ra;
return (a.price||0)-(b.price||0);
});
} else if(sort==='status'){
const order={'TOP':0,'NEW':1,'OK':2,'MAYBE':3,'SPÄTER':4,'NEIN':5};
filtered.sort((a,b)=>(order[a.status]||9)-(order[b.status]||9));
} else if(sort==='price-asc'){
filtered.sort((a,b)=>(a.price||9999)-(b.price||9999));
} else if(sort==='price-desc'){
filtered.sort((a,b)=>(b.price||0)-(a.price||0));
} else if(sort==='name'){
filtered.sort((a,b)=>a.name.localeCompare(b.name));
} else if(sort==='newest'){
filtered.sort((a,b)=>{
if(a.isOwn&&!b.isOwn)return -1;
if(!a.isOwn&&b.isOwn)return 1;
return (b.createdAt||0)-(a.createdAt||0);
});
}
const html=filtered.map((d,idx)=>renderCard(d,sort==='ranking'?idx:null)).join('');
document.getElementById('dg').innerHTML=html||'<div class="empty"><div class="empty-icon">🔍</div><div class="empty-title">Keine Treffer</div><div class="empty-sub">Filter ändern oder neue Idee einreichen.</div></div>';
updateStats();
updateOwnBadge();
}
function renderCard(d,rank){
const avg=getAvgRating(d.id);
const isOwn=d.isOwn===true;
const rankBadge=rank!==null&&avg>0?`<div class="rank-badge show ${rank===0?'top1':rank===1?'top2':rank===2?'top3':''}">#${rank+1}</div>`:'';
return `<div class="dest-card ${isOwn?'own':''}" data-status="${d.status}" data-id="${d.id}" id="card-${d.id}">
${isOwn?'<div class="own-marker">💜 Eigener Vorschlag</div>':''}
${rankBadge}
<div class="card-hdr" style="${isOwn?'padding-top:24px':''}">
<span class="dest-em">${d.emoji||'🌍'}</span>
<div class="dest-meta">
<div class="dest-name">${escapeHtml(d.name)}</div>
<div class="dest-sub">${escapeHtml(d.sub||d.country||'')}</div>
</div>
<div class="status-wrap">
<span class="sbadge s-${d.status}">${d.status}</span>
${d.isRecommendation?'<span class="sbadge s-NEW">⭐ Empfehlung</span>':''}
</div>
</div>
${d.price?`<div class="card-price">
<span class="price-main">~${d.price.toLocaleString('de')} €</span>
<span class="price-range">${escapeHtml(d.priceLabel||'')}</span>
${d.priceBadge?`<span class="price-badge ${d.priceBadgeClass||'pb-mid'}">${escapeHtml(d.priceBadge)}</span>`:''}
</div>`:''}
${d.scoreFelix!==undefined?`<div class="card-scores">
<div class="sc"><div class="sc-lbl">Felix</div><div class="sc-bar"><div class="sc-fill sc-f" style="width:${d.scoreFelix||50}%"></div></div></div>
<div class="sc"><div class="sc-lbl">Lea</div><div class="sc-bar"><div class="sc-fill sc-l" style="width:${d.scoreLea||50}%"></div></div></div>
<div class="sc"><div class="sc-lbl">Mietwagen</div><div class="sc-bar"><div class="sc-fill sc-m" style="width:${d.scoreCar||50}%"></div></div></div>
</div>`:''}
${d.flight?`<div class="card-stats">
<div class="cstat"><span class="cstat-val">${escapeHtml(d.flight)}</span><span class="cstat-key">Flug</span></div>
<div class="cstat"><span class="cstat-val">${escapeHtml(d.from||'')}</span><span class="cstat-key">Von</span></div>
<div class="cstat"><span class="cstat-val">${escapeHtml(d.climate||'')}</span><span class="cstat-key">Klima</span></div>
<div class="cstat"><span class="cstat-val">${escapeHtml(d.available||'')}</span><span class="cstat-key">Verfügbar</span></div>
</div>`:''}
${d.highlight||d.pros||d.note||d.why?`<div class="card-body">
${d.highlight?`<div class="card-highlight">${escapeHtml(d.highlight)}</div>`:''}
${d.why&&!d.highlight?`<div class="card-highlight">${escapeHtml(d.why)}</div>`:''}
${(d.pros&&d.pros.length)||(d.cons&&d.cons.length)?`<div class="pros-cons">
<ul class="pro-list">${(d.pros||[]).map(p=>`<li>${escapeHtml(p)}</li>`).join('')}</ul>
<ul class="con-list">${(d.cons||[]).map(c=>`<li>${escapeHtml(c)}</li>`).join('')}</ul>
</div>`:''}
${d.note?`<div class="card-note">${escapeHtml(d.note)}</div>`:''}
${isOwn?`<div style="display:flex;gap:6px;margin-top:8px"><button onclick="editOwn('${d.id}')" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;font-family:Outfit;color:var(--muted)">✏️ Bearbeiten</button><button onclick="deleteOwn('${d.id}')" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;font-family:Outfit;color:var(--coral)">🗑 Löschen</button></div>`:''}
</div>`:''}
<div class="rating-bar">
${renderStars(d.id)}
</div>
<div class="comments-section">
${renderComments(d.id)}
</div>
</div>`;
}
// ============= RANKING =============
function renderRanking(){
const all=getAllDestinations();
const ranked=all.map(d=>({...d,avg:getAvgRating(d.id),ratingCount:Object.values(getRatings()[d.id]||{}).filter(v=>v>0).length}))
.filter(d=>d.avg>0)
.sort((a,b)=>b.avg-a.avg);
if(ranked.length===0){
document.getElementById('ranking-list').innerHTML='<div class="empty"><div class="empty-icon">⭐</div><div class="empty-title">Noch keine Bewertungen</div><div class="empty-sub">Vergebt Sterne bei den Ideen-Karten Ranking erscheint dann automatisch.</div></div>';
return;
}
const html=ranked.map((d,i)=>{
const r=getRatings()[d.id]||{};
const tStars=r.Till?'⭐'.repeat(r.Till):'';
const lStars=r.Lea?'⭐'.repeat(r.Lea):'';
return`<div class="rank-row" onclick="scrollToCard('${d.id}');navTo('ideen',document.querySelector('.bnav-item'))" style="cursor:pointer">
<div class="rank-num">#${i+1}</div>
<div>
<div class="rank-name"><span class="rank-emoji">${d.emoji}</span>${escapeHtml(d.name)}</div>
<div class="rank-meta">Till: ${tStars} · Lea: ${lStars}</div>
</div>
<div class="rank-price">${d.price?'~'+d.price.toLocaleString('de')+'€':''}</div>
<div class="rank-score">${d.avg.toFixed(1)}<span class="rank-stars">★</span></div>
</div>`;
}).join('');
document.getElementById('ranking-list').innerHTML=html;
}
// ============= OWN IDEAS =============
function renderOwn(){
const own=getOwn();
if(own.length===0){
document.getElementById('own-list').innerHTML='<div class="empty"><div class="empty-icon">💡</div><div class="empty-title">Noch keine eigenen Ideen</div><div class="empty-sub">Tippe auf den ✚-Button rechts unten und reiche deine erste Idee ein.</div></div>';
return;
}
document.getElementById('own-list').innerHTML=own.map(d=>renderCard({...d,isOwn:true},null)).join('');
}
function updateOwnBadge(){
const own=getOwn();
const badge=document.getElementById('own-badge');
if(!badge)return;
if(own.length>0){
badge.style.display='block';
badge.textContent=own.length;
} else {
badge.style.display='none';
}
}
// ============= SUBMIT IDEA =============
function openSubmit(){
document.getElementById('edit-id').value='';
document.getElementById('submit-btn').textContent='Idee einreichen 🚀';
['s-dest','s-land','s-budget','s-why','s-sub','s-flight'].forEach(id=>document.getElementById(id).value='');
document.getElementById('s-emoji').value='🌍';
document.getElementById('submit-modal').classList.add('open');
}
function editOwn(id){
const own=getOwn();
const d=own.find(x=>x.id===id);
if(!d)return;
document.getElementById('edit-id').value=id;
document.getElementById('s-emoji').value=d.emoji||'🌍';
document.getElementById('s-dest').value=d.name||'';
document.getElementById('s-land').value=d.country||'';
document.getElementById('s-budget').value=d.price||'';
document.getElementById('s-sub').value=d.sub||'';
document.getElementById('s-why').value=d.why||d.highlight||'';
document.getElementById('s-who').value=d.submittedBy||'Till';
document.getElementById('s-status').value=d.status||'OK';
document.getElementById('s-flight').value=d.flight||'';
document.getElementById('submit-btn').textContent='Änderungen speichern 💾';
document.getElementById('submit-modal').classList.add('open');
}
function deleteOwn(id){
if(!confirm('Diese Idee wirklich löschen?'))return;
const own=getOwn().filter(d=>d.id!==id);
saveOwn(own);
renderCards();
renderOwn();
showToast('🗑 Idee gelöscht');
}
function submitIdea(){
const dest=document.getElementById('s-dest').value.trim();
if(!dest){
document.getElementById('s-dest').style.borderColor='var(--coral)';
setTimeout(()=>document.getElementById('s-dest').style.borderColor='',1500);
return;
}
const editId=document.getElementById('edit-id').value;
const own=getOwn();
const data={
id:editId||'own-'+Date.now(),
isOwn:true,
emoji:document.getElementById('s-emoji').value||'🌍',
name:dest,
country:document.getElementById('s-land').value||'',
sub:document.getElementById('s-sub').value||document.getElementById('s-land').value||'',
status:document.getElementById('s-status').value,
price:parseInt(document.getElementById('s-budget').value)||null,
priceLabel:'',
priceBadge:'',
priceBadgeClass:'pb-mid',
flight:document.getElementById('s-flight').value||'',
from:'',
climate:'',
available:'',
why:document.getElementById('s-why').value||'',
highlight:document.getElementById('s-why').value||'',
pros:[],
cons:[],
submittedBy:document.getElementById('s-who').value,
createdAt:editId?(own.find(x=>x.id===editId)||{}).createdAt||Date.now():Date.now()
};
if(editId){
const idx=own.findIndex(d=>d.id===editId);
if(idx>=0)own[idx]=data;
} else {
own.push(data);
}
saveOwn(own);
closeModal('submit-modal');
showToast(editId?'💾 Geändert!':'💡 Idee eingereicht!');
renderCards();
renderOwn();
updateOwnBadge();
}
function closeModal(id){document.getElementById(id).classList.remove('open');}
// ============= HISTORY =============
function renderHistory(){
const grouped={};
HISTORY.forEach(h=>{
if(!grouped[h.year])grouped[h.year]=[];
grouped[h.year].push(h);
});
const years=Object.keys(grouped).sort();
let html='';
years.forEach(y=>{
html+=`<div class="hist-year" data-year="${y}">${y}${y==='2023'?' ab hier mit Felix 👶':''}</div>`;
grouped[y].forEach(h=>{
html+=`<div class="hist-item ${h.type}" data-type="${h.type}">
<span class="hist-em">${h.emoji}</span>
<div class="hist-body">
<div class="hist-dest-name">${escapeHtml(h.dest)}</div>
<div class="hist-loc">${escapeHtml(h.loc)}</div>
<div class="hist-tags">
<span class="htag htag-${h.type==='ferienhaus'?'fh':h.type}">${h.type==='hotel'?'Hotel':h.type==='cruise'?'Kreuzfahrt':h.type==='ferienhaus'?'Ferienhaus':h.type==='airbnb'?'Airbnb':'Familie'}</span>
<span class="htag" style="background:var(--bg2);color:var(--muted)">${escapeHtml(h.date)}</span>
</div>
</div>
<span class="hist-pers">${escapeHtml(h.pers)}</span>
</div>`;
});
});
// Add open slot 2026
html+=`<div class="hist-item" style="border:2px dashed var(--gold);background:var(--gold-light);" data-type="hotel">
<span class="hist-em">❓</span>
<div class="hist-body">
<div class="hist-dest-name" style="color:var(--gold)">Sommerurlaub 2026 noch offen!</div>
<div class="hist-loc">Noch nicht gebucht · ab 13. Juli · max. 3.600 €</div>
<div class="hist-tags"><span class="htag" style="background:var(--gold);color:#fff">OFFEN</span></div>
</div>
<span class="hist-pers">Familie</span>
</div>`;
document.getElementById('hl').innerHTML=html;
}
function filterHistory(type,btn){
document.querySelectorAll('#tab-archiv .filter-row .chip').forEach(p=>p.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('#hl .hist-item').forEach(i=>{
i.style.display=(type==='ALL'||i.dataset.type===type)?'':'none';
});
document.querySelectorAll('#hl .hist-year').forEach(yr=>{
let el=yr.nextElementSibling;
let found=false;
while(el&&el.classList&&el.classList.contains('hist-item')){
if(el.style.display!=='none'){found=true;break;}
el=el.nextElementSibling;
}
yr.style.display=found?'':'none';
});
}
// ============= STATS =============
function updateStats(){
const all=getAllDestinations();
document.getElementById('stat-total').textContent=all.length;
const ratings=getRatings();
const ratedIds=Object.keys(ratings).filter(id=>Object.values(ratings[id]).some(v=>v>0));
document.getElementById('stat-rated').textContent=ratedIds.length;
const comments=getComments();
const cCount=Object.values(comments).reduce((sum,arr)=>sum+arr.length,0);
document.getElementById('stat-comments').textContent=cCount;
// Days until July 13 2026
const now=new Date();
const target=new Date('2026-07-13');
const days=Math.max(0,Math.ceil((target-now)/(1000*60*60*24)));
document.getElementById('stat-days').textContent=days>0?days:'🎉';
}
// ============= SCROLL TO CARD =============
function scrollToCard(id){
// Reset filter to ALL first
filterStatus('ALL',document.querySelector('#tab-ideen .filter-row .chip'));
setTimeout(()=>{
const card=document.getElementById('card-'+id);
if(card)card.scrollIntoView({behavior:'smooth',block:'start'});
},100);
}
// ============= EXPORT =============
function exportData(){
const data={
ratings:getRatings(),
comments:getComments(),
ownIdeas:getOwn(),
exportedAt:new Date().toISOString()
};
navigator.clipboard.writeText(JSON.stringify(data,null,2)).then(()=>{
showToast('📋 Daten in Zwischenablage kopiert');
});
}
// ============= TOAST =============
function showToast(msg){
const t=document.getElementById('toast');
t.textContent=msg;t.classList.add('show');
setTimeout(()=>t.classList.remove('show'),2200);
}
// ============= INIT =============
function init(){
document.getElementById('user-name').textContent=getCurrentUser();
renderCards();
updateStats();
updateOwnBadge();
setInterval(updateStats,60000); // Update days counter every minute
}
window.addEventListener('load',()=>{
if(sessionStorage.getItem('f24')==='1')init();
});
</script>
</body>
</html>