update solution page

This commit is contained in:
2026-07-29 17:47:05 +05:30
parent 136cc966c7
commit fee43cec3e
16 changed files with 4740 additions and 108 deletions

View File

@@ -14856,7 +14856,7 @@ img:is([sizes=auto i]) {
}
.elementor-element.elementor-element-7da6646:not(.elementor-motion-effects-element-type-background) {
background-image: url("/images/bg-header-5.webp");
background-image: url("/images/ev.webp");
background-position: center center;
background-repeat: no-repeat;
background-size: cover;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 0 B

BIN
public/images/ev fleet1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -27,7 +27,7 @@ export default function Home() {
<MarqueeTicker />
<ConnectedLogistics />
<TheDoormileWay />
<EVSection />
<EVSection bannerImage="/images/bg-header-5.png" image="/images/premium-ev-van.webp" />
<IndustrySolutions />
</div>
</div>

View File

@@ -1,6 +1,8 @@
import React from "react";
import SolutionsHero, { type HeroSlide } from "@/components/sections/SolutionsHero";
import Miles3 from "@/components/sections/Miles3";
import PlatformCapabilities from "@/components/sections/PlatformCapabilities";
import DeliveredCounter from "@/components/sections/DeliveredCounter";
import EVSection, { type EVFeature } from "@/components/sections/EVSection";
import {
SolutionsSectionsShell,
@@ -96,7 +98,17 @@ export default function SolutionsPage() {
so their Elementor markup is unaffected. */}
<SolutionsSectionsShell>
<SolOfferings />
<Miles3 eyebrow="/ What We Offer /" />
{/* Platform Capabilities carousel is passed as children so it
renders INSIDE the Miles3 dark card, directly under the
first/mid/last mile grid — one continuous "What We Offer"
section, not a second one. Scoped to #pcap-root, so it does
not inherit anything from the shell's `.sol-*` stylesheet. */}
<Miles3 eyebrow="/ What We Offer /">
{/* Big count-up stat over the dotted world map, then the
capabilities carousel — both inside the same dark card. */}
<DeliveredCounter />
<PlatformCapabilities />
</Miles3>
<SolWingsSpotlight />
<EVSection
eyebrow="/ EV Fleet /"
@@ -106,6 +118,8 @@ export default function SolutionsPage() {
titleAccent="YOUR ROUTES."
features={EV_FLEET_FEATURES}
ariaLabel="Doormile EV fleet"
bannerImage="/images/ev fleet1.png"
image="/images/ev.webp"
/>
<SolutionsWhyDoormile />
<SolMileTruthBand />

View File

@@ -0,0 +1,368 @@
"use client";
import React, { useState, useEffect } from "react";
interface AiCoreAnimationProps {
variant?: "compact" | "full";
className?: string;
}
export default function AiCoreAnimation({
variant = "full",
className = "",
}: AiCoreAnimationProps) {
const isCompact = variant === "compact";
// Cycle phase for organic ecosystem simulation (0 to 10)
const [phase, setPhase] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setPhase((prev) => (prev + 1) % 11);
}, 1250); // ~13.7s total continuous ecosystem cycle
return () => clearInterval(timer);
}, []);
if (isCompact) {
return (
<div className={`sol-s05-svg-core ${className}`} style={{ width: "100%", height: "100%", position: "relative" }}>
<style jsx>{`
.compact-route-dash {
stroke-dasharray: 4 6;
animation: cDashFlow 2s linear infinite;
}
@keyframes cDashFlow { to { stroke-dashoffset: -20; } }
`}</style>
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: "100%", height: "100%" }}>
<path d="M 10 30 H 90 M 10 70 H 90 M 30 10 V 90 M 70 10 V 90" stroke="rgba(14,14,16,0.1)" strokeWidth="1.5" />
<path d="M 30 70 C 45 30, 55 30, 70 30" stroke="#4ADE80" strokeWidth="2.5" strokeLinecap="round" className="compact-route-dash" />
<circle cx="30" cy="70" r="4" fill="#0C0C14" />
<circle cx="70" cy="30" r="4" fill="#4ADE80" />
</svg>
</div>
);
}
// Ecosystem phase indicators
const isTrafficActive = phase >= 2 && phase <= 4;
const isNetworkReacting = phase >= 3 && phase <= 8;
const isBypassIlluminated = phase >= 4 && phase <= 8;
const isEtaImproved = phase >= 6;
const isDelivered = phase >= 8;
const isSlaProtected = phase >= 9;
return (
<div
className={`mt-living-ecosystem ${className}`}
style={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
>
<style jsx>{`
.ecosystem-hero-svg {
width: 100%;
height: 100%;
max-width: 680px;
max-height: 440px;
overflow: visible;
}
/* Flowing Laser Signal Dashes across City Network */
.city-laser-flow {
stroke-dasharray: 10 16;
animation: cityLaserDash 2.4s linear infinite;
}
.city-laser-fast {
stroke-dasharray: 14 10;
animation: cityLaserDash 1.5s linear infinite;
}
@keyframes cityLaserDash {
to { stroke-dashoffset: -52; }
}
/* Traffic Congestion Segment Glow */
.traffic-red-pulse {
animation: redPulse 1.2s ease-in-out infinite alternate;
}
@keyframes redPulse {
0% { stroke-opacity: 0.45; }
100% { stroke-opacity: 1; }
}
/* Smooth Easing Vehicle Motion */
.vehicle-pod-smooth {
transition: all 1.15s cubic-bezier(0.25, 1, 0.5, 1);
}
/* Floating Apple-Style Badges */
.apple-pill-float {
animation: appleFloat 3s ease-in-out infinite alternate;
}
@keyframes appleFloat {
0% { transform: translateY(0px); }
100% { transform: translateY(-4px); }
}
/* Ambient Particle Drift */
.p-drift-1 { animation: pDrift1 14s linear infinite; }
.p-drift-2 { animation: pDrift2 18s linear infinite; }
@keyframes pDrift1 {
0% { transform: translate(0, 0); }
50% { transform: translate(15px, -12px); }
100% { transform: translate(0, 0); }
}
@keyframes pDrift2 {
0% { transform: translate(0, 0); }
50% { transform: translate(-20px, 15px); }
100% { transform: translate(0, 0); }
}
`}</style>
<svg
viewBox="0 0 680 440"
className="ecosystem-hero-svg"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Micro Dot Grid Pattern for Clean Off-White Background */}
<pattern id="appleHeroDotGrid" width="24" height="24" patternUnits="userSpaceOnUse">
<circle cx="12" cy="12" r="1" fill="rgba(14, 14, 16, 0.05)" />
</pattern>
{/* Emerald Green Route Gradient */}
<linearGradient id="emeraldCityGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#22C55E" />
<stop offset="50%" stopColor="#4ADE80" />
<stop offset="100%" stopColor="#22C55E" />
</linearGradient>
{/* Red Congestion Route Gradient */}
<linearGradient id="redTrafficGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#EF4444" />
<stop offset="100%" stopColor="#DC2626" />
</linearGradient>
{/* Apple Product Artwork Soft Shadow */}
<filter id="appleHeroShadow" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="4" stdDeviation="6" floodColor="#000000" floodOpacity="0.07" />
</filter>
</defs>
{/* ============================================================
1. OFF-WHITE CANVAS & AMBIENT TELEMETRY PARTICLES
============================================================ */}
<rect width="680" height="440" fill="url(#appleHeroDotGrid)" opacity="0.9" />
{/* Ambient Telemetry Particles Drifting Across Network */}
<g className="p-drift-1">
<circle cx="140" cy="80" r="1.5" fill="rgba(14,14,16,0.25)" />
<circle cx="520" cy="380" r="2" fill="rgba(74,222,128,0.3)" />
<circle cx="340" cy="60" r="1.5" fill="rgba(14,14,16,0.2)" />
</g>
<g className="p-drift-2">
<circle cx="480" cy="360" r="1.5" fill="rgba(14,14,16,0.25)" />
<circle cx="200" cy="380" r="2" fill="rgba(239,68,68,0.3)" />
<circle cx="580" cy="80" r="1.5" fill="rgba(14,14,16,0.2)" />
</g>
{/* ============================================================
2. ABSTRACT CITY LOGISTICS NETWORK TOPOLOGY
============================================================ */}
{/* THIN GREY ARTERIAL HIGHWAYS & STREETS */}
<path d="M 80 100 H 600" stroke="rgba(14, 14, 16, 0.1)" strokeWidth="2" />
<path d="M 80 220 H 600" stroke="rgba(14, 14, 16, 0.12)" strokeWidth="2.5" />
<path d="M 80 340 H 600" stroke="rgba(14, 14, 16, 0.1)" strokeWidth="2" />
{/* CONNECTING VERTICAL AVENUES */}
<path d="M 200 60 V 380" stroke="rgba(14, 14, 16, 0.08)" strokeWidth="1.8" />
<path d="M 340 60 V 380" stroke="rgba(14, 14, 16, 0.08)" strokeWidth="1.8" />
<path d="M 480 60 V 380" stroke="rgba(14, 14, 16, 0.08)" strokeWidth="1.8" />
{/* DIAGONAL BYPASS CONNECTORS & RING EXPRESSWAY ARCS */}
<path d="M 200 220 L 340 100 L 480 220" stroke="rgba(14, 14, 16, 0.09)" strokeWidth="2" fill="none" />
<path d="M 200 220 L 340 340 L 480 220" stroke="rgba(14, 14, 16, 0.09)" strokeWidth="2" fill="none" />
<path d="M 80 220 C 180 60, 500 60, 600 220" stroke="rgba(14, 14, 16, 0.06)" strokeWidth="1.5" strokeDasharray="4 6" fill="none" />
{/* LIVE DATA SIGNALS FLOWING ACROSS CITY NETWORK */}
<path d="M 80 100 H 600" stroke="rgba(14, 14, 16, 0.2)" strokeWidth="1.2" fill="none" className="city-laser-flow" />
<path d="M 80 340 H 600" stroke="rgba(14, 14, 16, 0.2)" strokeWidth="1.2" fill="none" className="city-laser-flow" />
<path d="M 340 60 V 380" stroke="rgba(14, 14, 16, 0.15)" strokeWidth="1.2" fill="none" className="city-laser-flow" />
{/* ============================================================
3. DYNAMIC ROAD STATES (Congestion Red vs Emerald Bypass)
============================================================ */}
{/* CONGESTED ROAD SEGMENT (200, 220 -> 480, 220) */}
<path
d="M 200 220 H 480"
stroke={isTrafficActive ? "url(#redTrafficGrad)" : "rgba(14, 14, 16, 0.12)"}
strokeWidth={isTrafficActive ? "5" : "2.5"}
strokeDasharray={isBypassIlluminated ? "4 8" : "none"}
className={isTrafficActive ? "traffic-red-pulse" : ""}
fill="none"
/>
{/* ORGANIC TRAFFIC ALERT INDICATOR AT CONGESTION ZONE */}
{isTrafficActive && (
<g transform="translate(340, 220)">
<circle cx="0" cy="0" r="16" fill="rgba(239, 68, 68, 0.2)" />
<circle cx="0" cy="0" r="7" fill="#EF4444" filter="url(#appleHeroShadow)" />
</g>
)}
{/* SUBTLE ILLUMINATED EMERALD GREEN BYPASS ROAD (200, 220 -> 340, 100 -> 480, 220) */}
{isBypassIlluminated && (
<g>
<path
d="M 200 220 L 340 100 L 480 220"
stroke="url(#emeraldCityGrad)"
strokeWidth="4.5"
strokeLinecap="round"
fill="none"
className="city-laser-fast"
filter="url(#appleHeroShadow)"
/>
{/* Ambient Green Glow Trail */}
<path
d="M 200 220 L 340 100 L 480 220"
stroke="#4ADE80"
strokeWidth="10"
strokeOpacity="0.22"
strokeLinecap="round"
fill="none"
/>
</g>
)}
{/* ============================================================
4. MINIMAL LOGISTICS FACILITIES (Warehouses, Hubs, Destinations)
============================================================ */}
{/* Warehouse 1 (North Dispatch at 80, 100) */}
<g transform="translate(80, 100)">
<rect x="-14" y="-12" width="28" height="24" rx="5" fill="#FFFFFF" stroke="#0C0C14" strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<path d="M-5 4V-2l5-3 5 3v5" stroke="#0C0C14" strokeWidth="1.2" fill="none" />
</g>
{/* Warehouse 2 (Main Dispatch at 80, 220) */}
<g transform="translate(80, 220)">
<rect x="-16" y="-14" width="32" height="28" rx="6" fill="#0C0C14" stroke="#FFFFFF" strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<path d="M-6 5V-2l6-4 6 4v7" stroke="#FFFFFF" strokeWidth="1.5" fill="none" />
</g>
{/* Distribution Hub 1 (200, 220) */}
<circle cx="200" cy="220" r="7" fill="#FFFFFF" stroke="#0C0C14" strokeWidth="1.8" filter="url(#appleHeroShadow)" />
{/* Distribution Hub 2 (340, 100) */}
<circle cx="340" cy="100" r="7" fill={isBypassIlluminated ? "#22C55E" : "#FFFFFF"} stroke={isBypassIlluminated ? "#22C55E" : "#0C0C14"} strokeWidth="1.8" filter="url(#appleHeroShadow)" />
{/* Distribution Hub 3 (480, 220) */}
<circle cx="480" cy="220" r="7" fill="#FFFFFF" stroke="#0C0C14" strokeWidth="1.8" filter="url(#appleHeroShadow)" />
{/* Destination 1 (North Target at 600, 100) */}
<circle cx="600" cy="100" r="8" fill="#FFFFFF" stroke="rgba(14,14,16,0.3)" strokeWidth="1.5" />
{/* Destination 2 (Main Target at 600, 220) */}
<g transform="translate(600, 220)">
<circle cx="0" cy="0" r="16" fill={isDelivered ? "rgba(34,197,94,0.18)" : "#FFFFFF"} stroke={isDelivered ? "#22C55E" : "#0C0C14"} strokeWidth="2" filter="url(#appleHeroShadow)" />
<circle cx="0" cy="0" r="5" fill={isDelivered ? "#22C55E" : "#0C0C14"} />
</g>
{/* Destination 3 (South Target at 600, 340) */}
<circle cx="600" cy="340" r="8" fill="#FFFFFF" stroke="rgba(14,14,16,0.3)" strokeWidth="1.5" />
{/* ============================================================
5. MULTIPLE DELIVERY VEHICLES MOVING SIMULTANEOUSLY
============================================================ */}
{/* VEHICLE 1 (PRIMARY FLEET UNIT - REROUTES UPON CONGESTION) */}
{(() => {
let vx = 80, vy = 220;
if (phase === 0) { vx = 80; vy = 220; }
else if (phase === 1) { vx = 140; vy = 220; }
else if (phase === 2) { vx = 200; vy = 220; } // Encounters traffic at Hub 1 (200, 220)
else if (phase === 3) { vx = 210; vy = 210; } // Network reacts
else if (phase === 4) { vx = 270; vy = 160; } // Smoothly curves onto Green Bypass Road
else if (phase === 5) { vx = 340; vy = 100; } // At Hub 2 Apex (340, 100)
else if (phase === 6) { vx = 410; vy = 160; } // Sweeping down to Hub 3 (480, 220)
else if (phase === 7) { vx = 480; vy = 220; } // At Hub 3
else if (phase >= 8) { vx = 600; vy = 220; } // Delivered at Destination 2
return (
<g transform={`translate(${vx}, ${vy})`} className="vehicle-pod-smooth">
<circle cx="0" cy="0" r="13" fill={isBypassIlluminated ? "rgba(34, 197, 94, 0.28)" : "rgba(226, 53, 74, 0.25)"} />
<rect x="-9" y="-6" width="18" height="12" rx="2.5" fill="#0C0C14" stroke={isBypassIlluminated ? "#22C55E" : "#E2354A"} strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<circle cx="4" cy="0" r="1.5" fill="#FFFFFF" />
</g>
);
})()}
{/* VEHICLE 2 (NORTH ARTERIAL VAN - MOVES UNINTERRUPTED) */}
<g transform={`translate(${80 + ((phase * 52) % 520)}, 100)`} className="vehicle-pod-smooth">
<rect x="-8" y="-5" width="16" height="10" rx="2" fill="#0C0C14" stroke="rgba(255,255,255,0.8)" strokeWidth="1" />
<circle cx="3" cy="0" r="1" fill="#FFFFFF" />
</g>
{/* VEHICLE 3 (SOUTH COURIER VAN - MOVES UNINTERRUPTED) */}
<g transform={`translate(${600 - ((phase * 48) % 520)}, 340)`} className="vehicle-pod-smooth">
<rect x="-8" y="-5" width="16" height="10" rx="2" fill="#0C0C14" stroke="rgba(255,255,255,0.8)" strokeWidth="1" />
<circle cx="-3" cy="0" r="1" fill="#FFFFFF" />
</g>
{/* VEHICLE 4 (CROSS-TOWN RELAY VAN) */}
<g transform={`translate(340, ${380 - ((phase * 32) % 320)})`} className="vehicle-pod-smooth">
<rect x="-5" y="-8" width="10" height="16" rx="2" fill="#0C0C14" stroke="rgba(255,255,255,0.8)" strokeWidth="1" />
</g>
{/* VEHICLE 5 (RING EXPRESSWAY SHUTTLE) */}
<g transform={`translate(${480 - ((phase * 28) % 280)}, 340)`} className="vehicle-pod-smooth">
<circle cx="0" cy="0" r="4.5" fill="#0C0C14" stroke="#E2354A" strokeWidth="1" />
</g>
{/* ============================================================
6. APPLE-STYLE MINIMAL BADGES (ETA & SLA CONFIRMATION)
============================================================ */}
{/* Live ETA Badge (Top Right) */}
<g transform="translate(485, 30)" className="apple-pill-float">
<rect x="0" y="0" width="165" height="42" rx="21" fill="#FFFFFF" stroke={isEtaImproved ? "#22C55E" : "rgba(14,14,16,0.15)"} strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<circle cx="21" cy="21" r="5" fill={isEtaImproved ? "#22C55E" : "#E2354A"} />
<text x="34" y="25" fill="#0E0E12" fontSize="13" fontWeight="800" fontFamily="sans-serif">
{isEtaImproved ? "41 min" : isTrafficActive ? "58 min" : "45 min"}
</text>
{isEtaImproved && (
<text x="96" y="25" fill="#22C55E" fontSize="9.5" fontWeight="800" fontFamily="sans-serif">
· Saved 17m
</text>
)}
</g>
{/* SLA Protected Pill (Appears at Step 9 Post-Reroute) */}
{isSlaProtected && (
<g transform="translate(460, 275)" className="apple-pill-float">
<rect x="0" y="0" width="165" height="32" rx="16" fill="#FFFFFF" stroke="#22C55E" strokeWidth="1.2" filter="url(#appleHeroShadow)" />
<circle cx="16" cy="16" r="4" fill="#22C55E" />
<text x="28" y="20" fill="#0E0E12" fontSize="10" fontWeight="800" fontFamily="sans-serif">
SLA Protected (99.2%)
</text>
</g>
)}
</svg>
</div>
);
}

