update miletruth page and remove unwanted files

This commit is contained in:
2026-06-03 13:42:12 +05:30
parent 3bad62851c
commit 6eea5636fb
153 changed files with 6089 additions and 36024 deletions

View File

@@ -0,0 +1,61 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
/**
* SmoothScroll
* ---------------------------------------------------------------------------
* One global Lenis instance, driven by a SINGLE rAF source (GSAP's ticker) and
* kept locked to ScrollTrigger. Deliberately gated OFF on:
* - /miletruth — it stacks 3 pinned WebGL sections; JS scroll-smoothing there
* fights the pins and causes the very lag we're trying to remove. Native
* scroll is used on that route.
* - touch devices — native momentum is smoother than emulated inertia.
* - prefers-reduced-motion.
*
* Re-evaluates on every route change: the effect cleanup destroys the previous
* instance, so entering /miletruth tears Lenis down and leaving it re-inits.
*/
const DISABLED_ROUTES = ["/miletruth"];
export default function SmoothScroll() {
const pathname = usePathname();
useEffect(() => {
const routeDisabled = DISABLED_ROUTES.some(
(r) => pathname === r || pathname.startsWith(`${r}/`),
);
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Mouse/desktop only — touch devices already have good native momentum.
const isPointerFine = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (routeDisabled || prefersReduced || !isPointerFine) return;
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({
duration: 1.05,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: "vertical",
gestureOrientation: "vertical",
smoothWheel: true,
});
lenis.on("scroll", ScrollTrigger.update);
const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is seconds, Lenis wants ms
gsap.ticker.add(tickerCb);
gsap.ticker.lagSmoothing(0);
ScrollTrigger.refresh();
return () => {
gsap.ticker.remove(tickerCb);
lenis.destroy();
};
}, [pathname]);
return null;
}

View File

