@@ -0,0 +1,537 @@
<!DOCTYPE html>
< html lang = "de" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< meta name = "theme-color" content = "#0ea5b7" >
< title > Urlaub 2026 – Familie Heidrich · Vergleich & Abstimmung< / title >
< link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" / >
< style >
: root {
--bg : #f7f9fc ; --surf : #ffffff ; --surf2 : #f1f5f9 ; --line : #e6ebf2 ; --line2 : #dbe3ee ;
--ink : #0f172a ; --ink2 : #475569 ; --mut : #7c8aa0 ;
--accent : #0ea5b7 ; --accent-d : #0b7e8c ; --hr : #e8643c ; --gold : #f6b73c ; --green : #16a36b ;
--kro : #e8643c ; --kan : #f1a23b ; --mad : #19a37a ; --cruise : #2f7fd6 ;
--shadow : 0 1 px 2 px rgba ( 15 , 23 , 42 , .04 ) , 0 8 px 24 px -12 px rgba ( 15 , 23 , 42 , .12 ) ;
--r : 18 px ;
}
* { box-sizing : border-box }
html { scroll-behavior : smooth }
body { margin : 0 ; background : var ( - - bg ) ; color : var ( - - ink ) ;
font-family : - apple-system , BlinkMacSystemFont , "Segoe UI" , Roboto , Helvetica , Arial , sans-serif ;
line-height : 1.55 ; -webkit- text-size-adjust : 100 % }
a { color : var ( - - accent - d ) }
img { max-width : 100 % }
. wrap { max-width : 1180 px ; margin : 0 auto ; padding : 0 16 px }
/* ---------- Voter bar ---------- */
. voterbar { position : sticky ; top : 0 ; z-index : 1000 ; background : rgba ( 255 , 255 , 255 , .92 ) ;
backdrop-filter : saturate ( 160 % ) blur ( 10 px ) ; border-bottom : 1 px solid var ( - - line ) }
. voterbar . in { max-width : 1180 px ; margin : 0 auto ; padding : 8 px 16 px ; display : flex ; align-items : center ; gap : 10 px ; flex-wrap : wrap }
. voterbar b { font-size : 13 px ; color : var ( - - ink2 ) ; white-space : nowrap }
. who { display : flex ; gap : 6 px ; flex-wrap : wrap }
. who button { border : 1 px solid var ( - - line2 ) ; background : #fff ; color : var ( - - ink2 ) ;
padding : 6 px 12 px ; border-radius : 999 px ; font-size : 13 px ; font-weight : 600 ; cursor : pointer ; display : flex ; align-items : center ; gap : 6 px }
. who button . av { width : 18 px ; height : 18 px ; border-radius : 50 % ; display : inline-block }
. who button . on { color : #fff ; border-color : transparent }
. who button . on . till { background : #2f7fd6 } . who button . on . lea { background : #e8643c } . who button . on . astrid { background : #19a37a }
. av . till { background : #2f7fd6 } . av . lea { background : #e8643c } . av . astrid { background : #19a37a }
. voterbar . sp { flex : 1 }
. linkbtn { font-size : 12.5 px ; color : var ( - - ink2 ) ; text-decoration : none ; border : 1 px solid var ( - - line2 ) ; padding : 5 px 10 px ; border-radius : 999 px }
/* ---------- Hero ---------- */
. hero { position : relative ; border-radius : 0 0 24 px 24 px ; overflow : hidden ; margin-bottom : 8 px }
. hero . bg { position : absolute ; inset : 0 ; background-size : cover ; background-position : center ; transform : scale ( 1.03 ) }
. hero . ov { position : absolute ; inset : 0 ; background : linear-gradient ( 180 deg , rgba ( 8 , 40 , 55 , .34 ) , rgba ( 8 , 40 , 55 , .66 ) ) }
. hero . ct { position : relative ; max-width : 1180 px ; margin : 0 auto ; padding : 54 px 18 px 30 px ; color : #fff }
. hero h1 { font-size : clamp ( 26 px , 5.4 vw , 42 px ) ; margin : 0 0 8 px ; letter-spacing : -.02 em ; line-height : 1.1 ; text-shadow : 0 2 px 18 px rgba ( 0 , 0 , 0 , .25 ) }
. hero p { margin : 0 ; max-width : 680 px ; font-size : clamp ( 14 px , 2.4 vw , 16.5 px ) ; color : #eaf3f6 ; text-shadow : 0 1 px 10 px rgba ( 0 , 0 , 0 , .3 ) }
. hero . facts { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin-top : 16 px }
. hero . facts span { background : rgba ( 255 , 255 , 255 , .16 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .25 ) ;
padding : 5 px 11 px ; border-radius : 999 px ; font-size : 12.5 px ; backdrop-filter : blur ( 4 px ) }
section { padding : 26 px 0 }
h2 . sec { font-size : clamp ( 19 px , 3.6 vw , 24 px ) ; letter-spacing : -.02 em ; margin : 0 0 4 px ; display : flex ; align-items : center ; gap : 9 px }
. sub { color : var ( - - ink2 ) ; margin : 0 0 16 px ; font-size : 14.5 px ; max-width : 720 px }
/* ---------- Recommendation cards ---------- */
. recos { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 16 px }
. reco { background : var ( - - surf ) ; border : 1 px solid var ( - - line ) ; border-radius : var ( - - r ) ; overflow : hidden ; box-shadow : var ( - - shadow ) ; display : flex ; flex-direction : column }
. reco . top { padding : 16 px 16 px 12 px ; border-bottom : 1 px solid var ( - - line ) }
. reco . rank { font-size : 11 px ; font-weight : 800 ; letter-spacing : .08 em ; text-transform : uppercase ; color : var ( - - accent - d ) }
. reco h3 { margin : 4 px 0 2 px ; font-size : 17.5 px ; letter-spacing : -.01 em }
. reco . place { color : var ( - - mut ) ; font-size : 12.5 px }
. reco . body { padding : 14 px 16 px ; display : flex ; flex-direction : column ; gap : 10 px ; flex : 1 }
. reco p { margin : 0 ; font-size : 13.5 px ; color : var ( - - ink2 ) }
. pricebox { background : var ( - - surf2 ) ; border-radius : 12 px ; padding : 11 px 12 px ; font-size : 13 px }
. pricebox . big { font-size : 20 px ; font-weight : 800 ; color : var ( - - ink ) ; letter-spacing : -.02 em }
. pricebox . li { display : flex ; justify-content : space-between ; gap : 8 px ; margin-top : 5 px ; color : var ( - - ink2 ) }
. pricebox . li b { color : var ( - - ink ) ; font-weight : 700 }
. reco . links { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin-top : auto ; padding-top : 4 px }
. btn { display : inline-block ; background : var ( - - accent ) ; color : #fff ; text-decoration : none ; font-weight : 700 ;
font-size : 12.5 px ; padding : 9 px 13 px ; border-radius : 10 px ; text-align : center }
. btn . ghost { background : #fff ; color : var ( - - accent - d ) ; border : 1 px solid var ( - - line2 ) }
. btn : active { transform : scale ( .97 ) }
/* ---------- Map ---------- */
# map { height : 430 px ; border-radius : var ( - - r ) ; border : 1 px solid var ( - - line ) ; box-shadow : var ( - - shadow ) }
. legend { display : flex ; gap : 14 px ; flex-wrap : wrap ; margin : 10 px 2 px 0 ; font-size : 12.5 px ; color : var ( - - ink2 ) }
. legend span { display : inline-flex ; align-items : center ; gap : 6 px }
. legend i { width : 11 px ; height : 11 px ; border-radius : 50 % ; display : inline-block }
/* ---------- Region + hotel cards ---------- */
. region-h { display : flex ; align-items : center ; gap : 10 px ; margin : 6 px 0 14 px }
. region-h . flag { font-size : 22 px }
. region-h h2 { font-size : 20 px ; margin : 0 ; letter-spacing : -.01 em }
. region-h . cnt { color : var ( - - mut ) ; font-size : 13 px }
. grid { display : grid ; grid-template-columns : repeat ( auto - fill , minmax ( 300 px , 1 fr ) ) ; gap : 16 px }
. card { background : var ( - - surf ) ; border : 1 px solid var ( - - line ) ; border-radius : var ( - - r ) ; overflow : hidden ; box-shadow : var ( - - shadow ) ; display : flex ; flex-direction : column }
. card . fav { border-color : var ( - - gold ) ; box-shadow : 0 0 0 1.5 px var ( - - gold ) inset , var ( - - shadow ) }
. ph { position : relative ; aspect-ratio : 16 / 10 ; background : linear-gradient ( 135 deg , #dfeaf2 , #eef3f8 ) ; overflow : hidden }
. ph img { position : absolute ; inset : 0 ; width : 100 % ; height : 100 % ; object-fit : cover ; opacity : 0 ; transition : opacity .4 s }
. ph img . on { opacity : 1 }
. ph . nav { position : absolute ; top : 0 ; bottom : 0 ; width : 42 % ; cursor : pointer ; z-index : 2 }
. ph . nav . l { left : 0 } . ph . nav . r { right : 0 }
. ph . dots { position : absolute ; bottom : 8 px ; left : 0 ; right : 0 ; display : flex ; gap : 5 px ; justify-content : center ; z-index : 3 }
. ph . dot { width : 6 px ; height : 6 px ; border-radius : 50 % ; background : #ffffff 90 ; box-shadow : 0 0 2 px rgba ( 0 , 0 , 0 , .3 ) }
. ph . dot . on { background : #fff }
. rtag { position : absolute ; top : 10 px ; left : 10 px ; z-index : 3 ; font-size : 11 px ; font-weight : 700 ; padding : 3 px 9 px ; border-radius : 999 px ; color : #fff }
. favtag { position : absolute ; top : 10 px ; right : 10 px ; z-index : 3 ; background : var ( - - gold ) ; color : #4a3000 ; font-size : 10.5 px ; font-weight : 800 ; padding : 3 px 9 px ; border-radius : 999 px }
. card . bd { padding : 13 px 14 px 15 px ; display : flex ; flex-direction : column ; gap : 9 px ; flex : 1 }
. card h3 { margin : 0 ; font-size : 16 px ; letter-spacing : -.01 em }
. loc { color : var ( - - mut ) ; font-size : 12.5 px ; margin-top : -3 px }
. stars-row { display : flex ; align-items : center ; gap : 8 px ; flex-wrap : wrap }
. crate { display : inline-flex ; align-items : center ; gap : 6 px ; font-size : 12.5 px ; color : var ( - - ink2 ) }
. crate b { background : #e7f6f4 ; color : #0b6f6b ; padding : 2 px 7 px ; border-radius : 6 px ; font-weight : 800 }
. chips { display : flex ; flex-wrap : wrap ; gap : 6 px }
. chip { font-size : 11.5 px ; background : var ( - - surf2 ) ; border : 1 px solid var ( - - line ) ; color : var ( - - ink2 ) ; padding : 3 px 8 px ; border-radius : 7 px }
. why { font-size : 12.5 px ; color : var ( - - ink2 ) ; display : flex ; flex-direction : column ; gap : 3 px ; background : #fbfdff ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; padding : 9 px 10 px }
. why . ok { color : var ( - - green ) ; font-weight : 700 } . why . no { color : var ( - - hr ) ; font-weight : 700 }
. foot { margin-top : auto ; display : flex ; justify-content : space-between ; align-items : center ; gap : 8 px ; padding-top : 2 px }
. price { font-size : 12.5 px ; color : var ( - - mut ) } . price b { color : var ( - - ink ) ; font-size : 15 px }
/* ---------- Voting widget ---------- */
. vote { border-top : 1 px dashed var ( - - line2 ) ; padding-top : 10 px ; margin-top : 2 px }
. vote . lead { font-size : 11.5 px ; color : var ( - - mut ) ; margin-bottom : 5 px }
. starpick { display : flex ; gap : 3 px }
. starpick . s { font-size : 22 px ; line-height : 1 ; cursor : pointer ; color : #d7dee8 ; transition : transform .08 s }
. starpick . s . fill { color : var ( - - gold ) }
. starpick . s : active { transform : scale ( 1.2 ) }
. voteinfo { display : flex ; align-items : center ; gap : 8 px ; margin-top : 7 px ; flex-wrap : wrap }
. avg { font-size : 12.5 px ; color : var ( - - ink2 ) }
. avg b { color : var ( - - ink ) ; font-weight : 800 }
. voters-mini { display : flex ; gap : 4 px }
. vm { width : 20 px ; height : 20 px ; border-radius : 50 % ; color : #fff ; font-size : 10 px ; font-weight : 800 ; display : flex ; align-items : center ; justify-content : center ; position : relative }
. vm . n { position : absolute ; - bottom : -2 px ; right : -3 px ; background : #fff ; color : #0f172a ; border-radius : 6 px ; font-size : 8.5 px ; padding : 0 2 px ; border : 1 px solid var ( - - line ) }
. vm . dim { opacity : .28 }
/* ---------- Results ---------- */
. resultbox { background : var ( - - surf ) ; border : 1 px solid var ( - - line ) ; border-radius : var ( - - r ) ; box-shadow : var ( - - shadow ) ; padding : 18 px }
. rank-list { display : flex ; flex-direction : column ; gap : 8 px ; margin-top : 8 px }
. rrow { display : flex ; align-items : center ; gap : 12 px ; padding : 10 px 12 px ; border-radius : 12 px ; background : var ( - - surf2 ) }
. rrow . pos { width : 26 px ; height : 26 px ; border-radius : 50 % ; background : #fff ; border : 1 px solid var ( - - line2 ) ; font-weight : 800 ; display : flex ; align-items : center ; justify-content : center ; font-size : 13 px ; flex : 0 0 auto }
. rrow . top1 { background : #fff7e6 ; border : 1 px solid #f6cf7a }
. rrow . nm { font-weight : 600 ; font-size : 14 px ; flex : 1 ; min-width : 0 }
. rrow . nm small { display : block ; color : var ( - - mut ) ; font-weight : 400 ; font-size : 11.5 px }
. rrow . bar { flex : 1.2 ; height : 8 px ; background : #e7edf4 ; border-radius : 6 px ; overflow : hidden ; max-width : 160 px }
. rrow . bar i { display : block ; height : 100 % ; background : linear-gradient ( 90 deg , #f6b73c , #e8643c ) }
. rrow . sc { font-weight : 800 ; font-size : 14 px ; white-space : nowrap }
. rrow . sc small { color : var ( - - mut ) ; font-weight : 500 ; font-size : 11 px }
. resethint { margin-top : 14 px ; display : flex ; justify-content : space-between ; align-items : center ; gap : 10 px ; flex-wrap : wrap }
. reset { background : #fff ; border : 1 px solid #f0c2c2 ; color : #c0392b ; font-weight : 700 ; font-size : 12.5 px ; padding : 8 px 13 px ; border-radius : 10 px ; cursor : pointer }
. reset : active { transform : scale ( .97 ) }
. saved { font-size : 12 px ; color : var ( - - green ) ; opacity : 0 ; transition : opacity .3 s }
. saved . on { opacity : 1 }
. note { font-size : 12 px ; color : var ( - - mut ) ; margin-top : 8 px }
footer { padding : 30 px 0 50 px ; color : var ( - - mut ) ; font-size : 12 px }
footer a { color : var ( - - ink2 ) }
/* ---------- Mobile ---------- */
@ media ( max-width : 860px ) { . recos { grid-template-columns : 1 fr } # map { height : 360 px } }
@ media ( max-width : 560px ) {
. wrap { padding : 0 12 px }
. hero . ct { padding : 40 px 14 px 24 px }
. grid { grid-template-columns : 1 fr ; gap : 13 px }
section { padding : 20 px 0 }
. voterbar . in { padding : 7 px 12 px ; gap : 8 px }
. voterbar b { flex-basis : 100 % ; margin-bottom : -2 px }
. linkbtn { display : none }
. rrow . bar { display : none }
}
< / style >
< / head >
< body >
<!-- Voter bar -->
< div class = "voterbar" >
< div class = "in" >
< b > Wer bist du?< / b >
< div class = "who" id = "who" > < / div >
< span class = "sp" > < / span >
< a class = "linkbtn" href = "#ergebnis" > 🏆 Ergebnis< / a >
< / div >
< / div >
<!-- Hero -->
< div class = "hero" >
< div class = "bg" id = "herobg" > < / div >
< div class = "ov" > < / div >
< div class = "ct" >
< h1 > Wohin im Sommer 2026? 🌅< / h1 >
< p > Hotels & AIDA-Kreuzfahrt im Vergleich – mit Fotos, Karte, Preisen und Mietwagen-Schätzung. Till, Lea und Astrid geben jeweils Sterne ab, unten seht ihr das Familien-Ergebnis.< / p >
< div class = "facts" >
< span > 👨👩👦 4 Personen (Till, Lea, Felix 3 J., Astrid)< / span >
< span > 📅 ~11.– 21. Juli 2026< / span >
< span > 🧳 3 große Koffer< / span >
< span > 🚗 max. 2 h Fahrt< / span >
< / div >
< / div >
< / div >
< div class = "wrap" >
<!-- 3 Empfehlungen -->
< section id = "empfehlungen" >
< h2 class = "sec" > 🎯 Unsere 3 Empfehlungen< / h2 >
< p class = "sub" > Familien-gewichtet (kleiner Felix, entspannt für Astrid, kurze Wege). Preise als Orientierung für 4 Personen – Hotelpreise auf Check24 mit eurer Belegung gegenprüfen.< / p >
< div class = "recos" id = "recos" > < / div >
< div class = "note" > 💡 Mietwagen für 4 Pers. + 3 große Koffer = mind. Kombi (z. B. VW Passat Variant) oder Kompakt-SUV/Van (Touran, Qashqai) – kein Kleinwagen. Hochsaison Juli: Kroatien ab Split ~60– 80 €/Tag, Kanaren ~45– 60 €/Tag.< / div >
< / section >
<!-- Karte -->
< section id = "karte" >
< h2 class = "sec" > 🗺️ Alles auf einer Karte< / h2 >
< p class = "sub" > Hotels als Punkte, die AIDA-Route als Linie. Tippe auf einen Marker für Details & Link.< / p >
< div id = "map" > < / div >
< div class = "legend" >
< span > < i style = "background:var(--kro)" > < / i > Kroatien< / span >
< span > < i style = "background:var(--kan)" > < / i > Kanaren< / span >
< span > < i style = "background:var(--mad)" > < / i > Madeira< / span >
< span > < i style = "background:var(--cruise)" > < / i > AIDA-Route< / span >
< span > ⭐ = unsere Empfehlung< / span >
< / div >
< / section >
<!-- Regions render here -->
< div id = "regions" > < / div >
<!-- Ergebnis -->
< section id = "ergebnis" >
< h2 class = "sec" > 🏆 Familien-Ergebnis< / h2 >
< p class = "sub" > Durchschnitt aller abgegebenen Sterne. Aktualisiert sich live, sobald jemand abstimmt.< / p >
< div class = "resultbox" >
< div class = "rank-list" id = "ranklist" > < / div >
< div class = "resethint" >
< span class = "note" > Stand wird auf dem Server gespeichert – alle drei sehen dasselbe.< / span >
< div style = "display:flex;align-items:center;gap:10px" >
< span class = "saved" id = "saved" > ✓ gespeichert< / span >
< button class = "reset" id = "resetBtn" > ↺ Alle Bewertungen zurücksetzen< / button >
< / div >
< / div >
< / div >
< / section >
< footer >
Fotos & Hoteldaten: Check24 · Kreuzfahrt: AIDA / Ahoi-Schiff.de · Karten: OpenStreetMap.
Preise & Verfügbarkeit ändern sich laufend – bitte vor Buchung prüfen. Erstellt für Familie Heidrich.
< / footer >
< / div >
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" > < / script >
< script >
const C24 = u => u; // check24 proxy urls already absolute
const REG = {
kroatien:{flag:'🇭🇷',name:'Kroatien',color:'var(--kro)',mk:'#e8643c'},
kanaren:{flag:'🇮🇨',name:'Kanaren',color:'var(--kan)',mk:'#f1a23b'},
madeira:{flag:'🇵🇹',name:'Madeira',color:'var(--mad)',mk:'#19a37a'},
kreuzfahrt:{flag:'🚢',name:'AIDA-Kreuzfahrt',color:'var(--cruise)',mk:'#2f7fd6'}
};
const OPTIONS = [
{id:'amadria',name:'Amadria Park Hotel Jakov',region:'kroatien',loc:'Šibenik · Norddalmatien',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:12,price:'ab 3.125 €',fav:true,geo:[43.698492,15.889556],
url:'https://urlaub.check24.de/suche/angebot?countryId=60& hotelId=12950& extendedSearch=1& airport=HAM,HAJ& transportType=flight& days=1w& departureDate=2026-07-11& returnDate=2026-07-21& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Direkte Strandlage','Familienresort','Flughafen Split 37 km'],
why:[['ok','Krka-Nationalpark ~30 Min'],['ok','Kornati-Inseln per Boot ab Resort'],['ok','Shopping Split ~1 h / Šibenik 6 km'],['ok','Sehr kinderfreundlich']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy8zNDAwMDAwMC8zMzYzMDAwMC8zMzYyNTkwMC8zMzYyNTg1OC9iM2EyNzQxY193LmpwZw==!07717e/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9ob3RlbGltYWdlcy5zdW5ob3RlbHMubmV0L0hvdGVsSW5mby9ob3RlbEltYWdlLmFzcHg-aWQ9MTYxODE3MjUmZnVsbD0x!cc720d/picture.jpg']},
{id:'valamar',name:'Valamar Meteor Hotel',region:'kroatien',loc:'Makarska · Mitteldalmatien',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:128,price:'ab 3.531 €',fav:false,geo:[43.29907,17.014845],
url:'https://urlaub.check24.de/suche/angebot?countryId=60& hotelId=4619& extendedSearch=1& airport=HAM,HAJ& transportType=flight& days=1w& departureDate=2026-07-11& returnDate=2026-07-21& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Top Hotel Award','Direkte Strandlage','Flughafen Split 64 km'],
why:[['ok','Top bewertet, Strand + Promenade'],['ok','Inselfähren Brač/Hvar via Split'],['no','Krka ~1h45, Plitvice 3h+'],['ok','Familientauglich']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL3Bob3Rvcy5ob3RlbGJlZHMuY29tL2dpYXRhL29yaWdpbmFsLzI1LzI1MzUxOS8yNTM1MTlhX2hiX3NfMDA1LmpwZw==!a9bbf6/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy84MDAwMDAwLzc3MDAwMDAvNzY5NzYwMC83Njk3NTMxLzhkMDY0YTM3X3cuanBn!7c92d2/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL21lZGlhLmRldi5wYXhpbXVtLmNvbS9ob3RlbGltYWdlcy8yNzA0MTgvNDcuanBn!8c2535/picture.jpg']},
{id:'bluesun',name:'Bluesun Hotel Berulia',region:'kroatien',loc:'Brela · Mitteldalmatien',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:13,price:'ab 4.706 €',fav:false,geo:[43.362448,16.938868],
url:'https://urlaub.check24.de/suche/angebot?countryId=60& hotelId=1739068& extendedSearch=1& airport=HAM,HAJ& transportType=flight& days=1w& departureDate=2026-07-11& returnDate=2026-07-21& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Direkte Strandlage','Traumstrand Brela','Flughafen Split 55 km'],
why:[['ok','Einer der schönsten Strände Kroatiens'],['no','Krka ~1h30– 2h, Plitvice zu weit'],['ok','Inseln via Split'],['no','Teuerste Hotel-Option']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy8zMDAwMDAwLzI4MjAwMDAvMjgxNDUwMC8yODE0NDA5LzZjYjI4NzAwX3cuanBn!774551/picture.jpg']},
{id:'morenia',name:'Morenia Beach Resort',region:'kroatien',loc:'Podaca · Mitteldalmatien',stars:4,
rate:'7,8',rlabel:'Gut',bew:48,price:'ab 3.527 €',fav:false,geo:[43.130196,17.286675],
url:'https://urlaub.check24.de/suche/angebot?hotelId=1859719& extendedSearch=1& airport=HAM,HAJ& transportType=flight& days=1w& departureDate=2026-07-11& returnDate=2026-07-21& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Direkte Strandlage','Familie & Strand','Flughafen Split 92 km'],
why:[['ok','Ruhige Bucht, familienorientiert'],['no','Weit von Split & Nationalparks'],['no','Längster Flughafentransfer'],['ok','Solide Preis-Leistung']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9hcGktaW1nLmhvdGVsc3Rvbi5jb20vcmVzb3VyY2UvaG90ZWwvaW1hZ2VzLzcvMy84LzMvNy8yLzYvMy8xMDMxMzQzNTY0LmpwZw==!9db0ac/picture.jpg']},
{id:'tui',name:'TUI KIDS CLUB Taurito Princess',region:'kanaren',loc:'Taurito · Gran Canaria',stars:4,
rate:'8,0',rlabel:'Sehr gut',bew:400,price:'Preis auf Check24',fav:true,geo:[27.815366,-15.754307],
url:'https://urlaub.check24.de/suche/angebot?hotelId=3265& extendedSearch=1& airport=HAM,HAJ& transportType=flight& days=1w& departureDate=2026-07-11& returnDate=2026-07-21& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Echter Kinderclub','Sommer mild ~26°C','Flughafen LPA 38 km'],
why:[['ok','Top für Felix (Betreuung & Animation)'],['ok','400 Bewertungen, sehr beliebt'],['no','Keine Kroatien-Wünsche (Insel/NP)'],['ok','Dünen & Berge für Ausflüge']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy85MDAwMDAwLzgxNjAwMDAvODE1ODAwMC84MTU3OTc5LzRjNjE3OTlkX3cuanBn!55230f/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL21lZGlhLmRldi5wYXhpbXVtLmNvbS9ob3RlbGltYWdlcy8yMjY0NTYvZDFhYzA3MGQ0OTg1Zjg3ZmI4YjM5OTA2MzAxNTMzZGQuanBn!c32a55/picture.jpg']},
{id:'iberostar',name:'Iberostar Waves Bouganville Playa',region:'kanaren',loc:'Playa de las Américas · Teneriffa',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:103,price:'Preis auf Check24',fav:false,geo:[28.074417,-16.732332],
url:'https://urlaub.check24.de/suche/angebot?hotelId=2134& extendedSearch=1& airport=HAM,HAJ& transportType=flight& days=1w& departureDate=2026-07-11& returnDate=2026-07-21& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Am Meer','Sehr kurzer Transfer','Flughafen TFS 16 km'],
why:[['ok','Nur 16 km vom Flughafen'],['ok','Teide-Nationalpark als Ausflug'],['no','Keine Boots-/Inselkultur wie HR'],['ok','Gut bewertet']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL3Bob3Rvcy5ob3RlbGJlZHMuY29tL2dpYXRhL29yaWdpbmFsLzAwLzAwMTEzNi8wMDExMzZhX2hiX3RfMDExLmpwZw==!e1aee2/picture.jpg']},
{id:'riu',name:'Hotel Riu Madeira',region:'madeira',loc:'Caniço de Baixo · Madeira',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:752,price:'Preis auf Check24',fav:false,geo:[32.645679,-16.826868],
url:'https://urlaub.check24.de/suche/angebot?countryId=88& hotelId=8761& extendedSearch=1& airport=HAM,HAJ,BRE,RLG,LBC,GWT& transportType=flight& days=1w& departureDate=2026-07-12& returnDate=2026-07-22& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Strand ~400 m','752 Bewertungen','Wander-Insel'],
why:[['ok','Sehr viele gute Bewertungen'],['no','Madeira = Wandern, kaum Sandstrand'],['no','Kein klassischer Badeurlaub für Felix'],['ok','Mild, grün, schöne Natur']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy81MDAwMDAwLzQ4MjAwMDAvNDgxMzQwMC80ODEzMzUxL2Y4ZjNkYTE3X3cuanBn!cb047b/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL3Bob3Rvcy5ob3RlbGJlZHMuY29tL2dpYXRhL29yaWdpbmFsLzAwLzAwNTUyOS8wMDU1MjlhX2hiX2FfMDIzLmpwZw==!406b07/picture.jpg']},
{id:'dreams',name:'Dreams Madeira Resort, Spa & Marina',region:'madeira',loc:'Caniçal · Madeira',stars:5,
rate:'8,4',rlabel:'Sehr gut',bew:111,price:'Preis auf Check24',fav:false,geo:[32.742452,-16.709186],
url:'https://urlaub.check24.de/suche/angebot?countryId=88& hotelId=30324& extendedSearch=1& airport=HAM,HAJ,BRE,RLG,LBC,GWT& transportType=flight& days=1w& departureDate=2026-07-12& returnDate=2026-07-22& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['5 Sterne','Direkte Strandlage','Marina & Spa'],
why:[['ok','Hochwertiges 5★-Resort'],['no','Ruhiger Osten – eher Ruhe/Paar'],['no','Madeira kein Bade-Klassiker'],['ok','Schön für Astrid']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy83MDAwMDAwLzYxNTAwMDAvNjE0MTgwMC82MTQxNzIzLzI5YmZiMjE0X3cuanBn!da02c7/picture.jpg']},
{id:'sentido',name:'Sentido Galosol',region:'madeira',loc:'Caniço de Baixo · Madeira',stars:4,
rate:null,rlabel:null,bew:216,price:'Preis auf Check24',fav:false,geo:[32.642268,-16.831963],
url:'https://urlaub.check24.de/suche/angebot?countryId=88& hotelId=2205& extendedSearch=1& airport=HAM,HAJ,BRE,RLG,LBC,GWT& transportType=flight& days=1w& departureDate=2026-07-12& returnDate=2026-07-22& roomAllocation=A-A-5& directFlight=1& hotelCategoryList=4,5& noRedirect=1',
chips:['Top Lage','216 Bewertungen','Meerblick'],
why:[['ok','Beliebte Lage, Klippenbad'],['no','Kein Sandstrand'],['no','Eher ruhig, nicht kinderfokussiert'],['ok','Gutes Preisniveau']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy8xMDAwMDAwLzkzMDAwMC85MjI0MDAvOTIyMzE5Lzg3YjE3ODc3X3ouanBn!d36a31/picture.jpg']},
{id:'aida',name:'AIDA Cosma · Mediterrane Schätze mit Korsika',region:'kreuzfahrt',loc:'ab/bis Mallorca · 7 Nächte · 11.– 18.07.',stars:0,
rate:'4,8',rlabel:'Ahoi',bew:null,price:'ab 6.477 €',fav:true,geo:[39.5696,2.6502],
url:'https://www.ahoi-schiff.de/aida/routen/mediterrane-schaetze-mit-korsika-ab-mallorca/aidacosma-2026-07-11?paxe=2',
url2:'https://aida.de/buchen/CO07260711/CLASSIC/meine-reise/anreise',
chips:['4 Pers., 2 Balkonkabinen','inkl. Flug & Vollpension','La Spezia · Rom · Korsika · Barcelona'],
why:[['ok','Null Selbstfahren – ideal für Astrid'],['ok','Fixer Komplettpreis, Vollpension'],['ok','Jeden Tag ein neues Ziel'],['no','Mit Felix (3) straffer Rhythmus']],
imgs:['https://files.ahoi-schiff.de/aida-cruises/s/aidacosma.webp']}
];
// AIDA route ports (for map polyline)
const ROUTE = [
{n:'Palma de Mallorca',c:[39.5696,2.6502]},
{n:'La Spezia / Florenz',c:[44.1025,9.8200]},
{n:'Rom / Civitavecchia',c:[42.0930,11.7896]},
{n:'Ajaccio (Korsika)',c:[41.9192,8.7386]},
{n:'Barcelona',c:[41.3568,2.1597]},
{n:'Palma de Mallorca',c:[39.5696,2.6502]}
];
const RECOS = [
{rank:'Empfehlung 1 · Rundum-Kroatien',opt:'amadria',
text:'Erfüllt alle Wünsche auf einmal: Krka-Nationalpark in ~30 Min, Kornati-Inseln per Boot, Shopping in Split – und ein großes, kinderfreundliches Strand-Resort. Kürzeste Wege, größtes „für alle was dabei".',
price:'ab ~5.400 €',
lines:[['Pauschal (Flug HAM/HAJ + Hotel)','~4.700 €*'],['Mietwagen Kombi/SUV (10 T)','~650 €'],['Nationalparks/Inseln','Krka, Kornati, Plitvice']],
foot:'*für 4 Pers. hochgerechnet – auf Check24 mit eurer Belegung prüfen.'},
{rank:'Empfehlung 2 · Entspannt ohne Fahren',opt:'aida',
text:'AIDA Cosma ab Mallorca: Florenz, Rom, Korsika und Barcelona – ohne Koffer-Schleppen und ohne Mietwagen. Fixer All-in-Preis mit Vollpension, perfekt wenn Astrid mitkommt. Mit Felix etwas straffer Tagesrhythmus.',
price:'~6.480 €',
lines:[['4 Pers., 2 Balkonkabinen','inkl. Flug'],['Vollpension an Bord','inkl.'],['Mietwagen','nicht nötig (0 €)']],
foot:'LIGHT/CLASSIC ab Hannover 6.477 € / ab HH 6.487– 6.677 €. Landausflüge optional extra.'},
{rank:'Empfehlung 3 · Stressfrei mit Kind',opt:'tui',
text:'TUI KIDS CLUB auf Gran Canaria: der entspannteste Familienurlaub – echter Kinderclub für Felix, mildes Sommerklima, kurzer Transfer. Ohne die Kroatien-Wünsche (Nationalpark/Insel), dafür maximal unkompliziert.',
price:'~4.500– 5.500 €',
lines:[['Pauschal (Flug + Hotel)','auf Check24 prüfen'],['Kinderclub für Felix','inkl.'],['Mietwagen optional','~500 € / 10 T']],
foot:'Preis grob für 4 Pers. – Gran Canaria & Teneriffa beide in der Auswahl unten.'}
];
const byId = id => OPTIONS.find(o=>o.id===id);
const VOTER_LABEL = {till:'Till',lea:'Lea',astrid:'Astrid'};
let me_ = localStorage.getItem('voter') || null;
let STATE = {votes:{}};
/* ---------- hero ---------- */
document.getElementById('herobg').style.backgroundImage =
"url('"+byId('bluesun').imgs[0]+"')";
/* ---------- voter selector ---------- */
const whoEl = document.getElementById('who');
function renderWho(){
whoEl.innerHTML='';
['till','lea','astrid'].forEach(v=>{
const b=document.createElement('button');
b.className=(me_===v?'on '+v:'');
b.innerHTML=`< span class = "av ${v}" > < / span > ${VOTER_LABEL[v]}`;
b.onclick=()=>{me_=v;localStorage.setItem('voter',v);renderWho();renderAll();};
whoEl.appendChild(b);
});
}
/* ---------- recommendations ---------- */
function renderRecos(){
const el=document.getElementById('recos');el.innerHTML='';
RECOS.forEach(r=>{
const o=byId(r.opt);
const links=`< a class = "btn" href = "${o.url}" target = "_blank" rel = "noopener" > ${o.region==='kreuzfahrt'?'Ahoi-Schiff →':'Check24 →'}< / a > `
+ (o.url2?`< a class = "btn ghost" href = "${o.url2}" target = "_blank" rel = "noopener" > AIDA.de →< / a > `:'')
+ `< a class = "btn ghost" href = "#${o.id}" > Details ↓< / a > `;
const d=document.createElement('div');d.className='reco';
d.innerHTML=`< div class = "top" > < div class = "rank" > ${r.rank}< / div > < h3 > ${o.name.replace(' · Mediterrane Schätze mit Korsika','')}< / h3 > < div class = "place" > 📍 ${o.loc}< / div > < / div >
< div class = "body" >
< p > ${r.text}< / p >
< div class = "pricebox" > < div class = "big" > ${r.price}< / div >
${r.lines.map(l=>`< div class = "li" > < span > ${l[0]}< / span > < b > ${l[1]}< / b > < / div > `).join('')}
< / div >
< div class = "note" > ${r.foot}< / div >
< div class = "links" > ${links}< / div >
< / div > `;
el.appendChild(d);
});
}
/* ---------- regions + cards ---------- */
function avgFor(id){
let sum=0,n=0,per={};
for(const v of ['till','lea','astrid']){
const s=STATE.votes[v]&&STATE.votes[v][id];
if(s){sum+=s;n++;per[v]=s;}
}
return {avg:n?sum/n:0,n,per};
}
function cardHTML(o){
const imgs=o.imgs.map((s,i)=>`< img src = "${s}" class = "${i===0?'on':''}" loading = "lazy" alt = "${o.name}" onerror = "this.remove()" > `).join('');
const dots=o.imgs.length>1?`< div class = "dots" > ${o.imgs.map((_,i)=>`< span class = "dot ${i===0?'on':''}" > < / span > `).join('')}< / div > `:'';
const nav=o.imgs.length>1?'< div class = "nav l" > < / div > < div class = "nav r" > < / div > ':'';
const rate=o.rate?`< span class = "crate" > < b > ${o.rate}< / b > ${o.rlabel}${o.bew?(' · '+o.bew+' Bew.'):''}< / span > `:(o.bew?`< span class = "crate" > ${o.bew} Bewertungen< / span > `:'');
const linkLabel=o.region==='kreuzfahrt'?'Ahoi-Schiff →':'Auf Check24 →';
const link2=o.url2?`< a class = "btn ghost" href = "${o.url2}" target = "_blank" rel = "noopener" style = "font-size:11.5px;padding:7px 10px" > AIDA.de< / a > `:'';
return `< div class = "card ${o.fav?'fav':''}" id = "${o.id}" >
< div class = "ph" > ${imgs}< span class = "rtag" style = "background:${REG[o.region].mk}" > ${REG[o.region].name}< / span > ${o.fav?'< span class = "favtag" > ★ Empfehlung< / span > ':''}${nav}${dots}< / div >
< div class = "bd" >
< h3 > ${o.name}< / h3 >
< div class = "loc" > 📍 ${o.loc} · ${'★'.repeat(o.stars)||'🚢'}< / div >
< div class = "stars-row" > ${rate}< / div >
< div class = "chips" > ${o.chips.map(c=>`< span class = "chip" > ${c}< / span > `).join('')}< / div >
< div class = "why" > ${o.why.map(w=>`< div > < span class = "${w[0]}" > ${w[0]==='ok'?'✓':'✕'}< / span > ${w[1]}< / div > `).join('')}< / div >
< div class = "foot" > < span class = "price" > ${o.price.startsWith('ab')||o.price.startsWith('~')?o.price:('< span style = \"color:var(--mut)\" > '+o.price+'< / span > ')}< / span >
< span style = "display:flex;gap:6px" > ${link2}< a class = "btn" href = "${o.url}" target = "_blank" rel = "noopener" > ${linkLabel}< / a > < / span > < / div >
< div class = "vote" data-opt = "${o.id}" > < / div >
< / div >
< / div > `;
}
function renderRegions(){
const host=document.getElementById('regions');host.innerHTML='';
['kroatien','kanaren','madeira','kreuzfahrt'].forEach(rk=>{
const items=OPTIONS.filter(o=>o.region===rk);
if(!items.length)return;
const sec=document.createElement('section');
sec.innerHTML=`< div class = "region-h" > < span class = "flag" > ${REG[rk].flag}< / span > < h2 > ${REG[rk].name}< / h2 > < span class = "cnt" > ${items.length} ${items.length>1?'Optionen':'Option'}< / span > < / div >
< div class = "grid" > ${items.map(cardHTML).join('')}< / div > `;
host.appendChild(sec);
});
// carousels
document.querySelectorAll('.ph').forEach(ph=>{
const ims=ph.querySelectorAll('img');if(ims.length< 2 ) return ;
const dts = ph.querySelectorAll('.dot');let i = 0;
const go = d= > {ims[i].classList.remove('on');dts[i]&&dts[i].classList.remove('on');
i=(i+d+ims.length)%ims.length;ims[i].classList.add('on');dts[i]&&dts[i].classList.add('on'); };
const l=ph.querySelector('.nav.l'),r=ph.querySelector('.nav.r');
if(l)l.onclick=()=>go(-1);if(r)r.onclick=()=>go(1);
});
renderVotes();
}
/* ---------- voting widgets ---------- */
function renderVotes(){
document.querySelectorAll('.vote').forEach(box=>{
const id=box.dataset.opt;const {avg,n,per}=avgFor(id);
const mine=me_&&STATE.votes[me_]&&STATE.votes[me_][id]||0;
let stars='';
for(let s=1;s< =5;s++) stars+=`< span class = "s ${s<=mine?'fill':''}" data-s = "${s}" > ★< / span > `;
const minis=['till','lea','astrid'].map(v=>{
const sv=per[v];
return `< span class = "vm ${v} ${sv?'':'dim'}" title = "${VOTER_LABEL[v]}${sv?': '+sv+'★':' – noch nicht'}" > ${VOTER_LABEL[v][0]}${sv?`< span class = "n" > ${sv}< / span > `:''}< / span > `;
}).join('');
box.innerHTML=`< div class = "lead" > ${me_?('Deine Bewertung als < b > '+VOTER_LABEL[me_]+'< / b > :'):'Wähle oben, wer du bist, dann bewerten:'}< / div >
< div class = "starpick" $ { me_ ? ' ' : ' style = "opacity:.4;pointer-events:none" ' } > ${stars}< / div >
< div class = "voteinfo" > < span class = "avg" > Schnitt: < b > ${avg?avg.toFixed(1):'– '}< / b > ${n?` < span style = "color:var(--mut)" > (${n}/3)< / span > `:''}< / span > < span class = "voters-mini" > ${minis}< / span > < / div > `;
box.querySelectorAll('.starpick .s').forEach(st=>{
st.onclick=()=>{const val=+st.dataset.s;const cur=mine;sendVote(id, val===cur?0:val);};
});
});
renderRanking();
}
/* ---------- ranking ---------- */
function renderRanking(){
const arr=OPTIONS.map(o=>({o,...avgFor(o.id)})).filter(x=>x.n>0).sort((a,b)=>b.avg-a.avg||b.n-a.n);
const el=document.getElementById('ranklist');
if(!arr.length){el.innerHTML='< div class = "note" style = "padding:6px 2px" > Noch keine Bewertungen – sobald Till, Lea oder Astrid Sterne vergeben, erscheint hier das Ranking.< / div > ';return;}
el.innerHTML=arr.map((x,idx)=>`< div class = "rrow ${idx===0?'top1':''}" >
< div class = "pos" > ${idx===0?'🥇':idx===1?'🥈':idx===2?'🥉':(idx+1)}< / div >
< div class = "nm" > ${x.o.name.replace(' · Mediterrane Schätze mit Korsika','')}< small > ${x.o.loc}< / small > < / div >
< div class = "bar" > < i style = "width:${(x.avg/5*100).toFixed(0)}%" > < / i > < / div >
< div class = "sc" > ${x.avg.toFixed(1)}< small > ★ · ${x.n}/3< / small > < / div >
< / div > `).join('');
}
/* ---------- server sync ---------- */
async function loadState(){
try{const r=await fetch('/api/state');STATE=await r.json();}catch(e){STATE={votes:{}};}
renderVotes();
}
async function sendVote(option,stars){
if(!me_)return;
// optimistic
STATE.votes[me_]=STATE.votes[me_]||{};
if(stars===0)delete STATE.votes[me_][option];else STATE.votes[me_][option]=stars;
renderVotes();flashSaved();
try{const r=await fetch('/api/vote',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({voter:me_,option,stars})});STATE=await r.json();renderVotes();}catch(e){}
}
function flashSaved(){const s=document.getElementById('saved');s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1400);}
document.getElementById('resetBtn').onclick=async()=>{
if(!confirm('Wirklich ALLE Bewertungen von Till, Lea und Astrid zurücksetzen?'))return;
try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved();}catch(e){}
};
function renderAll(){renderWho();renderRecos();renderRegions();}
/* ---------- map ---------- */
function initMap(){
const map=L.map('map',{scrollWheelZoom:false}).setView([41,6],5);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{attribution:'© OpenStreetMap, © CARTO',maxZoom:19}).addTo(map);
const pts=[];
// cruise route line
L.polyline(ROUTE.map(r=>r.c),{color:'#2f7fd6',weight:3,opacity:.75,dashArray:'6 6'}).addTo(map);
ROUTE.slice(0,5).forEach(r=>{
L.circleMarker(r.c,{radius:4,color:'#2f7fd6',fillColor:'#2f7fd6',fillOpacity:.9,weight:1})
.addTo(map).bindTooltip(r.n,{direction:'top'});
pts.push(r.c);
});
OPTIONS.forEach(o=>{
const col=REG[o.region].mk;
const icon=L.divIcon({className:'',html:`< div style = "width:${o.fav?20:15}px;height:${o.fav?20:15}px;border-radius:50%;background:${col};border:3px solid ${o.fav?'#f6b73c':'#fff'};box-shadow:0 1px 4px rgba(0,0,0,.4)" > < / div > `,iconSize:[20,20],iconAnchor:[10,10]});
const m=L.marker(o.geo,{icon}).addTo(map);
const lbl=o.region==='kreuzfahrt'?'Zur Route →':'Zum Angebot →';
m.bindPopup(`< div style = "min-width:170px" > < b > ${o.name}< / b > < br > ${o.loc}< br > ${o.rate?('⭐ '+o.rate+' '+o.rlabel+'< br > '):''}< a href = "${o.url}" target = "_blank" > ${lbl}< / a > < / div > `);
pts.push(o.geo);
});
map.fitBounds(pts,{padding:[35,35],maxZoom:6});
setTimeout(()=>map.invalidateSize(),300);
}
renderAll();
initMap();
loadState();
setInterval(loadState, 12000); // live-ish sync between the three voters