update miletruth page

This commit is contained in:
2026-07-27 19:59:25 +05:30
parent ca83f34b11
commit 136cc966c7
25 changed files with 5779 additions and 1195 deletions

24
probe.mjs Normal file
View File

@@ -0,0 +1,24 @@
import { chromium } from 'playwright';
const browser = await chromium.launch();
for (const w of [1600, 1440, 1366, 1280, 1180, 1024, 992, 900, 768, 640, 480, 390]) {
const page = await browser.newPage({ viewport: { width: w, height: 900 }, deviceScaleFactor: 1 });
await page.goto('http://localhost:3000/miletruth', { waitUntil: 'load', timeout: 90000 });
await page.waitForTimeout(1200);
const r = await page.evaluate(() => {
const copy = document.querySelector('#solution-miletruth-ai .sw-mt-copy');
const flow = document.querySelector('#solution-miletruth-ai .sw-mt-flow');
const labels = [...flow.querySelectorAll('.sw-mt-step-label')];
return {
copyW: Math.round(copy.getBoundingClientRect().width),
// widest single unbreakable label vs the track it was given
worst: labels.map(e => {
const track = e.parentElement.getBoundingClientRect().width;
return { txt: e.textContent, need: Math.round(e.scrollWidth), track: Math.round(track) };
}).filter(x => x.need > x.track + 1),
};
});
console.log(`${String(w).padStart(4)} copy=${String(r.copyW).padStart(3)} overflow=${r.worst.length ? JSON.stringify(r.worst) : 'none'}`);
await page.close();
}
await browser.close();

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

49
shot-miletruth.mjs Normal file
View File

@@ -0,0 +1,49 @@
import { chromium } from 'playwright';
const shots = [
{ name: 'desktop-1440', w: 1440, h: 1000 },
{ name: 'laptop-1280', w: 1280, h: 900 },
{ name: 'tablet-1024', w: 1024, h: 950 },
{ name: 'mobile-390', w: 390, h: 844 },
];
for (const s of shots) {
// fresh browser each time: reusing one exhausts software WebGL contexts
const browser = await chromium.launch({
args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'],
});
const page = await browser.newPage({ viewport: { width: s.w, height: s.h }, deviceScaleFactor: 1 });
const errs = [];
page.on('pageerror', e => errs.push('pageerror: ' + e.message));
await page.goto('http://localhost:3000/miletruth', { waitUntil: 'load', timeout: 90000 });
const toSec = () => page.evaluate(() => {
const el = document.querySelector('#solution-miletruth-ai');
if (el) window.scrollTo(0, window.scrollY + el.getBoundingClientRect().top - 8);
});
await toSec();
await page.waitForTimeout(18000);
await toSec();
await page.waitForTimeout(2500);
const info = await page.evaluate(() => {
const sec = document.querySelector('#solution-miletruth-ai');
const scene = sec.querySelector('.mtn-scene');
const sb = scene.getBoundingClientRect();
return {
canvas: sec.querySelectorAll('canvas').length,
labels: [...scene.querySelectorAll('.mtn-lbl')].map(e => {
const r = e.getBoundingClientRect();
const clip = r.left < sb.left || r.right > sb.right || r.top < sb.top || r.bottom > sb.bottom;
return e.querySelector('.mtn-lbl-n').textContent + (clip ? ' CLIPPED' : '');
}),
stepRows: new Set([...sec.querySelectorAll('.sw-mt-step')].map(e => Math.round(e.getBoundingClientRect().top))).size,
overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
};
});
console.log(`${s.name}: ` + JSON.stringify(info));
if (errs.length) console.log(' ERRORS:', errs.slice(0, 3));
await page.screenshot({ path: `/tmp/mt-${s.name}.png`, timeout: 90000 });
await browser.close();
}

View File

@@ -0,0 +1,35 @@
import React from "react";
import SolutionsHero from "@/components/sections/SolutionsHero";
import IndustryStack from "@/components/sections/IndustryStack";
/**
* Industries — "Who Doormile serves".
*
* This is the original industry-focused content that used to live at /solutions
* (see commit 205924e): the industry hero slider plus the IndustryStack cards
* ported from doormile-site/includes/sections/solution/card-1.php. The FMCG,
* Pharma and Enterprise & B2B copy is unchanged from that original; Retail and
* Quick Commerce were added later in the same layout.
*
* /solutions now answers "what Doormile offers" instead.
*/
export const metadata = {
title: "Industries Doormile",
description:
"Discover how Doormile's connected logistics platform serves diverse industries — FMCG, Pharma, Enterprise & B2B, Retail and Quick Commerce — with tailored solutions.",
};
export default function IndustriesPage() {
return (
<div id="industries" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
<div className="content">
<div className="content-inner">
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
<SolutionsHero />
<IndustryStack />
</div>
</div>
</div>
</div>
);
}

View File

