diff --git a/viz/board.html b/viz/board.html
index f1d869c8f7..816369b63c 100644
--- a/viz/board.html
+++ b/viz/board.html
@@ -28,10 +28,13 @@
.lens:hover{color:var(--ink); border-color:var(--statel)}
.lens[aria-pressed="true"]{color:var(--ground); background:var(--teal); border-color:var(--teal); font-weight:600}
.lens:focus-visible{outline:2px solid var(--teal); outline-offset:2px}
- .mapwrap{position:absolute; inset:132px 8px 34px}
+ .mapwrap{position:absolute; inset:132px 8px 34px; cursor:grab; touch-action:none}
+ .mapwrap.grab{cursor:grabbing}
+ .maphint{position:absolute; left:10px; bottom:8px; font-size:10px; letter-spacing:.04em;
+ color:#5c6b7a; opacity:.6; pointer-events:none; user-select:none}
svg{width:100%; height:100%; display:block}
- .county{stroke:#0a0f16; stroke-width:.15; transition:opacity .5s}
- .stateline{fill:none; stroke:var(--statel); stroke-width:.5; opacity:.55; pointer-events:none}
+ .county{stroke:#0a0f16; stroke-width:.15; vector-effect:non-scaling-stroke; transition:opacity .5s}
+ .stateline{fill:none; stroke:var(--statel); stroke-width:.5; vector-effect:non-scaling-stroke; opacity:.55; pointer-events:none}
.county.hot{stroke:var(--ink); stroke-width:.7}
.booting .county{opacity:0}
@media(prefers-reduced-motion:reduce){ .county{transition:none} .booting .county{opacity:1} }
@@ -109,7 +112,7 @@
Republic OS · the board
Every county in America, and what it's made of.
-
+ scroll to zoom · drag to pan · double-click to reset
@@ -198,10 +201,43 @@ function render(){
html+=``;
});
STATES.features.forEach(f=>{ html+=``; });
- map.innerHTML=html;
+ map.innerHTML=''+html+''; mapg=document.getElementById("mapg"); applyTransform();
}
function recolor(){ map.querySelectorAll(".county").forEach(p=>p.setAttribute("fill",colorFor(p.dataset.f))); }
+// ---- zoom / pan (transform on ; strokes are non-scaling) ----
+let mapg=null, zk=1, ztx=0, zty=0;
+const ZMIN=1, ZMAX=16;
+function clampPan(){ ztx=Math.min(0,Math.max((1-zk)*W, ztx)); zty=Math.min(0,Math.max((1-zk)*H, zty)); }
+function applyTransform(){ if(!mapg)return; clampPan();
+ mapg.setAttribute("transform",`translate(${ztx.toFixed(2)} ${zty.toFixed(2)}) scale(${zk.toFixed(4)})`); }
+function vbPoint(e){ const pt=map.createSVGPoint(); pt.x=e.clientX; pt.y=e.clientY;
+ const m=map.getScreenCTM(); return m?pt.matrixTransform(m.inverse()):{x:0,y:0}; }
+map.addEventListener("wheel",e=>{
+ e.preventDefault();
+ const v=vbPoint(e), old=zk;
+ zk=Math.min(ZMAX,Math.max(ZMIN, zk*(e.deltaY<0?1.18:1/1.18)));
+ if(zk===old)return;
+ ztx=v.x-(v.x-ztx)*(zk/old); zty=v.y-(v.y-zty)*(zk/old);
+ applyTransform();
+},{passive:false});
+const wrap=document.querySelector(".mapwrap");
+let dragging=false, dragLast=null, dragStart=null, dragMoved=false;
+map.addEventListener("pointerdown",e=>{
+ dragging=true; dragMoved=false; dragStart=dragLast=vbPoint(e);
+ wrap.classList.add("grab"); try{map.setPointerCapture(e.pointerId);}catch(_){}
+});
+map.addEventListener("pointermove",e=>{
+ if(!dragging)return;
+ const v=vbPoint(e); ztx+=v.x-dragLast.x; zty+=v.y-dragLast.y; dragLast=v;
+ if(Math.hypot(v.x-dragStart.x, v.y-dragStart.y)>3) dragMoved=true;
+ applyTransform();
+});
+function endDrag(e){ dragging=false; wrap.classList.remove("grab"); try{map.releasePointerCapture(e.pointerId);}catch(_){} }
+map.addEventListener("pointerup",endDrag);
+map.addEventListener("pointercancel",endDrag);
+map.addEventListener("dblclick",e=>{ e.preventDefault(); zk=1; ztx=0; zty=0; applyTransform(); });
+
// ---- readout ----
const readout=document.getElementById("readout");
function show(fips){
@@ -259,7 +295,7 @@ function closeModal(){ modal.hidden=true; }
document.getElementById("close").onclick=closeModal;
document.getElementById("backdrop").onclick=closeModal;
document.addEventListener("keydown",e=>{ if(e.key==="Escape"&&!modal.hidden)closeModal(); });
-map.addEventListener("click",e=>{ const t=e.target; if(t.dataset&&t.dataset.f)openDetail(t.dataset.f); });
+map.addEventListener("click",e=>{ const t=e.target; if(dragMoved||e.detail>1)return; if(t.dataset&&t.dataset.f)openDetail(t.dataset.f); });
// ---- legend ----
function renderLegend(){