update miletruth page
This commit is contained in:
24
probe.mjs
Normal file
24
probe.mjs
Normal 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();
|
||||||
BIN
public/images/miletruth-ai-core.png
Normal file
BIN
public/images/miletruth-ai-core.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 489 KiB |
49
shot-miletruth.mjs
Normal file
49
shot-miletruth.mjs
Normal 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();
|
||||||
|
}
|
||||||
35
src/app/industries/page.tsx
Normal file
35
src/app/industries/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,14 @@ import Workflow1 from "../../components/sections/Workflow1";
|
|||||||
import Workflow2 from "../../components/sections/Workflow2";
|
import Workflow2 from "../../components/sections/Workflow2";
|
||||||
import Workflow4 from "../../components/sections/Workflow4";
|
import Workflow4 from "../../components/sections/Workflow4";
|
||||||
import Workflow3Lazy from "../../components/sections/Workflow3Lazy";
|
import Workflow3Lazy from "../../components/sections/Workflow3Lazy";
|
||||||
|
import {
|
||||||
|
SolutionAIVehicleSelection,
|
||||||
|
SolutionSmartRouteIntelligence,
|
||||||
|
SolutionQuantumDecisionEngine,
|
||||||
|
SolutionLiveLogisticsIntelligence,
|
||||||
|
SolutionMileTruthAI,
|
||||||
|
SolutionOperationsDashboard,
|
||||||
|
} from "../../components/sections/SolutionsWorkflow";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "MileTruth – Doormile",
|
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">
|
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
||||||
<MileTruthHero />
|
<MileTruthHero />
|
||||||
<MileTruthIntro />
|
<MileTruthIntro />
|
||||||
|
<SolutionAIVehicleSelection />
|
||||||
<Workflow1 />
|
<Workflow1 />
|
||||||
|
<SolutionSmartRouteIntelligence />
|
||||||
<Workflow2 />
|
<Workflow2 />
|
||||||
|
<SolutionQuantumDecisionEngine />
|
||||||
<Workflow4 />
|
<Workflow4 />
|
||||||
|
<SolutionLiveLogisticsIntelligence />
|
||||||
|
<SolutionMileTruthAI />
|
||||||
|
<SolutionOperationsDashboard />
|
||||||
<Workflow3Lazy />
|
<Workflow3Lazy />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
"/contact",
|
"/contact",
|
||||||
"/blog",
|
"/blog",
|
||||||
"/air-freight",
|
"/air-freight",
|
||||||
"/first-mile-delivery",
|
"/solutions",
|
||||||
"/mid-mile-delivery",
|
"/industries",
|
||||||
"/last-mile-delivery",
|
|
||||||
"/ev-logistics",
|
|
||||||
"/strategy-optimization",
|
|
||||||
"/how-it-works",
|
"/how-it-works",
|
||||||
].map((route) => ({
|
].map((route) => ({
|
||||||
url: `${SITE_URL}${route}`,
|
url: `${SITE_URL}${route}`,
|
||||||
|
|||||||
@@ -1,65 +1,119 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import SolutionsHero from "@/components/sections/SolutionsHero";
|
import SolutionsHero, { type HeroSlide } from "@/components/sections/SolutionsHero";
|
||||||
import SolutionsWorkflow from "@/components/sections/SolutionsWorkflow";
|
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 = {
|
export const metadata = {
|
||||||
title: "Solutions – Doormile",
|
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() {
|
export default function SolutionsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div id="solutions" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||||
<style dangerouslySetInnerHTML={{ __html: `
|
<div className="content">
|
||||||
/* Fix "Why Businesses Choose Doormile" section — anchor absolute image left, push content right */
|
<div className="content-inner">
|
||||||
.elementor-element.elementor-element-ead59d3 {
|
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
||||||
position: relative;
|
<SolutionsHero slides={SOLUTIONS_SLIDES} />
|
||||||
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">
|
{/* One #sol-root scope for the whole page — the shell carries the
|
||||||
<div className="content">
|
stylesheet and must not be rendered twice. Miles3 and EVSection
|
||||||
<div className="content-inner">
|
sit between beats; the shell's CSS is scoped to `.sol-*` only,
|
||||||
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
so their Elementor markup is unaffected. */}
|
||||||
<SolutionsHero />
|
<SolutionsSectionsShell>
|
||||||
<SolutionsWorkflow />
|
<SolOfferings />
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ export default function Footer() {
|
|||||||
<Link href="/how-it-works">How It Works</Link>
|
<Link href="/how-it-works">How It Works</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link href="/solutions">Use Cases</Link>
|
<Link href="/industries">Use Cases</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link href="/contact">Request Demo</Link>
|
<Link href="/contact">Request Demo</Link>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const CURRENT_PAGE_MAP: Record<string, string> = {
|
|||||||
"/how-it-works": "how-it-works",
|
"/how-it-works": "how-it-works",
|
||||||
"/miletruth": "miletruth",
|
"/miletruth": "miletruth",
|
||||||
"/solutions": "solutions",
|
"/solutions": "solutions",
|
||||||
|
"/industries": "industries",
|
||||||
"/about": "about",
|
"/about": "about",
|
||||||
"/doormile-wings": "doormile-wings",
|
"/doormile-wings": "doormile-wings",
|
||||||
"/empowerment": "entarpreneurship",
|
"/empowerment": "entarpreneurship",
|
||||||
@@ -38,6 +39,8 @@ const CURRENT_PAGE_ALIASES: Record<string, string[]> = {
|
|||||||
miletruth: ["miletruth"],
|
miletruth: ["miletruth"],
|
||||||
// Solutions is now a plain top-level link — no child pages own its active state.
|
// Solutions is now a plain top-level link — no child pages own its active state.
|
||||||
solutions: ["solutions"],
|
solutions: ["solutions"],
|
||||||
|
// Industries is the sibling top-level link — "who Doormile serves".
|
||||||
|
industries: ["industries"],
|
||||||
"doormile-wings": ["doormile-wings"],
|
"doormile-wings": ["doormile-wings"],
|
||||||
entarpreneurship: ["entarpreneurship"],
|
entarpreneurship: ["entarpreneurship"],
|
||||||
blogs: ["blogs"],
|
blogs: ["blogs"],
|
||||||
@@ -45,8 +48,8 @@ const CURRENT_PAGE_ALIASES: Record<string, string[]> = {
|
|||||||
"privacy-policy": ["privacy-policy"],
|
"privacy-policy": ["privacy-policy"],
|
||||||
"terms-of-service": ["terms-of-service"],
|
"terms-of-service": ["terms-of-service"],
|
||||||
"cookie-policy": ["cookie-policy"],
|
"cookie-policy": ["cookie-policy"],
|
||||||
// Company is the dropdown parent for About + Blogs + How It Works — active if any is.
|
// Company is the dropdown parent for About + Industries + How It Works + Blogs — active if any is.
|
||||||
company: ["blogs", "how-it-works", "about"],
|
company: ["about", "industries", "how-it-works", "blogs"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
@@ -524,15 +527,13 @@ export default function Header() {
|
|||||||
About
|
About
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
|
<li className={currentPage === "industries" ? "dm-child-active" : ""}>
|
||||||
<Link
|
<Link
|
||||||
href="/blog#blogs"
|
href="/industries"
|
||||||
onClick={(event) => {
|
prefetch
|
||||||
setDesktopCompanyOpen(false);
|
onClick={() => setDesktopCompanyOpen(false)}
|
||||||
handleNavClick(event, "/blog", "blogs");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Blogs
|
Industries
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className={currentPage === "how-it-works" ? "dm-child-active" : ""}>
|
<li className={currentPage === "how-it-works" ? "dm-child-active" : ""}>
|
||||||
@@ -546,6 +547,17 @@ export default function Header() {
|
|||||||
How It Works
|
How It Works
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
|
||||||
|
<Link
|
||||||
|
href="/blog#blogs"
|
||||||
|
onClick={(event) => {
|
||||||
|
setDesktopCompanyOpen(false);
|
||||||
|
handleNavClick(event, "/blog", "blogs");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Blogs
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -621,12 +633,15 @@ export default function Header() {
|
|||||||
<li className={currentPage === "about" ? "dm-child-active" : ""}>
|
<li className={currentPage === "about" ? "dm-child-active" : ""}>
|
||||||
<Link href="/about-us#about" onClick={(event) => handleNavClick(event, "/about-us", "about", true)}>About</Link>
|
<Link href="/about-us#about" onClick={(event) => handleNavClick(event, "/about-us", "about", true)}>About</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
|
<li className={currentPage === "industries" ? "dm-child-active" : ""}>
|
||||||
<Link href="/blog#blogs" onClick={(event) => handleNavClick(event, "/blog", "blogs", true)}>Blogs</Link>
|
<Link href="/industries" prefetch onClick={closeMobileMenu}>Industries</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className={currentPage === "how-it-works" ? "dm-child-active" : ""}>
|
<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>
|
<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>
|
||||||
|
<li className={currentPage === "blogs" ? "dm-child-active" : ""}>
|
||||||
|
<Link href="/blog#blogs" onClick={(event) => handleNavClick(event, "/blog", "blogs", true)}>Blogs</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
289
src/components/sections/ConnectedLogisticsSection.tsx
Normal file
289
src/components/sections/ConnectedLogisticsSection.tsx
Normal 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="{"background_background":"classic"}">
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -368,6 +368,7 @@ export default function EVSection({
|
|||||||
}: EVSectionProps) {
|
}: EVSectionProps) {
|
||||||
const bannerRef = useRef<HTMLDivElement>(null);
|
const bannerRef = useRef<HTMLDivElement>(null);
|
||||||
const useDashboard = !!metrics && metrics.length > 0;
|
const useDashboard = !!metrics && metrics.length > 0;
|
||||||
|
const bottomBarItems = useDashboard ? barStats : stats;
|
||||||
const isVideo = image.endsWith(".mp4");
|
const isVideo = image.endsWith(".mp4");
|
||||||
|
|
||||||
/* In dashboard mode the workflow's accent theme is applied at the .evnd
|
/* In dashboard mode the workflow's accent theme is applied at the .evnd
|
||||||
@@ -1488,27 +1489,25 @@ export default function EVSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BOTTOM BAR — full-width KPI summary strip below the workflow
|
{bottomBarItems && (
|
||||||
content (Scene → Insight/Decision card → KPI bar). Required across
|
<div className="evnd__bar">
|
||||||
all breakpoints. In dashboard mode it summarises the same workflow
|
{bottomBarItems.map((s) => (
|
||||||
metrics; the homepage section uses its own stat set. */}
|
<div className="evnd__bar-item" key={s.label}>
|
||||||
<div className="evnd__bar">
|
<span className="evnd__bar-label">{s.label}</span>
|
||||||
{(useDashboard ? (barStats ?? metrics!) : stats).map((s) => (
|
{s.text ? (
|
||||||
<div className="evnd__bar-item" key={s.label}>
|
<b className="evnd__bar-val evnd__bar-val--text">{s.text}</b>
|
||||||
<span className="evnd__bar-label">{s.label}</span>
|
) : (
|
||||||
{s.text ? (
|
<CountUp
|
||||||
<b className="evnd__bar-val evnd__bar-val--text">{s.text}</b>
|
value={s.value}
|
||||||
) : (
|
decimals={s.decimals}
|
||||||
<CountUp
|
suffix={s.suffix}
|
||||||
value={s.value}
|
className="evnd__bar-val"
|
||||||
decimals={s.decimals}
|
/>
|
||||||
suffix={s.suffix}
|
)}
|
||||||
className="evnd__bar-val"
|
</div>
|
||||||
/>
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function IndustrySolutions() {
|
|||||||
<div className="industry-solutions-grid">
|
<div className="industry-solutions-grid">
|
||||||
|
|
||||||
{/* Card 1: FMCG */}
|
{/* Card 1: FMCG */}
|
||||||
<Link href="/solutions" className="industry-card-link">
|
<Link href="/industries" className="industry-card-link">
|
||||||
<div className="industry-card-bg">
|
<div className="industry-card-bg">
|
||||||
<Image
|
<Image
|
||||||
src="/images/tab-pic-1.webp"
|
src="/images/tab-pic-1.webp"
|
||||||
@@ -217,7 +217,7 @@ export default function IndustrySolutions() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Card 2: Pharmaceutical */}
|
{/* Card 2: Pharmaceutical */}
|
||||||
<Link href="/solutions" className="industry-card-link">
|
<Link href="/industries" className="industry-card-link">
|
||||||
<div className="industry-card-bg">
|
<div className="industry-card-bg">
|
||||||
<Image
|
<Image
|
||||||
src="/images/tab-pic-2.webp"
|
src="/images/tab-pic-2.webp"
|
||||||
@@ -281,7 +281,7 @@ export default function IndustrySolutions() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Card 3: Enterprise & B2B */}
|
{/* Card 3: Enterprise & B2B */}
|
||||||
<Link href="/solutions" className="industry-card-link">
|
<Link href="/industries" className="industry-card-link">
|
||||||
<div className="industry-card-bg">
|
<div className="industry-card-bg">
|
||||||
<Image
|
<Image
|
||||||
src="/images/tab-pic-3.webp"
|
src="/images/tab-pic-3.webp"
|
||||||
|
|||||||
@@ -76,6 +76,46 @@ const SECTIONS: Section[] = [
|
|||||||
"SLA monitoring dashboard tracks commitments and flags risk in real time.",
|
"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 }) {
|
function Card({ sec }: { sec: Section }) {
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useMemo, useRef } from "react";
|
import React, { useMemo, useRef } from "react";
|
||||||
import { Canvas, useFrame } from "@react-three/fiber";
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||||
import { Sparkles } from "@react-three/drei";
|
import { Html } from "@react-three/drei";
|
||||||
import { EffectComposer, Bloom } from "@react-three/postprocessing";
|
import { EffectComposer, Bloom } from "@react-three/postprocessing";
|
||||||
import { KernelSize } from "postprocessing";
|
import { KernelSize } from "postprocessing";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
MileTruthCanvas — small, self-contained R3F scene for the
|
MileTruthCanvas — the "MileTruth AI" learning loop.
|
||||||
"MileTruth AI" section: a 4-node data → AI → recommendations →
|
|
||||||
improvement network with glowing cores and travelling light
|
A CLOSED cycle, not a line: shipment data enters, the AI core
|
||||||
packets along each connector. Deliberately lightweight (a
|
reads it, a recommendation is issued, the result is measured,
|
||||||
handful of meshes + a low-count sparkle field) — this and the
|
and that result feeds the next shipment. Packets travel the
|
||||||
Quantum section are the ONLY two places on the Solutions page
|
whole ring and visibly return to where they started — the
|
||||||
that touch Three.js.
|
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";
|
const RED = "#C01227";
|
||||||
@@ -22,110 +27,304 @@ const RED_BRIGHT = "#E2354A";
|
|||||||
const STEEL = "#8b8b96";
|
const STEEL = "#8b8b96";
|
||||||
const WHITE = "#f4f4f6";
|
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[] = [
|
/** Packets. Offsets are deliberately uneven so one isn't parked on
|
||||||
{ pos: [-4.6, 0.3, 0], color: STEEL, label: "data", scale: 0.34 },
|
* a station at all times. SPEED ~= one full lap every 12s. */
|
||||||
{ pos: [-1.6, 0.75, 0.4], color: RED, label: "ai", scale: 0.46 },
|
const PACKET_OFFSETS = [0, 0.17, 0.34, 0.62, 0.81];
|
||||||
{ pos: [1.5, 0.25, -0.35], color: RED_BRIGHT, label: "recs", scale: 0.38 },
|
const SPEED = 0.085;
|
||||||
{ pos: [4.5, -0.15, 0.15], color: WHITE, label: "improve", scale: 0.34 },
|
|
||||||
|
/** Where the AI core sits on the loop (station 02). */
|
||||||
|
const INGEST_U = 0.25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LINKS: [number, number][] = [
|
/* ---------- the loop itself ---------- */
|
||||||
[0, 1],
|
|
||||||
[1, 2],
|
|
||||||
[2, 3],
|
|
||||||
];
|
|
||||||
|
|
||||||
function NodeMesh({ node, reduced, seed }: { node: Node; reduced: boolean; seed: number }) {
|
function LoopPath() {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const geo = useMemo(() => {
|
||||||
const haloRef = useRef<THREE.Mesh>(null);
|
const pts: THREE.Vector3[] = [];
|
||||||
|
for (let i = 0; i < 96; i++) pts.push(pointAt(i / 96));
|
||||||
useFrame((state) => {
|
const curve = new THREE.CatmullRomCurve3(pts, true, "centripetal");
|
||||||
const t = state.clock.elapsedTime;
|
// A tube (not lineBasicMaterial) so the path actually catches light
|
||||||
const pulse = reduced ? 1 : 1 + Math.sin(t * 1.6 + seed) * 0.08;
|
// and bloom — thin lines washed out to nothing in the old scene.
|
||||||
if (meshRef.current) meshRef.current.scale.setScalar(node.scale * pulse);
|
return new THREE.TubeGeometry(curve, 220, 0.016, 8, true);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={node.pos}>
|
<mesh geometry={geo}>
|
||||||
<mesh ref={haloRef}>
|
<meshBasicMaterial color={RED} transparent opacity={0.55} depthWrite={false} />
|
||||||
<sphereGeometry args={[1, 20, 20]} />
|
</mesh>
|
||||||
<meshBasicMaterial color={node.color} transparent opacity={0.16} depthWrite={false} />
|
|
||||||
</mesh>
|
|
||||||
<mesh ref={meshRef}>
|
|
||||||
<sphereGeometry args={[1, 28, 28]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
color={node.color}
|
|
||||||
emissive={node.color}
|
|
||||||
emissiveIntensity={1.4}
|
|
||||||
roughness={0.35}
|
|
||||||
metalness={0.1}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Connector({ a, b, reduced, index }: { a: Node; b: Node; reduced: boolean; index: number }) {
|
/* ---------- station artwork ---------- */
|
||||||
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]);
|
|
||||||
|
|
||||||
const geo = useMemo(() => {
|
/** Each station gets a distinct silhouette, so the four steps stay
|
||||||
const g = new THREE.BufferGeometry();
|
* tellable apart even before you read their labels. */
|
||||||
g.setAttribute("position", new THREE.BufferAttribute(new Float32Array([...a.pos, ...b.pos]), 3));
|
function StationArt({ index, color }: { index: number; color: string }) {
|
||||||
return g;
|
const mat = (
|
||||||
}, [a, b]);
|
<meshStandardMaterial
|
||||||
|
color={color}
|
||||||
|
emissive={color}
|
||||||
|
emissiveIntensity={0.9}
|
||||||
|
roughness={0.35}
|
||||||
|
metalness={0.1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
useFrame((state) => {
|
if (index === 0) {
|
||||||
const t = state.clock.elapsedTime;
|
// Shipment data — a stack of parcels.
|
||||||
if (lineMatRef.current) {
|
return (
|
||||||
lineMatRef.current.opacity = reduced ? 0.3 : 0.2 + Math.sin(t * 1.2 + index) * 0.12;
|
<group>
|
||||||
}
|
<mesh position={[-0.13, 0.07, 0]}>
|
||||||
if (packetRef.current) {
|
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||||
if (reduced) {
|
{mat}
|
||||||
packetRef.current.visible = false;
|
</mesh>
|
||||||
return;
|
<mesh position={[0.12, 0.05, 0.05]} rotation={[0, 0.5, 0]}>
|
||||||
}
|
<boxGeometry args={[0.16, 0.16, 0.16]} />
|
||||||
packetRef.current.visible = true;
|
{mat}
|
||||||
const speed = 0.28;
|
</mesh>
|
||||||
const u = (t * speed + index * 0.33) % 1;
|
<mesh position={[0, 0.26, -0.04]} rotation={[0, 0.25, 0]}>
|
||||||
packetRef.current.position.lerpVectors(start, end, u);
|
<boxGeometry args={[0.15, 0.15, 0.15]} />
|
||||||
const mat = packetRef.current.material as THREE.MeshBasicMaterial;
|
{mat}
|
||||||
mat.opacity = Math.sin(u * Math.PI);
|
</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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<lineSegments geometry={geo}>
|
{[0.14, 0.24, 0.36].map((h, i) => (
|
||||||
<lineBasicMaterial ref={lineMatRef} color={RED_BRIGHT} transparent opacity={0.25} depthWrite={false} />
|
<mesh key={h} position={[(i - 1) * 0.16, h / 2, 0]}>
|
||||||
</lineSegments>
|
<boxGeometry args={[0.1, h, 0.1]} />
|
||||||
<mesh ref={packetRef}>
|
{mat}
|
||||||
<sphereGeometry args={[0.09, 12, 12]} />
|
</mesh>
|
||||||
<meshBasicMaterial color="#ffffff" transparent opacity={0.9} depthWrite={false} />
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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 = 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 (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 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>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SlowSpin({ reduced, children }: { reduced: boolean; children: React.ReactNode }) {
|
/* ---------- packets travelling the loop ---------- */
|
||||||
const group = useRef<THREE.Group>(null);
|
|
||||||
useFrame((_state, dt) => {
|
function Packet({ offset, reduced }: { offset: number; reduced: boolean }) {
|
||||||
if (reduced || !group.current) return;
|
const ref = useRef<THREE.Group>(null);
|
||||||
group.current.rotation.y += dt * 0.035;
|
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({
|
export default function MileTruthCanvas({
|
||||||
reduced = false,
|
reduced = false,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
@@ -135,29 +334,42 @@ export default function MileTruthCanvas({
|
|||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const packets = isMobile ? PACKET_OFFSETS.slice(0, 3) : PACKET_OFFSETS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
flat
|
flat
|
||||||
frameloop={active ? "always" : "never"}
|
frameloop={active ? "always" : "never"}
|
||||||
dpr={[1, isMobile || reduced ? 1.2 : 1.6]}
|
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 }}
|
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
|
||||||
>
|
>
|
||||||
<color attach="background" args={["#0c0c10"]} />
|
<color attach="background" args={["#0c0c10"]} />
|
||||||
<ambientLight intensity={0.35} />
|
<ambientLight intensity={0.75} />
|
||||||
<SlowSpin reduced={reduced}>
|
<directionalLight position={[2.5, 4, 4]} intensity={1.1} />
|
||||||
{LINKS.map(([a, b], i) => (
|
|
||||||
<Connector key={i} a={NODES[a]} b={NODES[b]} reduced={reduced} index={i} />
|
{/* One fixed tilt on the whole loop. Nothing spins the group —
|
||||||
))}
|
the reading order has to hold still to stay legible. */}
|
||||||
{NODES.map((n, i) => (
|
<FitLoop>
|
||||||
<NodeMesh key={n.label} node={n} reduced={reduced} seed={i * 1.75} />
|
<group rotation={[TILT, 0, 0]}>
|
||||||
))}
|
{/* Faint disc inside the ring — grounds the ellipse as a plane so
|
||||||
</SlowSpin>
|
the loop reads as an orbit rather than a floating outline. */}
|
||||||
{!isMobile && (
|
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
<Sparkles count={70} scale={[11, 4.5, 6]} size={1.6} speed={reduced ? 0 : 0.15} color={RED_BRIGHT} opacity={0.3} />
|
<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} />
|
||||||
|
))}
|
||||||
|
{packets.map((off) => (
|
||||||
|
<Packet key={off} offset={off} reduced={reduced} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
</FitLoop>
|
||||||
|
|
||||||
<EffectComposer enabled={!isMobile} multisampling={isMobile ? 0 : 2}>
|
<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>
|
</EffectComposer>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,223 +1,49 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import React from "react";
|
||||||
import gsap from "gsap";
|
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
const MILETRUTH_METRICS = [
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
{
|
||||||
}
|
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">
|
||||||
/* A small route graph: three loose columns of candidate nodes converging on
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||||
* one destination. Coordinates are hand-placed (not a strict grid) so the
|
</svg>
|
||||||
* mesh reads as organic route options rather than a wiring diagram. */
|
),
|
||||||
const NODES = [
|
value: "100+",
|
||||||
{ x: 40, y: 50 },
|
label: "parallel route permutations evaluated per order",
|
||||||
{ x: 40, y: 130 },
|
},
|
||||||
{ x: 40, y: 210 },
|
{
|
||||||
{ x: 150, y: 30 },
|
icon: (
|
||||||
{ x: 150, y: 95 },
|
<svg className="mti-icon" width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
||||||
{ x: 150, y: 160 },
|
<circle cx="12" cy="12" r="10" />
|
||||||
{ x: 150, y: 225 },
|
<polyline points="12 6 12 12 16 14" />
|
||||||
{ x: 270, y: 55 },
|
</svg>
|
||||||
{ x: 270, y: 130 },
|
),
|
||||||
{ x: 270, y: 200 },
|
value: "38ms",
|
||||||
{ x: 400, y: 130 },
|
label: "decision latency before instant auto-dispatch",
|
||||||
] as const;
|
},
|
||||||
|
{
|
||||||
const FINAL_NODE = 10;
|
icon: (
|
||||||
const DIM_NODES = [0, 2, 3, 5, 6, 7, 9];
|
<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" />
|
||||||
const ALL_EDGES: [number, number][] = [
|
<polyline points="9 12 11 14 15 10" />
|
||||||
[0, 3], [0, 4], [1, 4], [1, 5], [2, 5], [2, 6],
|
</svg>
|
||||||
[3, 7], [4, 7], [4, 8], [5, 8], [5, 9], [6, 9],
|
),
|
||||||
[7, 10], [8, 10], [9, 10],
|
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",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
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" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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() {
|
export default function MileTruthIntro() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -226,157 +52,130 @@ export default function MileTruthIntro() {
|
|||||||
__html: `
|
__html: `
|
||||||
.mti-shell {
|
.mti-shell {
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
margin: 40px 20px 0;
|
margin: 40px 20px clamp(36px, 4vw, 56px);
|
||||||
}
|
}
|
||||||
.mti {
|
.mti {
|
||||||
background: #fafafa;
|
background: #ffffff;
|
||||||
padding: clamp(28px, 3.5vw, 40px) 0;
|
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 {
|
.mti__inner {
|
||||||
max-width: 1280px;
|
max-width: 1240px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 48px;
|
|
||||||
}
|
}
|
||||||
.mti__row {
|
.mti__head {
|
||||||
display: flex;
|
max-width: 860px;
|
||||||
align-items: center;
|
margin-bottom: clamp(36px, 4.5vw, 52px);
|
||||||
gap: clamp(32px, 5vw, 64px);
|
|
||||||
}
|
|
||||||
.mti__left {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
.mti__eyebrow {
|
.mti__eyebrow {
|
||||||
display: inline-flex;
|
font-size: 13.5px;
|
||||||
align-items: center;
|
font-weight: 600;
|
||||||
gap: 12px;
|
letter-spacing: 0.08em;
|
||||||
color: #C01227 !important;
|
color: #6b7280;
|
||||||
font-weight: 800;
|
margin-bottom: 14px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.16em;
|
display: block;
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: clamp(14px, 1.8vw, 18px);
|
|
||||||
}
|
|
||||||
.mti__eyebrow::before {
|
|
||||||
content: '';
|
|
||||||
width: 16px;
|
|
||||||
height: 2px;
|
|
||||||
background: #C01227;
|
|
||||||
}
|
}
|
||||||
.mti__divider {
|
.mti__divider {
|
||||||
border: 0;
|
border: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: rgba(17, 17, 17, 0.12);
|
background: rgba(0, 0, 0, 0.12);
|
||||||
margin: 0 0 clamp(20px, 2.4vw, 28px);
|
margin: 0 0 clamp(20px, 2.4vw, 28px);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.mti__heading {
|
.mti__heading {
|
||||||
margin: 0 0 clamp(18px, 2vw, 24px);
|
margin: 0 0 clamp(16px, 2vw, 24px);
|
||||||
color: #111111 !important;
|
color: #111827 !important;
|
||||||
font-weight: 800 !important;
|
font-weight: 800 !important;
|
||||||
font-size: clamp(28px, 3.4vw, 46px) !important;
|
font-size: clamp(32px, 4.2vw, 56px) !important;
|
||||||
line-height: 1.15 !important;
|
line-height: 1.12 !important;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.025em;
|
||||||
text-transform: uppercase;
|
text-transform: none !important;
|
||||||
}
|
}
|
||||||
.mti__accent {
|
.mti__accent {
|
||||||
color: #C01227 !important;
|
color: #dc2626 !important;
|
||||||
}
|
}
|
||||||
.mti__copy {
|
.mti__desc {
|
||||||
max-width: 680px;
|
font-size: clamp(16px, 1.15vw, 18px);
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #4b5563;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 780px;
|
||||||
}
|
}
|
||||||
.mti__copy p {
|
.mti-grid {
|
||||||
margin: 0 0 14px;
|
display: grid;
|
||||||
color: rgba(17, 17, 17, 0.68) !important;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
font-size: clamp(15px, 1.05vw, 17px) !important;
|
gap: clamp(20px, 3vw, 36px);
|
||||||
font-weight: 500 !important;
|
padding-top: clamp(28px, 3.5vw, 40px);
|
||||||
line-height: 1.6 !important;
|
|
||||||
}
|
}
|
||||||
.mti__copy p:last-child {
|
.mti-card {
|
||||||
margin-bottom: 0;
|
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 {
|
.mti-card:last-child {
|
||||||
margin-left: clamp(16px, 2vw, 24px);
|
border-right: none;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
.mti__viz {
|
.mti-card-icon {
|
||||||
flex: 0 1 38%;
|
color: #111827;
|
||||||
min-width: 240px;
|
margin-bottom: 16px;
|
||||||
max-width: 460px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
transition: transform 0.35s ease, color 0.35s ease;
|
||||||
}
|
}
|
||||||
.mti__viz-frame {
|
.mti-card:hover .mti-card-icon {
|
||||||
position: relative;
|
transform: translateY(-4px) scale(1.08);
|
||||||
width: 100%;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
.mti__viz-svg {
|
.mti-icon {
|
||||||
display: block;
|
transition: stroke 0.35s ease;
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
.mti__viz-edge {
|
|
||||||
stroke: rgba(17, 17, 17, 0.16);
|
.mti-card-val {
|
||||||
stroke-width: 1;
|
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 {
|
.mti-card-label {
|
||||||
fill: none;
|
font-size: 14.5px;
|
||||||
stroke: #C01227;
|
line-height: 1.5;
|
||||||
stroke-width: 1.4;
|
color: #4b5563;
|
||||||
}
|
font-weight: 450;
|
||||||
.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; }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.mti__row {
|
.mti-grid {
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
align-items: center;
|
gap: 28px;
|
||||||
gap: clamp(28px, 5vw, 40px);
|
|
||||||
}
|
}
|
||||||
.mti__left {
|
.mti-card {
|
||||||
width: 100%;
|
border-right: none;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
.mti__viz {
|
}
|
||||||
flex: none;
|
@media (max-width: 540px) {
|
||||||
width: 100%;
|
.mti-grid {
|
||||||
max-width: 360px;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.mti-shell {
|
.mti-shell {
|
||||||
width: calc(100% - 20px);
|
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">
|
<div className="mti-shell">
|
||||||
<section className="mti" aria-label="MileTruth AI introduction">
|
<section className="mti" aria-label="MileTruth AI introduction">
|
||||||
<div className="mti__inner">
|
<div className="mti__inner">
|
||||||
<div className="mti__row">
|
<div className="mti__head">
|
||||||
<div className="mti__left">
|
<span className="mti__eyebrow">/ MileTruth AI /</span>
|
||||||
<span className="mti__eyebrow">/ MILETRUTH AI /</span>
|
<hr className="mti__divider" />
|
||||||
<hr className="mti__divider" />
|
<h2 className="mti__heading">
|
||||||
<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>
|
||||||
</h2>
|
<p className="mti__desc">
|
||||||
<div className="mti__copy">
|
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 className="mti__lede">Routing is not one path between two points.</p>
|
</p>
|
||||||
<p>
|
</div>
|
||||||
Orders change. Vehicles have different limits. Capacity, range
|
|
||||||
and time change what is possible.
|
{/* 4-Column Animated SVG KPI Grid */}
|
||||||
</p>
|
<div className="mti-grid">
|
||||||
<p>
|
{MILETRUTH_METRICS.map((m) => (
|
||||||
MileTruth evaluates the options, checks what can actually work,
|
<div className="mti-card" key={m.value}>
|
||||||
and moves the strongest plan forward.
|
<div className="mti-card-icon">{m.icon}</div>
|
||||||
</p>
|
<div className="mti-card-val">{m.value}</div>
|
||||||
|
<div className="mti-card-label">{m.label}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<RouteConverge />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -60,10 +60,31 @@ export default function MileTruthNetwork({ ariaLabel }: { ariaLabel?: string })
|
|||||||
return (
|
return (
|
||||||
<div ref={wrapRef} className="mtn-scene" role="img" aria-label={ariaLabel ?? "MileTruth AI network"}>
|
<div ref={wrapRef} className="mtn-scene" role="img" aria-label={ariaLabel ?? "MileTruth AI network"}>
|
||||||
<style dangerouslySetInnerHTML={{ __html: `
|
<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; }
|
.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 && <MileTruthCanvas reduced={reduced} isMobile={isMobile} active={active} />}
|
||||||
|
{mountScene && (
|
||||||
|
<span className="mtn-tag" aria-hidden="true"><i />Illustrative — sample insight cycle</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,17 @@ import FirstMileIcon from "../icons/FirstMileIcon";
|
|||||||
import MidMileIcon from "../icons/MidMileIcon";
|
import MidMileIcon from "../icons/MidMileIcon";
|
||||||
import LastMileIcon from "../icons/LastMileIcon";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<style dangerouslySetInnerHTML={{ __html: `
|
<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-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-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="elementor-widget-container">
|
||||||
<div className="logico-title">/ How It Works /</div>
|
<div className="logico-title">{eyebrow}</div>
|
||||||
</div>
|
</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-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">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
import WorkflowScene from "./WorkflowScene";
|
import WorkflowScene from "./WorkflowScene";
|
||||||
@@ -9,17 +9,38 @@ if (typeof window !== "undefined") {
|
|||||||
gsap.registerPlugin(ScrollTrigger);
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
function Svg({ children, size = 18 }: { children: React.ReactNode; size?: number }) {
|
||||||
QuantumStage — the page's ONE true 3D moment. Wraps the existing
|
return (
|
||||||
production "Logistics Brain" scene (glowing core, orbiting particle
|
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
shells, six parallel route strategies racing, live network) —
|
{children}
|
||||||
already built and already on-brand red — inside a near-full-viewport
|
</svg>
|
||||||
frame, with a floating Traditional-vs-Quantum comparison overlaid.
|
);
|
||||||
GSAP drives only the entrance (fade/rise); the 3D scene animates
|
}
|
||||||
itself.
|
|
||||||
============================================================ */
|
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() {
|
export default function QuantumStage() {
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const root = rootRef.current;
|
const root = rootRef.current;
|
||||||
@@ -36,29 +57,34 @@ export default function QuantumStage() {
|
|||||||
}
|
}
|
||||||
gsap.set(frame, { opacity: 0, y: 26 });
|
gsap.set(frame, { opacity: 0, y: 26 });
|
||||||
gsap.set(tradCol, { opacity: 0, y: 26 });
|
gsap.set(tradCol, { opacity: 0, y: 26 });
|
||||||
gsap.set(qSteps, { opacity: 0.25, y: 10 });
|
gsap.set(qSteps, { opacity: 0.2, y: 14, scale: 0.96 });
|
||||||
gsap.set(qDot, { scale: 0.6, opacity: 0 });
|
if (qDot) gsap.set(qDot, { scale: 0.5, opacity: 0 });
|
||||||
|
|
||||||
const play = () => {
|
const play = () => {
|
||||||
const tl = gsap.timeline();
|
const tl = gsap.timeline();
|
||||||
// Frame + traditional baseline settle in first (the "boring" case).
|
tl.to(frame, { opacity: 1, y: 0, duration: 0.8, ease: "power3.out" }, 0)
|
||||||
tl.to(frame, { opacity: 1, y: 0, duration: 0.9, ease: "power3.out" }, 0)
|
.to(tradCol, { opacity: 1, y: 0, duration: 0.6, ease: "power3.out" }, 0.15);
|
||||||
.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.
|
|
||||||
qSteps.forEach((el, i) => {
|
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)
|
tl.to(el, { opacity: 1, y: 0, scale: 1, duration: 0.4, ease: "back.out(1.4)" }, 0.6 + i * 0.22);
|
||||||
.to(el, { scale: 1, duration: 0.22, ease: "power1.out" }, 0.7 + i * 0.42 + 0.32);
|
|
||||||
});
|
});
|
||||||
// Dispatch pulses brighter — the decisive moment.
|
|
||||||
const last = qSteps[qSteps.length - 1];
|
if (qDot) {
|
||||||
tl.to(qDot, { opacity: 1, scale: 1, duration: 0.4, ease: "back.out(2)" }, 0.7 + qSteps.length * 0.42)
|
tl.to(qDot, { opacity: 1, scale: 1, duration: 0.4, ease: "back.out(2)" }, 0.6 + qSteps.length * 0.22);
|
||||||
.to(last, { color: "#ff5a6e", duration: 0.3 }, "<");
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const st = ScrollTrigger.create({ trigger: root, start: "top 78%", once: true, onEnter: play });
|
const st = ScrollTrigger.create({ trigger: root, start: "top 78%", once: true, onEnter: play });
|
||||||
return () => st.kill();
|
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 (
|
return (
|
||||||
<div ref={rootRef} className="qs">
|
<div ref={rootRef} className="qs">
|
||||||
<style dangerouslySetInnerHTML={{ __html: CSS }} />
|
<style dangerouslySetInnerHTML={{ __html: CSS }} />
|
||||||
@@ -67,26 +93,121 @@ export default function QuantumStage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="qs-compare">
|
<div className="qs-compare">
|
||||||
|
{/* Traditional baseline */}
|
||||||
<div className="qs-col qs-col--trad">
|
<div className="qs-col qs-col--trad">
|
||||||
<span className="qs-col-tag">Traditional</span>
|
<div className="qs-col-head">
|
||||||
<div className="qs-col-steps">
|
<span className="qs-col-tag">
|
||||||
<span>One Route</span>
|
<span className="qs-tag-dot qs-tag-dot--muted" />
|
||||||
<span className="qs-col-arrow">↓</span>
|
Traditional Approach
|
||||||
<span>Dispatch</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Quantum Engine */}
|
||||||
<div className="qs-col qs-col--q">
|
<div className="qs-col qs-col--q">
|
||||||
<span className="qs-col-tag qs-col-tag--accent">Quantum</span>
|
<div className="qs-col-head">
|
||||||
<div className="qs-col-steps qs-col-steps--accent">
|
<span className="qs-col-tag qs-col-tag--accent">
|
||||||
<span className="qs-qstep">100+ Route Simulations</span>
|
<span className="qs-tag-dot qs-tag-dot--active" />
|
||||||
<span className="qs-col-arrow">→</span>
|
Quantum Engine
|
||||||
<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" />
|
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,29 +218,31 @@ export default function QuantumStage() {
|
|||||||
const CSS = `
|
const CSS = `
|
||||||
.qs { position: relative; }
|
.qs { position: relative; }
|
||||||
|
|
||||||
/* --------------------------------------------------------------
|
#sw-root #quantum h2,
|
||||||
Section 05 runs light — every other section on this page keeps
|
#sw-root #quantum .sw-h,
|
||||||
the shared dark "ground texture" system, but this one uses the
|
#quantum h2,
|
||||||
shared white .sw-bg--white variant (set via the Section's bg
|
#quantum .sw-h { color: #111827 !important; }
|
||||||
prop in SolutionsWorkflow.tsx) so the workflow reads as a
|
|
||||||
dashboard panel, not a visualization sitting in a black canvas —
|
#sw-root #quantum h2 .accent,
|
||||||
while still going through the exact same inset/rounded .sw-bg
|
#sw-root #quantum .sw-h .accent,
|
||||||
card surface as every other section. Only the text colors need
|
#quantum h2 .accent,
|
||||||
a local override here, for readability on that white surface.
|
#quantum .sw-h .accent { color: #dc2626 !important; }
|
||||||
-------------------------------------------------------------- */
|
|
||||||
#quantum .sw-h { color: #14151a !important; }
|
#sw-root #quantum p,
|
||||||
#quantum .sw-lede { color: #5a5b64 !important; }
|
#sw-root #quantum .sw-lede,
|
||||||
|
#quantum p,
|
||||||
|
#quantum .sw-lede { color: #4b5563 !important; }
|
||||||
|
|
||||||
.qs-frame {
|
.qs-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: auto;
|
aspect-ratio: auto;
|
||||||
min-height: min(84vh, 900px);
|
min-height: min(84vh, 900px);
|
||||||
border-radius: 28px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f6f6f7;
|
background: #0d0d11;
|
||||||
border: 1px solid rgba(20,21,26,0.08);
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
box-shadow: 0 30px 70px -45px rgba(20,21,26,0.25);
|
box-shadow: 0 24px 60px -20px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
.qs-frame .wf-scene {
|
.qs-frame .wf-scene {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -129,36 +252,274 @@ const CSS = `
|
|||||||
aspect-ratio: auto;
|
aspect-ratio: auto;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Redesigned Comparison Grid (White & Red Theme with Tight Bottom Padding) ---- */
|
||||||
.qs-compare {
|
.qs-compare {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: clamp(28px, 4vw, 48px);
|
margin-top: 24px;
|
||||||
|
margin-bottom: 0px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1.4fr;
|
grid-template-columns: 1fr 2.2fr;
|
||||||
gap: clamp(20px, 3vw, 40px);
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Card Base */
|
||||||
.qs-col {
|
.qs-col {
|
||||||
padding: clamp(20px, 2.4vw, 30px) clamp(22px, 2.6vw, 34px);
|
padding: 20px 22px;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background: #f6f6f7;
|
display: flex;
|
||||||
border: 1px solid rgba(20,21,26,0.08);
|
flex-direction: column;
|
||||||
box-shadow: 0 20px 44px -34px rgba(20,21,26,0.2);
|
justify-content: space-between;
|
||||||
display: flex; flex-direction: column; align-items: center; gap: 18px; text-align: center;
|
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; }
|
/* Traditional Card - Muted Off-White */
|
||||||
.qs-col-tag--accent { color: #C01227; }
|
.qs-col--trad {
|
||||||
.qs-col-steps { display: flex; flex-direction: column; align-items: center; gap: 8px; color: #4a4b53; font-weight: 700; font-size: 14px; }
|
background: #f8f9fa;
|
||||||
.qs-col-steps--accent { color: #14151a; font-size: 15px; flex-direction: row; flex-wrap: wrap; justify-content: center; }
|
border: 1px solid #e5e7eb;
|
||||||
.qs-qstep { display: inline-flex; align-items: center; position: relative; }
|
box-shadow: 0 8px 24px rgba(0,0,0,0.03);
|
||||||
.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; }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 767px) {
|
|
||||||
.qs-frame { min-height: 62vh; border-radius: 20px; }
|
/* Quantum Card - White & Soft Crimson Gradient */
|
||||||
.qs-frame .wf-scene { border-radius: 14px; }
|
.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; }
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,7 +3,29 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { preload } from "react-dom";
|
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" });
|
preload("/images/home1-slide-1.webp", { as: "image" });
|
||||||
const [activeSlide, setActiveSlide] = useState(0);
|
const [activeSlide, setActiveSlide] = useState(0);
|
||||||
|
|
||||||
@@ -233,11 +255,11 @@ export default function SolutionsHero() {
|
|||||||
<div className="slide-content">
|
<div className="slide-content">
|
||||||
<div className="slide-content-inner">
|
<div className="slide-content-inner">
|
||||||
<h1 className="content-slider-item-heading logico-content-wrapper-1">
|
<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>
|
</h1>
|
||||||
<div className="content-slider-item-text logico-content-wrapper-2">
|
<div className="content-slider-item-text logico-content-wrapper-2">
|
||||||
<div className="text-content">
|
<div className="text-content">
|
||||||
<p>Discover how Doormile's connected logistics platform serves diverse industries with tailored solutions.</p>
|
<p>{slides[0].text}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,11 +285,11 @@ export default function SolutionsHero() {
|
|||||||
<div className="slide-content">
|
<div className="slide-content">
|
||||||
<div className="slide-content-inner">
|
<div className="slide-content-inner">
|
||||||
<h1 className="content-slider-item-heading logico-content-wrapper-1">
|
<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>
|
</h1>
|
||||||
<div className="content-slider-item-text logico-content-wrapper-2">
|
<div className="content-slider-item-text logico-content-wrapper-2">
|
||||||
<div className="text-content">
|
<div className="text-content">
|
||||||
<p>Doormile adapts to the unique challenges of your vertical—not the other way around.</p>
|
<p>{slides[1].text}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2759
src/components/sections/SolutionsSections.tsx
Normal file
2759
src/components/sections/SolutionsSections.tsx
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@ import EVSection, { type EVFeature, type EVStat } from "./EVSection";
|
|||||||
// EVSection embeds this value in an UNQUOTED CSS url(...).
|
// EVSection embeds this value in an UNQUOTED CSS url(...).
|
||||||
|
|
||||||
const METRICS: EVStat[] = [
|
const METRICS: EVStat[] = [
|
||||||
{ value: 8.2, decimals: 1, suffix: "t", label: "ATR72 Payload" },
|
{ value: 8.2, decimals: 1, suffix: "t", label: "Payload Capacity" },
|
||||||
{ value: 1600, suffix: "km", label: "ATR72 Range" },
|
{ value: 1600, suffix: "km", label: "Regional Range" },
|
||||||
{ value: 8.6, decimals: 1, suffix: "t", label: "Q400 Payload" },
|
{ value: 8.6, decimals: 1, suffix: "t", label: "Max Payload" },
|
||||||
{ value: 667, suffix: "km/h", label: "Q400 Speed" },
|
{ value: 667, suffix: "km/h", label: "Regional Speed" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FEATURES: EVFeature[] = [
|
const FEATURES: EVFeature[] = [
|
||||||
|
|||||||
@@ -1,68 +1,115 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import gsap from "gsap";
|
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 = [
|
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",
|
title: "Demand Forecasting",
|
||||||
desc: "AI-driven forward projection of cargo demand, preemptively positioning aircraft and ground capacity.",
|
|
||||||
icon: (
|
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">
|
||||||
<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">
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
<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>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Route Optimization",
|
title: "Route & Fleet Optimization",
|
||||||
desc: "Dynamic scheduling for ground EV pickups and flights that bypasses congestion for the fastest path.",
|
|
||||||
icon: (
|
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">
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
<path d="M3 12h18" />
|
||||||
<polyline points="13 6 19 12 13 18" />
|
<path d="M12 3v18" />
|
||||||
|
<path d="m18 17 4-5-4-5" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Load Balancing",
|
title: "Anomaly Detection",
|
||||||
desc: "Automated weight-and-balance modeling for optimal, safe aircraft load layout within minutes.",
|
|
||||||
icon: (
|
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">
|
||||||
<path d="M12 3v18M5 8l7-5 7 5M5 8v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8" />
|
<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>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Predictive Maintenance",
|
title: "Performance Insights",
|
||||||
desc: "Continuous telemetry monitoring triggers preemptive alerts, drastically reducing aircraft downtime.",
|
|
||||||
icon: (
|
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">
|
||||||
<path d="M22 12h-6l-4 8-4-16-4 8H2" />
|
<path d="M12 20V10" />
|
||||||
|
<path d="M18 20V4" />
|
||||||
|
<path d="M6 20v-4" />
|
||||||
</svg>
|
</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 }) {
|
function TiltCard({ children, className }: { children: React.ReactNode, className: string }) {
|
||||||
@@ -73,9 +120,7 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
|
|||||||
const card = cardRef.current;
|
const card = cardRef.current;
|
||||||
const glare = glareRef.current;
|
const glare = glareRef.current;
|
||||||
if (!card || !glare) return;
|
if (!card || !glare) return;
|
||||||
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
||||||
if (reduced) return;
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
const rect = card.getBoundingClientRect();
|
const rect = card.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
@@ -84,39 +129,24 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
|
|||||||
const centerX = rect.width / 2;
|
const centerX = rect.width / 2;
|
||||||
const centerY = rect.height / 2;
|
const centerY = rect.height / 2;
|
||||||
|
|
||||||
// Softer, more premium 3D tilt
|
const rotateX = ((y - centerY) / centerY) * -4;
|
||||||
const rotateX = ((y - centerY) / centerY) * -3;
|
const rotateY = ((x - centerX) / centerX) * 4;
|
||||||
const rotateY = ((x - centerX) / centerX) * 3;
|
|
||||||
|
|
||||||
gsap.to(card, {
|
gsap.to(card, {
|
||||||
duration: 0.4,
|
duration: 0.5,
|
||||||
rotateX,
|
rotateX,
|
||||||
rotateY,
|
rotateY,
|
||||||
y: -4,
|
transformPerspective: 1200,
|
||||||
transformPerspective: 1000,
|
|
||||||
ease: "power2.out"
|
ease: "power2.out"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dynamic glare tracking
|
|
||||||
gsap.set(glare, {
|
gsap.set(glare, {
|
||||||
"--mx": `${x}px`,
|
"--mx": `${x}px`,
|
||||||
"--my": `${y}px`,
|
"--my": `${y}px`,
|
||||||
});
|
});
|
||||||
gsap.to(glare, {
|
gsap.to(glare, {
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
opacity: 1,
|
opacity: 0.15,
|
||||||
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,
|
|
||||||
ease: "power2.out"
|
ease: "power2.out"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -126,7 +156,6 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
|
|||||||
duration: 0.8,
|
duration: 0.8,
|
||||||
rotateX: 0,
|
rotateX: 0,
|
||||||
rotateY: 0,
|
rotateY: 0,
|
||||||
y: 0,
|
|
||||||
ease: "power3.out"
|
ease: "power3.out"
|
||||||
});
|
});
|
||||||
gsap.to(glare, {
|
gsap.to(glare, {
|
||||||
@@ -134,11 +163,6 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
ease: "power3.out"
|
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);
|
card.addEventListener("mousemove", handleMouseMove);
|
||||||
@@ -151,29 +175,29 @@ function TiltCard({ children, className }: { children: React.ReactNode, classNam
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={cardRef} className={className}>
|
<div ref={cardRef} className={className} style={{ perspective: "1000px" }}>
|
||||||
<div
|
<div
|
||||||
ref={glareRef}
|
ref={glareRef}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
borderRadius: "inherit",
|
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,
|
opacity: 0,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
zIndex: 0,
|
zIndex: 2,
|
||||||
transform: "translateZ(1px)",
|
transform: "translateZ(1px)",
|
||||||
mixBlendMode: "screen"
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WingsMileTruth() {
|
const ParticleCanvas = () => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -182,85 +206,90 @@ export default function WingsMileTruth() {
|
|||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
let animationFrameId: number;
|
||||||
|
|
||||||
let width = (canvas.width = canvas.offsetWidth);
|
let width = (canvas.width = canvas.offsetWidth);
|
||||||
let height = (canvas.height = canvas.offsetHeight);
|
let height = (canvas.height = canvas.offsetHeight);
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
if (!canvas) return;
|
||||||
width = canvas.width = canvas.offsetWidth;
|
width = canvas.width = canvas.offsetWidth;
|
||||||
height = canvas.height = canvas.offsetHeight;
|
height = canvas.height = canvas.offsetHeight;
|
||||||
};
|
};
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
const nodes = [
|
const particles: Array<{
|
||||||
{ x: 0.12, y: 0.5 },
|
x: number;
|
||||||
{ x: 0.34, y: 0.25 },
|
y: number;
|
||||||
{ x: 0.56, y: 0.65 },
|
size: number;
|
||||||
{ x: 0.78, y: 0.3 },
|
speedY: number;
|
||||||
{ x: 0.92, y: 0.6 },
|
speedX: number;
|
||||||
];
|
opacity: number;
|
||||||
const particles = [
|
fadeSpeed: number;
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
let raf: number;
|
const createParticle = (isInitial = false) => {
|
||||||
const draw = () => {
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
ctx.clearRect(0, 0, width, height);
|
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);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
|
|
||||||
if (!reduced) {
|
particles.forEach((p, idx) => {
|
||||||
particles.forEach((p) => {
|
p.y += p.speedY;
|
||||||
p.t += p.speed;
|
p.x += p.speedX;
|
||||||
if (p.t >= 1) p.t = 0;
|
p.opacity -= p.fadeSpeed;
|
||||||
const a = nodes[p.from];
|
|
||||||
const b = nodes[p.to];
|
if (p.opacity <= 0 || p.y < -10 || p.x < -10 || p.x > width + 10) {
|
||||||
const x = a.x * width + (b.x * width - a.x * width) * p.t;
|
particles[idx] = createParticle(false);
|
||||||
const y = a.y * height + (b.y * height - a.y * height) * p.t;
|
} else {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = "#c8102e";
|
ctx.fillStyle = `rgba(255, 59, 59, ${p.opacity})`;
|
||||||
ctx.shadowColor = "#c8102e";
|
ctx.shadowBlur = 4;
|
||||||
ctx.shadowBlur = 10;
|
ctx.shadowColor = "rgba(255, 59, 59, 0.6)";
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.shadowBlur = 0;
|
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 () => {
|
return () => {
|
||||||
window.removeEventListener("resize", handleResize);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<style
|
<style
|
||||||
@@ -273,45 +302,97 @@ export default function WingsMileTruth() {
|
|||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #09090b;
|
background: #08080a;
|
||||||
padding: clamp(64px, 8vw, 100px) clamp(24px, 5vw, 48px);
|
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;
|
position: absolute;
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmt-glow {
|
|
||||||
position: absolute;
|
|
||||||
width: 480px;
|
|
||||||
height: 480px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
pointer-events: none;
|
|
||||||
filter: blur(140px);
|
filter: blur(140px);
|
||||||
z-index: 0;
|
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-ambient-glow.red-left {
|
||||||
.wmt-glow.br { right: -8%; bottom: -12%; background: radial-gradient(circle, #ff3b3b 0%, transparent 70%); }
|
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 {
|
.wmt-inner {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
max-width: 1380px;
|
max-width: 1380px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.15fr 0.85fr;
|
||||||
|
gap: 64px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-header {
|
@media (max-width: 1024px) {
|
||||||
margin: 0 auto 48px auto;
|
.wmt-inner {
|
||||||
max-width: 720px;
|
grid-template-columns: 1fr;
|
||||||
text-align: center;
|
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 {
|
.wmt-eyebrow {
|
||||||
@@ -319,85 +400,262 @@ export default function WingsMileTruth() {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 2.5px;
|
letter-spacing: 2.5px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #c8102e;
|
color: #ff3b3b;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-title {
|
.wmt-title {
|
||||||
font-size: clamp(28px, 3.4vw, 48px) !important;
|
font-size: clamp(32px, 3.8vw, 44px) !important;
|
||||||
font-weight: 800 !important;
|
font-weight: 800 !important;
|
||||||
letter-spacing: -0.03em !important;
|
letter-spacing: -0.03em !important;
|
||||||
line-height: 1.15 !important;
|
line-height: 1.15 !important;
|
||||||
text-transform: none !important;
|
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
margin: 0;
|
margin: 0 0 16px 0;
|
||||||
|
text-transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-grid {
|
.wmt-title span {
|
||||||
display: grid;
|
color: #ff3b3b;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 18px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-card {
|
.wmt-desc {
|
||||||
min-width: 0;
|
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;
|
position: relative;
|
||||||
background: rgba(255, 255, 255, 0.035);
|
margin-bottom: 40px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-card:hover {
|
.wmt-flow-title {
|
||||||
border-color: rgba(255, 255, 255, 0.16);
|
font-size: 11px;
|
||||||
background-color: rgba(255, 255, 255, 0.055);
|
text-transform: uppercase;
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
|
letter-spacing: 1.5px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-card-icon {
|
.wmt-flow-pipeline {
|
||||||
width: 40px;
|
display: flex;
|
||||||
height: 40px;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 10px;
|
background: rgba(255, 255, 255, 0.02);
|
||||||
background: rgba(200, 16, 46, 0.12);
|
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;
|
color: #ffffff;
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-card-title {
|
.wmt-flow-connector {
|
||||||
font-size: 15.5px !important;
|
position: relative;
|
||||||
font-weight: 700 !important;
|
flex: 1;
|
||||||
line-height: 1.3 !important;
|
height: 1px;
|
||||||
text-transform: none !important;
|
background: rgba(255, 255, 255, 0.06);
|
||||||
color: #ffffff !important;
|
overflow: hidden;
|
||||||
margin: 0 0 8px 0;
|
max-width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-card-desc {
|
@media (max-width: 640px) {
|
||||||
font-size: 16px;
|
.wmt-flow-connector {
|
||||||
line-height: 1.55;
|
display: none;
|
||||||
color: rgba(255, 255, 255, 0.55);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
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 {
|
.wmt-callout {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 20px;
|
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%);
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-top: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmt-callout::after {
|
.wmt-callout::after {
|
||||||
@@ -405,7 +663,7 @@ export default function WingsMileTruth() {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -1.5px;
|
inset: -1.5px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: linear-gradient(90deg, #c8102e, #ff3366, #ff0033, #c8102e);
|
background: linear-gradient(90deg, #c8102e, #ff3b3b, #ff3b3b, #c8102e);
|
||||||
background-size: 300% 100%;
|
background-size: 300% 100%;
|
||||||
animation: wmt-sweep 6s linear infinite;
|
animation: wmt-sweep 6s linear infinite;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@@ -421,7 +679,7 @@ export default function WingsMileTruth() {
|
|||||||
.wmt-callout-body { max-width: 640px; }
|
.wmt-callout-body { max-width: 640px; }
|
||||||
|
|
||||||
.wmt-callout-title {
|
.wmt-callout-title {
|
||||||
font-size: 20px !important;
|
font-size: 19px !important;
|
||||||
font-weight: 800 !important;
|
font-weight: 800 !important;
|
||||||
line-height: 1.3 !important;
|
line-height: 1.3 !important;
|
||||||
text-transform: none !important;
|
text-transform: none !important;
|
||||||
@@ -430,7 +688,7 @@ export default function WingsMileTruth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wmt-callout-desc {
|
.wmt-callout-desc {
|
||||||
font-size: 14px;
|
font-size: 13.5px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -438,7 +696,7 @@ export default function WingsMileTruth() {
|
|||||||
|
|
||||||
.wmt-callout-badge {
|
.wmt-callout-badge {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 11.5px;
|
font-size: 11px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -449,18 +707,8 @@ export default function WingsMileTruth() {
|
|||||||
padding: 8px 16px;
|
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) {
|
@media (max-width: 600px) {
|
||||||
.wmt-grid { grid-template-columns: 1fr; }
|
.wmt-callout { flex-direction: column; align-items: flex-start; margin-top: 40px; }
|
||||||
.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-section { padding-top: 48px; padding-bottom: 48px; }
|
.wmt-section { padding-top: 48px; padding-bottom: 48px; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -468,39 +716,89 @@ export default function WingsMileTruth() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="wmt-section" id="wings-miletruth">
|
<section className="wmt-section" id="wings-miletruth">
|
||||||
<canvas ref={canvasRef} className="wmt-canvas" aria-hidden="true" />
|
<div className="wmt-ambient-glow red-left" aria-hidden="true" />
|
||||||
<div className="wmt-glow tl" aria-hidden="true" />
|
<div className="wmt-ambient-glow red-right" aria-hidden="true" />
|
||||||
<div className="wmt-glow br" aria-hidden="true" />
|
|
||||||
|
|
||||||
<div className="wmt-inner dm-section-container">
|
<div className="wmt-inner dm-section-container">
|
||||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wmt-header">
|
<ScrollReveal delay={0.05} duration={0.8} yOffset={20} className="wmt-visual-container">
|
||||||
<div className="wmt-eyebrow">/ MileTruth™ AI /</div>
|
<TiltCard className="wmt-visual-pane">
|
||||||
<h2 className="wmt-title">The intelligence layer behind every shipment</h2>
|
<img
|
||||||
|
src="/images/miletruth-ai-core.png"
|
||||||
|
alt="MileTruth AI Core Engine Visualization"
|
||||||
|
className="wmt-visual-img"
|
||||||
|
/>
|
||||||
|
<ParticleCanvas />
|
||||||
|
</TiltCard>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
<StaggerChildren stagger={0.08} duration={0.65} yOffset={25} className="wmt-grid">
|
<div className="wmt-content-pane">
|
||||||
{CAPABILITIES.map((c) => (
|
<ScrollReveal delay={0.1} duration={0.8} yOffset={20}>
|
||||||
<TiltCard key={c.title} className="wmt-card">
|
<div className="wmt-eyebrow">/ MileTruth™ AI /</div>
|
||||||
<div className="wmt-card-icon" aria-hidden="true">{c.icon}</div>
|
<h2 className="wmt-title">MILETRUTH™ <span>AI</span></h2>
|
||||||
<h3 className="wmt-card-title">{c.title}</h3>
|
<p className="wmt-desc">
|
||||||
<p className="wmt-card-desc">{c.desc}</p>
|
MileTruth continuously analyzes every completed shipment, turning raw operational data into recommendations that make next mile faster, cheaper and more reliable.
|
||||||
</TiltCard>
|
</p>
|
||||||
))}
|
</ScrollReveal>
|
||||||
</StaggerChildren>
|
|
||||||
|
|
||||||
<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-callout">
|
<div className="wmt-flow-title">Data Pipeline Flow</div>
|
||||||
<div className="wmt-callout-body">
|
<div
|
||||||
<h3 className="wmt-callout-title">Digital Twin of the Logistics Network</h3>
|
className="wmt-flow-pipeline"
|
||||||
<p className="wmt-callout-desc">
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
A live virtual model of every ground vehicle, terminal, and flight — simulating
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
what-if scenarios before bottlenecks ever form.
|
>
|
||||||
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="wmt-callout-badge">SaaS Ready</span>
|
</ScrollReveal>
|
||||||
</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>
|
</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>
|
||||||
|
<p className="wmt-callout-desc">
|
||||||
|
A live virtual model of every ground vehicle, terminal, and flight — simulating
|
||||||
|
what-if scenarios before bottlenecks ever form.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="wmt-callout-badge">SaaS Ready</span>
|
||||||
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function Workflow2() {
|
|||||||
ariaLabel="Workflow 2 — Innovation"
|
ariaLabel="Workflow 2 — Innovation"
|
||||||
gapBottom
|
gapBottom
|
||||||
bannerImage="/images/mid-mile-approach.webp"
|
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."
|
cardSubtitle="Analyze thousands of route possibilities and automatically select the most efficient routing strategy."
|
||||||
eyebrow="/ Innovation /"
|
eyebrow="/ Innovation /"
|
||||||
titleLead="MANY STRATEGIES. "
|
titleLead="MANY STRATEGIES. "
|
||||||
|
|||||||
@@ -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",
|
"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`,
|
"/miletruth": `${SHARED} page-id-59 elementor-page-59`,
|
||||||
"/solutions": `${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)
|
// PHP source quirk: about-us.php has `home` class (upstream bug, preserved)
|
||||||
"/about-us": `home ${SHARED} page-id-59 elementor-page-59`,
|
"/about-us": `home ${SHARED} page-id-59 elementor-page-59`,
|
||||||
// Reuse the About page Elementor/runtime context for the Wings Empowerment
|
// Reuse the About page Elementor/runtime context for the Wings Empowerment
|
||||||
|
|||||||
Reference in New Issue
Block a user