code/ cruisebuddy /index.html
492 lines · 40.6 KB · xml Raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0b0b12">
<title>CruiseBuddy</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
:root {
  --bg:#0b0b12;--bg-card:#131320;--bg-card2:#1a1a2e;--bg-input:#181830;
  --bg-nav:rgba(11,11,18,.92);
  --accent:#3b82f6;--accent-glow:rgba(59,130,246,.3);--accent-soft:rgba(59,130,246,.12);
  --green:#22c55e;--green-glow:rgba(34,197,94,.25);
  --orange:#f59e0b;--red:#ef4444;--red-glow:rgba(239,68,68,.25);
  --text:#eeeef5;--text2:#8888a0;--text3:#55556a;
  --border:rgba(255,255,255,.06);--border2:rgba(255,255,255,.1);
  --r:14px;--rs:10px;
  --sat:env(safe-area-inset-top,20px);--sab:env(safe-area-inset-bottom,20px);
  --nav-h:64px;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{font-family:'Outfit',-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100dvh;overflow:hidden;-webkit-font-smoothing:antialiased;position:relative}
.screen{display:none;position:absolute;inset:0;flex-direction:column;overflow:hidden;z-index:1}
.screen.active{display:flex}
input[type="text"],input[type="number"]{width:100%;background:var(--bg-input);border:1.5px solid var(--border);border-radius:var(--rs);padding:15px 16px;font-family:'Outfit',sans-serif;font-size:16px;font-weight:500;color:var(--text);outline:none;transition:border-color .2s,box-shadow .2s}
input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
input::placeholder{color:var(--text3);font-weight:400}
.field{margin-bottom:20px}
.field label{display:block;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1.5px;color:var(--text3);margin-bottom:8px}
.btn{display:flex;align-items:center;justify-content:center;gap:9px;padding:15px 22px;border-radius:var(--r);font-family:'Outfit',sans-serif;font-size:15px;font-weight:600;border:none;cursor:pointer;transition:all .15s;width:100%}
.btn:active{transform:scale(.97)}.btn svg{width:18px;height:18px;flex-shrink:0}
.btn-primary{background:var(--accent);color:#fff;box-shadow:0 4px 20px var(--accent-glow)}
.btn-green{background:var(--green);color:#fff;box-shadow:0 4px 20px var(--green-glow)}
.btn-outline{background:transparent;color:var(--text);border:1.5px solid var(--border)}
.btn-danger{background:rgba(239,68,68,.1);color:var(--red);border:1.5px solid rgba(239,68,68,.2)}
.btn-sm{padding:11px 16px;font-size:13px;border-radius:var(--rs)}
.btn-icon{width:44px;height:44px;padding:0;border-radius:50%;flex-shrink:0;min-width:44px}
.or-div{display:flex;align-items:center;gap:14px;color:var(--text3);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:2px;margin:4px 0}
.or-div::before,.or-div::after{content:'';flex:1;height:1px;background:var(--border)}

#home{align-items:center;justify-content:center;text-align:center;padding:32px 24px;z-index:5}
#home::before{content:'';position:absolute;top:-30%;left:-10%;width:120%;height:70%;background:radial-gradient(ellipse,var(--accent-glow) 0%,transparent 70%);opacity:.35;filter:blur(80px);pointer-events:none}
.logo{position:relative;margin-bottom:10px}.logo svg{width:72px;height:72px;filter:drop-shadow(0 0 25px var(--accent-glow))}
.app-title{font-size:36px;font-weight:800;letter-spacing:-1px;background:linear-gradient(135deg,#fff 30%,var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:4px;position:relative}
.app-sub{font-size:14px;color:var(--text2);margin-bottom:48px;letter-spacing:.5px}
.home-form{width:100%;max-width:340px;position:relative}

#createScreen{align-items:center;justify-content:center;text-align:center;padding:32px 24px;z-index:5}
.screen-title{font-size:24px;font-weight:700;margin-bottom:6px}
.screen-sub{font-size:13px;color:var(--text2);margin-bottom:32px}
.create-form{width:100%;max-width:340px}
.slider-wrap{display:flex;align-items:center;gap:14px}
.slider-wrap input[type="range"]{flex:1;-webkit-appearance:none;appearance:none;height:6px;background:var(--bg-card2);border-radius:3px;outline:none;border:none;padding:0}
.slider-wrap input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:22px;border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 10px var(--accent-glow)}
.slider-val{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:500;min-width:32px;text-align:center;color:var(--accent)}

#joinScreen{align-items:center;justify-content:center;text-align:center;padding:32px 24px;z-index:5;position:relative}
.code-wrap{display:flex;gap:8px;justify-content:center;margin-bottom:28px}
.code-digit{width:50px;height:62px;background:var(--bg-input);border:1.5px solid var(--border);border-radius:var(--rs);font-family:'JetBrains Mono',monospace;font-size:24px;font-weight:500;color:var(--text);text-align:center;outline:none;transition:border-color .2s,box-shadow .2s;text-transform:uppercase}
.code-digit:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}

.back{position:absolute;top:calc(var(--sat) + 10px);left:14px;background:var(--bg-card);border:1px solid var(--border);border-radius:50%;width:42px;height:42px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text);z-index:10}
.back svg{width:20px;height:20px}

#bottomNav{display:none;position:fixed;bottom:0;left:0;right:0;height:calc(var(--nav-h) + var(--sab));padding-bottom:var(--sab);background:var(--bg-nav);backdrop-filter:blur(20px);border-top:1px solid var(--border);z-index:50;flex-direction:row;align-items:stretch;justify-content:space-around}
#bottomNav.visible{display:flex}
.nav-tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;cursor:pointer;color:var(--text3);transition:color .2s;position:relative;border:none;background:none;font-family:'Outfit',sans-serif}
.nav-tab svg{width:22px;height:22px}.nav-tab span{font-size:10px;font-weight:600;letter-spacing:.5px}
.nav-tab.active{color:var(--accent)}
.nav-tab.active::before{content:'';position:absolute;top:-1px;left:25%;right:25%;height:2px;background:var(--accent);border-radius:0 0 2px 2px}
.chat-badge{position:absolute;top:8px;right:calc(50% - 18px);width:8px;height:8px;background:var(--red);border-radius:50%;display:none}
.chat-badge.visible{display:block}

#lobby{padding:calc(var(--sat) + 14px) 18px calc(var(--nav-h) + var(--sab) + 14px);overflow-y:auto}
.lobby-head{text-align:center;padding-top:36px;margin-bottom:24px}
.lobby-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1.5px;color:var(--text3);margin-bottom:4px}
.lobby-room{font-size:20px;font-weight:700;margin-bottom:14px}
.code-box{display:inline-flex;align-items:center;gap:10px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--rs);padding:10px 18px;cursor:pointer}
.code-box .c{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:500;letter-spacing:4px;color:var(--accent)}
.code-box svg{width:16px;height:16px;color:var(--text3)}
.sect-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1.5px;color:var(--text3);margin:22px 0 10px}
.p-list{display:flex;flex-direction:column;gap:6px}
.p-card{display:flex;align-items:center;gap:12px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--rs);padding:12px 14px}
.p-avatar{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px;flex-shrink:0}
.av-leader{background:var(--accent-soft);color:var(--accent)}.av-member{background:rgba(34,197,94,.1);color:var(--green)}
.p-info{flex:1;min-width:0}.p-name{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-role{font-size:11px;color:var(--text2)}
.p-kick{background:none;border:1px solid rgba(239,68,68,.2);color:var(--red);font-size:11px;font-weight:600;padding:5px 10px;border-radius:20px;cursor:pointer;font-family:'Outfit',sans-serif}
.p-kick:active{background:rgba(239,68,68,.1)}
.lobby-status{text-align:center;color:var(--text2);font-size:13px;padding:14px 0}
.lobby-actions{display:flex;flex-direction:column;gap:8px;margin-top:16px}

#driveView{z-index:2}
#map{position:absolute;inset:0;z-index:1}
.leaflet-control-zoom{display:none!important}
.leaflet-control-attribution{background:rgba(11,11,18,.7)!important;color:var(--text3)!important;font-size:9px!important;backdrop-filter:blur(8px)}
.leaflet-control-attribution a{color:var(--text2)!important}
.hud-top{position:absolute;top:0;left:0;right:0;padding:calc(var(--sat) + 6px) 14px 14px;background:linear-gradient(to bottom,rgba(11,11,18,.92) 0%,rgba(11,11,18,.5) 70%,transparent);z-index:10;display:flex;align-items:center;justify-content:space-between}
.hud-room{display:flex;align-items:center;gap:9px}
.live-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;animation:blink 1.5s ease-in-out infinite}
.live-dot.active{background:var(--green);box-shadow:0 0 8px var(--green-glow)}
.live-dot.paused{background:var(--orange);box-shadow:0 0 8px rgba(245,158,11,.3)}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
.hud-name{font-size:14px;font-weight:600}.hud-count{font-size:11px;color:var(--text2)}
.hud-btns{display:flex;gap:8px}