View File

@@ -0,0 +1,143 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
/* ============================================================
DELIVERED COUNTER — Solutions page
The big outlined stat that, in the Logico reference, sits between the
mile cards and the "what we do" carousel: a large count-up number over a
dotted world map, with a rotated caption alongside it.
Rebuilt with Doormile branding — the site's red accent instead of teal,
the project's own dotted-map asset (public/images/bg-map.png), and a
logistics stat. It renders INSIDE the Miles3 dark card (as a sibling of
the carousel), so it reads as part of the same section. Transparent
surface; everything scoped to #dcount-root.
============================================================ */
const TARGET = 24_836_517;
const DURATION = 2200;
/** de-DE groups thousands with ".", matching the reference's 358.895.941. */
function format(n: number): string {
return Math.round(n).toLocaleString("de-DE");
}
export default function DeliveredCounter() {
const rootRef = useRef<HTMLElement>(null);
const [val, setVal] = useState(0);
useEffect(() => {
const el = rootRef.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let raf = 0;
let fired = false;
const io = new IntersectionObserver(
(entries) => {
const e = entries[0];
if (!e.isIntersecting || fired) return;
fired = true;
io.disconnect();
if (reduce) { setVal(TARGET); return; }
const start = performance.now();
const tick = (now: number) => {
const p = Math.min(1, (now - start) / DURATION);
const eased = 1 - Math.pow(1 - p, 3); // easeOutCubic
setVal(TARGET * eased);
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
},
{ threshold: 0.35 }
);
io.observe(el);
return () => { io.disconnect(); cancelAnimationFrame(raf); };
}, []);
return (
<>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
<section id="dcount-root" ref={rootRef} aria-label="Deliveries completed to date">
<span className="dc-map" aria-hidden="true" />
<div className="dc-fig">
<span className="dc-num">{format(val)}</span>
<span className="dc-label" aria-hidden="true">
<b>Parcels delivered</b>
<i>end to end</i>
</span>
<span className="dc-sr">
{format(TARGET)} parcels delivered end to end
</span>
</div>
</section>
</>
);
}
const CSS = `
#dcount-root {
--pc-red-bright: #E2354A;
position: relative;
display: flex; align-items: center; justify-content: center;
padding: 0;
margin: 0;
overflow: visible;
}
#dcount-root, #dcount-root * { font-family: var(--font-manrope), "Manrope", sans-serif; box-sizing: border-box; }
/* Dotted world map backdrop — naturally overlays the statistic counter */
#dcount-root .dc-map {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: min(1300px, 98%); height: 100%; min-height: 380px; z-index: 0; pointer-events: none;
background: url(/images/bg-map.png) center / contain no-repeat;
filter: invert(1) brightness(2.2);
opacity: 0.18;
-webkit-mask-image: radial-gradient(85% 85% at 50% 50%, #000 50%, transparent 100%);
mask-image: radial-gradient(85% 85% at 50% 50%, #000 50%, transparent 100%);
}
#dcount-root .dc-fig {
position: relative; z-index: 1;
display: inline-flex; align-items: center; justify-content: center; gap: clamp(14px, 1.8vw, 30px);
}
/* Big outlined numeral, red stroke, transparent fill */
#dcount-root .dc-num {
color: transparent;
-webkit-text-stroke: 2px var(--pc-red-bright);
font-size: clamp(58px, 12.5vw, 200px);
font-weight: 500; line-height: 0.92; letter-spacing: -0.01em;
font-variant-numeric: tabular-nums; white-space: nowrap;
text-shadow: 0 0 40px rgba(226,53,74,0.18);
}
/* Firefox/older engines without text-stroke: fall back to a faint red fill so
the number never disappears. */
@supports not ((-webkit-text-stroke: 2px red)) {
#dcount-root .dc-num { color: rgba(226,53,74,0.85); }
}
/* Rotated caption, reading bottom-to-top like the reference */
#dcount-root .dc-label {
writing-mode: vertical-rl; transform: rotate(180deg);
display: inline-flex; align-items: center; gap: 10px;
color: #fff; line-height: 1.15; padding-bottom: 4px;
}
#dcount-root .dc-label b { font-size: clamp(12px, 1.1vw, 15px); font-weight: 700; letter-spacing: 0.02em; }
#dcount-root .dc-label i { font-style: normal; color: rgba(255,255,255,0.5); font-size: clamp(11px, 1vw, 14px); font-weight: 600; }
/* Screen-reader-only final figure (the visible number animates via aria-hidden fig) */
#dcount-root .dc-sr {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (max-width: 639px) {
#dcount-root { padding: 24px 0 20px; }
#dcount-root .dc-num { -webkit-text-stroke-width: 1.5px; }
#dcount-root .dc-fig { gap: 12px; }
}
`;

View File

@@ -343,7 +343,7 @@ function DashboardPanel({
}
export default function EVSection({
bannerImage = "/images/bg-header-5.webp",
bannerImage = "/images/ev.webp",
cardNumber = "",
cardTitle = "EV Logistics",
cardSubtitle = "Cleaner miles, lower costs",
@@ -351,7 +351,7 @@ export default function EVSection({
titleLead = "BUILT FOR ELECTRIC. ",
titleAccent = "NOT ADAPTED.",
features = DEFAULT_FEATURES,
image = "/images/premium-ev-van.webp",
image = "/images/ev.webp",
imageAlt = "DoorMile electric fleet van",
badges = DEFAULT_BADGES,
stats = DEFAULT_STATS,
@@ -768,20 +768,30 @@ export default function EVSection({
.evnd-feature__icon-container {
width: 48px; height: 48px;
display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
background: rgba(239, 68, 68, 0.14);
border: 1px solid rgba(239, 68, 68, 0.35);
border-radius: 12px;
transition: background-color 0.3s ease, border-color 0.3s ease;
color: #ef4444 !important;
transition: background-color 0.3s ease, border-color 0.3s ease, 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);
background: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.6);
color: #ff5555 !important;
}
.evnd-icon {
width: 22px;
height: 22px;
display: block;
color: #ef4444 !important;
stroke: #ef4444 !important;
}
.evnd-icon path,
.evnd-icon circle,
.evnd-icon polygon,
.evnd-icon rect {
stroke: #ef4444 !important;
}
.evnd-feature__title {
@@ -1297,7 +1307,7 @@ export default function EVSection({
data-settings='{"background_background":"classic"}'
style={{
backgroundPosition: "center 0px",
backgroundImage: `url(${bannerImage})`,
backgroundImage: `url("${encodeURI(bannerImage)}")`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
position: "relative",

View File

@@ -8,11 +8,20 @@ interface Miles3Props {
eyebrow?: string;
/** Section heading. Defaults to the original How It Works copy. */
heading?: string;
/**
* Extra content rendered INSIDE the same dark card, directly below the
* First/Mid/Last Mile grid — so it reads as one continuous section rather
* than a new one. Used on the Solutions page for the Platform Capabilities
* carousel. When present, the card also gains a bottom pad so the appended
* block never touches the rounded edge.
*/
children?: React.ReactNode;
}
export default function Miles3({
eyebrow = "/ How It Works /",
heading = "Doormile connects first, mid, and last mile into a seamless end-to-end logistics experience",
children,
}: Miles3Props = {}) {
return (
<>
@@ -31,6 +40,7 @@ export default function Miles3({
.elementor-element-c36a604 {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: auto;
margin: 20px 20px 0 20px;
background-color: #1F1F1F;
@@ -49,6 +59,7 @@ export default function Miles3({
.elementor-element-77d1265 {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 100%;
max-width: 1630px;
margin: 0 auto;
@@ -114,6 +125,13 @@ export default function Miles3({
not grid-template-columns directly, or the grid never collapses on mobile. */
--e-con-grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, 1fr);
/* Same trap for the ROWS: ".e-con.e-grid" also drives grid-template-rows
from a var that defaults to repeat(2, 1fr), reserving a second, EMPTY
row (plus the 70px row-gap) under the 3 mile cards — ~380px of dead
space before whatever follows in the card. "auto" sizes the one real
row to its content and still lets the 2-col/1-col breakpoints create
implicit rows. */
--e-con-grid-template-rows: auto;
grid-auto-flow: row;
gap: 70px 60px;
width: 100%;
@@ -171,6 +189,22 @@ export default function Miles3({
margin: 0;
}
/* Continuation block (Platform Capabilities carousel) — lives inside the
same dark card, below the mile grid. Its own top border + generous
top/bottom rhythm keep it reading as "more of this section". The
card itself has no bottom padding, so this block owns the bottom
breathing room too. */
.miles3-continue {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 100%;
min-width: 0;
align-self: stretch;
margin-top: 28px;
padding-bottom: clamp(60px, 6vw, 90px);
}
/* ---- Responsive (grid breakpoints mirror the original section-miles3
cascade: 3-col@120gap >1200, 3-col@40gap ≤1200, 2-col ≤1020, 1-col
≤480). The ≤1200 step is what keeps columns wide enough at laptop
@@ -282,6 +316,8 @@ export default function Miles3({
</div>
</div>
{children ? <div className="miles3-continue">{children}</div> : null}
</div>
</div>
</div>

View File

@@ -0,0 +1,332 @@
"use client";
import React, { useEffect, useRef } from "react";
/* ============================================================
NeuralField — the animated neural-network + particle backdrop behind the
MileTruth™ AI band on /solutions.
Deliberately a plain 2D canvas rather than the R3F MileTruthCanvas scene:
this one sits *behind* body copy at low opacity, so it has to stay cheap
enough to run under the fold of a long marketing page. It follows the same
rules as the other canvases on the site (WorkflowScene, MileTruthNetwork):
· DPR-aware, resized from the parent box (ResizeObserver)
· rAF paused while off-screen and while the tab is hidden
· prefers-reduced-motion paints ONE static frame and stops
Three layers, back to front:
1. particle field — tiny drifting motes, no links
2. neural mesh — drifting nodes + proximity links, accent-tinted as
the two ends get closer
3. signals — a few bright packets travelling node-to-node, which
is what reads as "a decision engine thinking"
============================================================ */
type Node = {
x: number;
y: number;
vx: number;
vy: number;
r: number;
hub: boolean;
/** phase offset so hub glow does not pulse in lockstep */
ph: number;
};
type Mote = { x: number; y: number; vy: number; sway: number; ph: number; a: number };
type Signal = { a: number; b: number; t: number; sp: number };
/** Distance at which two nodes stop being linked (CSS px). */
const LINK = 132;
/** Pointer radius that brightens the mesh. */
const CURSOR = 170;
function hexToRgb(hex: string): [number, number, number] {
const h = hex.replace("#", "");
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
const n = parseInt(full, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
export default function NeuralField({
accent = "#E2354A",
tone = "dark",
className,
}: {
accent?: string;
/** Surface the field is painted on. "light" inks the mesh near-black instead
* of white and drops the additive glow, which is invisible on paper. */
tone?: "dark" | "light";
className?: string;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const parent = canvas?.parentElement;
if (!canvas || !parent) return;
const ctx = canvas.getContext("2d", { alpha: true });
if (!ctx) return;
const [ar, ag, ab] = hexToRgb(accent);
const light = tone === "light";
// The ink the mesh fades towards when a link is long / far from the pointer.
const [ir, ig, ib] = light ? [17, 17, 17] : [255, 255, 255];
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let w = 0;
let h = 0;
let nodes: Node[] = [];
let motes: Mote[] = [];
let signals: Signal[] = [];
let raf = 0;
let visible = true;
let running = false;
let last = 0;
let nextSignal = 600;
const pointer = { x: -9999, y: -9999 };
const rand = (a: number, b: number) => a + Math.random() * (b - a);
function build() {
const area = w * h;
// One node per ~24k css px², clamped — enough to read as a mesh at
// 1440px wide without turning the O(n²) link pass into real work.
const count = Math.max(16, Math.min(72, Math.round(area / 24000)));
nodes = Array.from({ length: count }, (_, i) => ({
x: Math.random() * w,
y: Math.random() * h,
vx: rand(-0.14, 0.14),
vy: rand(-0.11, 0.11),
r: rand(1.1, 2.3),
hub: i % 9 === 0,
ph: Math.random() * Math.PI * 2,
}));
motes = Array.from({ length: Math.min(140, Math.round(area / 9000)) }, () => ({
x: Math.random() * w,
y: Math.random() * h,
vy: rand(-0.22, -0.06),
sway: rand(6, 20),
ph: Math.random() * Math.PI * 2,
a: rand(0.06, 0.28),
}));
signals = [];
}
function resize() {
const rect = parent!.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
w = rect.width;
h = rect.height;
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);
build();
if (reduce) draw(0, 0);
}
function spawnSignal() {
if (nodes.length < 2 || signals.length >= 3) return;
const a = Math.floor(Math.random() * nodes.length);
// Prefer a neighbour that is actually linked, so the packet rides a
// visible edge instead of crossing empty space.
let b = -1;
let best = Infinity;
for (let i = 0; i < nodes.length; i++) {
if (i === a) continue;
const d = Math.hypot(nodes[i].x - nodes[a].x, nodes[i].y - nodes[a].y);
if (d < LINK && d < best && Math.random() > 0.45) {
best = d;
b = i;
}
}
if (b < 0) return;
signals.push({ a, b, t: 0, sp: rand(0.5, 0.95) });
}
function draw(dt: number, time: number) {
ctx!.clearRect(0, 0, w, h);
/* ---- 1. particle field ---- */
for (const m of motes) {
ctx!.beginPath();
ctx!.arc(m.x + Math.sin(time * 0.0004 + m.ph) * m.sway, m.y, 0.9, 0, Math.PI * 2);
ctx!.fillStyle = "rgba(" + ir + "," + ig + "," + ib + "," + m.a * (light ? 0.4 : 0.55) + ")";
ctx!.fill();
}
/* ---- 2. neural mesh ---- */
ctx!.lineWidth = 1;
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const d2 = dx * dx + dy * dy;
if (d2 > LINK * LINK) continue;
const d = Math.sqrt(d2);
const prox = 1 - d / LINK;
// Near the pointer the mesh warms towards the accent and brightens.
const mx = (a.x + b.x) * 0.5;
const my = (a.y + b.y) * 0.5;
const cur = Math.max(0, 1 - Math.hypot(mx - pointer.x, my - pointer.y) / CURSOR);
const tint = Math.min(1, prox * 0.85 + cur * 0.6);
const alpha = (prox * 0.3 + cur * 0.35) * (light ? 0.6 : 1);
const r = Math.round(ir + (ar - ir) * tint);
const g = Math.round(ig + (ag - ig) * tint);
const bl = Math.round(ib + (ab - ib) * tint);
ctx!.strokeStyle = "rgba(" + r + "," + g + "," + bl + "," + alpha.toFixed(3) + ")";
ctx!.beginPath();
ctx!.moveTo(a.x, a.y);
ctx!.lineTo(b.x, b.y);
ctx!.stroke();
}
}
for (const n of nodes) {
const cur = Math.max(0, 1 - Math.hypot(n.x - pointer.x, n.y - pointer.y) / CURSOR);
if (n.hub) {
const pulse = 0.5 + 0.5 * Math.sin(time * 0.0016 + n.ph);
ctx!.save();
// An additive bloom only reads on a dark surface; on paper it muddies.
if (!light) {
ctx!.shadowColor = "rgba(" + ar + "," + ag + "," + ab + ",0.9)";
ctx!.shadowBlur = 10 + pulse * 12;
}
ctx!.beginPath();
ctx!.arc(n.x, n.y, n.r + 0.7 + pulse * 0.6, 0, Math.PI * 2);
ctx!.fillStyle = "rgba(" + ar + "," + ag + "," + ab + "," + (0.5 + pulse * 0.35) * (light ? 0.7 : 1) + ")";
ctx!.fill();
ctx!.restore();
} else {
ctx!.beginPath();
ctx!.arc(n.x, n.y, n.r, 0, Math.PI * 2);
ctx!.fillStyle = "rgba(" + ir + "," + ig + "," + ib + "," + (0.16 + cur * 0.4) * (light ? 0.55 : 1) + ")";
ctx!.fill();
}
}
/* ---- 3. travelling signals ---- */
for (const s of signals) {
const a = nodes[s.a];
const b = nodes[s.b];
if (!a || !b) continue;
const x = a.x + (b.x - a.x) * s.t;
const y = a.y + (b.y - a.y) * s.t;
// Fade in and out so packets never pop at the endpoints.
const fade = Math.sin(s.t * Math.PI);
ctx!.save();
if (!light) {
ctx!.shadowColor = "rgba(" + ar + "," + ag + "," + ab + ",1)";
ctx!.shadowBlur = 14;
}
ctx!.beginPath();
ctx!.arc(x, y, 1.9, 0, Math.PI * 2);
// On paper the packet is the accent itself; on dark it burns towards white.
ctx!.fillStyle = light
? "rgba(" + ar + "," + ag + "," + ab + "," + fade + ")"
: "rgba(255," + Math.round(ag * 0.7 + 90) + "," + Math.round(ab * 0.7 + 90) + "," + fade + ")";
ctx!.fill();
ctx!.restore();
}
if (reduce) return;
/* ---- integrate ---- */
const step = Math.min(3, dt / 16.67);
for (const n of nodes) {
n.x += n.vx * step;
n.y += n.vy * step;
if (n.x < -20) n.x = w + 20;
if (n.x > w + 20) n.x = -20;
if (n.y < -20) n.y = h + 20;
if (n.y > h + 20) n.y = -20;
}
for (const m of motes) {
m.y += m.vy * step;
if (m.y < -10) {
m.y = h + 10;
m.x = Math.random() * w;
}
}
for (let i = signals.length - 1; i >= 0; i--) {
signals[i].t += (signals[i].sp * step) / 60;
if (signals[i].t >= 1) signals.splice(i, 1);
}
nextSignal -= dt;
if (nextSignal <= 0) {
spawnSignal();
nextSignal = rand(700, 1800);
}
}
function frame(time: number) {
const dt = last ? time - last : 16.67;
last = time;
draw(dt, time);
raf = requestAnimationFrame(frame);
}
function start() {
if (running || reduce) return;
running = true;
last = 0;
raf = requestAnimationFrame(frame);
}
function stop() {
running = false;
cancelAnimationFrame(raf);
}
const ro = new ResizeObserver(resize);
ro.observe(parent);
resize();
const io = new IntersectionObserver(
(entries) => {
visible = entries.some((e) => e.isIntersecting);
if (visible && !document.hidden) start();
else stop();
},
{ rootMargin: "120px 0px" },
);
io.observe(canvas);
const onVisibility = () => {
if (document.hidden) stop();
else if (visible) start();
};
const onMove = (e: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
pointer.x = e.clientX - rect.left;
pointer.y = e.clientY - rect.top;
};
const onLeave = () => {
pointer.x = -9999;
pointer.y = -9999;
};
document.addEventListener("visibilitychange", onVisibility);
// Listener on the section (not the canvas — it is pointer-events: none).
const host = parent.closest("section") ?? parent;
host.addEventListener("pointermove", onMove as EventListener);
host.addEventListener("pointerleave", onLeave);
return () => {
stop();
ro.disconnect();
io.disconnect();
document.removeEventListener("visibilitychange", onVisibility);
host.removeEventListener("pointermove", onMove as EventListener);
host.removeEventListener("pointerleave", onLeave);
};
}, [accent, tone]);
return <canvas ref={canvasRef} className={className} aria-hidden="true" />;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,917 @@
"use client";
import React, { useState, useEffect } from "react";
interface SolutionsMileTruthAnimationProps {
variant?: "compact" | "full";
className?: string;
}
/* ------------------------------------------------------------------
Palette — ink + Doormile red, and nothing else.
Red carries everything the AI is responsible for (the core, the
optimised route, the confirmed outcomes). Ink at low opacity carries
structure and anything the AI rejected. There is deliberately no third
hue: an enterprise console reads as premium because it is restrained,
not because it is colourful.
------------------------------------------------------------------ */
const INK = "#0E0E12";
const RED = "#E2354A";
const RED_DEEP = "#C01227";
/* ==================================================================
LAYER GEOMETRY — one 680×400 board, three columns.
22 ┄┄ 158 inputs (live signals arriving)
206 ┄┄ 430 core (the engine deciding)
452 ┄┄ 658 outcome (what the decision protected)
The card frame, the viewBox and the wrapper's aspect-ratio are fixed;
only the interior is composed here.
================================================================== */
/** Layer 1. Six live feeds — enough to read as "many inputs" without the
* column turning into a list nobody scans. */
const INPUTS: { label: string; icon: React.ReactNode }[] = [
{
label: "Fleet GPS",
icon: (
<>
<path d="M8 13.2C8 13.2 12.6 9.3 12.6 6.5A4.6 4.6 0 1 0 3.4 6.5C3.4 9.3 8 13.2 8 13.2Z" />
<circle cx="8" cy="6.4" r="1.6" />
</>
),
},
{
label: "Traffic flow",
icon: (
<>
<rect x="4.6" y="2.4" width="6.8" height="11.2" rx="2.4" />
<circle cx="8" cy="5.4" r="0.9" />
<circle cx="8" cy="8" r="0.9" />
<circle cx="8" cy="10.6" r="0.9" />
</>
),
},
{
label: "Weather",
icon: (
<path d="M4.8 11.6A2.6 2.6 0 0 1 5.4 6.5 3.6 3.6 0 0 1 12 7.1 2.4 2.4 0 0 1 11.7 11.6Z" />
),
},
{
label: "Order load",
icon: (
<>
<path d="M3 5.4 8 2.8 13 5.4V11L8 13.6 3 11Z" />
<path d="M3 5.4 8 8m0 0 5-2.6M8 8v5.6" />
</>
),
},
{
label: "Driver status",
icon: (
<>
<circle cx="8" cy="5.6" r="2.2" />
<path d="M3.9 13.2a4.1 4.1 0 0 1 8.2 0" />
</>
),
},
{
label: "Vehicle health",
icon: <path d="M2.8 8h2.6L7 4.8 9.2 11.6 10.7 8h2.5" />,
},
];
/** Layer 2. A four-stage network — the shape of a decision, not decoration. */
const CORE = { cx: 318, cy: 200 };
const NET: number[][] = [
[152, 200, 248], // inputs
[132, 178, 222, 268], // hidden A
[132, 178, 222, 268], // hidden B
[168, 232], // decisions
];
const NET_X = [236, 288, 346, 400];
/** The one path lit red — the decision actually taken this cycle. Each entry
* is [layer, fromIndex, toIndex]. */
const LIVE_PATH: [number, number, number][] = [
[0, 1, 1],
[0, 0, 0],
[1, 1, 2],
[1, 0, 1],
[2, 2, 0],
[2, 1, 0],
];
const liveKey = (l: number, a: number, b: number) => `${l}-${a}-${b}`;
const LIVE_SET = new Set(LIVE_PATH.map(([l, a, b]) => liveKey(l, a, b)));
/** Nodes that fire — one per layer, following LIVE_PATH. */
const ACTIVE_NODES = new Set(["0-1", "1-1", "2-2", "3-0"]);
/** Layer 3. What the decision protected, in the order ops would confirm it. */
const OUTCOMES = ["Delay avoided", "SLA protected", "Driver updated", "Customer notified"];
/** The header's cycling state — the engine narrating itself. */
const PHASES = ["Analysing", "Correlating", "Deciding", "Optimised"] as const;
/* The CSS below stills every animation under prefers-reduced-motion, but the
route pod is SMIL, which CSS cannot stop — so the preference is read here
too and the pod is simply not rendered. Subscribed rather than set from an
effect so it stays correct if the user flips the OS setting mid-session. */
const REDUCE_QUERY = "(prefers-reduced-motion: reduce)";
const subscribeReduce = (cb: () => void) => {
const mq = window.matchMedia(REDUCE_QUERY);
mq.addEventListener("change", cb);
return () => mq.removeEventListener("change", cb);
};
const useReducedMotion = () =>
React.useSyncExternalStore(
subscribeReduce,
() => window.matchMedia(REDUCE_QUERY).matches,
() => false
);
export default function SolutionsMileTruthAnimation({
variant = "full",
className = "",
}: SolutionsMileTruthAnimationProps) {
const isCompact = variant === "compact";
const [phase, setPhase] = useState(0);
const [signals, setSignals] = useState(247);
const still = useReducedMotion();
useEffect(() => {
if (isCompact || still) return;
const t = setInterval(() => setPhase((p) => (p + 1) % PHASES.length), 2400);
return () => clearInterval(t);
}, [isCompact, still]);
useEffect(() => {
if (isCompact || still) return;
// A live counter that drifts rather than spins — telemetry, not a slot machine.
const t = setInterval(() => setSignals(244 + Math.floor(Math.random() * 8)), 1600);
return () => clearInterval(t);
}, [isCompact, still]);
if (isCompact) {
return (
<div
className={`sol-s05-svg-core ${className}`}
style={{ width: "100%", height: "100%", position: "relative" }}
>
<style jsx>{`
.sol-compact-route-dash {
stroke-dasharray: 4 6;
animation: solCDashFlow 2s linear infinite;
}
@keyframes solCDashFlow {
to {
stroke-dashoffset: -20;
}
}
@media (prefers-reduced-motion: reduce) {
.sol-compact-route-dash {
animation: none;
}
}
`}</style>
<svg
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ width: "100%", height: "100%" }}
>
<path
d="M 10 30 H 90 M 10 70 H 90 M 30 10 V 90 M 70 10 V 90"
stroke="rgba(14,14,16,0.1)"
strokeWidth="1.5"
/>
<path
d="M 30 70 C 45 30, 55 30, 70 30"
stroke={RED}
strokeWidth="2.5"
strokeLinecap="round"
className="sol-compact-route-dash"
/>
<circle cx="30" cy="70" r="4" fill={INK} />
<circle cx="70" cy="30" r="4" fill={RED} />
</svg>
</div>
);
}
return (
<div
className={`sol-mt-console ${className}`}
style={{ position: "relative", width: "100%", height: "100%" }}
>
<style jsx>{`
.sol-mtc-svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
/* ---- Signal flow. Long gaps, slow cycle: data arriving, not a
barber's pole. ---- */
.sol-mtc-feed {
stroke-dasharray: 13 42;
animation: solMtcFeed 2.6s linear infinite;
}
@keyframes solMtcFeed {
to {
stroke-dashoffset: -55;
}
}
.sol-mtc-synapse {
stroke-dasharray: 9 34;
animation: solMtcFeed 1.5s linear infinite;
}
.sol-mtc-route-flow {
stroke-dasharray: 10 20;
animation: solMtcRouteFlow 1.6s linear infinite;
}
@keyframes solMtcRouteFlow {
to {
stroke-dashoffset: -30;
}
}
/* ---- The core: two counter-rotating rings and a breathing glow.
This is the only thing on the board allowed to move
continuously, because it is the thing that never stops. ---- */
.sol-mtc-ring {
transform-origin: 318px 200px;
animation: solMtcSpin 34s linear infinite;
}
.sol-mtc-ring--b {
animation-duration: 22s;
animation-direction: reverse;
}
@keyframes solMtcSpin {
to {
transform: rotate(360deg);
}
}
.sol-mtc-glow {
transform-box: fill-box;
transform-origin: center;
animation: solMtcGlow 6s ease-in-out infinite;
}
@keyframes solMtcGlow {
0%,
100% {
opacity: 0.55;
transform: scale(0.94);
}
50% {
opacity: 1;
transform: scale(1.04);
}
}
/* ---- Firing node: a halo that expands and clears. ---- */
.sol-mtc-fire {
transform-box: fill-box;
transform-origin: center;
animation: solMtcFire 2.4s ease-out infinite;
}
@keyframes solMtcFire {
0% {
opacity: 0.55;
transform: scale(0.5);
}
60%,
100% {
opacity: 0;
transform: scale(2.1);
}
}
/* ---- Live dots on the input chips. ---- */
.sol-mtc-live {
transform-box: fill-box;
transform-origin: center;
animation: solMtcLive 2.2s ease-in-out infinite;
}
@keyframes solMtcLive {
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 1;
}
}
/* ---- Outcome ticks confirming in sequence. ---- */
.sol-mtc-tick {
transform-box: fill-box;
transform-origin: center;
animation: solMtcTick 4.8s ease-out infinite;
}
@keyframes solMtcTick {
0% {
opacity: 0.5;
transform: scale(0.7);
}
35%,
100% {
opacity: 0;
transform: scale(1.9);
}
}
.sol-mtc-phase {
transition: fill 0.25s ease, stroke 0.25s ease;
}
@media (prefers-reduced-motion: reduce) {
.sol-mtc-feed,
.sol-mtc-synapse,
.sol-mtc-route-flow,
.sol-mtc-ring,
.sol-mtc-glow,
.sol-mtc-fire,
.sol-mtc-live,
.sol-mtc-tick {
animation: none;
}
}
`}</style>
<svg
viewBox="0 0 680 400"
className="sol-mtc-svg"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="MileTruth AI decision engine: live telemetry feeds — fleet GPS, traffic, weather, order load, driver status and vehicle health — flow into a neural decision core, which returns an optimised route that avoids delay and holds the 99.2% SLA at a 41-minute ETA."
>
<defs>
<clipPath id="solMtcClip">
<rect x="8" y="8" width="664" height="384" rx="26" />
</clipPath>
<pattern id="solMtcDots" width="28" height="28" patternUnits="userSpaceOnUse">
<circle cx="14" cy="14" r="0.85" fill="rgba(14,14,16,0.055)" />
</pattern>
{/* Glass: the card is white, so depth comes from a barely-there
vertical wash rather than blur — anything stronger reads as dirt. */}
<linearGradient id="solMtcGlass" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FFFFFF" stopOpacity="0.9" />
<stop offset="60%" stopColor="#FFFFFF" stopOpacity="0" />
<stop offset="100%" stopColor={INK} stopOpacity="0.022" />
</linearGradient>
<radialGradient id="solMtcCoreGlow">
<stop offset="0%" stopColor={RED} stopOpacity="0.16" />
<stop offset="45%" stopColor={RED} stopOpacity="0.06" />
<stop offset="100%" stopColor={RED} stopOpacity="0" />
</radialGradient>
<linearGradient id="solMtcRouteGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor={RED_DEEP} />
<stop offset="100%" stopColor={RED} />
</linearGradient>
<filter id="solMtcCardShadow" x="-12%" y="-14%" width="124%" height="132%">
<feDropShadow dx="0" dy="12" stdDeviation="16" floodColor={INK} floodOpacity="0.07" />
<feDropShadow dx="0" dy="2" stdDeviation="2" floodColor={INK} floodOpacity="0.05" />
</filter>
<filter id="solMtcChipShadow" x="-25%" y="-55%" width="150%" height="210%">
<feDropShadow dx="0" dy="1.5" stdDeviation="2.5" floodColor={INK} floodOpacity="0.07" />
</filter>
<filter id="solMtcNodeGlow" x="-160%" y="-160%" width="420%" height="420%">
<feGaussianBlur stdDeviation="2.4" />
</filter>
</defs>
{/* ============ CARD ============ */}
<rect
x="8"
y="8"
width="664"
height="384"
rx="26"
fill="#FFFFFF"
stroke="rgba(14,14,16,0.09)"
strokeWidth="1"
filter="url(#solMtcCardShadow)"
/>
<g clipPath="url(#solMtcClip)">
<rect x="8" y="60" width="664" height="266" fill="url(#solMtcDots)" />
<rect x="8" y="8" width="664" height="384" fill="url(#solMtcGlass)" />
{/* ============ HEADER ============ */}
<circle cx="34" cy="34" r="4" fill={RED} />
<circle cx="34" cy="34" r="4" fill={RED} className="sol-mtc-fire" />
<text
x="47"
y="38"
fill="rgba(14,14,16,0.6)"
fontSize="11"
fontWeight="800"
letterSpacing="1.35"
fontFamily="Manrope, sans-serif"
>
MILETRUTH&#8482; AI DECISION ENGINE
</text>
<g filter="url(#solMtcChipShadow)">
<rect
x="438"
y="20"
width="214"
height="28"
rx="14"
fill="#FFFFFF"
stroke="rgba(226,53,74,0.35)"
strokeWidth="1.1"
/>
</g>
<circle cx="456" cy="34" r="4" fill={RED} />
<circle cx="456" cy="34" r="4" fill={RED} className="sol-mtc-live" />
<text
x="470"
y="38"
fill={INK}
fontSize="10.5"
fontWeight="800"
fontFamily="Manrope, sans-serif"
>
{PHASES[phase]}
<tspan fill="rgba(14,14,16,0.4)">{" · "}</tspan>
{signals} live signals
</text>
<line x1="8" y1="60" x2="672" y2="60" stroke="rgba(14,14,16,0.07)" strokeWidth="1" />
{/* ============ COLUMN LABELS ============ */}
{[
{ x: 91, t: "LIVE INPUTS" },
{ x: 318, t: "NEURAL CORE" },
{ x: 555, t: "PROTECTED OUTCOME" },
].map((c) => (
<text
key={c.t}
x={c.x}
y="84"
textAnchor="middle"
fill="rgba(14,14,16,0.38)"
fontSize="9.5"
fontWeight="800"
letterSpacing="1.25"
fontFamily="Manrope, sans-serif"
>
{c.t}
</text>
))}
{/* ============================================================
LAYER 1 — LIVE INPUTS
============================================================ */}
{INPUTS.map((inp, i) => {
const top = 96 + i * 37;
return (
<g key={inp.label}>
<g filter="url(#solMtcChipShadow)">
<rect
x="24"
y={top}
width="134"
height="26"
rx="9"
fill="#FFFFFF"
stroke="rgba(14,14,16,0.085)"
strokeWidth="1"
/>
</g>
<rect
x="32"
y={top + 5}
width="16"
height="16"
rx="5.5"
fill="rgba(226,53,74,0.08)"
/>
<g
transform={`translate(32, ${top + 5})`}
stroke={RED_DEEP}
strokeWidth="1.15"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
>
{inp.icon}
</g>
<text
x="56"
y={top + 17}
fill="rgba(14,14,16,0.78)"
fontSize="9.5"
fontWeight="700"
fontFamily="Manrope, sans-serif"
>
{inp.label}
</text>
<circle cx="147" cy={top + 13} r="2.6" fill={RED} fillOpacity="0.25" />
<circle
cx="147"
cy={top + 13}
r="2.6"
fill={RED}
className="sol-mtc-live"
style={{ animationDelay: `${i * 0.32}s` }}
/>
</g>
);
})}
{/* Feeds converging on the core's input layer. Two chips per input
node, which is why the column reads as convergence. */}
{INPUTS.map((inp, i) => {
const cy = 96 + i * 37 + 13;
const ny = NET[0][Math.floor(i / 2)];
const d = `M 160 ${cy} C 190 ${cy}, 202 ${ny}, ${NET_X[0] - 5} ${ny}`;
return (
<g key={`feed-${inp.label}`}>
{/* The wire is always drawn; only the signal on it moves. Without
the base line the dashes read as debris floating in the gap. */}
<path d={d} stroke={INK} strokeOpacity="0.13" strokeWidth="1" fill="none" />
<path
d={d}
stroke={RED}
strokeOpacity="0.75"
strokeWidth="1.3"
strokeLinecap="round"
fill="none"
className="sol-mtc-feed"
style={{ animationDelay: `${i * 0.4}s` }}
/>
</g>
);
})}
{/* ============================================================
LAYER 2 — THE DECISION ENGINE
============================================================ */}
<ellipse
cx={CORE.cx}
cy={CORE.cy}
rx="116"
ry="108"
fill="url(#solMtcCoreGlow)"
className="sol-mtc-glow"
/>
<circle
cx={CORE.cx}
cy={CORE.cy}
r="110"
fill="none"
stroke="rgba(14,14,16,0.13)"
strokeWidth="1"
strokeDasharray="2 11"
className="sol-mtc-ring"
/>
<circle
cx={CORE.cx}
cy={CORE.cy}
r="97"
fill="none"
stroke="rgba(226,53,74,0.3)"
strokeWidth="1"
strokeDasharray="3 15"
className="sol-mtc-ring sol-mtc-ring--b"
/>
{/* Synapses. The full mesh is drawn faint; only the path the engine
actually took this cycle is lit and carries signal. */}
{NET.slice(0, -1).map((layer, l) =>
layer.map((y1, a) =>
NET[l + 1].map((y2, b) => {
const live = LIVE_SET.has(liveKey(l, a, b));
return (
<line
key={`e-${l}-${a}-${b}`}
x1={NET_X[l]}
y1={y1}
x2={NET_X[l + 1]}
y2={y2}
stroke={live ? RED : INK}
strokeOpacity={live ? 0.55 : 0.1}
strokeWidth={live ? 1.4 : 0.8}
/>
);
})
)
)}
{LIVE_PATH.map(([l, a, b]) => (
<line
key={`live-${l}-${a}-${b}`}
x1={NET_X[l]}
y1={NET[l][a]}
x2={NET_X[l + 1]}
y2={NET[l + 1][b]}
stroke="#FFFFFF"
strokeOpacity="0.85"
strokeWidth="1.5"
strokeLinecap="round"
className="sol-mtc-synapse"
style={{ animationDelay: `${l * 0.3}s` }}
/>
))}
{NET.map((layer, l) =>
layer.map((y, i) => {
const on = ACTIVE_NODES.has(`${l}-${i}`);
return (
<g key={`n-${l}-${i}`}>
{on && (
<>
{/* Lit, not neon — a soft halo at half opacity rather than
a bloom that turns the node into a lens flare. */}
<circle
cx={NET_X[l]}
cy={y}
r="5"
fill={RED}
fillOpacity="0.45"
filter="url(#solMtcNodeGlow)"
/>
<circle
cx={NET_X[l]}
cy={y}
r="6"
fill="none"
stroke={RED}
strokeWidth="1.4"
className="sol-mtc-fire"
style={{ animationDelay: `${l * 0.28}s` }}
/>
</>
)}
<circle
cx={NET_X[l]}
cy={y}
r={on ? 4.6 : 3.6}
fill={on ? RED : "#FFFFFF"}
stroke={on ? RED : "rgba(14,14,16,0.28)"}
strokeWidth="1.3"
/>
</g>
);
})
)}
{/* Decision leaving the core for the outcome column. */}
{[
{ d: "M 406 168 C 428 168, 432 138, 449 132" },
{ d: "M 406 232 C 428 232, 432 252, 449 258" },
].map((c, i) => (
<g key={`out-${i}`}>
<path d={c.d} stroke={RED} strokeOpacity="0.28" strokeWidth="1.3" fill="none" />
<path
d={c.d}
stroke={RED}
strokeWidth="1.5"
strokeLinecap="round"
fill="none"
className="sol-mtc-feed"
style={{ animationDelay: `${i * 0.5}s` }}
/>
</g>
))}
{/* ============================================================
LAYER 3 — THE PROTECTED OUTCOME
============================================================ */}
<rect
x="452"
y="94"
width="206"
height="80"
rx="13"
fill="rgba(14,14,16,0.02)"
stroke="rgba(14,14,16,0.06)"
strokeWidth="1"
/>
{/* Rejected route in grey. It has to WANDER, not shadow the red one —
two near-identical curves read as a rendering artefact. */}
<path
d="M 472 148 C 492 167, 524 169, 546 156 C 568 143, 572 112, 598 110 C 616 108, 626 118, 638 124"
stroke="rgba(14,14,16,0.24)"
strokeWidth="1.8"
strokeDasharray="4 5"
strokeLinecap="round"
fill="none"
/>
<path
id="solMtcAiRoute"
d="M 472 148 C 520 143, 580 130, 638 124"
stroke="url(#solMtcRouteGrad)"
strokeWidth="7"
strokeOpacity="0.14"
strokeLinecap="round"
fill="none"
/>
<path
d="M 472 148 C 520 143, 580 130, 638 124"
stroke="url(#solMtcRouteGrad)"
strokeWidth="2.6"
strokeLinecap="round"
fill="none"
/>
<path
d="M 472 148 C 520 143, 580 130, 638 124"
stroke="#FFFFFF"
strokeOpacity="0.55"
strokeWidth="1.2"
strokeLinecap="round"
fill="none"
className="sol-mtc-route-flow"
/>
<circle cx="472" cy="148" r="4" fill={INK} />
<circle cx="638" cy="124" r="5.5" fill="#FFFFFF" stroke={RED} strokeWidth="2" />
<circle cx="638" cy="124" r="2" fill={RED} />
{!still && (
<g>
<circle cx="0" cy="0" r="5" fill={RED} fillOpacity="0.4" filter="url(#solMtcNodeGlow)" />
<circle cx="0" cy="0" r="3.2" fill={RED} />
<animateMotion dur="4.6s" repeatCount="indefinite">
<mpath href="#solMtcAiRoute" xlinkHref="#solMtcAiRoute" />
</animateMotion>
</g>
)}
{/* Legend below the panel. Labels sat on top of the curves before,
which made both unreadable. */}
<g>
<line
x1="454"
y1="188"
x2="468"
y2="188"
stroke="rgba(14,14,16,0.3)"
strokeWidth="1.8"
strokeDasharray="4 4"
strokeLinecap="round"
/>
<text
x="474"
y="191"
fill="rgba(14,14,16,0.42)"
fontSize="9.5"
fontWeight="800"
letterSpacing="0.5"
fontFamily="Manrope, sans-serif"
>
Original 58m
</text>
<line
x1="556"
y1="188"
x2="570"
y2="188"
stroke={RED}
strokeWidth="2.2"
strokeLinecap="round"
/>
<text
x="576"
y="191"
fill={RED_DEEP}
fontSize="9.5"
fontWeight="800"
letterSpacing="0.5"
fontFamily="Manrope, sans-serif"
>
MileTruth 41m
</text>
</g>
{OUTCOMES.map((o, i) => {
const cy = 212 + i * 28;
return (
<g key={o}>
<circle
cx="462"
cy={cy}
r="7"
fill={RED}
fillOpacity="0.18"
className="sol-mtc-tick"
style={{ animationDelay: `${i * 0.55}s` }}
/>
<circle cx="462" cy={cy} r="7" fill="#FFFFFF" stroke={RED} strokeWidth="1.4" />
<path
d={`M 458.8 ${cy} 461.2 ${cy + 2.4} 465.4 ${cy - 2.6}`}
stroke={RED_DEEP}
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<text
x="478"
y={cy + 3.6}
fill="rgba(14,14,16,0.8)"
fontSize="10.5"
fontWeight="700"
fontFamily="Manrope, sans-serif"
>
{o}
</text>
</g>
);
})}
{/* ============ KPI STRIP ============ */}
<line x1="8" y1="326" x2="672" y2="326" stroke="rgba(14,14,16,0.07)" strokeWidth="1" />
<rect x="8" y="326" width="664" height="66" fill="rgba(14,14,16,0.018)" />
<line x1="230" y1="342" x2="230" y2="376" stroke="rgba(14,14,16,0.085)" strokeWidth="1" />
<line x1="450" y1="342" x2="450" y2="376" stroke="rgba(14,14,16,0.085)" strokeWidth="1" />
{[
{
x: 54,
v: "41 min",
l: "OPTIMISED ETA",
ic: (
<>
<circle cx="13" cy="13" r="6.6" />
<path d="M13 8.8V13l3 2" />
</>
),
},
{
x: 275,
v: "17 min",
l: "TIME SAVED",
ic: <path d="M14.2 5.6 8 13.6h4.2L11.4 20.4 17.6 12.4h-4.2Z" />,
},
{
x: 496,
v: "99.2%",
l: "SLA PROTECTED",
ic: (
<>
<path d="M13 5.4 19 8v5c0 4.1-3 6.6-6 7.6-3-1-6-3.5-6-7.6V8Z" />
<path d="m10.4 12.9 2 2 3.4-3.6" />
</>
),
},
].map((k) => (
<g key={k.l}>
<rect x={k.x} y="337" width="26" height="26" rx="8.5" fill="rgba(226,53,74,0.08)" />
<g
transform={`translate(${k.x}, 337)`}
stroke={RED_DEEP}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
>
{k.ic}
</g>
<text
x={k.x + 36}
y="355"
fill={INK}
fontSize="17"
fontWeight="800"
letterSpacing="-0.3"
fontFamily="Manrope, sans-serif"
>
{k.v}
</text>
<text
x={k.x + 36}
y="371"
fill="rgba(14,14,16,0.45)"
fontSize="9.5"
fontWeight="800"
letterSpacing="1.15"
fontFamily="Manrope, sans-serif"
>
{k.l}
</text>
</g>
))}
</g>
</svg>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import { ScrollReveal } from "@/animations/Reveal";
import WingsNeuralEngineAnimation from "./WingsNeuralEngineAnimation";
const FLOW_STAGES = [
{
@@ -722,12 +723,7 @@ export default function WingsMileTruth() {
<div className="wmt-inner dm-section-container">
<ScrollReveal delay={0.05} duration={0.8} yOffset={20} className="wmt-visual-container">
<TiltCard className="wmt-visual-pane">
<img
src="/images/miletruth-ai-core.png"
alt="MileTruth AI Core Engine Visualization"
className="wmt-visual-img"
/>
<ParticleCanvas />
<WingsNeuralEngineAnimation />
</TiltCard>
</ScrollReveal>

View File

@@ -0,0 +1,937 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
/* ============================================================
WingsNeuralEngineAnimation — the live visual beside the MileTruth™ AI copy
on /doormile-wings. Wings-page only; the /solutions and home variants
(SolutionsMileTruthAnimation, AiCoreAnimation) are separate files.
This is a *simulation*, not a timeline: every packet, cadence and readout
is randomised per event, so the panel never repeats a frame and reads as
something actually running. Six named telemetry sources stream packets
into a core, the core breathes with its own throughput, and every few
seconds it fires a recommendation out to the insight stack.
Canvas paints the field / ring / core / bursts; the HUD is HTML so the
numbers stay crisp. One rAF loop drives both — HUD text is written through
refs, never through React state, so there is no re-render per frame.
Lifecycle follows the site canvas pattern (see NeuralField, WorkflowScene):
DPR-aware, ResizeObserver-sized, rAF paused off-screen and on hidden tabs,
prefers-reduced-motion paints one static frame and stops.
============================================================ */
/** Telemetry feeds on the ingest ring, in ring order from 12 o'clock running
* clockwise. Every transport value here is a fixed constant: `bend` is that
* feed's path curvature, `speed` its transit rate, `dwell` the pause after
* absorption before it sends again. The six packets differ because these
* numbers differ — there is no spawner and no randomness in the transport.
* Labels are ordered so the long ones land on the sides, where there is room
* to set them outside the ring. */
const SOURCES = [
{ label: "Orders", bend: 0.26, speed: 0.4, dwell: 900 },
{ label: "Fleet Telemetry", bend: -0.19, speed: 0.52, dwell: 520 },
{ label: "Routes", bend: 0.34, speed: 0.35, dwell: 1150 },
{ label: "Traffic", bend: -0.3, speed: 0.47, dwell: 700 },
{ label: "Weather", bend: 0.14, speed: 0.3, dwell: 1500 },
{ label: "POD Scans", bend: -0.38, speed: 0.43, dwell: 1050 },
];
/** Ring radius as a fraction of the short edge. Shared by the canvas and the
* HTML labels so the two layers stay locked together. */
const RING = 0.28;
/** Recommendation pool. `fn` builds the value half fresh each time so the
* same insight never reads identically twice. */
const INSIGHTS: Array<{ head: string; fn: () => string; good?: boolean }> = [
{ head: "Reroute DEL → JAI", fn: () => `${11 + Math.floor(Math.random() * 14)} min`, good: true },
{ head: "SLA risk cleared", fn: () => `BLR-${10 + Math.floor(Math.random() * 80)}`, good: true },
{ head: "Load rebalanced", fn: () => `Hub ${1 + Math.floor(Math.random() * 4)} → Hub ${5 + Math.floor(Math.random() * 3)}` },
{ head: "Demand spike", fn: () => `Chennai +${9 + Math.floor(Math.random() * 22)}%` },
{ head: "Idle dwell trimmed", fn: () => `${4 + Math.floor(Math.random() * 9)} min/stop`, good: true },
{ head: "Capacity shifted", fn: () => `${2 + Math.floor(Math.random() * 6)} vehicles` },
{ head: "Anomaly flagged", fn: () => `Lane HYD-${100 + Math.floor(Math.random() * 90)}` },
{ head: "Cold-chain hold", fn: () => `${2 + Math.floor(Math.random() * 5)} shipments` },
];
type Insight = { id: number; head: string; value: string; good: boolean };
const rand = (a: number, b: number) => a + Math.random() * (b - a);
export default function WingsNeuralEngineAnimation({ className = "" }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
const countRef = useRef<HTMLElement>(null);
const rateRef = useRef<HTMLSpanElement>(null);
const confRef = useRef<HTMLSpanElement>(null);
const slaRef = useRef<HTMLSpanElement>(null);
const sparkRef = useRef<HTMLCanvasElement>(null);
/** Set by the sim so the effect can push insight cards into React state. */
const pushInsightRef = useRef<() => void>(() => {});
const [insights, setInsights] = useState<Insight[]>([]);
// Insight cards are the one piece of React state here — they change a few
// times a minute, not 60 times a second.
useEffect(() => {
let nextId = 1;
pushInsightRef.current = () => {
const pick = INSIGHTS[Math.floor(Math.random() * INSIGHTS.length)];
const card: Insight = {
id: nextId++,
head: pick.head,
value: pick.fn(),
good: !!pick.good,
};
setInsights((prev) => [card, ...prev].slice(0, 3));
window.setTimeout(() => {
setInsights((prev) => prev.filter((c) => c.id !== card.id));
}, 4600);
};
}, []);
useEffect(() => {
const canvas = canvasRef.current;
const stage = stageRef.current;
if (!canvas || !stage) return;
const ctx = canvas.getContext("2d", { alpha: true });
if (!ctx) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let w = 0;
let h = 0;
/** Normalised unit — every radius below is a fraction of the short edge,
* so the scene composes identically at 580px and at 300px. */
let u = 0;
let cx = 0;
let cy = 0;
let raf = 0;
let running = false;
let last = 0;
/* ---- transport -------------------------------------------------------
One slot per feed. `pt[i]` is that feed's packet progress along its
path (0..1), or -1 while the feed is waiting. A slot cannot hold two
packets, so "one active packet per source, no duplicates" is a property
of the data structure rather than a rule a spawner has to respect. */
const pt: number[] = SOURCES.map(() => -1);
/** Countdown to the next send. Staggered by a fixed offset, not random. */
const wait: number[] = SOURCES.map((_, i) => 200 + i * 260);
/** Core pulse: 1 on absorption, back to 0 in ~300ms. */
let pulse = 0;
/** The single absorption ripple: -1 idle, else 0..1. One at a time. */
let ripple = -1;
/* Three segmented rings. Each drifts very slowly at its own rate, and on
absorption exactly one ring — taken in turn — is stepped a further 15°,
eased in rather than snapped. */
const RINGS = [
{ r: 1.2, seg: 3, gap: 0.66, a: 0.3, w: 1.1, sp: 0.000055 },
{ r: 1.4, seg: 4, gap: 0.82, a: 0.18, w: 1, sp: -0.000034 },
{ r: 1.6, seg: 2, gap: 1.0, a: 0.12, w: 1, sp: 0.000021 },
];
const ringPhase = [0.4, 2.1, 4.3];
const ringStep = [0, 0, 0];
const ringStepTo = [0, 0, 0];
let nextRing = 0;
/* ---- HUD readouts (data, not motion) ---- */
let analysed = 1284392;
let eventsPerSec = 1180;
let confidence = 97.4;
let sla = 99.2;
let hudDue = 0;
let nextInsight = 2600;
// The DoorMile symbol — icon only, the same asset the solutions engine
// core uses. Drawn on canvas so packets can vanish behind it.
const mark = new window.Image();
let markReady = false;
mark.onload = () => {
markReady = true;
if (reduce) draw();
};
mark.src = "/images/doormile-mark.png";
// Seeded with noise so the throughput trace never opens on a flat plateau.
const spark: number[] = Array.from({ length: 28 }, () => rand(0.3, 0.7));
/* ---- geometry ---- */
const ringR = () => u * RING;
const coreR = () => u * 0.052;
function srcPos(i: number) {
// Ring starts at the top and runs clockwise; the six labels in the HUD
// use the same formula so canvas and DOM stay locked together.
const a = -Math.PI / 2 + (i / SOURCES.length) * Math.PI * 2;
return { x: cx + Math.cos(a) * ringR(), y: cy + Math.sin(a) * ringR(), a };
}
/** Fixed quadratic bezier from feed `i` to the core. The control point is
* derived from that feed's constant `bend`, so the route never varies. */
function packetPos(i: number, t: number) {
const s = srcPos(i);
const mx = (s.x + cx) / 2;
const my = (s.y + cy) / 2;
const nx = -(cy - s.y);
const ny = cx - s.x;
const nl = Math.hypot(nx, ny) || 1;
const k = SOURCES[i].bend * u * 0.34;
const bx = mx + (nx / nl) * k;
const by = my + (ny / nl) * k;
const it = 1 - t;
return {
x: it * it * s.x + 2 * it * t * bx + t * t * cx,
y: it * it * s.y + 2 * it * t * by + t * t * cy,
};
}
/* ---- painting ------------------------------------------------------ */
/** Background: the dark grid and one fixed pool of light. Both static. */
function drawField() {
const gap = Math.max(18, u * 0.052);
ctx!.fillStyle = "rgba(255,255,255,0.045)";
for (let x = gap / 2; x < w; x += gap) {
for (let y = gap / 2; y < h; y += gap) {
ctx!.beginPath();
ctx!.arc(x, y, 1, 0, Math.PI * 2);
ctx!.fill();
}
}
const glow = ctx!.createRadialGradient(cx, cy, 0, cx, cy, u * 0.5);
glow.addColorStop(0, "rgba(226,53,74,0.09)");
glow.addColorStop(0.5, "rgba(192,18,39,0.035)");
glow.addColorStop(1, "rgba(226,53,74,0)");
ctx!.fillStyle = glow;
ctx!.fillRect(0, 0, w, h);
}
/** Input nodes and their route guide. Entirely static: a feed's activity
* is told by its packet, so the node itself never needs to flash. */
function drawRing() {
ctx!.strokeStyle = "rgba(255,255,255,0.07)";
ctx!.lineWidth = 1;
ctx!.setLineDash([3, 7]);
ctx!.beginPath();
ctx!.arc(cx, cy, ringR(), 0, Math.PI * 2);
ctx!.stroke();
ctx!.setLineDash([]);
for (let i = 0; i < SOURCES.length; i++) {
const s = srcPos(i);
ctx!.beginPath();
ctx!.arc(s.x, s.y, u * 0.019, 0, Math.PI * 2);
ctx!.strokeStyle = "rgba(255,59,59,0.26)";
ctx!.lineWidth = 1;
ctx!.stroke();
ctx!.beginPath();
ctx!.arc(s.x, s.y, Math.max(2, u * 0.0062), 0, Math.PI * 2);
ctx!.fillStyle = "rgba(255,130,130,0.7)";
ctx!.fill();
}
}
/** The six data packets. Nothing else on this canvas travels. */
function drawPackets() {
for (let i = 0; i < SOURCES.length; i++) {
const t = pt[i];
if (t < 0) continue;
const head = packetPos(i, t);
// A short trail behind the head: a bare dot reads as a floating
// particle, a trail reads as something traveling a route.
const tail = packetPos(i, Math.max(0, t - 0.075));
const a = Math.min(1, t * 5) * (0.5 + t * 0.5) * Math.min(1, (1 - t) / 0.07);
const tint = `255,${Math.round(130 + t * 100)},${Math.round(135 + t * 100)}`;
const tg = ctx!.createLinearGradient(tail.x, tail.y, head.x, head.y);
tg.addColorStop(0, `rgba(${tint},0)`);
tg.addColorStop(1, `rgba(${tint},${(a * 0.5).toFixed(3)})`);
ctx!.beginPath();
ctx!.moveTo(tail.x, tail.y);
ctx!.lineTo(head.x, head.y);
ctx!.strokeStyle = tg;
ctx!.lineWidth = 1.4;
ctx!.lineCap = "round";
ctx!.stroke();
ctx!.save();
ctx!.shadowColor = "rgba(255,59,59,0.8)";
ctx!.shadowBlur = 5;
ctx!.beginPath();
ctx!.arc(head.x, head.y, 2.3, 0, Math.PI * 2);
ctx!.fillStyle = `rgba(${tint},${a.toFixed(3)})`;
ctx!.fill();
ctx!.restore();
}
}
/* ---- the core: a glass processor with the brand symbol at its heart.
Painted in two passes so packets can fly over the glass and disappear
*into* the symbol rather than stopping at its rim. ---- */
/** Radius of the glass housing. */
const glassR = () => coreR() * 2.15;
function drawCoreBack() {
const R = glassR();
ctx!.save();
ctx!.translate(cx, cy);
// Seat the assembly into the field — gradient only, no drawn rim here.
const pg = ctx!.createRadialGradient(0, 0, R * 0.7, 0, 0, R * 2.4);
pg.addColorStop(0, "rgba(7,7,16,0)");
pg.addColorStop(0.45, "rgba(7,7,16,0.5)");
pg.addColorStop(1, "rgba(7,7,16,0)");
ctx!.fillStyle = pg;
ctx!.beginPath();
ctx!.arc(0, 0, R * 2.4, 0, Math.PI * 2);
ctx!.fill();
// The absorption ripple: one, small, inside the ring stack.
if (ripple >= 0) {
ctx!.beginPath();
ctx!.arc(0, 0, R * (1.02 + ripple * 0.55), 0, Math.PI * 2);
ctx!.strokeStyle = `rgba(255,140,140,${((1 - ripple) * 0.3).toFixed(3)})`;
ctx!.lineWidth = 1;
ctx!.stroke();
}
// Three segmented rings. Nothing rides them and nothing highlights
// them — the rotation alone is the computation.
for (let i = 0; i < RINGS.length; i++) {
const g = RINGS[i];
const span = (Math.PI * 2) / g.seg;
const base = ringPhase[i] + ringStep[i];
for (let j = 0; j < g.seg; j++) {
const s0 = base + j * span;
ctx!.beginPath();
ctx!.arc(0, 0, R * g.r, s0, s0 + span - g.gap);
ctx!.strokeStyle = `rgba(255,90,90,${g.a})`;
ctx!.lineWidth = g.w;
ctx!.stroke();
}
}
/* --- glass housing --- */
const body = ctx!.createLinearGradient(0, -R, 0, R);
body.addColorStop(0, "rgba(255,255,255,0.075)");
body.addColorStop(0.5, "rgba(255,255,255,0.022)");
body.addColorStop(1, "rgba(255,70,70,0.05)");
ctx!.beginPath();
ctx!.arc(0, 0, R, 0, Math.PI * 2);
ctx!.fillStyle = body;
ctx!.fill();
// Light held inside the glass, lifting only with the core pulse.
const inner = ctx!.createRadialGradient(0, 0, 0, 0, 0, R);
inner.addColorStop(0, `rgba(255,70,70,${(0.12 + pulse * 0.3).toFixed(3)})`);
inner.addColorStop(0.6, "rgba(226,53,74,0.05)");
inner.addColorStop(1, "rgba(226,53,74,0)");
ctx!.fillStyle = inner;
ctx!.fill();
// Rim: bright where the light falls, warm on the far side.
const rim = ctx!.createLinearGradient(-R * 0.75, -R * 0.75, R * 0.75, R * 0.75);
rim.addColorStop(0, "rgba(255,255,255,0.55)");
rim.addColorStop(0.4, "rgba(255,255,255,0.1)");
rim.addColorStop(1, `rgba(255,90,90,${(0.28 + pulse * 0.3).toFixed(3)})`);
ctx!.beginPath();
ctx!.arc(0, 0, R, 0, Math.PI * 2);
ctx!.strokeStyle = rim;
ctx!.lineWidth = 1.2;
ctx!.stroke();
// Specular sweep on the upper-left shoulder — the tell that it is glass.
// Faded at both ends so it reads as light on a curve, not a drawn arc.
const spec = ctx!.createLinearGradient(-R, -R * 0.4, 0, -R);
spec.addColorStop(0, "rgba(255,255,255,0)");
spec.addColorStop(0.5, "rgba(255,255,255,0.42)");
spec.addColorStop(1, "rgba(255,255,255,0)");
ctx!.beginPath();
ctx!.arc(0, 0, R - 2.2, Math.PI * 1.05, Math.PI * 1.55);
ctx!.strokeStyle = spec;
ctx!.lineWidth = 1.8;
ctx!.lineCap = "round";
ctx!.stroke();
ctx!.restore();
}
function drawCoreFront() {
if (!markReady) return;
const R = glassR();
ctx!.save();
ctx!.translate(cx, cy);
// The symbol itself never moves: only its glow answers an absorption.
const mw = R * 1.02;
const mh = mw * (mark.height / mark.width);
ctx!.shadowColor = `rgba(255,90,90,${(0.45 + pulse * 0.45).toFixed(3)})`;
ctx!.shadowBlur = R * (0.15 + pulse * 0.38);
ctx!.globalAlpha = 0.97;
ctx!.drawImage(mark, -mw / 2, -mh / 2, mw, mh);
if (pulse > 0.02) {
ctx!.globalAlpha = Math.min(0.5, pulse * 0.5);
ctx!.shadowColor = "rgba(255,255,255,0.95)";
ctx!.shadowBlur = R * (0.2 + pulse * 0.45);
ctx!.drawImage(mark, -mw / 2, -mh / 2, mw, mh);
}
ctx!.restore();
}
function drawSpark() {
const sc = sparkRef.current;
if (!sc) return;
const sctx = sc.getContext("2d");
if (!sctx) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
const rect = sc.getBoundingClientRect();
if (!rect.width || !rect.height) return;
if (sc.width !== Math.round(rect.width * dpr)) {
sc.width = Math.round(rect.width * dpr);
sc.height = Math.round(rect.height * dpr);
}
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
sctx.clearRect(0, 0, rect.width, rect.height);
// Normalise against the buffer's own range so the trace always uses the
// full height — a fixed scale reads as a dead flat line.
let lo = Infinity;
let hi = -Infinity;
for (const v of spark) {
if (v < lo) lo = v;
if (v > hi) hi = v;
}
const span = Math.max(0.06, hi - lo);
const px = (i: number) => (i / (spark.length - 1)) * rect.width;
const py = (v: number) => rect.height - 2 - ((v - lo) / span) * (rect.height - 4);
sctx.beginPath();
sctx.moveTo(px(0), py(spark[0]));
for (let i = 1; i < spark.length; i++) sctx.lineTo(px(i), py(spark[i]));
// Fill under the trace, then re-stroke the line on top.
sctx.save();
sctx.lineTo(rect.width, rect.height);
sctx.lineTo(0, rect.height);
sctx.closePath();
const fg = sctx.createLinearGradient(0, 0, 0, rect.height);
fg.addColorStop(0, "rgba(255,59,59,0.28)");
fg.addColorStop(1, "rgba(255,59,59,0)");
sctx.fillStyle = fg;
sctx.fill();
sctx.restore();
sctx.beginPath();
sctx.moveTo(px(0), py(spark[0]));
for (let i = 1; i < spark.length; i++) sctx.lineTo(px(i), py(spark[i]));
sctx.strokeStyle = "rgba(255,90,90,0.95)";
sctx.lineWidth = 1.2;
sctx.lineJoin = "round";
sctx.stroke();
// Live head dot.
sctx.beginPath();
sctx.arc(px(spark.length - 1), py(spark[spark.length - 1]), 1.8, 0, Math.PI * 2);
sctx.fillStyle = "#ff6b6b";
sctx.fill();
}
function draw() {
ctx!.clearRect(0, 0, w, h);
drawField();
drawRing();
drawCoreBack();
drawPackets();
drawCoreFront();
drawSpark();
}
/* ---- integration ---------------------------------------------------- */
/** A packet reached the centre: one ripple, one ring stepped 15°, the
* logo glow lifts, and the feed starts its fixed wait. Nothing else. */
function absorb(i: number) {
pulse = 1;
ripple = 0;
ringStepTo[nextRing % RINGS.length] += Math.PI / 12;
nextRing++;
pt[i] = -1;
wait[i] = SOURCES[i].dwell;
// The counter measures exactly this: packets the core has taken in.
analysed += 3;
}
function step(dt: number) {
const k = Math.min(3, dt / 16.67);
// Transport: each feed is either carrying or waiting. Never both.
for (let i = 0; i < SOURCES.length; i++) {
if (pt[i] < 0) {
wait[i] -= dt;
if (wait[i] <= 0) pt[i] = 0;
} else {
// Accelerates as it falls into the core.
pt[i] += ((SOURCES[i].speed * (0.55 + pt[i] * 0.9)) / 60) * k;
if (pt[i] >= 1) absorb(i);
}
}
// Core pulse settles in ~300ms.
pulse = Math.max(0, pulse - 0.055 * k);
if (ripple >= 0) {
ripple += 0.04 * k;
if (ripple >= 1) ripple = -1;
}
// Rings: constant slow drift, plus the eased 15° steps.
for (let i = 0; i < RINGS.length; i++) {
ringPhase[i] += dt * RINGS[i].sp;
ringStep[i] += (ringStepTo[i] - ringStep[i]) * Math.min(1, dt / 380);
}
/* ---- HUD: readouts and recommendation cards. No canvas motion. ---- */
nextInsight -= dt;
if (nextInsight <= 0) {
nextInsight = rand(2600, 4200);
pushInsightRef.current();
confidence = Math.min(99.1, Math.max(95.6, confidence + rand(-0.35, 0.4)));
sla = Math.min(99.8, Math.max(98.4, sla + rand(-0.12, 0.14)));
}
hudDue -= dt;
if (hudDue <= 0) {
hudDue = 140;
eventsPerSec = Math.min(2400, Math.max(620, eventsPerSec + rand(-70, 72)));
spark.push(Math.min(1, Math.max(0, (eventsPerSec - 600) / 1800)));
spark.shift();
if (countRef.current) countRef.current.textContent = analysed.toLocaleString("en-US");
if (rateRef.current) rateRef.current.textContent = (eventsPerSec / 1000).toFixed(2) + "k";
if (confRef.current) confRef.current.textContent = confidence.toFixed(1) + "%";
if (slaRef.current) slaRef.current.textContent = sla.toFixed(1) + "%";
}
}
function frame(time: number) {
const dt = last ? Math.min(64, time - last) : 16.67;
last = time;
step(dt);
draw();
raf = requestAnimationFrame(frame);
}
function start() {
if (running || reduce) return;
running = true;
last = 0;
raf = requestAnimationFrame(frame);
}
function stop() {
running = false;
cancelAnimationFrame(raf);
}
function resize() {
const rect = stage!.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
w = rect.width;
h = rect.height;
u = Math.min(w, h);
cx = w / 2;
cy = h / 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 (reduce) draw();
}
const ro = new ResizeObserver(resize);
ro.observe(stage);
resize();
if (reduce) {
// One composed static frame: every feed frozen mid-transit, steady
// numbers. Each packet sits at a different point on its own route.
for (let i = 0; i < SOURCES.length; i++) {
pt[i] = 0.28 + i * 0.1;
}
draw();
if (countRef.current) countRef.current.textContent = analysed.toLocaleString("en-US");
if (rateRef.current) rateRef.current.textContent = "1.18k";
if (confRef.current) confRef.current.textContent = confidence.toFixed(1) + "%";
if (slaRef.current) slaRef.current.textContent = sla.toFixed(1) + "%";
return () => {
ro.disconnect();
};
}
let visible = true;
const sync = () => {
if (visible && !document.hidden) start();
else stop();
};
const io = new IntersectionObserver(
(entries) => {
visible = entries.some((e) => e.isIntersecting);
sync();
},
{ rootMargin: "120px 0px" },
);
io.observe(canvas);
document.addEventListener("visibilitychange", sync);
return () => {
stop();
ro.disconnect();
io.disconnect();
document.removeEventListener("visibilitychange", sync);
};
}, []);
return (
<div
className={`wne-wrap ${className}`}
style={{
position: "relative",
width: "100%",
height: "100%",
minHeight: "100%",
background: "#070710",
borderRadius: "24px",
overflow: "hidden",
}}
>
<style jsx>{`
.wne-stage {
position: absolute;
inset: 0;
}
.wne-canvas {
position: absolute;
inset: 0;
display: block;
}
.wne-hud {
position: absolute;
inset: 0;
pointer-events: none;
font-family: inherit;
}
/* ---- source labels, positioned to match the canvas ring ---- */
.wne-src {
position: absolute;
font-size: clamp(7.5px, 1.5cqw, 9.5px);
font-weight: 700;
letter-spacing: 0.7px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.55);
white-space: nowrap;
}
/* ---- live pill + counter ---- */
.wne-live {
position: absolute;
top: 5.5%;
left: 5.5%;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(7, 7, 16, 0.72);
border: 1px solid rgba(255, 59, 59, 0.28);
backdrop-filter: blur(8px);
}
.wne-live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ff3b3b;
box-shadow: 0 0 10px #ff3b3b;
animation: wne-blink 1.6s ease-in-out infinite;
}
@keyframes wne-blink {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.35;
transform: scale(0.75);
}
}
.wne-live-text {
font-size: clamp(8px, 1.5cqw, 10px);
font-weight: 800;
letter-spacing: 1.4px;
color: #ffffff;
}
.wne-count {
position: absolute;
top: calc(5.5% + 40px);
left: 5.5%;
font-size: clamp(8px, 1.5cqw, 10.5px);
font-weight: 600;
letter-spacing: 0.4px;
color: rgba(255, 255, 255, 0.42);
}
.wne-count b {
color: rgba(255, 255, 255, 0.9);
font-weight: 800;
font-variant-numeric: tabular-nums;
}
/* ---- insight stack ---- */
.wne-insights {
position: absolute;
top: 5.5%;
right: 5.5%;
display: flex;
flex-direction: column;
gap: 6px;
align-items: flex-end;
max-width: 62%;
}
.wne-card {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 11px;
border-radius: 10px;
background: rgba(15, 8, 12, 0.82);
border: 1px solid rgba(255, 59, 59, 0.22);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
animation: wne-card-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
white-space: nowrap;
}
@keyframes wne-card-in {
from {
opacity: 0;
transform: translateX(14px) scale(0.96);
}
to {
opacity: 1;
transform: none;
}
}
.wne-card-head {
font-size: clamp(8px, 1.5cqw, 10.5px);
font-weight: 700;
color: rgba(255, 255, 255, 0.72);
}
.wne-card-value {
font-size: clamp(8px, 1.5cqw, 10.5px);
font-weight: 800;
color: #ff3b3b;
font-variant-numeric: tabular-nums;
}
.wne-card-value.good {
color: #22c55e;
}
/* ---- bottom metric strip ---- */
.wne-metrics {
position: absolute;
left: 5.5%;
right: 5.5%;
bottom: 5.5%;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 10px;
padding: 10px 14px;
border-radius: 14px;
background: rgba(7, 7, 16, 0.68);
border: 1px solid rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
}
.wne-metric {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.wne-metric-label {
font-size: clamp(7px, 1.35cqw, 9px);
font-weight: 700;
letter-spacing: 0.9px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.38);
white-space: nowrap;
}
.wne-metric-value {
font-size: clamp(11px, 2.3cqw, 15px);
font-weight: 800;
color: #ffffff;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.wne-metric-value small {
font-size: 0.62em;
font-weight: 700;
color: rgba(255, 255, 255, 0.4);
margin-left: 3px;
}
.wne-spark {
width: clamp(46px, 11cqw, 76px);
height: 20px;
display: block;
}
.wne-sr {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 520px) {
.wne-src {
display: none;
}
}
/* Narrow panel: the ring labels and the sparkline are the first things
to go, so the three readouts keep room to breathe. */
@container (max-width: 430px) {
.wne-src {
display: none;
}
.wne-spark {
display: none;
}
.wne-metrics {
padding: 9px 11px;
gap: 6px;
}
.wne-metric-label {
letter-spacing: 0.4px;
}
.wne-insights {
max-width: 74%;
}
.wne-live {
padding: 5px 9px;
gap: 6px;
}
.wne-live-text {
letter-spacing: 0.9px;
}
.wne-count {
top: calc(5.5% + 32px);
}
}
`}</style>
<div ref={stageRef} className="wne-stage" style={{ containerType: "size" }}>
<canvas ref={canvasRef} className="wne-canvas" aria-hidden="true" />
<div className="wne-hud" aria-hidden="true">
{SOURCES.map((s, i) => {
// Same polar formula the canvas uses. Side nodes set their label
// outward from an inner edge (so long labels grow away from the
// card wall); top/bottom nodes hang theirs above, which also keeps
// the 6 o'clock label clear of the metric strip.
const a = -Math.PI / 2 + (i / SOURCES.length) * Math.PI * 2;
const x = 50 + Math.cos(a) * RING * 100;
const y = 50 + Math.sin(a) * RING * 100;
const side = Math.abs(Math.cos(a)) > 0.3;
const style: React.CSSProperties = side
? Math.cos(a) > 0
? { left: `${x + 4.4}%`, top: `${y}%`, transform: "translateY(-50%)" }
: { right: `${100 - x + 4.4}%`, top: `${y}%`, transform: "translateY(-50%)" }
: { left: `${x}%`, top: `${y - 5.6}%`, transform: "translate(-50%, -50%)" };
return (
<span key={s.label} className="wne-src" style={style}>
{s.label}
</span>
);
})}
<div className="wne-live">
<span className="wne-live-dot" />
<span className="wne-live-text">MILETRUTH ENGINE LIVE</span>
</div>
<div className="wne-count">
<b ref={countRef}>1,284,392</b> shipments analysed
</div>
<div className="wne-insights">
{insights.map((c) => (
<div key={c.id} className="wne-card">
<span className="wne-card-head">{c.head}</span>
<span className={`wne-card-value${c.good ? " good" : ""}`}>{c.value}</span>
</div>
))}
</div>
<div className="wne-metrics">
<div className="wne-metric">
<span className="wne-metric-label">Throughput</span>
<span className="wne-metric-value">
<span ref={rateRef}>1.18k</span>
<small>ev/s</small>
</span>
</div>
<canvas ref={sparkRef} className="wne-spark" />
<div className="wne-metric">
<span className="wne-metric-label">Confidence</span>
<span className="wne-metric-value" ref={confRef}>
97.4%
</span>
</div>
<div className="wne-metric">
<span className="wne-metric-label">SLA Protected</span>
<span className="wne-metric-value" ref={slaRef}>
99.2%
</span>
</div>
</div>
</div>
</div>
<p className="wne-sr">
MileTruth AI ingesting live logistics telemetry from orders, fleet, routes, traffic,
weather and proof-of-delivery feeds, and emitting routing recommendations in real time.
</p>
</div>
);
}