update how it works

This commit is contained in:
2026-06-08 22:21:42 +05:30
parent 3d53f82e7b
commit 0ef51540e9
9 changed files with 523 additions and 245 deletions

View File

@@ -2,6 +2,14 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
// Required by the How It Works 3D experience. React StrictMode double-invokes
// mount/effects in dev, which tears down and re-creates the WebGL context of
// the heavy 32MB scene mid-initialization — the context is lost ("THREE.
// WebGLRenderer: Context Lost") and the canvas stays blank. This is a known
// React-Three-Fiber + StrictMode incompatibility. Disabling it is a DEV-ONLY
// change (production never runs StrictMode's double-mount) and does not affect
// any other page's runtime behavior.
reactStrictMode: false,
images: {
unoptimized: true,
formats: ["image/avif", "image/webp"],

8
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"react-leaflet": "^5.0.0",
"three": "^0.184.0",
"three": "0.171.0",
"zustand": "^5.0.14"
},
"devDependencies": {
@@ -10764,9 +10764,9 @@
}
},
"node_modules/three": {
"version": "0.184.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
"version": "0.171.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz",
"integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==",
"license": "MIT"
},
"node_modules/three-mesh-bvh": {

View File

@@ -26,7 +26,7 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"react-leaflet": "^5.0.0",
"three": "^0.184.0",
"three": "0.171.0",
"zustand": "^5.0.14"
},
"devDependencies": {

View File

@@ -19,7 +19,9 @@ export default function HowItWorksHero() {
return (
<>
<style dangerouslySetInnerHTML={{ __html: `
<style
dangerouslySetInnerHTML={{
__html: `
.howits-hero-custom-bg.elementor-repeater-item-3264830,
.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;
@@ -219,19 +221,44 @@ export default function HowItWorksHero() {
display: none !important;
}
}
`}} />
`,
}}
/>
<div className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="741f56c" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-6c7cbcb elementor-widget elementor-widget-logico_content_slider" data-id="6c7cbcb" data-element_type="widget" data-e-type="widget" data-widget_type="logico_content_slider.default">
<div
className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent"
data-id="741f56c"
data-element_type="container"
data-e-type="container"
>
<div
className="elementor-element elementor-element-6c7cbcb elementor-widget elementor-widget-logico_content_slider"
data-id="6c7cbcb"
data-element_type="widget"
data-e-type="widget"
data-widget_type="logico_content_slider.default"
>
<div className="elementor-widget-container">
<div className="logico-content-slider-widget">
<div className="content-slider-wrapper">
<div className="content-slider-container">
<div className="content-slider owl-carousel owl-theme nav-view-vertical nav-h-position-right nav-v-position-bottom owl-loaded owl-drag">
<div className="owl-stage-outer" style={{ position: "relative", overflow: "hidden", height: "800px" }}>
<div className="owl-stage" style={{ position: "relative", width: "100%", height: "100%" }}>
<div
className="owl-stage-outer"
style={{
position: "relative",
overflow: "hidden",
height: "800px",
}}
>
<div
className="owl-stage"
style={{
position: "relative",
width: "100%",
height: "100%",
}}
>
{/* Slide 1 */}
<div
className={`owl-item ${activeSlide === 0 ? "active" : ""}`}
@@ -239,20 +266,30 @@ export default function HowItWorksHero() {
position: "relative",
width: "100%",
opacity: activeSlide === 0 ? 1 : 0,
visibility: activeSlide === 0 ? "visible" : "hidden",
transition: "opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
zIndex: activeSlide === 0 ? 2 : 1
visibility:
activeSlide === 0 ? "visible" : "hidden",
transition:
"opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
zIndex: activeSlide === 0 ? 2 : 1,
}}
>
<div className="content-item slider-item elementor-repeater-item-3264830 slide-style-standard howits-hero-custom-bg">
<div className="slide-content">
<div className="slide-content-inner">
<h1 className="content-slider-item-heading logico-content-wrapper-1">
<span className="heading-content">One Journey. Complete<br />Control.</span>
<span className="heading-content">
One Journey. Complete
<br />
Control.
</span>
</h1>
<div className="content-slider-item-text logico-content-wrapper-2">
<div className="text-content">
<p>See how Doormile connects first, mid, and last mile into a seamless delivery experience powered by MileTruth AI.</p>
<p>
See how Doormile connects first, mid, and
last mile into a seamless delivery
experience powered by MileTruth AI.
</p>
</div>
</div>
</div>
@@ -269,27 +306,36 @@ export default function HowItWorksHero() {
left: 0,
width: "100%",
opacity: activeSlide === 1 ? 1 : 0,
visibility: activeSlide === 1 ? "visible" : "hidden",
transition: "opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
zIndex: activeSlide === 1 ? 2 : 1
visibility:
activeSlide === 1 ? "visible" : "hidden",
transition:
"opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
zIndex: activeSlide === 1 ? 2 : 1,
}}
>
<div className="content-item slider-item elementor-repeater-item-6867061 slide-style-standard howits-hero-custom-bg">
<div className="slide-content">
<div className="slide-content-inner">
<h1 className="content-slider-item-heading logico-content-wrapper-1">
<span className="heading-content">A New Freight<br />Experience</span>
<span className="heading-content">
A New Logisitics
<br />
Experience
</span>
</h1>
<div className="content-slider-item-text logico-content-wrapper-2">
<div className="text-content">
<p>See how Doormile connects first, mid, and last mile into a seamless delivery experience powered by MileTruth AI.</p>
<p>
See how Doormile connects first, mid, and
last mile into a seamless delivery
experience powered by MileTruth AI.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -298,42 +344,98 @@ export default function HowItWorksHero() {
<button
type="button"
className="owl-next"
onClick={() => setActiveSlide((prev) => (prev === 0 ? 1 : 0))}
onClick={() =>
setActiveSlide((prev) => (prev === 0 ? 1 : 0))
}
aria-label="Next"
style={{ cursor: "pointer", border: "none", outline: "none" }}
style={{
cursor: "pointer",
border: "none",
outline: "none",
}}
/>
<button
type="button"
className="owl-prev"
onClick={() => setActiveSlide((prev) => (prev === 0 ? 1 : 0))}
onClick={() =>
setActiveSlide((prev) => (prev === 0 ? 1 : 0))
}
aria-label="Previous"
style={{ cursor: "pointer", border: "none", outline: "none" }}
style={{
cursor: "pointer",
border: "none",
outline: "none",
}}
/>
</div>
{/* Progress indicators */}
<div className="slider-footer slider-footer-position-after slider-footer-width-full slider-footer-view-inside">
<div className="slider-footer-content">
<div className="slider-pagination" style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "10px" }}>
<div className="slider-progress-wrapper" style={{ marginRight: "35px", display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
<div style={{ fontSize: "16px", fontWeight: 600, color: "#FFFFFF", marginBottom: "4px" }}>
<span className="slider-progress-current">{activeSlide === 0 ? "01" : "02"}</span>
<div
className="slider-pagination"
style={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
gap: "10px",
}}
>
<div
className="slider-progress-wrapper"
style={{
marginRight: "35px",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}}
>
<div
style={{
fontSize: "16px",
fontWeight: 600,
color: "#FFFFFF",
marginBottom: "4px",
}}
>
<span className="slider-progress-current">
{activeSlide === 0 ? "01" : "02"}
</span>
{" / "}
<span className="slider-progress-all" style={{ opacity: 0.6 }}>02</span>
<span
className="slider-progress-all"
style={{ opacity: 0.6 }}
>
02
</span>
</div>
{/* Progress line — red bar slides to match the active slide (mirrors the home hero) */}
<div style={{ width: "80px", height: "2px", background: "rgba(255, 255, 255, 0.2)", position: "relative", borderRadius: "1px", overflow: "hidden" }}>
<div style={{
<div
style={{
width: "80px",
height: "2px",
background: "rgba(255, 255, 255, 0.2)",
position: "relative",
borderRadius: "1px",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
left: activeSlide === 0 ? "0" : "50%",
width: "50%",
height: "100%",
background: "#c01227",
transition: "left 0.3s ease"
}} />
transition: "left 0.3s ease",
}}
/>
</div>
</div>
<div className="owl-dots owl-dots-6c7cbcb" style={{ display: "none" }}>
<div
className="owl-dots owl-dots-6c7cbcb"
style={{ display: "none" }}
>
<button
type="button"
role="button"
@@ -354,7 +456,6 @@ export default function HowItWorksHero() {
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -2,35 +2,57 @@ import React from "react";
import Image from "next/image";
import { ScrollReveal } from "../../animations/Reveal";
type Stat = {
value: string;
label: string;
};
type Stage = {
img: string;
label: string;
title: string;
subtitle?: string;
desc: string;
points: string[];
stats?: Stat[];
};
const STAGES: Stage[] = [
{
img: "/images/first-mile-approach.jpg",
label: "01 / First Mile",
title: "Origin to Hub",
desc: "We collect freight directly from your facility, optimise vehicle assignment in real time, and consolidate loads for maximum efficiency before they reach the hub.",
label: "Stage 01",
title: "First Mile Warehouse",
subtitle: "Consolidation & Prep",
desc: "Incoming shipments are securely loaded, checked, and queued for transfer in our high-capacity fulfillment centers.",
points: ["AI-scheduled pickups", "Dynamic load consolidation", "Yard & dock management"],
stats: [
{ value: "14,250", label: "Parcels Processed" },
{ value: "99.98%", label: "Sorting Accuracy" }
]
},
{
img: "/images/mid-mile-approach.jpg",
label: "02 / Mid Mile",
title: "Hub to Hub Transit",
desc: "Freight moves between hubs on optimised line-haul routes. Real-time tracking, cross-docking, and SLA monitoring keep every shipment on schedule.",
label: "Stage 02",
title: "Mid Mile Transit",
subtitle: "Hub-to-Hub Transport",
desc: "Freight is routed dynamically through our network of strategically located hubs using automated sortation and linehaul scheduling.",
points: ["Optimised line-haul routing", "Cross-docking & sortation", "Live SLA monitoring"],
stats: [
{ value: "1,240+", label: "Daily Line-Hauls" },
{ value: "98.5%", label: "SLA Adherence" }
]
},
{
img: "/images/last-mile-approach.jpg",
label: "03 / Last Mile",
title: "Hub to Doorstep",
desc: "The final and most complex phase. We optimise multi-stop routes, deliver within precise windows, and capture digital proof of delivery at every door.",
label: "Stage 03",
title: "Last Mile Delivery",
subtitle: "Hub to Doorstep",
desc: "The final handoff. Our routing engine optimizes multi-stop itineraries to deliver parcels directly to customers' doorsteps in record time.",
points: ["Multi-stop route optimisation", "Precise delivery windows", "Digital proof of delivery"],
stats: [
{ value: "450K+", label: "Happy Deliveries" },
{ value: "2.8 Hours", label: "Average Turnaround" }
]
},
];
@@ -40,18 +62,12 @@ export default function WhyChooseDoormile() {
<style dangerouslySetInnerHTML={{ __html: `
/* =====================================================================
"Why Businesses Choose Doormile" — First / Mid / Last Mile stage cards.
Dark rounded card on the white page (consistent with the Miles3
section), each stage shown with a photo, numbered red label, title,
description and a red-checkmark feature list.
Card titles are <h3>; theme-core forces a dark color on bare headings
(.logico-front-end h3:not([class*=logico-title-h]) @ (0,2,1)), so the
white title rule is prefixed to outrank it.
===================================================================== */
.wcd-section {
display: flex;
flex-direction: column;
width: auto;
margin: -250px 20px 20px 20px; /* Snug pull-up overlap to touch Miles3 columns without covering their text! */
margin: -250px 20px 20px 20px;
background-color: #1F1F1F;
border-radius: 0 0 25px 25px;
padding: 50px 0 110px;
@@ -63,7 +79,6 @@ export default function WhyChooseDoormile() {
padding: 0 50px;
box-sizing: border-box;
}
/* Centered header block (eyebrow + heading) with a faint map backdrop */
.wcd-head {
position: relative;
text-align: center;
@@ -78,16 +93,16 @@ export default function WhyChooseDoormile() {
content: "";
position: absolute;
left: 50%;
top: 75%; /* Shifted down to the bottom of the header block to overlap the top of the cards */
top: 75%;
transform: translateX(-50%);
width: min(1180px, 95%);
aspect-ratio: 2 / 1;
background: url('/images/bg-map.png') center / contain no-repeat;
opacity: 0.06; /* Elegant faint visibility */
filter: invert(1); /* Invert dark map dots to white/light-gray to make them visible on the #1F1F1F background */
opacity: 0.06;
filter: invert(1);
z-index: 0;
pointer-events: none;
animation: wcd-float 20s ease-in-out infinite; /* Premium floating map animation */
animation: wcd-float 20s ease-in-out infinite;
}
.wcd-card-wrapper {
display: flex;
@@ -127,18 +142,23 @@ export default function WhyChooseDoormile() {
width: 100%;
}
/* Premium Glassmorphism & Card Interaction */
.wcd-card {
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.02);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
overflow: hidden;
transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: border-color 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
box-shadow 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.wcd-card:hover {
border-color: #c01227 !important;
box-shadow: 0 10px 30px rgba(192, 18, 39, 0.25) !important;
box-shadow: 0 20px 40px rgba(192, 18, 39, 0.15), inset 0 0 20px rgba(255, 255, 255, 0.02) !important;
transform: translateY(-8px);
}
.wcd-card-media {
@@ -152,75 +172,106 @@ export default function WhyChooseDoormile() {
transition: transform 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.wcd-card:hover .wcd-card-media img {
transform: scale(1.06);
transform: scale(1.05);
}
/* Body Data Container (Unified Div) */
.wcd-card-body {
display: flex;
flex-direction: column;
flex: 1;
padding: 40px;
}
/* Modern Pill Badge for Label */
.wcd-card-label {
display: inline-flex;
align-items: center;
align-self: flex-start;
padding: 6px 14px;
background: rgba(192, 18, 39, 0.08);
border: 1px solid rgba(192, 18, 39, 0.25);
border-radius: 100px;
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 14px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #c01227;
color: #ff3344;
margin: 0 0 20px;
transition: all 0.3s ease;
}
.wcd-card:hover .wcd-card-label {
background: rgba(192, 18, 39, 0.18);
border-color: #c01227;
box-shadow: 0 0 10px rgba(192, 18, 39, 0.15);
}
.logico-front-end .wcd-section h3.wcd-card-title {
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 32px;
font-size: 26px;
font-weight: 700;
line-height: 1.1;
line-height: 1.2;
letter-spacing: -0.02em;
text-transform: uppercase;
color: #FFFFFF;
-webkit-text-fill-color: #FFFFFF;
margin: 0 0 22px;
margin: 0 0 4px;
}
.wcd-card-subtitle {
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.55);
margin: 0 0 20px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.wcd-card-divider {
height: 1px;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.02) 100%);
margin: 0 0 20px;
width: 100%;
}
.wcd-card-desc {
font-size: 17px;
font-size: 15px;
font-weight: 400;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
margin: 0 0 34px;
color: rgba(255, 255, 255, 0.65);
margin: 0 0 24px;
}
.wcd-card-points {
list-style: none;
margin: auto 0 0;
margin: 0 0 30px;
padding: 0;
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
}
.wcd-section .wcd-card-points li {
/* Flex row so the check icon and its label always sit on the same line.
Scoped with .wcd-section to outrank the global ".logico-front-end ul li"
theme rule, which adds 1.7em padding + a fontello bullet icon. */
display: flex;
align-items: flex-start;
gap: 12px;
padding-left: 0;
font-size: 16px;
font-weight: 700;
font-size: 15px;
font-weight: 600;
line-height: 1.3;
color: #FFFFFF;
}
/* Suppress the theme's default fontello list bullet
(.logico-front-end ul li:before) so only our circle-check SVG renders. */
.wcd-section .wcd-card-points li::before {
content: none;
display: none;
}
/* Clean circle-check feature icon (inline SVG, see markup below) — replaces
the old border-based chevron. Brand red with thin, rounded strokes. */
.wcd-card-points .wcd-check {
flex: 0 0 auto;
width: 18px;
height: 18px;
margin-top: 0.12em;
margin-top: 0.08em;
color: #c01227;
transition: transform 0.3s ease;
}
@@ -228,16 +279,47 @@ export default function WhyChooseDoormile() {
transform: scale(1.1);
}
/* Integrated Stats Section */
.wcd-card-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: auto;
padding-top: 24px;
border-top: 1px dashed rgba(255, 255, 255, 0.12);
}
.wcd-stat-item {
display: flex;
flex-direction: column;
}
.wcd-stat-value {
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 24px;
font-weight: 800;
color: #FFFFFF;
line-height: 1.1;
letter-spacing: -0.02em;
transition: color 0.3s ease;
}
.wcd-card:hover .wcd-stat-value {
color: #ff3344;
}
.wcd-stat-label {
font-size: 11px;
font-weight: 600;
color: rgba(255, 255, 255, 0.45);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 4px;
line-height: 1.3;
}
@media (max-width: 1020px) {
/* No pull-up overlap on mobile/tablet: the Miles3 cards stack into a
12 col layout, so a negative margin-top covers the last card's
text. Both sections share #1F1F1F + equal side margins, so butting
them at margin-top:0 keeps the seamless dark panel. */
.wcd-section { margin: 0 15px 15px 15px; padding: 40px 0 80px; }
.wcd-inner { padding: 0 30px; }
.wcd-grid { grid-template-columns: 1fr; gap: 24px; }
.wcd-card-body { padding: 32px; }
.logico-front-end .wcd-section h3.wcd-card-title { font-size: 28px; }
.logico-front-end .wcd-section h3.wcd-card-title { font-size: 24px; }
}
@media (max-width: 480px) {
.wcd-section { margin: 0 12px 12px 12px; border-radius: 0 0 20px 20px; padding: 30px 0 64px; }
@@ -273,10 +355,20 @@ export default function WhyChooseDoormile() {
sizes="(max-width: 1020px) 100vw, 33vw"
/>
</div>
{/* Single Parent Div for All Data Content */}
<div className="wcd-card-body">
<div className="wcd-card-label">{stage.label}</div>
<h3 className="wcd-card-title">{stage.title}</h3>
{stage.subtitle && (
<div className="wcd-card-subtitle">{stage.subtitle}</div>
)}
<div className="wcd-card-divider" />
<p className="wcd-card-desc">{stage.desc}</p>
<ul className="wcd-card-points">
{stage.points.map((point) => (
<li key={point}>
@@ -297,6 +389,18 @@ export default function WhyChooseDoormile() {
</li>
))}
</ul>
{/* Integrated Statistics Grid */}
{stage.stats && stage.stats.length > 0 && (
<div className="wcd-card-stats">
{stage.stats.map((stat) => (
<div key={stat.label} className="wcd-stat-item">
<span className="wcd-stat-value">{stat.value}</span>
<span className="wcd-stat-label">{stat.label}</span>
</div>
))}
</div>
)}
</div>
</article>
</ScrollReveal>

View File

@@ -15,11 +15,21 @@ export default function CameraRig() {
useFrame((state, delta) => {
const { camera } = state
// maath's easing.damp3 divides by delta internally; a delta of 0 (coincident
// or first frames) yields NaN that poisons the damper and would push the
// camera to NaN — blanking the whole scene. Clamp delta to a safe range.
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
// Smoothly damp the camera position towards the target position
easing.damp3(camera.position, targetPosition, 0.35, delta)
easing.damp3(camera.position, targetPosition, 0.35, dt)
// Smoothly damp the camera focus target (lookAt)
easing.damp3(currentLookAt.current, lookAtTarget, 0.25, delta)
easing.damp3(currentLookAt.current, lookAtTarget, 0.25, dt)
// Defensive recovery: if anything upstream produced a non-finite value, snap
// back to the target so the camera never gets stuck at NaN (black screen).
if (!Number.isFinite(camera.position.x)) camera.position.copy(targetPosition)
if (!Number.isFinite(currentLookAt.current.x)) currentLookAt.current.copy(lookAtTarget)
// Apply lookAt orientation using the interpolated target vector
camera.lookAt(currentLookAt.current)

View File

@@ -2,7 +2,6 @@ import React, { useRef, useEffect } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { Environment, SoftShadows } from '@react-three/drei'
import * as THREE from 'three'
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing'
import { Model as SceneModel } from '../models/Scene3D'
import CameraRig from './CameraRig'
import TruckAnimation from './TruckAnimation'
@@ -127,22 +126,16 @@ export default React.memo(function Experience({ dashboardRefs, wheelRefs, truckR
{/* Dynamic camera rig with damping and target interpolation */}
<CameraRig />
{/* Post-processing Bloom + Vignette only.
The original Vite code added SSAO with a NormalPass, but on this heavy
scene (32MB GLB, ~500 meshes, SoftShadows) the extra full-scene normal
render exhausts the WebGL context and it is lost (blank canvas). The
site's other R3F canvases (e.g. StrategyCanvas) use a Bloom-only
composer for the same reason; Bloom + the screen-space Vignette keep the
cinematic look without the SSAO normal pass. */}
<EffectComposer multisampling={2}>
<Bloom
intensity={0.2}
luminanceThreshold={0.95}
luminanceSmoothing={0.05}
mipmapBlur
/>
<Vignette eskil={false} offset={0.1} darkness={0.4} />
</EffectComposer>
{/* Post-processing (EffectComposer/Bloom/Vignette) intentionally omitted.
@react-three/postprocessing's EffectComposer reads
`renderer.getContextAttributes().alpha` while initializing its buffers;
under Next dev's React StrictMode the canvas's WebGL context is torn
down and re-created, so that read hits a null context and throws
"Cannot read properties of null (reading 'alpha')", crashing the whole
scene. Dropping the composer renders the scene directly (lighting +
shadows + environment carry the look). To re-add Bloom later, set
`reactStrictMode: false` in next.config.ts and restore a Bloom-only
composer. */}
</Canvas>
</div>
)

View File

@@ -38,6 +38,14 @@ export default function TruckAnimation({ truckRef, wheelRefs }) {
useFrame((state, delta) => {
if (!truckRef.current) return
// r3f can emit delta === 0 (coincident frames, the first frame, or after a
// long main-thread block while the 32MB scene parses). maath's easing.damp
// divides by delta internally, so a 0 yields NaN/Infinity that poisons the
// damper's stored velocity — and from then on truckPath.getPoint(NaN) throws
// "Cannot read properties of undefined (reading 'x')". Clamp delta to a safe
// positive range before any damping.
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
// Detect scroll direction changes from the actual page scroll progress
const deltaScroll = scrollProgress - lastScrollProgressRef.current
if (deltaScroll < -0.0001) {
@@ -95,7 +103,15 @@ export default function TruckAnimation({ truckRef, wheelRefs }) {
}
// Smoothly damp the 1D progress scalar along the curve path
easing.damp(dampedProgressRef, 'current', truckProgress, 0.30, delta)
easing.damp(dampedProgressRef, 'current', truckProgress, 0.30, dt)
// Defensive: keep the spline parameter a finite value in [0,1]. getPoint(NaN)
// or an out-of-range t reads an undefined curve point and throws.
if (!Number.isFinite(dampedProgressRef.current)) {
dampedProgressRef.current = truckProgress
if (dampedProgressRef.__damp) dampedProgressRef.__damp = {} // clear any poisoned velocity
}
dampedProgressRef.current = THREE.MathUtils.clamp(dampedProgressRef.current, 0, 1)
// Evaluate the 3D position and orientation directly on the spline curve
const position = truckPath.getPoint(dampedProgressRef.current)
@@ -128,7 +144,7 @@ export default function TruckAnimation({ truckRef, wheelRefs }) {
}
// Smoothly damp the extra rotation angle directly (prevents pitch/roll glitches or 3D target collapse)
easing.damp(extraRotationRef, 'current', targetExtraRotation, 0.20, delta)
easing.damp(extraRotationRef, 'current', targetExtraRotation, 0.20, dt)
// Apply the yaw pivot around the local vertical axis
truckRef.current.rotateY(extraRotationRef.current)

View File

@@ -15,16 +15,31 @@
here because the site has a fixed header and an ancestor `overflow:hidden`.
============================================================================ */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
/* ---- Doormile card design tokens (single system for all four stages) ----
Brand red #c01227, ink #1F1F1F, fonts inherited from the site (Manrope body /
Space Grotesk headings, exposed as CSS vars on <html> by layout.tsx). */
.dm-hiw-3d {
--dm-red: #c01227;
--dm-red-soft: rgba(192, 18, 39, 0.10);
--dm-ink: #1f1f1f;
--dm-body: #4b5563;
--dm-muted: #8a8f98;
--dm-card-bg: rgba(255, 255, 255, 0.86);
--dm-card-border: rgba(15, 23, 42, 0.08);
--dm-card-radius: 18px;
--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-body: var(--font-manrope), system-ui, -apple-system, sans-serif;
}
/* ---- Section shell + self-managed fixed pin ---- */
.dm-hiw-3d {
position: relative;
width: 100%;
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.47;
font-family: var(--dm-font-body);
line-height: 1.5;
font-weight: 400;
color: #1d1d1f;
color: var(--dm-ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -117,39 +132,40 @@
transform: scale(1.15);
}
.dm-hiw-3d .side-nav-item.active .side-nav-label {
color: #0071e3;
color: var(--dm-red);
}
.dm-hiw-3d .side-nav-item.active .side-nav-dot {
background-color: #0071e3;
background-color: var(--dm-red);
transform: scale(1.3);
box-shadow: 0 0 8px rgba(0, 113, 227, 0.3);
box-shadow: 0 0 8px rgba(192, 18, 39, 0.35);
}
.dm-hiw-3d .section-close-btn {
margin-top: 20px;
background-color: #0071e3;
margin-top: 18px;
background-color: var(--dm-red);
color: #ffffff;
border: none;
font-family: inherit;
font-family: var(--dm-font-body);
font-size: 12px;
font-weight: 600;
padding: 8px 16px;
border-radius: 18px;
letter-spacing: 0.01em;
padding: 9px 18px;
border-radius: 999px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 4px 12px rgba(0, 113, 227, 0.15);
transition: background-color 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
box-shadow: 0 8px 18px -8px rgba(192, 18, 39, 0.55);
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
}
.dm-hiw-3d .section-close-btn:hover {
background-color: #0077ed;
box-shadow: 0 6px 16px rgba(0, 113, 227, 0.3);
background-color: #a30f20;
box-shadow: 0 10px 22px -8px rgba(192, 18, 39, 0.7);
transform: translateY(-1px);
}
.dm-hiw-3d .section-close-btn:active {
transform: translateY(1px);
transform: translateY(0);
}
/* ---- Story stage text panels ---- */
@@ -162,112 +178,136 @@
align-items: center;
}
/* Side placement keeps the moving truck / scene visible in the centre, with the
card vertically centred (top/bottom:0 + margin:auto works with the GSAP reveal,
which only animates translateY/scale). */
.dm-hiw-3d #first-mile-section,
.dm-hiw-3d #mid-mile-section,
.dm-hiw-3d #last-mile-section {
top: 0;
bottom: 0;
height: max-content;
margin-top: auto;
margin-bottom: auto;
}
.dm-hiw-3d #first-mile-section,
.dm-hiw-3d #last-mile-section {
left: 6%;
left: clamp(24px, 5vw, 72px);
}
.dm-hiw-3d #mid-mile-section {
right: 6%;
right: clamp(24px, 5vw, 72px);
}
/* 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. */
.dm-hiw-3d #analytics-section {
left: 50%;
right: auto;
top: 50%;
transform: translate(-50%, -50%) translateY(18px) scale(0.97);
max-width: 500px;
width: 90%;
background: rgba(20, 21, 26, 0.88); /* Deep slate blackboard theme */
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
color: #ffffff;
}
.dm-hiw-3d #analytics-section.active {
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
}
.dm-hiw-3d #analytics-section .section-title { color: #ffffff; }
.dm-hiw-3d #analytics-section .section-subtitle { color: #a1a1a6; }
.dm-hiw-3d #analytics-section .step-title { color: #ffffff; }
.dm-hiw-3d #analytics-section .step-description { color: #a1a1a6; }
.dm-hiw-3d #analytics-section .step-line {
background: linear-gradient(to bottom, #0071e3 40%, rgba(255, 255, 255, 0.1) 100%);
width: min(400px, 90vw);
max-height: calc(100vh - 140px);
}
/* ---- Unified card — all four stages share this exact chrome ---- */
.dm-hiw-3d .section-panel {
position: absolute;
max-width: 380px;
padding: 30px;
background: rgba(255, 255, 255, 0.76);
width: min(340px, 31vw);
max-height: calc(100vh - 168px); /* never taller than the viewport */
overflow-y: auto;
padding: 24px 26px 26px;
background: var(--dm-card-bg);
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 24px;
border: 1px solid var(--dm-card-border);
border-top: 3px solid var(--dm-red); /* brand accent that unifies every card */
border-radius: var(--dm-card-radius);
box-shadow: var(--dm-card-shadow);
opacity: 0;
transform: translateY(18px) scale(0.97);
visibility: hidden;
transition: backdrop-filter 0.9s cubic-bezier(0.16, 1, 0.3, 1),
-webkit-backdrop-filter 0.9s cubic-bezier(0.16, 1, 0.3, 1),
visibility 0.9s;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
pointer-events: none;
box-sizing: border-box;
}
.dm-hiw-3d .section-panel.active {
visibility: visible;
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
backdrop-filter: blur(22px) saturate(140%);
-webkit-backdrop-filter: blur(22px) saturate(140%);
pointer-events: auto;
}
/* Typography — one scale across all cards. !important defeats the page's
Elementor h2/h3 styles (loaded via /css/site.css with their own !important),
which were inflating these headings to ~80px and overflowing the cards. */
.dm-hiw-3d .section-badge {
font-size: 11px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 1px;
color: #0071e3;
margin-bottom: 8px;
display: inline-block;
font-family: var(--dm-font-body) !important;
font-size: 10.5px !important;
text-transform: uppercase !important;
font-weight: 700 !important;
letter-spacing: 0.14em !important;
color: var(--dm-red) !important;
margin: 0 0 10px !important;
}
.dm-hiw-3d .section-title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.6px;
color: #1d1d1f;
margin: 0 0 4px 0;
font-family: var(--dm-font-head) !important;
font-size: clamp(18px, 1.45vw, 22px) !important;
font-weight: 700 !important;
line-height: 1.15 !important;
letter-spacing: -0.015em !important;
text-transform: none !important;
color: var(--dm-ink) !important;
margin: 0 0 4px !important;
}
.dm-hiw-3d .section-subtitle {
font-size: 15px;
font-weight: 500;
color: #86868b;
margin: 0 0 14px 0;
font-family: var(--dm-font-body) !important;
font-size: 13px !important;
font-weight: 500 !important;
line-height: 1.35 !important;
letter-spacing: 0 !important;
text-transform: none !important;
color: var(--dm-muted) !important;
margin: 0 0 14px !important;
}
.dm-hiw-3d .section-description {
font-size: 13px;
line-height: 1.5;
color: #515154;
margin-bottom: 20px;
font-family: var(--dm-font-body) !important;
font-size: 13px !important;
font-weight: 400 !important;
line-height: 1.55 !important;
color: var(--dm-body) !important;
margin: 0 0 18px !important;
}
.dm-hiw-3d .section-metrics {
display: flex;
gap: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
gap: 18px;
border-top: 1px solid rgba(15, 23, 42, 0.08);
padding-top: 16px;
}
.dm-hiw-3d .metric-item {
display: flex;
flex-direction: column;
gap: 3px;
flex: 1;
}
.dm-hiw-3d .metric-value {
font-size: 20px;
font-weight: 600;
color: #1d1d1f;
letter-spacing: -0.3px;
font-family: var(--dm-font-head) !important;
font-size: 18px !important;
font-weight: 700 !important;
line-height: 1.1 !important;
letter-spacing: -0.01em !important;
color: var(--dm-ink) !important;
}
.dm-hiw-3d .metric-label {
font-size: 10px;
font-weight: 500;
color: #86868b;
font-family: var(--dm-font-body) !important;
font-size: 9.5px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.07em !important;
color: var(--dm-muted) !important;
}
.dm-hiw-3d .font-green .metric-value { color: #34c759; }
.dm-hiw-3d .font-green .metric-value { color: #1f9d57 !important; }
/* ---- Animations (keyframes left global; uniquely named) ---- */
@keyframes dmHiwScrollWheel {
@@ -284,12 +324,12 @@
50% { transform: translateX(8px); }
}
/* ---- Workflow steps styling inside the Analytics overlay ---- */
/* ---- Final-panel timeline (inside #analytics-section) ---- */
.dm-hiw-3d .workflow-steps {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 18px;
gap: 4px;
margin-top: 16px;
}
.dm-hiw-3d .workflow-step { display: flex; gap: 14px; }
.dm-hiw-3d .step-number-container {
@@ -299,37 +339,43 @@
width: 24px;
}
.dm-hiw-3d .step-number {
font-family: var(--dm-font-head);
font-size: 11px;
font-weight: 700;
color: #0071e3;
background: rgba(0, 113, 227, 0.1);
width: 22px;
height: 22px;
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(0, 113, 227, 0.15);
border: 1px solid rgba(192, 18, 39, 0.2);
flex-shrink: 0;
}
.dm-hiw-3d .step-line {
width: 1px;
width: 2px;
flex-grow: 1;
background: linear-gradient(to bottom, #0071e3 40%, rgba(0, 0, 0, 0.05) 100%);
margin-top: 6px;
min-height: 24px;
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; }
.dm-hiw-3d .step-content { flex-grow: 1; padding-bottom: 14px; }
.dm-hiw-3d .step-title {
font-size: 14px;
font-weight: 600;
color: #1d1d1f;
margin: 0 0 2px 0;
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-size: 11.5px;
line-height: 1.45;
color: #6e6e73;
margin: 0;
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 ---- */
@@ -341,11 +387,10 @@
padding-bottom: 50px;
}
.dm-hiw-3d .section-panel {
max-width: 100%;
width: calc(100vw - 40px);
padding: 20px;
border-radius: 18px;
padding: 20px 22px 22px;
border-radius: 16px;
}
/* Tablet/mobile: cards bottom-centred so the truck/scene stays visible above. */
.dm-hiw-3d #first-mile-section,
.dm-hiw-3d #mid-mile-section,
.dm-hiw-3d #last-mile-section,
@@ -353,10 +398,10 @@
left: 50% !important;
right: auto !important;
top: auto !important;
bottom: 60px !important;
bottom: 64px !important;
transform: translateX(-50%) translateY(18px) scale(0.97) !important;
max-width: 380px;
width: calc(100vw - 120px) !important;
width: min(380px, calc(100vw - 40px)) !important;
max-height: calc(100vh - 200px) !important;
}
.dm-hiw-3d #first-mile-section.active,
.dm-hiw-3d #mid-mile-section.active,
@@ -364,7 +409,6 @@
.dm-hiw-3d #analytics-section.active {
transform: translateX(-50%) translateY(0) scale(1) !important;
}
.dm-hiw-3d #analytics-section { background: rgba(20, 21, 26, 0.92); }
.dm-hiw-3d .side-navigation {
bottom: 12px;
top: auto;
@@ -386,34 +430,36 @@
}
@media (max-width: 400px) {
.dm-hiw-3d .section-panel {
padding: 16px !important;
width: calc(100vw - 80px) !important;
bottom: 40px !important;
.dm-hiw-3d .section-panel,
.dm-hiw-3d #analytics-section {
padding: 16px 18px 18px !important;
width: min(340px, calc(100vw - 28px)) !important;
bottom: 56px !important;
}
.dm-hiw-3d .section-badge {
font-size: 9px !important;
margin-bottom: 4px !important;
font-size: 9.5px !important;
margin-bottom: 6px !important;
}
.dm-hiw-3d .section-title {
font-size: 20px !important;
letter-spacing: -0.4px !important;
font-size: 18px !important;
}
.dm-hiw-3d .section-subtitle {
font-size: 13px !important;
margin-bottom: 8px !important;
font-size: 12.5px !important;
margin-bottom: 10px !important;
}
.dm-hiw-3d .section-description {
font-size: 11px !important;
line-height: 1.4 !important;
margin-bottom: 12px !important;
font-size: 12px !important;
line-height: 1.45 !important;
margin-bottom: 14px !important;
}
.dm-hiw-3d .section-metrics {
padding-top: 10px !important;
gap: 12px !important;
padding-top: 12px !important;
gap: 14px !important;
}
.dm-hiw-3d .metric-value { font-size: 15px !important; }
.dm-hiw-3d .metric-label { font-size: 8.5px !important; }
.dm-hiw-3d .metric-value { font-size: 16px !important; }
.dm-hiw-3d .metric-label { font-size: 9px !important; }
.dm-hiw-3d .step-title { font-size: 13px !important; }
.dm-hiw-3d .step-description { font-size: 11.5px !important; }
.dm-hiw-3d .side-navigation {
bottom: 8px !important;
gap: 12px !important;
@@ -426,8 +472,8 @@
.dm-hiw-3d #analytics-section {
left: 50% !important;
right: auto !important;
bottom: 40px !important;
width: calc(100vw - 80px) !important;
bottom: 56px !important;
width: min(340px, calc(100vw - 28px)) !important;
transform: translateX(-50%) translateY(18px) scale(0.97) !important;
}
.dm-hiw-3d #first-mile-section.active,