.speed-card{position:absolute;bottom:calc(var(--nav-h) + var(--sab) + 14px);left:14px;background:rgba(11,11,18,.88);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--r);padding:12px 18px;z-index:10}
.spd-row{display:flex;align-items:baseline;gap:3px}
.spd-num{font-family:'JetBrains Mono',monospace;font-size:44px;font-weight:500;line-height:1}
.spd-unit{font-size:13px;color:var(--text2);font-weight:500}
.spd-meta{display:flex;gap:14px;margin-top:6px}
.spd-item{display:flex;flex-direction:column;gap:1px}
.spd-label{font-size:8px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--text3)}
.spd-val{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500}

.leader-ctrls{position:absolute;bottom:calc(var(--nav-h) + var(--sab) + 14px);right:14px;z-index:10;display:flex;flex-direction:column;gap:8px}
.ctrl-btn{width:50px;height:50px;border-radius:50%;background:rgba(11,11,18,.88);backdrop-filter:blur(16px);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text);transition:all .15s}
.ctrl-btn:active{transform:scale(.9)}.ctrl-btn svg{width:22px;height:22px}
.ctrl-btn.warn{border-color:rgba(245,158,11,.3);color:var(--orange)}
.ctrl-btn.stop{border-color:rgba(239,68,68,.3);color:var(--red)}

