update how it works card update
This commit is contained in:
BIN
public/images/mile-1.png
Normal file
BIN
public/images/mile-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 MiB |
@@ -134,7 +134,7 @@ export default function Footer() {
|
|||||||
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z" />
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Call Center</span>
|
<span>Contect</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +152,7 @@ export default function Footer() {
|
|||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||||
<circle cx="12" cy="10" r="3" />
|
<circle cx="12" cy="10" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Our Location</span>
|
<span>Address</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,23 +142,26 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="slide-sidebar-close" onClick={closeAll}></div>
|
<div className="slide-sidebar-close" onClick={closeAll}></div>
|
||||||
<div className="slide-sidebar">
|
<div className="slide-sidebar">
|
||||||
<div className="slide-sidebar-content">
|
{/* Header — does not scroll. */}
|
||||||
|
<div className="slide-sidebar-header">
|
||||||
|
<figure className="wp-block-image size-full is-resized">
|
||||||
|
<Image
|
||||||
|
width={305}
|
||||||
|
height={58}
|
||||||
|
src="/images/doormile-logo.png"
|
||||||
|
alt="Doormile logo"
|
||||||
|
className="wp-image-5851"
|
||||||
|
style={{ width: "210px", height: "auto" }}
|
||||||
|
sizes="(max-width: 305px) 100vw, 305px"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable content — tabIndex makes it keyboard-scrollable. */}
|
||||||
|
<div className="slide-sidebar-content" tabIndex={0} role="region" aria-label="Menu content">
|
||||||
<div id="block-37" className="widget widget_block">
|
<div id="block-37" className="widget widget_block">
|
||||||
<div className="widget-wrapper">
|
<div className="widget-wrapper">
|
||||||
<div className="dm-block-group is-layout-constrained dm-block-group-is-layout-constrained">
|
<div className="dm-block-group is-layout-constrained dm-block-group-is-layout-constrained">
|
||||||
<figure className="wp-block-image size-full is-resized">
|
|
||||||
<Image
|
|
||||||
width={305}
|
|
||||||
height={58}
|
|
||||||
src="/images/doormile-logo.png"
|
|
||||||
alt="Doormile logo"
|
|
||||||
className="wp-image-5851"
|
|
||||||
style={{ width: "150px", height: "auto" }}
|
|
||||||
sizes="(max-width: 305px) 100vw, 305px"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<div style={{ height: "46px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
|
||||||
|
|
||||||
<div className="wp-block-title">
|
<div className="wp-block-title">
|
||||||
<h6
|
<h6
|
||||||
@@ -171,11 +174,87 @@ export default function Header() {
|
|||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Our Location
|
Address
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>5th Floor, Vision Ultima, Street No.3, Jayabheri Enclave, Gachibowli, Hyderabad, Telangana 500032.</p>
|
<h6
|
||||||
|
className="wp-block-heading has-text-font-font-family"
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontStyle: "normal",
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: "0px",
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hyderabad
|
||||||
|
</h6>
|
||||||
|
<p>
|
||||||
|
5th Floor, Vision Ultima,
|
||||||
|
<br />
|
||||||
|
Street No.3, Jayabheri Enclave,
|
||||||
|
<br />
|
||||||
|
Gachibowli, Hyderabad,
|
||||||
|
<br />
|
||||||
|
Telangana 500032.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ height: "12px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||||
|
|
||||||
|
<h6
|
||||||
|
className="wp-block-heading has-text-font-font-family"
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontStyle: "normal",
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: "0px",
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Coimbatore
|
||||||
|
</h6>
|
||||||
|
<p>
|
||||||
|
Mayflower Valencia,
|
||||||
|
<br />
|
||||||
|
Near Nava India Bus Stop,
|
||||||
|
<br />
|
||||||
|
Avinashi Road,
|
||||||
|
<br />
|
||||||
|
Udayampalayam,
|
||||||
|
<br />
|
||||||
|
Tamil Nadu 641037.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ height: "12px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||||
|
|
||||||
|
<h6
|
||||||
|
className="wp-block-heading has-text-font-font-family"
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontStyle: "normal",
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: "0px",
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bengaluru
|
||||||
|
</h6>
|
||||||
|
<p>
|
||||||
|
C612, 6th Floor,
|
||||||
|
<br />
|
||||||
|
Trifecta Starlight,
|
||||||
|
<br />
|
||||||
|
ITPL Road,
|
||||||
|
<br />
|
||||||
|
Garudacharapalya,
|
||||||
|
<br />
|
||||||
|
Mahadevapura,
|
||||||
|
<br />
|
||||||
|
Bangalore 560048,
|
||||||
|
<br />
|
||||||
|
Karnataka, India.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div style={{ height: "3px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
<div style={{ height: "3px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||||
|
|
||||||
@@ -272,19 +351,21 @@ export default function Header() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div style={{ height: "137px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
|
||||||
|
|
||||||
<div className="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
|
|
||||||
<div className="wp-block-button is-style-simple is-style-theme">
|
|
||||||
<Link href="/contact" className="wp-block-button__link wp-element-button" style={{ borderRadius: "10px" }}>
|
|
||||||
Get in touch
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* CTA — pinned at the bottom; never scrolls away. */}
|
||||||
|
<div className="slide-sidebar-cta">
|
||||||
|
<div className="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
|
||||||
|
<div className="wp-block-button is-style-simple is-style-theme">
|
||||||
|
<Link href="/contact" className="wp-block-button__link wp-element-button" style={{ borderRadius: "10px" }}>
|
||||||
|
Get in touch
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,14 +443,6 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="mobile-header-row">
|
<div className="mobile-header-row">
|
||||||
<div className="header-icons-container">
|
<div className="header-icons-container">
|
||||||
<div className="header-icon mini-cart">
|
|
||||||
<a href="#" className="mini-cart-trigger">
|
|
||||||
<i className="mini-cart-count"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<a className="header-icon search-link" href="#">
|
|
||||||
<span className="search-trigger-icon"></span>
|
|
||||||
</a>
|
|
||||||
<div className="header-icon login-logout">
|
<div className="header-icon login-logout">
|
||||||
<a href="#" title="Login/Register" className="link-login"></a>
|
<a href="#" title="Login/Register" className="link-login"></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -491,6 +564,69 @@ export default function Header() {
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__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 {
|
#masthead .elementor-element.elementor-element-466de1b {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 5px !important;
|
top: 5px !important;
|
||||||
|
|||||||
@@ -85,6 +85,15 @@
|
|||||||
box-shadow: 0 6px 18px rgba(192, 18, 39, 0.45);
|
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) {
|
@media (max-width: 480px) {
|
||||||
.controls {
|
.controls {
|
||||||
top: 12px;
|
top: 12px;
|
||||||
@@ -112,6 +121,53 @@
|
|||||||
transform: translateY(-3px) scale(1.06);
|
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) ---- */
|
/* ---- Themed Leaflet internals (scoped to this map only) ---- */
|
||||||
.root :global(.leaflet-container) {
|
.root :global(.leaflet-container) {
|
||||||
background: #0b0b0b;
|
background: #0b0b0b;
|
||||||
@@ -158,34 +214,36 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
|
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) {
|
.root :global(.leaflet-popup-content) {
|
||||||
margin: 12px 16px;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
line-height: 1.4;
|
||||||
letter-spacing: -0.01em;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
}
|
||||||
.root :global(.leaflet-popup-tip) {
|
.root :global(.leaflet-popup-tip) {
|
||||||
background: #141416;
|
background: #141416;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
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;
|
color: #ffffff;
|
||||||
}
|
white-space: nowrap;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
|
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
|
||||||
@@ -210,7 +268,9 @@
|
|||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.skeleton { animation: none; }
|
.skeleton { animation: none; }
|
||||||
.markerIcon { transition: none; }
|
.markerIcon,
|
||||||
|
.markerIconHq { transition: none; }
|
||||||
|
.pinPulse { animation: none; opacity: 0.45; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Graceful error fallback ---- */
|
/* ---- Graceful error fallback ---- */
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
* Doormile office markers, plus a row of "jump to office" buttons that fly the
|
* 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.
|
* 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`)
|
* Loaded via a `ssr:false` dynamic import so Leaflet (which touches `window`)
|
||||||
* never runs on the server and cannot cause hydration mismatches. Layout/spacing
|
* never runs on the server and cannot cause hydration mismatches. Layout/spacing
|
||||||
* is owned by the host container (see ContactMap).
|
* is owned by the host container (see ContactMap).
|
||||||
@@ -21,13 +26,11 @@ import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "rea
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ESRI_WORLD_IMAGERY,
|
ESRI_WORLD_IMAGERY,
|
||||||
MAP_FIT_MAX_ZOOM,
|
HQ_OFFICE,
|
||||||
MAP_FIT_PADDING,
|
|
||||||
MAP_FOCUS_ZOOM,
|
MAP_FOCUS_ZOOM,
|
||||||
MAP_INITIAL_CENTER,
|
MAP_INITIAL_CENTER,
|
||||||
MAP_INITIAL_ZOOM,
|
MAP_INITIAL_ZOOM,
|
||||||
OFFICE_LOCATIONS,
|
OFFICE_LOCATIONS,
|
||||||
type LatLng,
|
|
||||||
} from "./offices";
|
} from "./offices";
|
||||||
|
|
||||||
type MapStatus = "loading" | "ready" | "error";
|
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. */
|
/** A request to focus a specific office. `nonce` lets the same office be re-selected. */
|
||||||
type FocusTarget = { id: string; nonce: number };
|
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: `
|
||||||
|
<span class="${styles.pinPulse}" aria-hidden="true"></span>
|
||||||
|
<svg width="40" height="52" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M15 0C6.72 0 0 6.72 0 15c0 10.5 13.06 23.86 13.62 24.42a1.95 1.95 0 0 0 2.76 0C16.94 38.86 30 25.5 30 15 30 6.72 23.28 0 15 0Z" fill="#C01227"/>
|
||||||
|
<path d="M15 1.5C7.54 1.5 1.5 7.54 1.5 15c0 9.6 12.3 22.4 12.83 22.94a.95.95 0 0 0 1.34 0C16.2 37.4 28.5 24.6 28.5 15 28.5 7.54 22.46 1.5 15 1.5Z" fill="none" stroke="#ffffff" stroke-width="1.2" stroke-opacity="0.9"/>
|
||||||
|
<circle cx="15" cy="15" r="5.4" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
iconSize: [40, 52],
|
||||||
|
iconAnchor: [20, 52],
|
||||||
|
popupAnchor: [0, -46],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
className: styles.markerIcon,
|
className: styles.markerIcon,
|
||||||
html: `
|
html: `
|
||||||
@@ -54,77 +78,54 @@ function createMarkerIcon(): L.DivIcon {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Imperative map effects that need the Leaflet instance:
|
* Imperative map effects that need the Leaflet instance:
|
||||||
* - fit the viewport to every marker (with edge padding)
|
* - keep the viewport sized correctly across resizes / lazy reveals
|
||||||
* - keep that framing correct 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
|
* - fly to a single office when one is selected via the buttons
|
||||||
*/
|
*/
|
||||||
function MapController({
|
function MapController({
|
||||||
positions,
|
|
||||||
focus,
|
focus,
|
||||||
markerRefs,
|
markerRefs,
|
||||||
}: {
|
}: {
|
||||||
positions: LatLng[];
|
|
||||||
focus: FocusTarget | null;
|
focus: FocusTarget | null;
|
||||||
markerRefs: React.RefObject<Record<string, L.Marker>>;
|
markerRefs: React.RefObject<Record<string, L.Marker>>;
|
||||||
}) {
|
}) {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
const didInit = useRef(false);
|
||||||
|
|
||||||
// Latest focus, read inside the (stable) resize handler without resubscribing.
|
// Keep the map correctly sized on container resize / lazy reveal. We never
|
||||||
const focusRef = useRef(focus);
|
// re-frame the view here, so resizing keeps whatever office is in 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.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = map.getContainer();
|
const container = map.getContainer();
|
||||||
let raf = 0;
|
let raf = 0;
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
raf = requestAnimationFrame(() => {
|
raf = requestAnimationFrame(() => map.invalidateSize());
|
||||||
map.invalidateSize();
|
|
||||||
if (!focusRef.current) {
|
|
||||||
map.fitBounds(L.latLngBounds(positions), {
|
|
||||||
padding: MAP_FIT_PADDING,
|
|
||||||
maxZoom: MAP_FIT_MAX_ZOOM,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
observer.observe(container);
|
observer.observe(container);
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
observer.disconnect();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!focus) return;
|
if (!focus) return;
|
||||||
const office = OFFICE_LOCATIONS.find((item) => item.id === focus.id);
|
const office = OFFICE_LOCATIONS.find((item) => item.id === focus.id);
|
||||||
if (!office) return;
|
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 (!didInit.current) {
|
||||||
if (!marker) return;
|
didInit.current = true;
|
||||||
const openPopup = () => marker.openPopup();
|
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);
|
map.once("moveend", openPopup);
|
||||||
return () => {
|
return () => {
|
||||||
map.off("moveend", openPopup);
|
map.off("moveend", openPopup);
|
||||||
@@ -135,14 +136,12 @@ function MapController({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OfficeMap() {
|
export default function OfficeMap() {
|
||||||
const icon = useMemo(() => createMarkerIcon(), []);
|
const icon = useMemo(() => createMarkerIcon(false), []);
|
||||||
const positions = useMemo<LatLng[]>(
|
const hqIcon = useMemo(() => createMarkerIcon(true), []);
|
||||||
() => OFFICE_LOCATIONS.map((office) => office.position),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const markerRefs = useRef<Record<string, L.Marker>>({});
|
const markerRefs = useRef<Record<string, L.Marker>>({});
|
||||||
|
|
||||||
const [focus, setFocus] = useState<FocusTarget | null>(null);
|
// Default to the headquarters so its button reads active and its popup opens.
|
||||||
|
const [focus, setFocus] = useState<FocusTarget | null>({ id: HQ_OFFICE.id, nonce: 0 });
|
||||||
const focusOffice = useCallback((id: string) => {
|
const focusOffice = useCallback((id: string) => {
|
||||||
setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }));
|
setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -189,7 +188,9 @@ export default function OfficeMap() {
|
|||||||
<button
|
<button
|
||||||
key={office.id}
|
key={office.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.controlBtn} ${isActive ? styles.controlBtnActive : ""}`}
|
className={`${styles.controlBtn} ${isActive ? styles.controlBtnActive : ""} ${
|
||||||
|
office.isHeadquarters ? styles.controlBtnHq : ""
|
||||||
|
}`}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
aria-label={`Show ${office.name} on the map`}
|
aria-label={`Show ${office.name} on the map`}
|
||||||
onClick={() => focusOffice(office.id)}
|
onClick={() => focusOffice(office.id)}
|
||||||
@@ -221,29 +222,46 @@ export default function OfficeMap() {
|
|||||||
eventHandlers={{ load: handleTileLoad, tileerror: handleTileError }}
|
eventHandlers={{ load: handleTileLoad, tileerror: handleTileError }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{OFFICE_LOCATIONS.map((office) => (
|
{OFFICE_LOCATIONS.map((office) => {
|
||||||
<Marker
|
// Google-Maps-style tooltip: hovering shows just the location name;
|
||||||
key={office.id}
|
// clicking focuses the office.
|
||||||
position={office.position}
|
return (
|
||||||
icon={icon}
|
<Marker
|
||||||
keyboard
|
key={office.id}
|
||||||
title={office.name}
|
position={office.position}
|
||||||
alt={office.name}
|
icon={office.isHeadquarters ? hqIcon : icon}
|
||||||
eventHandlers={{ click: () => focusOffice(office.id) }}
|
zIndexOffset={office.isHeadquarters ? 1000 : 0}
|
||||||
ref={(instance) => {
|
keyboard
|
||||||
if (instance) markerRefs.current[office.id] = instance;
|
title={office.name}
|
||||||
}}
|
alt={office.name}
|
||||||
>
|
eventHandlers={{
|
||||||
<Popup>
|
click: () => focusOffice(office.id),
|
||||||
<span className="office-popup__name">
|
// Hover opens the compact popup without moving the map.
|
||||||
<span className="office-popup__dot" aria-hidden="true" />
|
mouseover: (event) => event.target.openPopup(),
|
||||||
{office.name}
|
mouseout: (event) => event.target.closePopup(),
|
||||||
</span>
|
}}
|
||||||
</Popup>
|
ref={(instance) => {
|
||||||
</Marker>
|
if (instance) markerRefs.current[office.id] = instance;
|
||||||
))}
|
}}
|
||||||
|
>
|
||||||
|
<Popup
|
||||||
|
className={styles.popup}
|
||||||
|
autoPan={false}
|
||||||
|
closeButton={false}
|
||||||
|
minWidth={120}
|
||||||
|
maxWidth={200}
|
||||||
|
>
|
||||||
|
<span className={styles.tip}>
|
||||||
|
<span className={styles.tipTitle}>
|
||||||
|
<span aria-hidden="true">📍</span> {office.shortLabel}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<MapController positions={positions} focus={focus} markerRefs={markerRefs} />
|
<MapController focus={focus} markerRefs={markerRefs} />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
{status === "error" && (
|
{status === "error" && (
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* Kept dependency-free (no Leaflet runtime import) so it can be safely consumed
|
* Kept dependency-free (no Leaflet runtime import) so it can be safely consumed
|
||||||
* by both server and client modules. Types model `[lat, lng]` tuples that are
|
* by both server and client modules. Types model `[lat, lng]` tuples that are
|
||||||
* directly compatible with Leaflet's `LatLngExpression`.
|
* directly compatible with Leaflet's `LatLngExpression`.
|
||||||
|
*
|
||||||
|
* Coordinates are the real Doormile operational sites, verified against
|
||||||
|
* satellite view — not generic city-centre points.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** A `[latitude, longitude]` coordinate pair. */
|
/** A `[latitude, longitude]` coordinate pair. */
|
||||||
@@ -12,21 +15,56 @@ export type LatLng = [number, number];
|
|||||||
export interface OfficeLocation {
|
export interface OfficeLocation {
|
||||||
/** Stable, unique key (used for React keys + analytics). */
|
/** Stable, unique key (used for React keys + analytics). */
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
/** Short city label, shown on the navigation buttons. */
|
/** Short city label, shown on the navigation button. */
|
||||||
readonly city: string;
|
readonly city: string;
|
||||||
/** Human-readable label shown in the marker popup + a11y fallback. */
|
/** Human-readable label shown in the a11y fallback + marker title. */
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
/** Compact heading shown in the marker tooltip/popup (e.g. "Hyderabad HQ"). */
|
||||||
|
readonly shortLabel: string;
|
||||||
/** `[latitude, longitude]`. */
|
/** `[latitude, longitude]`. */
|
||||||
readonly position: LatLng;
|
readonly position: LatLng;
|
||||||
|
/** Headquarters gets the largest, glowing, default-active marker. */
|
||||||
|
readonly isHeadquarters?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The three permanent office markers, ordered north-to-south is irrelevant — bounds are auto-fit. */
|
/**
|
||||||
|
* The three permanent office markers, ordered for the navigation row by
|
||||||
|
* operational hierarchy: Hyderabad (HQ) → Bengaluru → Coimbatore. The
|
||||||
|
* headquarters is conveyed purely through styling (active state + border +
|
||||||
|
* glow), not through icons or label text.
|
||||||
|
*/
|
||||||
export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
||||||
{ id: "coimbatore", city: "Coimbatore", name: "Coimbatore Office", position: [11.0168, 76.9558] },
|
{
|
||||||
{ id: "bengaluru", city: "Bengaluru", name: "Bengaluru Office", position: [12.9716, 77.5946] },
|
id: "hyderabad",
|
||||||
{ id: "hyderabad", city: "Hyderabad", name: "Hyderabad Office", position: [17.385, 78.4867] },
|
city: "Hyderabad",
|
||||||
|
name: "Doormile Headquarters",
|
||||||
|
shortLabel: "Hyderabad HQ",
|
||||||
|
// Vision Ultima, Jayabheri Enclave, Gachibowli — verified on satellite.
|
||||||
|
position: [17.4484, 78.3573],
|
||||||
|
isHeadquarters: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bengaluru",
|
||||||
|
city: "Bengaluru",
|
||||||
|
name: "Bengaluru Hub",
|
||||||
|
shortLabel: "Bengaluru Hub",
|
||||||
|
// Resolved from the supplied Google Maps share link — verified on satellite.
|
||||||
|
position: [12.9929351, 77.6988599],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "coimbatore",
|
||||||
|
city: "Coimbatore",
|
||||||
|
name: "Coimbatore Hub",
|
||||||
|
shortLabel: "Coimbatore Hub",
|
||||||
|
// Mayflower Valencia, Coimbatore — verified against satellite view.
|
||||||
|
position: [11.0191, 76.9883],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** The headquarters office — focused by default on load. Falls back to the first office. */
|
||||||
|
export const HQ_OFFICE: OfficeLocation =
|
||||||
|
OFFICE_LOCATIONS.find((office) => office.isHeadquarters) ?? OFFICE_LOCATIONS[0];
|
||||||
|
|
||||||
export interface TileLayerConfig {
|
export interface TileLayerConfig {
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
readonly attribution: string;
|
readonly attribution: string;
|
||||||
@@ -45,15 +83,12 @@ export const ESRI_WORLD_IMAGERY: TileLayerConfig = {
|
|||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Padding (in px) applied when auto-fitting bounds so markers never touch the edges. */
|
/** City-level zoom used when an office is selected (and for the initial HQ view). */
|
||||||
export const MAP_FIT_PADDING: LatLng = [50, 50];
|
|
||||||
|
|
||||||
/** Cap the auto-fit zoom so two close offices don't zoom the map in too far. */
|
|
||||||
export const MAP_FIT_MAX_ZOOM = 7;
|
|
||||||
|
|
||||||
/** City-level zoom used when a single office is selected via the nav buttons. */
|
|
||||||
export const MAP_FOCUS_ZOOM = 13;
|
export const MAP_FOCUS_ZOOM = 13;
|
||||||
|
|
||||||
/** Initial center/zoom (roughly the centroid of the offices) used before bounds are fit. */
|
/**
|
||||||
export const MAP_INITIAL_CENTER: LatLng = [14.0, 77.7];
|
* Initial center/zoom: the experience opens focused on the Hyderabad HQ so the
|
||||||
export const MAP_INITIAL_ZOOM = 5;
|
* command-centre reads as the heart of the network the instant the map paints.
|
||||||
|
*/
|
||||||
|
export const MAP_INITIAL_CENTER: LatLng = HQ_OFFICE.position;
|
||||||
|
export const MAP_INITIAL_ZOOM = MAP_FOCUS_ZOOM;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function ContactsHero() {
|
export default function ContactsHero() {
|
||||||
return (
|
return (
|
||||||
@@ -82,9 +81,9 @@ export default function ContactsHero() {
|
|||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
padding: 0 16px !important;
|
padding: 0 24px !important;
|
||||||
max-width: 820px !important;
|
max-width: 1500px !important;
|
||||||
width: 90% !important;
|
width: 92% !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
@@ -95,60 +94,53 @@ export default function ContactsHero() {
|
|||||||
border-color: transparent !important;
|
border-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Spaced kicker */
|
/* Hero headline — large, light, reference-matched display type.
|
||||||
.contacts-hero-kicker {
|
Size scales with the viewport so the line-to-container width ratio
|
||||||
display: inline-flex !important;
|
stays constant; the cap keeps the longest line inside the 1500px
|
||||||
align-items: center !important;
|
container (with nowrap on desktop) so it can never overflow/clip. */
|
||||||
gap: 12px !important;
|
|
||||||
margin-bottom: 24px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contacts-hero-kicker-line {
|
|
||||||
display: block !important;
|
|
||||||
width: 24px !important;
|
|
||||||
height: 1.5px !important;
|
|
||||||
background: #C01227 !important;
|
|
||||||
border-radius: 1px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contacts-hero-kicker-text {
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 850 !important;
|
|
||||||
letter-spacing: 4px !important;
|
|
||||||
color: #C01227 !important;
|
|
||||||
text-transform: uppercase !important;
|
|
||||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bold modern typography */
|
|
||||||
.contacts-hero-title {
|
.contacts-hero-title {
|
||||||
font-size: clamp(34px, 5.2vw, 62px) !important;
|
font-size: clamp(34px, 5.9vw, 98px) !important;
|
||||||
font-weight: 850 !important;
|
font-weight: 300 !important;
|
||||||
line-height: 1.15 !important;
|
line-height: 0.95 !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
text-transform: uppercase !important;
|
text-transform: uppercase !important;
|
||||||
letter-spacing: -1.8px !important;
|
letter-spacing: -0.02em !important;
|
||||||
margin: 0 0 20px 0 !important;
|
margin: 0 0 28px 0 !important;
|
||||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contacts-hero-title-line {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep each line intact on desktop — never split SYSTEM or PROMISE/KEPT */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.contacts-hero-title-line {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.contacts-hero-title-highlight {
|
.contacts-hero-title-highlight {
|
||||||
background: linear-gradient(135deg, #ffffff 40%, #c01227 100%) !important;
|
color: #c01227 !important;
|
||||||
-webkit-background-clip: text !important;
|
|
||||||
-webkit-text-fill-color: transparent !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Description text */
|
/* Description text */
|
||||||
.contacts-hero-desc {
|
.contacts-hero-desc {
|
||||||
font-size: clamp(15px, 1.22vw, 18px) !important;
|
font-size: clamp(15px, 1.3vw, 19px) !important;
|
||||||
line-height: 1.6 !important;
|
line-height: 1.6 !important;
|
||||||
color: rgba(255, 255, 255, 0.75) !important;
|
color: rgba(255, 255, 255, 0.82) !important;
|
||||||
max-width: 600px !important;
|
max-width: 640px !important;
|
||||||
margin: 0 auto 36px auto !important;
|
margin: 0 auto 24px auto !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contacts-hero-desc-trademark {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Breadcrumb capsule */
|
/* Breadcrumb capsule */
|
||||||
.contacts-hero-breadcrumbs {
|
.contacts-hero-breadcrumbs {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -201,6 +193,9 @@ export default function ContactsHero() {
|
|||||||
padding: 0 16px !important;
|
padding: 0 16px !important;
|
||||||
width: 95% !important;
|
width: 95% !important;
|
||||||
}
|
}
|
||||||
|
.contacts-hero-title {
|
||||||
|
letter-spacing: -1px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`}} />
|
`}} />
|
||||||
<div className="custom-standard-hero-container">
|
<div className="custom-standard-hero-container">
|
||||||
@@ -212,18 +207,16 @@ export default function ContactsHero() {
|
|||||||
<div className="contacts-hero-glow-blue"></div>
|
<div className="contacts-hero-glow-blue"></div>
|
||||||
|
|
||||||
<div className="contacts-hero-glass-card">
|
<div className="contacts-hero-glass-card">
|
||||||
<div className="contacts-hero-kicker">
|
|
||||||
<span className="contacts-hero-kicker-line"></span>
|
|
||||||
<span className="contacts-hero-kicker-text">24/7 support & sales</span>
|
|
||||||
<span className="contacts-hero-kicker-line"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="contacts-hero-title">
|
<h1 className="contacts-hero-title">
|
||||||
Get In <span className="contacts-hero-title-highlight">Touch</span>
|
<span className="contacts-hero-title-line">One Connected System.</span>
|
||||||
|
<span className="contacts-hero-title-line">
|
||||||
|
One Promise <span className="contacts-hero-title-highlight">Kept.</span>
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="contacts-hero-desc">
|
<p className="contacts-hero-desc">
|
||||||
Have questions about our smart delivery network, pricing plans, or partner ecosystem? Let's build the future of logistics together.
|
Stop managing separate logistics providers. Doormile unifies First Mile, Mid Mile and Last Mile into one intelligent delivery ecosystem powered by{" "}
|
||||||
|
<span className="contacts-hero-desc-trademark">MileTruth™ AI</span>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ export default function HowItWorksHero() {
|
|||||||
__html: `
|
__html: `
|
||||||
.howits-hero-custom-bg.elementor-repeater-item-3264830,
|
.howits-hero-custom-bg.elementor-repeater-item-3264830,
|
||||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.png') !important;
|
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home2-banner-1.jpg') !important;
|
||||||
background-position: center !important;
|
background-position: center !important;
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-size: cover !important;
|
background-size: cover !important;
|
||||||
|
|||||||
@@ -60,10 +60,32 @@ function pointInPoly(x: number, y: number, poly: number[][]) {
|
|||||||
return inside;
|
return inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IndustryWorldMap() {
|
/** Parse a #rrggbb hex into an [r,g,b] triple. Falls back to the section red. */
|
||||||
|
function hexToRgb(hex: string): [number, number, number] {
|
||||||
|
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
||||||
|
if (!m) return [239, 68, 68];
|
||||||
|
const int = parseInt(m[1], 16);
|
||||||
|
return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param accent Network accent colour (#rrggbb) for the hub nodes, pulse
|
||||||
|
* rings, travelling packets and dashed routes. The dotted continent
|
||||||
|
* silhouette stays neutral grey. Defaults to the section red so the Women
|
||||||
|
* Empowerment usage is unchanged; the MileTruth workflows pass their own
|
||||||
|
* accent (WF1 teal/cyan · WF2 crimson/red).
|
||||||
|
*/
|
||||||
|
export default function IndustryWorldMap({
|
||||||
|
accent = "#ef4444",
|
||||||
|
}: {
|
||||||
|
accent?: string;
|
||||||
|
}) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const [ar, ag, ab] = hexToRgb(accent);
|
||||||
|
const rgba = (a: number) => `rgba(${ar},${ag},${ab},${a})`;
|
||||||
|
const solid = `rgb(${ar},${ag},${ab})`;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const parent = canvas?.parentElement;
|
const parent = canvas?.parentElement;
|
||||||
if (!canvas || !parent) return;
|
if (!canvas || !parent) return;
|
||||||
@@ -135,7 +157,7 @@ export default function IndustryWorldMap() {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.setLineDash([4, 7]);
|
ctx.setLineDash([4, 7]);
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.strokeStyle = "rgba(239,68,68,0.13)";
|
ctx.strokeStyle = rgba(0.13);
|
||||||
for (const [a, b] of ROUTES) {
|
for (const [a, b] of ROUTES) {
|
||||||
const c = ctrl(cs[a], cs[b]);
|
const c = ctrl(cs[a], cs[b]);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -156,8 +178,8 @@ export default function IndustryWorldMap() {
|
|||||||
const tt = Math.max(0, t - 0.04);
|
const tt = Math.max(0, t - 0.04);
|
||||||
const pt = bezier(cs[a], c, cs[b], tt);
|
const pt = bezier(cs[a], c, cs[b], tt);
|
||||||
const grad = ctx.createLinearGradient(pt.x, pt.y, p.x, p.y);
|
const grad = ctx.createLinearGradient(pt.x, pt.y, p.x, p.y);
|
||||||
grad.addColorStop(0, "rgba(239,68,68,0)");
|
grad.addColorStop(0, rgba(0));
|
||||||
grad.addColorStop(1, "rgba(239,68,68,0.5)");
|
grad.addColorStop(1, rgba(0.5));
|
||||||
ctx.strokeStyle = grad;
|
ctx.strokeStyle = grad;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -165,9 +187,9 @@ export default function IndustryWorldMap() {
|
|||||||
ctx.lineTo(p.x, p.y);
|
ctx.lineTo(p.x, p.y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
ctx.shadowColor = "#ef4444";
|
ctx.shadowColor = solid;
|
||||||
ctx.shadowBlur = 12;
|
ctx.shadowBlur = 12;
|
||||||
ctx.fillStyle = "#ef4444";
|
ctx.fillStyle = solid;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, 2.6, 0, Math.PI * 2);
|
ctx.arc(p.x, p.y, 2.6, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
@@ -183,13 +205,13 @@ export default function IndustryWorldMap() {
|
|||||||
const radius = 3 + phase * 24;
|
const radius = 3 + phase * 24;
|
||||||
const alpha = (1 - phase) * 0.45;
|
const alpha = (1 - phase) * 0.45;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = `rgba(239,68,68,${alpha})`;
|
ctx.strokeStyle = rgba(alpha);
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.arc(c.x, c.y, radius, 0, Math.PI * 2);
|
ctx.arc(c.x, c.y, radius, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
ctx.fillStyle = "#ef4444";
|
ctx.fillStyle = solid;
|
||||||
ctx.shadowColor = "#ef4444";
|
ctx.shadowColor = solid;
|
||||||
ctx.shadowBlur = 8;
|
ctx.shadowBlur = 8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(c.x, c.y, 2.6, 0, Math.PI * 2);
|
ctx.arc(c.x, c.y, 2.6, 0, Math.PI * 2);
|
||||||
@@ -221,7 +243,7 @@ export default function IndustryWorldMap() {
|
|||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [accent]);
|
||||||
|
|
||||||
return <canvas ref={canvasRef} className="ind__map" aria-hidden="true" />;
|
return <canvas ref={canvasRef} className="ind__map" aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,80 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import EVSection, { EVStat, EVBadge, EVSlide, EVCardsTheme } from "./EVSection";
|
import EVSection, { EVStat, EVBadge, EVFeature, EVCardsTheme } from "./EVSection";
|
||||||
import WorkflowScene from "./WorkflowScene";
|
import WorkflowScene from "./WorkflowScene";
|
||||||
|
|
||||||
/* Cyan / electric-blue — matches the Optimization Engine scene palette. */
|
/* Cyan / Teal — matches the Optimization Engine scene palette. */
|
||||||
const THEME: EVCardsTheme = {
|
const THEME: EVCardsTheme = {
|
||||||
accent: "#00E5FF",
|
accent: "#00E5FF",
|
||||||
accent2: "#3B82F6",
|
accent2: "#14B8A6",
|
||||||
glow: "rgba(0,229,255,0.22)",
|
glow: "rgba(0,229,255,0.18)",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workflow 1 — Performance (hybrid split-screen).
|
* Workflow 1 — Performance (hybrid split-screen).
|
||||||
*
|
*
|
||||||
* Keeps the premium EVSection chrome (banner → floating card → dark section →
|
* • Left — the PRODUCTION Optimization Engine Three.js scene (depot, trucks,
|
||||||
* stat bar) but converts the body into a split layout:
|
* route optimization, shaders, particles).
|
||||||
* • Left — the PRODUCTION Optimization Engine Three.js scene (the same
|
* • Right — a COMPACT DASHBOARD: a quick KPI row over short feature cards,
|
||||||
* OptimizationCanvas used by OptimizationSection: depot, trucks,
|
* sitting on the animated network backdrop (cyan/teal). Surfaces the
|
||||||
* route optimization, shaders, particles). One instance, mounted
|
* key metrics fast, keeps copy short and reads well on mobile.
|
||||||
* compactly instead of as a multi-viewport pinned scroll.
|
|
||||||
* • Right — lightweight auto-rotating cards (4s / 600ms fade+slide).
|
|
||||||
*
|
|
||||||
* This preserves the 3D storytelling while dramatically cutting page height.
|
|
||||||
*/
|
*/
|
||||||
const SLIDES: EVSlide[] = [
|
const METRICS: EVStat[] = [
|
||||||
|
{ value: 42, suffix: "%", label: "Distance Saved" },
|
||||||
|
{ value: 28, suffix: "%", label: "Faster Routes" },
|
||||||
|
{ value: 31, suffix: "%", label: "Lower Cost" },
|
||||||
|
{ value: 99.9, decimals: 1, suffix: "%", label: "On-Time" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ico = (path: React.ReactNode) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FEATURES: EVFeature[] = [
|
||||||
{
|
{
|
||||||
status: "Optimization Running",
|
icon: ico(<polygon points="3 11 22 2 13 21 11 13 3 11" />),
|
||||||
title: "Route Optimization",
|
title: "Route Optimization",
|
||||||
value: 42,
|
desc: "AI selects the most efficient path across every zone.",
|
||||||
suffix: "%",
|
|
||||||
metricLabel: "Distance Saved",
|
|
||||||
kpis: ["Route optimization active", "37% fewer vehicles required", "SLA compliance 99.9%"],
|
|
||||||
desc: "AI selects the most efficient delivery paths across every zone, cutting unnecessary travel and fuel and battery consumption.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: "Fleet Balancing",
|
icon: ico(
|
||||||
|
<>
|
||||||
|
<polyline points="22 17 13.5 8.5 8.5 13.5 2 7" />
|
||||||
|
<polyline points="16 17 22 17 22 11" />
|
||||||
|
</>,
|
||||||
|
),
|
||||||
title: "Distance Reduction",
|
title: "Distance Reduction",
|
||||||
value: 37,
|
desc: "Same volume delivered with a leaner, better-used fleet.",
|
||||||
suffix: "%",
|
|
||||||
metricLabel: "Fewer Vehicles",
|
|
||||||
kpis: ["Load balancing engaged", "Same volume, leaner fleet", "Lower maintenance & staffing"],
|
|
||||||
desc: "Intelligent load balancing fulfils the same order volume with a leaner, better-utilised fleet — fewer miles, fewer vehicles.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: "Dispatch Active",
|
icon: ico(
|
||||||
|
<>
|
||||||
|
<path d="M12 14l4-4" />
|
||||||
|
<path d="M3.34 19a10 10 0 1 1 17.32 0" />
|
||||||
|
</>,
|
||||||
|
),
|
||||||
title: "Fleet Efficiency",
|
title: "Fleet Efficiency",
|
||||||
value: 31,
|
desc: "Higher utilisation and lower operating cost.",
|
||||||
suffix: "%",
|
|
||||||
metricLabel: "Lower Operating Cost",
|
|
||||||
kpis: ["Higher fleet utilisation", "Predictable operations", "Reduced fuel & overhead"],
|
|
||||||
desc: "Smart grouping and dispatch keep operations smooth and predictable while reducing maintenance and staffing cost.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: "SLA Safe",
|
icon: ico(
|
||||||
|
<>
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||||
|
<polyline points="9 12 11 14 15 10" />
|
||||||
|
</>,
|
||||||
|
),
|
||||||
title: "SLA Performance",
|
title: "SLA Performance",
|
||||||
value: 99.9,
|
desc: "Real-time correction keeps deliveries on time.",
|
||||||
decimals: 1,
|
|
||||||
suffix: "%",
|
|
||||||
metricLabel: "On-Time Delivery",
|
|
||||||
kpis: ["Real-time route correction", "Consistent delivery windows", "100% order fulfilment"],
|
|
||||||
desc: "Real-time routing keeps deliveries on time across all zones, sustaining high customer satisfaction and SLA performance.",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -69,31 +83,24 @@ const BADGES: EVBadge[] = [
|
|||||||
{ value: "-37%", label: "FEWER VEHICLES" },
|
{ value: "-37%", label: "FEWER VEHICLES" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATS: EVStat[] = [
|
|
||||||
{ value: 42, suffix: "%", label: "Distance Saved" },
|
|
||||||
{ value: 28, suffix: "%", label: "Faster Routes" },
|
|
||||||
{ value: 31, suffix: "%", label: "Lower Cost" },
|
|
||||||
{ value: 99.9, decimals: 1, suffix: "%", label: "On-Time" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Workflow1() {
|
export default function Workflow1() {
|
||||||
return (
|
return (
|
||||||
<EVSection
|
<EVSection
|
||||||
ariaLabel="Workflow 1 — Performance"
|
ariaLabel="Workflow 1 — Performance"
|
||||||
gapTop
|
gapTop
|
||||||
gapBottom
|
gapBottom
|
||||||
bannerImage="/images/home3-slide-1.jpg"
|
bannerImage="/images/mile-1.png"
|
||||||
cardTitle="OPTIMIZE EVERY MILE"
|
cardTitle="OPTIMIZE EVERY MILE"
|
||||||
cardSubtitle="Cut travel distance, reduce operating cost, and improve fleet productivity across every route."
|
cardSubtitle="Cut travel distance, reduce operating cost, and improve fleet productivity across every route."
|
||||||
eyebrow="/ Performance /"
|
eyebrow="/ Performance /"
|
||||||
titleLead="SMARTER ROUTES. "
|
titleLead="SMARTER ROUTES. "
|
||||||
titleAccent="LOWER COSTS."
|
titleAccent="LOWER COSTS."
|
||||||
mediaSlot={<WorkflowScene variant="optimization" ariaLabel="Live route optimization engine" />}
|
mediaSlot={<WorkflowScene variant="optimization" ariaLabel="Live route optimization engine" />}
|
||||||
slides={SLIDES}
|
metrics={METRICS}
|
||||||
|
features={FEATURES}
|
||||||
cardsHeading="Performance Insight"
|
cardsHeading="Performance Insight"
|
||||||
cardsTheme={THEME}
|
cardsTheme={THEME}
|
||||||
badges={BADGES}
|
badges={BADGES}
|
||||||
stats={STATS}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,82 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import EVSection, { EVStat, EVBadge, EVSlide, EVCardsTheme } from "./EVSection";
|
import EVSection, { EVStat, EVBadge, EVFeature, EVCardsTheme } from "./EVSection";
|
||||||
import WorkflowScene from "./WorkflowScene";
|
import WorkflowScene from "./WorkflowScene";
|
||||||
|
|
||||||
/* Red / crimson / orange — matches the Routing Engine (logistics brain) scene. */
|
/* Red / Crimson — matches the Routing Engine (logistics brain) scene. */
|
||||||
const THEME: EVCardsTheme = {
|
const THEME: EVCardsTheme = {
|
||||||
accent: "#E2354A",
|
accent: "#E2354A",
|
||||||
accent2: "#F59E0B",
|
accent2: "#C01227",
|
||||||
glow: "rgba(226,53,74,0.24)",
|
glow: "rgba(226,53,74,0.2)",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workflow 2 — Innovation (hybrid split-screen).
|
* Workflow 2 — Innovation (hybrid split-screen).
|
||||||
*
|
*
|
||||||
* Keeps the premium EVSection chrome but converts the body into a split layout:
|
* • Left — the PRODUCTION Routing Engine Three.js scene (city nodes,
|
||||||
* • Left — the PRODUCTION Routing Engine Three.js scene (the same
|
* buildings, multi-route generation, constraint evaluation).
|
||||||
* LogisticsBrainCanvas used by LogisticsBrainSection: city nodes,
|
* • Right — a COMPACT DASHBOARD: a quick KPI row over short feature cards,
|
||||||
* buildings, multi-route generation, constraint evaluation,
|
* sitting on the animated network backdrop (red/crimson). Surfaces
|
||||||
* network/brain animation). One instance, mounted compactly.
|
* the key metrics fast and keeps the section tight on mobile.
|
||||||
* • Right — lightweight auto-rotating cards (4s / 600ms fade+slide).
|
|
||||||
*
|
|
||||||
* Preserves the 3D storytelling while dramatically cutting page height.
|
|
||||||
*/
|
*/
|
||||||
const SLIDES: EVSlide[] = [
|
const METRICS: EVStat[] = [
|
||||||
|
{ value: 45, suffix: "ms", label: "Inference" },
|
||||||
|
{ value: 12, suffix: "+", label: "Strategies" },
|
||||||
|
{ value: 99.9, decimals: 1, suffix: "%", label: "SLA Met" },
|
||||||
|
{ value: 24, suffix: "/7", label: "Adaptive" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ico = (path: React.ReactNode) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FEATURES: EVFeature[] = [
|
||||||
{
|
{
|
||||||
status: "Generating Routes",
|
icon: ico(
|
||||||
|
<>
|
||||||
|
<polygon points="12 2 2 7 12 12 22 7 12 2" />
|
||||||
|
<polyline points="2 17 12 22 22 17" />
|
||||||
|
<polyline points="2 12 12 17 22 12" />
|
||||||
|
</>,
|
||||||
|
),
|
||||||
title: "Generate Routes",
|
title: "Generate Routes",
|
||||||
value: 6,
|
desc: "Many strategies explored per dispatch window.",
|
||||||
suffix: " plans",
|
|
||||||
metricLabel: "Route Plans Generated",
|
|
||||||
kpis: ["Parallel strategies explored", "59 orders in scope", "Real-time combinations"],
|
|
||||||
desc: "The Parallel Universe Engine evaluates many routing strategies at once for every dispatch window, exploring route combinations in real time.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: "Constraints Passed",
|
icon: ico(
|
||||||
|
<>
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01" />
|
||||||
|
</>,
|
||||||
|
),
|
||||||
title: "Check Constraints",
|
title: "Check Constraints",
|
||||||
value: 5,
|
desc: "Battery, capacity, distance and time validated.",
|
||||||
metricLabel: "Constraints Evaluated",
|
|
||||||
kpis: ["Battery aware", "Capacity & distance checked", "Powered by Google OR-Tools"],
|
|
||||||
desc: "Battery, distance, capacity and time are first-class inputs — battery-aware simulation solves the EV routing challenge.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: "Scoring Routes",
|
icon: ico(
|
||||||
|
<>
|
||||||
|
<line x1="6" y1="20" x2="6" y2="14" />
|
||||||
|
<line x1="12" y1="20" x2="12" y2="4" />
|
||||||
|
<line x1="18" y1="20" x2="18" y2="10" />
|
||||||
|
</>,
|
||||||
|
),
|
||||||
title: "Score & Compare",
|
title: "Score & Compare",
|
||||||
value: 12,
|
desc: "Plans ranked by total cost in parallel.",
|
||||||
suffix: "+",
|
|
||||||
metricLabel: "Strategies Compared",
|
|
||||||
kpis: ["Ranked by total cost", "SLA protected", "Real-time ETA validation"],
|
|
||||||
desc: "Every plan is benchmarked in parallel and ranked by total cost, with sub-45ms inference at production scale.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: "Delivery Ready",
|
icon: ico(<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />),
|
||||||
title: "Select Best Plan",
|
title: "Select Best Plan",
|
||||||
value: 45,
|
desc: "SLA-first plan locked in and dispatched.",
|
||||||
suffix: "ms",
|
|
||||||
metricLabel: "Decision Latency",
|
|
||||||
kpis: ["Late plans rejected", "Best plan locked in", "Dispatched to the fleet"],
|
|
||||||
desc: "Late plans are rejected automatically and the highest-performing, SLA-first plan is locked in and dispatched.",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -66,13 +85,6 @@ const BADGES: EVBadge[] = [
|
|||||||
{ value: "100%", label: "SLA-FIRST" },
|
{ value: "100%", label: "SLA-FIRST" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATS: EVStat[] = [
|
|
||||||
{ value: 45, suffix: "ms", label: "Inference" },
|
|
||||||
{ value: 12, suffix: "+", label: "Strategies" },
|
|
||||||
{ value: 99.9, decimals: 1, suffix: "%", label: "SLA Met" },
|
|
||||||
{ value: 24, suffix: "/7", label: "Adaptive" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Workflow2() {
|
export default function Workflow2() {
|
||||||
return (
|
return (
|
||||||
<EVSection
|
<EVSection
|
||||||
@@ -84,12 +96,17 @@ export default function Workflow2() {
|
|||||||
eyebrow="/ Innovation /"
|
eyebrow="/ Innovation /"
|
||||||
titleLead="MANY STRATEGIES. "
|
titleLead="MANY STRATEGIES. "
|
||||||
titleAccent="ONE BEST PLAN."
|
titleAccent="ONE BEST PLAN."
|
||||||
mediaSlot={<WorkflowScene variant="logistics" ariaLabel="Live multi-route logistics brain" />}
|
mediaSlot={
|
||||||
slides={SLIDES}
|
<WorkflowScene
|
||||||
|
variant="logistics"
|
||||||
|
ariaLabel="Live multi-route logistics brain"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
metrics={METRICS}
|
||||||
|
features={FEATURES}
|
||||||
cardsHeading="AI Decision Engine"
|
cardsHeading="AI Decision Engine"
|
||||||
cardsTheme={THEME}
|
cardsTheme={THEME}
|
||||||
badges={BADGES}
|
badges={BADGES}
|
||||||
stats={STATS}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,282 +1,38 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import StrategySection from "../strategy/StrategySection";
|
import StrategySection from "../strategy/StrategySection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workflow 3 — the "Happier Riders. Higher Fulfillment." 3D scroll-storytelling
|
||||||
|
* experience (StrategySection).
|
||||||
|
*
|
||||||
|
* The old bottom "STRATEGY" explanation card (heading + description + pagination
|
||||||
|
* + chevron graphic) has been removed: the workflow narrative is already covered
|
||||||
|
* by the previous sections, so that card only repeated it and lengthened the
|
||||||
|
* page. StrategySection now renders standalone (non-connected) with its own
|
||||||
|
* rounded card. The wrapper only cancels the global `section` padding so the
|
||||||
|
* tall pinned section doesn't gain empty bands.
|
||||||
|
*/
|
||||||
export default function Workflow3() {
|
export default function Workflow3() {
|
||||||
const [activeSlide, setActiveSlide] = useState(0);
|
|
||||||
const [paused, setPaused] = useState(false);
|
|
||||||
const [inView, setInView] = useState(false);
|
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const slides = [
|
|
||||||
{
|
|
||||||
title: "STRATEGY",
|
|
||||||
text: "Our grading engine continuously evaluates fulfillment performance, SLA compliance, and route efficiency before every dispatch. By comparing legacy routing methods with unified optimization, the system ensures smarter and more reliable delivery planning. This helps businesses maintain operational consistency while improving overall delivery performance."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "STRATEGY",
|
|
||||||
text: "Every EV route is pre-validated against real battery capacity and charging feasibility before a rider leaves the hub. This reduces the risk of delivery interruptions, charging failures, or delayed orders during operations. The platform ensures reliable route execution while maximizing EV fleet efficiency and rider confidence."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "STRATEGY",
|
|
||||||
text: "The system provides actionable fleet insights and optimized workload distribution to improve both rider experience and operational productivity. Balanced route allocation helps reduce rider fatigue, improve retention, and maintain consistent delivery quality across zones. Managers gain better visibility into fleet performance, enabling faster and more informed decision-making."
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Always begin on slide 1 (01/03) on mount. Scrolling away and back does NOT reset
|
|
||||||
// (the component stays mounted) — only a fresh page load / route change back to
|
|
||||||
// MileTruth re-mounts and restarts at slide 1.
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveSlide(0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Autoplay is gated on visibility: it starts only once the slider card scrolls into
|
|
||||||
// view (not on page load) and stops when it leaves — without touching activeSlide,
|
|
||||||
// so returning to the section resumes from wherever it was, never snapping to slide 1.
|
|
||||||
useEffect(() => {
|
|
||||||
const el = cardRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const io = new IntersectionObserver(
|
|
||||||
([entry]) => setInView(entry.isIntersecting),
|
|
||||||
{ threshold: 0.35 }
|
|
||||||
);
|
|
||||||
io.observe(el);
|
|
||||||
return () => io.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Auto-advance every 10s, looping — but only while the card is in view and the user
|
|
||||||
// isn't hovering it. Keyed on activeSlide so a manual jump restarts the 10s dwell.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!inView || paused) return;
|
|
||||||
const id = setTimeout(() => {
|
|
||||||
setActiveSlide((prev) => (prev + 1) % slides.length);
|
|
||||||
}, 10000);
|
|
||||||
return () => clearTimeout(id);
|
|
||||||
}, [activeSlide, inView, paused, slides.length]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="dm-wf3" aria-label="Workflow 3 — Happier Riders. Higher Fulfillment. & Strategy">
|
<section
|
||||||
|
className="dm-wf3"
|
||||||
|
aria-label="Workflow 3 — Happier Riders. Higher Fulfillment."
|
||||||
|
>
|
||||||
|
<StrategySection />
|
||||||
|
|
||||||
{/* ── Top sub-section: the full "Happier Riders. Higher Fulfillment."
|
<style
|
||||||
3D scroll-storytelling experience ── */}
|
dangerouslySetInnerHTML={{
|
||||||
<StrategySection connected />
|
__html: `
|
||||||
|
.dm-wf3 { position: relative; margin: 0 auto; }
|
||||||
{/* ── Bottom sub-section: Strategy content, flush + pulled up to butt against
|
/* Cancel the global "section { padding: 6rem 0 }": both this wrapper
|
||||||
the 3D card's flat bottom so the whole workflow reads as one container —
|
and the nested .dm-st are sections, so that padding would stack into
|
||||||
the same connected structure used in Workflow 1 & 2 ── */}
|
large empty bands around the pinned 3D experience. */
|
||||||
<div className="dm-wf3-card" ref={cardRef} onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
|
.dm-wf3, .dm-wf3 .dm-st { padding-top: 0; padding-bottom: 0; }
|
||||||
{/* Left Column: Overlapping Chevron Graphic */}
|
`,
|
||||||
<div className="dm-workflow-left">
|
}}
|
||||||
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
|
/>
|
||||||
<path
|
|
||||||
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
|
|
||||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
|
|
||||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Quotes & Text Content */}
|
|
||||||
<div className="dm-workflow-right">
|
|
||||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
|
|
||||||
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
|
||||||
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
|
|
||||||
|
|
||||||
<div className="dm-workflow-text-container">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.p
|
|
||||||
key={activeSlide}
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -12 }}
|
|
||||||
transition={{ duration: 0.7, ease: "easeInOut" }}
|
|
||||||
className="dm-workflow-text"
|
|
||||||
>
|
|
||||||
{slides[activeSlide].text}
|
|
||||||
</motion.p>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="dm-workflow-nav">
|
|
||||||
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
|
|
||||||
<div className="dm-workflow-bars">
|
|
||||||
{slides.map((_, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
aria-label={`Go to slide ${index + 1}`}
|
|
||||||
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
|
|
||||||
onClick={() => setActiveSlide(index)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style dangerouslySetInnerHTML={{ __html: styles }} />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = `
|
|
||||||
/* ============================================================
|
|
||||||
Workflow 3 = ONE container:
|
|
||||||
├─ Happier Riders. Higher Fulfillment. (full StrategySection — 3D)
|
|
||||||
└─ Strategy (content card, flush, pulled up)
|
|
||||||
The Strategy card aligns to the 3D card's 20px side insets, butts against
|
|
||||||
its flat bottom and rounds the bottom corners, so the two read as a single
|
|
||||||
continuous container — same technique as Workflow 1 & 2.
|
|
||||||
============================================================ */
|
|
||||||
.dm-wf3 {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cancel the global "section { padding: 6rem 0 }" (consolidated into /public/css/site.css): both
|
|
||||||
this wrapper and the nested .dm-st are sections, so that 96px top+bottom stacked
|
|
||||||
into large empty bands above / between the workflows. These are full-bleed pinned
|
|
||||||
experiences whose cards butt together via their own insets — no section padding. */
|
|
||||||
.dm-wf3, .dm-wf3 .dm-st { padding-top: 0; padding-bottom: 0; }
|
|
||||||
|
|
||||||
.dm-wf3-card {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
margin: 0 20px 0;
|
|
||||||
background: #181818;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
border-top: none;
|
|
||||||
border-radius: 0 0 28px 28px;
|
|
||||||
/* No shadow: this card is flush under the strategy 3D card and merges with it as one
|
|
||||||
continuous container — a shadow here would re-introduce a dark band at the seam. */
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 36px 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 40px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dm-workflow-left {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 440px;
|
|
||||||
}
|
|
||||||
.dm-workflow-svg {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
filter: drop-shadow(0 8px 24px rgba(0,0,0,0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
.dm-workflow-right {
|
|
||||||
flex: 1.2;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
.dm-workflow-quote { margin-bottom: 5px; }
|
|
||||||
|
|
||||||
.dm-workflow-title {
|
|
||||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
|
||||||
font-size: 38px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #F8FAFC !important;
|
|
||||||
letter-spacing: -0.015em;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dm-workflow-text-container { min-height: 150px; width: 100%; }
|
|
||||||
.dm-workflow-text {
|
|
||||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
|
||||||
font-size: 21px;
|
|
||||||
line-height: 1.75;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
color: #A3A3A3;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dm-workflow-nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.dm-workflow-counter {
|
|
||||||
font-family: var(--font-space-grotesk), sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #737373;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
.dm-workflow-bars { display: flex; gap: 8px; }
|
|
||||||
.dm-workflow-bar {
|
|
||||||
width: 40px;
|
|
||||||
height: 3px;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 999px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.dm-workflow-bar.is-active { background: #C01227; }
|
|
||||||
.dm-workflow-bar:hover { background: rgba(255, 255, 255, 0.35); }
|
|
||||||
.dm-workflow-bar.is-active:hover { background: #C01227; }
|
|
||||||
|
|
||||||
/* ── Responsive — keep insets/radius aligned to the 3D card ── */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.dm-wf3-card { padding: 44px 44px; gap: 44px; }
|
|
||||||
.dm-workflow-title { font-size: 32px; }
|
|
||||||
.dm-workflow-text { font-size: 19px; }
|
|
||||||
}
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
/* Mobile: compact card so it never exceeds ~500px (was ~850px from the full
|
|
||||||
desktop chevron + long paragraph). Smaller chevron, tighter spacing and a
|
|
||||||
line-clamped paragraph keep the workflow state readable without a long scroll. */
|
|
||||||
.dm-wf3-card {
|
|
||||||
/* Bottom gap separates this last workflow card from the contact section below. */
|
|
||||||
margin: 0 10px 16px;
|
|
||||||
border-radius: 0 0 20px 20px;
|
|
||||||
padding: 26px 22px;
|
|
||||||
gap: 16px;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.dm-workflow-left { max-width: 128px; }
|
|
||||||
.dm-workflow-right { width: 100%; gap: 12px; }
|
|
||||||
.dm-workflow-quote { margin-bottom: 2px; }
|
|
||||||
.dm-workflow-title { font-size: 22px; }
|
|
||||||
.dm-workflow-text-container { min-height: auto; }
|
|
||||||
.dm-workflow-text {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 5;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.dm-workflow-nav { margin-top: 4px; }
|
|
||||||
}
|
|
||||||
@media (max-width: 390px) {
|
|
||||||
.dm-workflow-left { max-width: 108px; }
|
|
||||||
.dm-workflow-title { font-size: 20px; }
|
|
||||||
.dm-workflow-text { font-size: 14px; -webkit-line-clamp: 4; }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Navbar from './components/ui/Navbar'
|
|||||||
import FirstMile from './components/sections/FirstMile'
|
import FirstMile from './components/sections/FirstMile'
|
||||||
import MidMile from './components/sections/MidMile'
|
import MidMile from './components/sections/MidMile'
|
||||||
import LastMile from './components/sections/LastMile'
|
import LastMile from './components/sections/LastMile'
|
||||||
import Analytics from './components/sections/Analytics'
|
import Promise from './components/sections/Promise'
|
||||||
import { useSceneStore } from './store/useSceneStore'
|
import { useSceneStore } from './store/useSceneStore'
|
||||||
import './styles/experience.css'
|
import './styles/experience.css'
|
||||||
|
|
||||||
@@ -145,8 +145,10 @@ export default function Experience3D() {
|
|||||||
<div className="sections-overlay-container">
|
<div className="sections-overlay-container">
|
||||||
<FirstMile active={scrollProgress >= 0.02 && scrollProgress < 0.14} />
|
<FirstMile active={scrollProgress >= 0.02 && scrollProgress < 0.14} />
|
||||||
<MidMile active={scrollProgress >= 0.38 && scrollProgress < 0.50} />
|
<MidMile active={scrollProgress >= 0.38 && scrollProgress < 0.50} />
|
||||||
<LastMile active={scrollProgress >= 0.80 && scrollProgress < 0.92} />
|
<LastMile active={scrollProgress >= 0.78 && scrollProgress < 0.875} />
|
||||||
<Analytics active={scrollProgress >= 0.94} />
|
{/* Final card: reveals as the journey closes (fills the slot the old
|
||||||
|
workflow timeline card used to occupy — no blank gap). */}
|
||||||
|
<Promise active={scrollProgress >= 0.90} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import RevealCard from '../ui/RevealCard'
|
|
||||||
|
|
||||||
export default function Analytics({ active }) {
|
|
||||||
return (
|
|
||||||
<RevealCard active={active} id="analytics-section">
|
|
||||||
<div className="section-badge">Workflow</div>
|
|
||||||
<h2 className="section-title">Doormile Insights</h2>
|
|
||||||
<h3 className="section-subtitle">3-Mile Logistics Ecosystem</h3>
|
|
||||||
|
|
||||||
<div className="workflow-steps">
|
|
||||||
<div className="workflow-step">
|
|
||||||
<div className="step-number-container">
|
|
||||||
<span className="step-number">01</span>
|
|
||||||
<div className="step-line"></div>
|
|
||||||
</div>
|
|
||||||
<div className="step-content">
|
|
||||||
<h4 className="step-title">First Mile</h4>
|
|
||||||
<p className="step-description">Incoming shipments are securely loaded, checked, and consolidated at initial fulfillment hubs.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="workflow-step">
|
|
||||||
<div className="step-number-container">
|
|
||||||
<span className="step-number">02</span>
|
|
||||||
<div className="step-line"></div>
|
|
||||||
</div>
|
|
||||||
<div className="step-content">
|
|
||||||
<h4 className="step-title">Mid Mile</h4>
|
|
||||||
<p className="step-description">Consolidated goods travel between primary distribution nodes via optimized express transit corridors.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="workflow-step">
|
|
||||||
<div className="step-number-container">
|
|
||||||
<span className="step-number">03</span>
|
|
||||||
</div>
|
|
||||||
<div className="step-content">
|
|
||||||
<h4 className="step-title">Last Mile</h4>
|
|
||||||
<p className="step-description">Local delivery units organize doorstep routes to transport packages to final customers.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RevealCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,10 @@ export default function LastMile({ active }) {
|
|||||||
const lenis = useSceneStore((state) => state.lenis)
|
const lenis = useSceneStore((state) => state.lenis)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
// Smoothly scroll to 97% progress, which is inside the Analytics Dashboard section.
|
// Smoothly scroll to 92% progress, which lands on the analytics-dashboard
|
||||||
|
// view where the closing promise card is revealed.
|
||||||
// Relative to the experience spacer (the section sits below the page hero).
|
// Relative to the experience spacer (the section sits below the page hero).
|
||||||
lenis?.scrollTo(progressToScrollY(0.97), { duration: 1.5 })
|
lenis?.scrollTo(progressToScrollY(0.92), { duration: 1.5 })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -22,12 +23,19 @@ export default function LastMile({ active }) {
|
|||||||
<p className="section-description">{config.description}</p>
|
<p className="section-description">{config.description}</p>
|
||||||
<div className="section-metrics">
|
<div className="section-metrics">
|
||||||
<div className="metric-item">
|
<div className="metric-item">
|
||||||
<span className="metric-value">12.5 min</span>
|
<span className="metric-value">99.4%</span>
|
||||||
<span className="metric-label">Avg. Delivery window</span>
|
<span className="metric-label">On-Time Delivery</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="metric-item">
|
<div className="metric-item">
|
||||||
<span className="metric-value">99.4%</span>
|
<span className="metric-value">12.5 min</span>
|
||||||
<span className="metric-label">On-Time Rate</span>
|
<span className="metric-label">Avg. Doorstep Time</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section-supporting">
|
||||||
|
<span className="supporting-dot"></span>
|
||||||
|
<div className="supporting-text">
|
||||||
|
<span className="supporting-value">Real-Time visibility</span>
|
||||||
|
<span className="supporting-label">Live GPS · Active now</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="section-close-btn" onClick={handleClose}>
|
<button className="section-close-btn" onClick={handleClose}>
|
||||||
|
|||||||
@@ -20,14 +20,38 @@ export default function MidMile({ active }) {
|
|||||||
<h2 className="section-title">{config.title}</h2>
|
<h2 className="section-title">{config.title}</h2>
|
||||||
<h3 className="section-subtitle">{config.subtitle}</h3>
|
<h3 className="section-subtitle">{config.subtitle}</h3>
|
||||||
<p className="section-description">{config.description}</p>
|
<p className="section-description">{config.description}</p>
|
||||||
<div className="section-metrics">
|
{/* Enterprise information strip — icon + title + description rows with
|
||||||
<div className="metric-item">
|
subtle hairline separators (no KPI cards / oversized statistics).
|
||||||
<span className="metric-value">4.2 hr</span>
|
Keeps the `section-metrics` class so the RevealCard entrance stagger
|
||||||
<span className="metric-label">Avg. Transit Time</span>
|
still treats it as a single animated block. */}
|
||||||
|
<div className="section-metrics mm-info-strip">
|
||||||
|
<div className="mm-info-row">
|
||||||
|
<span className="mm-info-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="1" y="3" width="15" height="13"></rect>
|
||||||
|
<polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon>
|
||||||
|
<circle cx="5.5" cy="18.5" r="2.5"></circle>
|
||||||
|
<circle cx="18.5" cy="18.5" r="2.5"></circle>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div className="mm-info-content">
|
||||||
|
<h4 className="mm-info-title">Vehicles In Transit</h4>
|
||||||
|
<p className="mm-info-text">A live view of active vehicles moving shipments between regional distribution hubs.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="metric-item">
|
<div className="mm-info-row">
|
||||||
<span className="metric-value">220 kw</span>
|
<span className="mm-info-icon" aria-hidden="true">
|
||||||
<span className="metric-label">Solar Output (Self-powered)</span>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"></line>
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div className="mm-info-content">
|
||||||
|
<h4 className="mm-info-title">Packages In Transit</h4>
|
||||||
|
<p className="mm-info-text">Real-time visibility into parcels currently moving through the mid-mile network.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="section-close-btn" onClick={handleClose}>
|
<button className="section-close-btn" onClick={handleClose}>
|
||||||
|
|||||||
29
src/modules/how-it-works-3d/components/sections/Promise.jsx
Normal file
29
src/modules/how-it-works-3d/components/sections/Promise.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import RevealCard from '../ui/RevealCard'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise — Final card
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Closing-statement card and the final beat of the experience, revealed as the
|
||||||
|
* journey closes (right after Stage 03 — Last Mile). It's a regular centred
|
||||||
|
* in-experience overlay card (same chrome as the others) — NOT a separate
|
||||||
|
* scroll section — so the camera, scene, and scroll timing are untouched.
|
||||||
|
*/
|
||||||
|
export default function Promise({ active }) {
|
||||||
|
return (
|
||||||
|
<RevealCard active={active} id="promise-section">
|
||||||
|
<div className="section-badge">The Doormile Promise</div>
|
||||||
|
<h2 className="section-title promise-title">
|
||||||
|
One Connected System.
|
||||||
|
<br />
|
||||||
|
One Promise Kept.
|
||||||
|
</h2>
|
||||||
|
<span className="promise-divider" aria-hidden></span>
|
||||||
|
<p className="section-description promise-desc">
|
||||||
|
Stop managing three separate logistics services. Doormile unifies first,
|
||||||
|
mid and last mile into a single intelligent delivery system powered by
|
||||||
|
MileTruth™ AI.
|
||||||
|
</p>
|
||||||
|
</RevealCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,8 +7,10 @@ export default function Navbar() {
|
|||||||
const lenis = useSceneStore((state) => state.lenis)
|
const lenis = useSceneStore((state) => state.lenis)
|
||||||
|
|
||||||
const handleNavClick = (index) => {
|
const handleNavClick = (index) => {
|
||||||
// Map index (0, 1, 2, 3) to the stable parking progress percentages (0.0, 0.38, 0.76, 0.97).
|
// Map index (0, 1, 2, 3) to the stable parking progress percentages.
|
||||||
const sectionFractions = [0, 0.38, 0.76, 0.97]
|
// The last lands on the analytics-dashboard view, where the closing promise
|
||||||
|
// card is revealed as the user finishes the final stretch of scroll.
|
||||||
|
const sectionFractions = [0, 0.38, 0.76, 0.92]
|
||||||
const targetProgress = sectionFractions[index]
|
const targetProgress = sectionFractions[index]
|
||||||
// Relative to the experience spacer (the section sits below the page hero).
|
// Relative to the experience spacer (the section sits below the page hero).
|
||||||
lenis?.scrollTo(progressToScrollY(targetProgress), { duration: 1.5 })
|
lenis?.scrollTo(progressToScrollY(targetProgress), { duration: 1.5 })
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ export default function RevealCard({ children, active, id, className = "" }) {
|
|||||||
|
|
||||||
// Find all target children inside the card to create a staggered entrance
|
// Find all target children inside the card to create a staggered entrance
|
||||||
const animTargets = card.querySelectorAll(
|
const animTargets = card.querySelectorAll(
|
||||||
'.section-badge, .section-title, .section-subtitle, .section-description, .section-metrics, .section-close-btn, .workflow-step'
|
'.section-badge, .section-title, .section-subtitle, .section-description, .section-metrics, .section-supporting, .section-close-btn'
|
||||||
)
|
)
|
||||||
|
|
||||||
const isAnalytics = id === 'analytics-section'
|
// The promise card (the final card) is centred, so its reveal/exit tweens
|
||||||
|
// must preserve the -50%/-50% centring offset.
|
||||||
|
const isCentered = id === 'promise-section'
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
// Clean up any ongoing animations first
|
// Clean up any ongoing animations first
|
||||||
@@ -21,8 +23,8 @@ export default function RevealCard({ children, active, id, className = "" }) {
|
|||||||
|
|
||||||
// Animate card container in
|
// Animate card container in
|
||||||
gsap.to(card, {
|
gsap.to(card, {
|
||||||
xPercent: isAnalytics ? -50 : 0,
|
xPercent: isCentered ? -50 : 0,
|
||||||
yPercent: isAnalytics ? -50 : 0,
|
yPercent: isCentered ? -50 : 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@@ -52,9 +54,9 @@ export default function RevealCard({ children, active, id, className = "" }) {
|
|||||||
|
|
||||||
// Animate card container out
|
// Animate card container out
|
||||||
gsap.to(card, {
|
gsap.to(card, {
|
||||||
xPercent: isAnalytics ? -50 : 0,
|
xPercent: isCentered ? -50 : 0,
|
||||||
yPercent: isAnalytics ? -50 : 0,
|
yPercent: isCentered ? -50 : 0,
|
||||||
y: isAnalytics ? 18 : 20,
|
y: isCentered ? 18 : 20,
|
||||||
scale: 0.96,
|
scale: 0.96,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
@@ -78,8 +80,8 @@ export default function RevealCard({ children, active, id, className = "" }) {
|
|||||||
className={`section-panel ${active ? 'active' : ''} ${className}`}
|
className={`section-panel ${active ? 'active' : ''} ${className}`}
|
||||||
style={{
|
style={{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transform: id === 'analytics-section'
|
transform: id === 'promise-section'
|
||||||
? 'translate(-50%, -50%) translateY(18px) scale(0.96)'
|
? 'translate(-50%, -50%) translateY(18px) scale(0.96)'
|
||||||
: 'translateY(20px) scale(0.96)',
|
: 'translateY(20px) scale(0.96)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const sections = [
|
|||||||
id: 'last-mile',
|
id: 'last-mile',
|
||||||
title: 'Last Mile Delivery',
|
title: 'Last Mile Delivery',
|
||||||
subtitle: 'Doorstep Courier Services',
|
subtitle: 'Doorstep Courier Services',
|
||||||
description: 'Local delivery units take over, planning optimal paths to transport packages directly to customer doorsteps.',
|
description: 'Local courier fleets take over for the final leg — MileTruth™ AI sequences the fastest doorstep routes and keeps every package tracked through to a confirmed delivery.',
|
||||||
progressStart: 0.5,
|
progressStart: 0.5,
|
||||||
progressEnd: 0.75,
|
progressEnd: 0.75,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,17 @@
|
|||||||
--dm-card-shadow: 0 24px 60px -28px rgba(15, 23, 42, 0.45);
|
--dm-card-shadow: 0 24px 60px -28px rgba(15, 23, 42, 0.45);
|
||||||
--dm-font-head: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
--dm-font-head: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
||||||
--dm-font-body: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
--dm-font-body: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
||||||
|
|
||||||
|
/* Container spacing — matches the site's full-width section frame
|
||||||
|
(.custom-standard-hero-container used by Contact/About/CTA: full width,
|
||||||
|
no max-width cap, 20px gutters, 25px radius). The stage spans ~98% of the
|
||||||
|
viewport with standard gutters rather than sitting in a narrow centred box.
|
||||||
|
Values scale down responsively. */
|
||||||
|
--hiw-max-width: none;
|
||||||
|
--hiw-gutter: 20px;
|
||||||
|
--hiw-gap-top: 20px;
|
||||||
|
--hiw-gap-bottom: 20px;
|
||||||
|
--hiw-radius: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Section shell + self-managed fixed pin ---- */
|
/* ---- Section shell + self-managed fixed pin ---- */
|
||||||
@@ -44,26 +55,38 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The stage is inset to sit inside the site's content container: a centred
|
||||||
|
max-width box with left/right gutters and equal top/bottom spacing. The SAME
|
||||||
|
inset is applied in every pin state (absolute-top → fixed → absolute-bottom)
|
||||||
|
so the stage never jumps at the pin hand-offs. Scroll progress and stage
|
||||||
|
maths are driven by the separate 900vh spacer (#scroll-trigger-trigger), so
|
||||||
|
insetting the stage doesn't touch them. Horizontal centring uses
|
||||||
|
left/right:0 + margin:auto (not transform) so it composes with the existing
|
||||||
|
translateZ(0) GPU layer. The 3D scene is not scaled — the canvas simply fills
|
||||||
|
the smaller framed box and CameraRig already adapts FOV to the aspect. */
|
||||||
.dm-hiw-3d-stage {
|
.dm-hiw-3d-stage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: var(--hiw-gap-top);
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
right: 0;
|
||||||
height: 100vh;
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: calc(100% - 2 * var(--hiw-gutter));
|
||||||
|
max-width: var(--hiw-max-width);
|
||||||
|
height: calc(100vh - var(--hiw-gap-top) - var(--hiw-gap-bottom));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: var(--hiw-radius);
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d.is-pinned .dm-hiw-3d-stage {
|
.dm-hiw-3d.is-pinned .dm-hiw-3d-stage {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
.dm-hiw-3d.is-after .dm-hiw-3d-stage {
|
.dm-hiw-3d.is-after .dm-hiw-3d-stage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 0;
|
bottom: var(--hiw-gap-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-hiw-3d .canvas-wrapper {
|
.dm-hiw-3d .canvas-wrapper {
|
||||||
@@ -194,27 +217,273 @@
|
|||||||
.dm-hiw-3d #last-mile-section {
|
.dm-hiw-3d #last-mile-section {
|
||||||
left: clamp(24px, 5vw, 72px);
|
left: clamp(24px, 5vw, 72px);
|
||||||
}
|
}
|
||||||
|
/* ===========================================================================
|
||||||
|
Stage 02 — Mid Mile Hub: premium enterprise dashboard card.
|
||||||
|
Scoped entirely to #mid-mile-section so the other three stage cards keep the
|
||||||
|
unified chrome. UI only — no transform/animation/scene/scroll changes (the
|
||||||
|
GSAP reveal still targets the same .section-* classes, which are preserved).
|
||||||
|
Sizes use clamp() so they scale down gracefully without fighting the shared
|
||||||
|
responsive rules below.
|
||||||
|
=========================================================================== */
|
||||||
.dm-hiw-3d #mid-mile-section {
|
.dm-hiw-3d #mid-mile-section {
|
||||||
right: clamp(24px, 5vw, 72px);
|
right: clamp(24px, 5vw, 72px);
|
||||||
|
/* Only marginally larger than the sibling side panels (Stage 01/03 are
|
||||||
|
min(404px, 37vw)) — ~13% wider to hold the extra info, not dominant. */
|
||||||
|
width: min(456px, 41vw);
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
padding: clamp(22px, 2.4vw, 30px) clamp(24px, 2.8vw, 34px) clamp(20px, 2.2vw, 26px);
|
||||||
|
/* Clean glassmorphism white surface with subtle transparency. */
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-top: 3px solid var(--dm-red); /* thin red accent along the top edge */
|
||||||
|
box-shadow: 0 32px 72px -30px rgba(15, 23, 42, 0.5);
|
||||||
}
|
}
|
||||||
/* Final ecosystem panel — same card system, centred, a touch wider for its
|
|
||||||
timeline, and reduced from the old 500px so it no longer blocks the scene. */
|
/* ---- Header: stronger hierarchy ---- */
|
||||||
.dm-hiw-3d #analytics-section {
|
.dm-hiw-3d #mid-mile-section .section-badge {
|
||||||
|
font-size: 12px !important;
|
||||||
|
letter-spacing: 0.22em !important;
|
||||||
|
margin: 0 0 10px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-title {
|
||||||
|
font-size: clamp(28px, 2.5vw, 40px) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
line-height: 1.07 !important;
|
||||||
|
letter-spacing: -0.025em !important;
|
||||||
|
margin: 0 0 7px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-subtitle {
|
||||||
|
font-size: clamp(14.5px, 1.2vw, 16px) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--dm-body) !important;
|
||||||
|
margin: 0 0 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Description: larger, more readable, contained measure ---- */
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-description {
|
||||||
|
font-size: clamp(14.5px, 1.15vw, 16px) !important;
|
||||||
|
line-height: 1.72 !important;
|
||||||
|
max-width: 44ch !important;
|
||||||
|
margin: 0 0 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Stage 02 information strip ----------------------------------------
|
||||||
|
Replaces the KPI/metric look with a clean enterprise info panel: a
|
||||||
|
full-width column of icon + title + description rows divided by subtle
|
||||||
|
hairlines. Scoped to #mid-mile-section so the shared .section-metrics /
|
||||||
|
.metric-* styling on the other cards is untouched. The ID specificity also
|
||||||
|
overrides the base `.section-metrics` flex-row + border styling that still
|
||||||
|
rides along on the retained class. */
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%; /* full-width content row */
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 11px 0;
|
||||||
|
}
|
||||||
|
/* Subtle separator between rows only (not above the first / below the last). */
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-row + .mm-info-row {
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.07);
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--dm-red-soft);
|
||||||
|
color: var(--dm-red);
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-icon svg {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-title {
|
||||||
|
font-family: var(--dm-font-head) !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
letter-spacing: -0.01em !important;
|
||||||
|
color: var(--dm-ink) !important;
|
||||||
|
margin: 0 0 3px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-text {
|
||||||
|
font-family: var(--dm-font-body) !important;
|
||||||
|
font-size: 13.5px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
line-height: 1.58 !important;
|
||||||
|
color: var(--dm-body) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Metrics: a simple horizontal row sitting DIRECTLY on the card surface —
|
||||||
|
no nested tiles / boxed containers. A thin divider rule above separates it
|
||||||
|
from the description; within each metric the icon sits to the left of the
|
||||||
|
value, with the label directly beneath it. Compact + uncluttered. ---- */
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-metrics {
|
||||||
|
/* The strip is a vertical column; rows space themselves via their own
|
||||||
|
padding, so no large flex gap (this was a major source of extra height). */
|
||||||
|
gap: 0;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.1);
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .metric-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
column-gap: 12px;
|
||||||
|
row-gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
/* Compact icon chip, vertically centred against the value/label stack. */
|
||||||
|
.dm-hiw-3d #mid-mile-section .metric-icon {
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: clamp(36px, 3vw, 42px);
|
||||||
|
height: clamp(36px, 3vw, 42px);
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: var(--dm-red-soft);
|
||||||
|
color: var(--dm-red);
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .metric-icon svg {
|
||||||
|
width: 56%;
|
||||||
|
height: 56%;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .metric-value {
|
||||||
|
grid-column: 2;
|
||||||
|
align-self: end;
|
||||||
|
font-size: clamp(26px, 2.4vw, 34px) !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
letter-spacing: -0.02em !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .metric-label {
|
||||||
|
grid-column: 2;
|
||||||
|
align-self: start;
|
||||||
|
font-size: 10.5px !important;
|
||||||
|
letter-spacing: 0.1em !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- CTA: larger red pill with subtle shadow + hover lift ---- */
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-close-btn {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: clamp(14px, 1vw, 15px);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: clamp(12px, 1vw, 15px) clamp(24px, 2vw, 30px);
|
||||||
|
box-shadow: 0 16px 30px -12px rgba(192, 18, 39, 0.6);
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-close-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet/mobile: relax the contained measure and tighten the KPI tiles so the
|
||||||
|
larger desktop styling above never overflows the bottom-docked card. */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-description {
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-metrics {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 10px;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .section-close-btn {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Closing promise card — a centred card occupying the final slot. */
|
||||||
|
.dm-hiw-3d #promise-section {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
right: auto;
|
right: auto;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%) translateY(18px) scale(0.97);
|
transform: translate(-50%, -50%) translateY(18px) scale(0.97);
|
||||||
width: min(400px, 90vw);
|
width: min(1060px, 92vw);
|
||||||
max-height: calc(100vh - 140px);
|
max-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
/* Closing-statement card — the final key message of the journey, scaled to the
|
||||||
|
authority of the page hero. The card spans most of the viewport width (small
|
||||||
|
left/right margins) so the headline can run wide; padding is kept lean so the
|
||||||
|
headline — not whitespace — dominates. */
|
||||||
|
.dm-hiw-3d #promise-section {
|
||||||
|
width: min(1060px, 92vw) !important; /* wide: uses most of the viewport */
|
||||||
|
padding: 36px 56px !important;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center; /* centre the group vertically */
|
||||||
|
align-items: center; /* centre the group horizontally */
|
||||||
|
}
|
||||||
|
/* Kicker/subtitle — bumped ~+25% to sit in proportion with the larger headline. */
|
||||||
|
.dm-hiw-3d #promise-section .section-badge {
|
||||||
|
display: block !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
letter-spacing: 0.2em !important;
|
||||||
|
margin: 0 0 18px !important;
|
||||||
|
}
|
||||||
|
/* Dominant headline — matches the hero scale: up to 80px, uppercase, heavy
|
||||||
|
weight, tight hero leading + word-spacing. This is the visual anchor of the
|
||||||
|
card and the strongest type on the screen. */
|
||||||
|
/* Scoped under #promise-section (ID specificity) so it reliably overrides the
|
||||||
|
shared `.section-title` base rule, which is declared later in the file. */
|
||||||
|
.dm-hiw-3d #promise-section .promise-title {
|
||||||
|
font-size: clamp(40px, 6vw, 80px) !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
line-height: 1.1 !important;
|
||||||
|
letter-spacing: -0.03em !important;
|
||||||
|
word-spacing: -0.03em !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
/* Real, flex-centred divider element — exactly under the headline centre. */
|
||||||
|
.dm-hiw-3d .promise-divider {
|
||||||
|
display: block;
|
||||||
|
width: 76px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--dm-red);
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
/* Supporting paragraph — hero body scale (20px) on a wide measure so it reads
|
||||||
|
as a balanced two-to-three line statement instead of wrapping too early.
|
||||||
|
Scoped under #promise-section so it overrides the later `.section-description`
|
||||||
|
base rule. */
|
||||||
|
.dm-hiw-3d #promise-section .promise-desc {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
max-width: 760px !important;
|
||||||
|
font-size: 20px !important;
|
||||||
|
line-height: 1.7 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Unified card — all four stages share this exact chrome ---- */
|
/* ---- Unified card — all four stages share this exact chrome ---- */
|
||||||
.dm-hiw-3d .section-panel {
|
.dm-hiw-3d .section-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: min(340px, 31vw);
|
width: min(404px, 37vw);
|
||||||
max-height: calc(100vh - 168px); /* never taller than the viewport */
|
max-height: calc(100vh - 140px); /* never taller than the viewport */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 24px 26px 26px;
|
padding: 34px 36px 34px;
|
||||||
background: var(--dm-card-bg);
|
background: var(--dm-card-bg);
|
||||||
backdrop-filter: blur(0px);
|
backdrop-filter: blur(0px);
|
||||||
-webkit-backdrop-filter: blur(0px);
|
-webkit-backdrop-filter: blur(0px);
|
||||||
@@ -247,53 +516,53 @@
|
|||||||
font-size: 10.5px !important;
|
font-size: 10.5px !important;
|
||||||
text-transform: uppercase !important;
|
text-transform: uppercase !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
letter-spacing: 0.14em !important;
|
letter-spacing: 0.16em !important;
|
||||||
color: var(--dm-red) !important;
|
color: var(--dm-red) !important;
|
||||||
margin: 0 0 10px !important;
|
margin: 0 0 12px !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-title {
|
.dm-hiw-3d .section-title {
|
||||||
font-family: var(--dm-font-head) !important;
|
font-family: var(--dm-font-head) !important;
|
||||||
font-size: clamp(18px, 1.45vw, 22px) !important;
|
font-size: clamp(19px, 1.55vw, 24px) !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
line-height: 1.15 !important;
|
line-height: 1.18 !important;
|
||||||
letter-spacing: -0.015em !important;
|
letter-spacing: -0.018em !important;
|
||||||
text-transform: none !important;
|
text-transform: none !important;
|
||||||
color: var(--dm-ink) !important;
|
color: var(--dm-ink) !important;
|
||||||
margin: 0 0 4px !important;
|
margin: 0 0 6px !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-subtitle {
|
.dm-hiw-3d .section-subtitle {
|
||||||
font-family: var(--dm-font-body) !important;
|
font-family: var(--dm-font-body) !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
line-height: 1.35 !important;
|
line-height: 1.4 !important;
|
||||||
letter-spacing: 0 !important;
|
letter-spacing: 0.005em !important;
|
||||||
text-transform: none !important;
|
text-transform: none !important;
|
||||||
color: var(--dm-muted) !important;
|
color: var(--dm-muted) !important;
|
||||||
margin: 0 0 14px !important;
|
margin: 0 0 16px !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-description {
|
.dm-hiw-3d .section-description {
|
||||||
font-family: var(--dm-font-body) !important;
|
font-family: var(--dm-font-body) !important;
|
||||||
font-size: 13px !important;
|
font-size: 13.5px !important;
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
line-height: 1.55 !important;
|
line-height: 1.62 !important;
|
||||||
color: var(--dm-body) !important;
|
color: var(--dm-body) !important;
|
||||||
margin: 0 0 18px !important;
|
margin: 0 0 20px !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-metrics {
|
.dm-hiw-3d .section-metrics {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 18px;
|
gap: 20px;
|
||||||
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
padding-top: 16px;
|
padding-top: 18px;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .metric-item {
|
.dm-hiw-3d .metric-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
gap: 4px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .metric-value {
|
.dm-hiw-3d .metric-value {
|
||||||
font-family: var(--dm-font-head) !important;
|
font-family: var(--dm-font-head) !important;
|
||||||
font-size: 18px !important;
|
font-size: 19px !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
line-height: 1.1 !important;
|
line-height: 1.1 !important;
|
||||||
letter-spacing: -0.01em !important;
|
letter-spacing: -0.01em !important;
|
||||||
@@ -304,11 +573,58 @@
|
|||||||
font-size: 9.5px !important;
|
font-size: 9.5px !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
text-transform: uppercase !important;
|
text-transform: uppercase !important;
|
||||||
letter-spacing: 0.07em !important;
|
letter-spacing: 0.08em !important;
|
||||||
color: var(--dm-muted) !important;
|
color: var(--dm-muted) !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .font-green .metric-value { color: #1f9d57 !important; }
|
.dm-hiw-3d .font-green .metric-value { color: #1f9d57 !important; }
|
||||||
|
|
||||||
|
/* Supporting metric (e.g. Last Mile real-time driver tracking) — a full-width
|
||||||
|
live-status row that sits beneath the two headline metrics. */
|
||||||
|
.dm-hiw-3d .section-supporting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
background: rgba(31, 157, 87, 0.07);
|
||||||
|
border: 1px solid rgba(31, 157, 87, 0.18);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d .supporting-dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #1f9d57;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 0 0 0 rgba(31, 157, 87, 0.5);
|
||||||
|
animation: dmHiwPulseDot 2s cubic-bezier(0.16, 1, 0.3, 1) infinite;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d .supporting-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d .supporting-value {
|
||||||
|
font-family: var(--dm-font-head) !important;
|
||||||
|
font-size: 12.5px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
line-height: 1.15 !important;
|
||||||
|
letter-spacing: -0.005em !important;
|
||||||
|
color: var(--dm-ink) !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d .supporting-label {
|
||||||
|
font-family: var(--dm-font-body) !important;
|
||||||
|
font-size: 10px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
letter-spacing: 0.04em !important;
|
||||||
|
color: #1f9d57 !important;
|
||||||
|
}
|
||||||
|
@keyframes dmHiwPulseDot {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(31, 157, 87, 0.45); }
|
||||||
|
70% { box-shadow: 0 0 0 7px rgba(31, 157, 87, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(31, 157, 87, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Animations (keyframes left global; uniquely named) ---- */
|
/* ---- Animations (keyframes left global; uniquely named) ---- */
|
||||||
@keyframes dmHiwScrollWheel {
|
@keyframes dmHiwScrollWheel {
|
||||||
0% { top: 6px; opacity: 1; height: 6px; }
|
0% { top: 6px; opacity: 1; height: 6px; }
|
||||||
@@ -324,62 +640,15 @@
|
|||||||
50% { transform: translateX(8px); }
|
50% { transform: translateX(8px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Final-panel timeline (inside #analytics-section) ---- */
|
|
||||||
.dm-hiw-3d .workflow-steps {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.dm-hiw-3d .workflow-step { display: flex; gap: 14px; }
|
|
||||||
.dm-hiw-3d .step-number-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 24px;
|
|
||||||
}
|
|
||||||
.dm-hiw-3d .step-number {
|
|
||||||
font-family: var(--dm-font-head);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--dm-red);
|
|
||||||
background: var(--dm-red-soft);
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid rgba(192, 18, 39, 0.2);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.dm-hiw-3d .step-line {
|
|
||||||
width: 2px;
|
|
||||||
flex-grow: 1;
|
|
||||||
background: linear-gradient(to bottom, var(--dm-red) 0%, rgba(192, 18, 39, 0.12) 100%);
|
|
||||||
margin: 5px 0;
|
|
||||||
min-height: 16px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.dm-hiw-3d .step-content { flex-grow: 1; padding-bottom: 14px; }
|
|
||||||
.dm-hiw-3d .step-title {
|
|
||||||
font-family: var(--dm-font-head) !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
font-weight: 700 !important;
|
|
||||||
line-height: 1.2 !important;
|
|
||||||
color: var(--dm-ink) !important;
|
|
||||||
margin: 1px 0 3px !important;
|
|
||||||
}
|
|
||||||
.dm-hiw-3d .step-description {
|
|
||||||
font-family: var(--dm-font-body) !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
line-height: 1.5 !important;
|
|
||||||
color: var(--dm-body) !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Responsive ---- */
|
/* ---- Responsive ---- */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
|
/* Moderate gutters on tablet (radius matches the site's tablet section card). */
|
||||||
|
.dm-hiw-3d {
|
||||||
|
--hiw-gutter: 16px;
|
||||||
|
--hiw-gap-top: 16px;
|
||||||
|
--hiw-gap-bottom: 16px;
|
||||||
|
--hiw-radius: 22px;
|
||||||
|
}
|
||||||
.dm-hiw-3d .sections-overlay-container {
|
.dm-hiw-3d .sections-overlay-container {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
@@ -387,28 +656,45 @@
|
|||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-panel {
|
.dm-hiw-3d .section-panel {
|
||||||
padding: 20px 22px 22px;
|
padding: 26px 28px 28px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
/* Tablet/mobile: cards bottom-centred so the truck/scene stays visible above. */
|
/* Tablet/mobile: cards bottom-centred so the truck/scene stays visible above. */
|
||||||
.dm-hiw-3d #first-mile-section,
|
.dm-hiw-3d #first-mile-section,
|
||||||
.dm-hiw-3d #mid-mile-section,
|
.dm-hiw-3d #mid-mile-section,
|
||||||
.dm-hiw-3d #last-mile-section,
|
.dm-hiw-3d #last-mile-section,
|
||||||
.dm-hiw-3d #analytics-section {
|
.dm-hiw-3d #promise-section {
|
||||||
left: 50% !important;
|
left: 50% !important;
|
||||||
right: auto !important;
|
right: auto !important;
|
||||||
top: auto !important;
|
top: auto !important;
|
||||||
bottom: 64px !important;
|
bottom: 64px !important;
|
||||||
transform: translateX(-50%) translateY(18px) scale(0.97) !important;
|
transform: translateX(-50%) translateY(18px) scale(0.97) !important;
|
||||||
width: min(380px, calc(100vw - 40px)) !important;
|
width: min(460px, calc(100vw - 32px)) !important;
|
||||||
max-height: calc(100vh - 200px) !important;
|
max-height: calc(100vh - 180px) !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d #first-mile-section.active,
|
.dm-hiw-3d #first-mile-section.active,
|
||||||
.dm-hiw-3d #mid-mile-section.active,
|
.dm-hiw-3d #mid-mile-section.active,
|
||||||
.dm-hiw-3d #last-mile-section.active,
|
.dm-hiw-3d #last-mile-section.active,
|
||||||
.dm-hiw-3d #analytics-section.active {
|
.dm-hiw-3d #promise-section.active {
|
||||||
transform: translateX(-50%) translateY(0) scale(1) !important;
|
transform: translateX(-50%) translateY(0) scale(1) !important;
|
||||||
}
|
}
|
||||||
|
/* Tablet: keep the promise card wider than the stage cards so the larger
|
||||||
|
headline still runs wide and balanced. */
|
||||||
|
.dm-hiw-3d #promise-section {
|
||||||
|
width: min(760px, calc(100vw - 40px)) !important;
|
||||||
|
padding: 30px 40px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #promise-section .section-badge {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #promise-section .promise-title {
|
||||||
|
font-size: clamp(34px, 5.4vw, 54px) !important;
|
||||||
|
line-height: 1.08 !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #promise-section .promise-desc {
|
||||||
|
font-size: 18px !important;
|
||||||
|
max-width: 640px !important;
|
||||||
|
}
|
||||||
.dm-hiw-3d .side-navigation {
|
.dm-hiw-3d .side-navigation {
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
top: auto;
|
top: auto;
|
||||||
@@ -430,10 +716,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
|
/* Slimmest gutters on phones — matches the hero's mobile frame. */
|
||||||
|
.dm-hiw-3d {
|
||||||
|
--hiw-gutter: 12px;
|
||||||
|
--hiw-gap-top: 12px;
|
||||||
|
--hiw-gap-bottom: 12px;
|
||||||
|
--hiw-radius: 16px;
|
||||||
|
}
|
||||||
.dm-hiw-3d .section-panel,
|
.dm-hiw-3d .section-panel,
|
||||||
.dm-hiw-3d #analytics-section {
|
.dm-hiw-3d #promise-section {
|
||||||
padding: 16px 18px 18px !important;
|
padding: 20px 20px 20px !important;
|
||||||
width: min(340px, calc(100vw - 28px)) !important;
|
width: min(360px, calc(100vw - 24px)) !important;
|
||||||
bottom: 56px !important;
|
bottom: 56px !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-badge {
|
.dm-hiw-3d .section-badge {
|
||||||
@@ -441,7 +734,18 @@
|
|||||||
margin-bottom: 6px !important;
|
margin-bottom: 6px !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-title {
|
.dm-hiw-3d .section-title {
|
||||||
font-size: 18px !important;
|
font-size: 19px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #promise-section .section-badge {
|
||||||
|
font-size: 12.5px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #promise-section .promise-title {
|
||||||
|
font-size: clamp(28px, 8.5vw, 34px) !important;
|
||||||
|
line-height: 1.08 !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #promise-section .promise-desc {
|
||||||
|
font-size: 16px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d .section-subtitle {
|
.dm-hiw-3d .section-subtitle {
|
||||||
font-size: 12.5px !important;
|
font-size: 12.5px !important;
|
||||||
@@ -458,8 +762,17 @@
|
|||||||
}
|
}
|
||||||
.dm-hiw-3d .metric-value { font-size: 16px !important; }
|
.dm-hiw-3d .metric-value { font-size: 16px !important; }
|
||||||
.dm-hiw-3d .metric-label { font-size: 9px !important; }
|
.dm-hiw-3d .metric-label { font-size: 9px !important; }
|
||||||
.dm-hiw-3d .step-title { font-size: 13px !important; }
|
/* Stage 02 info strip stays a tidy hairline-divided column on small screens
|
||||||
.dm-hiw-3d .step-description { font-size: 11.5px !important; }
|
(overrides the shared .section-metrics !important gap/padding above). */
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-strip {
|
||||||
|
gap: 0 !important;
|
||||||
|
padding-top: 2px !important;
|
||||||
|
}
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-row { padding: 13px 0; gap: 13px; }
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-icon { width: 36px; height: 36px; }
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-icon svg { width: 19px; height: 19px; }
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-title { font-size: 14px !important; }
|
||||||
|
.dm-hiw-3d #mid-mile-section .mm-info-text { font-size: 12.5px !important; }
|
||||||
.dm-hiw-3d .side-navigation {
|
.dm-hiw-3d .side-navigation {
|
||||||
bottom: 8px !important;
|
bottom: 8px !important;
|
||||||
gap: 12px !important;
|
gap: 12px !important;
|
||||||
@@ -469,17 +782,17 @@
|
|||||||
.dm-hiw-3d #first-mile-section,
|
.dm-hiw-3d #first-mile-section,
|
||||||
.dm-hiw-3d #mid-mile-section,
|
.dm-hiw-3d #mid-mile-section,
|
||||||
.dm-hiw-3d #last-mile-section,
|
.dm-hiw-3d #last-mile-section,
|
||||||
.dm-hiw-3d #analytics-section {
|
.dm-hiw-3d #promise-section {
|
||||||
left: 50% !important;
|
left: 50% !important;
|
||||||
right: auto !important;
|
right: auto !important;
|
||||||
bottom: 56px !important;
|
bottom: 56px !important;
|
||||||
width: min(340px, calc(100vw - 28px)) !important;
|
width: min(360px, calc(100vw - 24px)) !important;
|
||||||
transform: translateX(-50%) translateY(18px) scale(0.97) !important;
|
transform: translateX(-50%) translateY(18px) scale(0.97) !important;
|
||||||
}
|
}
|
||||||
.dm-hiw-3d #first-mile-section.active,
|
.dm-hiw-3d #first-mile-section.active,
|
||||||
.dm-hiw-3d #mid-mile-section.active,
|
.dm-hiw-3d #mid-mile-section.active,
|
||||||
.dm-hiw-3d #last-mile-section.active,
|
.dm-hiw-3d #last-mile-section.active,
|
||||||
.dm-hiw-3d #analytics-section.active {
|
.dm-hiw-3d #promise-section.active {
|
||||||
transform: translateX(-50%) translateY(0) scale(1) !important;
|
transform: translateX(-50%) translateY(0) scale(1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user