{"id":108,"date":"2026-08-12T15:29:01","date_gmt":"2026-08-13T01:29:01","guid":{"rendered":"https:\/\/mana-corp.com\/?page_id=108"},"modified":"2026-08-12T15:41:26","modified_gmt":"2026-08-13T01:41:26","slug":"hexv2","status":"publish","type":"page","link":"http:\/\/mana-corp.com\/?page_id=108","title":{"rendered":"Hex v2"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"108\" class=\"elementor elementor-108\">\n\t\t\t\t<div class=\"elementor-element elementor-element-66d00e9 e-flex e-con-boxed e-con e-parent\" data-id=\"66d00e9\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-e93f38f elementor-widget elementor-widget-html\" data-id=\"e93f38f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1\">\n<title>HexMarket \u2014 hyperlocal, hex-private<\/title>\n<meta name=\"theme-color\" content=\"#0A0D10\">\n\n<link rel=\"stylesheet\" href=\"https:\/\/unpkg.com\/maplibre-gl@4.7.1\/dist\/maplibre-gl.css\" \/>\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link href=\"https:\/\/fonts.cdnfonts.com\/css\/space-grotesk\" rel=\"stylesheet\">\n<link href=\"https:\/\/fonts.cdnfonts.com\/css\/inter\" rel=\"stylesheet\">\n<link href=\"https:\/\/fonts.cdnfonts.com\/css\/jetbrains-mono\" rel=\"stylesheet\">\n\n<style>\n:root{\n  --accent:#17E3A6;\n  --accent-2:#FF9F45;\n  --danger:#FF5C72;\n  --radius:16px;\n  --dur:220ms;\n}\nhtml[data-theme=\"dark\"]{\n  --bg:#0A0D10;\n  --panel:#12171B;\n  --panel-2:#171D22;\n  --border:rgba(255,255,255,.08);\n  --text:#E9EDEC;\n  --text-dim:#8B979A;\n  --sheet-shadow: 0 -12px 40px rgba(0,0,0,.5);\n}\nhtml[data-theme=\"light\"]{\n  --bg:#EEF1EF;\n  --panel:#FFFFFF;\n  --panel-2:#F5F7F6;\n  --border:rgba(10,20,18,.08);\n  --text:#0E1613;\n  --text-dim:#5B6A66;\n  --sheet-shadow: 0 -12px 40px rgba(20,40,35,.12);\n}\n*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}\nhtml,body{height:100%;margin:0;overscroll-behavior:none;}\nbody{\n  background:var(--bg); color:var(--text);\n  font-family:'Inter',sans-serif;\n  -webkit-user-select:none; user-select:none;\n}\n.mono{font-family:'JetBrains Mono',monospace;}\n.display{font-family:'Space Grotesk',sans-serif;}\n\n#map{position:fixed; inset:0; z-index:0; background:var(--bg);}\n#map:fullscreen, #map:-webkit-full-screen{ width:100vw; height:100vh; }\n\n\/* ---------- MapLibre control skin ---------- *\/\n.maplibregl-ctrl-top-right{\n  top:calc(env(safe-area-inset-top,0px) + 68px) !important;\n  right:12px !important;\n}\n.maplibregl-ctrl-group{\n  background:var(--panel) !important;\n  border:1px solid var(--border) !important;\n  border-radius:14px !important;\n  overflow:hidden;\n  box-shadow:0 6px 20px rgba(0,0,0,.15) !important;\n}\n.maplibregl-ctrl-group button{ width:38px !important; height:38px !important; }\n.maplibregl-ctrl-group button + button{ border-top:1px solid var(--border) !important; }\nhtml[data-theme=\"dark\"] .maplibregl-ctrl-group button .maplibregl-ctrl-icon{ filter:invert(1) brightness(1.7); }\n.maplibregl-ctrl-attrib{ font-size:9px !important; opacity:.6; border-radius:8px 0 0 0 !important; background:transparent !important; }\n.maplibregl-ctrl-attrib a{ color:inherit !important; }\n.maplibregl-ctrl-bottom-right .maplibregl-ctrl-attrib{ margin-right:4px !important; }\n.maplibregl-ctrl-logo{ opacity:.55; }\n.maplibregl-popup{ display:none !important; }\n\n\/* Hide zoom controls completely *\/\n.maplibregl-ctrl-zoom-in, .maplibregl-ctrl-zoom-out { display: none !important; }\n\n\/* ---------- top bar ---------- *\/\n.topbar{\n  position:fixed; top:0; left:0; right:0; z-index:500;\n  padding:calc(env(safe-area-inset-top,0px) + 12px) 12px 0 12px;\n  display:flex; gap:8px; align-items:center;\n  pointer-events:none;\n}\n.brand{\n  display:flex; align-items:center; gap:8px;\n  background:var(--panel); border:1px solid var(--border);\n  padding:10px 14px; border-radius:999px; pointer-events:auto;\n  box-shadow:0 6px 20px rgba(0,0,0,.15);\n}\n.brand .hexmark{width:16px;height:16px; color:var(--accent);}\n.brand span{font-weight:700; font-size:14px; letter-spacing:.2px;}\n.search{\n  flex:1; display:flex; align-items:center; gap:8px;\n  background:var(--panel); border:1px solid var(--border);\n  padding:10px 14px; border-radius:999px; pointer-events:auto;\n  color:var(--text-dim); font-size:13px;\n  box-shadow:0 6px 20px rgba(0,0,0,.15);\n}\n.icon-btn{\n  width:40px; height:40px; min-width:40px; border-radius:50%;\n  background:var(--panel); border:1px solid var(--border);\n  display:flex; align-items:center; justify-content:center;\n  pointer-events:auto; cursor:pointer; color:var(--text);\n  box-shadow:0 6px 20px rgba(0,0,0,.15);\n  transition:transform var(--dur);\n}\n.icon-btn:active{transform:scale(.9);}\n.icon-btn svg{width:18px;height:18px;}\n\n\/* ---------- HUD chip ---------- *\/\n.hud{\n  position:fixed; z-index:480; left:12px;\n  bottom:calc(var(--footer-h, 72px) + 14px);\n  background:var(--panel); border:1px solid var(--border);\n  border-radius:12px; padding:8px 10px;\n  display:flex; align-items:center; gap:8px;\n  box-shadow:0 6px 20px rgba(0,0,0,.15);\n  transition:bottom var(--dur);\n}\n.hud .dot{width:8px;height:8px;border-radius:50%; background:var(--accent); box-shadow:0 0 0 4px rgba(23,227,166,.18);}\n.hud .lines{display:flex; flex-direction:column; line-height:1.15;}\n.hud .l1{font-size:10px; color:var(--text-dim); letter-spacing:.4px; text-transform:uppercase;}\n.hud .l2{font-size:12px; font-weight:600;}\n\n\/* ---------- right toolbar ---------- *\/\n.toolbar{\n  position:fixed; right:12px; z-index:480;\n  bottom:calc(var(--footer-h, 72px) + 14px);\n  display:flex; flex-direction:column; gap:10px;\n  transition:bottom var(--dur);\n}\n.toolbar .icon-btn{position:relative;}\n.badge{\n  position:absolute; top:-4px; right:-4px; background:var(--danger); color:#fff;\n  font-size:9px; font-weight:700; min-width:16px; height:16px; border-radius:8px;\n  display:flex; align-items:center; justify-content:center; padding:0 4px;\n}\n.fab-sell{\n  width:52px;height:52px;border-radius:50%; border:none; cursor:pointer;\n  background:linear-gradient(145deg, var(--accent), #0FBF8A); color:#04140F;\n  display:flex; align-items:center; justify-content:center;\n  box-shadow:0 10px 24px rgba(23,227,166,.35); pointer-events:auto;\n  transition:transform var(--dur);\n}\n.fab-sell:active{transform:scale(.92);}\n.fab-sell svg{width:24px;height:24px;}\n\n\/* ---------- markers (plain DOM elements, positioned by MapLibre) ---------- *\/\n.self-dot-wrap{width:22px;height:22px; display:flex; align-items:center; justify-content:center;}\n.self-dot{width:16px;height:16px;border-radius:50%;background:var(--accent); border:3px solid var(--bg); box-shadow:0 0 0 4px rgba(23,227,166,.25);}\n.self-dot.pulsing::after{\n  content:''; position:absolute; width:16px;height:16px;border-radius:50%;\n  background:var(--accent); opacity:.5; animation:pulse 2.2s ease-out infinite;\n}\n@keyframes pulse{ 0%{transform:scale(1); opacity:.45;} 100%{transform:scale(2.6); opacity:0;} }\n\n\/* Compact marker with popup card *\/\n.marker-compact{\n  position:relative; cursor:pointer;\n}\n.marker-dot{\n  width:36px;height:36px;border-radius:50%;\n  background:var(--panel); border:2px solid var(--accent);\n  display:flex; align-items:center; justify-content:center;\n  box-shadow:0 4px 12px rgba(0,0,0,.25);\n  font-size:16px;\n  transition:transform 0.2s;\n}\n.marker-dot:hover{transform:scale(1.1);}\n.marker-dot.sell{border-color:var(--accent);}\n.marker-dot.buy{border-color:var(--accent-2);}\n.marker-dot.share{border-color:#7CA8FF;}\n.marker-dot.business{border-color:#A855F7;}\n\n\/* Popup ID Card *\/\n.marker-popup{\n  position:absolute; bottom:calc(100% + 8px); left:50%; transform:translateX(-50%) scale(0.9);\n  background:var(--panel); border:1px solid var(--border);\n  border-radius:14px; padding:10px; min-width:200px; max-width:240px;\n  box-shadow:0 8px 24px rgba(0,0,0,.3);\n  opacity:0; pointer-events:none;\n  transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n  z-index:100;\n}\n.marker-popup.show{\n  opacity:1; pointer-events:auto; transform:translateX(-50%) scale(1);\n}\n.marker-popup::after{\n  content:''; position:absolute; top:100%; left:50%; transform:translateX(-50%);\n  border:6px solid transparent; border-top-color:var(--panel);\n}\n.popup-header{display:flex; align-items:center; gap:8px; margin-bottom:8px;}\n.popup-avatar{\n  width:32px;height:32px;border-radius:50%;\n  background:linear-gradient(135deg, var(--accent), #0FBF8A);\n  display:flex;align-items:center;justify-content:center;\n  font-weight:700; font-size:12px; color:#04140F;\n}\n.popup-name{font-weight:600; font-size:13px; flex:1;}\n.popup-type{font-size:9px; padding:2px 6px; border-radius:999px; font-weight:700; text-transform:uppercase;}\n.popup-type.sell{background:rgba(23,227,166,.15); color:var(--accent);}\n.popup-type.buy{background:rgba(255,159,69,.15); color:var(--accent-2);}\n.popup-type.share{background:rgba(124,168,255,.15); color:#7CA8FF;}\n.popup-type.business{background:rgba(168,85,247,.15); color:#A855F7;}\n.popup-title{font-size:12px; font-weight:600; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}\n.popup-meta{font-size:10px; color:var(--text-dim); display:flex; justify-content:space-between;}\n.popup-price{font-weight:700; color:var(--accent);}\n.popup-action{\n  margin-top:8px; padding:6px 10px; border-radius:8px; border:none;\n  background:var(--accent); color:#04140F; font-weight:600; font-size:11px;\n  cursor:pointer; width:100%;\n}\n.popup-action:active{transform:scale(0.98);}\n\n\/* ---------- Footer Navigation ---------- *\/\n.footer-nav{\n  position:fixed; left:0; right:0; bottom:0; z-index:490;\n  background:var(--panel); border-top:1px solid var(--border);\n  padding:8px 0 calc(env(safe-area-inset-bottom,0px) + 8px);\n  display:flex; justify-content:space-around; align-items:center;\n  box-shadow:0 -4px 20px rgba(0,0,0,.1);\n}\n.nav-item{\n  display:flex; flex-direction:column; align-items:center; gap:4px;\n  padding:6px 16px; border-radius:12px; cursor:pointer;\n  color:var(--text-dim); transition:all var(--dur);\n  position:relative;\n}\n.nav-item.active{color:var(--accent);}\n.nav-item.active::before{\n  content:''; position:absolute; top:-8px; left:50%; transform:translateX(-50%);\n  width:20px; height:3px; border-radius:2px; background:var(--accent);\n}\n.nav-item svg{width:22px;height:22px;}\n.nav-item span{font-size:10px; font-weight:600;}\n.nav-item .nav-badge{\n  position:absolute; top:2px; right:8px;\n  background:var(--danger); color:#fff;\n  font-size:8px; font-weight:700; min-width:14px; height:14px; border-radius:7px;\n  display:flex; align-items:center; justify-content:center; padding:0 3px;\n}\n\n\/* ---------- Bottom Sheet (now triggered by nav) ---------- *\/\n.sheet{\n  position:fixed; left:0; right:0; bottom:0; z-index:485;\n  background:var(--panel); border-radius:22px 22px 0 0;\n  box-shadow:var(--sheet-shadow);\n  transform:translateY(calc(100% - 0px));\n  transition:transform 320ms cubic-bezier(.2,.8,.2,1);\n  touch-action:none;\n  padding-bottom:calc(env(safe-area-inset-bottom,0px) + 72px);\n  max-height:75vh; display:flex; flex-direction:column;\n}\n.sheet.open{transform:translateY(0);}\n.sheet .handle-wrap{padding:10px 0 4px; display:flex; justify-content:center; cursor:grab;}\n.sheet .handle{width:36px; height:4px; border-radius:2px; background:var(--border);}\n.sheet-tabs{display:flex; gap:6px; padding:2px 16px 12px; border-bottom:1px solid var(--border);}\n.tab{\n  flex:1; text-align:center; padding:10px 8px; border-radius:10px; font-size:13px; font-weight:600;\n  color:var(--text-dim); cursor:pointer; position:relative;\n}\n.tab.active{background:var(--panel-2); color:var(--text);}\n.sheet-body{overflow-y:auto; padding:14px 16px 24px; flex:1;}\n.sheet-title{font-size:12px; text-transform:uppercase; letter-spacing:.6px; color:var(--text-dim); margin:4px 0 10px;}\n\n\/* listing card *\/\n.card{\n  display:flex; gap:12px; align-items:center;\n  padding:12px; border-radius:14px; background:var(--panel-2);\n  border:1px solid var(--border); margin-bottom:10px; cursor:pointer;\n}\n.card .thumb{\n  width:52px;height:52px;border-radius:12px; flex-shrink:0;\n  background:linear-gradient(145deg, var(--panel), var(--border));\n  display:flex; align-items:center; justify-content:center; font-size:22px; overflow:hidden;\n}\n.card .thumb img{width:100%;height:100%;object-fit:cover;}\n.card .meta{flex:1; min-width:0;}\n.card .meta .t1{font-weight:700; font-size:14px; display:flex; align-items:center; gap:6px;}\n.card .meta .t2{font-size:12px; color:var(--text-dim); margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}\n.chip{font-size:10px; padding:2px 7px; border-radius:999px; font-weight:700; text-transform:uppercase; letter-spacing:.3px;}\n.chip.sell{background:rgba(23,227,166,.15); color:var(--accent);}\n.chip.buy{background:rgba(255,159,69,.15); color:var(--accent-2);}\n.chip.share{background:rgba(124,168,255,.15); color:#7CA8FF;}\n.chip.business{background:rgba(168,85,247,.15); color:#A855F7;}\n.price{font-weight:700; font-size:14px; color:var(--accent);}\n\n.empty{padding:40px 10px; text-align:center; color:var(--text-dim); font-size:13px;}\n.empty .big{font-size:32px; margin-bottom:8px;}\n\n\/* chat list item *\/\n.chatrow{display:flex; gap:12px; align-items:center; padding:12px 0; border-bottom:1px solid var(--border); cursor:pointer;}\n.avatar{width:42px;height:42px;border-radius:50%; background:var(--panel-2); border:1px solid var(--border); display:flex;align-items:center;justify-content:center; font-weight:700; color:var(--accent); flex-shrink:0;}\n.chatrow .meta{flex:1; min-width:0;}\n.chatrow .name{font-weight:600; font-size:14px;}\n.chatrow .last{font-size:12px; color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}\n.dot-online{width:8px;height:8px;border-radius:50%;background:var(--accent);}\n\n\/* form elements *\/\nlabel{font-size:11px; text-transform:uppercase; letter-spacing:.5px; color:var(--text-dim); display:block; margin:14px 0 6px;}\ninput[type=text], input[type=number], textarea, select{\n  width:100%; background:var(--panel-2); border:1px solid var(--border); color:var(--text);\n  padding:12px 14px; border-radius:12px; font-size:14px; font-family:inherit;\n}\ntextarea{resize:none; min-height:70px;}\n.seg{display:flex; gap:8px;}\n.seg button{\n  flex:1; padding:10px; border-radius:10px; border:1px solid var(--border); background:var(--panel-2);\n  color:var(--text-dim); font-weight:600; font-size:13px; cursor:pointer;\n}\n.seg button.on{background:var(--accent); color:#04140F; border-color:var(--accent);}\n.flagrow{display:flex; flex-wrap:wrap; gap:8px;}\n.flag-chip{\n  padding:8px 12px; border-radius:999px; border:1px solid var(--border); background:var(--panel-2);\n  font-size:12px; font-weight:600; color:var(--text-dim); cursor:pointer;\n}\n.flag-chip.on{background:var(--accent); border-color:var(--accent); color:#04140F;}\n.btn-primary{\n  width:100%; padding:14px; border-radius:14px; border:none; background:var(--accent); color:#04140F;\n  font-weight:700; font-size:14px; cursor:pointer; margin-top:18px;\n}\n.btn-primary:active{transform:scale(.98);}\n.range-row{display:flex; align-items:center; gap:10px;}\ninput[type=range]{flex:1; accent-color:var(--accent);}\n\n\/* Hex grid options panel *\/\n.hex-options{\n  display:grid; grid-template-columns:repeat(2,1fr); gap:8px; margin-top:12px;\n}\n.hex-option{\n  padding:10px; border-radius:10px; border:1px solid var(--border); background:var(--panel-2);\n  text-align:center; cursor:pointer; transition:all var(--dur);\n}\n.hex-option.active{border-color:var(--accent); background:rgba(23,227,166,.1);}\n.hex-option .val{font-size:16px; font-weight:700; color:var(--accent);}\n.hex-option .lbl{font-size:10px; color:var(--text-dim); margin-top:2px;}\n\n\/* Zoom lock indicator *\/\n.zoom-lock{\n  display:flex; align-items:center; gap:8px; padding:10px;\n  background:var(--panel-2); border-radius:10px; margin-top:12px;\n}\n.zoom-lock input[type=checkbox]{\n  width:18px; height:18px; accent-color:var(--accent);\n}\n.zoom-lock label{margin:0; font-size:12px; color:var(--text); text-transform:none; letter-spacing:0;}\n\n\/* ---------- modal \/ overlays ---------- *\/\n.overlay{\n  position:fixed; inset:0; z-index:900; background:rgba(0,0,0,.55);\n  display:flex; align-items:flex-end; opacity:0; pointer-events:none; transition:opacity var(--dur);\n}\n.overlay.show{opacity:1; pointer-events:auto;}\n.modal{\n  background:var(--panel); width:100%; border-radius:22px 22px 0 0; max-height:92vh; overflow-y:auto;\n  padding:16px 18px calc(env(safe-area-inset-bottom,0px) + 20px); transform:translateY(30px); transition:transform var(--dur);\n}\n.overlay.show .modal{transform:translateY(0);}\n.modal-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;}\n.modal-head h2{font-size:17px; margin:0; font-family:'Space Grotesk';}\n.close-x{width:32px;height:32px;border-radius:50%; background:var(--panel-2); border:none; color:var(--text); cursor:pointer; display:flex;align-items:center;justify-content:center;}\n\n\/* full-screen chat\/onboarding *\/\n.fullscreen{\n  position:fixed; inset:0; z-index:950; background:var(--bg); display:flex; flex-direction:column;\n  transform:translateX(100%); transition:transform 280ms cubic-bezier(.2,.8,.2,1);\n}\n.fullscreen.show{transform:translateX(0);}\n.fs-head{\n  display:flex; align-items:center; gap:10px; padding:calc(env(safe-area-inset-top,0px) + 12px) 14px 12px;\n  border-bottom:1px solid var(--border); background:var(--panel);\n}\n.fs-head .name{font-weight:700; font-size:15px;}\n.fs-head .sub{font-size:11px; color:var(--text-dim);}\n.fs-body{flex:1; overflow-y:auto; padding:14px;}\n.msg{max-width:75%; padding:10px 13px; border-radius:16px; margin-bottom:8px; font-size:13.5px; line-height:1.4;}\n.msg.me{margin-left:auto; background:var(--accent); color:#04140F; border-bottom-right-radius:4px;}\n.msg.them{background:var(--panel-2); border:1px solid var(--border); border-bottom-left-radius:4px;}\n.msg.sys{margin:10px auto; text-align:center; max-width:90%; background:transparent; color:var(--text-dim); font-size:11px;}\n.fs-input{\n  display:flex; gap:8px; padding:10px 12px calc(env(safe-area-inset-bottom,0px) + 10px);\n  border-top:1px solid var(--border); background:var(--panel);\n}\n.fs-input input{flex:1;}\n.send-btn, .call-btn{\n  width:44px;height:44px;border-radius:50%; border:none; background:var(--accent); color:#04140F;\n  display:flex;align-items:center;justify-content:center; cursor:pointer; flex-shrink:0;\n}\n.call-btn{background:var(--panel-2); color:var(--text); border:1px solid var(--border);}\n\n\/* video call overlay *\/\n.callscreen{position:fixed; inset:0; z-index:980; background:#000; display:none; flex-direction:column;}\n.callscreen.show{display:flex;}\n.callscreen video{width:100%; height:100%; object-fit:cover; position:absolute; inset:0;}\n#remoteVideo{background:#000;}\n#localVideo{\n  position:absolute; width:100px; height:140px; right:14px; top:calc(env(safe-area-inset-top,0px) + 14px);\n  border-radius:14px; border:2px solid rgba(255,255,255,.3); object-fit:cover; z-index:2;\n}\n.call-status{position:absolute; top:calc(env(safe-area-inset-top,0px) + 14px); left:14px; z-index:2; color:#fff; font-size:13px; background:rgba(0,0,0,.4); padding:6px 12px; border-radius:999px;}\n.call-controls{position:absolute; bottom:calc(env(safe-area-inset-bottom,0px) + 30px); left:0; right:0; display:flex; justify-content:center; gap:18px; z-index:2;}\n.call-controls button{\n  width:56px;height:56px;border-radius:50%; border:none; cursor:pointer; display:flex;align-items:center;justify-content:center;\n  background:rgba(255,255,255,.15); color:#fff; backdrop-filter:blur(10px);\n}\n.call-controls button.end{background:var(--danger);}\n\n.toast{\n  position:fixed; left:50%; top:calc(env(safe-area-inset-top,0px) + 60px); transform:translate(-50%,-20px);\n  background:var(--panel); border:1px solid var(--border); padding:10px 16px; border-radius:999px; font-size:12.5px;\n  box-shadow:0 10px 24px rgba(0,0,0,.25); z-index:1200; opacity:0; transition:all var(--dur); pointer-events:none;\n}\n.toast.show{opacity:1; transform:translate(-50%,0);}\n\n::-webkit-scrollbar{width:0;height:0;}\n<\/style>\n<\/head>\n<body>\n\n<div id=\"map\"><\/div>\n\n<div class=\"topbar\">\n  <div class=\"brand\">\n    <svg class=\"hexmark\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 1 22 7v10l-10 6-10-6V7z\"\/><\/svg>\n    <span class=\"display\">HexMarket<\/span>\n  <\/div>\n  <div class=\"search\" id=\"searchBar\">\n    <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"11\" cy=\"11\" r=\"7\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg>\n    <span id=\"searchLabel\">Nearby, buy \u00b7 sell \u00b7 share<\/span>\n  <\/div>\n  <div class=\"icon-btn\" id=\"themeBtn\" title=\"Toggle theme\">\n    <svg id=\"themeIcon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"\/><\/svg>\n  <\/div>\n<\/div>\n\n<div class=\"hud\" id=\"hud\">\n  <div class=\"dot\" id=\"hudDot\"><\/div>\n  <div class=\"lines\">\n    <div class=\"l1\">anchor hex \u00b7 res <span id=\"hudRes\">9<\/span><\/div>\n    <div class=\"l2 mono\" id=\"hudHex\">locating\u2026<\/div>\n  <\/div>\n<\/div>\n\n<div class=\"toolbar\" id=\"toolbar\">\n  <div class=\"icon-btn\" id=\"locateBtn\" title=\"Recenter\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"3\"\/><path d=\"M12 2v3M12 19v3M2 12h3M19 12h3\"\/><\/svg>\n  <\/div>\n  <div class=\"icon-btn\" id=\"layersBtn\" title=\"Hex privacy resolution\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 2 2 7l10 5 10-5-10-5Z\"\/><path d=\"M2 17l10 5 10-5M2 12l10 5 10-5\"\/><\/svg>\n  <\/div>\n  <div class=\"icon-btn\" id=\"fullscreenBtn\" title=\"Fullscreen\">\n    <svg id=\"fsIcon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3\"\/><\/svg>\n  <\/div>\n  <button class=\"fab-sell\" id=\"sellBtn\" title=\"New listing\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\"><path d=\"M12 5v14M5 12h14\"\/><\/svg>\n  <\/button>\n<\/div>\n\n<!-- ============ FOOTER NAVIGATION ============ -->\n<div class=\"footer-nav\" id=\"footerNav\">\n  <div class=\"nav-item active\" data-tab=\"nearby\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 2 2 7l10 5 10-5-10-5Z\"\/><path d=\"M2 17l10 5 10-5M2 12l10 5 10-5\"\/><\/svg>\n    <span>Nearby<\/span>\n  <\/div>\n  <div class=\"nav-item\" data-tab=\"messages\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5H3l3-3.5A8.4 8.4 0 0 1 21 11.5Z\"\/><\/svg>\n    <span>Messages<\/span>\n    <span class=\"nav-badge\" id=\"navChatBadge\" style=\"display:none;\">0<\/span>\n  <\/div>\n  <div class=\"nav-item\" data-tab=\"hexgrid\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polygon points=\"12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2\"\/><line x1=\"12\" y1=\"22\" x2=\"12\" y2=\"15.5\"\/><polyline points=\"22 8.5 12 15.5 2 8.5\"\/><\/svg>\n    <span>Hex Grid<\/span>\n  <\/div>\n  <div class=\"nav-item\" data-tab=\"me\">\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"\/><circle cx=\"12\" cy=\"7\" r=\"4\"\/><\/svg>\n    <span>Me<\/span>\n  <\/div>\n<\/div>\n\n<!-- ============ BOTTOM SHEET ============ -->\n<div class=\"sheet\" id=\"sheet\">\n  <div class=\"handle-wrap\" id=\"sheetHandle\"><div class=\"handle\"><\/div><\/div>\n  <div class=\"sheet-body\" id=\"sheetBody\">\n    <div class=\"sheet-title\" id=\"nearbyTitle\">Nearby in your hex ring<\/div>\n    <div id=\"listingsList\"><\/div>\n  <\/div>\n<\/div>\n\n<!-- ============ NEW LISTING MODAL ============ -->\n<div class=\"overlay\" id=\"listingOverlay\">\n  <div class=\"modal\">\n    <div class=\"modal-head\"><h2>New listing<\/h2><button class=\"close-x\" data-close=\"listingOverlay\">\u2715<\/button><\/div>\n    <div class=\"seg\" id=\"typeSeg\">\n      <button class=\"on\" data-type=\"sell\">Sell<\/button>\n      <button data-type=\"buy\">Buy<\/button>\n      <button data-type=\"share\">Share<\/button>\n      <button data-type=\"business\">Business<\/button>\n    <\/div>\n    <label>Title<\/label>\n    <input type=\"text\" id=\"lTitle\" placeholder=\"e.g. Vintage film camera\">\n    <label>Details<\/label>\n    <textarea id=\"lDesc\" placeholder=\"Condition, pickup notes, anything a neighbor should know\u2026\"><\/textarea>\n    <label>Price (leave 0 for free \/ share)<\/label>\n    <input type=\"number\" id=\"lPrice\" placeholder=\"0\" inputmode=\"decimal\">\n    <label>Category<\/label>\n    <div class=\"seg\" id=\"catSeg\">\n      <button class=\"on\" data-cat=\"\ud83d\udecd\ufe0f\">Goods<\/button>\n      <button data-cat=\"\ud83c\udf5c\">Food<\/button>\n      <button data-cat=\"\ud83e\uddf0\">Service<\/button>\n      <button data-cat=\"\ud83d\udeb2\">Delivery<\/button>\n      <button data-cat=\"\ud83c\udfea\">Shop<\/button>\n    <\/div>\n    <label>Photo (optional, stays on this device)<\/label>\n    <input type=\"file\" id=\"lPhoto\" accept=\"image\/*\">\n    <button class=\"btn-primary\" id=\"publishBtn\">Publish to my anchor hex<\/button>\n  <\/div>\n<\/div>\n\n<!-- ============ LAYERS \/ PRIVACY MODAL ============ -->\n<div class=\"overlay\" id=\"layersOverlay\">\n  <div class=\"modal\">\n    <div class=\"modal-head\"><h2>Hex privacy layer<\/h2><button class=\"close-x\" data-close=\"layersOverlay\">\u2715<\/button><\/div>\n    <p style=\"font-size:12.5px; color:var(--text-dim); line-height:1.5; margin-top:0;\">\n      Your exact GPS point never leaves this device. Only the H3 hex cell you're standing in \u2014 smoothed by a Kalman filter to kill jitter \u2014 is shared. Lower resolution = bigger hex = more privacy.\n    <\/p>\n    <label>Resolution: <span id=\"resLabel\" class=\"mono\">9<\/span> (~<span id=\"resSize\">0.1<\/span> km\u00b2 cell)<\/label>\n    <div class=\"range-row\">\n      <input type=\"range\" id=\"resRange\" min=\"6\" max=\"10\" step=\"1\" value=\"9\">\n    <\/div>\n    \n    <label>Hex Grid Display Options<\/label>\n    <div class=\"hex-options\">\n      <div class=\"hex-option active\" data-opt=\"fill\" data-val=\"1\">\n        <div class=\"val\">\u25fc<\/div>\n        <div class=\"lbl\">Fill<\/div>\n      <\/div>\n      <div class=\"hex-option\" data-opt=\"fill\" data-val=\"0\">\n        <div class=\"val\">\u25fb<\/div>\n        <div class=\"lbl\">Outline<\/div>\n      <\/div>\n      <div class=\"hex-option active\" data-opt=\"ring\" data-val=\"1\">\n        <div class=\"val\">\u2b21<\/div>\n        <div class=\"lbl\">Ring<\/div>\n      <\/div>\n      <div class=\"hex-option\" data-opt=\"ring\" data-val=\"2\">\n        <div class=\"val\">\u2b21\u2b21<\/div>\n        <div class=\"lbl\">Ring 2<\/div>\n      <\/div>\n    <\/div>\n    \n    <div class=\"zoom-lock\">\n      <input type=\"checkbox\" id=\"zoomLock\" checked>\n      <label for=\"zoomLock\">Lock zoom to hex resolution<\/label>\n    <\/div>\n    \n    <label>Intent flags (visible to matches only)<\/label>\n    <div class=\"flagrow\" id=\"flagRow\">\n      <div class=\"flag-chip on\" data-flag=\"1\">\ud83d\uded2 Buying<\/div>\n      <div class=\"flag-chip on\" data-flag=\"2\">\ud83d\udcb0 Selling<\/div>\n      <div class=\"flag-chip\" data-flag=\"4\">\ud83c\udf81 Sharing<\/div>\n      <div class=\"flag-chip\" data-flag=\"8\">\ud83c\udfea Business<\/div>\n      <div class=\"flag-chip\" data-flag=\"16\">\ud83d\udeb4 Can deliver<\/div>\n    <\/div>\n    <button class=\"btn-primary\" id=\"saveLayersBtn\">Save<\/button>\n  <\/div>\n<\/div>\n\n<!-- ============ HEX GRID OPTIONS MODAL ============ -->\n<div class=\"overlay\" id=\"hexGridOverlay\">\n  <div class=\"modal\">\n    <div class=\"modal-head\"><h2>Hex Grid Settings<\/h2><button class=\"close-x\" data-close=\"hexGridOverlay\">\u2715<\/button><\/div>\n    \n    <label>Current Resolution<\/label>\n    <div class=\"card\" style=\"cursor:default;\">\n      <div class=\"thumb\">\u2b21<\/div>\n      <div class=\"meta\">\n        <div class=\"t1\">Resolution <span id=\"currentResDisplay\">9<\/span><\/div>\n        <div class=\"t2\">Cell size: ~<span id=\"currentSizeDisplay\">0.105<\/span> km\u00b2<\/div>\n      <\/div>\n    <\/div>\n    \n    <label>Zoom Level Lock<\/label>\n    <div class=\"seg\" id=\"zoomSeg\">\n      <button class=\"on\" data-zoom=\"auto\">Auto<\/button>\n      <button data-zoom=\"locked\">Locked<\/button>\n      <button data-zoom=\"free\">Free<\/button>\n    <\/div>\n    \n    <label>Hex Ring Radius<\/label>\n    <div class=\"range-row\">\n      <span class=\"mono\">0<\/span>\n      <input type=\"range\" id=\"ringRadius\" min=\"0\" max=\"3\" step=\"1\" value=\"1\">\n      <span class=\"mono\" id=\"ringRadiusVal\">1<\/span>\n    <\/div>\n    \n    <label>Display Layers<\/label>\n    <div class=\"flagrow\" id=\"layerFlags\">\n      <div class=\"flag-chip on\" data-layer=\"current\">Current Hex<\/div>\n      <div class=\"flag-chip on\" data-layer=\"ring\">Neighbor Ring<\/div>\n      <div class=\"flag-chip\" data-layer=\"grid\">Full Grid<\/div>\n      <div class=\"flag-chip\" data-layer=\"labels\">Hex IDs<\/div>\n    <\/div>\n    \n    <label>Hex Style<\/label>\n    <div class=\"seg\" id=\"hexStyleSeg\">\n      <button class=\"on\" data-style=\"solid\">Solid<\/button>\n      <button data-style=\"gradient\">Gradient<\/button>\n      <button data-style=\"pulse\">Pulse<\/button>\n    <\/div>\n    \n    <button class=\"btn-primary\" id=\"saveHexGridBtn\">Apply Settings<\/button>\n  <\/div>\n<\/div>\n\n<!-- ============ LISTING DETAIL MODAL ============ -->\n<div class=\"overlay\" id=\"detailOverlay\">\n  <div class=\"modal\" id=\"detailModal\"><\/div>\n<\/div>\n\n<!-- ============ ONBOARDING ============ -->\n<div class=\"overlay show\" id=\"onboardOverlay\" style=\"z-index:1100;\">\n  <div class=\"modal\">\n    <div class=\"modal-head\"><h2>Welcome to HexMarket<\/h2><\/div>\n    <p style=\"font-size:12.5px; color:var(--text-dim); line-height:1.5; margin-top:0;\">A hyperlocal map for buying, selling and sharing with people in your hex. Pick a name \u2014 no account, no server database, everything lives on your device and syncs peer\u2011to\u2011peer.<\/p>\n    <label>Display name<\/label>\n    <input type=\"text\" id=\"onName\" placeholder=\"e.g. Alex\">\n    <button class=\"btn-primary\" id=\"onboardBtn\">Drop my pin<\/button>\n  <\/div>\n<\/div>\n\n<!-- ============ FULLSCREEN CHAT ============ -->\n<div class=\"fullscreen\" id=\"chatScreen\">\n  <div class=\"fs-head\">\n    <button class=\"icon-btn\" id=\"chatBack\" style=\"box-shadow:none;\">\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M15 18l-6-6 6-6\"\/><\/svg>\n    <\/button>\n    <div class=\"avatar\" id=\"chatAvatar\">?<\/div>\n    <div style=\"flex:1;\">\n      <div class=\"name\" id=\"chatName\">\u2014<\/div>\n      <div class=\"sub mono\" id=\"chatPeerId\">\u2014<\/div>\n    <\/div>\n    <button class=\"call-btn\" id=\"videoCallBtn\">\n      <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M23 7l-7 5 7 5V7Z\"\/><rect x=\"1\" y=\"5\" width=\"15\" height=\"14\" rx=\"2\"\/><\/svg>\n    <\/button>\n  <\/div>\n  <div class=\"fs-body\" id=\"chatBody\"><\/div>\n  <div class=\"fs-input\">\n    <input type=\"text\" id=\"chatInput\" placeholder=\"Message\u2026\">\n    <button class=\"send-btn\" id=\"chatSend\">\n      <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z\"\/><\/svg>\n    <\/button>\n  <\/div>\n<\/div>\n\n<!-- ============ VIDEO CALL SCREEN ============ -->\n<div class=\"callscreen\" id=\"callScreen\">\n  <video id=\"remoteVideo\" autoplay playsinline><\/video>\n  <video id=\"localVideo\" autoplay playsinline muted><\/video>\n  <div class=\"call-status\" id=\"callStatus\">Calling\u2026<\/div>\n  <div class=\"call-controls\">\n    <button id=\"muteBtn\"><svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3Z\"\/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2M12 19v4\"\/><\/svg><\/button>\n    <button class=\"end\" id=\"endCallBtn\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 9c-2.5 0-4.8.6-6.8 1.7a1.7 1.7 0 0 0-.5 2.5l1.6 2.1c.4.5 1.1.7 1.7.4.8-.4 1.7-.7 2.6-.9.4-.1.7-.4.7-.8v-1.6c0-.4.3-.8.7-.9 1.3-.3 2.7-.3 4 0 .4.1.7.5.7.9V14c0 .4.3.7.7.8.9.2 1.8.5 2.6.9.6.3 1.3.1 1.7-.4l1.6-2.1a1.7 1.7 0 0 0-.5-2.5C16.8 9.6 14.5 9 12 9Z\"\/><\/svg><\/button>\n  <\/div>\n<\/div>\n\n<div class=\"toast\" id=\"toast\"><\/div>\n\n<script src=\"https:\/\/unpkg.com\/maplibre-gl@4.7.1\/dist\/maplibre-gl.js\"><\/script>\n<script src=\"https:\/\/unpkg.com\/h3-js@4.1.0\/dist\/h3-js.umd.js\"><\/script>\n<script src=\"https:\/\/unpkg.com\/dexie@3\/dist\/dexie.js\"><\/script>\n<script src=\"https:\/\/unpkg.com\/peerjs@1.5.4\/dist\/peerjs.min.js\"><\/script>\n\n<script>\n\/* =========================================================================\n   HexMarket \u2014 standalone hyperlocal P2P map marketplace\n   Enhanced with: Footer nav, compact markers with popup cards, \n   advanced H3 hex grid options, zoom\/resolution locking\n   ========================================================================= *\/\n\n\/* ---------- tiny helpers ---------- *\/\nconst $ = (s,el=document)=>el.querySelector(s);\nconst $$ = (s,el=document)=>[...el.querySelectorAll(s)];\nconst uid = ()=> 'x'+Math.random().toString(36).slice(2,10)+Date.now().toString(36).slice(-4);\nfunction toast(msg,ms=2200){\n  const t=$('#toast'); t.textContent=msg; t.classList.add('show');\n  clearTimeout(toast._t); toast._t=setTimeout(()=>t.classList.remove('show'),ms);\n}\nfunction haptic(ms=8){ if(navigator.vibrate) navigator.vibrate(ms); }\nfunction escapeHtml(s){ return (s||'').replace(\/[&<>\"']\/g, m=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[m])); }\n\n\/* ---------- persistent local identity ---------- *\/\nlet ME = JSON.parse(localStorage.getItem('hx_me')||'null');\nif(!ME){\n  ME = { id: 'hx_'+uid(), name:'', flags:1|2, res:9, hexOpts:{fill:1,ring:1,ringRadius:1,zoomLock:true,zoomMode:'auto',layers:['current','ring'],style:'solid'} };\n  localStorage.setItem('hx_me', JSON.stringify(ME));\n}\nif(!ME.hexOpts) ME.hexOpts = {fill:1,ring:1,ringRadius:1,zoomLock:true,zoomMode:'auto',layers:['current','ring'],style:'solid'};\nfunction saveMe(){ localStorage.setItem('hx_me', JSON.stringify(ME)); }\n\n\/* ---------- IndexedDB (Dexie) ---------- *\/\nconst db = new Dexie('HexMarketDB');\ndb.version(1).stores({\n  listings: 'id, hex, type, ts',\n  chats: 'id, peerId, ts',\n  messages: '++auto, chatId, ts'\n});\n\n\/* =========================================================================\n   Kalman filter for GPS smoothing\n   ========================================================================= *\/\nclass GeoKalman{\n  constructor(){ this.initialized=false; this.varProcess = 0.7; }\n  update(lat,lng,accuracyMeters,timestamp){\n    accuracyMeters = Math.max(accuracyMeters||10, 3);\n    if(!this.initialized){\n      this.lat=lat; this.lng=lng; this.variance=accuracyMeters*accuracyMeters;\n      this.t=timestamp; this.initialized=true;\n      return {lat:this.lat,lng:this.lng};\n    }\n    const dt = Math.max((timestamp-this.t)\/1000, 0.001);\n    this.t = timestamp;\n    this.variance += dt * this.varProcess * this.varProcess;\n    const k = this.variance \/ (this.variance + accuracyMeters*accuracyMeters);\n    this.lat += k*(lat-this.lat);\n    this.lng += k*(lng-this.lng);\n    this.variance *= (1-k);\n    return {lat:this.lat, lng:this.lng};\n  }\n}\nconst kalman = new GeoKalman();\n\n\/* =========================================================================\n   Map setup \u2014 MapLibre GL\n   ========================================================================= *\/\nconst STYLES = {\n  light: 'https:\/\/basemaps.cartocdn.com\/gl\/positron-gl-style\/style.json',\n  dark:  'https:\/\/basemaps.cartocdn.com\/gl\/dark-matter-gl-style\/style.json'\n};\nlet theme = localStorage.getItem('hx_theme') || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark':'light');\ndocument.documentElement.setAttribute('data-theme', theme);\n\n\/\/ Resolution to zoom level mapping\nconst RES_ZOOM = {6:10, 7:12, 8:13, 9:15, 10:17};\n\nconst map = new maplibregl.Map({\n  container:'map',\n  style: STYLES[theme],\n  center:[-74.0060,40.7128],\n  zoom:15,\n  attributionControl:true,\n  maxPitch:70,\n  hash:false,\n  minZoom: 3,\n  maxZoom: 19\n});\n\n\/\/ Only add compass control, no zoom buttons\nmap.addControl(new maplibregl.NavigationControl({showCompass:true, showZoom:false, visualizePitch:false}), 'top-right');\n\nfunction themeIconSwap(t){\n  $('#themeIcon').innerHTML = t==='dark'\n    ? '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"\/>'\n    : '<circle cx=\"12\" cy=\"12\" r=\"4\"\/><path d=\"M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M5 19l1.5-1.5M17.5 6.5 19 5\"\/>';\n}\nthemeIconSwap(theme);\n\nfunction setTheme(t){\n  theme=t; localStorage.setItem('hx_theme', t);\n  document.documentElement.setAttribute('data-theme', t);\n  themeIconSwap(t);\n  map.setStyle(STYLES[t]);\n}\n$('#themeBtn').onclick = ()=>{ haptic(); setTheme(theme==='dark'?'light':'dark'); };\n\n\/* ---------- hex overlay with advanced options ---------- *\/\nconst ACCENT = '#17E3A6';\nfunction emptyFC(){ return {type:'FeatureCollection', features:[]}; }\nfunction hexPolygonFeature(hexId, props={}){\n  const ring = h3.cellToBoundary(hexId, true);\n  ring.push(ring[0]);\n  return {type:'Feature', properties:{id:hexId, ...props}, geometry:{type:'Polygon', coordinates:[ring]}};\n}\n\nfunction ensureHexLayers(){\n  if(!map.getSource('hex-current')){\n    map.addSource('hex-current', {type:'geojson', data: emptyFC()});\n    map.addLayer({id:'hex-current-fill', type:'fill', source:'hex-current', paint:{'fill-color':ACCENT, 'fill-opacity':0.12}});\n    map.addLayer({id:'hex-current-line', type:'line', source:'hex-current', paint:{'line-color':ACCENT, 'line-width':2.5, 'line-opacity':0.8}});\n  }\n  if(!map.getSource('hex-ring')){\n    map.addSource('hex-ring', {type:'geojson', data: emptyFC()});\n    map.addLayer({id:'hex-ring-fill', type:'fill', source:'hex-ring', paint:{'fill-color':'#8B979A', 'fill-opacity':0.05}});\n    map.addLayer({id:'hex-ring-line', type:'line', source:'hex-ring', paint:{'line-color':'#8B979A', 'line-width':1.5, 'line-opacity':0.4, 'line-dasharray':[2,2]}});\n  }\n  if(!map.getSource('hex-grid')){\n    map.addSource('hex-grid', {type:'geojson', data: emptyFC()});\n    map.addLayer({id:'hex-grid-line', type:'line', source:'hex-grid', paint:{'line-color':'#8B979A', 'line-width':0.5, 'line-opacity':0.2}});\n  }\n  if(!map.getSource('hex-labels')){\n    map.addSource('hex-labels', {type:'geojson', data: emptyFC()});\n    map.addLayer({\n      id:'hex-labels-text', type:'symbol', source:'hex-labels',\n      layout:{'text-field':['get','label'], 'text-size':10, 'text-font':['Open Sans Regular']},\n      paint:{'text-color':'#8B979A', 'text-opacity':0.6}\n    });\n  }\n}\n\nfunction renderHexOverlay(){\n  if(!currentHex || !map.isStyleLoaded()) return;\n  ensureHexLayers();\n  \n  const opts = ME.hexOpts;\n  const layers = opts.layers || ['current','ring'];\n  \n  \/\/ Current hex\n  if(layers.includes('current')){\n    const currentFeature = hexPolygonFeature(currentHex);\n    map.getSource('hex-current').setData({type:'FeatureCollection', features:[currentFeature]});\n    map.setPaintProperty('hex-current-fill', 'fill-opacity', opts.fill ? 0.12 : 0);\n  } else {\n    map.getSource('hex-current').setData(emptyFC());\n  }\n  \n  \/\/ Ring neighbors using gridDisk [[3]]\n  if(layers.includes('ring') && opts.ring > 0){\n    const radius = opts.ringRadius || 1;\n    const neighbors = h3.gridDisk(currentHex, radius).filter(h=>h!==currentHex).map(h=>hexPolygonFeature(h));\n    map.getSource('hex-ring').setData({type:'FeatureCollection', features:neighbors});\n    map.setPaintProperty('hex-ring-fill', 'fill-opacity', opts.fill ? 0.05 : 0);\n  } else {\n    map.getSource('hex-ring').setData(emptyFC());\n  }\n  \n  \/\/ Full grid view\n  if(layers.includes('grid')){\n    const radius = opts.ringRadius || 1;\n    const allHexes = h3.gridDisk(currentHex, radius + 1).map(h=>hexPolygonFeature(h));\n    map.getSource('hex-grid').setData({type:'FeatureCollection', features:allHexes});\n  } else {\n    map.getSource('hex-grid').setData(emptyFC());\n  }\n  \n  \/\/ Hex ID labels\n  if(layers.includes('labels')){\n    const radius = opts.ringRadius || 1;\n    const labels = h3.gridDisk(currentHex, radius).map(h=>{\n      const [lat,lng] = h3.cellToLatLng(h);\n      return {type:'Feature', properties:{label:h.slice(-6)}, geometry:{type:'Point', coordinates:[lng,lat]}};\n    });\n    map.getSource('hex-labels').setData({type:'FeatureCollection', features:labels});\n  } else {\n    map.getSource('hex-labels').setData(emptyFC());\n  }\n}\n\nmap.on('style.load', renderHexOverlay);\n\n\/* ---------- Zoom lock functionality ---------- *\/\nlet zoomLocked = true;\nmap.on('zoomend', ()=>{\n  if(zoomLocked && ME.hexOpts.zoomMode === 'locked'){\n    const targetZoom = RES_ZOOM[ME.res] || 15;\n    if(Math.abs(map.getZoom() - targetZoom) > 0.5){\n      map.setZoom(targetZoom);\n    }\n  }\n});\n\n\/* ---------- self marker + smooth motion ---------- *\/\nlet selfMarker = null;\nlet currentHex = null;\nlet lastLatLng = null;\n\nconst RES_META = {6:'36',7:'5.1',8:'0.74',9:'0.105',10:'0.015'};\n\nfunction makeSelfEl(){\n  const wrap=document.createElement('div'); wrap.className='self-dot-wrap';\n  const dot=document.createElement('div'); dot.className='self-dot pulsing'; dot.style.position='relative';\n  wrap.appendChild(dot);\n  return wrap;\n}\nfunction animateMarkerTo(marker,lat,lng){\n  const start = marker.getLngLat();\n  const t0 = performance.now(); const dur = 260;\n  function step(t){\n    const p = Math.min(1,(t-t0)\/dur);\n    const el = 1-Math.pow(1-p,3);\n    marker.setLngLat([start.lng+(lng-start.lng)*el, start.lat+(lat-start.lat)*el]);\n    if(p<1) requestAnimationFrame(step);\n  }\n  requestAnimationFrame(step);\n}\n\nfunction onPosition(lat,lng,accuracy,timestamp){\n  const f = kalman.update(lat,lng,accuracy,timestamp||Date.now());\n  lastLatLng = f;\n  const hex = h3.latLngToCell(f.lat, f.lng, ME.res);\n  const hexChanged = hex!==currentHex;\n  currentHex = hex;\n\n  if(!selfMarker){\n    selfMarker = new maplibregl.Marker({element:makeSelfEl(), anchor:'center'}).setLngLat([f.lng, f.lat]).addTo(map);\n    const targetZoom = RES_ZOOM[ME.res] || 15;\n    map.jumpTo({center:[f.lng, f.lat], zoom:targetZoom});\n  } else {\n    animateMarkerTo(selfMarker, f.lat, f.lng);\n  }\n  $('#hudHex').textContent = hex;\n  $('#hudRes').textContent = ME.res;\n  if(hexChanged){ renderHexOverlay(); refreshListings(); }\n}\n\nlet geoWatchId=null;\nfunction startGeo(){\n  if(!navigator.geolocation){ toast('Geolocation unavailable \u2014 using demo location'); demoDrift(); return; }\n  geoWatchId = navigator.geolocation.watchPosition(\n    pos=> onPosition(pos.coords.latitude,pos.coords.longitude,pos.coords.accuracy,pos.timestamp),\n    err=> { toast('Location unavailable \u2014 using demo location'); demoDrift(); },\n    {enableHighAccuracy:true, maximumAge:1000, timeout:8000}\n  );\n}\nfunction demoDrift(){\n  if(demoDrift._on) return; demoDrift._on=true;\n  let lat=40.7128,lng=-74.0060;\n  onPosition(lat,lng,15,Date.now());\n  setInterval(()=>{\n    lat += (Math.random()-0.5)*0.0006;\n    lng += (Math.random()-0.5)*0.0006;\n    onPosition(lat + (Math.random()-.5)*0.00006, lng + (Math.random()-.5)*0.00006, 12, Date.now());\n  }, 3000);\n}\nmap.on('load', startGeo);\n\n$('#locateBtn').onclick = ()=>{ haptic(); if(lastLatLng){ \n  const targetZoom = RES_ZOOM[ME.res] || 15;\n  map.flyTo({center:[lastLatLng.lng,lastLatLng.lat], zoom:targetZoom, duration:600}); \n} };\n\n\/* ---------- fullscreen toggle ---------- *\/\nfunction isFs(){ return !!(document.fullscreenElement || document.webkitFullscreenElement); }\nfunction updateFsIcon(){\n  $('#fsIcon').innerHTML = isFs()\n    ? '<path d=\"M9 3v4a2 2 0 0 1-2 2H3M15 3v4a2 2 0 0 0 2 2h4M21 15h-4a2 2 0 0 0-2 2v4M3 15h4a2 2 0 0 1 2 2v4\"\/>'\n    : '<path d=\"M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3\"\/>';\n}\n$('#fullscreenBtn').onclick = ()=>{\n  haptic();\n  const el = document.documentElement;\n  if(!isFs()){ (el.requestFullscreen||el.webkitRequestFullscreen)?.call(el); }\n  else { (document.exitFullscreen||document.webkitExitFullscreen)?.call(document); }\n};\ndocument.addEventListener('fullscreenchange', ()=>{ updateFsIcon(); setTimeout(()=>map.resize(),120); });\ndocument.addEventListener('webkitfullscreenchange', ()=>{ updateFsIcon(); setTimeout(()=>map.resize(),120); });\nupdateFsIcon();\n\n\/* =========================================================================\n   Listings with compact markers and popup cards\n   ========================================================================= *\/\nfunction hexCenter(hex){ const [lat,lng]=h3.cellToLatLng(hex); return {lat,lng}; }\nfunction jitter(v,amt){ return v + (Math.random()-0.5)*amt; }\n\nasync function seedDemoIfEmpty(){\n  const count = await db.listings.count();\n  if(count>0 || !currentHex) return;\n  const ring = h3.gridDisk(currentHex,2);\n  const demo = [\n    {title:'Sourdough starter, active', price:0, cat:'\ud83c\udf5c', type:'share', peer:'demo_bea'},\n    {title:'Vintage 35mm camera', price:45, cat:'\ud83d\udecd\ufe0f', type:'sell', peer:'demo_sam'},\n    {title:'Looking for a bike pump', price:0, cat:'\ud83d\udecd\ufe0f', type:'buy', peer:'demo_kai'},\n    {title:'Bike courier, 20 min radius', price:6, cat:'\ud83d\udeb2', type:'sell', peer:'demo_ren'},\n    {title:'Spare basil plant', price:0, cat:'\ud83c\udf81', type:'share', peer:'demo_lu'},\n    {title:'Corner Coffee Shop', price:0, cat:'\ud83c\udfea', type:'business', peer:'demo_cafe'},\n    {title:'Handmade pottery studio', price:0, cat:'\ud83c\udfea', type:'business', peer:'demo_pottery'},\n  ];\n  for(const d of demo){\n    const hex = ring[Math.floor(Math.random()*ring.length)];\n    await db.listings.put({\n      id: uid(), hex, ...d, desc:'Demo listing \u2014 part of the local showcase feed.',\n      ts: Date.now()-Math.floor(Math.random()*3600e3), photo:null\n    });\n  }\n}\n\nasync function refreshListings(){\n  if(!currentHex) return;\n  await seedDemoIfEmpty();\n  const ring = new Set(h3.gridDisk(currentHex,2));\n  const all = await db.listings.toArray();\n  const nearby = all.filter(l=>ring.has(l.hex)).sort((a,b)=>b.ts-a.ts);\n  renderListingsList(nearby);\n  renderListingsOnMap(nearby);\n}\n\nfunction timeAgo(ts){\n  const s=(Date.now()-ts)\/1000;\n  if(s<60) return 'just now';\n  if(s<3600) return Math.floor(s\/60)+'m ago';\n  if(s<86400) return Math.floor(s\/3600)+'h ago';\n  return Math.floor(s\/86400)+'d ago';\n}\n\nfunction renderListingsList(items){\n  const list=$('#listingsList');\n  $('#nearbyTitle').textContent = `Nearby in your hex ring \u00b7 ${items.length}`;\n  if(!items.length){ list.innerHTML='<div class=\"empty\"><div class=\"big\">\u2b21<\/div>No listings in range yet.<br>Be the first to post.<\/div>'; return; }\n  list.innerHTML = items.map(l=>`\n    <div class=\"card\" data-id=\"${l.id}\">\n      <div class=\"thumb\">${l.photo?`<img decoding=\"async\" src=\"${l.photo}\">`:l.cat}<\/div>\n      <div class=\"meta\">\n        <div class=\"t1\">${escapeHtml(l.title)} <span class=\"chip ${l.type}\">${l.type}<\/span><\/div>\n        <div class=\"t2\">${escapeHtml(l.peer)} \u00b7 ${timeAgo(l.ts)} \u00b7 ${l.hex.slice(-6)}<\/div>\n      <\/div>\n      <div class=\"price\">${l.price>0? '$'+l.price : (l.type==='buy'?'':'free')}<\/div>\n    <\/div>`).join('');\n  $$('.card',list).forEach(c=>c.onclick=()=>openDetail(c.dataset.id));\n}\n\nlet listingMarkers = [];\nlet activePopup = null;\n\nfunction closeAllPopups(){\n  if(activePopup){\n    activePopup.classList.remove('show');\n    activePopup = null;\n  }\n}\n\nfunction renderListingsOnMap(items){\n  listingMarkers.forEach(m=>m.remove()); listingMarkers=[];\n  closeAllPopups();\n  \n  items.forEach(l=>{\n    const c = hexCenter(l.hex);\n    const lat = jitter(c.lat,0.0006), lng = jitter(c.lng,0.0006);\n    \n    \/\/ Create compact marker with popup\n    const wrap = document.createElement('div');\n    wrap.className = 'marker-compact';\n    \n    const dot = document.createElement('div');\n    dot.className = 'marker-dot ' + l.type;\n    dot.innerHTML = l.cat;\n    \n    const popup = document.createElement('div');\n    popup.className = 'marker-popup';\n    popup.innerHTML = `\n      <div class=\"popup-header\">\n        <div class=\"popup-avatar\">${(l.peer||'?').replace('demo_','').replace('hx_','').slice(0,1).toUpperCase()}<\/div>\n        <div class=\"popup-name\">${escapeHtml(l.peer.replace('demo_','').replace('hx_',''))}<\/div>\n        <span class=\"popup-type ${l.type}\">${l.type}<\/span>\n      <\/div>\n      <div class=\"popup-title\">${escapeHtml(l.title)}<\/div>\n      <div class=\"popup-meta\">\n        <span>${timeAgo(l.ts)}<\/span>\n        <span class=\"popup-price\">${l.price>0?'$'+l.price:(l.type==='buy'?'offers':'free')}<\/span>\n      <\/div>\n      <button class=\"popup-action\" data-id=\"${l.id}\">View Details<\/button>\n    `;\n    \n    wrap.appendChild(popup);\n    wrap.appendChild(dot);\n    \n    \/\/ Toggle popup on click\n    dot.addEventListener('click', (e)=>{\n      e.stopPropagation();\n      if(activePopup === popup){\n        closeAllPopups();\n      } else {\n        closeAllPopups();\n        popup.classList.add('show');\n        activePopup = popup;\n      }\n    });\n    \n    \/\/ Action button\n    popup.querySelector('.popup-action').addEventListener('click', (e)=>{\n      e.stopPropagation();\n      closeAllPopups();\n      openDetail(l.id);\n    });\n    \n    const m = new maplibregl.Marker({element:wrap, anchor:'bottom'}).setLngLat([lng,lat]).addTo(map);\n    listingMarkers.push(m);\n  });\n}\n\n\/\/ Close popups when clicking map\nmap.on('click', closeAllPopups);\n\nasync function openDetail(id){\n  const l = await db.listings.get(id);\n  if(!l) return;\n  $('#detailModal').innerHTML = `\n    <div class=\"modal-head\"><h2>${escapeHtml(l.title)}<\/h2><button class=\"close-x\" data-close=\"detailOverlay\">\u2715<\/button><\/div>\n    <div class=\"thumb\" style=\"width:100%;height:160px;border-radius:14px;font-size:44px;margin-bottom:12px;\">${l.photo?`<img decoding=\"async\" src=\"${l.photo}\" style=\"width:100%;height:100%;object-fit:cover;border-radius:14px;\">`:l.cat}<\/div>\n    <div style=\"display:flex; gap:8px; margin-bottom:10px;\">\n      <span class=\"chip ${l.type}\">${l.type}<\/span>\n      <span class=\"price\">${l.price>0?'$'+l.price:(l.type==='buy'?'open to offers':'free')}<\/span>\n    <\/div>\n    <p style=\"font-size:13.5px; line-height:1.5; color:var(--text);\">${escapeHtml(l.desc||'No further details.')}<\/p>\n    <p class=\"mono\" style=\"font-size:11px; color:var(--text-dim);\">posted by ${escapeHtml(l.peer)} \u00b7 hex ${l.hex}<\/p>\n    <button class=\"btn-primary\" id=\"msgSellerBtn\">Message ${l.peer.startsWith('demo_')?'(demo)':''}<\/button>\n  `;\n  $('#msgSellerBtn').onclick = ()=>{ closeOverlay('detailOverlay'); openChat(l.peer, l.peer.replace('demo_','').replace('hx_','') || 'Neighbor'); };\n  $$('[data-close]',$('#detailModal')).forEach(b=> b.onclick = ()=> closeOverlay(b.dataset.close));\n  openOverlay('detailOverlay');\n}\n\n\/* =========================================================================\n   Overlay \/ modal plumbing\n   ========================================================================= *\/\nfunction openOverlay(id){ $('#'+id).classList.add('show'); }\nfunction closeOverlay(id){ $('#'+id).classList.remove('show'); }\n$$('.overlay').forEach(o=> o.addEventListener('click', e=>{ if(e.target===o) closeOverlay(o.id); }));\n$$('[data-close]').forEach(b=> b.onclick = ()=> closeOverlay(b.dataset.close));\n\n\/* ---------- Footer Navigation ---------- *\/\nconst footerNav = $('#footerNav');\nconst sheet = $('#sheet');\nlet currentTab = 'nearby';\nlet sheetOpen = false;\n\nfunction setFooterHeight(){\n  document.documentElement.style.setProperty('--footer-h', '72px');\n}\nsetFooterHeight();\n\nfunction setSheetOpen(v){\n  sheetOpen = v;\n  sheet.classList.toggle('open', v);\n}\n\n$$('.nav-item').forEach(item => {\n  item.addEventListener('click', () => {\n    haptic();\n    const tab = item.dataset.tab;\n    \n    \/\/ Update active state\n    $$('.nav-item').forEach(x => x.classList.remove('active'));\n    item.classList.add('active');\n    \n    currentTab = tab;\n    \n    if(tab === 'hexgrid'){\n      openHexGridModal();\n    } else {\n      setSheetOpen(true);\n      renderTab(tab);\n    }\n  });\n});\n\n\/\/ Sheet drag handling\n(function dragSheet(){\n  const handle = $('#sheetHandle');\n  let startY=0, dragging=false;\n  const start=y=>{ dragging=true; startY=y; sheet.style.transition='none'; };\n  const move=y=>{\n    if(!dragging) return;\n    const dy = y-startY;\n    let ty = dy; ty=Math.max(0,Math.min(ty, sheet.offsetHeight-100));\n    sheet.style.transform=`translateY(${ty}px)`;\n  };\n  const end=y=>{\n    if(!dragging) return; dragging=false; sheet.style.transition='';\n    const dy=y-startY;\n    setSheetOpen(dy < -40);\n    sheet.style.transform='';\n  };\n  handle.addEventListener('touchstart',e=>start(e.touches[0].clientY),{passive:true});\n  handle.addEventListener('touchmove',e=>move(e.touches[0].clientY),{passive:true});\n  handle.addEventListener('touchend',e=>end(e.changedTouches[0].clientY));\n  handle.addEventListener('mousedown',e=>{start(e.clientY);\n    const mm=ev=>move(ev.clientY), mu=ev=>{end(ev.clientY); window.removeEventListener('mousemove',mm); window.removeEventListener('mouseup',mu);};\n    window.addEventListener('mousemove',mm); window.addEventListener('mouseup',mu);\n  });\n})();\n\nfunction renderTab(tab){\n  if(tab==='nearby'){ refreshListings(); return; }\n  if(tab==='messages'){ renderMessagesTab(); return; }\n  if(tab==='me'){ renderMeTab(); return; }\n}\n\n\/* =========================================================================\n   Hex Grid Options Modal\n   ========================================================================= *\/\nfunction openHexGridModal(){\n  $('#currentResDisplay').textContent = ME.res;\n  $('#currentSizeDisplay').textContent = RES_META[ME.res];\n  $('#ringRadius').value = ME.hexOpts.ringRadius || 1;\n  $('#ringRadiusVal').textContent = ME.hexOpts.ringRadius || 1;\n  \n  \/\/ Update zoom mode buttons\n  $$('#zoomSeg button').forEach(b => {\n    b.classList.toggle('on', b.dataset.zoom === (ME.hexOpts.zoomMode || 'auto'));\n  });\n  \n  \/\/ Update layer flags\n  $$('#layerFlags .flag-chip').forEach(c => {\n    c.classList.toggle('on', (ME.hexOpts.layers || ['current','ring']).includes(c.dataset.layer));\n  });\n  \n  \/\/ Update style buttons\n  $$('#hexStyleSeg button').forEach(b => {\n    b.classList.toggle('on', b.dataset.style === (ME.hexOpts.style || 'solid'));\n  });\n  \n  openOverlay('hexGridOverlay');\n}\n\n$('#ringRadius').oninput = e => {\n  $('#ringRadiusVal').textContent = e.target.value;\n};\n\n$('#zoomSeg').onclick = e => {\n  const b = e.target.closest('button');\n  if(!b) return;\n  $$('#zoomSeg button').forEach(x => x.classList.remove('on'));\n  b.classList.add('on');\n};\n\n$('#layerFlags').onclick = e => {\n  const c = e.target.closest('.flag-chip');\n  if(!c) return;\n  c.classList.toggle('on');\n};\n\n$('#hexStyleSeg').onclick = e => {\n  const b = e.target.closest('button');\n  if(!b) return;\n  $$('#hexStyleSeg button').forEach(x => x.classList.remove('on'));\n  b.classList.add('on');\n};\n\n$('#saveHexGridBtn').onclick = () => {\n  ME.hexOpts.ringRadius = Number($('#ringRadius').value);\n  ME.hexOpts.zoomMode = $('#zoomSeg .on')?.dataset.zoom || 'auto';\n  ME.hexOpts.layers = $$('#layerFlags .flag-chip.on').map(c => c.dataset.layer);\n  ME.hexOpts.style = $('#hexStyleSeg .on')?.dataset.style || 'solid';\n  \n  zoomLocked = ME.hexOpts.zoomMode === 'locked';\n  \n  saveMe();\n  closeOverlay('hexGridOverlay');\n  renderHexOverlay();\n  toast('Hex grid settings applied');\n};\n\n\/* =========================================================================\n   New listing flow\n   ========================================================================= *\/\n$('#sellBtn').onclick = ()=>{ haptic(); openOverlay('listingOverlay'); };\nlet newType='sell', newCat='\ud83d\udecd\ufe0f', newPhoto=null;\n$('#typeSeg').onclick = e=>{ const b=e.target.closest('button'); if(!b) return; $$('#typeSeg button').forEach(x=>x.classList.remove('on')); b.classList.add('on'); newType=b.dataset.type; };\n$('#catSeg').onclick = e=>{ const b=e.target.closest('button'); if(!b) return; $$('#catSeg button').forEach(x=>x.classList.remove('on')); b.classList.add('on'); newCat=b.dataset.cat; };\n$('#lPhoto').onchange = e=>{\n  const f=e.target.files[0]; if(!f) return;\n  const r=new FileReader(); r.onload=()=> newPhoto=r.result; r.readAsDataURL(f);\n};\n$('#publishBtn').onclick = async ()=>{\n  const title=$('#lTitle').value.trim();\n  if(!title){ toast('Give it a title'); return; }\n  if(!currentHex){ toast('Still finding your hex\u2026'); return; }\n  await db.listings.put({\n    id:uid(), hex:currentHex, type:newType, cat:newCat, title,\n    desc:$('#lDesc').value.trim(), price:Number($('#lPrice').value)||0,\n    peer: ME.id, ts:Date.now(), photo:newPhoto\n  });\n  $('#lTitle').value=''; $('#lDesc').value=''; $('#lPrice').value=''; newPhoto=null; $('#lPhoto').value='';\n  closeOverlay('listingOverlay'); toast('Published to your anchor hex \u2b21');\n  refreshListings();\n};\n\n\/* =========================================================================\n   Layers \/ privacy resolution modal\n   ========================================================================= *\/\n$('#layersBtn').onclick = ()=>{\n  $('#resRange').value=ME.res; $('#resLabel').textContent=ME.res; $('#resSize').textContent=RES_META[ME.res];\n  $$('.flag-chip','#flagRow').forEach(c=> c.classList.toggle('on', (ME.flags & Number(c.dataset.flag))!==0));\n  \n  \/\/ Update hex options display\n  $$('.hex-option').forEach(opt => {\n    const optType = opt.dataset.opt;\n    const optVal = Number(opt.dataset.val);\n    if(optType === 'fill') opt.classList.toggle('active', ME.hexOpts.fill === optVal);\n    if(optType === 'ring') opt.classList.toggle('active', ME.hexOpts.ring === optVal);\n  });\n  \n  $('#zoomLock').checked = ME.hexOpts.zoomLock !== false;\n  \n  openOverlay('layersOverlay');\n};\n\n$$('.hex-option').forEach(opt => {\n  opt.addEventListener('click', () => {\n    const optType = opt.dataset.opt;\n    const optVal = Number(opt.dataset.val);\n    $$(`.hex-option[data-opt=\"${optType}\"]`).forEach(o => o.classList.remove('active'));\n    opt.classList.add('active');\n  });\n});\n\n$('#resRange').oninput = e=>{ $('#resLabel').textContent=e.target.value; $('#resSize').textContent=RES_META[e.target.value]; };\n$('#flagRow').onclick = e=>{ const c=e.target.closest('.flag-chip'); if(!c) return; c.classList.toggle('on'); };\n$('#saveLayersBtn').onclick = ()=>{\n  ME.res = Number($('#resRange').value);\n  let flags=0; $$('.flag-chip.on','#flagRow').forEach(c=> flags |= Number(c.dataset.flag));\n  ME.flags = flags;\n  \n  \/\/ Save hex options\n  ME.hexOpts.fill = Number($('.hex-option[data-opt=\"fill\"].active')?.dataset.val ?? 1);\n  ME.hexOpts.ring = Number($('.hex-option[data-opt=\"ring\"].active')?.dataset.val ?? 1);\n  ME.hexOpts.zoomLock = $('#zoomLock').checked;\n  \n  zoomLocked = ME.hexOpts.zoomLock;\n  \n  saveMe();\n  closeOverlay('layersOverlay');\n  if(lastLatLng) onPosition(lastLatLng.lat,lastLatLng.lng,10,Date.now());\n  toast('Privacy layer updated');\n};\n\n\/* =========================================================================\n   Onboarding\n   ========================================================================= *\/\nif(ME.name){ closeOverlay('onboardOverlay'); } else { $('#onName').focus(); }\n$('#onboardBtn').onclick = ()=>{\n  const n = $('#onName').value.trim();\n  ME.name = n || 'Neighbor'; saveMe();\n  closeOverlay('onboardOverlay');\n  toast(`Welcome, ${ME.name} \ud83d\udc4b`);\n};\n\n\/* =========================================================================\n   Chat \u2014 PeerJS data connections\n   ========================================================================= *\/\nlet peer=null, conns={};\nconst openChats = {};\n\nfunction initPeer(){\n  try{\n    peer = new Peer(ME.id, { debug:0 });\n    peer.on('open', ()=>{});\n    peer.on('connection', conn=>{ conns[conn.peer]=conn; wireConn(conn); });\n    peer.on('call', call=>{ pendingCall = call; answerIncomingCall(call); });\n    peer.on('error', ()=>{});\n  }catch(e){}\n}\ninitPeer();\n\nfunction wireConn(conn){\n  conn.on('data', async data=>{\n    if(data && data.type==='msg'){\n      await storeMessage(conn.peer, data.text, false);\n      if(currentChatPeer===conn.peer) appendMsgBubble(data.text,false);\n      else { bumpUnread(); toast(`New message from ${chatDisplayName(conn.peer)}`); }\n      renderMessagesTab();\n    }\n  });\n}\n\nasync function getOrCreateChat(peerId,name){\n  let c = await db.chats.get(peerId);\n  if(!c){ c={id:peerId, peerId, name, ts:Date.now(), unread:0}; await db.chats.put(c); }\n  else if(name && c.name!==name){ c.name=name; await db.chats.put(c); }\n  return c;\n}\nasync function storeMessage(chatId,text,me){\n  await db.messages.add({chatId, text, me, ts:Date.now()});\n  const c = await db.chats.get(chatId); if(c){ c.ts=Date.now(); c.last=text; if(!me) c.unread=(c.unread||0)+ (currentChatPeer===chatId?0:1); await db.chats.put(c); }\n}\nfunction bumpUnread(){ updateChatBadge(); }\nasync function updateChatBadge(){\n  const chats = await db.chats.toArray();\n  const total = chats.reduce((a,c)=>a+(c.unread||0),0);\n  const b=$('#navChatBadge');\n  const b2=$('#chatBadge');\n  if(total>0){ \n    b.style.display='flex'; b.textContent=total>9?'9+':total;\n    if(b2){ b2.style.display='flex'; b2.textContent=total>9?'9+':total; }\n  } else {\n    b.style.display='none';\n    if(b2) b2.style.display='none';\n  }\n}\nupdateChatBadge();\n\nfunction chatDisplayName(peerId){ return openChats[peerId]?.name || peerId.replace('demo_','').replace('hx_',''); }\n\nlet currentChatPeer=null;\nasync function openChat(peerId,name){\n  currentChatPeer=peerId; openChats[peerId]={name};\n  await getOrCreateChat(peerId,name);\n  const c = await db.chats.get(peerId); c.unread=0; await db.chats.put(c); updateChatBadge();\n\n  $('#chatName').textContent=name;\n  $('#chatAvatar').textContent=(name||'?').slice(0,1).toUpperCase();\n  $('#chatPeerId').textContent=peerId;\n  $('#chatBody').innerHTML='';\n  const msgs = await db.messages.where('chatId').equals(peerId).sortBy('ts');\n  msgs.forEach(m=>appendMsgBubble(m.text,m.me,false));\n  if(!msgs.length) appendMsgBubble(peerId.startsWith('demo_')? 'This is a demo neighbor \u2014 replies are simulated locally.' : \"Say hello \u2014 this connects over WebRTC when they're online.\", null, true);\n  scrollChatBottom();\n  $('#chatScreen').classList.add('show');\n\n  if(!peerId.startsWith('demo_') && peer && !conns[peerId]){\n    try{ const conn=peer.connect(peerId); conns[peerId]=conn; wireConn(conn); }catch(e){}\n  }\n}\nfunction scrollChatBottom(){ const b=$('#chatBody'); b.scrollTop=b.scrollHeight; }\nfunction appendMsgBubble(text,me,sys=false){\n  const d=document.createElement('div');\n  d.className='msg '+(sys?'sys':(me?'me':'them'));\n  d.textContent=text;\n  $('#chatBody').appendChild(d); scrollChatBottom();\n}\n$('#chatBack').onclick = ()=>{ $('#chatScreen').classList.remove('show'); currentChatPeer=null; renderMessagesTab(); };\n\nasync function sendChat(){\n  const input=$('#chatInput'); const text=input.value.trim();\n  if(!text || !currentChatPeer) return;\n  input.value='';\n  appendMsgBubble(text,true);\n  await storeMessage(currentChatPeer,text,true);\n  if(currentChatPeer.startsWith('demo_')){\n    setTimeout(async ()=>{\n      const replies=['Sounds good!','Still available, want to swing by?','Sure \u2014 what hex are you in?','\ud83d\udc4d','Can do in about 20 min.'];\n      const r=replies[Math.floor(Math.random()*replies.length)];\n      appendMsgBubble(r,false); await storeMessage(currentChatPeer,r,false); renderMessagesTab();\n    }, 700+Math.random()*900);\n  } else if(conns[currentChatPeer]){\n    try{ conns[currentChatPeer].send({type:'msg', text}); }catch(e){}\n  }\n  renderMessagesTab();\n}\n$('#chatSend').onclick=sendChat;\n$('#chatInput').addEventListener('keydown', e=>{ if(e.key==='Enter') sendChat(); });\n\nasync function renderMessagesTab(){\n  const chats = (await db.chats.toArray()).sort((a,b)=>b.ts-a.ts);\n  const body = $('#sheetBody');\n  if(currentTab !== 'messages') return;\n  if(!chats.length){ body.innerHTML='<div class=\"sheet-title\">Messages<\/div><div class=\"empty\"><div class=\"big\">\ud83d\udcac<\/div>No conversations yet.<br>Tap a listing to say hello.<\/div>'; return; }\n  body.innerHTML = '<div class=\"sheet-title\">Messages<\/div><div id=\"chatListWrap\"><\/div>';\n  const wrap=$('#chatListWrap');\n  wrap.innerHTML = chats.map(c=>`\n    <div class=\"chatrow\" data-peer=\"${c.peerId}\" data-name=\"${escapeHtml(c.name||chatDisplayName(c.peerId))}\">\n      <div class=\"avatar\">${(c.name||'?').slice(0,1).toUpperCase()}<\/div>\n      <div class=\"meta\">\n        <div class=\"name\">${escapeHtml(c.name||chatDisplayName(c.peerId))} ${c.unread?`<span class=\"badge\" style=\"position:static;display:inline-flex;\">${c.unread}<\/span>`:''}<\/div>\n        <div class=\"last\">${escapeHtml(c.last||'No messages yet')}<\/div>\n      <\/div>\n      <div class=\"dot-online\" style=\"opacity:${c.peerId.startsWith('demo_')?0.3:0.9}\"><\/div>\n    <\/div>`).join('');\n  $$('.chatrow',wrap).forEach(r=> r.onclick=()=> openChat(r.dataset.peer, r.dataset.name));\n}\n\n\/* =========================================================================\n   Me tab\n   ========================================================================= *\/\nasync function renderMeTab(){\n  if(currentTab !== 'me') return;\n  const myListings = await db.listings.where('peer').equals(ME.id).toArray();\n  const flagLabels = [[1,'\ud83d\uded2 Buying'],[2,'\ud83d\udcb0 Selling'],[4,'\ud83c\udf81 Sharing'],[8,'\ud83c\udfea Business'],[16,'\ud83d\udeb4 Delivers']].filter(f=>ME.flags & f[0]).map(f=>f[1]);\n  $('#sheetBody').innerHTML = `\n    <div class=\"sheet-title\">My presence<\/div>\n    <div class=\"card\" style=\"cursor:default;\">\n      <div class=\"thumb\">${(ME.name||'?').slice(0,1).toUpperCase()}<\/div>\n      <div class=\"meta\">\n        <div class=\"t1\">${escapeHtml(ME.name||'Neighbor')}<\/div>\n        <div class=\"t2 mono\">${ME.id}<\/div>\n      <\/div>\n    <\/div>\n    <label>Flags<\/label>\n    <div class=\"flagrow\">${flagLabels.map(f=>`<div class=\"flag-chip on\">${f}<\/div>`).join('')||'<span style=\"font-size:12px;color:var(--text-dim)\">none set<\/span>'}<\/div>\n    <label>Anchor hex (res ${ME.res})<\/label>\n    <div class=\"card mono\" style=\"cursor:default; font-size:12px;\">${currentHex||'locating\u2026'}<\/div>\n    <label>My listings (${myListings.length})<\/label>\n    <div id=\"myListingsList\"><\/div>\n    <button class=\"btn-primary\" id=\"renameBtn\" style=\"background:var(--panel-2); color:var(--text); border:1px solid var(--border);\">Edit profile<\/button>\n  `;\n  const ml=$('#myListingsList');\n  ml.innerHTML = myListings.length ? myListings.map(l=>`\n    <div class=\"card\" data-id=\"${l.id}\">\n      <div class=\"thumb\">${l.photo?`<img decoding=\"async\" src=\"${l.photo}\">`:l.cat}<\/div>\n      <div class=\"meta\"><div class=\"t1\">${escapeHtml(l.title)} <span class=\"chip ${l.type}\">${l.type}<\/span><\/div><div class=\"t2\">${timeAgo(l.ts)}<\/div><\/div>\n      <div class=\"price\">${l.price>0?'$'+l.price:'\u2014'}<\/div>\n    <\/div>`).join('') : '<div class=\"empty\" style=\"padding:20px;\">Nothing posted yet.<\/div>';\n  $$('.card',ml).forEach(c=>c.onclick=()=>openDetail(c.dataset.id));\n  $('#renameBtn').onclick=()=>{ openOverlay('onboardOverlay'); $('#onName').value=ME.name; $('#onboardBtn').textContent='Save'; };\n}\n\n\/* =========================================================================\n   Video calling\n   ========================================================================= *\/\nlet localStream=null, currentCall=null, pendingCall=null, muted=false;\nasync function ensureLocalStream(){\n  if(localStream) return localStream;\n  localStream = await navigator.mediaDevices.getUserMedia({video:{facingMode:'user'}, audio:true});\n  $('#localVideo').srcObject = localStream;\n  return localStream;\n}\n$('#videoCallBtn').onclick = async ()=>{\n  if(!currentChatPeer) return;\n  if(currentChatPeer.startsWith('demo_')){ toast(\"Video isn't available for demo neighbors\"); return; }\n  if(!peer){ toast('P2P layer unavailable right now'); return; }\n  try{\n    const stream = await ensureLocalStream();\n    $('#callScreen').classList.add('show'); $('#callStatus').textContent='Calling '+chatDisplayName(currentChatPeer)+'\u2026';\n    currentCall = peer.call(currentChatPeer, stream);\n    wireCall(currentCall);\n  }catch(e){ toast('Camera \/ mic permission needed for video'); }\n};\nfunction wireCall(call){\n  call.on('stream', remoteStream=>{\n    $('#remoteVideo').srcObject = remoteStream;\n    $('#callStatus').textContent = 'Connected';\n  });\n  call.on('close', endCall);\n  call.on('error', endCall);\n}\nasync function answerIncomingCall(call){\n  try{\n    const stream = await ensureLocalStream();\n    $('#callScreen').classList.add('show'); $('#callStatus').textContent='Incoming call\u2026';\n    call.answer(stream);\n    currentCall=call;\n    wireCall(call);\n  }catch(e){ call.close(); }\n}\nfunction endCall(){\n  if(currentCall){ try{currentCall.close();}catch(e){} currentCall=null; }\n  $('#callScreen').classList.remove('show');\n  $('#callStatus').textContent='';\n}\n$('#endCallBtn').onclick = endCall;\n$('#muteBtn').onclick = ()=>{\n  if(!localStream) return;\n  muted=!muted;\n  localStream.getAudioTracks().forEach(t=> t.enabled=!muted);\n  toast(muted?'Muted':'Unmuted');\n};\n\n\/* ---------- Search bar ---------- *\/\n$('#searchBar').onclick = ()=>{\n  $$('.nav-item').forEach(x => x.classList.remove('active'));\n  $('.nav-item[data-tab=\"nearby\"]').classList.add('active');\n  currentTab = 'nearby';\n  setSheetOpen(true);\n  renderTab('nearby');\n};\n\n\/* initial paint *\/\nconst bootPoll = setInterval(()=>{ if(currentHex){ clearInterval(bootPoll); refreshListings(); } }, 300);\n<\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>HexMarket \u2014 hyperlocal, hex-private HexMarket Nearby, buy \u00b7 sell \u00b7 share anchor hex \u00b7 res 9 locating\u2026 Nearby Messages 0 Hex Grid Me Nearby in your hex ring New listing \u2715 Sell Buy Share Business Title Details Price (leave 0 for free \/ share) Category Goods Food Service Delivery Shop Photo (optional, stays on this [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-108","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages\/108","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/mana-corp.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=108"}],"version-history":[{"count":5,"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages\/108\/revisions"}],"predecessor-version":[{"id":113,"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages\/108\/revisions\/113"}],"wp:attachment":[{"href":"http:\/\/mana-corp.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=108"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}