.pause-banner{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:15;background:rgba(11,11,18,.92);backdrop-filter:blur(16px);border:1px solid rgba(245,158,11,.3);border-radius:var(--r);padding:20px 28px;text-align:center}
.pause-banner.visible{display:block}
.pause-banner .icon{font-size:28px;margin-bottom:6px}.pause-banner .msg{font-size:15px;font-weight:600;color:var(--orange)}.pause-banner .sub{font-size:12px;color:var(--text2);margin-top:4px}

#chatView{padding:calc(var(--sat) + 14px) 0 calc(var(--nav-h) + var(--sab));flex-direction:column;z-index:3}
.chat-header{padding:0 18px 12px;border-bottom:1px solid var(--border);flex-shrink:0}
.chat-header h2{font-size:20px;font-weight:700}.chat-header p{font-size:12px;color:var(--text2);margin-top:2px}
.chat-messages{flex:1;overflow-y:auto;padding:12px 18px;display:flex;flex-direction:column;gap:6px}
.chat-msg{max-width:85%;padding:10px 14px;border-radius:var(--rs);font-size:14px;line-height:1.4}
.chat-msg.self{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}
.chat-msg.other{align-self:flex-start;background:var(--bg-card);border:1px solid var(--border);border-bottom-left-radius:4px}
.chat-msg .sender{font-size:11px;font-weight:600;margin-bottom:2px;opacity:.7}
.chat-msg .sender.leader{color:var(--accent)}
.chat-msg.self .sender{display:none}
.chat-msg .body{word-break:break-word}
.chat-msg .ts{font-size:10px;opacity:.4;margin-top:3px;text-align:right}
.chat-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:14px}
.chat-input-bar{display:flex;gap:8px;padding:10px 14px;border-top:1px solid var(--border);flex-shrink:0;background:var(--bg)}
.chat-input-bar input{flex:1;padding:12px 14px;font-size:14px}
.chat-send{width:44px;height:44px;border-radius:50%;background:var(--accent);color:#fff;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0}
.chat-send svg{width:18px;height:18px}

#infoView{padding:calc(var(--sat) + 14px) 18px calc(var(--nav-h) + var(--sab) + 14px);overflow-y:auto;z-index:3}
.info-header{margin-bottom:24px}.info-header h2{font-size:24px;font-weight:700;margin-bottom:4px}.info-header p{font-size:13px;color:var(--text2)}
.faq-item{margin-bottom:18px}.faq-q{font-size:14px;font-weight:600;margin-bottom:4px}.faq-a{font-size:13px;color:var(--text2);line-height:1.5}
.info-footer{margin-top:32px;padding-top:18px;border-top:1px solid var(--border);text-align:center}.info-footer p{font-size:12px;color:var(--text3)}

.toast{position:fixed;top:calc(var(--sat) + 14px);left:50%;transform:translateX(-50%) translateY(-80px);background:var(--bg-card);border:1px solid var(--border);border-radius:var(--rs);padding:11px 20px;font-size:13px;font-weight:500;z-index:200;backdrop-filter:blur(16px);transition:transform .3s cubic-bezier(.4,0,.2,1);white-space:nowrap;max-width:90vw;overflow:hidden;text-overflow:ellipsis}
.toast.show{transform:translateX(-50%) translateY(0)}.toast.error{border-color:rgba(239,68,68,.3);color:var(--red)}
.conn-bar{display:none;position:fixed;top:0;left:0;right:0;padding:calc(var(--sat) + 4px) 14px 6px;background:rgba(239,68,68,.9);color:#fff;font-size:12px;font-weight:600;text-align:center;z-index:300}
.conn-bar.visible{display:block}

@keyframes fadeUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
.anim{animation:fadeUp .4s ease-out both}.d1{animation-delay:.08s}.d2{animation-delay:.16s}.d3{animation-delay:.24s}.d4{animation-delay:.32s}
</style>
</head>
<body>

<div id="home" class="screen active">
  <div class="logo anim"><svg viewBox="0 0 80 80" fill="none"><circle cx="40" cy="40" r="36" stroke="#3b82f6" stroke-width="2.5" opacity=".3"/><circle cx="40" cy="40" r="24" stroke="#3b82f6" stroke-width="2" opacity=".5"/><circle cx="40" cy="40" r="8" fill="#3b82f6"/><path d="M40 4v12" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" opacity=".6"/><path d="M40 64v12" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" opacity=".6"/><path d="M4 40h12" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" opacity=".6"/><path d="M64 40h12" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" opacity=".6"/></svg></div>
  <h1 class="app-title anim d1">CruiseBuddy</h1>
  <p class="app-sub anim d2">Group drives, synced.</p>
  <div class="home-form">
    <div class="field anim d3"><label>Your Name</label><input type="text" id="nameInput" placeholder="Enter your name" maxlength="20" autocomplete="off"></div>
    <div class="anim d4" style="display:flex;flex-direction:column;gap:10px">
      <button class="btn btn-primary" onclick="goCreate()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>Create a Drive</button>
      <div class="or-div">or</div>
      <button class="btn btn-outline" onclick="goJoin()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4M10 17l5-5-5-5M15 12H3"/></svg>Join a Drive</button>
    </div>
  </div>
</div>

<div id="createScreen" class="screen">
  <button class="back" onclick="goHome()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M15 18l-6-6 6-6"/></svg></button>
  <h2 class="screen-title">Create a Drive</h2>
  <p class="screen-sub">Set up your group drive</p>
  <div class="create-form">
    <div class="field"><label>Max Participants</label><div class="slider-wrap"><input type="range" id="maxSlider" min="2" max="10" value="5" oninput="document.getElementById('maxVal').textContent=this.value"><span class="slider-val" id="maxVal">5</span></div></div>
    <button class="btn btn-green" onclick="createDrive()" style="margin-top:12px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Create Room</button>
  </div>
</div>

<div id="joinScreen" class="screen">
  <button class="back" onclick="goHome()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M15 18l-6-6 6-6"/></svg></button>
  <h2 class="screen-title">Join a Drive</h2>
  <p class="screen-sub">Enter the 5-digit room code</p>
  <div class="code-wrap" id="codeWrap"></div>
  <div style="width:100%;max-width:260px"><button class="btn btn-primary" onclick="joinDrive()">Join Drive</button></div>
</div>

<div id="lobby" class="screen">
  <button class="back" onclick="leaveRoom()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M15 18l-6-6 6-6"/></svg></button>
  <div class="lobby-head">
    <div class="lobby-label">Drive Room</div>
    <div class="lobby-room" id="lobbyRoom">Drive</div>
    <div class="code-box" onclick="copyCode()"><span class="c" id="lobbyCode">—</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></div>
  </div>
  <div class="sect-label" id="pCountLabel">Participants (0/5)</div>
  <div class="p-list" id="pList"></div>
  <div class="lobby-status" id="lobbyStatus"></div>
  <div class="lobby-actions" id="lobbyActions"></div>
</div>

<div id="driveView" class="screen">
  <div id="map"></div>
  <div class="hud-top">
    <div class="hud-room"><div class="live-dot active" id="liveDot"></div><div><div class="hud-name" id="hudName">Drive</div><div class="hud-count" id="hudCount">0 drivers</div></div></div>
    <div class="hud-btns"><button class="btn-icon btn-outline" onclick="centerMap()" style="background:rgba(11,11,18,.85);backdrop-filter:blur(12px)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg></button></div>
  </div>
  <div class="speed-card" id="speedCard">
    <div class="spd-row"><span class="spd-num" id="spdNum">0</span><span class="spd-unit">mph</span></div>
    <div class="spd-meta">
      <div class="spd-item" id="distItem"><span class="spd-label">To Leader</span><span class="spd-val" id="distVal">—</span></div>
      <div class="spd-item"><span class="spd-label">Duration</span><span class="spd-val" id="durVal">0:00</span></div>
      <div class="spd-item"><span class="spd-label">Heading</span><span class="spd-val" id="hdgVal">—</span></div>
    </div>
  </div>
  <div class="leader-ctrls" id="leaderCtrls" style="display:none">
    <button class="ctrl-btn warn" id="pauseBtn" onclick="togglePause()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg></button>
    <button class="ctrl-btn stop" onclick="endDrive()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/></svg></button>
  </div>
  <div class="pause-banner" id="pauseBanner"><div class="icon">⏸</div><div class="msg">Drive Paused</div><div class="sub">The group leader paused the drive</div></div>
</div>

<div id="chatView" class="screen">
  <div class="chat-header"><h2>Group Chat</h2><p id="chatSub">Send messages to your group</p></div>
  <div class="chat-messages" id="chatMessages"><div class="chat-empty" id="chatEmpty">No messages yet. Say hi!</div></div>
  <div class="chat-input-bar"><input type="text" id="chatInput" placeholder="Type a message..." maxlength="500" autocomplete="off"><button class="chat-send" onclick="sendChat()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button></div>
</div>

<div id="infoView" class="screen">
  <div class="info-header"><h2>CruiseBuddy</h2><p>Group drive tracking — stay together on the road.</p></div>
  <div class="faq-item"><div class="faq-q">How does it work?</div><div class="faq-a">A leader creates a drive room and shares the 5-digit code. Others join with the code. Once the leader starts the drive, everyone's GPS position is tracked in real-time. Followers see a line pointing to the leader so they never lose the group.</div></div>
  <div class="faq-item"><div class="faq-q">Do I need to install anything?</div><div class="faq-a">Nope! CruiseBuddy runs entirely in your browser. For the best experience on mobile, add it to your home screen.</div></div>
  <div class="faq-item"><div class="faq-q">What about background tracking?</div><div class="faq-a">GPS tracking pauses when the browser is in the background on most phones. CruiseBuddy uses screen wake lock to keep your phone awake during drives. Keep it in the foreground for continuous tracking.</div></div>
  <div class="faq-item"><div class="faq-q">Is my location data stored?</div><div class="faq-a">No. All position data is relayed in real-time and never saved to disk. Once the drive ends, all data is gone. No accounts, no tracking history.</div></div>
  <div class="faq-item"><div class="faq-q">What's the max group size?</div><div class="faq-a">The leader sets the group size between 2 and 10 when creating the drive. Default is 5.</div></div>
  <div class="faq-item"><div class="faq-q">What if I lose connection?</div><div class="faq-a">CruiseBuddy auto-reconnects when your connection comes back. Your position picks up right where you left off.</div></div>
  <div class="info-footer"><p>CruiseBuddy v1.0</p><p style="margin-top:4px">Built for group drives.</p></div>
</div>

<div id="bottomNav">
  <button class="nav-tab active" data-tab="drive" onclick="switchTab('drive')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg><span>Drive</span></button>
  <button class="nav-tab" data-tab="chat" onclick="switchTab('chat')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg><span>Chat</span><div class="chat-badge" id="chatBadge"></div></button>
  <button class="nav-tab" data-tab="info" onclick="switchTab('info')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg><span>Info</span></button>
</div>

<div class="toast" id="toast"></div>
<div class="conn-bar" id="connBar">Reconnecting...</div>

<script>
const S={name:'',role:null,roomCode:null,status:null,participants:[],maxP:5,leaderName:null,myLat:null,myLng:null,mySpeed:0,myHeading:0,leaderLat:null,leaderLng:null,followers:{},driveStart:null,chatMessages:[],activeTab:'drive',unreadChat:false,map:null,myMarker:null,leaderMarker:null,followerMarkers:{},trackingLine:null,centered:true,watchId:null,ws:null,wsReady:false,reconnectTimer:null,wakeLock:null,posInterval:null};

/* ═══ WEBSOCKET ═══ */
function getWsUrl(){const p=location.protocol==='https:'?'wss:':'ws:';return`${p}//${location.host}/ws`}

function wsConnect(){
  if(S.ws&&S.ws.readyState<2)return;
  S.ws=new WebSocket(getWsUrl());
  S.ws.onopen=()=>{S.wsReady=true;document.getElementById('connBar').classList.remove('visible');console.log('[WS] Connected')};
  S.ws.onclose=()=>{S.wsReady=false;if(S.roomCode)document.getElementById('connBar').classList.add('visible');clearTimeout(S.reconnectTimer);S.reconnectTimer=setTimeout(wsConnect,2000)};
  S.ws.onerror=()=>S.ws.close();
  S.ws.onmessage=e=>{try{handleMsg(JSON.parse(e.data))}catch(err){console.error(err)}};
}

function wsSend(d){if(S.ws&&S.wsReady)S.ws.send(JSON.stringify(d))}

function handleMsg(m){
  const h={room_created:onRoomCreated,joined:onJoined,participant_update:onPUpdate,drive_started:onDriveStarted,drive_paused:onDrivePaused,drive_resumed:onDriveResumed,drive_ended:onDriveEnded,you_kicked:onKicked,leader_position:onLeaderPos,follower_position:onFollowerPos,chat_msg:onChatMsg,room_closed:onRoomClosed,error:onErr};
  if(h[m.type])h[m.type](m);
}

function onRoomCreated(m){S.role='leader';S.roomCode=m.code;S.maxP=m.max_participants;S.participants=m.participants;S.leaderName=m.name;S.status='lobby';S.chatMessages=[];renderLobby();showNav();showScreen('lobby');toast('Room created!')}
function onJoined(m){S.role='member';S.roomCode=m.code;S.status=m.status;S.leaderName=m.leader_name;S.maxP=m.max_participants;S.participants=m.participants;S.chatMessages=(m.chat||[]).map(c=>({...c}));renderLobby();renderChat();showNav();if(S.status==='active'||S.status==='paused')enterDrive();else showScreen('lobby');toast('Joined the drive!')}
function onPUpdate(m){S.participants=m.participants;if(m.event==='joined')toast(m.name+' joined');else if(m.event==='left')toast(m.name+' left');else if(m.event==='kicked')toast(m.name+' was removed');renderLobby();updHudCount()}
function onDriveStarted(){S.status='active';S.driveStart=Date.now();enterDrive();toast('Drive started!')}
function onDrivePaused(){S.status='paused';document.getElementById('pauseBanner').classList.add('visible');document.getElementById('liveDot').className='live-dot paused';updLine();updPauseBtn();if(S.role!=='leader')toast('Leader paused the drive')}
function onDriveResumed(){S.status='active';document.getElementById('pauseBanner').classList.remove('visible');document.getElementById('liveDot').className='live-dot active';updLine();updPauseBtn();if(S.role!=='leader')toast('Drive resumed!')}
function onDriveEnded(m){S.status='lobby';S.driveStart=null;S.followers={};S.leaderLat=null;S.leaderLng=null;if(m.participants)S.participants=m.participants;stopGeo();relWake();clearInterval(S.posInterval);document.getElementById('pauseBanner').classList.remove('visible');renderLobby();switchTab('drive');showScreen('lobby');toast('Drive ended')}
function onKicked(){cleanup();showScreen('home');hideNav();toast('You were removed from the drive',1)}
function onRoomClosed(m){cleanup();showScreen('home');hideNav();toast(m.reason||'Room closed',1)}
function onLeaderPos(m){S.leaderLat=m.lat;S.leaderLng=m.lng;updLeaderMkr(m.lat,m.lng);updLine();updDist()}
function onFollowerPos(m){S.followers[m.name]={lat:m.lat,lng:m.lng,speed:m.speed,heading:m.heading};updFollowerMkr(m.name,m.lat,m.lng)}
function onChatMsg(m){S.chatMessages.push(m);renderChat();if(S.activeTab!=='chat'){S.unreadChat=true;document.getElementById('chatBadge').classList.add('visible')}}
function onErr(m){toast(m.msg,1)}

/* ═══ SCREENS ═══ */
function showScreen(id){['lobby','driveView','chatView','infoView','home','createScreen','joinScreen'].forEach(s=>document.getElementById(s).classList.remove('active'));document.getElementById(id).classList.add('active')}
function showNav(){document.getElementById('bottomNav').classList.add('visible')}
function hideNav(){document.getElementById('bottomNav').classList.remove('visible')}

function switchTab(tab){
  S.activeTab=tab;
  document.querySelectorAll('.nav-tab').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
  ['lobby','driveView','chatView','infoView'].forEach(s=>document.getElementById(s).classList.remove('active'));
  if(tab==='drive'){
    if(S.status==='active'||S.status==='paused'){document.getElementById('driveView').classList.add('active');if(S.map)setTimeout(()=>S.map.invalidateSize(),50)}
    else document.getElementById('lobby').classList.add('active');
  }else if(tab==='chat'){document.getElementById('chatView').classList.add('active');S.unreadChat=false;document.getElementById('chatBadge').classList.remove('visible');scrollChat()}
  else if(tab==='info')document.getElementById('infoView').classList.add('active');
}

/* ═══ NAVIGATION ═══ */
function goHome(){showScreen('home')}
function getName(){const n=document.getElementById('nameInput').value.trim();if(!n){toast('Enter your name first',1);return null}S.name=n;return n}
function goCreate(){if(!getName())return;showScreen('createScreen')}
function goJoin(){if(!getName())return;showScreen('joinScreen');buildCode()}

function buildCode(){
  const w=document.getElementById('codeWrap');w.innerHTML='';
  for(let i=0;i<5;i++){const el=document.createElement('input');el.type='text';el.className='code-digit';el.maxLength=1;el.autocomplete='off';el.inputMode='text';w.appendChild(el)}
  const d=[...w.querySelectorAll('.code-digit')];
  d.forEach((el,i)=>{el.addEventListener('input',()=>{el.value=el.value.toUpperCase();if(el.value&&i<4)d[i+1].focus()});el.addEventListener('keydown',e=>{if(e.key==='Backspace'&&!el.value&&i>0)d[i-1].focus()})});
  setTimeout(()=>d[0].focus(),100);
}

function createDrive(){const max=parseInt(document.getElementById('maxSlider').value)||5;wsSend({type:'create_room',name:S.name,max_participants:max})}
function joinDrive(){const d=document.querySelectorAll('.code-digit');let c='';d.forEach(x=>c+=x.value);if(c.length<5)return toast('Enter the full 5-digit code',1);wsSend({type:'join_room',name:S.name,code:c.toUpperCase()})}

/* ═══ LOBBY ═══ */
function renderLobby(){
  document.getElementById('lobbyCode').textContent=S.roomCode||'—';
  document.getElementById('lobbyRoom').textContent=(S.leaderName||'Unknown')+"'s Drive";
  document.getElementById('pCountLabel').textContent=`Participants (${S.participants.length}/${S.maxP})`;
  document.getElementById('chatSub').textContent=S.participants.length+' members in this drive';
  const list=document.getElementById('pList');
  list.innerHTML=S.participants.map(p=>{
    const me=p.name===S.name,ldr=p.role==='leader',canKick=S.role==='leader'&&!ldr;
    return`<div class="p-card"><div class="p-avatar ${ldr?'av-leader':'av-member'}">${p.name[0].toUpperCase()}</div><div class="p-info"><div class="p-name">${esc(p.name)}${me?' (You)':''}</div><div class="p-role">${ldr?'Leader':'Member'}</div></div>${canKick?`<button class="p-kick" onclick="kickUser('${esc(p.name)}')">Remove</button>`:''}</div>`;
  }).join('');
  const act=document.getElementById('lobbyActions'),st=document.getElementById('lobbyStatus');
  if(S.role==='leader'){
    st.textContent=S.participants.length<2?'Waiting for participants to join...':'';
    act.innerHTML=`<button class="btn btn-green" onclick="startDrive()" ${S.participants.length<2?'disabled style="opacity:.5;pointer-events:none"':''}>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Start Drive</button>
      <button class="btn btn-outline btn-sm" onclick="leaveRoom()">Cancel</button>`;
  }else{st.textContent='Waiting for leader to start the drive...';act.innerHTML='<button class="btn btn-outline btn-sm" onclick="leaveRoom()">Leave</button>'}
}

function copyCode(){if(S.roomCode){navigator.clipboard?.writeText(S.roomCode);toast('Code copied!')}}
function kickUser(n){wsSend({type:'kick',target:n})}
function leaveRoom(){wsSend({type:'leave'});cleanup();hideNav();showScreen('home')}

function cleanup(){
  S.role=null;S.roomCode=null;S.status=null;S.participants=[];S.chatMessages=[];S.followers={};
  S.leaderLat=null;S.leaderLng=null;S.driveStart=null;S.unreadChat=false;
  document.getElementById('chatBadge').classList.remove('visible');
  stopGeo();relWake();clearInterval(S.posInterval);cleanMap();
}

/* ═══ DRIVE ═══ */
function startDrive(){wsSend({type:'start_drive'})}

function enterDrive(){
  S.driveStart=S.driveStart||Date.now();switchTab('drive');showScreen('driveView');
  if(S.role==='leader'){document.getElementById('leaderCtrls').style.display='flex';document.getElementById('distItem').style.display='none'}
  else{document.getElementById('leaderCtrls').style.display='none';document.getElementById('distItem').style.display=''}
  if(S.status==='paused'){document.getElementById('pauseBanner').classList.add('visible');document.getElementById('liveDot').className='live-dot paused'}
  else{document.getElementById('pauseBanner').classList.remove('visible');document.getElementById('liveDot').className='live-dot active'}
  updPauseBtn();updHudCount();initMap();startGeo();reqWake();startDur();startPosInterval();
}

function togglePause(){if(S.status==='active')wsSend({type:'pause_drive'});else if(S.status==='paused')wsSend({type:'resume_drive'})}

function updPauseBtn(){
  const b=document.getElementById('pauseBtn');
  if(S.status==='paused'){b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>';b.className='ctrl-btn';b.style.borderColor='rgba(34,197,94,.3)';b.style.color='var(--green)'}
  else{b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';b.className='ctrl-btn warn';b.style.borderColor='';b.style.color=''}
}

function endDrive(){if(S.role==='leader')wsSend({type:'end_drive'});else leaveRoom()}
function updHudCount(){document.getElementById('hudCount').textContent=S.participants.length+` driver${S.participants.length!==1?'s':''}`;document.getElementById('hudName').textContent=(S.leaderName||'Unknown')+"'s Drive"}

function startPosInterval(){
  clearInterval(S.posInterval);
  S.posInterval=setInterval(()=>{
    if(S.status==='active'&&S.myLat!=null){
      wsSend({type:'position',lat:S.myLat,lng:S.myLng,speed:Math.round(S.mySpeed),heading:Math.round(S.myHeading)});
    }
  },2000);
}

/* ═══ MAP ═══ */
function initMap(){
  if(S.map){setTimeout(()=>S.map.invalidateSize(),100);return}
  S.map=L.map('map',{zoomControl:false,attributionControl:true,center:[48.75,-122.47],zoom:15});
  L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'&copy; <a href="https://carto.com/">CARTO</a> &copy; <a href="https://osm.org/copyright">OSM</a>',subdomains:'abcd',maxZoom:20}).addTo(S.map);
  S.map.on('dragstart',()=>{S.centered=false});
}

function cleanMap(){if(S.map){S.map.remove();S.map=null;S.myMarker=null;S.leaderMarker=null;S.followerMarkers={};S.trackingLine=null}}

function mkIcon(color,label){
  const g=color==='#3b82f6'?'rgba(59,130,246,.4)':'rgba(34,197,94,.4)';
  return L.divIcon({className:'',iconSize:[0,0],iconAnchor:[22,22],html:`<div style="position:relative;width:44px;height:44px"><div style="width:44px;height:44px;border-radius:50%;background:${color};box-shadow:0 0 20px ${g},0 0 50px ${g};display:flex;align-items:center;justify-content:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polygon points="12 2 20 21 4 21 12 2"/></svg></div><div style="position:absolute;top:48px;left:50%;transform:translateX(-50%);font-family:Outfit,sans-serif;font-size:11px;font-weight:600;white-space:nowrap;background:rgba(11,11,18,.9);color:#eee;padding:3px 8px;border-radius:6px;border:1px solid rgba(255,255,255,.06)">${label}</div></div>`});
}

function updMyMkr(lat,lng){
  if(!S.map)return;
  const label=S.role==='leader'?'You (Leader)':'You',color=S.role==='leader'?'#3b82f6':'#22c55e';
  if(!S.myMarker){S.myMarker=L.marker([lat,lng],{icon:mkIcon(color,label),zIndexOffset:1000}).addTo(S.map);S.map.setView([lat,lng],16)}
  else S.myMarker.setLatLng([lat,lng]);
  if(S.centered)S.map.panTo([lat,lng],{animate:true,duration:.5});
}

function updLeaderMkr(lat,lng){
  if(!S.map||S.role==='leader'||!lat||!lng)return;
  if(!S.leaderMarker)S.leaderMarker=L.marker([lat,lng],{icon:mkIcon('#3b82f6',S.leaderName||'Leader'),zIndexOffset:900}).addTo(S.map);
  else S.leaderMarker.setLatLng([lat,lng]);
}

function updFollowerMkr(name,lat,lng){
  if(!S.map||S.role!=='leader'||!lat||!lng)return;
  if(!S.followerMarkers[name])S.followerMarkers[name]=L.marker([lat,lng],{icon:mkIcon('#22c55e',name),zIndexOffset:800}).addTo(S.map);
  else S.followerMarkers[name].setLatLng([lat,lng]);
}

function updLine(){
  if(!S.map||S.role==='leader'||!S.myLat||!S.leaderLat)return;
  const color=S.status==='paused'?'#ef4444':'#3b82f6',pts=[[S.myLat,S.myLng],[S.leaderLat,S.leaderLng]];
  if(!S.trackingLine)S.trackingLine=L.polyline(pts,{color,weight:4,opacity:.7,dashArray:'12 8',lineCap:'round'}).addTo(S.map);
  else{S.trackingLine.setLatLngs(pts);S.trackingLine.setStyle({color})}
}

function updDist(){
  if(!S.myLat||!S.leaderLat)return;
  const d=haver(S.myLat,S.myLng,S.leaderLat,S.leaderLng);
  document.getElementById('distVal').textContent=d<.1?Math.round(d*5280)+' ft':d.toFixed(1)+' mi';
}

function haver(a,b,c,d){const R=3959,dL=(c-a)*Math.PI/180,dN=(d-b)*Math.PI/180,x=Math.sin(dL/2)**2+Math.cos(a*Math.PI/180)*Math.cos(c*Math.PI/180)*Math.sin(dN/2)**2;return R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x))}
function centerMap(){S.centered=true;if(S.myLat&&S.map)S.map.setView([S.myLat,S.myLng],S.map.getZoom(),{animate:true});toast('Centered')}