@@ -5,6 +5,14 @@ import Workflow1 from "../../components/sections/Workflow1";
import Workflow2 from "../../components/sections/Workflow2";
import Workflow4 from "../../components/sections/Workflow4";
import Workflow3Lazy from "../../components/sections/Workflow3Lazy";
import {
SolutionAIVehicleSelection,
SolutionSmartRouteIntelligence,
SolutionQuantumDecisionEngine,
SolutionLiveLogisticsIntelligence,
SolutionMileTruthAI,
SolutionOperationsDashboard,
} from "../../components/sections/SolutionsWorkflow";
export const metadata = {
title: "MileTruth Doormile",
@@ -19,9 +27,15 @@ export default function MileTruthPage() {
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
<MileTruthHero />
<MileTruthIntro />
<SolutionAIVehicleSelection />
<Workflow1 />
<SolutionSmartRouteIntelligence />
<Workflow2 />
<SolutionQuantumDecisionEngine />
<Workflow4 />
<SolutionLiveLogisticsIntelligence />
<SolutionMileTruthAI />
<SolutionOperationsDashboard />
<Workflow3Lazy />
</div>
</div>

View File

@@ -10,11 +10,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
"/contact",
"/blog",
"/air-freight",
"/first-mile-delivery",
"/mid-mile-delivery",
"/last-mile-delivery",
"/ev-logistics",
"/strategy-optimization",
"/solutions",
"/industries",
"/how-it-works",
].map((route) => ({
url: `${SITE_URL}${route}`,

View File

@@ -1,65 +1,119 @@
import React from "react";
import SolutionsHero from "@/components/sections/SolutionsHero";
import SolutionsWorkflow from "@/components/sections/SolutionsWorkflow";
import SolutionsHero, { type HeroSlide } from "@/components/sections/SolutionsHero";
import Miles3 from "@/components/sections/Miles3";
import EVSection, { type EVFeature } from "@/components/sections/EVSection";
import {
SolutionsSectionsShell,
SolOfferings,
SolWingsSpotlight,
SolMileTruthBand,
SolutionsWhyDoormile,
SolutionsResults,
} from "@/components/sections/SolutionsSections";
/**
* Solutions — "What Doormile offers".
*
* Services only: first mile, mid mile, last mile, Doormile Wings, EV fleet, and
* the fact that all of it is powered by MileTruth™. The technology story (AI
* architecture, routing, the decision engine) belongs to /miletruth and is
* deliberately not repeated here; industry detail belongs to /industries.
*/
export const metadata = {
title: "Solutions Doormile",
description: "See how Doormile's AI-powered Logistics Intelligence Platform works end-to-end — from pickup and AI vehicle selection to Quantum routing, live shipment tracking, door-to-door movement, MileTruth AI and real-time operations monitoring.",
description:
"What Doormile offers: first mile pickup, mid mile line haul, last mile delivery, same-day air with Doormile Wings, and an electric fleet — every mile powered by MileTruth™.",
};
const SOLUTIONS_SLIDES: HeroSlide[] = [
{
heading: "One Partner for Every Mile",
text: "First mile, mid mile and last mile — on road and in the air, with an electric fleet and one tracking link end to end.",
},
{
heading: "Everything Doormile Delivers",
text: "Take one service or the whole chain. Either way it is one contract, one team, and one place to see where your shipment is.",
},
];
/* Customer-facing framing of the EV fleet — the homepage section's own copy is
written around the EV-native platform design, which belongs on MileTruth. */
const EV_FLEET_FEATURES: EVFeature[] = [
{
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 10h-1.28A6 6 0 0 0 12 5V3M6 10h1.28A6 6 0 0 0 12 5M12 18v2M8 10v6a4 4 0 0 0 8 0v-6" />
</svg>
),
title: "Lower Cost Per Kilometre",
desc: "Electric running costs are a fraction of diesel, and that saving shows up on the miles you move with us.",
},
{
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<path d="M9 12l2 2 4-4" />
</svg>
),
title: "No Range Surprises",
desc: "Charging is planned into the day before the vehicle leaves the depot, so an EV run arrives when a diesel run would.",
},
{
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2C8 6 6 9 6 13a6 6 0 0 0 12 0c0-4-2-7-6-11z" />
<path d="M12 8v9" />
</svg>
),
title: "Zero Tailpipe Emissions",
desc: "Clean deliveries into cities with low-emission zones, and a number you can put in your own sustainability reporting.",
},
{
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="1" y="11" width="14" height="8" rx="1.5" />
<path d="M15 15h4l2 2v2h-6" />
<circle cx="5.5" cy="21" r="2" />
<circle cx="17.5" cy="21" r="2" />
</svg>
),
title: "Built For Your Routes",
desc: "Urban and regional runs served by a fleet sized to the job, from small city vehicles to larger regional loads.",
},
];
export default function SolutionsPage() {
return (
<>
<style dangerouslySetInnerHTML={{ __html: `
/* Fix "Why Businesses Choose Doormile" section — anchor absolute image left, push content right */
.elementor-element.elementor-element-ead59d3 {
position: relative;
overflow: hidden;
}
.elementor-element.elementor-element-f35119c {
position: absolute !important;
left: -2% !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: 48% !important;
max-width: 520px !important;
opacity: 0.18 !important;
z-index: 0 !important;
pointer-events: none;
}
.elementor-element.elementor-element-56ecbb3 {
position: relative !important;
z-index: 1 !important;
width: 55% !important;
max-width: 55% !important;
margin-left: auto !important;
margin-right: 4% !important;
}
.elementor-element.elementor-element-56ecbb3 .e-con-inner {
max-width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
@media (max-width: 1020px) {
.elementor-element.elementor-element-f35119c { display: none !important; }
.elementor-element.elementor-element-56ecbb3 {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
}
`}} />
<div id="solutions" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
<div className="content">
<div className="content-inner">
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
<SolutionsHero />
<SolutionsWorkflow />
<SolutionsHero slides={SOLUTIONS_SLIDES} />
{/* One #sol-root scope for the whole page — the shell carries the
stylesheet and must not be rendered twice. Miles3 and EVSection
sit between beats; the shell's CSS is scoped to `.sol-*` only,
so their Elementor markup is unaffected. */}
<SolutionsSectionsShell>
<SolOfferings />
<Miles3 eyebrow="/ What We Offer /" />
<SolWingsSpotlight />
<EVSection
eyebrow="/ EV Fleet /"
cardTitle="EV Fleet"
cardSubtitle="Cleaner miles, lower costs"
titleLead="AN ELECTRIC FLEET ON "
titleAccent="YOUR ROUTES."
features={EV_FLEET_FEATURES}
ariaLabel="Doormile EV fleet"
/>
<SolutionsWhyDoormile />
<SolMileTruthBand />
<SolutionsResults />
</SolutionsSectionsShell>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -435,7 +435,7 @@ export default function Footer() {
<Link href="/how-it-works">How It Works</Link>
</li>
<li>
<Link href="/solutions">Use Cases</Link>
<Link href="/industries">Use Cases</Link>
</li>
<li>
<Link href="/contact">Request Demo</Link>

View File

@@ -19,6 +19,7 @@ const CURRENT_PAGE_MAP: Record<string, string> = {
"/how-it-works": "how-it-works",
"/miletruth": "miletruth",
"/solutions": "solutions",
"/industries": "industries",
"/about": "about",
"/doormile-wings": "doormile-wings",
"/empowerment": "entarpreneurship",
@@ -38,6 +39,8 @@ const CURRENT_PAGE_ALIASES: Record<string, string[]> = {
miletruth: ["miletruth"],
// Solutions is now a plain top-level link — no child pages own its active state.
solutions: ["solutions"],
// Industries is the sibling top-level link — "who Doormile serves".
industries: ["industries"],
"doormile-wings": ["doormile-wings"],
entarpreneurship: ["entarpreneurship"],
blogs: ["blogs"],
@@ -45,8 +48,8 @@ const CURRENT_PAGE_ALIASES: Record<string, string[]> = {
"privacy-policy": ["privacy-policy"],
"terms-of-service": ["terms-of-service"],
"cookie-policy": ["cookie-policy"],
// Company is the dropdown parent for About + Blogs + How It Works — active if any is.
company: ["blogs", "how-it-works", "about"],
// Company is the dropdown parent for About + Industries + How It Works + Blogs — active if any is.
company: ["about", "industries", "how-it-works", "blogs"],
};
export default function Header() {
@@ -524,15 +527,13 @@ export default function Header() {
About
</Link>
</li>
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
<li className={currentPage === "industries" ? "dm-child-active" : ""}>
<Link
href="/blog#blogs"
onClick={(event) => {
setDesktopCompanyOpen(false);
handleNavClick(event, "/blog", "blogs");
}}
href="/industries"
prefetch
onClick={() => setDesktopCompanyOpen(false)}
>
Blogs
Industries
</Link>
</li>
<li className={currentPage === "how-it-works" ? "dm-child-active" : ""}>
@@ -546,6 +547,17 @@ export default function Header() {
How It Works
</Link>
</li>
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
<Link
href="/blog#blogs"
onClick={(event) => {
setDesktopCompanyOpen(false);
handleNavClick(event, "/blog", "blogs");
}}
>
Blogs
</Link>
</li>
</ul>
</li>
</ul>
@@ -621,12 +633,15 @@ export default function Header() {
<li className={currentPage === "about" ? "dm-child-active" : ""}>
<Link href="/about-us#about" onClick={(event) => handleNavClick(event, "/about-us", "about", true)}>About</Link>
</li>
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
<Link href="/blog#blogs" onClick={(event) => handleNavClick(event, "/blog", "blogs", true)}>Blogs</Link>
<li className={currentPage === "industries" ? "dm-child-active" : ""}>
<Link href="/industries" prefetch onClick={closeMobileMenu}>Industries</Link>
</li>
<li className={currentPage === "how-it-works" ? "dm-child-active" : ""}>
<Link href="/how-it-works#how-it-works" onClick={(event) => handleNavClick(event, "/how-it-works", "how-it-works", true)}>How It Works</Link>
</li>
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
<Link href="/blog#blogs" onClick={(event) => handleNavClick(event, "/blog", "blogs", true)}>Blogs</Link>
</li>
</ul>
</li>
</ul>

View File

@@ -0,0 +1,289 @@
"use client";
import React, { useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { useInView } from "framer-motion";
import { ScrollReveal, Magnetic } from "@/animations/Reveal";
interface ConnectedLogisticsSectionProps {
id?: string;
eyebrow?: string;
title?: string;
imageSrc?: string;
imageAlt?: string;
items?: Array<{ title: string; text: string }>;
showButton?: boolean;
}
const DEFAULT_ITEMS = [
{ title: "Real-Time Visibility", text: "See every shipment, every vehicle, every handoff—live. No blind spots." },
{ title: "Dynamic Route Adjustment", text: "AI continuously optimizes routes based on traffic, weather, and real-world conditions." },
{ title: "Proactive Delay Prevention", text: "Detect SLA risks hours before they become problems. Act, don't react." },
{ title: "Execution Intelligence", text: "Learn from every shipment. Get smarter with every mile traveled." },
];
export default function ConnectedLogisticsSection({
id = "vehicle-ai",
eyebrow = "/ Connected Logistics /",
title = "Smart logistics solutions we deliver for modern supply chains",
imageSrc = "/images/home2-pic-3.webp",
imageAlt = "Connected Logistics",
items = DEFAULT_ITEMS,
showButton = false,
}: ConnectedLogisticsSectionProps) {
const sectionRef = useRef<HTMLDivElement>(null);
const isInView = useInView(sectionRef, { once: true, margin: "-50px" });
return (
<div ref={sectionRef} className="elementor elementor-61" data-elementor-type="wp-page" data-elementor-id="61" id={id} style={{ width: "100%", scrollMarginTop: "100px" }}>
<div className="elementor-element elementor-element-89a0ca1 e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="89a0ca1" data-element_type="container" data-e-type="container">
<style dangerouslySetInnerHTML={{ __html: `
/* Ensure the inner content wrapper is responsive and never overflows */
.elementor-element-fdb2e58 {
width: 100% !important;
max-width: 650px !important;
}
/* Make heading font size fluid and responsive */
.elementor-element-7500280 .logico-title {
font-size: clamp(26px, 3.5vw, 54px) !important;
line-height: 1.25em !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
/* Fix text colors for the dark background */
.elementor-element-71c3e1d .logico-title {
color: #ffffff !important;
}
.elementor-element-71c3e1d p {
color: rgba(255, 255, 255, 0.85) !important;
}
/* Make sure paragraphs are responsive on all screen widths */
.elementor-element-165dfa5 {
width: 100% !important;
max-width: min(619px, 100%) !important;
}
.elementor-element-3888a1e {
width: 100% !important;
max-width: min(526px, 100%) !important;
}
/* Sizing and identical padding on all 4 sides for Connected Logistics Image container */
.elementor-element-99768ba .elementor-widget-container {
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding: 40px !important;
box-sizing: border-box !important;
}
.elementor-element-99768ba img.wp-image-4481 {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
object-fit: cover !important;
border-radius: 25px !important;
margin: 0 auto !important;
display: block !important;
}
/* Desktop/Laptop (min-width: 1025px) column width and flex rules */
@media (min-width: 1025px) {
.elementor-element-9ffed33 {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: center !important;
}
.elementor-element-96343ba,
.elementor-element-71c3e1d {
width: 50% !important;
max-width: 50% !important;
flex: 1 1 50% !important;
box-sizing: border-box !important;
}
}
@media (max-width: 1024px) {
.elementor-element-9ffed33 {
flex-direction: column !important;
align-items: stretch !important;
}
.elementor-element-96343ba,
.elementor-element-71c3e1d {
width: 100% !important;
max-width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
.elementor-element-96343ba > div {
width: 100% !important;
height: auto !important;
}
.elementor-element-96343ba {
margin-top: 0px !important;
}
.elementor-element-71c3e1d {
padding-bottom: 24px !important;
}
.elementor-element-165dfa5,
.elementor-element-3888a1e {
width: 100% !important;
max-width: 100% !important;
}
.elementor-element-99768ba {
width: 100% !important;
max-width: 100% !important;
}
.elementor-element-99768ba .elementor-widget-container {
padding: 10px !important;
display: block !important;
width: 100% !important;
}
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
width: 100% !important;
max-width: 600px !important;
border-radius: 25px !important;
}
}
@media (max-width: 767px) {
.elementor-element-99768ba {
width: 100% !important;
max-width: 100% !important;
}
.elementor-element-71c3e1d {
padding-bottom: 12px !important;
}
.elementor-element-99768ba .elementor-widget-container {
padding: 0px !important;
display: block !important;
width: 100% !important;
}
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
object-fit: cover !important;
border-radius: 25px !important;
}
}
` }} />
<div className="elementor-element elementor-element-9ffed33 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9ffed33" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
{/* Image side */}
<div className="elementor-element elementor-element-96343ba e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="96343ba" data-element_type="container">
<ScrollReveal delay={0.1} duration={0.9} yOffset={30} className="w-full h-full">
<div className="elementor-element elementor-element-99768ba elementor-widget elementor-widget-image" data-id="99768ba" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
<div className="elementor-widget-container">
<Image
fetchPriority="high"
priority
width={578}
height={790}
src={imageSrc}
className="attachment-full size-full wp-image-4481"
alt={imageAlt}
style={{
width: "100%",
height: "auto",
objectFit: "cover",
borderRadius: "25px",
}}
/>
</div>
</div>
</ScrollReveal>
</div>
{/* Content side */}
<div className="elementor-element elementor-element-71c3e1d e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="71c3e1d" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-fdb2e58 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="fdb2e58" data-element_type="container" data-e-type="container">
<ScrollReveal delay={0.1} duration={0.8} yOffset={20}>
<div className="elementor-element elementor-element-0136e6e elementor-widget elementor-widget-logico_heading" data-id="0136e6e" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">{eyebrow}</div>
</div>
</div>
</ScrollReveal>
<ScrollReveal delay={0.2} duration={0.85} yOffset={25}>
<div className="elementor-element elementor-element-7500280 elementor-widget elementor-widget-logico_heading" data-id="7500280" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<h2 className="logico-title">{title}</h2>
</div>
</div>
</ScrollReveal>
{/* Staggered List items */}
{items.map((item, index) => (
<ScrollReveal key={index} delay={0.25 + index * 0.1} duration={0.8} yOffset={25}>
<div className="elementor-element elementor-element-4cc07a7 elementor-widget elementor-widget-logico_heading" data-id="4cc07a7" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">{item.title}</div>
</div>
</div>
<div className="elementor-element elementor-element-165dfa5 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="165dfa5" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
<div className="elementor-widget-container">
<p>{item.text}</p>
</div>
</div>
</ScrollReveal>
))}
{showButton && (
<ScrollReveal delay={0.65} duration={0.8} yOffset={20}>
<div className="elementor-element elementor-element-c0a45e4 elementor-widget elementor-widget-logico_button" data-id="c0a45e4" data-element_type="widget" data-e-type="widget" data-widget_type="logico_button.default">
<div className="elementor-widget-container">
<div className="button-widget">
<div className="button-container">
<Magnetic>
<Link href="/solutions" className="logico-alter-button">Explore more</Link>
</Magnetic>
</div>
</div>
</div>
</div>
</ScrollReveal>
)}
</div>
</div>
{/* Absolutely Positioned Block Decoration Badge */}
<div
className="elementor-element elementor-element-0efc59e elementor-absolute elementor-widget elementor-widget-logico_block_decoration"
data-id="0efc59e"
data-element_type="widget"
data-settings='{"_position":"absolute"}'
data-widget_type="logico_block_decoration.default"
style={{ position: "absolute" }}
>
<div className="elementor-widget-container">
<div className={`block-decoration animation-enable block-decoration-style-1 ${isInView ? "animated" : ""}`}>
<div className="block-decoration-item"></div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -368,6 +368,7 @@ export default function EVSection({
}: EVSectionProps) {
const bannerRef = useRef<HTMLDivElement>(null);
const useDashboard = !!metrics && metrics.length > 0;
const bottomBarItems = useDashboard ? barStats : stats;
const isVideo = image.endsWith(".mp4");
/* In dashboard mode the workflow's accent theme is applied at the .evnd
@@ -1488,12 +1489,9 @@ export default function EVSection({
</div>
</div>
{/* BOTTOM BAR — full-width KPI summary strip below the workflow
content (Scene → Insight/Decision card → KPI bar). Required across
all breakpoints. In dashboard mode it summarises the same workflow
metrics; the homepage section uses its own stat set. */}
{bottomBarItems && (
<div className="evnd__bar">
{(useDashboard ? (barStats ?? metrics!) : stats).map((s) => (
{bottomBarItems.map((s) => (
<div className="evnd__bar-item" key={s.label}>
<span className="evnd__bar-label">{s.label}</span>
{s.text ? (
@@ -1509,6 +1507,7 @@ export default function EVSection({
</div>
))}
</div>
)}
</div>
</section>
</div>

View File

@@ -153,7 +153,7 @@ export default function IndustrySolutions() {
<div className="industry-solutions-grid">
{/* Card 1: FMCG */}
<Link href="/solutions" className="industry-card-link">
<Link href="/industries" className="industry-card-link">
<div className="industry-card-bg">
<Image
src="/images/tab-pic-1.webp"
@@ -217,7 +217,7 @@ export default function IndustrySolutions() {
</Link>
{/* Card 2: Pharmaceutical */}
<Link href="/solutions" className="industry-card-link">
<Link href="/industries" className="industry-card-link">
<div className="industry-card-bg">
<Image
src="/images/tab-pic-2.webp"
@@ -281,7 +281,7 @@ export default function IndustrySolutions() {
</Link>
{/* Card 3: Enterprise & B2B */}
<Link href="/solutions" className="industry-card-link">
<Link href="/industries" className="industry-card-link">
<div className="industry-card-bg">
<Image
src="/images/tab-pic-3.webp"

View File

@@ -76,6 +76,46 @@ const SECTIONS: Section[] = [
"SLA monitoring dashboard tracks commitments and flags risk in real time.",
],
},
{
id: 4,
title: "Retail",
image: "/images/last-mile-approach.webp",
alt: "Retail doorstep delivery",
desc:
"Retail logistics is judged at the doorstep. Shoppers expect to watch the delivery rather than call about it, and every failed attempt costs more than the trip itself — so visibility, first-attempt success, and clean returns decide the experience.",
ch: [
"Customers expect to track the delivery live instead of phoning for updates.",
"Failed delivery attempts cost more than the delivery itself and erode trust.",
"Returns must flow back through the same trip to stay economical.",
"Same-day promises leave no room for slow dispatch or manual coordination.",
],
so: [
"Live customer tracking links replace status calls with a shareable map.",
"OTP-verified handover confirms delivery and lifts first-attempt success.",
"Reverse pickup on the same route brings returns back without an extra trip.",
"Fast doorstep dispatch keeps same-day commitments consistently on time.",
],
},
{
id: 5,
title: "Quick Commerce",
image: "/images/home2-pic-3.webp",
alt: "Quick commerce dark store delivery",
desc:
"Quick commerce compresses the entire logistics day into minutes. Promise windows are measured in tens of minutes, dark store stock moves constantly, and rider capacity has to match demand the moment it spikes.",
ch: [
"Sub-hour promise windows collapse if dispatch waits on manual decisions.",
"Dense micro-zone orders arrive faster than they can be grouped by hand.",
"Peak-hour demand outpaces available rider capacity within minutes.",
"Dark store stock counts drift out of date and trigger cancelled orders.",
],
so: [
"Instant dispatch assigns every order the moment it lands, with no queue.",
"Micro-zone batching groups nearby orders into one short, efficient run.",
"Live capacity balancing shifts riders to the zones that are surging.",
"Real-time dark store stock sync prevents cancellations and substitutions.",
],
},
];
function Card({ sec }: { sec: Section }) {

View File

@@ -1,20 +1,25 @@
"use client";
import React, { useMemo, useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Sparkles } from "@react-three/drei";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { Html } from "@react-three/drei";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import * as THREE from "three";
/* ============================================================
MileTruthCanvas — small, self-contained R3F scene for the
"MileTruth AI" section: a 4-node data → AI → recommendations →
improvement network with glowing cores and travelling light
packets along each connector. Deliberately lightweight (a
handful of meshes + a low-count sparkle field) — this and the
Quantum section are the ONLY two places on the Solutions page
that touch Three.js.
MileTruthCanvas — the "MileTruth AI" learning loop.
A CLOSED cycle, not a line: shipment data enters, the AI core
reads it, a recommendation is issued, the result is measured,
and that result feeds the next shipment. Packets travel the
whole ring and visibly return to where they started — the
shape itself is the pitch ("every shipment makes the next one
smarter"). Each station is labelled on screen, so the diagram
explains itself without reading the copy beside it.
Deliberately lightweight — this and the Quantum section are
the ONLY two places on the Solutions page that touch Three.js.
============================================================ */
const RED = "#C01227";
@@ -22,110 +27,304 @@ const RED_BRIGHT = "#E2354A";
const STEEL = "#8b8b96";
const WHITE = "#f4f4f6";
type Node = { pos: [number, number, number]; color: string; label: string; scale: number };
/** Loop radii (world units) and the tilt that turns the flat ring
* into a readable orbit with depth. */
const RX = 2.55;
const RZ = 1.7;
const TILT = 0.55;
const NODES: Node[] = [
{ pos: [-4.6, 0.3, 0], color: STEEL, label: "data", scale: 0.34 },
{ pos: [-1.6, 0.75, 0.4], color: RED, label: "ai", scale: 0.46 },
{ pos: [1.5, 0.25, -0.35], color: RED_BRIGHT, label: "recs", scale: 0.38 },
{ pos: [4.5, -0.15, 0.15], color: WHITE, label: "improve", scale: 0.34 },
];
/** Packets. Offsets are deliberately uneven so one isn't parked on
* a station at all times. SPEED ~= one full lap every 12s. */
const PACKET_OFFSETS = [0, 0.17, 0.34, 0.62, 0.81];
const SPEED = 0.085;
const LINKS: [number, number][] = [
[0, 1],
[1, 2],
[2, 3],
];
/** Where the AI core sits on the loop (station 02). */
const INGEST_U = 0.25;
function NodeMesh({ node, reduced, seed }: { node: Node; reduced: boolean; seed: number }) {
const meshRef = useRef<THREE.Mesh>(null);
const haloRef = useRef<THREE.Mesh>(null);
/**
* Analytic point on the loop. u=0 is the left-hand station and u
* increases left -> back -> right -> front, so the cycle reads
* clockwise on screen. Kept analytic (rather than curve.getPointAt)
* so packets pass exactly through each station.
*/
function pointAt(u: number, out = new THREE.Vector3()) {
const th = Math.PI + u * Math.PI * 2;
return out.set(RX * Math.cos(th), 0, RZ * Math.sin(th));
}
useFrame((state) => {
const t = state.clock.elapsedTime;
const pulse = reduced ? 1 : 1 + Math.sin(t * 1.6 + seed) * 0.08;
if (meshRef.current) meshRef.current.scale.setScalar(node.scale * pulse);
if (haloRef.current) {
const haloPulse = reduced ? 1.4 : 1.4 + Math.sin(t * 1.6 + seed) * 0.18;
haloRef.current.scale.setScalar(node.scale * haloPulse);
const mat = haloRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = reduced ? 0.16 : 0.14 + Math.sin(t * 1.6 + seed) * 0.06;
/** Shortest distance between two positions on a unit-length loop. */
function loopDist(a: number, b: number) {
const d = Math.abs(a - b) % 1;
return Math.min(d, 1 - d);
}
/**
* How hard the core is "ingesting" right now: 1 when a packet is on
* the AI station, falling off either side. Derived purely from the
* clock, so every component can recompute it without shared state.
*/
function ingestEnergy(t: number) {
let e = 0;
for (const off of PACKET_OFFSETS) {
const u = (t * SPEED + off) % 1;
e = Math.max(e, Math.exp(-Math.pow(loopDist(u, INGEST_U) / 0.045, 2)));
}
});
return e;
}
type Station = {
u: number;
n: string;
name: string;
figure: string;
color: string;
/** Label offset along Y, in loop-space units. Negative parks the label
* below the station — used for the front stop, whose artwork grows
* upward and would otherwise sit under its own caption. */
labelY: number;
};
/** Illustrative sample figures — see the "Illustrative" marker rendered
* by MileTruthNetwork. Swap these four strings for real numbers. */
const STATIONS: Station[] = [
{ u: 0, n: "01", name: "Shipment Data", figure: "12,480 analysed", color: STEEL, labelY: 0.95 },
{ u: 0.25, n: "02", name: "MileTruth AI", figure: "148 patterns found", color: RED_BRIGHT, labelY: 1.15 },
{ u: 0.5, n: "03", name: "Recommendation", figure: "36 actions issued", color: RED_BRIGHT, labelY: 0.95 },
{ u: 0.75, n: "04", name: "Measured Result", figure: "+6.2% on-time", color: WHITE, labelY: -0.75 },
];
/* ---------- the loop itself ---------- */
function LoopPath() {
const geo = useMemo(() => {
const pts: THREE.Vector3[] = [];
for (let i = 0; i < 96; i++) pts.push(pointAt(i / 96));
const curve = new THREE.CatmullRomCurve3(pts, true, "centripetal");
// A tube (not lineBasicMaterial) so the path actually catches light
// and bloom — thin lines washed out to nothing in the old scene.
return new THREE.TubeGeometry(curve, 220, 0.016, 8, true);
}, []);
return (
<group position={node.pos}>
<mesh ref={haloRef}>
<sphereGeometry args={[1, 20, 20]} />
<meshBasicMaterial color={node.color} transparent opacity={0.16} depthWrite={false} />
<mesh geometry={geo}>
<meshBasicMaterial color={RED} transparent opacity={0.55} depthWrite={false} />
</mesh>
<mesh ref={meshRef}>
<sphereGeometry args={[1, 28, 28]} />
);
}
/* ---------- station artwork ---------- */
/** Each station gets a distinct silhouette, so the four steps stay
* tellable apart even before you read their labels. */
function StationArt({ index, color }: { index: number; color: string }) {
const mat = (
<meshStandardMaterial
color={node.color}
emissive={node.color}
emissiveIntensity={1.4}
color={color}
emissive={color}
emissiveIntensity={0.9}
roughness={0.35}
metalness={0.1}
/>
);
if (index === 0) {
// Shipment data — a stack of parcels.
return (
<group>
<mesh position={[-0.13, 0.07, 0]}>
<boxGeometry args={[0.2, 0.2, 0.2]} />
{mat}
</mesh>
<mesh position={[0.12, 0.05, 0.05]} rotation={[0, 0.5, 0]}>
<boxGeometry args={[0.16, 0.16, 0.16]} />
{mat}
</mesh>
<mesh position={[0, 0.26, -0.04]} rotation={[0, 0.25, 0]}>
<boxGeometry args={[0.15, 0.15, 0.15]} />
{mat}
</mesh>
</group>
);
}
if (index === 1) {
// MileTruth AI — handled by <AiCore/>, which needs its own frame loop.
return null;
}
if (index === 2) {
// Recommendation — a marker inside a ring, i.e. an issued action.
return (
<group position={[0, 0.15, 0]}>
<mesh>
<sphereGeometry args={[0.13, 20, 20]} />
{mat}
</mesh>
<mesh rotation={[Math.PI / 2, 0, 0]}>
<torusGeometry args={[0.26, 0.018, 10, 40]} />
<meshBasicMaterial color={color} transparent opacity={0.7} />
</mesh>
</group>
);
}
// Measured result — three rising bars.
return (
<group>
{[0.14, 0.24, 0.36].map((h, i) => (
<mesh key={h} position={[(i - 1) * 0.16, h / 2, 0]}>
<boxGeometry args={[0.1, h, 0.1]} />
{mat}
</mesh>
))}
</group>
);
}
function Connector({ a, b, reduced, index }: { a: Node; b: Node; reduced: boolean; index: number }) {
const packetRef = useRef<THREE.Mesh>(null);
const lineMatRef = useRef<THREE.LineBasicMaterial>(null);
const start = useMemo(() => new THREE.Vector3(...a.pos), [a]);
const end = useMemo(() => new THREE.Vector3(...b.pos), [b]);
function StationNode({
station,
index,
compact,
reduced,
}: {
station: Station;
index: number;
compact: boolean;
reduced: boolean;
}) {
const pos = useMemo(() => pointAt(station.u).toArray() as [number, number, number], [station.u]);
const geo = useMemo(() => {
const g = new THREE.BufferGeometry();
g.setAttribute("position", new THREE.BufferAttribute(new Float32Array([...a.pos, ...b.pos]), 3));
return g;
}, [a, b]);
return (
<group position={pos}>
{index === 1 ? <AiCore reduced={reduced} /> : <StationArt index={index} color={station.color} />}
{/* Pad on the loop, so each station reads as a stop rather than
a mesh floating near the path. */}
{/* Always red, never the station colour — a white or steel pad reads
as a grey smudge under the artwork rather than as a stop. */}
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[0.32, 32]} />
<meshBasicMaterial color={RED_BRIGHT} transparent opacity={0.12} depthWrite={false} />
</mesh>
<Html center pointerEvents="none" zIndexRange={[12, 0]} position={[0, station.labelY, 0]}>
<div className="mtn-lbl">
<span className="mtn-lbl-n">{station.n}</span>
<span className="mtn-lbl-name">{station.name}</span>
{!compact && <span className="mtn-lbl-fig">{station.figure}</span>}
</div>
</Html>
</group>
);
}
/* ---------- station 02: the AI core ---------- */
/**
* The brain sits ON the loop as station 02 rather than in the middle of
* it. A centre core reads as a second, competing object and — from this
* camera — hides whatever station sits directly behind it.
*/
function AiCore({ reduced }: { reduced: boolean }) {
const innerRef = useRef<THREE.Mesh>(null);
const shellRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
const t = state.clock.elapsedTime;
if (lineMatRef.current) {
lineMatRef.current.opacity = reduced ? 0.3 : 0.2 + Math.sin(t * 1.2 + index) * 0.12;
const t = reduced ? 0 : state.clock.elapsedTime;
// Steady mid-glow when motion is reduced, so the core still reads as
// the active element without animating.
const e = reduced ? 0.45 : ingestEnergy(t);
if (innerRef.current) {
innerRef.current.scale.setScalar(1 + e * 0.14);
const m = innerRef.current.material as THREE.MeshStandardMaterial;
m.emissiveIntensity = 0.8 + e * 1.2;
}
if (packetRef.current) {
if (reduced) {
packetRef.current.visible = false;
return;
}
packetRef.current.visible = true;
const speed = 0.28;
const u = (t * speed + index * 0.33) % 1;
packetRef.current.position.lerpVectors(start, end, u);
const mat = packetRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = Math.sin(u * Math.PI);
if (shellRef.current) {
if (!reduced) shellRef.current.rotation.y = t * 0.25;
const m = shellRef.current.material as THREE.MeshBasicMaterial;
m.opacity = 0.2 + e * 0.35;
}
});
return (
<group>
<lineSegments geometry={geo}>
<lineBasicMaterial ref={lineMatRef} color={RED_BRIGHT} transparent opacity={0.25} depthWrite={false} />
</lineSegments>
<mesh ref={packetRef}>
<sphereGeometry args={[0.09, 12, 12]} />
<meshBasicMaterial color="#ffffff" transparent opacity={0.9} depthWrite={false} />
<group position={[0, 0.22, 0]}>
<mesh ref={innerRef}>
<icosahedronGeometry args={[0.26, 1]} />
<meshStandardMaterial
color={RED}
emissive={RED_BRIGHT}
emissiveIntensity={0.8}
roughness={0.3}
metalness={0.15}
/>
</mesh>
<mesh ref={shellRef} rotation={[0.4, 0, 0]}>
<icosahedronGeometry args={[0.46, 1]} />
<meshBasicMaterial color={RED_BRIGHT} wireframe transparent opacity={0.22} depthWrite={false} />
</mesh>
</group>
);
}
function SlowSpin({ reduced, children }: { reduced: boolean; children: React.ReactNode }) {
const group = useRef<THREE.Group>(null);
useFrame((_state, dt) => {
if (reduced || !group.current) return;
group.current.rotation.y += dt * 0.035;
/* ---------- packets travelling the loop ---------- */
function Packet({ offset, reduced }: { offset: number; reduced: boolean }) {
const ref = useRef<THREE.Group>(null);
const vec = useMemo(() => new THREE.Vector3(), []);
useFrame((state) => {
if (!ref.current) return;
// When reduced, packets freeze at their offsets rather than
// disappearing — the ring still shows that something flows.
const t = reduced ? 0 : state.clock.elapsedTime;
const u = (t * SPEED + offset) % 1;
pointAt(u, vec);
ref.current.position.set(vec.x, 0.16, vec.z);
});
return <group ref={group}>{children}</group>;
return (
<group ref={ref}>
<mesh>
<sphereGeometry args={[0.07, 12, 12]} />
<meshBasicMaterial color={WHITE} />
</mesh>
<mesh>
<sphereGeometry args={[0.15, 12, 12]} />
<meshBasicMaterial color={RED_BRIGHT} transparent opacity={0.25} depthWrite={false} />
</mesh>
</group>
);
}
/* ---------- fit ---------- */
/** Half-width of the widest station label ("Recommendation"), plus a
* little breathing room, in CSS pixels. */
const LABEL_HALF_PX = 60;
/**
* Scales the loop so the outermost stations AND their labels stay inside
* the canvas at any width. The labels are DOM, so their size is fixed in
* pixels while the scene is measured in world units — the fit therefore
* has to be computed against the pixel size, not the aspect ratio alone.
* Vertical fit needs no handling: with a fixed camera the visible world
* height is constant regardless of how short the card gets.
*/
function FitLoop({ children }: { children: React.ReactNode }) {
const size = useThree((s) => s.size);
const viewport = useThree((s) => s.viewport);
const scale = useMemo(() => {
const pxPerWorld = size.width / viewport.width;
const room = size.width / 2 - LABEL_HALF_PX;
return Math.max(0.55, Math.min(1, room / (RX * pxPerWorld)));
}, [size.width, viewport.width]);
return <group scale={scale}>{children}</group>;
}
/* ---------- scene ---------- */
export default function MileTruthCanvas({
reduced = false,
isMobile = false,
@@ -135,29 +334,42 @@ export default function MileTruthCanvas({
isMobile?: boolean;
active?: boolean;
}) {
const packets = isMobile ? PACKET_OFFSETS.slice(0, 3) : PACKET_OFFSETS;
return (
<Canvas
flat
frameloop={active ? "always" : "never"}
dpr={[1, isMobile || reduced ? 1.2 : 1.6]}
camera={{ position: [0, 1.4, 9.2], fov: 42, near: 0.1, far: 60 }}
camera={{ position: [0, 0, 7.6], fov: 40, near: 0.1, far: 60 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
>
<color attach="background" args={["#0c0c10"]} />
<ambientLight intensity={0.35} />
<SlowSpin reduced={reduced}>
{LINKS.map(([a, b], i) => (
<Connector key={i} a={NODES[a]} b={NODES[b]} reduced={reduced} index={i} />
<ambientLight intensity={0.75} />
<directionalLight position={[2.5, 4, 4]} intensity={1.1} />
{/* One fixed tilt on the whole loop. Nothing spins the group —
the reading order has to hold still to stay legible. */}
<FitLoop>
<group rotation={[TILT, 0, 0]}>
{/* Faint disc inside the ring — grounds the ellipse as a plane so
the loop reads as an orbit rather than a floating outline. */}
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[RX * 0.92, 48]} />
<meshBasicMaterial color={RED} transparent opacity={0.035} depthWrite={false} />
</mesh>
<LoopPath />
{STATIONS.map((s, i) => (
<StationNode key={s.n} station={s} index={i} compact={isMobile} reduced={reduced} />
))}
{NODES.map((n, i) => (
<NodeMesh key={n.label} node={n} reduced={reduced} seed={i * 1.75} />
{packets.map((off) => (
<Packet key={off} offset={off} reduced={reduced} />
))}
</SlowSpin>
{!isMobile && (
<Sparkles count={70} scale={[11, 4.5, 6]} size={1.6} speed={reduced ? 0 : 0.15} color={RED_BRIGHT} opacity={0.3} />
)}
</group>
</FitLoop>
<EffectComposer enabled={!isMobile} multisampling={isMobile ? 0 : 2}>
<Bloom kernelSize={KernelSize.SMALL} luminanceThreshold={0.2} intensity={0.55} mipmapBlur />
<Bloom kernelSize={KernelSize.SMALL} luminanceThreshold={0.25} intensity={0.4} mipmapBlur />
</EffectComposer>
</Canvas>
);

View File

@@ -1,223 +1,49 @@
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import React from "react";
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
/* A small route graph: three loose columns of candidate nodes converging on
* one destination. Coordinates are hand-placed (not a strict grid) so the
* mesh reads as organic route options rather than a wiring diagram. */
const NODES = [
{ x: 40, y: 50 },
{ x: 40, y: 130 },
{ x: 40, y: 210 },
{ x: 150, y: 30 },
{ x: 150, y: 95 },
{ x: 150, y: 160 },
{ x: 150, y: 225 },
{ x: 270, y: 55 },
{ x: 270, y: 130 },
{ x: 270, y: 200 },
{ x: 400, y: 130 },
] as const;
const FINAL_NODE = 10;
const DIM_NODES = [0, 2, 3, 5, 6, 7, 9];
const ALL_EDGES: [number, number][] = [
[0, 3], [0, 4], [1, 4], [1, 5], [2, 5], [2, 6],
[3, 7], [4, 7], [4, 8], [5, 8], [5, 9], [6, 9],
[7, 10], [8, 10], [9, 10],
];
const SURVIVOR_EDGES: [number, number][] = [
[1, 4],
[4, 8],
[8, 10],
];
const edgeLength = (a: number, b: number) =>
Math.hypot(NODES[b].x - NODES[a].x, NODES[b].y - NODES[a].y);
/** Conceptual preview of Workflow1's route optimization: many candidate
* routes converge into the single plan MileTruth keeps. Plays once on
* scroll-in — grey mesh appears, a soft sweep suggests evaluation, unused
* routes fade, the surviving path draws in red, the destination pulses
* once — then stops. No loop, no labels, respects prefers-reduced-motion. */
function RouteConverge() {
const wrapRef = useRef<HTMLDivElement>(null);
const sweepRef = useRef<HTMLDivElement>(null);
const edgeRefs = useRef<(SVGLineElement | null)[]>([]);
const survivorEdgeRefs = useRef<(SVGLineElement | null)[]>([]);
const nodeRefs = useRef<(SVGCircleElement | null)[]>([]);
const haloRef = useRef<SVGCircleElement>(null);
const pulseRef = useRef<SVGCircleElement>(null);
useEffect(() => {
const wrap = wrapRef.current;
const sweep = sweepRef.current;
const halo = haloRef.current;
const pulse = pulseRef.current;
const edges = edgeRefs.current.filter(Boolean) as SVGLineElement[];
const survivorEdges = survivorEdgeRefs.current.filter(Boolean) as SVGLineElement[];
const nodes = nodeRefs.current.filter(Boolean) as SVGCircleElement[];
if (!wrap || !sweep || !halo || !pulse || edges.length === 0 || nodes.length === 0) return;
const dimNodes = DIM_NODES.map((i) => nodeRefs.current[i]).filter(Boolean) as SVGCircleElement[];
const survivorNodes = [1, 4, 8].map((i) => nodeRefs.current[i]).filter(Boolean) as SVGCircleElement[];
const finalNode = nodeRefs.current[FINAL_NODE] as SVGCircleElement;
gsap.set(edges, { opacity: 0 });
gsap.set(nodes, { opacity: 0, attr: { fill: "#fdfdfd", stroke: "rgba(17, 17, 17, 0.3)" } });
survivorEdges.forEach((el, i) => {
const [a, b] = SURVIVOR_EDGES[i];
const len = edgeLength(a, b);
gsap.set(el, { attr: { "stroke-dasharray": len, "stroke-dashoffset": len } });
});
gsap.set(halo, { opacity: 0 });
gsap.set(pulse, { opacity: 0, attr: { r: 5 } });
gsap.set(sweep, { opacity: 0, left: "-30%" });
const showFinalState = () => {
gsap.set(edges, { opacity: 0.08 });
gsap.set(dimNodes, { opacity: 0.3 });
gsap.set(nodes, { opacity: 1 });
gsap.set([...survivorNodes, finalNode], { attr: { fill: "#C01227", stroke: "#C01227" } });
survivorEdges.forEach((el) => gsap.set(el, { attr: { "stroke-dashoffset": 0 } }));
gsap.set(halo, { opacity: 0.22 });
};
if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
showFinalState();
return;
}
let tl: gsap.core.Timeline | undefined;
const play = () => {
tl = gsap.timeline();
tl.to(edges, { opacity: 1, duration: 0.5, ease: "power1.out" }, 0)
.to(nodes, { opacity: 1, duration: 0.5, ease: "power1.out" }, 0.05)
.set(sweep, { opacity: 0.9 }, 0.55)
.to(sweep, { left: "104%", duration: 1, ease: "power1.inOut" }, 0.55)
.to(sweep, { opacity: 0, duration: 0.3 }, 1.35)
.to(dimNodes, { opacity: 0.28, duration: 0.6, ease: "power1.out" }, 1.3)
.to(edges, { opacity: 0.08, duration: 0.6, ease: "power1.out" }, 1.3)
.to(survivorNodes, { attr: { fill: "#C01227", stroke: "#C01227" }, duration: 0.35, ease: "power1.out" }, 1.5)
.to(survivorEdges[0], { attr: { "stroke-dashoffset": 0 }, duration: 0.4, ease: "power2.inOut" }, 1.55)
.to(survivorEdges[1], { attr: { "stroke-dashoffset": 0 }, duration: 0.4, ease: "power2.inOut" }, 1.85)
.to(survivorEdges[2], { attr: { "stroke-dashoffset": 0 }, duration: 0.4, ease: "power2.inOut" }, 2.15)
.to(finalNode, { attr: { fill: "#C01227", stroke: "#C01227" }, duration: 0.3, ease: "power1.out" }, 2.5)
.to(halo, { opacity: 0.22, duration: 0.4, ease: "power1.out" }, 2.55)
.fromTo(
pulse,
{ opacity: 0.7, attr: { r: 5 } },
{ opacity: 0, attr: { r: 22 }, duration: 0.7, ease: "power2.out" },
2.55,
);
};
const st = ScrollTrigger.create({
trigger: wrap,
start: "top 82%",
once: true,
onEnter: play,
});
const raf = requestAnimationFrame(() => {
const r = wrap.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
if (r.top < vh && r.bottom > 0) {
play();
st.kill();
}
});
return () => {
cancelAnimationFrame(raf);
st.kill();
tl?.kill();
};
}, []);
return (
<div className="mti__viz" ref={wrapRef} aria-hidden="true">
<div className="mti__viz-frame">
<div className="mti__viz-sweep" ref={sweepRef} />
<svg className="mti__viz-svg" viewBox="0 0 440 260" preserveAspectRatio="xMidYMid meet">
<g>
{ALL_EDGES.map(([a, b], i) => (
<line
key={i}
ref={(el) => {
edgeRefs.current[i] = el;
}}
x1={NODES[a].x}
y1={NODES[a].y}
x2={NODES[b].x}
y2={NODES[b].y}
className="mti__viz-edge"
/>
))}
</g>
<g>
{SURVIVOR_EDGES.map(([a, b], i) => (
<line
key={i}
ref={(el) => {
survivorEdgeRefs.current[i] = el;
}}
x1={NODES[a].x}
y1={NODES[a].y}
x2={NODES[b].x}
y2={NODES[b].y}
className="mti__viz-edge-active"
/>
))}
</g>
<circle ref={haloRef} cx={NODES[FINAL_NODE].x} cy={NODES[FINAL_NODE].y} r={11} className="mti__viz-halo" />
<g>
{NODES.map((n, i) => (
<circle
key={i}
ref={(el) => {
nodeRefs.current[i] = el;
}}
cx={n.x}
cy={n.y}
r={i === FINAL_NODE ? 4.5 : 3.2}
className={i === FINAL_NODE ? "mti__viz-node mti__viz-node--final" : "mti__viz-node"}
/>
))}
</g>
<circle ref={pulseRef} cx={NODES[FINAL_NODE].x} cy={NODES[FINAL_NODE].y} r={5} className="mti__viz-pulse" />
const MILETRUTH_METRICS = [
{
icon: (
<svg className="mti-icon" width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
</div>
);
}
),
value: "100+",
label: "parallel route permutations evaluated per order",
},
{
icon: (
<svg className="mti-icon" width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
),
value: "38ms",
label: "decision latency before instant auto-dispatch",
},
{
icon: (
<svg className="mti-icon" width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<polyline points="9 12 11 14 15 10" />
</svg>
),
value: "99.9%",
label: "guaranteed on-time SLA delivery accuracy",
},
{
icon: (
<svg className="mti-icon" width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 18l-9.5-9.5-5 5L1 6" />
<polyline points="17 18 23 18 23 12" />
</svg>
),
value: "-22%",
label: "reduction in total fuel & operational costs",
},
];
/** Short editorial bridge between the MileTruth hero/metrics and Workflow1 —
* a quick read, not a second hero. No motion, no media, no CTA.
*
* Spacing mirrors the workflow shells (EVSection): a 20px outer inset
* (width calc(100% - 40px) / margin 0 20px) and a 1280px inner with 48px
* horizontal padding, so the heading/body left-align with the workflow
* cards above and below instead of using an unrelated flat gutter. The
* shell also carries the same external top margin as EVSection's
* `.evnd-gap-top` (40 / 32 / 24 desktop-tablet-mobile) so the gap above
* this section matches the gap above every workflow section below it —
* otherwise the Hero sits flush against this section while every other
* section boundary on the page carries that gap.
*
* The right side carries a minimal "many routes -> one route" visualization
* (see RouteConverge above) instead of sitting empty — it never touches the
* heading/copy, spacing, or section height on the left. */
export default function MileTruthIntro() {
return (
<>
@@ -226,157 +52,130 @@ export default function MileTruthIntro() {
__html: `
.mti-shell {
width: calc(100% - 40px);
margin: 40px 20px 0;
margin: 40px 20px clamp(36px, 4vw, 56px);
}
.mti {
background: #fafafa;
padding: clamp(28px, 3.5vw, 40px) 0;
background: #ffffff;
border-radius: 24px;
padding: clamp(36px, 4.5vw, 56px) clamp(24px, 4vw, 48px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.04);
}
.mti__inner {
max-width: 1280px;
max-width: 1240px;
margin: 0 auto;
padding: 0 48px;
}
.mti__row {
display: flex;
align-items: center;
gap: clamp(32px, 5vw, 64px);
}
.mti__left {
flex: 1;
min-width: 0;
.mti__head {
max-width: 860px;
margin-bottom: clamp(36px, 4.5vw, 52px);
}
.mti__eyebrow {
display: inline-flex;
align-items: center;
gap: 12px;
color: #C01227 !important;
font-weight: 800;
font-size: 13.5px;
font-weight: 600;
letter-spacing: 0.08em;
color: #6b7280;
margin-bottom: 14px;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 13px;
margin-bottom: clamp(14px, 1.8vw, 18px);
}
.mti__eyebrow::before {
content: '';
width: 16px;
height: 2px;
background: #C01227;
display: block;
}
.mti__divider {
border: 0;
height: 1px;
background: rgba(17, 17, 17, 0.12);
background: rgba(0, 0, 0, 0.12);
margin: 0 0 clamp(20px, 2.4vw, 28px);
width: 100%;
}
.mti__heading {
margin: 0 0 clamp(18px, 2vw, 24px);
color: #111111 !important;
margin: 0 0 clamp(16px, 2vw, 24px);
color: #111827 !important;
font-weight: 800 !important;
font-size: clamp(28px, 3.4vw, 46px) !important;
line-height: 1.15 !important;
letter-spacing: -0.02em;
text-transform: uppercase;
font-size: clamp(32px, 4.2vw, 56px) !important;
line-height: 1.12 !important;
letter-spacing: -0.025em;
text-transform: none !important;
}
.mti__accent {
color: #C01227 !important;
color: #dc2626 !important;
}
.mti__copy {
max-width: 680px;
.mti__desc {
font-size: clamp(16px, 1.15vw, 18px);
line-height: 1.65;
color: #4b5563;
font-weight: 400;
margin: 0;
max-width: 780px;
}
.mti__copy p {
margin: 0 0 14px;
color: rgba(17, 17, 17, 0.68) !important;
font-size: clamp(15px, 1.05vw, 17px) !important;
font-weight: 500 !important;
line-height: 1.6 !important;
.mti-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: clamp(20px, 3vw, 36px);
padding-top: clamp(28px, 3.5vw, 40px);
}
.mti__copy p:last-child {
margin-bottom: 0;
.mti-card {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-right: 12px;
border-right: 1px solid rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease;
}
.mti__lede {
margin-left: clamp(16px, 2vw, 24px);
.mti-card:last-child {
border-right: none;
padding-right: 0;
}
.mti__viz {
flex: 0 1 38%;
min-width: 240px;
max-width: 460px;
.mti-card-icon {
color: #111827;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.35s ease, color 0.35s ease;
}
.mti__viz-frame {
position: relative;
width: 100%;
.mti-card:hover .mti-card-icon {
transform: translateY(-4px) scale(1.08);
color: #dc2626;
}
.mti__viz-svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
.mti-icon {
transition: stroke 0.35s ease;
}
.mti__viz-edge {
stroke: rgba(17, 17, 17, 0.16);
stroke-width: 1;
.mti-card-val {
font-size: clamp(32px, 3.2vw, 46px);
font-weight: 800;
color: #111827;
line-height: 1.05;
letter-spacing: -0.02em;
margin-bottom: 8px;
}
.mti__viz-edge-active {
fill: none;
stroke: #C01227;
stroke-width: 1.4;
}
.mti__viz-node {
fill: #fdfdfd;
stroke: rgba(17, 17, 17, 0.3);
stroke-width: 1.1;
}
.mti__viz-node--final {
stroke-width: 1.3;
}
.mti__viz-halo {
fill: #C01227;
opacity: 0;
filter: blur(6px);
}
.mti__viz-pulse {
fill: none;
stroke: #C01227;
stroke-width: 1.4;
opacity: 0;
}
.mti__viz-sweep {
position: absolute;
top: 0;
bottom: 0;
width: 24%;
background: linear-gradient(90deg, transparent, rgba(192, 18, 39, 0.14), transparent);
opacity: 0;
pointer-events: none;
}
@media (max-width: 1024px) {
.mti-shell { margin-top: 32px; }
.mti__inner { padding: 0 32px; }
.mti-card-label {
font-size: 14.5px;
line-height: 1.5;
color: #4b5563;
font-weight: 450;
}
@media (max-width: 900px) {
.mti__row {
flex-direction: column;
align-items: center;
gap: clamp(28px, 5vw, 40px);
.mti-grid {
grid-template-columns: repeat(2, 1fr);
gap: 28px;
}
.mti__left {
width: 100%;
.mti-card {
border-right: none;
padding-right: 0;
}
.mti__viz {
flex: none;
width: 100%;
max-width: 360px;
}
@media (max-width: 540px) {
.mti-grid {
grid-template-columns: 1fr;
gap: 24px;
}
}
@media (max-width: 767px) {
.mti-shell {
width: calc(100% - 20px);
margin: 24px 10px 0;
margin: 24px 10px 32px;
}
.mti {
padding: 28px 20px;
}
.mti__inner { padding: 0 20px; }
}
`,
}}
@@ -384,26 +183,26 @@ export default function MileTruthIntro() {
<div className="mti-shell">
<section className="mti" aria-label="MileTruth AI introduction">
<div className="mti__inner">
<div className="mti__row">
<div className="mti__left">
<span className="mti__eyebrow">/ MILETRUTH AI /</span>
<div className="mti__head">
<span className="mti__eyebrow">/ MileTruth AI /</span>
<hr className="mti__divider" />
<h2 className="mti__heading">
ROUTING IS A <span className="mti__accent">DECISION PROBLEM.</span>
Routing is a <span className="mti__accent">Decision Problem.</span>
</h2>
<div className="mti__copy">
<p className="mti__lede">Routing is not one path between two points.</p>
<p>
Orders change. Vehicles have different limits. Capacity, range
and time change what is possible.
</p>
<p>
MileTruth evaluates the options, checks what can actually work,
and moves the strongest plan forward.
<p className="mti__desc">
Routing is not simply one static line between two points. Orders change, vehicles have strict capacity limits, and traffic dictates what is possible. MileTruth evaluates every option in real time to commit to the single strongest plan.
</p>
</div>
{/* 4-Column Animated SVG KPI Grid */}
<div className="mti-grid">
{MILETRUTH_METRICS.map((m) => (
<div className="mti-card" key={m.value}>
<div className="mti-card-icon">{m.icon}</div>
<div className="mti-card-val">{m.value}</div>
<div className="mti-card-label">{m.label}</div>
</div>
<RouteConverge />
))}
</div>
</div>
</section>

View File

@@ -60,10 +60,31 @@ export default function MileTruthNetwork({ ariaLabel }: { ariaLabel?: string })
return (
<div ref={wrapRef} className="mtn-scene" role="img" aria-label={ariaLabel ?? "MileTruth AI network"}>
<style dangerouslySetInnerHTML={{ __html: `
.mtn-scene { position: relative; width: 100%; height: 100%; min-height: 320px; }
.mtn-scene { position: relative; width: 100%; height: 100%; min-height: 360px; }
.mtn-scene canvas { display: block; width: 100% !important; height: 100% !important; }
/* Station labels. Rendered as DOM by drei <Html> inside the canvas
wrapper, so the text stays crisp at any DPR and can use the
site's own type scale. */
.mtn-lbl { display: flex; flex-direction: column; align-items: center; gap: 2px; white-space: nowrap; text-align: center; user-select: none; pointer-events: none; }
.mtn-lbl-n { font-size: 9.5px; font-weight: 800; letter-spacing: 0.14em; color: #E2354A; line-height: 1; }
.mtn-lbl-name { font-size: 12px; font-weight: 700; color: #fff; line-height: 1.2; text-shadow: 0 1px 6px rgba(0,0,0,0.8); }
.mtn-lbl-fig { font-size: 10.5px; font-weight: 500; color: rgba(255,255,255,0.6); line-height: 1.2; }
/* Sample-data marker — these figures are illustrative, not audited. */
.mtn-tag { position: absolute; right: 12px; bottom: 12px; z-index: 13; display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 999px; background: rgba(12,12,16,0.72); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.5); font-size: 10px; font-weight: 600; letter-spacing: 0.04em; pointer-events: none; }
.mtn-tag i { width: 4px; height: 4px; border-radius: 50%; background: #E2354A; display: block; }
@media (max-width: 767px) {
.mtn-scene { min-height: 300px; }
.mtn-lbl-name { font-size: 11px; }
.mtn-tag { right: 8px; bottom: 8px; font-size: 9px; padding: 4px 8px; }
}
`}} />
{mountScene && <MileTruthCanvas reduced={reduced} isMobile={isMobile} active={active} />}
{mountScene && (
<span className="mtn-tag" aria-hidden="true"><i />Illustrative sample insight cycle</span>
)}
</div>
);
}

View File

@@ -3,7 +3,17 @@ import FirstMileIcon from "../icons/FirstMileIcon";
import MidMileIcon from "../icons/MidMileIcon";
import LastMileIcon from "../icons/LastMileIcon";
export default function Miles3() {
interface Miles3Props {
/** Eyebrow above the heading. Defaults to the original How It Works label. */
eyebrow?: string;
/** Section heading. Defaults to the original How It Works copy. */
heading?: string;
}
export default function Miles3({
eyebrow = "/ How It Works /",
heading = "Doormile connects first, mid, and last mile into a seamless end-to-end logistics experience",
}: Miles3Props = {}) {
return (
<>
<style dangerouslySetInnerHTML={{ __html: `
@@ -190,12 +200,12 @@ export default function Miles3() {
<div className="elementor-element elementor-element-5bc90f3 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5bc90f3" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-176d17f elementor-widget elementor-widget-logico_heading" data-id="176d17f" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">/ How It Works /</div>
<div className="logico-title">{eyebrow}</div>
</div>
</div>
<div className="elementor-element elementor-element-63a9de5 elementor-widget elementor-widget-logico_heading" data-id="63a9de5" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<h3 className="logico-title">Doormile connects first, mid, and last mile into a seamless end-to-end logistics experience</h3>
<h3 className="logico-title">{heading}</h3>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import WorkflowScene from "./WorkflowScene";
@@ -9,17 +9,38 @@ if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
/* ============================================================
QuantumStage — the page's ONE true 3D moment. Wraps the existing
production "Logistics Brain" scene (glowing core, orbiting particle
shells, six parallel route strategies racing, live network) —
already built and already on-brand red — inside a near-full-viewport
frame, with a floating Traditional-vs-Quantum comparison overlaid.
GSAP drives only the entrance (fade/rise); the 3D scene animates
itself.
============================================================ */
function Svg({ children, size = 18 }: { children: React.ReactNode; size?: number }) {
return (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{children}
</svg>
);
}
const IcLegacy = <Svg><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 3" /></Svg>;
const IcCpu = <Svg><rect x="4" y="4" width="16" height="16" rx="2" /><rect x="9" y="9" width="6" height="6" /><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3" /></Svg>;
const IcBrain = <Svg><path d="M12 2a9 9 0 019 9c0 5-4 9-9 9s-9-4-9-9a9 9 0 019-9z" /><path d="M12 8v8M8 12h8" /></Svg>;
const IcTarget = <Svg><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></Svg>;
const IcDispatch = <Svg><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" /></Svg>;
export default function QuantumStage() {
const rootRef = useRef<HTMLDivElement>(null);
const [activeStep, setActiveStep] = useState(0);
const [simCount, setSimCount] = useState(148);
// Live dynamic step simulation loop
useEffect(() => {
const timer = setInterval(() => {
setActiveStep((prev) => {
const next = (prev + 1) % 4;
if (next === 0) {
setSimCount(Math.floor(140 + Math.random() * 90));
}
return next;
});
}, 2200);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const root = rootRef.current;
@@ -36,29 +57,34 @@ export default function QuantumStage() {
}
gsap.set(frame, { opacity: 0, y: 26 });
gsap.set(tradCol, { opacity: 0, y: 26 });
gsap.set(qSteps, { opacity: 0.25, y: 10 });
gsap.set(qDot, { scale: 0.6, opacity: 0 });
gsap.set(qSteps, { opacity: 0.2, y: 14, scale: 0.96 });
if (qDot) gsap.set(qDot, { scale: 0.5, opacity: 0 });
const play = () => {
const tl = gsap.timeline();
// Frame + traditional baseline settle in first (the "boring" case).
tl.to(frame, { opacity: 1, y: 0, duration: 0.9, ease: "power3.out" }, 0)
.to(tradCol, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 0.15);
// Quantum steps light up one at a time — a process unfolding, not a fade group.
tl.to(frame, { opacity: 1, y: 0, duration: 0.8, ease: "power3.out" }, 0)
.to(tradCol, { opacity: 1, y: 0, duration: 0.6, ease: "power3.out" }, 0.15);
qSteps.forEach((el, i) => {
tl.to(el, { opacity: 1, y: 0, scale: 1.04, duration: 0.32, ease: "power2.out" }, 0.7 + i * 0.42)
.to(el, { scale: 1, duration: 0.22, ease: "power1.out" }, 0.7 + i * 0.42 + 0.32);
tl.to(el, { opacity: 1, y: 0, scale: 1, duration: 0.4, ease: "back.out(1.4)" }, 0.6 + i * 0.22);
});
// Dispatch pulses brighter — the decisive moment.
const last = qSteps[qSteps.length - 1];
tl.to(qDot, { opacity: 1, scale: 1, duration: 0.4, ease: "back.out(2)" }, 0.7 + qSteps.length * 0.42)
.to(last, { color: "#ff5a6e", duration: 0.3 }, "<");
if (qDot) {
tl.to(qDot, { opacity: 1, scale: 1, duration: 0.4, ease: "back.out(2)" }, 0.6 + qSteps.length * 0.22);
}
};
const st = ScrollTrigger.create({ trigger: root, start: "top 78%", once: true, onEnter: play });
return () => st.kill();
}, []);
const STEP_STATUSES = [
`⚡ RUNNING ${simCount} PARALLEL PERMUTATIONS`,
`🧠 SCORING SLA, TRAFFIC & FUEL COSTS`,
`🎯 OPTIMAL ROUTE MATCH LOCKED (99.8% EFFICIENCY)`,
`🚀 INSTANT AUTO-DISPATCH EXECUTED (0.02s)`,
];
return (
<div ref={rootRef} className="qs">
<style dangerouslySetInnerHTML={{ __html: CSS }} />
@@ -67,26 +93,121 @@ export default function QuantumStage() {
</div>
<div className="qs-compare">
{/* Traditional baseline */}
<div className="qs-col qs-col--trad">
<span className="qs-col-tag">Traditional</span>
<div className="qs-col-steps">
<span>One Route</span>
<span className="qs-col-arrow"></span>
<span>Dispatch</span>
</div>
</div>
<div className="qs-col qs-col--q">
<span className="qs-col-tag qs-col-tag--accent">Quantum</span>
<div className="qs-col-steps qs-col-steps--accent">
<span className="qs-qstep">100+ Route Simulations</span>
<span className="qs-col-arrow"></span>
<span className="qs-qstep">AI Comparison</span>
<span className="qs-col-arrow"></span>
<span className="qs-qstep">Best Route</span>
<span className="qs-col-arrow"></span>
<span className="qs-qstep qs-col-final">
Dispatch<span className="qs-qdot" aria-hidden="true" />
<div className="qs-col-head">
<span className="qs-col-tag">
<span className="qs-tag-dot qs-tag-dot--muted" />
Traditional Approach
</span>
<span className="qs-col-subtag">Legacy Sequential</span>
</div>
<div className="qs-col-steps qs-col-steps--trad">
<div className="qs-step-pill qs-step-pill--trad">
<span className="qs-step-ic">{IcLegacy}</span>
<div className="qs-step-txt">
<b>Single Route Calculation</b>
<span>Linear, non-simulated path</span>
</div>
</div>
<span className="qs-col-arrow-v"></span>
<div className="qs-step-pill qs-step-pill--trad">
<span className="qs-step-ic">{IcDispatch}</span>
<div className="qs-step-txt">
<b>Manual Dispatch</b>
<span>Static SLA execution</span>
</div>
</div>
</div>
<div className="qs-col-foot">
<span className="qs-metric-chip">1 Route Evaluated</span>
<span className="qs-metric-chip qs-metric-chip--warn">High Exception Risk</span>
</div>
</div>
{/* Quantum Engine */}
<div className="qs-col qs-col--q">
<div className="qs-col-head">
<span className="qs-col-tag qs-col-tag--accent">
<span className="qs-tag-dot qs-tag-dot--active" />
Quantum Engine
</span>
<span className="qs-col-sys">{STEP_STATUSES[activeStep]}</span>
</div>
<div className="qs-col-steps qs-col-steps--accent">
<div
className={`qs-qstep qs-step-pill qs-step-pill--q ${
activeStep === 0 ? "qs-step-pill--active" : ""
}`}
onClick={() => setActiveStep(0)}
>
<div className="qs-step-head">
<span className="qs-step-ic qs-step-ic--accent">{IcCpu}</span>
</div>
<div className="qs-step-txt">
<b>{simCount}+ Parallel Simulations</b>
<span>Instant route permutations</span>
</div>
{activeStep === 0 && <div className="qs-step-bar" />}
</div>
<div
className={`qs-qstep qs-step-pill qs-step-pill--q ${
activeStep === 1 ? "qs-step-pill--active" : ""
}`}
onClick={() => setActiveStep(1)}
>
<div className="qs-step-head">
<span className="qs-step-ic qs-step-ic--accent">{IcBrain}</span>
</div>
<div className="qs-step-txt">
<b>AI Outcome Scoring</b>
<span>Evaluates SLA, traffic & cost</span>
</div>
{activeStep === 1 && <div className="qs-step-bar" />}
</div>
<div
className={`qs-qstep qs-step-pill qs-step-pill--q ${
activeStep === 2 ? "qs-step-pill--active" : ""
}`}
onClick={() => setActiveStep(2)}
>
<div className="qs-step-head">
<span className="qs-step-ic qs-step-ic--accent">{IcTarget}</span>
</div>
<div className="qs-step-txt">
<b>Optimal Route Match</b>
<span>Highest efficiency path</span>
</div>
{activeStep === 2 && <div className="qs-step-bar" />}
</div>
<div
className={`qs-qstep qs-step-pill qs-step-pill--q qs-col-final ${
activeStep === 3 ? "qs-step-pill--active" : ""
}`}
onClick={() => setActiveStep(3)}
>
<div className="qs-step-head">
<span className="qs-step-ic qs-step-ic--live">{IcDispatch}</span>
<span className="qs-qdot" aria-hidden="true" />
</div>
<div className="qs-step-txt">
<b>Instant Auto-Dispatch</b>
<span>Optimized in real time</span>
</div>
{activeStep === 3 && <div className="qs-step-bar" />}
</div>
</div>
<div className="qs-col-foot">
<span className="qs-metric-chip qs-metric-chip--good"> {simCount}+ Scenarios Checked</span>
<span className="qs-metric-chip qs-metric-chip--good"> Zero SLA Risk</span>
<span className="qs-metric-chip qs-metric-chip--good"> Real-Time Sync</span>
</div>
</div>
</div>
@@ -97,29 +218,31 @@ export default function QuantumStage() {
const CSS = `
.qs { position: relative; }
/* --------------------------------------------------------------
Section 05 runs light — every other section on this page keeps
the shared dark "ground texture" system, but this one uses the
shared white .sw-bg--white variant (set via the Section's bg
prop in SolutionsWorkflow.tsx) so the workflow reads as a
dashboard panel, not a visualization sitting in a black canvas —
while still going through the exact same inset/rounded .sw-bg
card surface as every other section. Only the text colors need
a local override here, for readability on that white surface.
-------------------------------------------------------------- */
#quantum .sw-h { color: #14151a !important; }
#quantum .sw-lede { color: #5a5b64 !important; }
#sw-root #quantum h2,
#sw-root #quantum .sw-h,
#quantum h2,
#quantum .sw-h { color: #111827 !important; }
#sw-root #quantum h2 .accent,
#sw-root #quantum .sw-h .accent,
#quantum h2 .accent,
#quantum .sw-h .accent { color: #dc2626 !important; }
#sw-root #quantum p,
#sw-root #quantum .sw-lede,
#quantum p,
#quantum .sw-lede { color: #4b5563 !important; }
.qs-frame {
position: relative;
width: 100%;
aspect-ratio: auto;
min-height: min(84vh, 900px);
border-radius: 28px;
border-radius: 24px;
overflow: hidden;
background: #f6f6f7;
border: 1px solid rgba(20,21,26,0.08);
box-shadow: 0 30px 70px -45px rgba(20,21,26,0.25);
background: #0d0d11;
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 24px 60px -20px rgba(0,0,0,0.15);
}
.qs-frame .wf-scene {
position: absolute;
@@ -129,36 +252,274 @@ const CSS = `
aspect-ratio: auto;
border-radius: 16px;
}
/* ---- Redesigned Comparison Grid (White & Red Theme with Tight Bottom Padding) ---- */
.qs-compare {
position: relative;
margin-top: clamp(28px, 4vw, 48px);
margin-top: 24px;
margin-bottom: 0px;
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: clamp(20px, 3vw, 40px);
grid-template-columns: 1fr 2.2fr;
gap: 16px;
}
/* Card Base */
.qs-col {
padding: clamp(20px, 2.4vw, 30px) clamp(22px, 2.6vw, 34px);
padding: 20px 22px;
border-radius: 22px;
background: #f6f6f7;
border: 1px solid rgba(20,21,26,0.08);
box-shadow: 0 20px 44px -34px rgba(20,21,26,0.2);
display: flex; flex-direction: column; align-items: center; gap: 18px; text-align: center;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 16px;
position: relative;
overflow: hidden;
}
.qs-col--q { background: linear-gradient(180deg, rgba(192,18,39,0.06), rgba(246,246,247,0.4)); border-color: rgba(192,18,39,0.22); }
.qs-col-tag { font-size: 12px; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; color: #8a8b93; }
.qs-col-tag--accent { color: #C01227; }
.qs-col-steps { display: flex; flex-direction: column; align-items: center; gap: 8px; color: #4a4b53; font-weight: 700; font-size: 14px; }
.qs-col-steps--accent { color: #14151a; font-size: 15px; flex-direction: row; flex-wrap: wrap; justify-content: center; }
.qs-qstep { display: inline-flex; align-items: center; position: relative; }
.qs-col-arrow { color: rgba(20,21,26,0.28); font-size: 13px; }
.qs-col-final { color: #C01227; display: inline-flex; align-items: center; gap: 8px; }
.qs-qdot { width: 8px; height: 8px; border-radius: 50%; background: #E2354A; box-shadow: 0 0 12px 3px rgba(226,53,74,0.55); }
@media (max-width: 860px) {
.qs-compare { grid-template-columns: 1fr; }
.qs-col-steps--accent { flex-direction: column; }
/* Traditional Card - Muted Off-White */
.qs-col--trad {
background: #f8f9fa;
border: 1px solid #e5e7eb;
box-shadow: 0 8px 24px rgba(0,0,0,0.03);
}
@media (max-width: 767px) {
.qs-frame { min-height: 62vh; border-radius: 20px; }
.qs-frame .wf-scene { border-radius: 14px; }
/* Quantum Card - White & Soft Crimson Gradient */
.qs-col--q {
background: linear-gradient(135deg, #ffffff 0%, #fff6f6 100%);
border: 1.5px solid rgba(220,38,38,0.22);
box-shadow: 0 14px 36px -8px rgba(220,38,38,0.08), 0 2px 6px rgba(0,0,0,0.02);
}
/* Header strip */
.qs-col-head {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.qs-col-tag {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #4b5563;
}
.qs-col-tag--accent {
color: #dc2626;
}
.qs-tag-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.qs-tag-dot--muted {
background: #9ca3af;
}
.qs-tag-dot--active {
background: #dc2626;
box-shadow: 0 0 10px rgba(220,38,38,0.6);
animation: qsPulse 2s infinite ease-in-out;
}
@keyframes qsPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.85); }
}
.qs-col-subtag, .qs-col-sys {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #9ca3af;
}
.qs-col-sys {
color: #dc2626;
transition: all 0.3s ease;
}
/* Steps Pipeline Layout */
.qs-col-steps {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.qs-col-steps--trad {
flex-direction: column;
gap: 10px;
}
.qs-col-steps--accent {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
align-items: stretch;
}
/* Step Pills */
.qs-step-pill {
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px 16px;
border-radius: 16px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
}
.qs-step-pill--trad {
background: #ffffff;
border: 1px solid #e5e7eb;
width: 100%;
flex-direction: row;
align-items: center;
cursor: default;
}
.qs-step-pill--q {
background: #ffffff;
border: 1.5px solid #f3f4f6;
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
height: 100%;
justify-content: space-between;
}
.qs-step-pill--q:hover {
border-color: rgba(220,38,38,0.3);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(220,38,38,0.08);
}
.qs-step-pill--active {
border-color: #dc2626 !important;
background: #ffffff !important;
box-shadow: 0 8px 24px rgba(220,38,38,0.18) !important;
transform: translateY(-3px) !important;
}
.qs-col-final {
background: rgba(220,38,38,0.04) !important;
border-color: rgba(220,38,38,0.3) !important;
}
/* Animated active step progress bar */
.qs-step-bar {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: #dc2626;
width: 100%;
animation: qsBar 2.4s linear infinite;
}
@keyframes qsBar {
0% { width: 0%; }
100% { width: 100%; }
}
.qs-step-head {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.qs-step-num {
font-size: 11px;
font-weight: 800;
color: #9ca3af;
}
.qs-step-ic {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 10px;
background: #f3f4f6;
color: #4b5563;
flex-shrink: 0;
}
.qs-step-ic--accent {
background: #fef2f2;
color: #dc2626;
}
.qs-step-ic--live {
background: #dc2626;
color: #ffffff;
box-shadow: 0 4px 12px rgba(220,38,38,0.4);
}
.qs-step-txt {
display: flex;
flex-direction: column;
gap: 2px;
text-align: left;
}
.qs-step-txt b {
color: #111827;
font-size: 12.5px;
font-weight: 700;
line-height: 1.25;
}
.qs-step-txt span {
color: #6b7280;
font-size: 11px;
line-height: 1.35;
}
.qs-col-arrow-v {
color: #d1d5db;
font-size: 14px;
}
/* Radar pulse dot on final step */
.qs-qdot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #dc2626;
box-shadow: 0 0 10px rgba(220,38,38,0.8);
}
/* Footer chips */
.qs-col-foot {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.qs-metric-chip {
font-size: 11px;
font-weight: 700;
padding: 5px 12px;
border-radius: 999px;
background: #ffffff;
border: 1px solid #e5e7eb;
color: #4b5563;
}
.qs-metric-chip--warn {
background: #fffbe6;
border-color: #fef08a;
color: #b45309;
}
.qs-metric-chip--good {
background: #ecfdf5;
border-color: #a7f3d0;
color: #047857;
}
@media (max-width: 1100px) {
.qs-compare {
grid-template-columns: 1fr;
}
.qs-col-steps--accent {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.qs-col-steps--accent {
grid-template-columns: 1fr;
}
.qs-frame { min-height: 52vh; border-radius: 20px; }
}
`;

View File

@@ -3,7 +3,29 @@
import React, { useState, useEffect } from "react";
import { preload } from "react-dom";
export default function SolutionsHero() {
export interface HeroSlide {
heading: string;
text: string;
}
/**
* Original industry-focused copy from the PHP Solutions page
* (doormile-site/includes/sections/hero/solutions-hero-section.php).
* Kept as the default so the Industries page renders the original hero verbatim;
* the Solutions page passes its own offerings copy.
*/
const DEFAULT_SLIDES: HeroSlide[] = [
{
heading: "Solutions for Every Industry",
text: "Discover how Doormile's connected logistics platform serves diverse industries with tailored solutions.",
},
{
heading: "One Platform. All Verticals",
text: "Doormile adapts to the unique challenges of your vertical—not the other way around.",
},
];
export default function SolutionsHero({ slides = DEFAULT_SLIDES }: { slides?: HeroSlide[] } = {}) {
preload("/images/home1-slide-1.webp", { as: "image" });
const [activeSlide, setActiveSlide] = useState(0);
@@ -233,11 +255,11 @@ export default function SolutionsHero() {
<div className="slide-content">
<div className="slide-content-inner">
<h1 className="content-slider-item-heading logico-content-wrapper-1">
<span className="heading-content">Solutions for Every Industry</span>
<span className="heading-content">{slides[0].heading}</span>
</h1>
<div className="content-slider-item-text logico-content-wrapper-2">
<div className="text-content">
<p>Discover how Doormile&apos;s connected logistics platform serves diverse industries with tailored solutions.</p>
<p>{slides[0].text}</p>
</div>
</div>
</div>
@@ -263,11 +285,11 @@ export default function SolutionsHero() {
<div className="slide-content">
<div className="slide-content-inner">
<h1 className="content-slider-item-heading logico-content-wrapper-1">
<span className="heading-content">One Platform. All Verticals</span>
<span className="heading-content">{slides[1].heading}</span>
</h1>
<div className="content-slider-item-text logico-content-wrapper-2">
<div className="text-content">
<p>Doormile adapts to the unique challenges of your verticalnot the other way around.</p>
<p>{slides[1].text}</p>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@ import EVSection, { type EVFeature, type EVStat } from "./EVSection";
// EVSection embeds this value in an UNQUOTED CSS url(...).
const METRICS: EVStat[] = [
{ value: 8.2, decimals: 1, suffix: "t", label: "ATR72 Payload" },
{ value: 1600, suffix: "km", label: "ATR72 Range" },
{ value: 8.6, decimals: 1, suffix: "t", label: "Q400 Payload" },
{ value: 667, suffix: "km/h", label: "Q400 Speed" },
{ value: 8.2, decimals: 1, suffix: "t", label: "Payload Capacity" },
{ value: 1600, suffix: "km", label: "Regional Range" },
{ value: 8.6, decimals: 1, suffix: "t", label: "Max Payload" },
{ value: 667, suffix: "km/h", label: "Regional Speed" },
];
const FEATURES: EVFeature[] = [

View File

@@ -1,68 +1,115 @@
"use client";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import { ScrollReveal, StaggerChildren } from "@/animations/Reveal";
import { ScrollReveal } from "@/animations/Reveal";
const FLOW_STAGES = [
{
id: "data",
label: "Logistics Data",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
<path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3" />
</svg>
),
description: "Orders, fleet telemetry, routes, and traffic updates compiled in real-time."
},
{
id: "ai",
label: "AI Core",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
<rect x="9" y="9" width="6" height="6" />
<line x1="9" y1="1" x2="9" y2="4" />
<line x1="15" y1="1" x2="15" y2="4" />
<line x1="9" y1="20" x2="9" y2="23" />
<line x1="15" y1="20" x2="15" y2="23" />
<line x1="20" y1="9" x2="23" y2="9" />
<line x1="20" y1="15" x2="23" y2="15" />
<line x1="1" y1="9" x2="4" y2="9" />
<line x1="1" y1="15" x2="4" y2="15" />
</svg>
),
description: "Neural engine simulating network patterns and predicting constraints."
},
{
id: "recommendations",
label: "Recommendations",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5" />
<path d="M9 18h6" />
<path d="M10 22h4" />
</svg>
),
description: "Optimized route configurations, SLA status warnings, and capacity balancing."
},
{
id: "improvement",
label: "Continuous Learning",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 4v6h-6" />
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
),
description: "Post-flight feedback loops refining algorithms with every shipment."
}
];
const CAPABILITIES = [
{
title: "SLA Monitoring",
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
),
},
{
title: "Demand Forecasting",
desc: "AI-driven forward projection of cargo demand, preemptively positioning aircraft and ground capacity.",
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 3v18h18" />
<path d="M7 14l4-4 3 3 6-7" />
</svg>
),
},
{
title: "Dynamic Pricing",
desc: "Real-time rate adjustments that track market capacity fluctuations and optimize yield.",
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M9.5 9.5c0-1.4 1.1-2.3 2.5-2.3s2.5.9 2.5 2c0 1.6-2.2 1.8-2.5 3.3M12 17h.01" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>
),
},
{
title: "Route Optimization",
desc: "Dynamic scheduling for ground EV pickups and flights that bypasses congestion for the fastest path.",
title: "Route & Fleet Optimization",
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="13 6 19 12 13 18" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 12h18" />
<path d="M12 3v18" />
<path d="m18 17 4-5-4-5" />
</svg>
),
},
{
title: "Load Balancing",
desc: "Automated weight-and-balance modeling for optimal, safe aircraft load layout within minutes.",
title: "Anomaly Detection",
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 3v18M5 8l7-5 7 5M5 8v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
),
},
{
title: "Predictive Maintenance",
desc: "Continuous telemetry monitoring triggers preemptive alerts, drastically reducing aircraft downtime.",
title: "Performance Insights",
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 12h-6l-4 8-4-16-4 8H2" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20V10" />
<path d="M18 20V4" />
<path d="M6 20v-4" />
</svg>
),
},
{
title: "Real-Time Tracking",
desc: "End-to-end sensor monitoring keeps shippers, airline, and EV operators updated on exact shipment state.",
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
),
},
}
];
function TiltCard({ children, className }: { children: React.ReactNode, className: string }) {
@@ -73,8 +120,6 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
const card = cardRef.current;
const glare = glareRef.current;
if (!card || !glare) return;
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) return;
const handleMouseMove = (e: MouseEvent) => {
const rect = card.getBoundingClientRect();
@@ -84,39 +129,24 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Softer, more premium 3D tilt
const rotateX = ((y - centerY) / centerY) * -3;
const rotateY = ((x - centerX) / centerX) * 3;
const rotateX = ((y - centerY) / centerY) * -4;
const rotateY = ((x - centerX) / centerX) * 4;
gsap.to(card, {
duration: 0.4,
duration: 0.5,
rotateX,
rotateY,
y: -4,
transformPerspective: 1000,
transformPerspective: 1200,
ease: "power2.out"
});
// Dynamic glare tracking
gsap.set(glare, {
"--mx": `${x}px`,
"--my": `${y}px`,
});
gsap.to(glare, {
duration: 0.3,
opacity: 1,
ease: "power2.out"
});
// Z-axis pop
gsap.to(card.querySelector('.wmt-card-icon'), {
duration: 0.4,
z: 25,
ease: "power2.out"
});
gsap.to(card.querySelector('.wmt-card-title'), {
duration: 0.4,
z: 15,
opacity: 0.15,
ease: "power2.out"
});
};
@@ -126,7 +156,6 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
duration: 0.8,
rotateX: 0,
rotateY: 0,
y: 0,
ease: "power3.out"
});
gsap.to(glare, {
@@ -134,11 +163,6 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
opacity: 0,
ease: "power3.out"
});
gsap.to(card.querySelectorAll('.wmt-card-icon, .wmt-card-title'), {
duration: 0.8,
z: 0,
ease: "power3.out"
});
};
card.addEventListener("mousemove", handleMouseMove);
@@ -151,29 +175,29 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
}, []);
return (
<div ref={cardRef} className={className}>
<div ref={cardRef} className={className} style={{ perspective: "1000px" }}>
<div
ref={glareRef}
style={{
position: "absolute",
inset: 0,
borderRadius: "inherit",
background: "radial-gradient(circle 220px at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.08) 0%, rgba(200, 16, 46, 0.03) 40%, transparent 100%)",
background: "radial-gradient(circle 300px at var(--mx, 50%) var(--my, 50%), rgba(255, 59, 59, 0.1) 0%, transparent 100%)",
opacity: 0,
pointerEvents: "none",
zIndex: 0,
zIndex: 2,
transform: "translateZ(1px)",
mixBlendMode: "screen"
}}
/>
<div style={{ position: "relative", zIndex: 1, transformStyle: "preserve-3d" }}>
<div style={{ position: "relative", zIndex: 1, transformStyle: "preserve-3d", height: "100%", width: "100%" }}>
{children}
</div>
</div>
);
}
export default function WingsMileTruth() {
const ParticleCanvas = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
@@ -182,85 +206,90 @@ export default function WingsMileTruth() {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
let animationFrameId: number;
let width = (canvas.width = canvas.offsetWidth);
let height = (canvas.height = canvas.offsetHeight);
const handleResize = () => {
if (!canvas) return;
width = canvas.width = canvas.offsetWidth;
height = canvas.height = canvas.offsetHeight;
};
window.addEventListener("resize", handleResize);
const nodes = [
{ x: 0.12, y: 0.5 },
{ x: 0.34, y: 0.25 },
{ x: 0.56, y: 0.65 },
{ x: 0.78, y: 0.3 },
{ x: 0.92, y: 0.6 },
];
const particles = [
{ from: 0, to: 1, t: 0, speed: 0.006 },
{ from: 1, to: 2, t: 0.4, speed: 0.005 },
{ from: 2, to: 3, t: 0.15, speed: 0.007 },
{ from: 3, to: 4, t: 0.6, speed: 0.005 },
];
const particles: Array<{
x: number;
y: number;
size: number;
speedY: number;
speedX: number;
opacity: number;
fadeSpeed: number;
}> = [];
let raf: number;
const draw = () => {
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = 1.5;
ctx.setLineDash([6, 6]);
ctx.strokeStyle = "rgba(200, 16, 46, 0.16)";
ctx.beginPath();
for (let i = 0; i < nodes.length - 1; i++) {
ctx.moveTo(nodes[i].x * width, nodes[i].y * height);
ctx.lineTo(nodes[i + 1].x * width, nodes[i + 1].y * height);
const createParticle = (isInitial = false) => {
return {
x: Math.random() * width,
y: isInitial ? Math.random() * height : height + 10,
size: Math.random() * 2 + 0.5,
speedY: -(Math.random() * 0.4 + 0.1),
speedX: (Math.random() - 0.5) * 0.2,
opacity: Math.random() * 0.5 + 0.1,
fadeSpeed: Math.random() * 0.005 + 0.002,
};
};
for (let i = 0; i < 40; i++) {
particles.push(createParticle(true));
}
ctx.stroke();
ctx.setLineDash([]);
if (!reduced) {
particles.forEach((p) => {
p.t += p.speed;
if (p.t >= 1) p.t = 0;
const a = nodes[p.from];
const b = nodes[p.to];
const x = a.x * width + (b.x * width - a.x * width) * p.t;
const y = a.y * height + (b.y * height - a.y * height) * p.t;
const animate = () => {
ctx.clearRect(0, 0, width, height);
particles.forEach((p, idx) => {
p.y += p.speedY;
p.x += p.speedX;
p.opacity -= p.fadeSpeed;
if (p.opacity <= 0 || p.y < -10 || p.x < -10 || p.x > width + 10) {
particles[idx] = createParticle(false);
} else {
ctx.beginPath();
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
ctx.fillStyle = "#c8102e";
ctx.shadowColor = "#c8102e";
ctx.shadowBlur = 10;
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 59, 59, ${p.opacity})`;
ctx.shadowBlur = 4;
ctx.shadowColor = "rgba(255, 59, 59, 0.6)";
ctx.fill();
ctx.shadowBlur = 0;
});
}
nodes.forEach((n) => {
const x = n.x * width;
const y = n.y * height;
ctx.beginPath();
ctx.arc(x, y, 7, 0, Math.PI * 2);
ctx.fillStyle = "rgba(200, 16, 46, 0.1)";
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fillStyle = "rgba(255, 255, 255, 0.25)";
ctx.fill();
});
if (!reduced) raf = requestAnimationFrame(draw);
animationFrameId = requestAnimationFrame(animate);
};
draw();
animate();
return () => {
window.removeEventListener("resize", handleResize);
if (raf) cancelAnimationFrame(raf);
cancelAnimationFrame(animationFrameId);
};
}, []);
return <canvas ref={canvasRef} className="wmt-particle-canvas" aria-hidden="true" />;
};
export default function WingsMileTruth() {
const [activeStage, setActiveStage] = useState(0);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
if (isHovered) return;
const interval = setInterval(() => {
setActiveStage((prev) => (prev + 1) % FLOW_STAGES.length);
}, 4000);
return () => clearInterval(interval);
}, [isHovered]);
return (
<>
<style
@@ -273,45 +302,97 @@ export default function WingsMileTruth() {
border-radius: 28px;
overflow: hidden;
box-sizing: border-box;
background: #09090b;
background: #08080a;
padding: clamp(64px, 8vw, 100px) clamp(24px, 5vw, 48px);
border: 1px solid rgba(255, 255, 255, 0.03);
}
.wmt-canvas {
.wmt-ambient-glow {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.12;
}
.wmt-glow {
position: absolute;
width: 480px;
height: 480px;
border-radius: 50%;
pointer-events: none;
filter: blur(140px);
z-index: 0;
opacity: 0.06;
opacity: 0.1;
}
.wmt-glow.tl { left: -8%; top: -12%; background: radial-gradient(circle, #c8102e 0%, transparent 70%); }
.wmt-glow.br { right: -8%; bottom: -12%; background: radial-gradient(circle, #ff3b3b 0%, transparent 70%); }
.wmt-ambient-glow.red-left {
left: -10%;
top: 20%;
width: 480px;
height: 480px;
background: radial-gradient(circle, #ff3b3b 0%, transparent 70%);
}
.wmt-ambient-glow.red-right {
right: -5%;
bottom: 10%;
width: 420px;
height: 420px;
background: radial-gradient(circle, #ff3b3b 0%, transparent 70%);
}
.wmt-inner {
position: relative;
z-index: 1;
max-width: 1380px;
margin: 0 auto;
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 64px;
align-items: center;
}
.wmt-header {
margin: 0 auto 48px auto;
max-width: 720px;
text-align: center;
@media (max-width: 1024px) {
.wmt-inner {
grid-template-columns: 1fr;
gap: 48px;
}
}
.wmt-visual-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.wmt-visual-pane {
position: relative;
width: 100%;
aspect-ratio: 1/1;
max-width: 580px;
border-radius: 24px;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.02) 0%, transparent 80%), rgba(255, 255, 255, 0.01);
border: 1px solid rgba(255, 255, 255, 0.04);
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.05);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.wmt-visual-img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.95;
pointer-events: none;
}
.wmt-particle-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.wmt-content-pane {
display: flex;
flex-direction: column;
justify-content: center;
}
.wmt-eyebrow {
@@ -319,85 +400,262 @@ export default function WingsMileTruth() {
font-weight: 800;
letter-spacing: 2.5px;
text-transform: uppercase;
color: #c8102e;
color: #ff3b3b;
margin-bottom: 16px;
}
.wmt-title {
font-size: clamp(28px, 3.4vw, 48px) !important;
font-size: clamp(32px, 3.8vw, 44px) !important;
font-weight: 800 !important;
letter-spacing: -0.03em !important;
line-height: 1.15 !important;
text-transform: none !important;
color: #ffffff !important;
margin: 0;
margin: 0 0 16px 0;
text-transform: none !important;
}
.wmt-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
margin-bottom: 20px;
.wmt-title span {
color: #ff3b3b;
}
.wmt-card {
min-width: 0;
.wmt-desc {
font-size: clamp(15px, 1.8vw, 16px);
line-height: 1.6;
color: rgba(255, 255, 255, 0.55);
margin: 0 0 40px 0;
max-width: 600px;
}
.wmt-flow-container {
position: relative;
background: rgba(255, 255, 255, 0.035);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 18px;
padding: 26px 22px;
transition: border-color 0.5s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.5s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.5s cubic-bezier(0.16, 1, 0.3, 1);
transform-style: preserve-3d;
will-change: transform;
margin-bottom: 40px;
}
.wmt-card:hover {
border-color: rgba(255, 255, 255, 0.16);
background-color: rgba(255, 255, 255, 0.055);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
.wmt-flow-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: rgba(255, 255, 255, 0.4);
font-weight: 700;
margin-bottom: 16px;
}
.wmt-card-icon {
width: 40px;
height: 40px;
.wmt-flow-pipeline {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
background: rgba(255, 255, 255, 0.015);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 16px 12px;
backdrop-filter: blur(10px);
}
@media (max-width: 640px) {
.wmt-flow-pipeline {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
}
.wmt-flow-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
text-align: center;
z-index: 2;
cursor: pointer;
}
@media (max-width: 640px) {
.wmt-flow-node {
flex-direction: row;
text-align: left;
width: 100%;
}
}
.wmt-flow-icon-wrapper {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background: rgba(200, 16, 46, 0.12);
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.6);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.wmt-flow-node:hover .wmt-flow-icon-wrapper,
.wmt-flow-node.active .wmt-flow-icon-wrapper {
background: rgba(255, 59, 59, 0.12);
border-color: rgba(255, 59, 59, 0.5);
color: #ff3b3b;
box-shadow: 0 0 15px rgba(255, 59, 59, 0.25);
transform: translateY(-2px);
}
.wmt-flow-label {
font-size: 11px;
font-weight: 700;
color: rgba(255, 255, 255, 0.5);
transition: color 0.3s ease;
}
.wmt-flow-node:hover .wmt-flow-label,
.wmt-flow-node.active .wmt-flow-label {
color: #ffffff;
margin-bottom: 18px;
}
.wmt-card-title {
font-size: 15.5px !important;
font-weight: 700 !important;
line-height: 1.3 !important;
text-transform: none !important;
color: #ffffff !important;
margin: 0 0 8px 0;
.wmt-flow-connector {
position: relative;
flex: 1;
height: 1px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
max-width: 60px;
}
.wmt-card-desc {
font-size: 16px;
line-height: 1.55;
color: rgba(255, 255, 255, 0.55);
@media (max-width: 640px) {
.wmt-flow-connector {
display: none;
}
}
.wmt-flow-connector.active {
background: rgba(255, 59, 59, 0.2);
}
.wmt-flow-connector::after {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, #ff3b3b, transparent);
animation: laser-flow 3s infinite linear;
}
.wmt-flow-connector.active::after {
animation-duration: 1.8s;
}
@keyframes laser-flow {
0% { left: -100%; }
100% { left: 100%; }
}
.wmt-flow-details {
margin-top: 14px;
padding: 12px 18px;
background: rgba(255, 59, 59, 0.015);
border: 1px solid rgba(255, 59, 59, 0.08);
border-radius: 12px;
min-height: 48px;
display: flex;
align-items: center;
backdrop-filter: blur(4px);
}
.wmt-flow-details-text {
font-size: 12.5px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.65);
margin: 0;
}
.wmt-chips-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: rgba(255, 255, 255, 0.4);
font-weight: 700;
margin-bottom: 16px;
}
.wmt-chips-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
@media (max-width: 500px) {
.wmt-chips-grid {
grid-template-columns: 1fr;
}
}
.wmt-chip {
display: flex;
align-items: center;
gap: 12px;
background: rgba(255, 255, 255, 0.015);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 12px 16px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
backdrop-filter: blur(8px);
}
.wmt-chip:hover {
background: rgba(255, 255, 255, 0.035);
border-color: rgba(255, 59, 59, 0.3);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4), 0 0 10px rgba(255, 59, 59, 0.04);
transform: translateY(-1px);
}
.wmt-chip-icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.6);
transition: all 0.3s ease;
}
.wmt-chip:hover .wmt-chip-icon-container {
background: rgba(255, 59, 59, 0.1);
border-color: rgba(255, 59, 59, 0.35);
color: #ff3b3b;
}
.wmt-chip-text {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
transition: color 0.3s ease;
}
.wmt-chip:hover .wmt-chip-text {
color: #ffffff;
}
.wmt-callout {
position: relative;
border-radius: 20px;
padding: 34px 32px;
padding: 30px 32px;
background: linear-gradient(135deg, rgba(200, 16, 46, 0.95) 0%, rgba(110, 8, 26, 0.98) 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.12);
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
overflow: hidden;
margin-top: 56px;
}
.wmt-callout::after {
@@ -405,7 +663,7 @@ export default function WingsMileTruth() {
position: absolute;
inset: -1.5px;
border-radius: 20px;
background: linear-gradient(90deg, #c8102e, #ff3366, #ff0033, #c8102e);
background: linear-gradient(90deg, #c8102e, #ff3b3b, #ff3b3b, #c8102e);
background-size: 300% 100%;
animation: wmt-sweep 6s linear infinite;
z-index: -1;
@@ -421,7 +679,7 @@ export default function WingsMileTruth() {
.wmt-callout-body { max-width: 640px; }
.wmt-callout-title {
font-size: 20px !important;
font-size: 19px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
text-transform: none !important;
@@ -430,7 +688,7 @@ export default function WingsMileTruth() {
}
.wmt-callout-desc {
font-size: 14px;
font-size: 13.5px;
line-height: 1.55;
color: rgba(255, 255, 255, 0.85);
margin: 0;
@@ -438,7 +696,7 @@ export default function WingsMileTruth() {
.wmt-callout-badge {
flex-shrink: 0;
font-size: 11.5px;
font-size: 11px;
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
@@ -449,18 +707,8 @@ export default function WingsMileTruth() {
padding: 8px 16px;
}
@media (max-width: 1024px) {
.wmt-section { width: calc(100% - 24px); border-radius: 22px; }
.wmt-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 600px) {
.wmt-grid { grid-template-columns: 1fr; }
.wmt-callout { flex-direction: column; align-items: flex-start; }
/* The vertical clamp() floors at a fixed 64px on any phone width;
trimmed to a real mobile value to match the section rhythm near
the footer. Horizontal padding is left untouched (its own
clamp already resolves small on phones). */
.wmt-callout { flex-direction: column; align-items: flex-start; margin-top: 40px; }
.wmt-section { padding-top: 48px; padding-bottom: 48px; }
}
`,
@@ -468,27 +716,78 @@ export default function WingsMileTruth() {
/>
<section className="wmt-section" id="wings-miletruth">
<canvas ref={canvasRef} className="wmt-canvas" aria-hidden="true" />
<div className="wmt-glow tl" aria-hidden="true" />
<div className="wmt-glow br" aria-hidden="true" />
<div className="wmt-ambient-glow red-left" aria-hidden="true" />
<div className="wmt-ambient-glow red-right" aria-hidden="true" />
<div className="wmt-inner dm-section-container">
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wmt-header">
<div className="wmt-eyebrow">/ MileTruth&trade; AI /</div>
<h2 className="wmt-title">The intelligence layer behind every shipment</h2>
<ScrollReveal delay={0.05} duration={0.8} yOffset={20} className="wmt-visual-container">
<TiltCard className="wmt-visual-pane">
<img
src="/images/miletruth-ai-core.png"
alt="MileTruth AI Core Engine Visualization"
className="wmt-visual-img"
/>
<ParticleCanvas />
</TiltCard>
</ScrollReveal>
<StaggerChildren stagger={0.08} duration={0.65} yOffset={25} className="wmt-grid">
{CAPABILITIES.map((c) => (
<TiltCard key={c.title} className="wmt-card">
<div className="wmt-card-icon" aria-hidden="true">{c.icon}</div>
<h3 className="wmt-card-title">{c.title}</h3>
<p className="wmt-card-desc">{c.desc}</p>
</TiltCard>
))}
</StaggerChildren>
<div className="wmt-content-pane">
<ScrollReveal delay={0.1} duration={0.8} yOffset={20}>
<div className="wmt-eyebrow">/ MileTruth&trade; AI /</div>
<h2 className="wmt-title">MILETRUTH <span>AI</span></h2>
<p className="wmt-desc">
MileTruth continuously analyzes every completed shipment, turning raw operational data into recommendations that make next mile faster, cheaper and more reliable.
</p>
</ScrollReveal>
<ScrollReveal delay={0.15} duration={0.8} yOffset={25}>
<ScrollReveal delay={0.15} duration={0.8} yOffset={20} className="wmt-flow-container">
<div className="wmt-flow-title">Data Pipeline Flow</div>
<div
className="wmt-flow-pipeline"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{FLOW_STAGES.map((stage, idx) => (
<div key={stage.id} style={{ display: 'contents' }}>
<div
className={`wmt-flow-node ${activeStage === idx ? 'active' : ''}`}
onMouseEnter={() => setActiveStage(idx)}
>
<div className="wmt-flow-icon-wrapper">
{stage.icon}
</div>
<div className="wmt-flow-label">{stage.label}</div>
</div>
{idx < FLOW_STAGES.length - 1 && (
<div className={`wmt-flow-connector ${activeStage >= idx ? 'active' : ''}`} />
)}
</div>
))}
</div>
<div className="wmt-flow-details">
<p className="wmt-flow-details-text">
{FLOW_STAGES[activeStage].description}
</p>
</div>
</ScrollReveal>
<ScrollReveal delay={0.2} duration={0.8} yOffset={20}>
<div className="wmt-chips-title">Engine Capabilities</div>
<div className="wmt-chips-grid">
{CAPABILITIES.map((c) => (
<div key={c.title} className="wmt-chip">
<div className="wmt-chip-icon-container">
{c.icon}
</div>
<span className="wmt-chip-text">{c.title}</span>
</div>
))}
</div>
</ScrollReveal>
</div>
</div>
<ScrollReveal delay={0.25} duration={0.8} yOffset={20}>
<div className="wmt-callout">
<div className="wmt-callout-body">
<h3 className="wmt-callout-title">Digital Twin of the Logistics Network</h3>
@@ -500,7 +799,6 @@ export default function WingsMileTruth() {
<span className="wmt-callout-badge">SaaS Ready</span>
</div>
</ScrollReveal>
</div>
</section>
</>
);

View File

@@ -90,7 +90,7 @@ export default function Workflow2() {
ariaLabel="Workflow 2 — Innovation"
gapBottom
bannerImage="/images/mid-mile-approach.webp"
cardTitle="CHOOSE THE BEST PLAN"
cardTitle="BEST PLAN"
cardSubtitle="Analyze thousands of route possibilities and automatically select the most efficient routing strategy."
eyebrow="/ Innovation /"
titleLead="MANY STRATEGIES. "

View File

@@ -22,6 +22,9 @@ const ROUTE_CLASSES: Record<string, string> = {
"wp-singular page-template-default page page-id-59 wp-theme-logico wp-child-theme-logico-child theme-logico ehf-header ehf-footer ehf-template-logico ehf-stylesheet-logico-child logico-front-end logico-theme-style-rounded elementor-default elementor-page elementor-page-59",
"/miletruth": `${SHARED} page-id-59 elementor-page-59`,
"/solutions": `${SHARED} page-id-59 elementor-page-59`,
// Industries carries the original Solutions page's Elementor context — it is
// the same migrated markup (solutions hero + industry cards), just re-routed.
"/industries": `${SHARED} page-id-59 elementor-page-59`,
// PHP source quirk: about-us.php has `home` class (upstream bug, preserved)
"/about-us": `home ${SHARED} page-id-59 elementor-page-59`,
// Reuse the About page Elementor/runtime context for the Wings Empowerment