-
Watching
-
-
-
☑ Kim / CNN
— Blah blah blah
-
-
-
+function closeCenteredOverlay(map) {
+ try {
+ const ov = map?.__swCenteredOverlay;
+ if (!ov) return;
+
+ if (ov.onKey) window.removeEventListener("keydown", ov.onKey);
+ try { if (ov.unmountFull) ov.unmountFull(); } catch {}
+ try { if (ov.unmountMini) ov.unmountMini(); } catch {}
+ try { ov.backdrop?.remove?.(); } catch {}
+
+ map.__swCenteredOverlay = null;
+ } catch {}
+}
+
+function openCenteredOverlay({ map, post, theme }) {
+ if (!map) return;
+
+ closeCenteredOverlay(map);
+
+ const data = adaptPostToTemplateData(post);
+ const cat = normCatLabel(post);
+ const relTime = formatRelativeTime(post?.created_at || post?.CreatedAt || post?.createdAt);
+ const author = (post?.author || post?.Author || "").toString().trim();
+ const sub = (post?.sub_category || post?.subCategory || "").toString().trim();
+ const metaLeft = author ? `by ${author}` : "";
+ const metaRight = sub ? sub : "";
+ const img = data?.image || "";
+ const url = data?.url || "";
+
+ // Backdrop catches outside click to close
+ const backdrop = document.createElement("div");
+ backdrop.style.position = "fixed";
+ backdrop.style.inset = "0";
+ backdrop.style.zIndex = "9999999";
+ backdrop.style.background = "rgba(0,0,0,0.08)";
+ backdrop.style.backdropFilter = "blur(1px)";
+ backdrop.style.display = "flex";
+ backdrop.style.alignItems = "center";
+ backdrop.style.justifyContent = "center";
+ backdrop.style.padding = "12px";
+ backdrop.style.pointerEvents = "auto";
+
+ // Wrapper uses existing styling hooks
+ const modalWrap = document.createElement("div");
+ modalWrap.className = "post-pin--expanded";
+ modalWrap.style.pointerEvents = "auto";
+
+ // Keep size behavior from your CSS (max-width/min(92vw,...), max-height etc.)
+ modalWrap.innerHTML = `
+
+
+
+
${escapeHtml(cat)}
+
+ ${metaLeft ? `${escapeHtml(metaLeft)}` : ""}
+ ${metaRight ? `• ${escapeHtml(metaRight)}` : ""}
+ • ${escapeHtml(relTime)}
-
-
-
The news here (generated)
-
+
+
-
+
+ ${
+ img
+ ? `
})
`
+ : `
`
+ }
+
-
-
-
+
+
+
+
+
`;
- // helper close fn
- root.__swClose = () => {
- try {
- if (root.__swUnmountFull) root.__swUnmountFull();
- } catch {}
- try {
- if (root.__swMoveHandler && root.__swMap) {
- root.__swMap.off("move", root.__swMoveHandler);
- root.__swMap.off("zoom", root.__swMoveHandler);
- }
- } catch {}
- try {
- root.remove();
- } catch {}
+ backdrop.appendChild(modalWrap);
+ document.body.appendChild(backdrop);
+
+ // outside click closes
+ backdrop.addEventListener("click", () => closeCenteredOverlay(map));
+ // inside click doesn't close
+ modalWrap.addEventListener("click", (e) => e.stopPropagation());
+
+ // close button
+ const xbtn = modalWrap.querySelector(".sw-modal-x");
+ if (xbtn) xbtn.addEventListener("click", () => closeCenteredOverlay(map));
+
+ // ESC closes
+ const onKey = (e) => {
+ if (e.key === "Escape") closeCenteredOverlay(map);
};
+ window.addEventListener("keydown", onKey);
- container.appendChild(root);
- return root;
-}
+ // CTA open url
+ const cta = modalWrap.querySelector(".sw-cta-primary");
+ if (cta) {
+ cta.addEventListener("click", () => {
+ const u = cta.getAttribute("data-url") || "";
+ if (!u) return;
+ try { window.open(u, "_blank", "noopener,noreferrer"); } catch {}
+ });
+ }
-function updateOverlayPointer(map, overlayRoot, lngLat) {
- if (!map || !overlayRoot || !lngLat) return;
+ // still mount template full (hidden) so you can switch back later instantly
+ let unmountFull = null;
+ const fullWrap = modalWrap.querySelector(".sw-template-full-wrap");
+ if (fullWrap) {
+ const spec = getTemplateSpecForPost(post, "full");
+ unmountFull = mountCard(fullWrap, spec, data, theme);
+ }
- const p = map.project(lngLat);
- const pointer = overlayRoot.querySelector(".sw-overlay-pointer");
- const line = overlayRoot.querySelector(".sw-overlay-line");
- const cardWrap = overlayRoot.querySelector(".sw-expanded-overlay-card");
- const card = overlayRoot.querySelector(".sw-expanded-overlay-card .post-card");
-
- if (!pointer || !line || !cardWrap || !card) return;
-
- // pointer follows marker
- pointer.style.left = `${p.x}px`;
- pointer.style.top = `${p.y}px`;
-
- // simple line from marker to bottom center of the card
- const contRect = map.getContainer().getBoundingClientRect();
- const cardRect = cardWrap.getBoundingClientRect();
-
- const x1 = p.x;
- const y1 = p.y;
-
- const x2 = (cardRect.left - contRect.left) + cardRect.width / 2;
- const y2 = (cardRect.top - contRect.top) + cardRect.height; // bottom of card
-
- const dx = x2 - x1;
- const dy = y2 - y1;
- const len = Math.sqrt(dx * dx + dy * dy);
-
- // place a thin line
- line.style.left = `${x1}px`;
- line.style.top = `${y1}px`;
- line.style.width = `${len}px`;
- line.style.transform = `rotate(${Math.atan2(dy, dx)}rad)`;
-}
-
-function minimalPanToKeepMarkerVisible(map, overlayRoot, lngLat) {
- if (!map || !overlayRoot || !lngLat) return;
-
- const p = map.project(lngLat);
- const cont = map.getContainer();
- if (!cont) return;
-
- const contRect = cont.getBoundingClientRect();
- const cardWrap = overlayRoot.querySelector(".sw-expanded-overlay-card");
- if (!cardWrap) return;
-
- const cardRect = cardWrap.getBoundingClientRect();
-
- const topPad = 18;
- const bottomSafe = (cardRect.top - contRect.top) - 18;
-
- let dy = 0;
-
- // marker too high -> bring it down a bit
- if (p.y < topPad) dy = (topPad - p.y);
-
- // marker behind the card -> bring it up a bit
- if (p.y > bottomSafe) dy = (bottomSafe - p.y);
-
- // clamp so it never "vent dalle trop loin"
- dy = Math.max(-160, Math.min(160, dy));
-
- if (Math.abs(dy) < 2) return;
-
- try {
- const until = Date.now() + 2000;
- map.__swIgnoreMapClickUntil = until;
- map.__swIgnoreCloseUntil = until;
- map.__swIgnoreFetchUntil = until;
- map.__swIgnoreRebuildUntil = until;
- } catch {}
-
- map.panBy([0, dy], { duration: 350 });
+ map.__swCenteredOverlay = {
+ postId: post?.id,
+ backdrop,
+ onKey,
+ unmountFull,
+ unmountMini: null,
+ };
}
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
@@ -293,10 +266,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
? post.lng
: null;
- if (lat == null || lon == null) {
- console.warn("post sans coordonnée:", post);
- return;
- }
+ if (lat == null || lon == null) return;
const lngLat = [lon, lat];
@@ -306,9 +276,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
function unmountIfAny() {
if (root.__swUnmount) {
- try {
- root.__swUnmount();
- } catch {}
+ try { root.__swUnmount(); } catch {}
root.__swUnmount = null;
}
}
@@ -333,88 +301,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
clearOcclusion(markersRef);
}
- function closeExpandedOverlayIfAny() {
- const cur = expandedElRef.current;
- if (cur && cur.__swClose) {
- try {
- cur.__swClose();
- } catch {}
- }
- expandedElRef.current = null;
- clearOcclusion(markersRef);
- }
-
- function renderExpandedOverlay() {
- // locks so fetch/rebuild doesn't close while opening
- try {
- const until = Date.now() + 2000;
- map.__swIgnoreMapClickUntil = until;
- map.__swIgnoreCloseUntil = until;
- map.__swIgnoreFetchUntil = until;
- map.__swIgnoreRebuildUntil = until;
- } catch {}
-
- closeExpandedOverlayIfAny();
-
- const overlayRoot = getOrCreateExpandedOverlay(map);
- if (!overlayRoot) return;
-
- // fill headline
- const headline = escapeHtml(post?.title || "Untitled");
- const titleEl = overlayRoot.querySelector(".sw-news-title");
- if (titleEl) titleEl.innerHTML = headline;
-
- // mount template full into the left area
- try {
- if (overlayRoot.__swUnmountFull) overlayRoot.__swUnmountFull();
- } catch {}
- overlayRoot.__swUnmountFull = null;
-
- const wrap = overlayRoot.querySelector(".sw-template-full-wrap");
- if (wrap) {
- wrap.innerHTML = ""; // ensure clean
- const spec = getTemplateSpecForPost(post, "full");
- const data = adaptPostToTemplateData(post);
- overlayRoot.__swUnmountFull = mountCard(wrap, spec, data, theme);
- }
-
- // pointer updater
- const moveHandler = () => {
- try {
- updateOverlayPointer(map, overlayRoot, lngLat);
- applyOcclusionForExpanded(markersRef, overlayRoot);
- } catch {}
- };
-
- overlayRoot.__swMap = map;
- overlayRoot.__swMoveHandler = moveHandler;
-
- map.on("move", moveHandler);
- map.on("zoom", moveHandler);
-
- // initial layout after paint
- requestAnimationFrame(() => {
- try {
- // 1) update pointer/line
- updateOverlayPointer(map, overlayRoot, lngLat);
-
- // 2) minimal pan so marker isn't hidden behind the fixed card
- minimalPanToKeepMarkerVisible(map, overlayRoot, lngLat);
-
- // 3) occlusion now + after moveend
- applyOcclusionForExpanded(markersRef, overlayRoot);
- map.once("moveend", () => {
- try {
- updateOverlayPointer(map, overlayRoot, lngLat);
- applyOcclusionForExpanded(markersRef, overlayRoot);
- } catch {}
- });
- } catch {}
- });
-
- expandedElRef.current = overlayRoot;
- }
-
renderCompact();
root.__renderCompact = renderCompact;
@@ -427,17 +313,13 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
root.addEventListener("click", (ev) => {
ev.stopPropagation();
- // toggle: if overlay already open for this post, close it
- const cur = expandedElRef.current;
- if (cur && cur.__swForId && cur.__swForId === (post.id ?? `${lon},${lat}`)) {
- closeExpandedOverlayIfAny();
+ const existing = map.__swCenteredOverlay;
+ if (existing && existing.postId === post?.id) {
+ closeCenteredOverlay(map);
return;
}
- // open overlay
- renderExpandedOverlay();
- if (expandedElRef.current) {
- expandedElRef.current.__swForId = post.id ?? `${lon},${lat}`;
- }
+ openCenteredOverlay({ map, post, theme });
+ if (expandedElRef) expandedElRef.current = null;
});
}
diff --git a/src/components/Map/templateSpecs.js b/src/components/Map/templateSpecs.js
index 6cad01f..efd562a 100644
--- a/src/components/Map/templateSpecs.js
+++ b/src/components/Map/templateSpecs.js
@@ -7,7 +7,7 @@
export const TEMPLATE_SPECS = {
news: {
mini_spec: {
- size: { w: 240, h: 96 }, /* ✅ smaller to avoid layout push */
+ size: { w: 240, h: 96 },
background: { type: "gradient", value: "newsBlue" },
radius: 18,
layers: [
@@ -16,23 +16,16 @@ export const TEMPLATE_SPECS = {
{ id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
],
},
+
+ // ✅ FULL: remove useless empty top (no hero image block), keep SAME overall size
full_spec: {
- size: { w: 360, h: 520 },
+ size: { w: 360, h: 260 },
background: { type: "solid", value: "surface" },
radius: 22,
layers: [
- { id: "hero", type: "image", x: 0, y: 0, w: 360, h: 220, bind: "data.image" },
- { id: "badge", type: "chip", x: 16, y: 16, text: "NEWS", style: "chip.news" },
- { id: "title", type: "text", x: 16, y: 240, w: 328, h: 78, bind: "data.headline", style: "text.h1", maxLines: 3 },
- { id: "summary", type: "text", x: 16, y: 325, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 5 },
- {
- id: "cta",
- type: "button",
- x: 16, y: 450, w: 328, h: 48,
- text: "Open source",
- style: "button.primary",
- action: { type: "open_url", bind: "data.url" }
- }
+ { id: "badge", type: "chip", x: 16, y: 12, text: "NEWS", style: "chip.news" },
+ { id: "title", type: "text", x: 16, y: 52, w: 328, h: 72, bind: "data.headline", style: "text.h1", maxLines: 3 },
+ { id: "summary", type: "text", x: 16, y: 132, w: 328, h: 78, bind: "data.summary", style: "text.body", maxLines: 4 }
],
},
},
diff --git a/src/components/Map/useMapCore.js b/src/components/Map/useMapCore.js
index 546292e..7bfe4e6 100644
--- a/src/components/Map/useMapCore.js
+++ b/src/components/Map/useMapCore.js
@@ -105,21 +105,9 @@ export function useMapCore(theme) {
map.on("zoom", reOcclude);
// ignore close while auto actions are happening
- map.on("click", () => {
- try {
- const until = map.__swIgnoreMapClickUntil || 0;
- if (until && Date.now() < until) return;
- } catch {}
- closeExpandedIfAny();
- });
+ map.on("click", () => { closeExpandedIfAny(); });
- map.on("dragstart", () => {
- try {
- const until = map.__swIgnoreCloseUntil || 0;
- if (until && Date.now() < until) return;
- } catch {}
- closeExpandedIfAny();
- });
+ map.on("dragstart", () => { closeExpandedIfAny(); });
map.on("load", () => setViewParams(getViewFromMap(map)));
diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js
index 6b69fed..b63b226 100644
--- a/src/components/Map/usePostsEngine.js
+++ b/src/components/Map/usePostsEngine.js
@@ -22,21 +22,13 @@ export function usePostsEngine({
const [loadingPosts, setLoadingPosts] = useState(false);
const [loadError, setLoadError] = useState("");
- const isExpandedOpen = () => {
- try {
- return !!expandedElRef?.current;
- } catch {
- return false;
- }
- };
-
const rebuildMarkersForFilters = useCallback(
(tf) => {
const map = mapRef.current;
if (!map) return;
- // ✅ If expanded overlay/popup is open: NEVER rebuild markers (it closes the expanded)
- if (isExpandedOpen()) return;
+ // ✅ If expanded popup is open, NEVER rebuild markers (rebuild would close it)
+ if (expandedElRef?.current) return;
clearAllMarkers(markersRef, expandedElRef);
@@ -62,7 +54,6 @@ export function usePostsEngine({
);
useEffect(() => {
- // ✅ Sociowall list can still update via visiblePosts; markers rebuild is blocked while expanded open.
rebuildMarkersForFilters(timeFilter);
}, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
@@ -74,13 +65,26 @@ export function usePostsEngine({
let cancelled = false;
async function load() {
- // ✅ If expanded is open: delay fetch indefinitely until user closes it
- if (isExpandedOpen()) {
+ const mapObj = mapRef.current;
+
+ // ✅ If expanded popup is open: delay fetch (do NOT clear/rebuild while user reads)
+ if (expandedElRef?.current) {
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
- delayedFetchRef.current = setTimeout(load, 1200);
+ delayedFetchRef.current = setTimeout(load, 2500);
return;
}
+ // Also respect ignore window (during auto-pan)
+ try {
+ const until = mapObj?.__swIgnoreFetchUntil || 0;
+ if (until && Date.now() < until) {
+ const wait = Math.min(4000, until - Date.now());
+ if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
+ delayedFetchRef.current = setTimeout(load, wait + 25);
+ return;
+ }
+ } catch {}
+
const [lng, lat] = viewParams.center;
const radiusKm = viewParams.radiusKm || 750;
const filterKey = `${mainFilter}|${subFilter}`;
@@ -93,6 +97,7 @@ export function usePostsEngine({
const lastR = last.radiusKm || 0;
const radiusChanged = !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25;
+
const minMoveKm = Math.max(50, radiusKm * 0.25);
if (!radiusChanged && distKm < minMoveKm) return;
@@ -132,9 +137,30 @@ export function usePostsEngine({
allPostsRef.current = Array.from(byId.values());
lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
- // ✅ rebuild markers only if expanded is NOT open (double-safety)
- if (!isExpandedOpen()) rebuildMarkersForFilters(timeFilter);
+ // If expanded popup is open, don't rebuild markers (keeps it open)
+ try {
+ if (expandedElRef?.current) {
+ const posts = allPostsRef.current || [];
+ const catCode2 = categoryCode(mainFilter);
+ const visible2 = posts.filter((p) => {
+ if (catCode2) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode2) return false;
+ }
+ if (!matchesSubFilter(p, subFilter)) return false;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return false;
+ return true;
+ });
+
+ setVisiblePosts(visible2);
+ setStatus(visible2.length ? "" : "No posts found.");
+ setLoadingPosts(false);
+ return;
+ }
+ } catch {}
+
+ rebuildMarkersForFilters(timeFilter);
setLoadingPosts(false);
} catch (err) {
console.error("Erreur chargement posts:", err);
@@ -150,13 +176,11 @@ export function usePostsEngine({
return () => {
cancelled = true;
if (delayedFetchRef.current) {
- try {
- clearTimeout(delayedFetchRef.current);
- } catch {}
+ try { clearTimeout(delayedFetchRef.current); } catch {}
delayedFetchRef.current = null;
}
};
- }, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter]);
+ }, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter, expandedElRef]);
const handleIncomingPost = useCallback(
(p) => {
diff --git a/src/styles/map.css b/src/styles/map.css
index 1e88db9..c8a310e 100644
--- a/src/styles/map.css
+++ b/src/styles/map.css
@@ -1,4 +1,5 @@
:root{
+ --sw-subcats-nudge: -35px; /* subcats vertical nudge */
--sw-below-overlap: 40px; /* wall overlaps map a bit */
}
diff --git a/src/styles/mapMarkers.css b/src/styles/mapMarkers.css
index 70cdc9f..391ea0e 100644
--- a/src/styles/mapMarkers.css
+++ b/src/styles/mapMarkers.css
@@ -247,3 +247,263 @@
display:none;
}
}
+
+
+/* SW_FIX_FULL_WRAP_COLLAPSE:BEGIN */
+/* Fix: expanded template wrapper collapsing into a thin line */
+.post-pin--expanded .post-card.sw-expanded-shell{
+ width: 360px !important;
+ min-width: 360px !important;
+ max-width: 360px !important;
+ height: 520px !important;
+ max-height: 520px !important;
+ overflow: hidden !important; /* no useless scroll */
+}
+
+.sw-template-full-wrap{
+ display: block !important;
+ width: 360px !important;
+ height: 520px !important;
+}
+
+.sw-template-full-wrap > *{
+ display: block !important;
+}
+/* SW_FIX_FULL_WRAP_COLLAPSE:END */
+
+
+
+/* ===== Expanded header (mock) ===== */
+.sw-expanded-badge{
+ display:inline-flex;
+ align-items:center;
+ justify-content:center;
+ padding: 6px 10px;
+ border-radius: 999px;
+ font-weight: 900;
+ font-size: 12px;
+ background: rgba(56,189,248,0.18);
+ color: #D7F3FF;
+ width: fit-content;
+}
+
+.sw-expanded-title{
+ margin-top: 10px;
+ font-size: 26px;
+ font-weight: 900;
+ line-height: 1.05;
+ color: #F0F7FF;
+}
+
+.sw-expanded-snippet{
+ margin-top: 8px;
+ font-size: 14px;
+ font-weight: 700;
+ color: #B5C7E6;
+}
+
+/* =========================================================
+ SW_EXPANDED_POLISH (visual only)
+ - make expanded look like the mini gradients / modern UI
+ ========================================================= */
+.post-pin--expanded .post-card{
+ background: linear-gradient(135deg, rgba(56,189,248,0.16), rgba(7,18,37,0.98) 55%, rgba(2,6,23,0.98));
+ border: 1px solid rgba(96,165,250,0.75);
+ box-shadow: 0 18px 60px rgba(0,0,0,0.55), 0 0 24px rgba(56,189,248,0.22);
+}
+
+.sw-expanded-badge{
+ display:inline-flex;
+ align-items:center;
+ justify-content:center;
+ padding: 7px 12px;
+ border-radius: 999px;
+ font-weight: 900;
+ font-size: 12px;
+ letter-spacing: .04em;
+ color: #d7f3ff;
+ background: rgba(56,189,248,0.16);
+ border: 1px solid rgba(56,189,248,0.35);
+ margin-bottom: 10px;
+}
+
+.sw-expanded-title{
+ font-size: 28px;
+ font-weight: 950;
+ line-height: 1.05;
+ color: #f0f7ff;
+ margin-bottom: 8px;
+}
+
+.sw-expanded-meta{
+ font-size: 12px;
+ font-weight: 700;
+ color: rgba(181,199,230,0.95);
+ margin-bottom: 10px;
+}
+
+.sw-expanded-snippet{
+ font-size: 14px;
+ line-height: 1.35;
+ color: rgba(215,233,255,0.95);
+ background: rgba(3,14,32,0.45);
+ border: 1px solid rgba(90,190,255,0.22);
+ border-radius: 14px;
+ padding: 10px 12px;
+}
+
+.post-card-btn{
+ background: rgba(56,189,248,0.14);
+ border: 1px solid rgba(56,189,248,0.35);
+ color: #e8f5ff;
+ font-weight: 900;
+ padding: 6px 12px;
+}
+
+.post-card-btn:active{
+ transform: scale(0.98);
+}
+
+/* ===== CENTER MODAL CONTENT (pretty + consistent) ===== */
+.sw-modal{
+ padding: 12px 14px;
+}
+
+.sw-modal-toprow{
+ display:flex;
+ align-items:flex-start;
+ justify-content:space-between;
+ gap:10px;
+ margin-bottom: 10px;
+}
+
+.sw-modal-badge{
+ display:inline-flex;
+ align-items:center;
+ justify-content:center;
+ padding: 6px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 900;
+ letter-spacing: .06em;
+ background: rgba(56,189,248,0.16);
+ border: 1px solid rgba(90, 190, 255, 0.28);
+ color:#D7F3FF;
+ text-transform: uppercase;
+}
+
+.sw-modal-meta{
+ margin-top: 6px;
+ font-size: 11px;
+ color:#9eb7ff;
+ opacity:.95;
+ display:flex;
+ gap:6px;
+ flex-wrap:wrap;
+}
+
+.sw-modal-x{
+ border:none;
+ border-radius:999px;
+ width:34px;
+ height:34px;
+ background: rgba(3,14,32,0.55);
+ color:#e8f5ff;
+ font-weight:900;
+ cursor:pointer;
+ border:1px solid rgba(90,190,255,0.25);
+ flex-shrink:0;
+}
+
+.sw-modal-hero{
+ width:100%;
+ height: 180px;
+ border-radius: 16px;
+ overflow:hidden;
+ border: 1px solid rgba(90, 190, 255, 0.22);
+ background: radial-gradient(circle at 30% 30%, rgba(56,189,248,0.18), rgba(3,14,32,0.85));
+ box-shadow: 0 10px 24px rgba(0,0,0,0.35);
+ margin-bottom: 10px;
+}
+
+.sw-modal-hero img{
+ width:100%;
+ height:100%;
+ object-fit: cover;
+ display:block;
+}
+
+.sw-modal-hero-fallback{
+ width:100%;
+ height:100%;
+ background: radial-gradient(circle at 35% 35%, rgba(56,189,248,0.22), rgba(3,14,32,0.85));
+}
+
+.sw-modal-title{
+ font-size: 22px;
+ font-weight: 900;
+ line-height: 26px;
+ color:#F0F7FF;
+ margin: 6px 0 8px;
+}
+
+.sw-modal-summary{
+ font-size: 13px;
+ line-height: 17px;
+ color:#D7E9FF;
+ opacity:.95;
+ margin-bottom: 8px;
+}
+
+.sw-stat-row{
+ display:flex;
+ gap:8px;
+ flex-wrap:wrap;
+ margin: 6px 0 10px;
+}
+
+.sw-stat-pill{
+ font-size: 11px;
+ color:#c7e3ff;
+ background: rgba(3, 14, 32, 0.58);
+ border: 1px solid rgba(90, 190, 255, 0.22);
+ padding: 6px 10px;
+ border-radius: 999px;
+}
+
+.sw-modal-cta-row{
+ display:flex;
+ flex-direction:column;
+ gap:10px;
+ margin-bottom: 10px;
+}
+
+.sw-cta-primary{
+ width:100%;
+ border:none;
+ border-radius: 14px;
+ padding: 14px 12px;
+ font-weight: 900;
+ font-size: 14px;
+ color:#e8f5ff;
+ cursor:pointer;
+ background: rgba(56,189,248,0.22);
+ box-shadow: 0 10px 18px rgba(0,0,0,0.35);
+}
+
+.sw-cta-primary:disabled{
+ opacity:.55;
+ cursor:default;
+}
+
+.sw-cta-actions{
+ display:flex;
+ gap:8px;
+ flex-wrap:wrap;
+}
+
+/* mobile tighten */
+@media (max-width: 640px){
+ .sw-modal-hero{ height: 150px; }
+ .sw-modal-title{ font-size: 20px; line-height: 24px; }
+}
diff --git a/src/styles/overlays.css b/src/styles/overlays.css
index b910a33..61bc6f6 100644
--- a/src/styles/overlays.css
+++ b/src/styles/overlays.css
@@ -15,14 +15,9 @@
display:flex; flex-direction:column; gap:.5rem;
}
-/* ✅ sub-cats: keep above wall overlap + moved DOWN by 10px more */
-.map-overlay-bottom{
- bottom: calc(2.6rem + var(--sw-below-overlap, 0px) + env(safe-area-inset-bottom) - 20px);
- left:50%;
- transform:translateX(-50%);
- display:flex;
- gap:.4rem;
-}
+/* ✅ Sub-categories row MUST stay above the Sociowall overlap */
+/* ✅ DESCEND 25px (was 0 / 10 / 20): */
+.map-overlay-bottom { bottom: calc(3.2rem + var(--sw-below-overlap, 0px) + env(safe-area-inset-bottom) + var(--sw-subcats-nudge, 0px)); left:50%; transform:translateX(-50%); display:flex; gap:.4rem; }
.map-overlay-myloc { right:.7rem; bottom:6rem; }