/* ═══ GEO ═══ */
function startGeo(){
  if(S.watchId)return;
  if(!navigator.geolocation){toast('GPS not available',1);return}
  S.watchId=navigator.geolocation.watchPosition(p=>{
    S.myLat=p.coords.latitude;S.myLng=p.coords.longitude;
    S.mySpeed=p.coords.speed!=null?p.coords.speed*2.237:0;
    S.myHeading=p.coords.heading||0;
    updMyMkr(S.myLat,S.myLng);updLine();updDist();updSpd();
  },err=>{if(err.code===1)toast('Location permission denied',1)},{enableHighAccuracy:true,maximumAge:2000,timeout:10000});
}
function stopGeo(){if(S.watchId){navigator.geolocation.clearWatch(S.watchId);S.watchId=null}}

/* ═══ HUD ═══ */
function updSpd(){
  document.getElementById('spdNum').textContent=Math.max(0,Math.round(S.mySpeed));
  const h=['N','NE','E','SE','S','SW','W','NW'],d=((S.myHeading%360)+360)%360;
  document.getElementById('hdgVal').textContent=h[Math.round(d/45)%8];
}

let durTimer;
function startDur(){clearInterval(durTimer);durTimer=setInterval(()=>{if(!S.driveStart)return;const s=Math.floor((Date.now()-S.driveStart)/1000);document.getElementById('durVal').textContent=Math.floor(s/60)+':'+(s%60).toString().padStart(2,'0')},1000)}

