-
-
-
-
@@ -491,6 +564,69 @@ export default function Header() {
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `
+ /* ── Off-canvas menu: full-height flex column ──
+ Header (logo) at top, scrollable content in the middle, and the
+ "Get in touch" CTA pinned at the bottom — so the panel stays
+ usable however much content (e.g. multiple office addresses) it
+ holds. Scoped to #side-panel-2f31137; no other sidebar is touched.
+ Width, colors, the slide-in animation, and open/close behaviour
+ (driven by the .active class on the wrapper) are all unchanged. */
+ #side-panel-2f31137 .slide-sidebar {
+ display: flex !important;
+ flex-direction: column !important;
+ height: 100% !important;
+ padding: 0 !important; /* the three sections own their padding */
+ overflow: hidden !important; /* scrolling lives on the content area */
+ }
+ /* Fit the *visible* viewport. The panel height is calc(100vh - 20px),
+ but on mobile 100vh is the larger, URL-bar-inclusive height, which
+ pushed the bottom of the scroll list + the CTA below the fold and
+ made scrolling appear broken. dvh tracks the actually-visible area;
+ vh is kept as a fallback for older browsers. */
+ #side-panel-2f31137.slide-sidebar-wrapper {
+ height: calc(100vh - 20px);
+ height: calc(100dvh - 20px) !important;
+ }
+ #side-panel-2f31137 .slide-sidebar-header {
+ flex: 0 0 auto;
+ padding: 52px 36px 18px; /* top clears the floating close button */
+ text-align: center; /* centre the logo */
+ }
+ #side-panel-2f31137 .slide-sidebar-header figure {
+ margin: 0;
+ text-align: center;
+ }
+ #side-panel-2f31137 .slide-sidebar-header img {
+ display: inline-block; /* centred by the header's text-align */
+ }
+ #side-panel-2f31137 .slide-sidebar-content {
+ flex: 1 1 auto;
+ min-height: 0; /* let the flex child shrink so it can scroll */
+ overflow-y: auto;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch; /* momentum scroll on iOS */
+ overscroll-behavior: contain; /* don't chain scroll to the page */
+ padding: 18px 60px 8px; /* top gap below the logo header */
+ }
+ #side-panel-2f31137 .slide-sidebar-content:focus-visible {
+ outline: none; /* container is scroll-focusable, not a control */
+ }
+ #side-panel-2f31137 .slide-sidebar-cta {
+ flex: 0 0 auto;
+ padding: 16px 60px 36px;
+ }
+ /* Compact, readable address blocks (tighter line + lead-in than the
+ default 1.75em body spacing). */
+ #side-panel-2f31137 .slide-sidebar-content p {
+ line-height: 1.5;
+ margin-top: 4px;
+ }
+ /* Larger social icons — the logos-only block style renders them at 18px. */
+ #side-panel-2f31137 .wp-block-social-links.is-style-logos-only .wp-block-social-link a svg {
+ width: 26px !important;
+ height: 26px !important;
+ }
+
#masthead .elementor-element.elementor-element-466de1b {
position: absolute !important;
top: 5px !important;
diff --git a/src/components/map/OfficeMap.module.css b/src/components/map/OfficeMap.module.css
index 03d7de4..3716f64 100644
--- a/src/components/map/OfficeMap.module.css
+++ b/src/components/map/OfficeMap.module.css
@@ -85,6 +85,15 @@
box-shadow: 0 6px 18px rgba(192, 18, 39, 0.45);
}
+/* Headquarters button — subtly elevated so it reads as the primary location. */
+.controlBtnHq {
+ border-color: rgba(192, 18, 39, 0.55);
+ box-shadow: 0 0 0 1px rgba(192, 18, 39, 0.25), 0 4px 14px rgba(192, 18, 39, 0.2);
+}
+.controlBtnHq.controlBtnActive {
+ box-shadow: 0 6px 20px rgba(192, 18, 39, 0.55);
+}
+
@media (max-width: 480px) {
.controls {
top: 12px;
@@ -112,6 +121,53 @@
transform: translateY(-3px) scale(1.06);
}
+/* ---- Headquarters pin — larger, glowing, pulsing, always on top ---- */
+.markerIconHq {
+ background: transparent;
+ border: 0;
+ /* red glow + grounding shadow */
+ filter: drop-shadow(0 0 9px rgba(192, 18, 39, 0.85))
+ drop-shadow(0 5px 7px rgba(0, 0, 0, 0.55));
+ transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
+}
+.markerIconHq svg {
+ position: relative;
+ z-index: 2;
+}
+.markerIconHq:hover,
+.markerIconHq:focus-visible {
+ transform: translateY(-3px) scale(1.05);
+}
+
+/* Soft expanding ring radiating from the HQ pin head. */
+.pinPulse {
+ position: absolute;
+ top: 19px;
+ left: 20px;
+ width: 18px;
+ height: 18px;
+ margin: -9px 0 0 -9px;
+ border-radius: 50%;
+ background: rgba(192, 18, 39, 0.5);
+ z-index: 1;
+ pointer-events: none;
+ animation: hqPulse 2.2s ease-out infinite;
+}
+@keyframes hqPulse {
+ 0% {
+ transform: scale(0.6);
+ opacity: 0.75;
+ }
+ 70% {
+ transform: scale(3);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(3);
+ opacity: 0;
+ }
+}
+
/* ---- Themed Leaflet internals (scoped to this map only) ---- */
.root :global(.leaflet-container) {
background: #0b0b0b;
@@ -158,34 +214,36 @@
border-radius: 12px;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
}
+.root :global(.leaflet-popup-content-wrapper) {
+ border-radius: 10px;
+}
.root :global(.leaflet-popup-content) {
- margin: 12px 16px;
- font-size: 14px;
- font-weight: 700;
- letter-spacing: -0.01em;
- line-height: 1.3;
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.4;
}
.root :global(.leaflet-popup-tip) {
background: #141416;
border: 1px solid rgba(255, 255, 255, 0.1);
}
-.root :global(.leaflet-popup-close-button) {
- color: rgba(255, 255, 255, 0.6);
+
+/* ---- Compact, Google-Maps-style tooltip content ---- */
+.tip {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: 8px 12px;
+ min-width: 120px;
+ max-width: 200px;
+ text-align: left;
+ font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
}
-.root :global(.leaflet-popup-close-button:hover) {
+.tipTitle {
+ font-size: 13px;
+ font-weight: 800;
+ letter-spacing: -0.01em;
color: #ffffff;
-}
-.root :global(.leaflet-popup-content .office-popup__name) {
- display: block;
-}
-.root :global(.leaflet-popup-content .office-popup__dot) {
- display: inline-block;
- width: 7px;
- height: 7px;
- margin-right: 8px;
- border-radius: 50%;
- background: #c01227;
- vertical-align: middle;
+ white-space: nowrap;
}
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
@@ -210,7 +268,9 @@
}
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
- .markerIcon { transition: none; }
+ .markerIcon,
+ .markerIconHq { transition: none; }
+ .pinPulse { animation: none; opacity: 0.45; }
}
/* ---- Graceful error fallback ---- */
diff --git a/src/components/map/OfficeMap.tsx b/src/components/map/OfficeMap.tsx
index 72b6978..f6e824d 100644
--- a/src/components/map/OfficeMap.tsx
+++ b/src/components/map/OfficeMap.tsx
@@ -7,6 +7,11 @@
* Doormile office markers, plus a row of "jump to office" buttons that fly the
* map to a selected office's coordinates and open its popup.
*
+ * The experience opens focused on the Hyderabad headquarters — the largest,
+ * glowing, pulsing marker — with its popup open by default, so the command
+ * centre of the network is immediately legible. Hovering any marker opens its
+ * popup; leaving closes it. Clicking a marker or a nav button flies to it.
+ *
* Loaded via a `ssr:false` dynamic import so Leaflet (which touches `window`)
* never runs on the server and cannot cause hydration mismatches. Layout/spacing
* is owned by the host container (see ContactMap).
@@ -21,13 +26,11 @@ import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "rea
import {
ESRI_WORLD_IMAGERY,
- MAP_FIT_MAX_ZOOM,
- MAP_FIT_PADDING,
+ HQ_OFFICE,
MAP_FOCUS_ZOOM,
MAP_INITIAL_CENTER,
MAP_INITIAL_ZOOM,
OFFICE_LOCATIONS,
- type LatLng,
} from "./offices";
type MapStatus = "loading" | "ready" | "error";
@@ -35,8 +38,29 @@ type MapStatus = "loading" | "ready" | "error";
/** A request to focus a specific office. `nonce` lets the same office be re-selected. */
type FocusTarget = { id: string; nonce: number };
-/** Build the branded SVG pin once (module scope is fine — this file is client-only). */
-function createMarkerIcon(): L.DivIcon {
+/**
+ * Build a branded SVG pin. The headquarters pin is larger, carries a red glow
+ * and a soft pulse ring, and sits above every other marker.
+ * (Module-scope-safe construction — this file is client-only.)
+ */
+function createMarkerIcon(isHeadquarters: boolean): L.DivIcon {
+ if (isHeadquarters) {
+ return L.divIcon({
+ className: styles.markerIconHq,
+ html: `
+
+
+ `,
+ iconSize: [40, 52],
+ iconAnchor: [20, 52],
+ popupAnchor: [0, -46],
+ });
+ }
+
return L.divIcon({
className: styles.markerIcon,
html: `
@@ -54,77 +78,54 @@ function createMarkerIcon(): L.DivIcon {
/**
* Imperative map effects that need the Leaflet instance:
- * - fit the viewport to every marker (with edge padding)
- * - keep that framing correct across resizes / lazy reveals
+ * - keep the viewport sized correctly across resizes / lazy reveals
+ * - on first paint, snap to the HQ and open its popup (no jarring long fly)
* - fly to a single office when one is selected via the buttons
*/
function MapController({
- positions,
focus,
markerRefs,
}: {
- positions: LatLng[];
focus: FocusTarget | null;
markerRefs: React.RefObject
>;
}) {
const map = useMap();
+ const didInit = useRef(false);
- // Latest focus, read inside the (stable) resize handler without resubscribing.
- const focusRef = useRef(focus);
- useEffect(() => {
- focusRef.current = focus;
- }, [focus]);
-
- const fitAll = useCallback(() => {
- if (positions.length === 0) return;
- map.invalidateSize();
- map.fitBounds(L.latLngBounds(positions), {
- padding: MAP_FIT_PADDING,
- maxZoom: MAP_FIT_MAX_ZOOM,
- });
- }, [map, positions]);
-
- // Initial fit, after the container has its final size.
- useEffect(() => {
- const raf = requestAnimationFrame(fitAll);
- return () => cancelAnimationFrame(raf);
- }, [fitAll]);
-
- // Re-measure on container resize; only re-frame all markers when nothing is
- // focused, so resizing while zoomed into one office doesn't jump the view away.
+ // Keep the map correctly sized on container resize / lazy reveal. We never
+ // re-frame the view here, so resizing keeps whatever office is in focus.
useEffect(() => {
const container = map.getContainer();
let raf = 0;
const observer = new ResizeObserver(() => {
cancelAnimationFrame(raf);
- raf = requestAnimationFrame(() => {
- map.invalidateSize();
- if (!focusRef.current) {
- map.fitBounds(L.latLngBounds(positions), {
- padding: MAP_FIT_PADDING,
- maxZoom: MAP_FIT_MAX_ZOOM,
- });
- }
- });
+ raf = requestAnimationFrame(() => map.invalidateSize());
});
observer.observe(container);
return () => {
cancelAnimationFrame(raf);
observer.disconnect();
};
- }, [map, positions]);
+ }, [map]);
- // Fly to the selected office, then open its popup once movement settles.
+ // React to the focused office: snap on first paint, fly thereafter.
useEffect(() => {
if (!focus) return;
const office = OFFICE_LOCATIONS.find((item) => item.id === focus.id);
if (!office) return;
- map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 });
+ const openPopup = () => markerRefs.current[office.id]?.openPopup();
- const marker = markerRefs.current[office.id];
- if (!marker) return;
- const openPopup = () => marker.openPopup();
+ if (!didInit.current) {
+ didInit.current = true;
+ map.invalidateSize();
+ map.setView(office.position, MAP_FOCUS_ZOOM, { animate: false });
+ // Open the HQ popup once the marker has mounted on this frame.
+ const raf = requestAnimationFrame(openPopup);
+ return () => cancelAnimationFrame(raf);
+ }
+
+ map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 });
map.once("moveend", openPopup);
return () => {
map.off("moveend", openPopup);
@@ -135,14 +136,12 @@ function MapController({
}
export default function OfficeMap() {
- const icon = useMemo(() => createMarkerIcon(), []);
- const positions = useMemo(
- () => OFFICE_LOCATIONS.map((office) => office.position),
- [],
- );
+ const icon = useMemo(() => createMarkerIcon(false), []);
+ const hqIcon = useMemo(() => createMarkerIcon(true), []);
const markerRefs = useRef>({});
- const [focus, setFocus] = useState(null);
+ // Default to the headquarters so its button reads active and its popup opens.
+ const [focus, setFocus] = useState({ id: HQ_OFFICE.id, nonce: 0 });
const focusOffice = useCallback((id: string) => {
setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }));
}, []);
@@ -189,7 +188,9 @@ export default function OfficeMap() {