update blog page issue

This commit is contained in:
2026-06-12 14:55:29 +05:30
parent f412b9f71e
commit ba34c80761
16 changed files with 405 additions and 239 deletions

View File

@@ -6,7 +6,7 @@ import { blogPosts } from "@/data/blog";
/**
* Client-side blog search. The site is a static export, so there is no search
* server — we filter the known posts in the browser and link straight to the
* server. We filter the known posts in the browser and link straight to the
* matching /blog/[slug] routes.
*/
export default function BlogSearch() {

View File

@@ -72,10 +72,10 @@ export default function BlogSidebar({ current }: { current?: BlogPost }) {
{/* CTA Card */}
<section className="dm-blog-widget dm-blog-cta-card">
<h2 className="dm-blog-cta-title">Ready to optimise your fleet?</h2>
<h2 className="dm-blog-cta-title">Planning delivery routes?</h2>
<p className="dm-blog-cta-text">
See how MileTruth AI cuts distance, vehicles and emissions without
missing an SLA.
Talk to us about reducing wasted distance, missed windows, and avoidable
vehicle time.
</p>
<Link href="/contact" className="dm-blog-cta-btn">
Contact Us

View File

@@ -216,14 +216,14 @@ export default function Footer() {
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
</svg>
<span>Social network</span>
<span>Social</span>
</div>
</div>
</div>
<div className="elementor-element elementor-element-a6bccba elementor-shape-square elementor-grid-0 elementor-widget elementor-widget-social-icons" data-id="a6bccba" data-element_type="widget" data-e-type="widget" data-widget_type="social-icons.default">
<div className="elementor-widget-container">
<div className="elementor-social-icons-wrapper elementor-grid" role="list" style={socialIconSpacing}>
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
{/* <span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-facebook-f elementor-repeater-item-3fbe893" href="https://www.facebook.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">Facebook</span>
<svg aria-hidden="true" className="e-font-icon-svg e-fab-facebook-f" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
@@ -238,7 +238,7 @@ export default function Footer() {
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path>
</svg>
</a>
</span>
</span> */}
<span className="elementor-grid-item" role="listitem"style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-linkedin-in elementor-repeater-item-38e1bcc" href="https://www.linkedin.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">LinkedIn</span>
@@ -403,7 +403,7 @@ export default function Footer() {
<div className="elementor-element elementor-element-e4e6486 elementor-shape-square elementor-grid-0 elementor-widget elementor-widget-social-icons" data-id="e4e6486" data-element_type="widget" data-e-type="widget" data-widget_type="social-icons.default">
<div className="elementor-widget-container">
<div className="elementor-social-icons-wrapper elementor-grid" role="list" style={socialIconSpacing}>
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
{/* <span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-facebook-f" href="https://www.facebook.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">Facebook</span>
<svg aria-hidden="true" className="e-font-icon-svg e-fab-facebook-f" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
@@ -418,7 +418,7 @@ export default function Footer() {
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path>
</svg>
</a>
</span>
</span> */}
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-linkedin-in" href="https://www.linkedin.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">LinkedIn</span>

View File

@@ -1,7 +1,6 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import Image from "next/image";
/**
@@ -10,21 +9,20 @@ import Image from "next/image";
* Native reimplementation of the legacy WordPress page-loader: a black
* full-screen overlay with a centered, pulsing Doormile logo that fades out.
*
* Shows on initial load (until the window finishes loading, min ~450ms to avoid
* a flash, capped at 2.5s so it never blocks) and again briefly on each route
* navigation. CWV-safe: fixed/out-of-flow (no layout shift), logo is priority,
* and it never delays hydration.
* Shows only on initial application boot (until the window finishes loading,
* min ~450ms to avoid a flash, capped at 2.5s so it never blocks). It must not
* reappear during client-side route transitions: Next keeps the current page
* visible while the next route payload is prepared, and a global overlay here
* would create an artificial black flash between otherwise-ready pages.
*/
type Phase = "visible" | "hiding" | "gone";
const MIN_SHOW_MS = 450;
const MAX_SHOW_MS = 2500;
const NAV_SHOW_MS = 520;
export default function LoadingScreen() {
const pathname = usePathname();
const [phase, setPhase] = useState<Phase>("visible");
const isFirstRender = useRef(true);
const bootComplete = useRef(false);
// Initial load: hide once the page is ready.
useEffect(() => {
@@ -33,7 +31,7 @@ export default function LoadingScreen() {
let fadeTimer: ReturnType<typeof setTimeout>;
const begin = () => {
if (began) return;
if (began || bootComplete.current) return;
began = true;
const wait = Math.max(0, MIN_SHOW_MS - (performance.now() - start));
fadeTimer = setTimeout(() => setPhase("hiding"), wait);
@@ -52,17 +50,6 @@ export default function LoadingScreen() {
};
}, []);
// Route navigations: flash the loader briefly for an app-like transition.
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
setPhase("visible");
const t = setTimeout(() => setPhase("hiding"), NAV_SHOW_MS);
return () => clearTimeout(t);
}, [pathname]);
if (phase === "gone") return null;
return (
@@ -72,7 +59,10 @@ export default function LoadingScreen() {
aria-live="polite"
aria-label="Loading"
onTransitionEnd={(e) => {
if (e.propertyName === "opacity" && phase === "hiding") setPhase("gone");
if (e.propertyName === "opacity" && phase === "hiding") {
bootComplete.current = true;
setPhase("gone");
}
}}
>
<div className="dm-loader__pulse">

View File

@@ -1,16 +1,11 @@
/* ===========================================================================
Office satellite map — scoped styles.
All Leaflet global classes are namespaced under `.root` via :global() so this
module cannot leak into the rest of the app and vice-versa.
Colours/radii reference the app's theme (dark surface, #C01227 brand red).
The map fills its container 100% — no hard-coded layout values live here.
=========================================================================== */
.root {
position: relative;
width: 100%;
height: 100%;
/* inherit the host container's rounded corners so tiles/controls clip cleanly */
border-radius: inherit;
overflow: hidden;
background: #0b0b0b;
@@ -34,10 +29,6 @@
.controls {
position: absolute;
top: 14px;
/* Span the full width (with side insets) instead of left:50%+auto-width:
an absolutely-positioned auto-width box anchored at left:50% can only grow
to 50% of the map, which on a narrow phone forced the city buttons to stack
vertically. With left/right insets the row uses the full width and centres. */
left: 12px;
right: 12px;
z-index: 600; /* above tiles + markers; popups open lower so they never collide */
@@ -47,6 +38,7 @@
gap: 8px;
pointer-events: none; /* let the row be transparent to drags; buttons re-enable */
}
.controlBtn {
pointer-events: auto;
appearance: none;
@@ -68,15 +60,18 @@
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease,
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
}
.controlBtn:hover {
background: rgba(192, 18, 39, 0.9);
border-color: #c01227;
transform: translateY(-1px);
}
.controlBtn:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}
.controlBtnActive,
.controlBtnActive:hover {
background: #c01227;
@@ -90,6 +85,7 @@
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);
}
@@ -98,8 +94,6 @@
.controls {
top: 12px;
gap: 5px;
/* Keep all three city buttons on a single line — shrink them (below) so the
row fits instead of wrapping to two lines on narrow phones. */
flex-wrap: nowrap;
}
.controlBtn {
@@ -110,12 +104,12 @@
/* ---- Branded marker pin ---- */
.markerIcon {
/* divIcon resets — keep only the pin's own drop shadow */
background: transparent;
border: 0;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.55));
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
.markerIcon:hover,
.markerIcon:focus-visible {
transform: translateY(-3px) scale(1.06);
@@ -125,15 +119,16 @@
.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);
@@ -153,6 +148,7 @@
pointer-events: none;
animation: hqPulse 2.2s ease-out infinite;
}
@keyframes hqPulse {
0% {
transform: scale(0.6);
@@ -168,26 +164,28 @@
}
}
/* ---- Themed Leaflet internals (scoped to this map only) ---- */
/* ---- Leaflet Branded Elements ---- */
.root :global(.leaflet-container) {
background: #0b0b0b;
}
/* Zoom + attribution controls — dark, on-theme */
.root :global(.leaflet-bar) {
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
}
.root :global(.leaflet-bar a) {
background: rgba(15, 15, 17, 0.92);
color: #f5f5f5;
border-bottom-color: rgba(255, 255, 255, 0.12);
transition: background-color 0.2s ease, color 0.2s ease;
}
.root :global(.leaflet-bar a:hover) {
background: #c01227;
color: #ffffff;
}
.root :global(.leaflet-bar a:focus-visible) {
outline: 2px solid #c01227;
outline-offset: 2px;
@@ -202,51 +200,122 @@
border-radius: 6px 0 0 0;
padding: 2px 8px;
}
.root :global(.leaflet-control-attribution a) {
color: rgba(255, 255, 255, 0.85);
}
/* Popup — dark card matching the app surfaces */
.root :global(.leaflet-popup-content-wrapper) {
background: #141416;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
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: 0;
font-size: 13px;
line-height: 1.4;
}
.root :global(.leaflet-popup-tip) {
background: #141416;
border: 1px solid rgba(255, 255, 255, 0.1);
/* ---- Floating Popup overlays ---- */
.root :global(.leaflet-popup) {
animation: popupFade 0.2s ease-out forwards;
}
/* ---- Compact, Google-Maps-style tooltip content ---- */
.tip {
.root :global(.leaflet-popup-content-wrapper) {
background: #0f0f11;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px; /* Clean business card rounded corners */
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
overflow: hidden;
padding: 0;
animation: popupScale 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
transform-origin: bottom center;
}
.root :global(.leaflet-popup-content) {
margin: 0 !important;
width: auto !important;
height: auto !important;
}
.root :global(.leaflet-popup-tip) {
background: #0f0f11;
border: 1px solid rgba(255, 255, 255, 0.08);
}
@keyframes popupFade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes popupScale {
from { transform: scale(0.92); }
to { transform: scale(1); }
}
/* Compact Details Card inside Popup */
.card {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 12px;
min-width: 120px;
max-width: 200px;
text-align: left;
padding: 12px 16px;
min-width: 230px;
max-width: 280px;
box-sizing: border-box;
font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
}
.tipTitle {
font-size: 13px;
font-weight: 800;
letter-spacing: -0.01em;
color: #ffffff;
white-space: nowrap;
border-top: 2px solid #c01227; /* Thinner brand accent line */
background: #0f0f11;
height: auto;
}
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
.cardHeader {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: 6px;
}
.cardIcon {
font-size: 15px;
display: inline-flex;
align-items: center;
}
.cardTitle {
font-size: 17px !important;
font-weight: 800 !important;
color: #ffffff !important; /* Force white header text */
margin: 0 !important;
letter-spacing: -0.01em !important;
}
.cardBody {
display: flex;
flex-direction: column;
gap: 4px;
}
.addressLine {
font-size: 14px !important;
line-height: 1.5 !important;
color: rgba(255, 255, 255, 0.75) !important;
margin: 0 0 6px 0 !important;
padding: 0 !important;
font-weight: 500 !important;
}
.addressLine:last-child {
margin-bottom: 0 !important;
}
@media (max-width: 480px) {
.card {
padding: 9px 12px;
min-width: 200px;
max-width: 230px;
}
.cardTitle {
font-size: 14px !important;
}
.addressLine {
font-size: 12px !important;
margin-bottom: 3px !important;
line-height: 1.4 !important;
}
}
/* ---- Loading skeleton ---- */
.skeleton {
width: 100%;
height: 100%;
@@ -262,10 +331,12 @@
background-size: 200% 100%;
animation: shimmer 1.4s ease-in-out infinite;
}
@keyframes shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
.markerIcon,
@@ -289,12 +360,14 @@
color: rgba(255, 255, 255, 0.82);
font-family: var(--font-manrope), system-ui, sans-serif;
}
.errorTitle {
font-size: clamp(15px, 2.4vw, 18px);
font-weight: 800;
color: #ffffff;
margin: 0;
}
.errorText {
font-size: 13px;
max-width: 38ch;
@@ -302,6 +375,7 @@
line-height: 1.55;
color: rgba(255, 255, 255, 0.6);
}
.errorList {
list-style: none;
margin: 4px 0 0;
@@ -311,6 +385,7 @@
gap: 8px 12px;
justify-content: center;
}
.errorList li {
display: inline-flex;
align-items: center;
@@ -319,6 +394,7 @@
font-weight: 700;
color: #f1f1f1;
}
.errorList li::before {
content: "";
width: 7px;
@@ -327,7 +403,7 @@
background: #c01227;
}
/* ---- Screen-reader-only office list (semantic fallback) ---- */
/* ---- Screen-reader-only office list ---- */
.srOnly {
position: absolute;
width: 1px;

View File

@@ -7,14 +7,9 @@
* 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).
* The map centers with a North latitude offset to keep markers lower in the viewport,
* avoiding collisions with the navigation tabs. Popups are compact and styled like
* clean business cards.
*/
import "leaflet/dist/leaflet.css";
@@ -25,6 +20,7 @@ import L from "leaflet";
import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "react-leaflet";
import {
LatLng,
ESRI_WORLD_IMAGERY,
HQ_OFFICE,
MAP_FOCUS_ZOOM,
@@ -41,7 +37,6 @@ type FocusTarget = { id: string; nonce: number };
/**
* 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) {
@@ -57,7 +52,7 @@ function createMarkerIcon(isHeadquarters: boolean): L.DivIcon {
`,
iconSize: [40, 52],
iconAnchor: [20, 52],
popupAnchor: [0, -46],
popupAnchor: [0, -52], // Anchored to top tip of pin
});
}
@@ -72,15 +67,14 @@ function createMarkerIcon(isHeadquarters: boolean): L.DivIcon {
`,
iconSize: [30, 40],
iconAnchor: [15, 40],
popupAnchor: [0, -36],
popupAnchor: [0, -40], // Anchored to top tip of pin
});
}
/**
* Imperative map effects that need the Leaflet instance:
* - 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
* - snap to the offset HQ on first paint, fly thereafter and open popups.
*/
function MapController({
focus,
@@ -92,8 +86,7 @@ function MapController({
const map = useMap();
const didInit = useRef(false);
// 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.
// Keep the map correctly sized on container resize / lazy reveal.
useEffect(() => {
const container = map.getContainer();
let raf = 0;
@@ -116,16 +109,20 @@ function MapController({
const openPopup = () => markerRefs.current[office.id]?.openPopup();
// Offset the center slightly North so the marker sits lower in the viewport,
// preventing the popup card from colliding with the top controls.
const offsetLatitude = 0.022;
const centeredPosition: LatLng = [office.position[0] + offsetLatitude, office.position[1]];
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.
map.setView(centeredPosition, MAP_FOCUS_ZOOM, { animate: false });
const raf = requestAnimationFrame(openPopup);
return () => cancelAnimationFrame(raf);
}
map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 });
map.flyTo(centeredPosition, MAP_FOCUS_ZOOM, { duration: 1.1 });
map.once("moveend", openPopup);
return () => {
map.off("moveend", openPopup);
@@ -140,12 +137,21 @@ export default function OfficeMap() {
const hqIcon = useMemo(() => createMarkerIcon(true), []);
const markerRefs = useRef<Record<string, L.Marker>>({});
// Default to the headquarters so its button reads active and its popup opens.
// Default to the headquarters.
const [focus, setFocus] = useState<FocusTarget | null>({ id: HQ_OFFICE.id, nonce: 0 });
const focusOffice = useCallback((id: string) => {
setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }));
}, []);
const [hoveredOfficeId, setHoveredOfficeId] = useState<string | null>(null);
// Restore the focused office popup when mouse leaves other markers.
useEffect(() => {
if (hoveredOfficeId === null && focus?.id) {
markerRefs.current[focus.id]?.openPopup();
}
}, [hoveredOfficeId, focus]);
const [status, setStatus] = useState<MapStatus>("loading");
const loadedRef = useRef(false);
const errorCountRef = useRef(0);
@@ -155,8 +161,6 @@ export default function OfficeMap() {
setStatus("ready");
}, []);
// Only surface an error if tiles never render (network/CORS/down). Once any
// tile load succeeds the map is considered healthy and stays that way.
const handleTileError = useCallback(() => {
errorCountRef.current += 1;
if (!loadedRef.current && errorCountRef.current >= 6) setStatus("error");
@@ -223,8 +227,9 @@ export default function OfficeMap() {
/>
{OFFICE_LOCATIONS.map((office) => {
// Google-Maps-style tooltip: hovering shows just the location name;
// clicking focuses the office.
const isFocused = focus?.id === office.id;
const isHovered = hoveredOfficeId === office.id;
return (
<Marker
key={office.id}
@@ -236,9 +241,16 @@ export default function OfficeMap() {
alt={office.name}
eventHandlers={{
click: () => focusOffice(office.id),
// Hover opens the compact popup without moving the map.
mouseover: (event) => event.target.openPopup(),
mouseout: (event) => event.target.closePopup(),
mouseover: (event) => {
setHoveredOfficeId(office.id);
event.target.openPopup();
},
mouseout: (event) => {
setHoveredOfficeId(null);
if (focus?.id !== office.id) {
event.target.closePopup();
}
},
}}
ref={(instance) => {
if (instance) markerRefs.current[office.id] = instance;
@@ -246,16 +258,25 @@ export default function OfficeMap() {
>
<Popup
className={styles.popup}
autoPan={false}
autoPan={isFocused}
autoPanPadding={[25, 25]}
closeButton={false}
minWidth={120}
maxWidth={200}
minWidth={240}
maxWidth={290}
>
<span className={styles.tip}>
<span className={styles.tipTitle}>
<span aria-hidden="true">📍</span> {office.shortLabel}
</span>
</span>
<div className={styles.card}>
<div className={styles.cardHeader}>
<span className={styles.cardIcon} aria-hidden="true">📍</span>
<h4 className={styles.cardTitle}>{office.city} Office</h4>
</div>
<div className={styles.cardBody}>
{office.address.map((line, idx) => (
<p key={idx} className={styles.addressLine}>
{line}
</p>
))}
</div>
</div>
</Popup>
</Marker>
);

View File

@@ -25,6 +25,8 @@ export interface OfficeLocation {
readonly position: LatLng;
/** Headquarters gets the largest, glowing, default-active marker. */
readonly isHeadquarters?: boolean;
/** Full office address lines. */
readonly address: readonly string[];
}
/**
@@ -42,6 +44,12 @@ export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
// Vision Ultima, Jayabheri Enclave, Gachibowli — verified on satellite.
position: [17.4484, 78.3573],
isHeadquarters: true,
address: [
"5th Floor, Vision Ultima,",
"Street No.3, Jayabheri Enclave,",
"Gachibowli, Hyderabad,",
"Telangana 500032"
],
},
{
id: "bengaluru",
@@ -50,6 +58,13 @@ export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
shortLabel: "Bengaluru Hub",
// Resolved from the supplied Google Maps share link — verified on satellite.
position: [12.9929351, 77.6988599],
address: [
"C612, 6th Floor,",
"Trifecta Starlight, ITPL Road,",
"Garudacharapalya, Mahadevapura,",
"Bangalore 560048,",
"Karnataka, India"
],
},
{
id: "coimbatore",
@@ -58,6 +73,12 @@ export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
shortLabel: "Coimbatore Hub",
// Mayflower Valencia, Coimbatore — verified against satellite view.
position: [11.0191, 76.9883],
address: [
"Mayflower Valencia,",
"Near Nava India Bus Stop,",
"Avinashi Road, Udayampalayam,",
"Tamil Nadu 641003"
],
},
];

View File

@@ -82,7 +82,7 @@ export default function BlogGrid() {
font-family: var(--font-manrope), sans-serif !important;
}
/* Bottom block pinned to the card base — keeps Read More + image at the
/* Bottom block pinned to the card base. Keeps Read More + image at the
same vertical position across cards with different text lengths. */
.custom-blog-bottom {
display: flex !important;

View File

@@ -33,6 +33,25 @@ export default function ConnectedLogistics() {
max-width: min(526px, 100%) !important;
}
/* Sizing and identical padding on all 4 sides for Connected Logistics Image container */
.elementor-element-99768ba .elementor-widget-container {
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding: 40px !important; /* Identical gap on left, right, top, and bottom */
box-sizing: border-box !important;
}
.elementor-element-99768ba img.wp-image-4481 {
width: 100% !important;
max-width: 100% !important; /* Allow natural responsive scaling on desktop */
height: auto !important;
object-fit: cover !important;
border-radius: 25px !important; /* Preserve 25px border radius */
margin: 0 auto !important;
display: block !important;
}
/* Desktop/Laptop (min-width: 1025px) column width and flex rules */
@media (min-width: 1025px) {
.elementor-element-9ffed33 {
@@ -78,6 +97,30 @@ export default function ConnectedLogistics() {
width: 100% !important;
max-width: 100% !important;
}
/* Tablet overrides: ~15% smaller than site.css's 450px with centered layout */
.elementor-element-99768ba .elementor-widget-container {
padding: 30px !important;
}
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
width: 100% !important;
max-width: 382px !important;
border-radius: 25px !important; /* Explicitly keep 25px */
}
}
@media (max-width: 767px) {
/* Mobile overrides: ~15% smaller than site.css's 90% with 10% identical padding on all 4 sides */
.elementor-element-99768ba .elementor-widget-container {
padding: 10% !important;
}
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
width: 100% !important;
max-width: 100% !important;
border-radius: 25px !important; /* Explicitly keep 25px */
}
}
`}} />
<div className="elementor-element elementor-element-9ffed33 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9ffed33" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">

View File

@@ -15,7 +15,7 @@ const ROADMAP_DATA = [
trackLeft: "12.5%",
phase: "Pilot Phase",
phaseClass: "yellow",
title: "Hyderabad Pilot",
title: "Pilot",
desc: "Launch operations in Hyderabad with dedicated EV hubs and MileTruth AI v1.0.",
icon: (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@@ -25,7 +25,7 @@ const ROADMAP_DATA = [
</svg>
),
stats: [
{ text: "50-80 orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "100+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "1 city", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 21h18M19 21v-2a4 4 0 0 0-3-3.87M5 21v-2a4 4 0 0 1 3-3.87M9 21v-5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v5"></path></svg> },
{ text: "10+ women partners", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle></svg> }
]
@@ -36,7 +36,7 @@ const ROADMAP_DATA = [
trackLeft: "37.5%",
phase: "Multi-City",
phaseClass: "green",
title: "Multi-City Scale",
title: "Scale",
desc: "Expand to Bengaluru and Chennai, securing key B2B enterprise traction.",
icon: (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
@@ -46,7 +46,7 @@ const ROADMAP_DATA = [
</svg>
),
stats: [
{ text: "300-500 orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "500+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "3 cities", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 21h18M19 21v-2a4 4 0 0 0-3-3.87M5 21v-2a4 4 0 0 1 3-3.87M9 21v-5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v5"></path></svg> },
{ text: "50+ EVs", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> }
]
@@ -57,8 +57,8 @@ const ROADMAP_DATA = [
trackLeft: "62.5%",
phase: "Platform",
phaseClass: "blue",
title: "Platform Expansion",
desc: "Scale to 5+ cities. Launch developer API marketplace and Series A readiness.",
title: "Expansion",
desc: "Scale to 5+ cities. Strengthen regional operations.",
icon: (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="12" cy="12" r="3"></circle>
@@ -66,9 +66,9 @@ const ROADMAP_DATA = [
</svg>
),
stats: [
{ text: "1,200+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "5000+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "5+ cities", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 21h18M19 21v-2a4 4 0 0 0-3-3.87M5 21v-2a4 4 0 0 1 3-3.87M9 21v-5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v5"></path></svg> },
{ text: "API marketplace", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg> }
{ text: "100+ women partners", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg> }
]
},
{
@@ -86,8 +86,8 @@ const ROADMAP_DATA = [
</svg>
),
stats: [
{ text: "5,000+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "Rs 65 Cr+ revenue", icon: <span className="currency-symbol" style={{ marginRight: "4px", fontSize: "11px", fontWeight: 800 }}>Rs</span> },
{ text: "50,000+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
{ text: "50+ cities", icon: <span className="currency-symbol" style={{ marginRight: "4px", fontSize: "11px", fontWeight: 800 }}>Rs</span> },
{ text: "2,000+ women partners", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> }
]
}

View File

@@ -38,7 +38,7 @@ function ContentRenderer({ block }: { block: ContentBlock }) {
return (
<blockquote className="dm-article-quote">
<p>{block.text}</p>
{block.cite && <cite> {block.cite}</cite>}
{block.cite && <cite>{block.cite}</cite>}
</blockquote>
);
case "image":
@@ -168,7 +168,7 @@ const STYLES = `
font-family: var(--font-manrope), sans-serif;
}
/* Heading normalization — beat the global theme's .elementor-kit-5 h1h6
/* Heading normalization. Beat the global theme's .elementor-kit-5 h1-h6
(120/80/60px UPPERCASE) rules with !important on our own classes. */
.dm-single-blog :where(h1, h2, h3, h4, h5, h6) {
font-family: var(--font-manrope), sans-serif !important;
@@ -180,7 +180,7 @@ const STYLES = `
specificity 0-1-1) so blog links keep our colors and never get underlined. */
.dm-single-blog a { text-decoration: none !important; }
/* ── Page banner tall (homepage-scale); only badge + title inside ── */
/* Page banner: tall homepage-scale frame with only badge + title inside */
/* Compound selector (specificity 20) + !important beats the global 800px
single-class height rules so the blog banner can use viewport heights. */
.custom-standard-hero-card.dm-banner-card {
@@ -216,7 +216,7 @@ const STYLES = `
@media (max-width: 1024px) { .dm-banner-title { font-size: clamp(32px, 6vw, 48px) !important; max-width: 90%; } }
@media (max-width: 600px) { .dm-banner-title { font-size: clamp(28px, 8vw, 38px) !important; max-width: 90%; } }
/* ── Content wrap begins immediately below the banner ── */
/* Content wrap begins immediately below the banner */
/* Shared content container: the SAME max-width + horizontal padding is used
by BlogPostFooter (.dm-blog-footer-inner) so the article body, headings,
images, Prev/Next, Related Articles and the CTA banner all align to one
@@ -287,7 +287,7 @@ const STYLES = `
}
/* Each article block is wrapped in its OWN ScrollReveal <div>, so a bare
:first-child rule matched every heading (each is the only child of its
wrapper) and zeroed its top margin collapsing the gap above every
wrapper) and zeroed its top margin, collapsing the gap above every
section heading. Scope the reset to only the article body's first block. */
.dm-article-body > :first-child :where(.dm-article-h2, .dm-article-h3),
.dm-article-body > .dm-article-h2:first-child,