@@ -10,6 +10,26 @@ html {
scroll-behavior: smooth;
}
/* Lenis global smooth scroll (src/animations/SmoothScroll.tsx). These classes are
only present on routes/devices where Lenis is active; on /miletruth and touch
devices Lenis is off and native scroll-behavior:smooth (above) applies. */
html.lenis,
html.lenis body {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
*, *::before, *::after {
font-family: var(--font-manrope), "Manrope", system-ui, -apple-system, sans-serif;
}

View File

@@ -8,6 +8,8 @@ import BodyOverlay from "@/components/layout/BodyOverlay";
import { HeaderUIProvider } from "@/components/layout/HeaderUIProvider";
import { SHARED_BODY_CLASSES } from "@/lib/bodyClasses";
import AnimationProvider from "@/animations/AnimationProvider";
import SmoothScroll from "@/animations/SmoothScroll";
import LoadingScreen from "@/components/layout/LoadingScreen";
const manrope = Manrope({
subsets: ["latin"],
@@ -95,7 +97,9 @@ export default function RootLayout({
*/}
<body className={SHARED_BODY_CLASSES}>
<BodyClasses />
<LoadingScreen />
<AnimationProvider>
<SmoothScroll />
<HeaderUIProvider>
<BodyOverlay />
<div className="body-container">

View File

@@ -3,7 +3,6 @@ import MileTruthHero from "../../components/sections/MileTruthHero";
import Workflow1 from "../../components/sections/Workflow1";
import Workflow2 from "../../components/sections/Workflow2";
import Workflow3 from "../../components/sections/Workflow3";
import LogisticsBrainSection from "../../components/logisticsbrain/LogisticsBrainSection";
export const metadata = {
title: "MileTruth Doormile",
@@ -20,7 +19,6 @@ export default function MileTruthPage() {
<Workflow1 />
<Workflow2 />
<Workflow3 />
<LogisticsBrainSection />
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
* Menu open/close + sidebar state is read from HeaderUIProvider so BodyOverlay (sibling at body level) can react.
*/
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
@@ -48,7 +48,7 @@ export default function Header() {
// - on doc.ready: $('.header-hide-until-scroll').addClass('header-visible-scrolled')
// - on scroll: toggleClass('dm-header-scrolled', scrollTop > 50)
const [visibleScrolled, setVisibleScrolled] = useState(false);
const [dmHeaderScrolled, setDmHeaderScrolled] = useState(false);
const isScrolledRef = useRef(false);
useEffect(() => {
const handle = requestAnimationFrame(() => {
@@ -58,10 +58,31 @@ export default function Header() {
}, []);
useEffect(() => {
const onScroll = () => setDmHeaderScrolled(window.scrollY > 50);
let rafId: number | null = null;
const onScroll = () => {
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
const scrolled = window.scrollY > 50;
if (isScrolledRef.current !== scrolled) {
isScrolledRef.current = scrolled;
const headerEl = document.querySelector(".header-hide-until-scroll");
if (headerEl) {
if (scrolled) {
headerEl.classList.add("dm-header-scrolled");
} else {
headerEl.classList.remove("dm-header-scrolled");
}
}
}
rafId = null;
});
};
onScroll();
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
if (rafId !== null) cancelAnimationFrame(rafId);
};
}, []);
// Mirror of header.php $header_style (line 21): inline opacity:0;visibility:hidden on home (before JS adds .header-visible-scrolled)
@@ -78,7 +99,7 @@ export default function Header() {
"e-parent",
"header-hide-until-scroll",
visibleScrolled ? "header-visible-scrolled" : "",
dmHeaderScrolled ? "dm-header-scrolled" : "",
isScrolledRef.current ? "dm-header-scrolled" : "",
]
.filter(Boolean)
.join(" ");

View File

@@ -0,0 +1,114 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import Image from "next/image";
/**
* LoadingScreen
* ---------------------------------------------------------------------------
* Native reimplementation of the legacy WordPress page-loader: a black
* full-screen overlay with a centered, pulsing Doormile logo that fades out.
*
* Shows on initial load (until the window finishes loading, min ~450ms to avoid
* a flash, capped at 2.5s so it never blocks) and again briefly on each route
* navigation. CWV-safe: fixed/out-of-flow (no layout shift), logo is priority,
* and it never delays hydration.
*/
type Phase = "visible" | "hiding" | "gone";
const MIN_SHOW_MS = 450;
const MAX_SHOW_MS = 2500;
const NAV_SHOW_MS = 520;
export default function LoadingScreen() {
const pathname = usePathname();
const [phase, setPhase] = useState<Phase>("visible");
const isFirstRender = useRef(true);
// Initial load: hide once the page is ready.
useEffect(() => {
const start = performance.now();
let began = false;
let fadeTimer: ReturnType<typeof setTimeout>;
const begin = () => {
if (began) return;
began = true;
const wait = Math.max(0, MIN_SHOW_MS - (performance.now() - start));
fadeTimer = setTimeout(() => setPhase("hiding"), wait);
};
const cap = setTimeout(begin, MAX_SHOW_MS);
const onReady = () => begin();
if (document.readyState === "complete") begin();
else window.addEventListener("load", onReady, { once: true });
return () => {
clearTimeout(cap);
clearTimeout(fadeTimer);
window.removeEventListener("load", onReady);
};
}, []);
// Route navigations: flash the loader briefly for an app-like transition.
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
setPhase("visible");
const t = setTimeout(() => setPhase("hiding"), NAV_SHOW_MS);
return () => clearTimeout(t);
}, [pathname]);
if (phase === "gone") return null;
return (
<div
className={`dm-loader${phase === "hiding" ? " is-hiding" : ""}`}
role="status"
aria-live="polite"
aria-label="Loading"
onTransitionEnd={(e) => {
if (e.propertyName === "opacity" && phase === "hiding") setPhase("gone");
}}
>
<div className="dm-loader__pulse">
<Image
src="/images/preloader.png"
alt="Doormile"
width={200}
height={38}
priority
className="dm-loader__logo"
/>
</div>
<style>{`
.dm-loader {
position: fixed;
inset: 0;
z-index: 100000;
display: grid;
place-items: center;
background: #000;
opacity: 1;
transition: opacity 0.32s ease;
will-change: opacity;
}
.dm-loader.is-hiding { opacity: 0; pointer-events: none; }
.dm-loader__pulse { animation: dmLoaderPulse 1.5s linear infinite; }
.dm-loader__logo { width: clamp(140px, 18vw, 200px); height: auto; }
@keyframes dmLoaderPulse {
50% { transform: scale(0.85); }
100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.dm-loader__pulse { animation: none; }
}
`}</style>
</div>
);
}

View File

@@ -2,7 +2,7 @@
import React, { useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import * as THREE from "three";
import { C, WAYPOINTS } from "./theme";
@@ -85,7 +85,7 @@ function LogisticsBrainCanvas({ progress, reduced = false, isMobile = false, act
return (
<Canvas
flat
dpr={[1, isMobile || reduced ? 1.3 : 1.7]}
dpr={[1, isMobile || reduced ? 1.25 : 1.5]}
camera={{ position: WAYPOINTS[0].pos, fov: 52, near: 0.1, far: 200 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"}
@@ -115,7 +115,6 @@ function LogisticsBrainCanvas({ progress, reduced = false, isMobile = false, act
radius={isMobile ? 0.65 : 0.82}
kernelSize={KernelSize.MEDIUM}
/>
<Vignette eskil={false} offset={0.22} darkness={0.62} />
</EffectComposer>
)}
</Canvas>

View File

@@ -77,7 +77,7 @@ function StoryCard({
* progress value that drives the R3F scene, the camera spline and this overlay
* in lockstep, so the whole thing reads as one continuous shot.
*/
export default function LogisticsBrainSection() {
export default function LogisticsBrainSection({ connected = false }: { connected?: boolean } = {}) {
const containerRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
const scroll = useMotionValue(0);
@@ -110,7 +110,7 @@ export default function LogisticsBrainSection() {
mountIo.disconnect();
}
},
{ rootMargin: "120% 0px" },
{ rootMargin: "70% 0px" },
);
const activeIo = new IntersectionObserver(
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
@@ -164,12 +164,6 @@ export default function LogisticsBrainSection() {
const p5o = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [0, 1, 1, 0]);
const p5y = useTransform(scroll, [0.75, 0.79], [26, 0]);
// Readability scrim behind the lower-left story pillars. The bright street-level
// city (esp. the EV beat) leaves the text with no contrast, so we darken the
// bottom-left corner across all pillar beats and fade it out for the intro hint
// and the centered finale.
const scrimOpacity = useTransform(scroll, [0.08, 0.13, 0.84, P.finale], [0, 1, 1, 0]);
const finaleOpacity = useTransform(scroll, [P.finale - 0.02, P.finale + 0.04], [0, 1]);
const finaleY = useTransform(scroll, [P.finale - 0.02, P.finale + 0.06], [40, 0]);
const taglineOpacity = useTransform(scroll, [P.finale + 0.04, P.finale + 0.1], [0, 1]);
@@ -178,7 +172,7 @@ export default function LogisticsBrainSection() {
const cost = useTransform(scroll, [P.finale, 0.97], [0, 18]);
return (
<section ref={containerRef} className={`dm-lb is-${pinState}`} aria-label="Logistics Brain — one intelligent system">
<section ref={containerRef} className={`dm-lb is-${pinState}${connected ? " is-connected" : ""}`} aria-label="Logistics Brain — one intelligent system">
<div className="dm-lb-sticky">
<div className="dm-lb-card">
{mountScene && (
@@ -186,9 +180,6 @@ export default function LogisticsBrainSection() {
<LogisticsBrainCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} />
</div>
)}
<div className="dm-lb-vignette" aria-hidden />
<motion.div className="dm-lb-scrim" style={{ opacity: scrimOpacity }} aria-hidden />
<div className="dm-lb-ui">
{/* Persistent header: what this is + where we are in the workflow */}
<motion.div className="dm-lb-top" style={{ opacity: headerOpacity }}>
@@ -280,10 +271,6 @@ export default function LogisticsBrainSection() {
<span className="dm-lb-logo__mark" />
MileTruth
</motion.div>
<motion.p className="dm-lb-tagline" style={{ opacity: taglineOpacity }}>
This isn&apos;t just software.<br />
<strong>This is your logistics brain.</strong>
</motion.p>
</motion.div>
</div>
</div>
@@ -309,32 +296,36 @@ const styles = `
}
@media (max-width: 767px) { .dm-lb-card { inset: 10px !important; border-radius: 20px !important; } }
/* Connected mode (inside Workflow 2): flatten the card's bottom edge and flush it to
the section's bottom so the Innovation card below butts directly against it, reading
as one continuous container — mirrors the Optimisation → Performance seam in Workflow 1. */
.dm-lb.is-connected .dm-lb-card {
top: 16px !important; left: 16px !important; right: 16px !important; bottom: 0 !important;
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
}
@media (max-width: 767px) {
.dm-lb.is-connected .dm-lb-card {
top: 10px !important; left: 10px !important; right: 10px !important; bottom: 0 !important;
border-radius: 20px 20px 0 0 !important;
}
}
.dm-lb-canvas { position: absolute; inset: 0; z-index: 1; }
.dm-lb-canvas canvas { display: block; }
.dm-lb-vignette { position: absolute; inset: 0; z-index: 2; pointer-events: none;
background: radial-gradient(130% 110% at 50% 45%, transparent 58%, rgba(3,4,10,0.9) 100%),
linear-gradient(180deg, rgba(3,4,10,0.55) 0%, transparent 18%, transparent 70%, rgba(3,4,10,0.92) 100%); }
/* Lower-left readability scrim — keeps the story pillars legible over the bright
street-level skyline. Anchored to the bottom-left corner where the pillars sit. */
.dm-lb-scrim { position: absolute; inset: 0; z-index: 3; pointer-events: none;
background:
linear-gradient(to top right, rgba(3,4,10,0.94) 0%, rgba(3,4,10,0.74) 20%, rgba(3,4,10,0.34) 40%, transparent 60%),
linear-gradient(0deg, rgba(3,4,10,0.6) 0%, transparent 38%); }
@media (max-width: 767px) {
.dm-lb-scrim { background: linear-gradient(0deg, rgba(3,4,10,0.92) 0%, rgba(3,4,10,0.55) 28%, transparent 52%); }
}
.dm-lb-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none;
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; color: #eaf2ff; }
/* ---- Persistent header: title + 6-step engine rail ---- */
.dm-lb-top { position: absolute; top: clamp(16px, 3.5vh, 34px); left: 0; right: 0;
display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 0 16px; }
/* ---- Persistent header: title + 6-step engine rail ----
Offset the bar below the site's fixed navbar (~104px desktop / ~100px mobile once
.dm-header-scrolled is active). The section pins full-viewport, so without this the
eyebrow badge would sit under the navbar (z-index 10000) and get clipped. */
.dm-lb-top { position: absolute; top: clamp(96px, 13vh, 128px); left: 0; right: 0; z-index: 5;
display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 0 16px; overflow: visible; }
.dm-lb-eyebrow {
display: inline-flex; align-items: center; gap: 8px; font-size: 11px; letter-spacing: 0.28em; text-transform: uppercase;
color: #F2667A; padding: 6px 16px; border-radius: 999px; background: rgba(192,18,39,0.10);
border: 1px solid rgba(226,53,66,0.32); backdrop-filter: blur(8px); white-space: nowrap; }
display: inline-flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1.35; letter-spacing: 0.28em; text-transform: uppercase;
color: #F2667A; padding: 9px 18px; border-radius: 999px; background: rgba(192,18,39,0.10);
border: 1px solid rgba(226,53,66,0.32); backdrop-filter: blur(8px); white-space: nowrap; overflow: visible; }
.dm-lb-dot { width: 6px; height: 6px; border-radius: 50%; background: #E2354A; box-shadow: 0 0 10px #E2354A; }
.dm-lb-rail { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; max-width: 940px; }
@@ -362,8 +353,9 @@ const styles = `
.dm-lb-card-story { position: absolute; left: clamp(18px, 4vw, 56px); bottom: clamp(26px, 7vh, 64px);
width: min(440px, 84vw); pointer-events: auto; will-change: opacity, transform;
padding: 18px 20px; border-radius: 18px;
background: rgba(14,8,10,0.6); border: 1px solid rgba(226,53,66,0.22);
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
background: rgba(14,8,10,0.9); border: 1px solid rgba(226,53,66,0.22);
/* backdrop blur removed — this card cross-fades/translates on scroll, so the blur
was recomputed every frame; a near-opaque fill keeps the look at no per-frame cost. */
box-shadow: 0 24px 64px -30px rgba(0,0,0,0.92); }
.dm-lb-card-story__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.dm-lb-pillar__num { font-size: 12px; font-weight: 700; letter-spacing: 0.1em; color: #ffffff;
@@ -418,8 +410,8 @@ const styles = `
.dm-lb-finale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0 20px; }
.dm-lb-kpis { display: flex; gap: clamp(14px, 2.4vw, 28px); margin-bottom: clamp(28px, 6vh, 56px); flex-wrap: wrap; justify-content: center; }
.dm-lb-kpi { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: clamp(150px, 18vw, 210px);
padding: 22px 26px; border-radius: 18px; background: rgba(16,9,11,0.6); border: 1px solid rgba(226,53,66,0.28);
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); box-shadow: 0 24px 60px -28px rgba(0,0,0,0.9); }
padding: 22px 26px; border-radius: 18px; background: rgba(16,9,11,0.9); border: 1px solid rgba(226,53,66,0.28);
box-shadow: 0 24px 60px -28px rgba(0,0,0,0.9); }
.dm-lb-kpi--green { border-color: rgba(34,197,94,0.4); }
.dm-lb-kpi__num { font-size: clamp(38px, 5.5vw, 72px); font-weight: 800; line-height: 1; letter-spacing: -0.03em;
color: #fff; text-shadow: 0 0 32px rgba(226,53,66,0.55), 0 0 12px rgba(192,18,39,0.5); }
@@ -431,11 +423,6 @@ const styles = `
.dm-lb-logo__mark { width: clamp(20px, 2.4vw, 30px); height: clamp(20px, 2.4vw, 30px); border-radius: 8px;
background: conic-gradient(from 140deg, #E2354A, #C01227, #8A0E1F, #C8102E, #E2354A);
box-shadow: 0 0 28px rgba(192,18,39,0.75); }
.dm-lb-tagline { margin: 0; font-size: clamp(15px, 1.9vw, 24px); line-height: 1.4; font-weight: 400;
color: rgba(240,224,226,0.78); letter-spacing: 0.02em; }
.dm-lb-tagline strong { display: inline-block; margin-top: 4px; font-weight: 700; color: #fff;
background: linear-gradient(90deg, #E2354A, #C01227, #C8102E); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
text-transform: uppercase; letter-spacing: 0.06em; }
/* Hide the step titles on narrower screens so the rail stays a single tidy row of numbers. */
@media (max-width: 1000px) {

View File

@@ -0,0 +1,48 @@
import { clamp01, lerp, smoothstep, damp, seeded } from "./math";
// The Logistics Brain scene intentionally keeps its own copy of these helpers
// (see the note in math.ts about Turbopack cross-folder const-arrow imports),
// so they are covered independently from optimization/math.
describe("logisticsbrain/math — clamp01() & lerp()", () => {
it("clamps to [0,1]", () => {
expect(clamp01(-1)).toBe(0);
expect(clamp01(0.5)).toBe(0.5);
expect(clamp01(2)).toBe(1);
});
it("interpolates linearly", () => {
expect(lerp(0, 10, 0.5)).toBe(5);
expect(lerp(-4, 4, 0.5)).toBe(0);
});
});
describe("logisticsbrain/math — smoothstep()", () => {
it("hits edges and midpoint", () => {
expect(smoothstep(0, 1, 0)).toBe(0);
expect(smoothstep(0, 1, 1)).toBe(1);
expect(smoothstep(0, 1, 0.5)).toBeCloseTo(0.5, 10);
});
it("guards equal edges (no NaN)", () => {
expect(Number.isNaN(smoothstep(2, 2, 2))).toBe(false);
});
});
describe("logisticsbrain/math — damp()", () => {
it("is a no-op at dt=0 and converges with large lambda*dt", () => {
expect(damp(5, 20, 4, 0)).toBe(5);
expect(damp(0, 100, 50, 1)).toBeCloseTo(100, 4);
});
});
describe("logisticsbrain/math — seeded()", () => {
it("is deterministic and bounded to [0,1)", () => {
expect(seeded(42)).toBe(seeded(42));
for (let i = 0; i < 100; i++) {
const v = seeded(i);
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThan(1);
}
});
});

View File

@@ -12,18 +12,7 @@ type Props = {
const COUNT_START = 0.7;
const COUNT_END = 0.97;
function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue<number>; index: number }) {
const [jitter, setJitter] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
// Tiny micro-fluctuation between -0.4% and +0.4%
const val = (Math.random() - 0.5) * 0.008;
setJitter(val);
}, 800 + Math.random() * 600); // slightly staggered updates
return () => clearInterval(interval);
}, []);
function Metric({ kpi, scroll, index, jitter }: { kpi: Kpi; scroll: MotionValue<number>; index: number; jitter: number }) {
// Single reactive string — updates without re-rendering React.
const value = useTransform(scroll, (v) => {
const t = Math.min(1, Math.max(0, (v - COUNT_START) / (COUNT_END - COUNT_START)));
@@ -140,10 +129,24 @@ function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue<number>;
const MetricCard = React.memo(Metric);
function MetricsPanel({ scroll }: Props) {
// One shared interval drives the live "operational" micro-fluctuation for all
// cards (was 5 independent timers). Paused under reduced-motion and when the
// tab is hidden.
const [jitters, setJitters] = React.useState<number[]>(() => KPIS.map(() => 0));
React.useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const id = setInterval(() => {
if (document.hidden) return;
setJitters(KPIS.map(() => (Math.random() - 0.5) * 0.008));
}, 1100);
return () => clearInterval(id);
}, []);
return (
<div className="dm-opt-metrics" role="group" aria-label="Optimization results">
{KPIS.map((kpi, i) => (
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} />
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} jitter={jitters[i]} />
))}
</div>
);

View File

@@ -2,7 +2,7 @@
import React, { useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import { COLORS } from "./constants";
import { damp, lerp } from "./math";
@@ -47,7 +47,7 @@ function OptimizationCanvas({ progress, reduced = false, isMobile = false, activ
return (
<Canvas
flat
dpr={[1, isMobile || reduced ? 1.3 : 1.6]}
dpr={[1, isMobile || reduced ? 1.25 : 1.5]}
camera={{ position: [0, 9, 19], fov: 50, near: 0.1, far: 120 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"}
@@ -74,7 +74,6 @@ function OptimizationCanvas({ progress, reduced = false, isMobile = false, activ
radius={isMobile ? 0.6 : 0.75}
kernelSize={KernelSize.MEDIUM}
/>
<Vignette eskil={false} offset={0.25} darkness={0.5} />
</EffectComposer>
)}
</Canvas>

View File

@@ -5,7 +5,6 @@ import dynamic from "next/dynamic";
import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
import {
COLORS,
PHASE_LABELS,
@@ -130,7 +129,7 @@ export default function OptimizationSection() {
mountIo.disconnect();
}
},
{ rootMargin: "120% 0px" },
{ rootMargin: "70% 0px" },
);
const activeIo = new IntersectionObserver(
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
@@ -155,23 +154,11 @@ export default function OptimizationSection() {
if (!el) return;
gsap.registerPlugin(ScrollTrigger);
// Smooth scroll (Lenis), driven by a SINGLE rAF source — GSAP's ticker.
// Previously lenis.raf() was called from both a manual requestAnimationFrame
// loop AND gsap.ticker, double-stepping the integrator every frame, which is
// what made scrolling stutter. One source keeps Lenis + ScrollTrigger locked.
const lenis = new Lenis({
duration: 1.05,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: "vertical",
gestureOrientation: "vertical",
smoothWheel: true,
});
lenis.on("scroll", ScrollTrigger.update);
const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is in seconds, Lenis wants ms
gsap.ticker.add(tickerCb);
gsap.ticker.lagSmoothing(0);
// NOTE: /miletruth runs on native scroll (no Lenis). Smooth-scrolling this
// page fought the three stacked pinned WebGL sections and caused scroll lag;
// global Lenis (src/animations/SmoothScroll.tsx) is intentionally disabled on
// this route. ScrollTrigger's scrub + self-managed fixed pin work as-is on
// native scroll.
let lastPhase: PhaseKey = "chaos";
let lastPin: "before" | "pinned" | "after" = "before";
const st = ScrollTrigger.create({
@@ -201,8 +188,6 @@ export default function OptimizationSection() {
return () => {
clearTimeout(refresh);
st.kill();
gsap.ticker.remove(tickerCb);
lenis.destroy();
};
}, [scroll]);
@@ -211,7 +196,8 @@ export default function OptimizationSection() {
const leftBlur = useTransform(scroll, [0.3, 0.55], [0, 3]);
const leftFilter = useTransform(leftBlur, (b) => `blur(${b}px)`);
const rightOpacity = useTransform(scroll, [0.42, 0.66], [0.36, 1]);
const scanWidth = useTransform(scroll, [0, 1], ["0%", "100%"]);
// GPU transform (scaleX) instead of animating width, which forces layout each frame.
const scanScaleX = useTransform(scroll, [0, 1], [0, 1]);
const scanLineY = useTransform(scroll, [0.2, 0.42], ["8%", "92%"]);
const scanLineOpacity = useTransform(scroll, [0.18, 0.22, 0.42, 0.46], [0, 1, 1, 0]);
const dividerOpacity = useTransform(scroll, [0.45, 0.6], [0.15, 0.75]);
@@ -350,7 +336,7 @@ export default function OptimizationSection() {
{/* Scan progress bar */}
<div className="dm-opt-progress">
<div className="dm-opt-progress__track">
<motion.div className="dm-opt-progress__fill" style={{ width: scanWidth }} />
<motion.div className="dm-opt-progress__fill" style={{ scaleX: scanScaleX }} />
</div>
</div>
</header>
@@ -470,17 +456,20 @@ const styles = `
}
/* Animated subtle grid pattern */
.dm-opt-card::before {
content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
/* Expanded one tile beyond the (clipped) card so we can drift it via a GPU
transform instead of animating background-position (which repaints each frame). */
content: ""; position: absolute; inset: -60px; z-index: 0; pointer-events: none;
opacity: 0.035;
background-image:
linear-gradient(${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px),
linear-gradient(90deg, ${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px);
background-size: 60px 60px;
animation: dmOptGridDrift 25s linear infinite;
will-change: transform;
}
@keyframes dmOptGridDrift {
0% { background-position: 0 0; }
100% { background-position: 60px 60px; }
0% { transform: translate3d(0, 0, 0); }
100% { transform: translate3d(60px, 60px, 0); }
}
/* Radial center glow behind 3D scene */
.dm-opt-card::after {
@@ -669,7 +658,7 @@ const styles = `
background: ${rgba(COLORS.cyan, 0.10)}; max-width: 420px; margin: 0 auto;
}
.dm-opt-progress__fill {
height: 100%; border-radius: 999px;
height: 100%; width: 100%; transform-origin: left center; border-radius: 999px;
background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green});
box-shadow: 0 0 12px ${rgba(COLORS.cyan, 0.6)};
}
@@ -697,9 +686,10 @@ const styles = `
.dm-opt-panel {
pointer-events: auto; width: clamp(230px, 26vw, 340px);
padding: 18px 20px; border-radius: 20px;
background: ${rgba(COLORS.ink, 0.72)};
background: ${rgba(COLORS.ink, 0.92)};
border: 1px solid ${rgba(COLORS.slate, 0.22)};
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
/* backdrop blur removed — panel opacity is scroll-driven, so the blur was
recomputed every scroll frame; a near-opaque fill reads the same and is free. */
}
.dm-opt-panel--bad {
border-color: ${rgba(COLORS.red, 0.45)};
@@ -772,9 +762,9 @@ const styles = `
}
.dm-opt-metric {
position: relative; padding: 14px 14px 12px; border-radius: 16px;
background: ${rgba(COLORS.ink, 0.72)};
background: ${rgba(COLORS.ink, 0.92)};
border: 1px solid ${rgba(COLORS.slate, 0.25)};
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
/* backdrop blur removed — 5 metric cards animate on scroll; per-frame blur recompute was a hotspot. */
overflow: hidden;
opacity: 0; transform: translateY(22px);
animation: dmOptCardIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;

View File

@@ -0,0 +1,58 @@
import { rgba, phaseFromProgress, PHASES, PHASE_LABELS, type PhaseKey } from "./constants";
describe("optimization/constants — rgba()", () => {
it("converts a hex string to an rgba() string", () => {
expect(rgba("#22C55E", 0.5)).toBe("rgba(34, 197, 94, 0.5)");
expect(rgba("#000000", 1)).toBe("rgba(0, 0, 0, 1)");
expect(rgba("#FFFFFF", 0)).toBe("rgba(255, 255, 255, 0)");
});
it("is case-insensitive for hex digits", () => {
expect(rgba("#ffffff", 1)).toBe(rgba("#FFFFFF", 1));
});
});
describe("optimization/constants — phaseFromProgress()", () => {
it("returns the right phase at each threshold", () => {
expect(phaseFromProgress(PHASES.chaos)).toBe("chaos");
expect(phaseFromProgress(PHASES.scan)).toBe("scan");
expect(phaseFromProgress(PHASES.dissolve)).toBe("dissolve");
expect(phaseFromProgress(PHASES.optimize)).toBe("optimize");
expect(phaseFromProgress(PHASES.reorganize)).toBe("reorganize");
expect(phaseFromProgress(PHASES.metrics)).toBe("metrics");
});
it("returns the previous phase just below a threshold", () => {
expect(phaseFromProgress(PHASES.scan - 0.001)).toBe("chaos");
expect(phaseFromProgress(PHASES.dissolve - 0.001)).toBe("scan");
expect(phaseFromProgress(PHASES.metrics - 0.001)).toBe("reorganize");
});
it("handles the 0 and 1 extremes", () => {
expect(phaseFromProgress(0)).toBe("chaos");
expect(phaseFromProgress(1)).toBe("metrics");
});
it("only ever returns a defined phase key across a sweep", () => {
const keys = Object.keys(PHASES) as PhaseKey[];
for (let p = 0; p <= 1.0001; p += 0.02) {
expect(keys).toContain(phaseFromProgress(p));
}
});
});
describe("optimization/constants — phase tables", () => {
it("has strictly increasing phase thresholds", () => {
const vals = Object.values(PHASES);
for (let i = 1; i < vals.length; i++) {
expect(vals[i]).toBeGreaterThan(vals[i - 1]);
}
});
it("has a label for every phase key", () => {
for (const key of Object.keys(PHASES) as PhaseKey[]) {
expect(typeof PHASE_LABELS[key]).toBe("string");
expect(PHASE_LABELS[key].length).toBeGreaterThan(0);
}
});
});

View File

@@ -0,0 +1,99 @@
import { clamp01, lerp, remap, smoothstep, damp, seeded } from "./math";
describe("optimization/math — clamp01()", () => {
it("clamps to [0,1]", () => {
expect(clamp01(-2)).toBe(0);
expect(clamp01(0)).toBe(0);
expect(clamp01(0.37)).toBe(0.37);
expect(clamp01(1)).toBe(1);
expect(clamp01(5)).toBe(1);
});
});
describe("optimization/math — lerp()", () => {
it("interpolates linearly", () => {
expect(lerp(0, 10, 0)).toBe(0);
expect(lerp(0, 10, 1)).toBe(10);
expect(lerp(0, 10, 0.5)).toBe(5);
expect(lerp(2, 4, 0.25)).toBe(2.5);
});
it("extrapolates past the endpoints", () => {
expect(lerp(0, 10, 2)).toBe(20);
expect(lerp(0, 10, -1)).toBe(-10);
});
});
describe("optimization/math — remap()", () => {
it("remaps and clamps to the output range", () => {
expect(remap(5, 0, 10, 0, 100)).toBe(50);
expect(remap(-5, 0, 10, 0, 100)).toBe(0);
expect(remap(15, 0, 10, 0, 100)).toBe(100);
});
it("defaults the output range to [0,1]", () => {
expect(remap(2.5, 0, 10)).toBeCloseTo(0.25, 10);
});
it("guards against a zero-width input range (no NaN)", () => {
const v = remap(5, 2, 2, 0, 1);
expect(Number.isNaN(v)).toBe(false);
});
});
describe("optimization/math — smoothstep()", () => {
it("hits 0 and 1 at the edges and 0.5 at the middle", () => {
expect(smoothstep(0, 1, 0)).toBe(0);
expect(smoothstep(0, 1, 1)).toBe(1);
expect(smoothstep(0, 1, 0.5)).toBeCloseTo(0.5, 10);
});
it("clamps outside the edges", () => {
expect(smoothstep(0, 1, -1)).toBe(0);
expect(smoothstep(0, 1, 2)).toBe(1);
});
it("is monotonically non-decreasing", () => {
let prev = -Infinity;
for (let x = -0.2; x <= 1.2; x += 0.1) {
const v = smoothstep(0, 1, x);
expect(v).toBeGreaterThanOrEqual(prev);
prev = v;
}
});
it("guards against equal edges (no NaN)", () => {
expect(Number.isNaN(smoothstep(1, 1, 1))).toBe(false);
});
});
describe("optimization/math — damp()", () => {
it("does not move when dt is 0", () => {
expect(damp(3, 10, 5, 0)).toBe(3);
});
it("moves toward the target and converges with large lambda*dt", () => {
const once = damp(0, 10, 1, 0.5);
expect(once).toBeGreaterThan(0);
expect(once).toBeLessThan(10);
expect(damp(0, 10, 50, 1)).toBeCloseTo(10, 5);
});
});
describe("optimization/math — seeded()", () => {
it("is deterministic for a given input", () => {
expect(seeded(7)).toBe(seeded(7));
});
it("returns values within [0,1)", () => {
for (let i = 0; i < 200; i++) {
const v = seeded(i);
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThan(1);
}
});
it("produces different values for different seeds", () => {
expect(seeded(1)).not.toBe(seeded(2));
});
});

View File

@@ -74,8 +74,10 @@ export default function CompetitiveEdge() {
<div className="container">
{/* Section Header */}
<div className="comparison-header" ref={headerRef}>
<div className="advantage-badge">DoorMile Advantage</div>
<h2 className="moat-heading">WHERE DOORMILE SITS AND WHY IT WINS</h2>
<div className="advantage-eyebrow-container">
<span className="advantage-eyebrow">/ DoorMile wins/</span>
</div>
<h2 className="moat-heading" data-text="WHERE DOORMILE SITS AND WHY IT WINS">WHERE DOORMILE SITS AND WHY IT WINS</h2>
<p className="moat-desc">
A side-by-side technical capabilities comparison showing how operational fleet ownership and dynamic AI planning disrupt basic aggregators.
</p>
@@ -253,59 +255,61 @@ export default function CompetitiveEdge() {
/* Section Header Layout */
.comparison-header {
text-align: center;
text-align: left;
margin-bottom: 60px;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
/* DoorMile Advantage Badge */
.advantage-badge {
.advantage-eyebrow-container {
width: 100%;
border-bottom: 2px solid rgba(0, 0, 0, 0.16);
padding-bottom: 16px;
margin-bottom: 28px;
}
/* DoorMile Advantage Eyebrow */
.advantage-eyebrow {
font-family: 'Manrope', sans-serif;
font-weight: 800;
font-size: 0.75rem;
letter-spacing: 0.12em;
font-weight: 700;
font-size: 0.85rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #c8102e;
background: rgba(200, 16, 46, 0.06);
padding: 6px 14px;
border-radius: 30px;
margin: 0 auto 24px auto;
border: 1.5px solid rgba(200, 16, 46, 0.15);
color: #060606ff;
display: inline-block;
white-space: nowrap;
}
/* Centered heading with bottom accent underline */
/* Outlined heading style with clean duplicate layering hack */
.moat-heading {
font-family: 'Manrope', sans-serif;
font-size: clamp(2rem, 3.4vw, 3.2rem); /* Slightly enlarged for premium visual weight */
font-weight: 800;
line-height: 1.15;
color: #111111;
margin: 0 auto 20px auto;
letter-spacing: -0.03em;
position: relative;
font-family: var(--font-syne), 'Syne', sans-serif !important;
font-size: clamp(2.4rem, 6.8vw, 6.6rem) !important;
font-weight: 800 !important;
line-height: 1.1 !important;
color: #fafafa !important; /* solid background color to cover inner overlapping outlines */
margin: 0 0 24px 0;
letter-spacing: -0.02em;
text-transform: uppercase;
word-wrap: break-word;
overflow-wrap: break-word;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
}
/* Centered horizontal red underline accent decoration */
.moat-heading::after {
content: "";
display: block;
width: 72px;
height: 4px;
background: #c8102e;
margin-top: 18px;
border-radius: 2px;
content: attr(data-text) !important;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -1;
color: transparent !important;
-webkit-text-stroke: 2.2px #c8102e;
-webkit-text-fill-color: transparent !important;
pointer-events: none;
display: block !important; /* override any old display: none */
}
.moat-desc {
@@ -313,9 +317,9 @@ export default function CompetitiveEdge() {
font-size: 1.05rem;
line-height: 1.65;
color: #585c67;
margin: 16px auto 0 auto !important;
max-width: 760px !important;
text-align: center !important;
margin: 16px 0 0 0 !important;
max-width: 820px !important;
text-align: left !important;
}
/* Spacious table styling wrapper (100% width on Desktop) */

View File

@@ -1,405 +0,0 @@
"use client";
import React, { useState } from "react";
import Image from "next/image";
import { ScrollReveal } from "@/animations/Reveal";
import emailjs from "@emailjs/browser";
// Type definitions for EmailJS template parameters
interface EmailJSTemplateParams extends Record<string, unknown> {
name: string;
email: string;
phone: string;
company: string;
subject: string;
message: string;
}
export default function ContactForm() {
const socialIconSpacing = {
"--grid-column-gap": "52px",
"--grid-row-gap": "18px",
columnGap: "52px",
rowGap: "18px",
} as React.CSSProperties;
const [formData, setFormData] = useState({
fullName: "",
email: "",
subject: "",
message: "",
});
const [formStatus, setFormStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
const [errorMessage, setErrorMessage] = useState<string>("");
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
// Pre-submission validation function
const validateForm = (): string | null => {
if (!formData.fullName.trim()) {
return "Full name is required.";
}
if (formData.fullName.trim().length < 2) {
return "Full name must be at least 2 characters.";
}
if (!formData.email.trim()) {
return "Email is required.";
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email.trim())) {
return "Please enter a valid email address.";
}
if (!formData.subject.trim()) {
return "Subject is required.";
}
if (!formData.message.trim()) {
return "Message is required.";
}
if (formData.message.trim().length < 10) {
return "Message must be at least 10 characters.";
}
return null;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMessage("");
// Validate inputs before submitting to EmailJS
const validationError = validateForm();
if (validationError) {
setErrorMessage(validationError);
setFormStatus("error");
return;
}
setFormStatus("submitting");
// Fetch credentials from Next.js environment variables
const serviceId = process.env.NEXT_PUBLIC_EMAILJS_SERVICE_ID;
const templateId = process.env.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID;
const publicKey = process.env.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY;
if (!serviceId || !templateId || !publicKey) {
console.error("EmailJS credentials are not configured in environment variables.");
setErrorMessage("Email service configuration error. Please contact the administrator.");
setFormStatus("error");
return;
}
try {
// Map form fields to EmailJS template variables matching user's template config
const templateParams: EmailJSTemplateParams = {
name: formData.fullName.trim(),
email: formData.email.trim(),
phone: "", // phone is not available in the current contact form UI
company: "", // company is not available in the current contact form UI
subject: formData.subject.trim(),
message: formData.message.trim(),
};
// Send email via EmailJS API
await emailjs.send(serviceId, templateId, templateParams, publicKey);
setFormStatus("success");
// Reset form fields after successful submission
setFormData({ fullName: "", email: "", subject: "", message: "" });
} catch (error) {
console.error("EmailJS Error:", error);
setErrorMessage("Failed to send message. Please try again later.");
setFormStatus("error");
}
};
return (
<div className="elementor elementor-6585">
<style dangerouslySetInnerHTML={{ __html: `
/* ---- Clean contact form (scoped to this section) ---- */
.elementor-6585 .elementor-element.elementor-element-a5c503d {
--padding-top: 60px;
--padding-bottom: 60px;
--padding-left: 60px;
--padding-right: 60px;
}
.elementor-6585 .elementor-element.elementor-element-0e6fedf > .elementor-widget-container {
margin: 4px 0 0 0;
}
/* drop the legacy notched / floating-label borders */
.elementor-6585 .wpforms-form .logico-form-field:before,
.elementor-6585 .wpforms-form .logico-form-field:after,
.elementor-6585 .wpforms-form .logico-label-placeholder { display: none !important; }
/* even field rhythm */
.elementor-6585 .wpforms-form .wpforms-field-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.elementor-6585 .wpforms-form .wpforms-field { padding: 0 !important; margin: 0 !important; }
/* labels stay for screen readers; placeholders carry the visible text */
.elementor-6585 .wpforms-form .wpforms-field-label {
position: absolute !important;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
/* clean rounded inputs */
.elementor-6585 .wpforms-form input[type="text"],
.elementor-6585 .wpforms-form input[type="email"],
.elementor-6585 .wpforms-form textarea {
width: 100%;
border: 1px solid #e3e3e3 !important;
border-radius: 12px !important;
padding: 16px 20px !important;
font-size: 15px;
line-height: 1.5;
color: #111;
background: #fff;
box-shadow: none !important;
transition: border-color .25s ease;
}
.elementor-6585 .wpforms-form textarea { min-height: 150px; resize: vertical; }
.elementor-6585 .wpforms-form input::placeholder,
.elementor-6585 .wpforms-form textarea::placeholder { color: #9a9a9a; opacity: 1; }
.elementor-6585 .wpforms-form input:focus,
.elementor-6585 .wpforms-form textarea:focus { border-color: #c01227 !important; outline: none; }
.elementor-6585 .wpforms-form .wpforms-submit-container { padding-top: 26px !important; }
@media (max-width: 1020px) {
.elementor-6585 .elementor-element.elementor-element-a5c503d {
--padding-top: 40px;
--padding-bottom: 40px;
--padding-left: 32px;
--padding-right: 32px;
}
}
` }} />
<div className="elementor-element elementor-element-3cd920c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="3cd920c" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-b29b8fc e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-child" data-id="b29b8fc" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
<div className="e-con-inner">
<div className="elementor-element elementor-element-ef6fa6d e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="ef6fa6d" data-element_type="container" data-e-type="container">
{/* Left Dark Panel */}
<div className="elementor-element elementor-element-9990148 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9990148" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-8899bdf elementor-absolute elementor-widget elementor-widget-image" data-id="8899bdf" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_position&quot;:&quot;absolute&quot;}" data-widget_type="image.default">
<div className="elementor-widget-container">
<Image width={965} height={474} src="/images/bg-map.png" className="attachment-full size-full wp-image-1148" alt="" />
</div>
</div>
<div className="elementor-element elementor-element-a0e7516 elementor-widget elementor-widget-logico_heading" data-id="a0e7516" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">/ get in touch /</div>
</div>
</div>
<div className="elementor-element elementor-element-51cdf4f elementor-widget elementor-widget-logico_heading" data-id="51cdf4f" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<ScrollReveal delay={0.1} duration={0.8} yOffset={25}>
<h3 className="logico-title">
We are always ready to help you and answer your questions
</h3>
</ScrollReveal>
</div>
</div>
<div className="elementor-element elementor-element-670d1b2 elementor-widget elementor-widget-text-editor" data-id="670d1b2" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
<div className="elementor-widget-container">
<p>Connecting businesses with fast, secure, smart deliveries.</p>
</div>
</div>
<div className="elementor-element elementor-element-2631b42 e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-child" data-id="2631b42" data-element_type="container" data-e-type="container">
<div className="e-con-inner">
<div className="elementor-element elementor-element-df89993 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="df89993" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-69b6892 elementor-widget elementor-widget-logico_heading" data-id="69b6892" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Call Center</div>
</div>
</div>
<div className="elementor-element elementor-element-87be926 elementor-widget elementor-widget-text-editor" data-id="87be926" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
<div className="elementor-widget-container">
<p>Tel : +91 86886 97941</p>
</div>
</div>
</div>
<div className="elementor-element elementor-element-f5d8e63 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="f5d8e63" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-774e540 elementor-widget elementor-widget-logico_heading" data-id="774e540" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Our Location</div>
</div>
</div>
<div className="elementor-element elementor-element-9c1cf03 elementor-widget elementor-widget-text-editor" data-id="9c1cf03" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
<div className="elementor-widget-container">
<p>5th Floor, Vision Ultima, Street No.3, Jayabheri Enclave, Gachibowli, Hyderabad, Telangana 500032.</p>
</div>
</div>
</div>
</div>
</div>
<div className="elementor-element elementor-element-645be8d e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-child" data-id="645be8d" data-element_type="container" data-e-type="container">
<div className="e-con-inner">
<div className="elementor-element elementor-element-a96d151 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="a96d151" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-37e647f elementor-widget elementor-widget-logico_heading" data-id="37e647f" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Email</div>
</div>
</div>
<div className="elementor-element elementor-element-ba67644 elementor-widget elementor-widget-text-editor" data-id="ba67644" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
<div className="elementor-widget-container">
<p>
<a href="mailto:care@doormile.com">care@doormile.com</a>
</p>
</div>
</div>
</div>
<div className="elementor-element elementor-element-9ba4b82 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9ba4b82" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-e9a5d79 elementor-widget elementor-widget-logico_heading" data-id="e9a5d79" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Social network</div>
</div>
</div>
<div className="elementor-element elementor-element-a6bccba elementor-shape-square elementor-grid-0 elementor-widget elementor-widget-social-icons" data-id="a6bccba" data-element_type="widget" data-e-type="widget" data-widget_type="social-icons.default">
<div className="elementor-widget-container">
<div className="elementor-social-icons-wrapper elementor-grid" role="list" style={socialIconSpacing}>
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-facebook-f elementor-repeater-item-3fbe893" href="https://www.facebook.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">Facebook</span>
<svg aria-hidden="true" className="e-font-icon-svg e-fab-facebook-f" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
<path d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z"></path>
</svg>
</a>
</span>
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-x-twitter elementor-repeater-item-64ac94e" href="https://x.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">X</span>
<svg aria-hidden="true" className="e-font-icon-svg e-fab-x-twitter" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path>
</svg>
</a>
</span>
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-linkedin-in elementor-repeater-item-38e1bcc" href="https://www.linkedin.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">LinkedIn</span>
<svg aria-hidden="true" className="e-font-icon-svg e-fab-linkedin-in" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
<path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"></path>
</svg>
</a>
</span>
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
<a className="elementor-icon elementor-social-icon elementor-social-icon-youtube elementor-repeater-item-b0d5e1f" href="https://www.youtube.com" target="_blank" rel="noopener noreferrer">
<span className="elementor-screen-only">YouTube</span>
<svg aria-hidden="true" className="e-font-icon-svg e-fab-youtube" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg">
<path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"></path>
</svg>
</a>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Right White Form Card */}
<div className="elementor-element elementor-element-a5c503d e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="a5c503d" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
<div className="elementor-element elementor-element-535776a elementor-widget elementor-widget-logico_heading" data-id="535776a" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Get in Touch</div>
</div>
</div>
<div className="elementor-element elementor-element-0e6fedf elementor-widget elementor-widget-logico_wpforms" data-id="0e6fedf" data-element_type="widget" data-e-type="widget" data-widget_type="logico_wpforms.default">
<div className="elementor-widget-container">
<div className="logico-wpforms-widget">
<div className="wpforms-container wpforms-render-modern" id="wpforms-369-contact">
<form id="wpforms-form-369-contact" className="wpforms-validate wpforms-form" onSubmit={handleSubmit} noValidate>
<div className="wpforms-field-container">
<div className="wpforms-field logico-form-field">
<label className="wpforms-field-label" htmlFor="contact-field-name">Full name</label>
<input
type="text"
id="contact-field-name"
name="fullName"
placeholder="Full name"
value={formData.fullName}
onChange={handleInputChange}
required
/>
</div>
<div className="wpforms-field logico-form-field">
<label className="wpforms-field-label" htmlFor="contact-field-email">Email</label>
<input
type="email"
id="contact-field-email"
name="email"
placeholder="Email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
<div className="wpforms-field logico-form-field">
<label className="wpforms-field-label" htmlFor="contact-field-subject">Subject</label>
<input
type="text"
id="contact-field-subject"
name="subject"
placeholder="Subject"
value={formData.subject}
onChange={handleInputChange}
required
/>
</div>
<div className="wpforms-field logico-form-field">
<label className="wpforms-field-label" htmlFor="contact-field-message">Message</label>
<textarea
id="contact-field-message"
name="message"
placeholder="Message"
value={formData.message}
onChange={handleInputChange}
required
></textarea>
</div>
</div>
<div className="wpforms-submit-container">
<button
type="submit"
id="wpforms-submit-369-contact"
className="logico-alter-button wpforms-submit"
disabled={formStatus === "submitting"}
>
{formStatus === "submitting" ? "Sending..." : "Send a message"}
</button>
{formStatus === "success" && (
<div style={{ color: "#4caf50", marginTop: "10px", fontSize: "14px" }}>
Message sent successfully!
</div>
)}
{formStatus === "error" && (
<div style={{ color: "#f44336", marginTop: "10px", fontSize: "14px" }}>
{errorMessage || "Something went wrong. Please try again."}
</div>
)}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,138 +0,0 @@
"use client";
import React, { useEffect, useRef } from "react";
/**
* EV-Native Design background — drifting SQUARE particles with 3D depth.
* Each square has a depth (0.31): nearer squares are larger, brighter, and
* parallax further on mouse-move + a slow auto-sway, creating a layered 3D
* field. Mix of brand-red (#dc2626) and soft gray/white squares. Full section,
* pointer-events:none, behind content.
*/
export default function EVParticles() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const parent = canvas?.parentElement;
if (!canvas || !parent) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
const COUNT = 120;
type P = { x: number; y: number; vx: number; vy: number; s: number; red: boolean; a: number; d: number };
let particles: P[] = [];
let w = 0, h = 0, raf = 0, startTs = 0;
// mouse parallax (canvas is pointer-events:none, so track on window)
const mouse = { x: 0, y: 0 };
const cur = { x: 0, y: 0 };
const onMove = (e: MouseEvent) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = (e.clientY / window.innerHeight) * 2 - 1;
};
const init = () => {
particles = [];
for (let i = 0; i < COUNT; i++) {
const t = Math.random();
const s = t > 0.92 ? 12 + Math.random() * 6 : t > 0.68 ? 7 + Math.random() * 5 : 3 + Math.random() * 4;
const red = Math.random() < 0.3;
particles.push({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.22,
vy: (Math.random() - 0.5) * 0.22,
s,
red,
a: red ? Math.random() * 0.4 + 0.35 : Math.random() * 0.28 + 0.12,
d: Math.random() * 0.7 + 0.3, // depth 0.3 (far) .. 1 (near)
});
}
};
const resize = () => {
const rect = parent.getBoundingClientRect();
w = Math.max(1, rect.width);
h = Math.max(1, rect.height);
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
canvas.style.width = w + "px";
canvas.style.height = h + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!particles.length) init();
};
const render = (time: number, move: boolean) => {
ctx.clearRect(0, 0, w, h);
// ease the parallax origin toward the pointer
cur.x += (mouse.x - cur.x) * 0.04;
cur.y += (mouse.y - cur.y) * 0.04;
// slow auto-sway so the 3D depth animates even without mouse input
const swayX = Math.sin(time * 0.00026) * 0.6;
const swayY = Math.cos(time * 0.0002) * 0.45;
const ox = (cur.x + swayX) * 34;
const oy = (cur.y + swayY) * 26;
for (const p of particles) {
if (move) {
p.x += p.vx * p.d;
p.y += p.vy * p.d;
const m = p.s + 6;
if (p.x < -m) p.x = w + m;
else if (p.x > w + m) p.x = -m;
if (p.y < -m) p.y = h + m;
else if (p.y > h + m) p.y = -m;
}
// depth-scaled size + parallax offset (near squares move/scale more)
const size = p.s * (0.55 + p.d * 0.75);
const dx = p.x + ox * p.d;
const dy = p.y + oy * p.d;
const alpha = p.a * (0.55 + p.d * 0.45);
if (p.red) {
ctx.shadowColor = "rgba(220,38,38,0.55)";
ctx.shadowBlur = 8 * p.d;
ctx.fillStyle = `rgba(220,38,38,${alpha})`;
} else {
ctx.shadowBlur = 0;
ctx.fillStyle = `rgba(200,202,210,${alpha})`;
}
ctx.fillRect(dx - size / 2, dy - size / 2, size, size);
}
ctx.shadowBlur = 0;
};
const loop = (ts: number) => {
if (!startTs) startTs = ts;
render(ts - startTs, true);
raf = requestAnimationFrame(loop);
};
resize();
init();
if (reduced) {
render(0, false);
} else {
window.addEventListener("mousemove", onMove, { passive: true });
raf = requestAnimationFrame(loop);
}
const ro = new ResizeObserver(() => {
resize();
if (reduced) render(0, false);
});
ro.observe(parent);
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
window.removeEventListener("mousemove", onMove);
};
}, []);
return <canvas ref={canvasRef} className="evnd__canvas" aria-hidden="true" />;
}

View File

@@ -8,46 +8,50 @@ if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
const PILLS: { value: string; label: string }[] = [
{ value: "100%", label: "Electric Fleet" },
{ value: "Live", label: "Route Sync" },
{ value: "Real-time", label: "Battery Monitoring" },
];
const MINI_STATS: { value: number; decimals?: number; suffix: string; label: string }[] = [
{ value: 94, suffix: "K+", label: "Routes Optimised" },
{ value: 23, suffix: "%", label: "Avg Battery Saved" },
{ value: 1.4, decimals: 1, suffix: "x", label: "Charging Stops Saved" },
];
const FEATURES: { icon: string; title: string; desc: string }[] = [
const FEATURES: { icon: React.ReactNode; title: string; desc: string }[] = [
{
icon: "⚡",
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
),
title: "Battery-Aware Routing",
desc: "Battery level, health, and degradation are first-class inputs to route optimization — not afterthoughts.",
},
{
icon: "🔌",
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 10h-1.28A6 6 0 0 0 12 5V3M12 5V3M6 10h1.28A6 6 0 0 0 12 5M12 18v2M12 18v2M8 10v6a4 4 0 0 0 8 0v-6" />
</svg>
),
title: "Charging Integration",
desc: "Seamlessly integrate charging stops without compromising delivery windows or SLA commitments.",
},
{
icon: "⛰",
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m8 3 4 8 5-5 5 15H2L8 3z" />
</svg>
),
title: "Energy-Optimized Paths",
desc: "Factor in elevation, speed limits, payload weight, and live weather for maximum range efficiency.",
},
{
icon: "🛡",
icon: (
<svg className="evnd-icon" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
),
title: "Predictable Operations",
desc: "EVs become predictable assets, not operational risks. Full visibility from depot to doorstep.",
},
];
const BOTTOM_STATS: { value: number; decimals?: number; suffix: string; label: string }[] = [
{ value: 120, suffix: "K+", label: "Deliveries Completed" },
{ value: 98, suffix: "%", label: "On-Time Rate" },
{ value: 31, suffix: "%", label: "Range Efficiency Gain" },
{ value: 340, suffix: "ms", label: "Avg Route Calc Time" },
{ value: 99.9, decimals: 1, suffix: "%", label: "SLA Compliance" },
{ value: 42, suffix: "%", label: "Distance Saved" },
{ value: 37, suffix: "%", label: "Fewer Vehicles" },
{ value: 45, suffix: "ms", label: "Dispatch Latency" },
];
/** Count-up that fires once when scrolled ~20% into view (ease-out cubic). */
@@ -143,7 +147,7 @@ export default function EVSection() {
<style dangerouslySetInnerHTML={{ __html: `
/* ============================================================
EV-Native Design — redesigned section
bg #0d0d0d · red #dc2626 / #ef4444 · Syne + DM Sans
bg #080808 · red #ef4444 · Manrope
============================================================ */
#evnd, #evnd * { font-family: "Manrope", Sans-serif !important; }
@@ -152,12 +156,12 @@ export default function EVSection() {
position: relative;
isolation: isolate;
overflow: hidden;
background: #0d0d0d;
background: #080808;
/* flat top so it connects seamlessly to the banner above; rounded
bottom only, and no top margin so there is no white gap */
border-radius: 0 0 clamp(16px, 2vw, 28px) clamp(16px, 2vw, 28px);
margin: 0 0 clamp(28px, 5vw, 64px);
padding: 56px 48px 64px;
padding: 64px 48px clamp(48px, 6vw, 80px);
}
/* subtle diagonal light band for depth (matches reference) */
.evnd::before {
@@ -166,181 +170,206 @@ export default function EVSection() {
inset: 0;
z-index: 0;
pointer-events: none;
background: linear-gradient(120deg, transparent 28%, rgba(255,255,255,0.025) 50%, transparent 72%);
}
.evnd__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
background: linear-gradient(120deg, transparent 28%, rgba(255,255,255,0.015) 50%, transparent 72%);
}
.evnd__inner { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; }
/* ---- TOP ROW ---- */
.evnd__top {
/* ---- MAIN GRID ---- */
.evnd__grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 44px;
grid-template-columns: 1.15fr 1fr;
gap: clamp(32px, 4vw, 56px);
align-items: center;
margin-bottom: 48px;
}
.evnd__left {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.evnd__right {
display: flex;
flex-direction: column;
}
.evnd__eyebrow {
display: inline-flex;
align-items: center;
gap: 12px;
color: #dc2626 !important;
font-weight: 700;
color: #ef4444 !important;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 13px;
margin-bottom: 16px;
margin-bottom: 20px;
}
.evnd__eyebrow::before { content: ''; width: 24px; height: 2px; background: #dc2626; }
.evnd__eyebrow::before {
content: '';
width: 16px;
height: 2px;
background: #ef4444;
}
.evnd__title {
color: #fff !important;
font-weight: 800 !important;
font-size: clamp(30px, 4.4vw, 56px) !important;
line-height: 1.04 !important;
font-size: clamp(32px, 3.8vw, 48px) !important;
line-height: 1.15 !important;
letter-spacing: -0.01em;
margin: 0;
margin: 0 0 36px 0;
}
.evnd__title .accent { color: #ef4444 !important; }
.evnd__pills { display: flex; flex-direction: column; gap: 12px; }
.evnd__pill {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 20px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 100px;
@media (min-width: 768px) {
.evnd__title {
white-space: nowrap;
}
}
.evnd__pill .dot {
flex: 0 0 auto;
width: 8px; height: 8px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 8px #22c55e;
animation: evndBlink 1.4s ease-in-out infinite;
.evnd__title .accent {
color: #ef4444 !important;
}
.evnd__pill b { color: #ef4444 !important; font-weight: 800; font-size: 15px; }
.evnd__pill span { color: rgba(255,255,255,0.62) !important; font-size: 13px; }
/* ---- MAIN GRID ---- */
.evnd__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: start; }
.evnd__media { position: relative; }
.evnd__media {
position: relative;
width: 100%;
}
.evnd__glow {
position: absolute;
left: 50%; bottom: -4%;
width: 72%; height: 64px;
width: 80%; height: 80px;
transform: translateX(-50%);
background: radial-gradient(50% 50% at 50% 50%, rgba(220,38,38,0.5), transparent 72%);
filter: blur(30px);
background: radial-gradient(50% 50% at 50% 50%, rgba(239,68,68,0.3), transparent 72%);
filter: blur(35px);
z-index: 0;
animation: evndGlow 4s ease-in-out infinite;
}
.evnd__imgwrap { position: relative; z-index: 1; animation: evndFloat 7s ease-in-out infinite; will-change: transform; }
.evnd__imgwrap {
position: relative;
z-index: 1;
overflow: hidden;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 30px 60px -25px rgba(0,0,0,0.85);
}
.evnd__img {
display: block;
width: 100%;
height: auto;
border-radius: 14px;
box-shadow: 0 30px 60px -25px rgba(0,0,0,0.7);
object-fit: cover;
transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
}
.evnd__imgwrap:hover .evnd__img {
transform: scale(1.03);
}
/* Badge overlay styling */
.evnd__badge {
position: absolute;
z-index: 2;
display: flex;
flex-direction: column;
gap: 2px;
padding: 10px 14px;
background: rgba(10,10,10,0.88);
border: 1px solid rgba(255,255,255,0.1);
padding: 12px 16px;
background: rgba(13,13,13,0.72);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.evnd__badge b { color: #ef4444 !important; font-weight: 800; font-size: 24px; line-height: 1; }
.evnd__badge span { color: rgba(255,255,255,0.55) !important; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; }
.evnd__badge--tl { top: 14px; left: 14px; }
.evnd__badge--br { bottom: 14px; right: 14px; }
.evnd__ministats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 20px; }
.evnd__mini {
position: relative;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px;
padding: 18px 14px 14px;
overflow: hidden;
.evnd__badge b {
color: #ef4444 !important;
font-weight: 800;
font-size: 24px;
line-height: 1;
}
.evnd__mini::before {
content: '';
position: absolute; top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, #dc2626, transparent);
.evnd__badge span {
color: rgba(255,255,255,0.7) !important;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.evnd__mini b { display: block; color: #fff !important; font-weight: 800; font-size: clamp(20px, 2.3vw, 28px); line-height: 1; margin-bottom: 6px; }
.evnd__mini span { color: rgba(255,255,255,0.5) !important; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; line-height: 1.3; display: block; }
.evnd__badge--tl { top: 20px; left: 20px; }
.evnd__badge--br { bottom: 20px; right: 20px; }
/* ---- Feature cards ---- */
.evnd__features { display: flex; flex-direction: column; gap: 10px; }
.evnd__features {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
justify-content: space-between;
}
.evnd-feature {
position: relative;
display: grid;
grid-template-columns: 40px 1fr auto;
gap: 16px;
grid-template-columns: 48px 1fr auto;
gap: 20px;
align-items: start;
background: rgba(255,255,255,0.028);
border: 1px solid rgba(255,255,255,0.07);
border-radius: 12px;
padding: 18px 20px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 16px;
padding: 24px;
overflow: hidden;
transition: background-color 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(.25,1,.5,1);
transition: background-color 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(.25,1,.5,1);
}
.evnd-feature::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: #dc2626;
background: #ef4444;
transform: scaleY(0);
transform-origin: bottom;
transition: transform 0.35s ease;
transition: transform 0.4s ease;
}
.evnd-feature:hover {
background: rgba(239,68,68,0.03);
border-color: rgba(239,68,68,0.2);
transform: translateY(-2px);
}
.evnd-feature:hover { background: rgba(220,38,38,0.06); border-color: rgba(220,38,38,0.25); transform: translateX(4px); }
.evnd-feature:hover::before { transform: scaleY(1); }
.evnd-feature__icon {
width: 40px; height: 40px;
.evnd-feature__icon-container {
width: 48px; height: 48px;
display: flex; align-items: center; justify-content: center;
background: rgba(220,38,38,0.1);
border: 1px solid rgba(220,38,38,0.2);
border-radius: 10px;
font-size: 18px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.evnd-feature:hover .evnd-feature__icon-container {
background: rgba(239,68,68,0.08);
border-color: rgba(239,68,68,0.25);
}
.evnd-icon {
width: 22px;
height: 22px;
display: block;
}
.evnd-feature__title {
color: #fff !important;
font-weight: 700;
font-size: 15px !important;
text-transform: uppercase;
letter-spacing: 0.04em;
margin: 3px 0 7px;
letter-spacing: 0.05em;
margin: 4px 0 8px;
transition: color 0.3s ease;
}
.evnd-feature:hover .evnd-feature__title { color: #ef4444 !important; }
.evnd-feature__desc {
color: rgba(255,255,255,0.75) !important;
color: rgba(255,255,255,0.65) !important;
font-weight: 400 !important;
font-size: 14.5px !important;
line-height: 1.65 !important;
font-size: 14px !important;
line-height: 1.6 !important;
margin: 0;
}
.evnd-feature__arrow {
color: rgba(255,255,255,0.2);
font-size: 14px;
color: rgba(255,255,255,0.25);
font-size: 16px;
align-self: flex-start;
margin-top: 4px;
transition: color 0.3s ease, transform 0.3s ease;
}
.evnd-feature:hover .evnd-feature__arrow { color: #ef4444; transform: translate(3px, -3px); }
@@ -349,47 +378,69 @@ export default function EVSection() {
.evnd__bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: rgba(255,255,255,0.06);
border-radius: 12px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 16px;
overflow: hidden;
margin-top: 40px;
margin-top: 60px;
padding: 38px 0;
}
.evnd__bar-item {
background: #0d0d0d;
padding: 24px 22px;
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
gap: 12px;
align-items: center;
justify-content: center;
text-align: center;
padding: 12px 24px;
}
.evnd__bar-item .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #dc2626;
box-shadow: 0 0 8px rgba(220,38,38,0.85);
animation: evndBlink 1.4s ease-in-out infinite;
.evnd__bar-item:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 15%;
height: 70%;
width: 1px;
background: rgba(255, 255, 255, 0.08);
}
.evnd__bar-val {
color: #ef4444 !important;
font-weight: 800;
font-size: clamp(32px, 4vw, 56px);
line-height: 1;
}
.evnd__bar-label {
color: #fff !important;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: none;
opacity: 0.9;
}
.evnd__bar-item b { color: #fff !important; font-weight: 800; font-size: clamp(22px, 2.6vw, 30px); line-height: 1; }
.evnd__bar-item span { color: rgba(255,255,255,0.45) !important; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; }
@keyframes evndFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
@keyframes evndGlow { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
@keyframes evndBlink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
@keyframes evndGlow { 0%,100% { opacity: 0.75; } 50% { opacity: 1; } }
/* ---- Responsive ---- */
@media (max-width: 900px) {
.evnd { padding: 48px 28px 52px; }
.evnd__top { grid-template-columns: 1fr; gap: 28px; margin-bottom: 36px; }
.evnd__grid { grid-template-columns: 1fr; gap: 36px; }
@media (max-width: 991px) {
.evnd { padding: 48px 32px 56px; }
.evnd__grid { grid-template-columns: 1fr; gap: 40px; }
.evnd__title { margin-bottom: 28px; }
.evnd__features { gap: 14px; }
}
@media (max-width: 600px) {
.evnd { padding: 40px 18px 44px; }
.evnd__bar { grid-template-columns: repeat(2, 1fr); }
.evnd__pill { padding: 11px 16px; }
@media (max-width: 767px) {
.evnd__bar { grid-template-columns: repeat(2, 1fr); gap: 24px 0; padding: 24px 0; }
.evnd__bar-item:nth-child(even)::after { display: none; }
.evnd__bar-item:nth-child(2)::after { display: none; }
.evnd__bar-item { padding: 12px 16px; }
}
@media (prefers-reduced-motion: reduce) {
.evnd__imgwrap, .evnd__glow, .evnd__pill .dot, .evnd__bar-item .dot { animation: none !important; }
@media (max-width: 480px) {
.evnd { padding: 40px 16px 48px; }
.evnd__bar { grid-template-columns: 1fr; gap: 28px 0; }
.evnd__bar-item::after { display: none !important; }
.evnd__badge { padding: 8px 12px; }
.evnd__badge b { font-size: 20px; }
}
`}} />
@@ -441,24 +492,10 @@ export default function EVSection() {
{/* ===== EV-Native Design (redesigned) ===== */}
<section className="evnd" id="evnd" aria-label="EV-Native Design">
<div className="evnd__inner">
{/* TOP ROW */}
<div className="evnd__top">
<div className="evnd__head">
<span className="evnd__eyebrow">/ EV-Native Design /</span>
<div className="evnd__title">
BUILT FOR ELECTRIC. <span className="accent">NOT ADAPTED.</span>
</div>
</div>
<div className="evnd__pills">
{PILLS.map((p) => (
<div className="evnd__pill" key={p.label}>
<span className="dot" />
<b>{p.value}</b>
<span>{p.label}</span>
</div>
))}
</div>
</div>
<span className="evnd__eyebrow">/ EV-Native Design /</span>
<h2 className="evnd__title">
BUILT FOR ELECTRIC. <span className="accent">NOT ADAPTED.</span>
</h2>
{/* MAIN GRID */}
<div className="evnd__grid">
@@ -471,37 +508,32 @@ export default function EVSection() {
<img className="evnd__img" src="/images/premium-ev-van.png" alt="DoorMile electric delivery van" decoding="async" />
<div className="evnd__badge evnd__badge--tl">
<b>100%</b>
<span>Electric Fleet</span>
<span>ELECTRIC FLEET</span>
</div>
<div className="evnd__badge evnd__badge--br">
<b>&minus;40%</b>
<span>Cost / Mile</span>
<b>-40%</b>
<span>COST / MILE</span>
</div>
</div>
</div>
<div className="evnd__ministats">
{MINI_STATS.map((s) => (
<div className="evnd__mini" key={s.label}>
<CountUp value={s.value} decimals={s.decimals} suffix={s.suffix} />
<span>{s.label}</span>
</div>
))}
</div>
</div>
{/* Right column */}
<div className="evnd__features">
{FEATURES.map((f) => (
<div className="evnd-feature" key={f.title}>
<span className="evnd-feature__icon" aria-hidden="true">{f.icon}</span>
<div className="evnd-feature__body">
<div className="evnd-feature__title">{f.title}</div>
<p className="evnd-feature__desc">{f.desc}</p>
<div className="evnd__right">
<div className="evnd__features">
{FEATURES.map((f) => (
<div className="evnd-feature" key={f.title}>
<div className="evnd-feature__icon-container" aria-hidden="true">
{f.icon}
</div>
<div className="evnd-feature__body">
<div className="evnd-feature__title">{f.title}</div>
<p className="evnd-feature__desc">{f.desc}</p>
</div>
<span className="evnd-feature__arrow" aria-hidden="true"></span>
</div>
<span className="evnd-feature__arrow" aria-hidden="true"></span>
</div>
))}
))}
</div>
</div>
</div>
@@ -509,9 +541,8 @@ export default function EVSection() {
<div className="evnd__bar">
{BOTTOM_STATS.map((s) => (
<div className="evnd__bar-item" key={s.label}>
<span className="dot" />
<CountUp value={s.value} decimals={s.decimals} suffix={s.suffix} />
<span>{s.label}</span>
<span className="evnd__bar-label">{s.label}</span>
<CountUp value={s.value} decimals={s.decimals} suffix={s.suffix} className="evnd__bar-val" />
</div>
))}
</div>

View File

@@ -25,15 +25,16 @@ export default function IndustrySolutions() {
style={{marginLeft: "50px"}}
>
<div className="elementor-widget-container" style={{ margin: "30px 0 0 0"}}>
<h3 className="logico-title" style={{ fontSize: "clamp(36px, 5.5vw, 62px)", lineHeight: "1.1", fontWeight: 800, textTransform: "uppercase", paddingRight: "clamp(20px, 8vw, 120px)" }}>
<style dangerouslySetInnerHTML={{ __html: `
@media (min-width: 1024px) {
.industry-title-single-line {
white-space: nowrap !important;
}
}
`}} />
<h3 className="logico-title industry-title-single-line" style={{ fontSize: "clamp(28px, 3.5vw, 48px)", lineHeight: "1.1", fontWeight: 800, textTransform: "uppercase" }}>
<ScrollReveal delay={0.05} duration={0.8} yOffset={25}>
<span className="block">Smart solutions built</span>
</ScrollReveal>
<ScrollReveal delay={0.15} duration={0.8} yOffset={25}>
<span className="block">exclusively for your</span>
</ScrollReveal>
<ScrollReveal delay={0.25} duration={0.8} yOffset={25}>
<span className="block" style={{ color: "#c01227" }}>industry</span>
Smart solutions built exclusively for your <span style={{ color: "#c01227" }}>industry</span>
</ScrollReveal>
</h3>
</div>

View File

@@ -1,498 +0,0 @@
"use client";
import React, { useState } from "react";
import IndustryWorldMap from "./IndustryWorldMap";
type Tab = "challenges" | "solutions";
interface Industry {
id: string;
tab: string;
eyebrow: string;
title: string;
image: string;
alt: string;
desc: string;
chips: [string, string];
challenges: string[];
solutions: string[];
}
const INDUSTRIES: Industry[] = [
{
id: "fmcg",
tab: "FMCG",
eyebrow: "Fast-Moving Consumer Goods",
title: "FMCG",
image: "/images/tab-pic-1-solution.jpeg",
alt: "FMCG logistics",
desc:
"FMCG logistics demands speed, precision, and continuous fulfillment across high-volume delivery networks — balancing tight timelines, inventory movement, and efficiency without compromising availability.",
chips: ["99.2% On-Time", "Live Route Sync"],
challenges: [
"Unpredictable demand spikes create delivery pressure during peak periods.",
"Fresh-product expiry constraints require faster, precisely timed deliveries.",
"Multi-stop route complexity increases travel time and coordination cost.",
"Inventory stockout risks rise when delays disrupt fast-moving distribution.",
],
solutions: [
"AI demand forecasting adapts delivery plans instantly to real-time order demand.",
"Expiry-aware routing prioritises perishable goods for on-time freshness.",
"Smart multi-stop optimisation groups orders to cut cost and travel time.",
"Real-time inventory sync prevents stockouts and improves fulfilment accuracy.",
],
},
{
id: "pharma",
tab: "Pharma",
eyebrow: "Pharmaceutical Logistics",
title: "Pharma",
image: "/images/tab-pic-2-solution.jpeg",
alt: "Pharma logistics",
desc:
"Pharma logistics requires precision, compliance, and real-time monitoring so every shipment arrives safely and on time — from temperature-sensitive medicines to urgent emergency deliveries.",
chips: ["Cold Chain Active", "Zero Delay SLA"],
challenges: [
"Cold chain integrity demands precise temperature control throughout transit.",
"Regulatory compliance must be tracked and documented on every delivery.",
"Critical delivery time windows require highly accurate scheduling.",
"Emergency shipments need instant dispatch and zero-delay execution.",
],
solutions: [
"Cold chain monitoring with automatic re-routing keeps shipments in-spec.",
"Compliance engine with audit trails ensures full chain-of-custody visibility.",
"Precision scheduling locks in critical delivery windows reliably.",
"Priority dispatch queue fast-tracks urgent, life-critical shipments.",
],
},
{
id: "b2b",
tab: "Enterprise & B2B",
eyebrow: "Enterprise & B2B",
title: "Enterprise & B2B",
image: "/images/tab-pic-3-solution.jpeg",
alt: "Enterprise and B2B logistics",
desc:
"Enterprise and B2B logistics require coordination and reliability to manage high-value shipments at scale — with appointment scheduling, white-glove standards, and strict SLA commitments.",
chips: ["SLA Guaranteed", "White-Glove Ready"],
challenges: [
"Appointment scheduling requires precise timing across many locations.",
"White-glove delivery standards demand premium handling and accuracy.",
"Multi-location routing complexity grows with large-scale operations.",
"Strict SLA commitments pressure teams to stay timely and error-free.",
],
solutions: [
"Intelligent appointment engine streamlines and automates delivery slots.",
"White-glove workflow module enforces premium handling end to end.",
"Enterprise route planner coordinates efficient multi-location delivery.",
"SLA monitoring dashboard tracks commitments and flags risk in real time.",
],
},
];
export default function SolutionCard1() {
const [active, setActive] = useState(0);
const [tab, setTab] = useState<Tab>("challenges");
const ind = INDUSTRIES[active];
const selectIndustry = (i: number) => {
setActive(i);
setTab("challenges"); // reset to Challenges on industry switch
};
return (
<>
<style dangerouslySetInnerHTML={{ __html: `
/* ============================================================
Solutions — Industry section (FMCG / Pharma / Enterprise & B2B)
Brand red #dc2626 / #ef4444 · bg #0d0d0d · Syne + DM Sans
============================================================ */
/* The theme forces Manrope on every element via a high-specificity
universal :not() !important rule; re-assert Syne / DM Sans from the
ID selector (which outranks it) directly on each text node. */
#ind-solutions .ind__eyebrow,
#ind-solutions .ind__title,
#ind-solutions .ind__tab,
#ind-solutions .ind__toggle-btn {
font-family: var(--font-syne), 'Syne', sans-serif !important;
}
#ind-solutions .ind__desc,
#ind-solutions .ind__chip,
#ind-solutions .ind__list li {
font-family: var(--font-dm-sans), 'DM Sans', sans-serif !important;
}
/* kit-5 also forces heading color/size on the title (an <h3>). */
#ind-solutions .ind__title {
color: #fff !important;
font-size: clamp(34px, 5.5vw, 68px) !important;
font-weight: 800 !important;
line-height: 1.02 !important;
margin: 0 0 16px !important;
letter-spacing: -0.01em !important;
}
#ind-solutions .ind__list li { color: #c9c9c9 !important; }
.ind {
position: relative;
isolation: isolate;
overflow: hidden;
background: #0d0d0d;
border-radius: clamp(16px, 2vw, 26px);
max-width: 1400px;
margin: clamp(24px, 4vw, 56px) auto;
padding: clamp(34px, 5vw, 76px) clamp(18px, 4vw, 64px);
}
.ind__map {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0.55;
z-index: 0;
pointer-events: none;
}
.ind__inner {
position: relative;
z-index: 1;
max-width: 1240px;
margin: 0 auto;
}
/* ---- Top tab bar ---- */
.ind__tabs {
display: flex;
gap: clamp(10px, 3vw, 36px);
border-bottom: 1px solid rgba(255,255,255,0.1);
margin-bottom: clamp(28px, 4vw, 52px);
flex-wrap: wrap;
}
.ind__tab {
appearance: none;
background: none;
border: none;
cursor: pointer;
padding: 10px 2px 16px;
font-weight: 700;
font-size: clamp(14px, 1.4vw, 18px);
color: #888;
position: relative;
letter-spacing: 0.01em;
transition: color 0.3s ease;
}
.ind__tab::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: -1px;
height: 2px;
background: #dc2626;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.35s cubic-bezier(.25,1,.5,1);
}
.ind__tab:hover { color: #ccc; }
.ind__tab.active { color: #fff; }
.ind__tab.active::after { transform: scaleX(1); }
/* ---- Grid ---- */
.ind__grid {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: clamp(28px, 4vw, 64px);
align-items: center;
}
/* ---- Left media ---- */
.ind__media {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.ind__glow {
position: absolute;
left: 50%; bottom: 4%;
width: 74%; height: 70px;
transform: translateX(-50%);
background: radial-gradient(50% 50% at 50% 50%, rgba(220,38,38,0.5), transparent 72%);
filter: blur(32px);
z-index: 0;
animation: indGlow 4s ease-in-out infinite;
}
.ind__img-wrap {
position: relative;
z-index: 1;
width: 100%;
max-width: 460px;
animation: indFloat 6s ease-in-out infinite;
will-change: transform;
}
.ind__img-wrap::before,
.ind__img-wrap::after {
content: '';
position: absolute;
width: 44px; height: 44px;
border: 2px solid #dc2626;
z-index: 3;
}
.ind__img-wrap::before {
top: -10px; left: -10px;
border-right: none; border-bottom: none;
border-radius: 10px 0 0 0;
}
.ind__img-wrap::after {
bottom: -10px; right: -10px;
border-left: none; border-top: none;
border-radius: 0 0 10px 0;
}
.ind__img {
display: block;
width: 100%;
height: auto;
border-radius: 12px;
object-fit: cover;
box-shadow: 0 30px 60px -25px rgba(0,0,0,0.7);
animation: indImgFade 0.55s ease both;
}
.ind__chip {
position: absolute;
z-index: 4;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 16px;
border-radius: 999px;
background: rgba(16,16,16,0.82);
border: 1px solid rgba(239,68,68,0.4);
backdrop-filter: blur(8px);
color: #fff;
font-weight: 500;
font-size: 13px;
white-space: nowrap;
box-shadow: 0 12px 28px -12px rgba(0,0,0,0.7);
animation: indFloat 6s ease-in-out infinite;
}
.ind__chip .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #ef4444;
box-shadow: 0 0 8px #ef4444;
animation: indDot 1.6s ease-in-out infinite;
}
.ind__chip--1 { top: 7%; left: -5%; animation-delay: 0.4s; }
.ind__chip--2 { bottom: 12%; right: -5%; animation-delay: 1.3s; }
/* ---- Right text ---- */
.ind__text { min-width: 0; }
.ind__eyebrow {
display: inline-flex;
align-items: center;
gap: 12px;
color: #ef4444;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: clamp(11px, 1vw, 13px);
margin-bottom: 14px;
}
.ind__eyebrow::before {
content: '';
width: 30px; height: 2px;
background: #ef4444;
}
.ind__desc {
color: #b4b4b4;
font-size: clamp(15px, 1.2vw, 18px);
line-height: 1.7;
font-weight: 400;
margin: 0 0 24px;
max-width: 580px;
}
/* ---- Challenges / Solutions toggle ---- */
.ind__toggle {
display: inline-flex;
gap: 4px;
padding: 5px;
border-radius: 999px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.09);
margin-bottom: 24px;
}
.ind__toggle-btn {
appearance: none;
border: none;
cursor: pointer;
padding: 9px 28px;
border-radius: 999px;
font-weight: 700;
font-size: 14px;
color: #9a9a9a;
background: transparent;
transition: color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
}
.ind__toggle-btn:hover { color: #ddd; }
.ind__toggle-btn.active {
background: #dc2626;
color: #fff;
box-shadow: 0 8px 20px -8px rgba(220,38,38,0.6);
}
/* ---- Sliding panels ---- */
.ind__slider { overflow: hidden; }
.ind__track {
display: flex;
width: 200%;
transition: transform 0.4s cubic-bezier(.4,0,.2,1);
}
.ind__panel { width: 50%; flex: 0 0 50%; }
.ind__list { list-style: none; margin: 0; padding: 0; }
.ind__list li {
position: relative;
padding-left: 30px;
margin-bottom: 16px;
font-size: clamp(14px, 1.1vw, 16px);
line-height: 1.6;
opacity: 0;
transform: translateX(-16px);
}
.ind__list li:last-child { margin-bottom: 0; }
.ind__list li::before {
content: '';
position: absolute;
left: 4px; top: 8px;
width: 8px; height: 8px;
border-radius: 50%;
background: #dc2626;
box-shadow: 0 0 8px rgba(220,38,38,0.7);
}
.ind__panel.is-active .ind__list li {
animation: indBullet 0.5s cubic-bezier(.25,1,.5,1) forwards;
}
.ind__panel.is-active .ind__list li:nth-child(1) { animation-delay: 0.05s; }
.ind__panel.is-active .ind__list li:nth-child(2) { animation-delay: 0.13s; }
.ind__panel.is-active .ind__list li:nth-child(3) { animation-delay: 0.21s; }
.ind__panel.is-active .ind__list li:nth-child(4) { animation-delay: 0.29s; }
.ind__panel.is-active .ind__list li:nth-child(5) { animation-delay: 0.37s; }
@keyframes indFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-14px); } }
@keyframes indGlow { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
@keyframes indDot { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes indBullet { to { opacity: 1; transform: translateX(0); } }
@keyframes indImgFade { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
/* ---- Responsive ---- */
@media (max-width: 900px) {
.ind__grid { grid-template-columns: 1fr; gap: clamp(40px, 8vw, 56px); }
.ind__media { order: -1; }
.ind__img-wrap { max-width: 380px; }
}
@media (max-width: 600px) {
.ind__chip { font-size: 12px; padding: 7px 12px; }
.ind__chip--1 { left: 0; }
.ind__chip--2 { right: 0; }
.ind__toggle-btn { padding: 9px 20px; }
}
@media (prefers-reduced-motion: reduce) {
.ind__img-wrap, .ind__glow, .ind__chip, .ind__chip .dot { animation: none !important; }
.ind__track { transition: none !important; }
.ind__panel.is-active .ind__list li { animation: none !important; opacity: 1; transform: none; }
}
`}} />
<section id="ind-solutions" className="ind" aria-label="Industry solutions">
<IndustryWorldMap />
<div className="ind__inner">
{/* Top tab bar */}
<div className="ind__tabs" role="tablist" aria-label="Industries">
{INDUSTRIES.map((it, i) => (
<button
key={it.id}
type="button"
role="tab"
aria-selected={i === active}
className={`ind__tab ${i === active ? "active" : ""}`}
onClick={() => selectIndustry(i)}
>
{it.tab}
</button>
))}
</div>
<div className="ind__grid">
{/* Left media */}
<div className="ind__media">
<div className="ind__glow" />
<div className="ind__img-wrap">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img key={active} className="ind__img" src={ind.image} alt={ind.alt} decoding="async" />
</div>
<div className="ind__chip ind__chip--1">
<span className="dot" />
{ind.chips[0]}
</div>
<div className="ind__chip ind__chip--2">
<span className="dot" />
{ind.chips[1]}
</div>
</div>
{/* Right text */}
<div className="ind__text">
<span className="ind__eyebrow">{ind.eyebrow}</span>
<h3 className="ind__title">{ind.title}</h3>
<p className="ind__desc">{ind.desc}</p>
<div className="ind__toggle" role="tablist" aria-label="Challenges or Solutions">
<button
type="button"
role="tab"
aria-selected={tab === "challenges"}
className={`ind__toggle-btn ${tab === "challenges" ? "active" : ""}`}
onClick={() => setTab("challenges")}
>
Challenges
</button>
<button
type="button"
role="tab"
aria-selected={tab === "solutions"}
className={`ind__toggle-btn ${tab === "solutions" ? "active" : ""}`}
onClick={() => setTab("solutions")}
>
Solutions
</button>
</div>
<div className="ind__slider">
<div
className="ind__track"
key={active}
style={{ transform: tab === "challenges" ? "translateX(0)" : "translateX(-50%)" }}
>
<div className={`ind__panel ${tab === "challenges" ? "is-active" : ""}`}>
<ul className="ind__list">
{ind.challenges.map((c, idx) => (
<li key={idx}>{c}</li>
))}
</ul>
</div>
<div className={`ind__panel ${tab === "solutions" ? "is-active" : ""}`}>
<ul className="ind__list">
{ind.solutions.map((s, idx) => (
<li key={idx}>{s}</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</>
);
}

View File

@@ -2,6 +2,7 @@
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import LogisticsBrainSection from "../logisticsbrain/LogisticsBrainSection";
export default function Workflow2() {
const [activeSlide, setActiveSlide] = useState(0);
@@ -22,83 +23,67 @@ export default function Workflow2() {
];
return (
<section className="dm-workflow" aria-label="Workflow 2 — Competitive Edge & Innovation">
<div className="dm-workflow__container">
<section className="dm-wf2" aria-label="Workflow 2 — How Our Logistics Brain Works & Innovation">
{/* ── Top sub-section: Competitive Edge banner ── */}
<div className="dm-workflow-banner">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="dm-workflow-banner__img"
src="/images/miletruth-2.png"
alt="Our Competitive Edge"
width={1733}
height={773}
loading="lazy"
decoding="async"
/>
<div className="dm-workflow-banner__caption">
<span className="dm-workflow-banner__title-text">Our Competitive Edge</span>
</div>
{/* ── Top sub-section: the complete "How Our Logistics Brain Works" experience ── */}
<LogisticsBrainSection connected />
{/* ── Bottom sub-section: Innovation content, flush + colour-matched to the
logistics-brain card above so the whole workflow reads as one container ── */}
<div className="dm-wf2-card">
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
<path
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
/>
<path
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
/>
</svg>
</div>
{/* ── Bottom sub-section: Innovation content ── */}
<div className="dm-workflow-card">
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
<path
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
/>
<path
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
/>
</svg>
{/* Right Column: Quotes & Text Content */}
<div className="dm-workflow-right">
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
</svg>
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
<div className="dm-workflow-text-container">
<AnimatePresence mode="wait">
<motion.p
key={activeSlide}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
className="dm-workflow-text"
>
{slides[activeSlide].text}
</motion.p>
</AnimatePresence>
</div>
{/* Right Column: Quotes & Text Content */}
<div className="dm-workflow-right">
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
</svg>
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
<div className="dm-workflow-text-container">
<AnimatePresence mode="wait">
<motion.p
key={activeSlide}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
className="dm-workflow-text"
>
{slides[activeSlide].text}
</motion.p>
</AnimatePresence>
</div>
<div className="dm-workflow-nav">
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
<div className="dm-workflow-bars">
{slides.map((_, index) => (
<button
key={index}
type="button"
aria-label={`Go to slide ${index + 1}`}
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
onClick={() => setActiveSlide(index)}
/>
))}
</div>
<div className="dm-workflow-nav">
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
<div className="dm-workflow-bars">
{slides.map((_, index) => (
<button
key={index}
type="button"
aria-label={`Go to slide ${index + 1}`}
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
onClick={() => setActiveSlide(index)}
/>
))}
</div>
</div>
</div>
</div>
<style dangerouslySetInnerHTML={{ __html: styles }} />
@@ -108,73 +93,37 @@ export default function Workflow2() {
const styles = `
/* ============================================================
Workflow = ONE container: image-title banner (top) flush with
the dark content card (bottom). Single overflow:hidden wrapper
rounds the whole stack — no gap, no separate backgrounds.
Workflow 2 = ONE container:
├─ How Our Logistics Brain Works (full LogisticsBrainSection)
└─ Innovation (content card, flush + colour-matched)
The Innovation card is pulled up to butt against the logistics-brain
card's flat bottom and shares its dark red/black surface, so the two
read as a single continuous container with no gap / no break — the
same connected storytelling structure used in Workflow 1
(Impact of Optimisation → Performance).
============================================================ */
.dm-workflow {
max-width: 100%;
margin: 24px auto;
padding: 0 40px;
box-sizing: border-box;
.dm-wf2 {
position: relative;
margin: 0 auto 24px;
}
.dm-workflow__container {
/* Innovation card — aligned to the logistics-brain card (16px side insets),
red/black-matched, flat top, rounded bottom, pulled up to close the seam. */
.dm-wf2-card {
position: relative;
border-radius: 40px;
overflow: hidden;
background: #181818;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 14px 50px -16px rgba(0, 0, 0, 0.55);
}
/* ── Banner (top) ── */
.dm-workflow-banner {
position: relative;
display: block;
width: 100%;
font-size: 0;
line-height: 0;
}
.dm-workflow-banner__img {
display: block;
width: 100%;
height: auto;
max-height: 380px;
object-fit: cover;
object-position: center;
}
.dm-workflow-banner__caption {
position: absolute;
left: 40px;
bottom: 32px;
background: rgba(255, 255, 255, 0.92);
padding: 16px 26px;
border-radius: 12px;
box-shadow: 0 10px 28px -10px rgba(0, 0, 0, 0.45);
line-height: normal;
}
.dm-workflow-banner__title-text {
display: block;
color: #C01227 !important;
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif !important;
font-size: clamp(20px, 2.4vw, 34px) !important;
font-weight: 700 !important;
line-height: 1.1 !important;
letter-spacing: -0.01em;
text-transform: none !important;
}
/* ── Content card (bottom), flush under the banner ── */
.dm-workflow-card {
position: relative;
background: #181818;
border-top: 1px solid rgba(255, 255, 255, 0.06);
z-index: 2;
margin: 0 16px 0;
background: radial-gradient(120% 100% at 50% 0%, #12090c 0%, #0a070a 55%, #060507 100%);
border: 1px solid rgba(192, 18, 39, 0.16);
border-top: none;
border-radius: 0 0 28px 28px;
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
padding: 48px 60px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
overflow: hidden;
box-sizing: border-box;
}
@@ -251,24 +200,21 @@ const styles = `
.dm-workflow-bar:hover { background: rgba(255, 255, 255, 0.35); }
.dm-workflow-bar.is-active:hover { background: #C01227; }
/* ── Responsive ── */
/* ── Responsive — keep insets/radius aligned to the logistics-brain card ── */
@media (max-width: 1024px) {
.dm-workflow__container { border-radius: 32px; }
.dm-workflow-banner__img { max-height: 300px; }
.dm-workflow-banner__caption { left: 28px; bottom: 24px; padding: 13px 20px; }
.dm-workflow-card { padding: 44px 44px; gap: 44px; }
.dm-wf2-card {
padding: 44px 44px;
gap: 44px;
}
.dm-workflow-title { font-size: 32px; }
}
@media (max-width: 768px) {
.dm-workflow { padding: 0 16px; }
.dm-workflow__container { border-radius: 24px; }
.dm-workflow-banner__img { max-height: 220px; }
.dm-workflow-banner__caption { left: 16px; bottom: 16px; padding: 11px 16px; }
.dm-workflow-card {
flex-direction: column;
@media (max-width: 767px) {
.dm-wf2-card {
margin: 0 10px 0;
border-radius: 0 0 20px 20px;
padding: 36px 28px;
gap: 36px;
flex-direction: column;
}
.dm-workflow-left { max-width: 280px; }
.dm-workflow-right { width: 100%; }

View File

@@ -2,6 +2,7 @@
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import StrategySection from "../strategy/StrategySection";
export default function Workflow3() {
const [activeSlide, setActiveSlide] = useState(0);
@@ -22,83 +23,69 @@ export default function Workflow3() {
];
return (
<section className="dm-workflow" aria-label="Workflow 3 — Happier Riders & Strategy">
<div className="dm-workflow__container">
<section className="dm-wf3" aria-label="Workflow 3 — Happier Riders. Higher Fulfillment. & Strategy">
{/* ── Top sub-section: Happier Riders banner ── */}
<div className="dm-workflow-banner">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="dm-workflow-banner__img"
src="/images/miletruth-3.png"
alt="Happier Riders. Higher Fulfillment"
width={1733}
height={773}
loading="lazy"
decoding="async"
/>
<div className="dm-workflow-banner__caption">
<span className="dm-workflow-banner__title-text">Happier Riders. Higher Fulfillment</span>
</div>
{/* ── Top sub-section: the full "Happier Riders. Higher Fulfillment."
3D scroll-storytelling experience ── */}
<StrategySection connected />
{/* ── Bottom sub-section: Strategy content, flush + pulled up to butt against
the 3D card's flat bottom so the whole workflow reads as one container —
the same connected structure used in Workflow 1 & 2 ── */}
<div className="dm-wf3-card">
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
<path
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
/>
<path
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
/>
</svg>
</div>
{/* ── Bottom sub-section: Strategy content ── */}
<div className="dm-workflow-card">
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
<path
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
/>
<path
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
/>
</svg>
{/* Right Column: Quotes & Text Content */}
<div className="dm-workflow-right">
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
</svg>
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
<div className="dm-workflow-text-container">
<AnimatePresence mode="wait">
<motion.p
key={activeSlide}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
className="dm-workflow-text"
>
{slides[activeSlide].text}
</motion.p>
</AnimatePresence>
</div>
{/* Right Column: Quotes & Text Content */}
<div className="dm-workflow-right">
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
</svg>
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
<div className="dm-workflow-text-container">
<AnimatePresence mode="wait">
<motion.p
key={activeSlide}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
className="dm-workflow-text"
>
{slides[activeSlide].text}
</motion.p>
</AnimatePresence>
</div>
<div className="dm-workflow-nav">
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
<div className="dm-workflow-bars">
{slides.map((_, index) => (
<button
key={index}
type="button"
aria-label={`Go to slide ${index + 1}`}
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
onClick={() => setActiveSlide(index)}
/>
))}
</div>
<div className="dm-workflow-nav">
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
<div className="dm-workflow-bars">
{slides.map((_, index) => (
<button
key={index}
type="button"
aria-label={`Go to slide ${index + 1}`}
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
onClick={() => setActiveSlide(index)}
/>
))}
</div>
</div>
</div>
</div>
<style dangerouslySetInnerHTML={{ __html: styles }} />
@@ -108,73 +95,33 @@ export default function Workflow3() {
const styles = `
/* ============================================================
Workflow = ONE container: image-title banner (top) flush with
the dark content card (bottom). Single overflow:hidden wrapper
rounds the whole stack — no gap, no separate backgrounds.
Workflow 3 = ONE container:
├─ Happier Riders. Higher Fulfillment. (full StrategySection — 3D)
└─ Strategy (content card, flush, pulled up)
The Strategy card aligns to the 3D card's 16px side insets, butts against
its flat bottom and rounds the bottom corners, so the two read as a single
continuous container — same technique as Workflow 1 & 2.
============================================================ */
.dm-workflow {
max-width: 100%;
margin: 24px auto;
padding: 0 40px;
box-sizing: border-box;
.dm-wf3 {
position: relative;
margin: 0 auto 24px;
}
.dm-workflow__container {
.dm-wf3-card {
position: relative;
border-radius: 40px;
overflow: hidden;
z-index: 2;
margin: 0 16px 0;
background: #181818;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 14px 50px -16px rgba(0, 0, 0, 0.55);
}
/* ── Banner (top) ── */
.dm-workflow-banner {
position: relative;
display: block;
width: 100%;
font-size: 0;
line-height: 0;
}
.dm-workflow-banner__img {
display: block;
width: 100%;
height: auto;
max-height: 380px;
object-fit: cover;
object-position: center;
}
.dm-workflow-banner__caption {
position: absolute;
left: 40px;
bottom: 32px;
background: rgba(255, 255, 255, 0.92);
padding: 16px 26px;
border-radius: 12px;
box-shadow: 0 10px 28px -10px rgba(0, 0, 0, 0.45);
line-height: normal;
}
.dm-workflow-banner__title-text {
display: block;
color: #C01227 !important;
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif !important;
font-size: clamp(20px, 2.4vw, 34px) !important;
font-weight: 700 !important;
line-height: 1.1 !important;
letter-spacing: -0.01em;
text-transform: none !important;
}
/* ── Content card (bottom), flush under the banner ── */
.dm-workflow-card {
position: relative;
background: #181818;
border-top: 1px solid rgba(255, 255, 255, 0.06);
border-top: none;
border-radius: 0 0 28px 28px;
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
padding: 48px 60px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
overflow: hidden;
box-sizing: border-box;
}
@@ -251,24 +198,18 @@ const styles = `
.dm-workflow-bar:hover { background: rgba(255, 255, 255, 0.35); }
.dm-workflow-bar.is-active:hover { background: #C01227; }
/* ── Responsive ── */
/* ── Responsive — keep insets/radius aligned to the 3D card ── */
@media (max-width: 1024px) {
.dm-workflow__container { border-radius: 32px; }
.dm-workflow-banner__img { max-height: 300px; }
.dm-workflow-banner__caption { left: 28px; bottom: 24px; padding: 13px 20px; }
.dm-workflow-card { padding: 44px 44px; gap: 44px; }
.dm-wf3-card { padding: 44px 44px; gap: 44px; }
.dm-workflow-title { font-size: 32px; }
}
@media (max-width: 768px) {
.dm-workflow { padding: 0 16px; }
.dm-workflow__container { border-radius: 24px; }
.dm-workflow-banner__img { max-height: 220px; }
.dm-workflow-banner__caption { left: 16px; bottom: 16px; padding: 11px 16px; }
.dm-workflow-card {
flex-direction: column;
@media (max-width: 767px) {
.dm-wf3-card {
margin: 0 10px 0;
border-radius: 0 0 20px 20px;
padding: 36px 28px;
gap: 36px;
flex-direction: column;
}
.dm-workflow-left { max-width: 280px; }
.dm-workflow-right { width: 100%; }

View File

@@ -0,0 +1,421 @@
"use client";
import React, { useMemo, useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { RoundedBox, Line, Sparkles, Html } from "@react-three/drei";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import * as THREE from "three";
import { STAGES, N, stagePosition, samplePath } from "./theme";
type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
isMobile?: boolean;
active?: boolean;
};
const BG = "#eef1f6";
/* ---------------------------------------------------------------------------
Camera — fully scroll-driven. A fixed back/up offset from the active stage
keeps the active stage at a near-constant distance, so a constant DoF plane
keeps it sharp while past/future stages fall out of focus (Apple-style).
--------------------------------------------------------------------------- */
function CameraRig({ progress, reduced }: { progress: React.RefObject<number>; reduced: boolean }) {
const look = useRef(new THREE.Vector3(...stagePosition(0)));
useFrame((state, dt) => {
const p = progress.current ?? 0;
const idx = p * (N - 1);
const [tx, ty, tz] = samplePath(idx);
const t = state.clock.elapsedTime;
// Gentle parallax: camera slides toward the centre-line and breathes.
const camX = tx * 0.45 + Math.sin(t * 0.25) * 0.25;
const camY = ty + 1.35 + Math.sin(t * 0.35) * 0.12;
const camZ = tz + 5.4;
const k = reduced ? 1 : Math.min(1, dt * 3.2);
const cam = state.camera;
cam.position.x += (camX - cam.position.x) * k;
cam.position.y += (camY - cam.position.y) * k;
cam.position.z += (camZ - cam.position.z) * k;
look.current.x += (tx - look.current.x) * k;
look.current.y += (ty - look.current.y) * k;
look.current.z += (tz - look.current.z) * k;
cam.lookAt(look.current);
});
return null;
}
/* ---------------------------------------------------------------------------
Themed accent object floating in front of each glass stage.
--------------------------------------------------------------------------- */
function Accent({ i, color }: { i: number; color: THREE.Color }) {
const mat = (
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={1.4}
roughness={0.25}
metalness={0.2}
toneMapped={false}
/>
);
switch (i) {
case 0: // Rider happiness — soft orb
return <mesh><icosahedronGeometry args={[0.46, 1]} />{mat}</mesh>;
case 1: // Dispatch — node / octahedron
return <mesh rotation={[0.4, 0, 0.4]}><octahedronGeometry args={[0.55, 0]} />{mat}</mesh>;
case 2: // Route execution — flowing knot
return <mesh><torusKnotGeometry args={[0.34, 0.12, 96, 16]} />{mat}</mesh>;
case 3: // Fulfillment — gauge ring
return <mesh rotation={[Math.PI / 2.2, 0, 0]}><torusGeometry args={[0.46, 0.1, 24, 64]} />{mat}</mesh>;
default: // Hero — trophy (cup + stem + base)
return (
<group>
<mesh position={[0, 0.28, 0]}><sphereGeometry args={[0.34, 32, 16, 0, Math.PI * 2, 0, Math.PI / 1.7]} />{mat}</mesh>
<mesh position={[0, -0.05, 0]}><cylinderGeometry args={[0.07, 0.07, 0.34, 16]} />{mat}</mesh>
<mesh position={[0, -0.26, 0]}><cylinderGeometry args={[0.26, 0.3, 0.12, 24]} />{mat}</mesh>
</group>
);
}
}
/* ===========================================================================
STAGE 01 — a real logistics intake scene rendered on the glass panel:
orders.csv → 59 orders streaming in → AI assignment hub → 4 rider markers,
with glowing assignment beams. Labels/counters/icons are crisp drei <Html>
(real DOM, glass-styled) positioned in 3D; structure + motion stay in WebGL.
=========================================================================== */
const GREEN = "#22C55E";
const RIDERS = [
{ id: "A", veh: "EV Bike", icon: "🚲" },
{ id: "B", veh: "Auto", icon: "🛺" },
{ id: "C", veh: "Cargo Truck", icon: "🚚" },
{ id: "D", veh: "EV Van", icon: "🚐" },
];
const RIDER_X = [-1.18, -0.39, 0.39, 1.18];
const HUB = new THREE.Vector3(0, 0.14, 0.22);
/** A small order card that streams down from the source into the AI hub, on a loop. */
function OrderCard({ index }: { index: number }) {
const group = useRef<THREE.Group>(null);
const mat = useRef<THREE.MeshStandardMaterial>(null);
useFrame((state) => {
const t = (state.clock.elapsedTime * 0.55 + index * 0.27) % 1;
const o = Math.sin(t * Math.PI); // fade/scale in at the ends of the fall
if (group.current) {
group.current.position.set((index - 1.5) * 0.16, 0.78 - t * 0.62, 0.32);
group.current.scale.setScalar(0.66 + o * 0.34);
}
if (mat.current) mat.current.opacity = o * 0.95;
});
return (
<group ref={group}>
<RoundedBox args={[0.5, 0.34, 0.02]} radius={0.05} smoothness={3}>
<meshStandardMaterial ref={mat} color="#ffffff" emissive={GREEN} emissiveIntensity={0.35} transparent opacity={0.9} />
</RoundedBox>
<mesh position={[0, 0.1, 0.012]}><planeGeometry args={[0.5, 0.05]} /><meshBasicMaterial color={GREEN} toneMapped={false} transparent opacity={0.85} /></mesh>
</group>
);
}
/** Glowing assignment beam from the hub to a rider, with a travelling packet. */
function AssignBeam({ to, delay }: { to: [number, number, number]; delay: number }) {
const dot = useRef<THREE.Mesh>(null);
const b = useMemo(() => new THREE.Vector3(...to), [to]);
const points = useMemo(() => [HUB.clone(), b.clone()], [b]);
useFrame((state) => {
if (dot.current) dot.current.position.lerpVectors(HUB, b, (state.clock.elapsedTime * 0.6 + delay) % 1);
});
return (
<group>
<Line points={points} color={GREEN} lineWidth={1.4} transparent opacity={0.5} toneMapped={false} />
<mesh ref={dot}><sphereGeometry args={[0.045, 12, 12]} /><meshBasicMaterial color={GREEN} toneMapped={false} /></mesh>
</group>
);
}
/** A rider node (glowing marker) + a glass DOM chip with vehicle icon + name. */
function RiderMarker({ x, rider, register }: { x: number; rider: typeof RIDERS[number]; register: (el: HTMLElement | null) => void }) {
const pulse = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (pulse.current) {
const s = 1 + Math.sin(state.clock.elapsedTime * 2 + x) * 0.18;
pulse.current.scale.setScalar(s);
}
});
return (
<group position={[x, -0.74, 0.22]}>
<mesh><sphereGeometry args={[0.11, 24, 24]} /><meshStandardMaterial color={GREEN} emissive={GREEN} emissiveIntensity={0.9} toneMapped={false} /></mesh>
<mesh ref={pulse}><ringGeometry args={[0.15, 0.17, 32]} /><meshBasicMaterial color={GREEN} transparent opacity={0.5} toneMapped={false} side={THREE.DoubleSide} /></mesh>
<Html center distanceFactor={6.5} position={[0, -0.3, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-chip" ref={register}>
<span className="dm-st3d-chip__ico">{rider.icon}</span>
<span className="dm-st3d-chip__txt"><b>Rider {rider.id}</b>{rider.veh}</span>
</div>
</Html>
</group>
);
}
function Stage1Scene({ progress }: { progress: React.RefObject<number> }) {
const counter = useRef<HTMLSpanElement>(null);
const labels = useRef<HTMLElement[]>([]);
const register = (el: HTMLElement | null) => { if (el && !labels.current.includes(el)) labels.current.push(el); };
useFrame((state) => {
// Live order intake — count up 0→59, hold, repeat.
if (counter.current) {
const cyc = state.clock.elapsedTime % 3.6;
counter.current.textContent = String(Math.min(59, Math.round((cyc / 2.4) * 59)));
}
// Fade the DOM labels out as the camera leaves stage 1.
const idx = (progress.current ?? 0) * (N - 1);
const op = THREE.MathUtils.clamp(1 - (idx - 0.45) / 0.6, 0, 1);
for (const el of labels.current) el.style.opacity = String(op);
});
return (
<group>
{/* Source file */}
<Html center distanceFactor={6.5} position={[0, 0.96, 0.34]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-file" ref={register}>📄 orders.csv</div>
</Html>
{/* Live intake counter */}
<Html center distanceFactor={6.5} position={[0, 0.62, 0.34]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-count" ref={register}><span ref={counter}>0</span> Orders</div>
</Html>
{/* Order cards streaming in */}
{[0, 1, 2, 3].map((j) => <OrderCard key={j} index={j} />)}
{/* AI assignment hub */}
<mesh position={[HUB.x, HUB.y, 0.16]}><icosahedronGeometry args={[0.16, 1]} /><meshStandardMaterial color={GREEN} emissive={GREEN} emissiveIntensity={1.2} toneMapped={false} /></mesh>
<Html center distanceFactor={6.5} position={[0, HUB.y, 0.36]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-ai" ref={register}>🤖 AI Assignment</div>
</Html>
{/* Assignment beams + rider markers */}
{RIDERS.map((r, j) => <AssignBeam key={r.id} to={[RIDER_X[j], -0.66, 0.22]} delay={j * 0.25} />)}
{RIDERS.map((r, j) => <RiderMarker key={r.id} x={RIDER_X[j]} rider={r} register={register} />)}
</group>
);
}
/* ---------------------------------------------------------------------------
One floating glassmorphism stage. Per-frame it reacts to scroll: the active
stage scales up + brightens; completed stages push back + dim.
--------------------------------------------------------------------------- */
function StageNode({ i, progress, reduced }: { i: number; progress: React.RefObject<number>; reduced: boolean }) {
const group = useRef<THREE.Group>(null);
const accentGroup = useRef<THREE.Group>(null);
const glassMat = useRef<THREE.MeshPhysicalMaterial>(null);
const haloMat = useRef<THREE.MeshBasicMaterial>(null);
const base = useMemo(() => stagePosition(i), [i]);
const color = useMemo(() => new THREE.Color(STAGES[i].theme), [i]);
const sEased = useRef(0.6);
const pushEased = useRef(0);
const oEased = useRef(0.2);
useFrame((state, dt) => {
const p = progress.current ?? 0;
const idx = p * (N - 1);
const d = idx - i; // >0 completed, <0 future, ~0 active
const ad = Math.abs(d);
const active = ad < 0.5;
const targetScale = THREE.MathUtils.clamp(1.12 - ad * 0.17, 0.62, 1.14);
const targetPush = d > 0 ? d * 2.8 : 0; // completed recede
const targetOpacity =
d > 0 ? THREE.MathUtils.clamp(0.95 - d * 0.42, 0.16, 0.95) // completed dim
: d < 0 ? THREE.MathUtils.clamp(0.95 + d * 0.28, 0.28, 0.95) // future faintly hidden
: 0.95;
const k = reduced ? 1 : Math.min(1, dt * 4);
sEased.current = THREE.MathUtils.lerp(sEased.current, targetScale, k);
pushEased.current = THREE.MathUtils.lerp(pushEased.current, targetPush, k);
oEased.current = THREE.MathUtils.lerp(oEased.current, targetOpacity, k);
const t = state.clock.elapsedTime;
if (group.current) {
group.current.scale.setScalar(sEased.current);
group.current.position.set(
base[0],
base[1] + Math.sin(t * 0.6 + i) * 0.16,
base[2] - pushEased.current,
);
group.current.rotation.y = Math.sin(t * 0.3 + i * 1.3) * 0.07;
}
if (glassMat.current) {
glassMat.current.opacity = oEased.current * 0.3;
(glassMat.current.emissive as THREE.Color).copy(color).multiplyScalar(active ? 0.55 : 0.16);
}
if (haloMat.current) {
haloMat.current.opacity = oEased.current * (active ? 0.92 : 0.4);
}
if (accentGroup.current) {
accentGroup.current.rotation.y += dt * (active ? 0.7 : 0.25);
accentGroup.current.position.y = Math.sin(t * 0.9 + i) * 0.12;
}
});
return (
<group ref={group} position={base}>
{/* Coloured halo backplate — reads as the stage glow under Bloom */}
<RoundedBox args={[3.62, 2.42, 0.05]} radius={0.22} smoothness={4} position={[0, 0, -0.05]}>
<meshBasicMaterial ref={haloMat} color={color} transparent opacity={0.8} toneMapped={false} />
</RoundedBox>
{/* Frosted glass panel */}
<RoundedBox args={[3.4, 2.2, 0.14]} radius={0.2} smoothness={4}>
<meshPhysicalMaterial
ref={glassMat}
color="#ffffff"
transparent
opacity={0.3}
roughness={0.12}
metalness={0}
clearcoat={1}
clearcoatRoughness={0.18}
ior={1.25}
reflectivity={0.45}
emissive={color}
emissiveIntensity={1}
/>
</RoundedBox>
{/* Stage 1 = a real logistics intake scene; other stages = floating accent */}
{i === 0 ? (
<Stage1Scene progress={progress} />
) : (
<group ref={accentGroup} position={[0, 0, 0.55]}>
<Accent i={i} color={color} />
</group>
)}
</group>
);
}
/* ---------------------------------------------------------------------------
Glowing connector + a light packet travelling from one stage to the next.
--------------------------------------------------------------------------- */
function Connector({ i }: { i: number }) {
const dot = useRef<THREE.Mesh>(null);
const a = useMemo(() => stagePosition(i), [i]);
const b = useMemo(() => stagePosition(i + 1), [i]);
const cA = useMemo(() => new THREE.Color(STAGES[i].theme), [i]);
const cB = useMemo(() => new THREE.Color(STAGES[i + 1].theme), [i]);
const dotColor = useMemo(() => cA.clone().lerp(cB, 0.5), [cA, cB]);
// A gentle arc between the two stages (midpoint lifted).
const points = useMemo(() => {
const mid: [number, number, number] = [
(a[0] + b[0]) / 2,
(a[1] + b[1]) / 2 + 0.9,
(a[2] + b[2]) / 2,
];
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(...a),
new THREE.Vector3(...mid),
new THREE.Vector3(...b),
);
return curve.getPoints(40);
}, [a, b]);
const curve = useMemo(() => {
const mid = new THREE.Vector3(
(a[0] + b[0]) / 2,
(a[1] + b[1]) / 2 + 0.9,
(a[2] + b[2]) / 2,
);
return new THREE.QuadraticBezierCurve3(new THREE.Vector3(...a), mid, new THREE.Vector3(...b));
}, [a, b]);
useFrame((state) => {
if (!dot.current) return;
const t = (state.clock.elapsedTime * 0.32 + i * 0.27) % 1;
dot.current.position.copy(curve.getPoint(t));
});
return (
<group>
<Line points={points} color={dotColor} lineWidth={1.5} transparent opacity={0.5} toneMapped={false} />
<mesh ref={dot}>
<sphereGeometry args={[0.09, 16, 16]} />
<meshBasicMaterial color={dotColor} toneMapped={false} />
</mesh>
</group>
);
}
function Scene({ progress, reduced, isMobile }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean }) {
return (
<>
<color attach="background" args={[BG]} />
<fog attach="fog" args={[BG, 16, 46]} />
<ambientLight intensity={0.95} />
<hemisphereLight args={["#ffffff", "#dfe4ee", 0.7]} />
<directionalLight position={[6, 10, 8]} intensity={0.85} />
<directionalLight position={[-8, 4, -4]} intensity={0.35} color="#cdd6ff" />
<CameraRig progress={progress} reduced={reduced} />
{STAGES.map((_, i) => (
<StageNode key={i} i={i} progress={progress} reduced={reduced} />
))}
{STAGES.slice(0, -1).map((_, i) => (
<Connector key={i} i={i} />
))}
{/* Ambient floating particles + a confetti-like burst near the hero stage */}
{!reduced && (
<>
<Sparkles
count={isMobile ? 50 : 110}
scale={[14, 8, N * 6.4]}
position={[0, 0, (-(N - 1) * 6.4) / 2]}
size={2.2}
speed={0.3}
opacity={0.5}
color="#9aa6c4"
/>
<Sparkles
count={isMobile ? 28 : 60}
scale={[5, 4, 4]}
position={stagePosition(N - 1)}
size={3.4}
speed={0.5}
opacity={0.9}
color="#ff9aa9"
/>
</>
)}
{!reduced && (
<EffectComposer multisampling={isMobile ? 0 : 2}>
<Bloom
mipmapBlur
intensity={isMobile ? 0.7 : 1.05}
luminanceThreshold={0.55}
luminanceSmoothing={0.06}
radius={isMobile ? 0.6 : 0.78}
kernelSize={KernelSize.MEDIUM}
/>
</EffectComposer>
)}
</>
);
}
export default function StrategyCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) {
return (
<Canvas
dpr={[1, isMobile || reduced ? 1.25 : 1.5]}
camera={{ position: [0, 1.4, 5.4], fov: 48, near: 0.1, far: 90 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"}
>
<Scene progress={progress} reduced={reduced} isMobile={isMobile} />
</Canvas>
);
}

View File

@@ -0,0 +1,516 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { STAGES, N } from "./theme";
const StrategyCanvas = dynamic(() => import("./StrategyCanvas"), { ssr: false });
/** Center of each stage's scroll window (0…1). */
const CENTER = (i: number) => i / (N - 1);
/** Persistent top rail: the 5 stages, current one highlighted. */
function StageRail({ active }: { active: number }) {
return (
<div className="dm-st-rail" aria-hidden>
{STAGES.map((s, i) => {
const state = i < active ? "done" : i === active ? "current" : "todo";
return (
<React.Fragment key={s.n}>
{i > 0 && <span className={`dm-st-rail__line is-${i <= active ? "on" : "off"}`} />}
<div className={`dm-st-rail__step is-${state}`} style={{ ["--c" as string]: s.theme }}>
<span className="dm-st-rail__num">{i < active ? "✓" : s.n}</span>
<span className="dm-st-rail__title">{s.kicker}</span>
</div>
</React.Fragment>
);
})}
</div>
);
}
/** A cross-fading glass content card pinned to one side, themed per stage. */
function StageCard({
i,
scroll,
side,
children,
}: {
i: number;
scroll: MotionValue<number>;
side: "left" | "right";
children: React.ReactNode;
}) {
const c = CENTER(i);
const opacity = useTransform(scroll, [c - 0.14, c - 0.06, c + 0.06, c + 0.14], [0, 1, 1, 0]);
const y = useTransform(scroll, [c - 0.14, c - 0.05], [34, 0]);
const s = STAGES[i];
return (
<motion.div
className={`dm-st-card-story is-${side}`}
style={{ opacity, y, ["--c" as string]: s.theme }}
>
<div className="dm-st-card-story__head">
<span className="dm-st-pillar__num">{s.n}</span>
<span className="dm-st-pillar__kicker">{s.kicker}</span>
</div>
{children}
</motion.div>
);
}
/**
* "Strategy" — a premium, Apple-keynote-style 3D scroll-storytelling section.
* A single GSAP ScrollTrigger maps scroll to a normalized progress that drives
* the R3F camera through five floating glass stages while DOM glass cards
* cross-fade in lockstep. Pins via a self-managed fixed element (the site's
* fixed header + an ancestor `overflow:hidden` break CSS sticky / GSAP pin).
*/
export default function StrategySection({ connected = false }: { connected?: boolean } = {}) {
const containerRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
const scroll = useMotionValue(0);
const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before");
const [active, setActive] = useState(0);
const [mountScene, setMountScene] = useState(false);
const [sceneActive, setSceneActive] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mqMobile = window.matchMedia("(max-width: 767px)");
const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)");
const sync = () => { setIsMobile(mqMobile.matches); setReduced(mqReduce.matches); };
sync();
mqMobile.addEventListener("change", sync);
mqReduce.addEventListener("change", sync);
return () => { mqMobile.removeEventListener("change", sync); mqReduce.removeEventListener("change", sync); };
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const mountIo = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setMountScene(true);
setSceneActive(true);
mountIo.disconnect();
}
},
{ rootMargin: "70% 0px" },
);
const activeIo = new IntersectionObserver(
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
{ rootMargin: "10% 0px" },
);
mountIo.observe(el);
activeIo.observe(el);
return () => { mountIo.disconnect(); activeIo.disconnect(); };
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
gsap.registerPlugin(ScrollTrigger);
let lastPin: "before" | "pinned" | "after" = "before";
let lastActive = 0;
const st = ScrollTrigger.create({
trigger: el,
start: "top top",
end: "bottom bottom",
scrub: 0.5,
invalidateOnRefresh: true,
onUpdate: (self) => {
const p = self.progress;
progressRef.current = p;
scroll.set(p);
const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned";
if (ns !== lastPin) { lastPin = ns; setPinState(ns); }
const na = Math.round(p * (N - 1));
if (na !== lastActive) { lastActive = na; setActive(na); }
},
});
const refresh = setTimeout(() => ScrollTrigger.refresh(), 300);
return () => { clearTimeout(refresh); st.kill(); };
}, [scroll]);
// Intro hint fades out as the journey begins.
const introOpacity = useTransform(scroll, [0, 0.03, 0.06], [1, 1, 0]);
// Persistent header fades in after the intro.
const headerOpacity = useTransform(scroll, [0.02, 0.07], [0, 1]);
return (
<section
ref={containerRef}
className={`dm-st is-${pinState}${connected ? " is-connected" : ""}`}
aria-label="Strategy — Happier Riders. Higher Fulfillment."
>
<div className="dm-st-sticky">
<div className="dm-st-card">
{mountScene && (
<div className="dm-st-canvas">
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} />
</div>
)}
<div className="dm-st-ui">
{/* Persistent header */}
<motion.div className="dm-st-top" style={{ opacity: headerOpacity }}>
<div className="dm-st-eyebrow"><span className="dm-st-dot" /> MileTruth Strategy Engine</div>
<StageRail active={active} />
</motion.div>
{/* Intro hint */}
<motion.div className="dm-st-scrollhint" style={{ opacity: introOpacity }}>
<span>Scroll to follow the strategy</span>
<span className="dm-st-arrow"></span>
</motion.div>
{/* STAGE 01 — INPUT (green): orders + riders enter the system */}
<StageCard i={0} scroll={scroll} side="left">
<h3 className="dm-st-pillar__title">Orders &amp; riders enter the system</h3>
<div className="dm-st-grid2">
<div className="dm-st-tile">
<span className="dm-st-tile__ico">📄</span>
<span className="dm-st-tile__num">59</span>
<span className="dm-st-tile__lbl">Orders</span>
<span className="dm-st-tile__sub">Order intake</span>
</div>
<div className="dm-st-tile">
<span className="dm-st-tile__ico">🧑</span>
<span className="dm-st-tile__num">4</span>
<span className="dm-st-tile__lbl">Total Riders</span>
<span className="dm-st-tile__sub">Available fleet</span>
</div>
</div>
<div className="dm-st-file">
<span className="dm-st-file__ico"></span>
<span className="dm-st-file__name">orders.csv</span>
<span className="dm-st-file__meta">59 rows · uploaded</span>
<span className="dm-st-file__ok"></span>
</div>
<ul className="dm-st-riders">
{[
{ a: "A", name: "Rider A", v: "EV Bike", cap: "12 orders" },
{ a: "B", name: "Rider B", v: "Auto", cap: "18 orders" },
{ a: "C", name: "Rider C", v: "Cargo Truck", cap: "20 orders" },
{ a: "D", name: "Rider D", v: "EV Van", cap: "9 orders" },
].map((r) => (
<li key={r.a} className="dm-st-rider">
<span className="dm-st-rider__av">{r.a}<i /></span>
<span className="dm-st-rider__name">{r.name}</span>
<span className="dm-st-rider__veh">{r.v}</span>
<span className="dm-st-rider__cap">{r.cap}</span>
</li>
))}
</ul>
</StageCard>
{/* STAGE 02 — PARALLEL EXECUTION (purple): 6 strategies at once */}
<StageCard i={1} scroll={scroll} side="right">
<h3 className="dm-st-pillar__title">Six strategies, evaluated in parallel</h3>
<div className="dm-st-engines">
<div className="dm-st-engine">
<div className="dm-st-engine__head"><span className="dm-st-engine__name">Legacy Engine</span><span className="dm-st-engine__tag">Baseline</span></div>
<div className="dm-st-pills">
{["Proximity", "Balanced", "Fuel Saver"].map((p) => <span key={p} className="dm-st-pill">{p}</span>)}
</div>
</div>
<div className="dm-st-engine is-unified">
<div className="dm-st-engine__head"><span className="dm-st-engine__name">Unified Engine</span><span className="dm-st-engine__tag dm-st-engine__tag--u">MileTruth</span></div>
<div className="dm-st-pills">
{["EV Aware", "Multi Trip", "Time Aware"].map((p) => <span key={p} className="dm-st-pill dm-st-pill--u">{p}</span>)}
</div>
</div>
</div>
<p className="dm-st-foot"><span className="dm-st-livedot" /> All 6 strategies run at the same time</p>
</StageCard>
{/* STAGE 03 — SMART OPTIMIZATION (blue): validation pipeline */}
<StageCard i={2} scroll={scroll} side="left">
<h3 className="dm-st-pillar__title">Routes validated &amp; optimized</h3>
<div className="dm-st-pipe">
{[
{ ico: "⚙️", name: "VRP Optimizer", desc: "Google OR-Tools solver" },
{ ico: "🔋", name: "Battery Simulation", desc: "EV range & charging feasibility" },
{ ico: "⏱️", name: "SLA Validator", desc: "ETA vs promised window" },
].map((m, j) => (
<React.Fragment key={m.name}>
{j > 0 && <span className="dm-st-pipe__arrow"><i /></span>}
<div className="dm-st-pipe__node">
<span className="dm-st-pipe__ico">{m.ico}</span>
<span className="dm-st-pipe__name">{m.name}</span>
<span className="dm-st-pipe__desc">{m.desc}</span>
</div>
</React.Fragment>
))}
</div>
</StageCard>
{/* STAGE 04 — PERFORMANCE GRADING (orange): every strategy scored */}
<StageCard i={3} scroll={scroll} side="right">
<h3 className="dm-st-pillar__title">Every strategy is scored</h3>
<div className="dm-st-stars"><span className="dm-st-stars__on"></span><span className="dm-st-stars__off"></span><span className="dm-st-stars__txt">4.5 / 5 grade</span></div>
<ul className="dm-st-metrics">
{[
{ name: "Fulfillment Rate", score: "88%", w: 88, status: "Good", desc: "Orders delivered vs total" },
{ name: "SLA Compliance", score: "95%", w: 95, status: "Pass", desc: "On-time within window" },
{ name: "Efficiency Score", score: "92", w: 92, status: "Strong", desc: "Distance & fleet usage" },
{ name: "Battery & Route", score: "OK", w: 100, status: "Feasible", desc: "EV range respected" },
].map((m) => (
<li key={m.name} className="dm-st-metric">
<span className="dm-st-metric__name">{m.name}</span>
<span className="dm-st-metric__bar"><i style={{ width: `${m.w}%` }} /></span>
<span className="dm-st-metric__score">{m.score}</span>
<span className="dm-st-metric__status"> {m.status}</span>
<span className="dm-st-metric__desc">{m.desc}</span>
</li>
))}
</ul>
</StageCard>
{/* STAGE 05 — STRATEGY COMPARISON (red, hero): the winner */}
<StageCard i={4} scroll={scroll} side="right">
<div className="dm-st-winner">
<span className="dm-st-winner__eyebrow">Best strategy recommendation</span>
<div className="dm-st-winner__name"><span className="dm-st-trophy">🏆</span> EV Aware</div>
<span className="dm-st-winner__grade">High Performance Grade</span>
</div>
<div className="dm-st-hero">
<div className="dm-st-hero__ring">
<span className="dm-st-hero__pct">88%</span>
<span className="dm-st-hero__sub">Score</span>
</div>
<ul className="dm-st-wins">
<li><strong>52/59</strong> Orders Fulfilled</li>
<li><strong>88%</strong> Performance Score</li>
<li><strong>3</strong> SLA Violations</li>
<li><strong>A</strong> Performance Grade</li>
</ul>
</div>
</StageCard>
</div>
</div>
</div>
<style>{styles}</style>
</section>
);
}
const styles = `
.dm-st { position: relative; height: 620vh; background: transparent; }
.dm-st-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; }
.dm-st.is-pinned .dm-st-sticky { position: fixed; top: 0; left: 0; }
.dm-st.is-after .dm-st-sticky { position: absolute; top: auto; bottom: 0; }
.dm-st-card {
position: absolute !important; inset: 16px !important;
border-radius: 28px !important; overflow: hidden !important;
background: radial-gradient(120% 100% at 50% 0%, #ffffff 0%, #eef1f6 60%, #e6eaf2 100%) !important;
border: 1px solid rgba(15,23,42,0.08) !important;
box-shadow: 0 30px 90px -34px rgba(15,23,42,0.4) !important;
box-sizing: border-box !important;
}
@media (max-width: 767px) { .dm-st-card { inset: 10px !important; border-radius: 20px !important; } }
/* Connected mode (inside Workflow 3): flatten the card's bottom so the Strategy
content card below butts directly against it — same seam as Workflow 1 & 2. */
.dm-st.is-connected .dm-st-card {
top: 16px !important; left: 16px !important; right: 16px !important; bottom: 0 !important;
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
}
@media (max-width: 767px) {
.dm-st.is-connected .dm-st-card {
top: 10px !important; left: 10px !important; right: 10px !important; bottom: 0 !important;
border-radius: 20px 20px 0 0 !important;
}
}
.dm-st-canvas { position: absolute; inset: 0; z-index: 1; }
.dm-st-canvas canvas { display: block; }
.dm-st-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none;
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; color: #0f172a; }
/* ---- Persistent header: title + 5-stage rail ---- */
.dm-st-top { position: absolute; top: clamp(96px, 13vh, 128px); left: 0; right: 0; z-index: 5;
display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 0 16px; }
.dm-st-eyebrow { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1.35;
letter-spacing: 0.28em; text-transform: uppercase; color: #475569; padding: 9px 18px; border-radius: 999px;
background: rgba(255,255,255,0.72); border: 1px solid rgba(15,23,42,0.08); backdrop-filter: blur(10px); white-space: nowrap; }
.dm-st-dot { width: 6px; height: 6px; border-radius: 50%; background: #6366f1; box-shadow: 0 0 10px #6366f1; }
.dm-st-rail { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; max-width: 980px; }
.dm-st-rail__step { display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px; border-radius: 999px;
background: rgba(255,255,255,0.7); border: 1px solid rgba(15,23,42,0.08); backdrop-filter: blur(8px);
transition: all 0.45s cubic-bezier(0.22,1,0.36,1); }
.dm-st-rail__num { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 800; color: #64748b; background: rgba(15,23,42,0.06); }
.dm-st-rail__title { font-size: 11px; font-weight: 600; letter-spacing: 0.02em; color: #64748b; white-space: nowrap; }
.dm-st-rail__step.is-current { background: color-mix(in srgb, var(--c) 16%, white); border-color: var(--c);
box-shadow: 0 0 22px -6px var(--c); }
.dm-st-rail__step.is-current .dm-st-rail__num { background: var(--c); color: #fff; }
.dm-st-rail__step.is-current .dm-st-rail__title { color: #0f172a; }
.dm-st-rail__step.is-done .dm-st-rail__num { background: #22C55E; color: #fff; }
.dm-st-rail__step.is-done .dm-st-rail__title { color: #334155; }
.dm-st-rail__line { width: 14px; height: 1px; background: rgba(15,23,42,0.14); margin: 0 3px; transition: background 0.45s ease; }
.dm-st-rail__line.is-on { background: var(--c, #22C55E); }
.dm-st-scrollhint { position: absolute; bottom: clamp(26px, 6vh, 60px); left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 12px; letter-spacing: 0.12em;
color: #64748b; text-transform: uppercase; text-align: center; }
.dm-st-arrow { font-size: 18px; animation: dmStBob 1.8s ease-in-out infinite; }
@keyframes dmStBob { 0%,100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(6px); opacity: 1; } }
/* ---- Per-stage glass content card ---- */
.dm-st-card-story { position: absolute; bottom: clamp(24px, 6vh, 64px); width: min(484px, 88vw);
pointer-events: auto; will-change: opacity, transform; padding: 20px 22px; border-radius: 20px;
background: rgba(255,255,255,0.94); border: 1px solid rgba(15,23,42,0.08);
/* backdrop blur removed — card cross-fades/translates per scroll-stage; blur was the
heaviest per-frame cost on this section. Near-opaque white keeps the glass look. */
border-top: 3px solid var(--c);
box-shadow: 0 28px 70px -34px rgba(15,23,42,0.5); }
.dm-st-card-story.is-left { left: clamp(18px, 5vw, 72px); }
.dm-st-card-story.is-right { right: clamp(18px, 5vw, 72px); }
.dm-st-card-story__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.dm-st-pillar__num { font-size: 12px; font-weight: 800; letter-spacing: 0.1em; color: #fff;
background: var(--c); border-radius: 7px; padding: 3px 8px; }
.dm-st-pillar__kicker { font-size: clamp(11px, 1.1vw, 13px); font-weight: 700; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--c); }
.dm-st .dm-st-pillar__title { margin: 0 0 14px !important; padding: 0 !important; color: #0f172a !important;
font-weight: 700 !important; text-transform: none !important; letter-spacing: -0.015em !important;
font-size: clamp(18px, 2vw, 26px) !important; line-height: 1.16 !important; }
.dm-st .dm-st-pillar__title--hero { font-size: clamp(22px, 2.6vw, 34px) !important;
background: linear-gradient(90deg, #C01227, #E2354A) !important; -webkit-background-clip: text !important;
background-clip: text !important; -webkit-text-fill-color: transparent !important; }
.dm-st-foot { margin: 12px 0 0; font-size: clamp(12px, 1.1vw, 13.5px); line-height: 1.5; color: #475569;
display: flex; align-items: center; gap: 8px; }
.dm-st-livedot { width: 8px; height: 8px; border-radius: 50%; background: var(--c); box-shadow: 0 0 0 0 var(--c);
animation: dmStPulse 1.8s ease-out infinite; }
@keyframes dmStPulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--c) 55%, transparent); } 100% { box-shadow: 0 0 0 9px transparent; } }
/* In-scene 3D labels (drei <Html>) — crisp glass chips floating in the WebGL scene */
.dm-st3d-file, .dm-st3d-count, .dm-st3d-ai, .dm-st3d-chip {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
pointer-events: none; user-select: none; white-space: nowrap; transition: opacity 0.2s linear; }
.dm-st3d-file, .dm-st3d-ai {
display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #0f172a;
background: rgba(255,255,255,0.88); border: 1px solid rgba(34,197,94,0.4); border-radius: 999px;
padding: 6px 13px; box-shadow: 0 8px 22px -12px rgba(34,197,94,0.7); backdrop-filter: blur(8px); }
.dm-st3d-count { font-size: 15px; font-weight: 800; color: #0f172a; background: rgba(255,255,255,0.9);
border: 1px solid rgba(34,197,94,0.45); border-radius: 12px; padding: 6px 14px;
box-shadow: 0 10px 26px -12px rgba(34,197,94,0.8); backdrop-filter: blur(8px); }
.dm-st3d-count span { color: #16a34a; font-size: 19px; }
.dm-st3d-chip { display: inline-flex; align-items: center; gap: 8px; background: rgba(255,255,255,0.92);
border: 1px solid rgba(34,197,94,0.4); border-radius: 12px; padding: 6px 11px;
box-shadow: 0 10px 26px -14px rgba(15,23,42,0.7); backdrop-filter: blur(8px); }
.dm-st3d-chip__ico { font-size: 17px; }
.dm-st3d-chip__txt { display: flex; flex-direction: column; line-height: 1.15; }
.dm-st3d-chip__txt b { font-size: 12.5px; font-weight: 800; color: #0f172a; }
.dm-st3d-chip__txt { font-size: 10.5px; color: #475569; }
/* STAGE 01 — Input */
.dm-st-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.dm-st-tile { position: relative; background: color-mix(in srgb, var(--c) 7%, white); border: 1px solid color-mix(in srgb, var(--c) 22%, white);
border-radius: 14px; padding: 12px 14px; display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; align-items: center; }
.dm-st-tile__ico { grid-row: 1 / 3; font-size: 24px; }
.dm-st-tile__num { font-size: 26px; font-weight: 800; color: #0f172a; line-height: 1; }
.dm-st-tile__lbl { font-size: 12px; font-weight: 700; color: #334155; }
.dm-st-tile__sub { grid-column: 1 / 3; margin-top: 4px; font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
.dm-st-file { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; padding: 9px 12px; border-radius: 12px;
background: rgba(15,23,42,0.04); border: 1px dashed rgba(15,23,42,0.18); }
.dm-st-file__ico { font-size: 15px; }
.dm-st-file__name { font-size: 13px; font-weight: 700; color: #0f172a; }
.dm-st-file__meta { font-size: 11.5px; color: #64748b; }
.dm-st-file__ok { margin-left: auto; font-size: 12px; font-weight: 800; color: #22C55E; }
.dm-st-riders { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.dm-st-rider { display: grid; grid-template-columns: 26px 1fr auto auto; align-items: center; gap: 9px;
padding: 6px 8px; border-radius: 10px; background: rgba(15,23,42,0.03); }
.dm-st-rider__av { position: relative; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 800; color: #fff; background: linear-gradient(135deg, var(--c), color-mix(in srgb, var(--c) 60%, #1e293b)); }
.dm-st-rider__av i { position: absolute; right: -1px; bottom: -1px; width: 8px; height: 8px; border-radius: 50%; background: #22C55E; border: 2px solid #fff; }
.dm-st-rider__name { font-size: 12.5px; font-weight: 700; color: #0f172a; }
.dm-st-rider__veh { font-size: 11px; font-weight: 600; color: #475569; padding: 2px 8px; border-radius: 999px; background: rgba(15,23,42,0.06); }
.dm-st-rider__cap { font-size: 11px; color: #64748b; white-space: nowrap; }
/* STAGE 02 — Parallel execution */
.dm-st-engines { display: grid; gap: 10px; }
.dm-st-engine { padding: 11px 12px; border-radius: 14px; background: rgba(15,23,42,0.03); border: 1px solid rgba(15,23,42,0.07); }
.dm-st-engine.is-unified { background: color-mix(in srgb, var(--c) 8%, white); border-color: color-mix(in srgb, var(--c) 28%, white); }
.dm-st-engine__head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.dm-st-engine__name { font-size: 13px; font-weight: 800; color: #0f172a; }
.dm-st-engine__tag { font-size: 9.5px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: #64748b;
background: rgba(15,23,42,0.07); padding: 2px 7px; border-radius: 6px; }
.dm-st-engine__tag--u { color: #fff; background: var(--c); }
.dm-st-pills { display: flex; flex-wrap: wrap; gap: 7px; }
.dm-st-pill { font-size: 12px; font-weight: 700; color: #334155; padding: 6px 12px; border-radius: 999px;
background: rgba(255,255,255,0.9); border: 1px solid rgba(15,23,42,0.12); }
.dm-st-pill--u { color: #0f172a; background: color-mix(in srgb, var(--c) 14%, white); border-color: color-mix(in srgb, var(--c) 40%, white); }
/* STAGE 03 — Optimization pipeline */
.dm-st-pipe { display: grid; gap: 0; }
.dm-st-pipe__node { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 11px; align-items: center;
padding: 11px 14px; border-radius: 14px; background: rgba(255,255,255,0.92);
border: 1px solid color-mix(in srgb, var(--c) 30%, white); box-shadow: 0 8px 22px -14px var(--c); }
.dm-st-pipe__ico { grid-row: 1 / 3; font-size: 22px; }
.dm-st-pipe__name { font-size: 13.5px; font-weight: 800; color: #0f172a; }
.dm-st-pipe__desc { font-size: 11.5px; color: #64748b; }
.dm-st-pipe__arrow { display: flex; align-items: center; justify-content: center; height: 22px; }
.dm-st-pipe__arrow i { position: relative; width: 2px; height: 22px; background: linear-gradient(180deg, color-mix(in srgb, var(--c) 50%, transparent), var(--c)); overflow: visible; }
.dm-st-pipe__arrow i::after { content: ""; position: absolute; left: 50%; top: -4px; width: 6px; height: 6px; border-radius: 50%;
background: var(--c); box-shadow: 0 0 8px var(--c); transform: translateX(-50%); animation: dmStFlow 1.4s linear infinite; }
@keyframes dmStFlow { 0% { top: -4px; opacity: 0; } 20% { opacity: 1; } 100% { top: 22px; opacity: 0; } }
/* STAGE 04 — Performance grading */
.dm-st-stars { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; }
.dm-st-stars__on { color: var(--c); letter-spacing: 2px; font-size: 16px; }
.dm-st-stars__off { color: rgba(15,23,42,0.18); font-size: 16px; }
.dm-st-stars__txt { font-size: 12px; font-weight: 700; color: #475569; margin-left: 4px; }
.dm-st-metrics { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.dm-st-metric { display: grid; grid-template-columns: 1fr 70px auto; grid-template-rows: auto auto; column-gap: 10px; row-gap: 3px; align-items: center; }
.dm-st-metric__name { font-size: 12.5px; font-weight: 700; color: #0f172a; }
.dm-st-metric__bar { grid-column: 1 / 2; grid-row: 2; height: 6px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
.dm-st-metric__bar i { display: block; height: 100%; border-radius: 999px; background: var(--c); }
.dm-st-metric__score { grid-column: 2; grid-row: 1 / 3; font-size: 17px; font-weight: 800; color: #0f172a; text-align: right; }
.dm-st-metric__status { grid-column: 3; grid-row: 1 / 3; font-size: 11px; font-weight: 800; color: #16a34a;
background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.3); padding: 3px 8px; border-radius: 999px; white-space: nowrap; }
.dm-st-metric__desc { grid-column: 1 / 2; grid-row: 1; font-size: 10.5px; color: #64748b; align-self: end; display: none; }
/* STAGE 05 — Strategy comparison (hero) */
.dm-st-winner { margin-bottom: 14px; }
.dm-st-winner__eyebrow { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: #64748b; }
.dm-st-winner__name { display: flex; align-items: center; gap: 10px; margin: 4px 0; font-size: clamp(24px, 2.8vw, 34px); font-weight: 800; color: #0f172a; letter-spacing: -0.02em; }
.dm-st-winner__grade { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 0.04em; color: #fff;
background: linear-gradient(90deg, #C01227, #E2354A); padding: 4px 11px; border-radius: 999px; }
.dm-st-trophy { font-size: 32px; filter: drop-shadow(0 8px 18px rgba(192,18,39,0.4)); animation: dmStFloat 3s ease-in-out infinite; }
@keyframes dmStFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
.dm-st-hero { display: flex; align-items: center; gap: 20px; }
.dm-st-hero__ring { position: relative; flex-shrink: 0; width: 96px; height: 96px; border-radius: 50%; display: flex; flex-direction: column;
align-items: center; justify-content: center; background: conic-gradient(#C01227 88%, rgba(15,23,42,0.1) 0); }
.dm-st-hero__ring::after { content: ""; position: absolute; inset: 9px; border-radius: 50%; background: #fff; }
.dm-st-hero__pct { position: relative; z-index: 1; font-size: 24px; font-weight: 800; color: #C01227; }
.dm-st-hero__sub { position: relative; z-index: 1; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: #64748b; }
.dm-st-wins { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.dm-st-wins li { font-size: 13px; color: #334155; display: flex; align-items: baseline; gap: 8px; }
.dm-st-wins strong { color: #C01227; font-weight: 800; min-width: 48px; }
@media (max-width: 1000px) {
.dm-st-rail__title { display: none; }
.dm-st-rail__step { padding: 5px 7px; }
.dm-st-rail__line { width: 9px; }
}
@media (max-width: 767px) {
.dm-st { height: 560vh; }
.dm-st-card-story { left: 0 !important; right: 0 !important; margin: 0 auto; width: calc(100% - 28px);
bottom: clamp(18px, 4vh, 40px); padding: 15px 16px; }
.dm-st-rider__cap { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.dm-st-arrow, .dm-st-trophy { animation: none !important; }
}
`;

View File

@@ -0,0 +1,67 @@
import { STAGES, N, Z_SPACING, X_OFFSET, stagePosition, samplePath } from "./theme";
describe("strategy/theme — stage data", () => {
it("exposes five stages and a matching N", () => {
expect(STAGES).toHaveLength(5);
expect(N).toBe(5);
});
it("has unique, well-formed stage descriptors", () => {
const keys = STAGES.map((s) => s.key);
expect(new Set(keys).size).toBe(keys.length);
for (const s of STAGES) {
expect(s.n).toMatch(/^\d{2}$/);
expect(s.theme).toMatch(/^#[0-9A-Fa-f]{6}$/);
expect(s.kicker.length).toBeGreaterThan(0);
}
});
});
describe("strategy/theme — stagePosition()", () => {
it("alternates x sign by parity and steps z by -Z_SPACING", () => {
expect(stagePosition(0)).toEqual([-X_OFFSET, -0.45, -0]);
expect(stagePosition(1)).toEqual([X_OFFSET, 0.55, -Z_SPACING]);
expect(stagePosition(2)).toEqual([-X_OFFSET, -0.45, -2 * Z_SPACING]);
expect(stagePosition(3)).toEqual([X_OFFSET, 0.55, -3 * Z_SPACING]);
});
it("places even stages left and odd stages right", () => {
for (let i = 0; i < N; i++) {
const [x] = stagePosition(i);
if (i % 2 === 0) expect(x).toBeLessThan(0);
else expect(x).toBeGreaterThan(0);
}
});
});
describe("strategy/theme — samplePath()", () => {
it("equals stagePosition() at integer indices", () => {
for (let i = 0; i < N; i++) {
const sampled = samplePath(i);
const exact = stagePosition(i);
sampled.forEach((v, k) => expect(v).toBeCloseTo(exact[k], 10));
}
});
it("linearly interpolates between adjacent stages", () => {
const [x, y, z] = samplePath(0.5);
expect(x).toBeCloseTo(0, 10); // midpoint of -2.5 and +2.5
expect(y).toBeCloseTo(0.05, 10); // midpoint of -0.45 and 0.55
expect(z).toBeCloseTo(-Z_SPACING / 2, 10);
});
it("clamps indices below 0 to the first stage", () => {
expect(samplePath(-3)).toEqual(stagePosition(0));
});
it("clamps indices above N-1 to the last stage", () => {
expect(samplePath(99)).toEqual(stagePosition(N - 1));
});
it("never produces NaN across a fine sweep", () => {
for (let p = 0; p <= 1.0001; p += 0.05) {
const idx = p * (N - 1);
for (const v of samplePath(idx)) expect(Number.isNaN(v)).toBe(false);
}
});
});

View File

@@ -0,0 +1,43 @@
// Shared data + geometry for the "Strategy" 3D scroll-storytelling section.
// Five themed stages laid out along a zig-zag path in 3D. The camera travels
// stage-to-stage as a normalized scroll progress (0→1) advances.
export type StageTheme = {
n: string; // "01"
key: string;
kicker: string; // short label for the step rail
theme: string; // hex accent
};
export const STAGES: StageTheme[] = [
{ n: "01", key: "input", kicker: "Input", theme: "#22C55E" }, // green
{ n: "02", key: "parallel", kicker: "Parallel Execution", theme: "#8B5CF6" }, // purple
{ n: "03", key: "optimize", kicker: "Smart Optimization", theme: "#3B82F6" }, // blue
{ n: "04", key: "grading", kicker: "Performance Grading", theme: "#F59E0B" }, // orange
{ n: "05", key: "winner", kicker: "Strategy Comparison", theme: "#C01227" }, // red — hero
];
export const N = STAGES.length;
// Zig-zag layout constants (world units).
export const Z_SPACING = 6.4;
export const X_OFFSET = 2.5;
/** Resting world position of stage `i` along the zig-zag path. */
export function stagePosition(i: number): [number, number, number] {
const x = (i % 2 === 0 ? -1 : 1) * X_OFFSET;
const y = i % 2 === 0 ? -0.45 : 0.55;
const z = -i * Z_SPACING;
return [x, y, z];
}
/** Continuous position sampled at a fractional stage index (lerp between stages). */
export function samplePath(idx: number): [number, number, number] {
const clamped = Math.max(0, Math.min(N - 1, idx));
const f = Math.floor(clamped);
const c = Math.min(N - 1, f + 1);
const t = clamped - f;
const a = stagePosition(f);
const b = stagePosition(c);
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}