/* ═══ CHAT ═══ */
function renderChat(){
  const c=document.getElementById('chatMessages'),e=document.getElementById('chatEmpty');
  if(!S.chatMessages.length){e.style.display='flex';return}e.style.display='none';
  c.innerHTML=S.chatMessages.map(m=>{
    const me=m.name===S.name,ldr=m.role==='leader',t=new Date(m.time);
    const ts=`${t.getHours()%12||12}:${t.getMinutes().toString().padStart(2,'0')} ${t.getHours()>=12?'PM':'AM'}`;
    return`<div class="chat-msg ${me?'self':'other'}"><div class="sender ${ldr?'leader':''}">${esc(m.name)}${ldr?' ★':''}</div><div class="body">${esc(m.text)}</div><div class="ts">${ts}</div></div>`;
  }).join('');scrollChat();
}
function sendChat(){const i=document.getElementById('chatInput'),t=i.value.trim();if(!t)return;wsSend({type:'chat',text:t});i.value=''}
function scrollChat(){const c=document.getElementById('chatMessages');setTimeout(()=>c.scrollTop=c.scrollHeight,50)}
document.getElementById('chatInput').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat()}});

/* ═══ WAKE LOCK ═══ */
async function reqWake(){if(!('wakeLock' in navigator))return;try{S.wakeLock=await navigator.wakeLock.request('screen')}catch(e){}}
function relWake(){if(S.wakeLock){S.wakeLock.release();S.wakeLock=null}}
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible'&&S.status==='active')reqWake()});

/* ═══ UTIL ═══ */
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
let toastT;function toast(m,err){const t=document.getElementById('toast');t.textContent=m;t.className='toast show'+(err?' error':'');clearTimeout(toastT);toastT=setTimeout(()=>t.classList.remove('show'),2800)}

document.getElementById('nameInput').addEventListener('keydown',e=>{if(e.key==='Enter')document.querySelector('.home-form .btn-primary').click()});

wsConnect();
</script>
</body>
</html>