{"id":103,"date":"2026-08-12T15:10:36","date_gmt":"2026-08-13T01:10:36","guid":{"rendered":"https:\/\/mana-corp.com\/?page_id=103"},"modified":"2026-08-12T15:41:55","modified_gmt":"2026-08-13T01:41:55","slug":"hexv1","status":"publish","type":"page","link":"http:\/\/mana-corp.com\/?page_id=103","title":{"rendered":"Hex v1"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"103\" class=\"elementor elementor-103\">\n\t\t\t\t<div class=\"elementor-element elementor-element-e37bc2e e-flex e-con-boxed e-con e-parent\" data-id=\"e37bc2e\" 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-c4d8dd1 elementor-widget elementor-widget-html\" data-id=\"c4d8dd1\" 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.googleapis.com\/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap\" 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 (mapcn-style: rounded panel, theme-aware) ---------- *\/\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; } \/* we use our own modal, never native popups *\/\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(--sheet-h, 200px) + 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(--sheet-h, 200px) + 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.pin{\n  width:30px;height:30px;border-radius:50% 50% 50% 0; transform:rotate(-45deg);\n  background:var(--panel); border:2px solid var(--accent);\n  display:flex; align-items:center; justify-content:center; cursor:pointer;\n  box-shadow:0 4px 12px rgba(0,0,0,.25);\n}\n.pin span{transform:rotate(45deg); font-size:14px;}\n.pin.buy{border-color:var(--accent-2);}\n.pin.share{border-color:#7CA8FF;}\n\n\/* ---------- bottom sheet ---------- *\/\n.sheet{\n  position:fixed; left:0; right:0; bottom:0; z-index:490;\n  background:var(--panel); border-radius:22px 22px 0 0;\n  box-shadow:var(--sheet-shadow);\n  transform:translateY(calc(100% - 178px));\n  transition:transform 320ms cubic-bezier(.2,.8,.2,1);\n  touch-action:none;\n  padding-bottom:env(safe-area-inset-bottom,0px);\n  max-height:88vh; 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.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\/* ---------- 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  <div class=\"icon-btn\" id=\"chatBtn\" title=\"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 class=\"badge\" id=\"chatBadge\" style=\"display:none;\">0<\/span>\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<!-- ============ BOTTOM SHEET ============ -->\n<div class=\"sheet\" id=\"sheet\">\n  <div class=\"handle-wrap\" id=\"sheetHandle\"><div class=\"handle\"><\/div><\/div>\n  <div class=\"sheet-tabs\">\n    <div class=\"tab active\" data-tab=\"nearby\">Nearby<\/div>\n    <div class=\"tab\" data-tab=\"messages\">Messages<\/div>\n    <div class=\"tab\" data-tab=\"me\">Me<\/div>\n  <\/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    <\/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    <\/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    <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=\"16\">\ud83d\udeb4 Can deliver<\/div>\n    <\/div>\n    <button class=\"btn-primary\" id=\"saveLayersBtn\">Save<\/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   Map: MapLibre GL (vector, \"mapcn\"-style: CARTO GL styles, theme-aware,\n        fullscreen toggle, native nav controls) \u2014 no Leaflet anywhere.\n   Privacy: H3 hexgrid \u00b7 Smoothing: Kalman filter\n   Storage: IndexedDB (Dexie) \u00b7 Realtime: PeerJS (WebRTC data + media)\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 };\n  localStorage.setItem('hx_me', JSON.stringify(ME));\n}\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 (constant-velocity model)\n   Reduces jitter from raw geolocation before it ever touches a hex index.\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, CARTO vector styles (mapcn's own basemap\n   source), auto light\/dark switching, native fullscreen + nav controls.\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\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});\nmap.addControl(new maplibregl.NavigationControl({showCompass:true, visualizePitch:false}), 'top-right');\n\n\/\/ Native fullscreen control (mapcn\/mapbox-style toggle) \u2014 plus our own\n\/\/ toolbar button drives the same Fullscreen API for a reachable thumb target.\nconst fsControl = new maplibregl.FullscreenControl();\nmap.addControl(fsControl, '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]); \/\/ hex overlay is re-added by the style.load handler below\n}\n$('#themeBtn').onclick = ()=>{ haptic(); setTheme(theme==='dark'?'light':'dark'); };\n\n\/* ---------- hex overlay (GeoJSON sources, re-attached on every style load) ---------- *\/\nconst ACCENT = '#17E3A6';\nfunction emptyFC(){ return {type:'FeatureCollection', features:[]}; }\nfunction hexPolygonFeature(hexId){\n  const ring = h3.cellToBoundary(hexId, true); \/\/ [[lng,lat], ...] GeoJSON order\n  ring.push(ring[0]); \/\/ close the ring\n  return {type:'Feature', properties:{}, geometry:{type:'Polygon', coordinates:[ring]}};\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.08}});\n    map.addLayer({id:'hex-current-line', type:'line', source:'hex-current', paint:{'line-color':ACCENT, 'line-width':2, 'line-dasharray':[2,2]}});\n  }\n  if(!map.getSource('hex-ring')){\n    map.addSource('hex-ring', {type:'geojson', data: emptyFC()});\n    map.addLayer({id:'hex-ring-line', type:'line', source:'hex-ring', paint:{'line-color':'#8B979A', 'line-width':1, 'line-opacity':0.35}});\n  }\n}\nfunction renderHexOverlay(){\n  if(!currentHex || !map.isStyleLoaded()) return;\n  ensureHexLayers();\n  map.getSource('hex-current').setData({type:'FeatureCollection', features:[hexPolygonFeature(currentHex)]});\n  const neighbors = h3.gridDisk(currentHex,1).filter(h=>h!==currentHex).map(hexPolygonFeature);\n  map.getSource('hex-ring').setData({type:'FeatureCollection', features:neighbors});\n}\nmap.on('style.load', renderHexOverlay); \/\/ fires on initial load AND every setStyle()\n\n\/* ---------- self marker + smooth (lerp) 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    map.jumpTo({center:[f.lng, f.lat], zoom:16});\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){ map.flyTo({center:[lastLatLng.lng,lastLatLng.lat], zoom:16, duration:600}); } };\n\n\/* ---------- fullscreen toggle (our own thumb-reachable button, mirrors the native control) ---------- *\/\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 \u2014 stored locally, placed at hex centroid (never exact GPS)\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  ];\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 = [];\nfunction renderListingsOnMap(items){\n  listingMarkers.forEach(m=>m.remove()); listingMarkers=[];\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    const el = document.createElement('div');\n    el.className = 'pin '+l.type;\n    el.innerHTML = `<span>${l.cat}<\/span>`;\n    el.addEventListener('click', (e)=>{ e.stopPropagation(); openDetail(l.id); });\n    const m = new maplibregl.Marker({element:el, anchor:'bottom'}).setLngLat([lng,lat]).addTo(map);\n    listingMarkers.push(m);\n  });\n}\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\/* ---------- bottom sheet drag + tabs ---------- *\/\nconst sheet = $('#sheet');\nlet sheetOpen=false;\nfunction setSheetOpen(v){\n  sheetOpen=v; sheet.classList.toggle('open', v);\n  document.documentElement.style.setProperty('--sheet-h', v? Math.min(window.innerHeight*0.72,560)+'px' : '178px');\n}\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    const base = sheetOpen? 0 : sheet.offsetHeight-178;\n    let ty = base+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(sheetOpen? dy<60 : 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  handle.addEventListener('click', ()=> setSheetOpen(!sheetOpen));\n})();\nsetSheetOpen(false);\n\n$$('.tab').forEach(t=> t.onclick = ()=>{\n  $$('.tab').forEach(x=>x.classList.remove('active')); t.classList.add('active');\n  setSheetOpen(true);\n  renderTab(t.dataset.tab);\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   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').forEach(c=> c.classList.toggle('on', (ME.flags & Number(c.dataset.flag))!==0));\n  openOverlay('layersOverlay');\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').forEach(c=> flags |= Number(c.dataset.flag));\n  ME.flags = flags; 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, persisted in IndexedDB\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', ()=>{ \/* offline \/ broker unreachable \u2014 chat still works locally *\/ });\n  }catch(e){ \/* PeerJS unavailable \u2014 app still fully usable offline *\/ }\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=$('#chatBadge');\n  if(total>0){ b.style.display='flex'; b.textContent=total>9?'9+':total; } else b.style.display='none';\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$('#chatBtn').onclick = ()=>{ haptic(); setSheetOpen(true); $$('.tab').forEach(x=>x.classList.remove('active')); $('.tab[data-tab=\"messages\"]').classList.add('active'); renderTab('messages'); };\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($('.tab.active').dataset.tab!=='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($('.tab.active').dataset.tab!=='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'],[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 \u2014 PeerJS media call\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 \u2014 invite a real friend and share your Peer ID.\"); 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\/* =========================================================================\n   Search bar \u2192 simple listing title filter (surfaces sheet)\n   ========================================================================= *\/\n$('#searchBar').onclick = ()=>{\n  setSheetOpen(true); $$('.tab').forEach(x=>x.classList.remove('active')); $('.tab[data-tab=\"nearby\"]').classList.add('active');\n  renderTab('nearby');\n};\n\n\/* keep --sheet-h in sync so HUD\/toolbar float above the collapsed sheet *\/\nwindow.addEventListener('resize', ()=> setSheetOpen(sheetOpen));\n\n\/* initial paint once geolocation resolves the first hex *\/\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 0 Nearby Messages Me Nearby in your hex ring New listing \u2715 Sell Buy Share Title Details Price (leave 0 for free \/ share) Category Goods Food Service Delivery Photo (optional, stays on this device) Publish to my [&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-103","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages\/103","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=103"}],"version-history":[{"count":5,"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages\/103\/revisions"}],"predecessor-version":[{"id":114,"href":"http:\/\/mana-corp.com\/index.php?rest_route=\/wp\/v2\/pages\/103\/revisions\/114"}],"wp:attachment":[{"href":"http:\/\/mana-corp.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=103"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}