Files
doormile_react/src/components/sections/SolutionsWorkflow.tsx
2026-08-03 11:27:21 +05:30

3679 lines
190 KiB
TypeScript

"use client";
import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { motion, AnimatePresence, useInView, useMotionValue, useTransform, useSpring, animate, useReducedMotion, useScroll } from "framer-motion";
import { ScrollReveal, CountUp } from "@/animations/Reveal";
import QuantumStage from "./QuantumStage";
import MileTruthNetwork from "./MileTruthNetwork";
import ConnectedLogistics from "./ConnectedLogistics";
import ConnectedLogisticsSection from "./ConnectedLogisticsSection";
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
/* ============================================================
Doormile Solutions — premium storytelling redesign (v2).
Visual system: seven distinct ground textures (Dark Grey, Charcoal,
Mesh Gradient, Soft Graph Texture, Radial Red Glow, Blueprint,
Logistics Grid) so no two sections read as "black on black". Width
matches the Hero/Footer near-edge-to-edge convention (calc(100% -
40px), no narrow content cap) with true split layouts for two
sections. Every section carries a distinct layout identity and a
motion sequence that explains what's happening, not just a fade-up.
============================================================ */
/* ---------------------------------------------------------------
Icon primitives
--------------------------------------------------------------- */
function Svg({ children }: { children: React.ReactNode }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
{children}
</svg>
);
}
const IconBag = <Svg><path d="M6 8h12l-1 12.5a1.5 1.5 0 01-1.5 1.5H8.5A1.5 1.5 0 017 20.5L6 8z" /><path d="M9 8V6a3 3 0 016 0v2" /></Svg>;
const IconDriver = <Svg><circle cx="12" cy="7" r="3" /><path d="M5 21c0-4 3-6.5 7-6.5s7 2.5 7 6.5" /></Svg>;
const IconRoute = <Svg><path d="M3 17l5-10 4 6 4-8 5 12" /></Svg>;
const IconPin = <Svg><path d="M12 2a6 6 0 00-6 6c0 4.5 6 12 6 12s6-7.5 6-12a6 6 0 00-6-6z" /><circle cx="12" cy="8" r="2" /></Svg>;
const IconBoxCheck = <Svg><rect x="4" y="8" width="16" height="12" rx="1.5" /><path d="M4 8l8-4 8 4" /><path d="M9 14l2 2 4-4" /></Svg>;
const IconBox = <Svg><rect x="4" y="8" width="16" height="12" rx="1.5" /><path d="M4 8l8-4 8 4" /></Svg>;
const IconShieldCheck = <Svg><path d="M12 2l7 3v6c0 5-3 8-7 9-4-1-7-4-7-9V5l7-3z" /><path d="M9.3 12l2 2 3.6-4.2" /></Svg>;
const IconTruck = <Svg><rect x="1" y="7" width="13" height="10" rx="1" /><path d="M14 11h4l3 3v3h-3" /><circle cx="5" cy="19" r="2" /><circle cx="17" cy="19" r="2" /></Svg>;
const IconLayers = <Svg><path d="M12 3l8 4-8 4-8-4 8-4z" /><path d="M4 13l8 4 8-4" /></Svg>;
const IconDatabase = <Svg><ellipse cx="12" cy="5" rx="8" ry="3" /><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5" /><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3" /></Svg>;
const IconCpu = <Svg><rect x="7" y="7" width="10" height="10" rx="1.5" /><path d="M7 3v3M12 3v3M17 3v3M7 18v3M12 18v3M17 18v3M3 7h3M3 12h3M3 17h3M18 7h3M18 12h3M18 17h3" /></Svg>;
const IconBulb = <Svg><path d="M9 18h6" /><path d="M10 21h4" /><path d="M12 3a6 6 0 00-3 11.2c.6.4 1 1.1 1 1.8h4c0-.7.4-1.4 1-1.8A6 6 0 0012 3z" /></Svg>;
const IconTrendUp = <Svg><path d="M3 17l6-6 4 4 8-8" /><path d="M15 7h6v6" /></Svg>;
const IconTrendDown = <Svg><path d="M3 7l6 6 4-4 8 8" /><path d="M15 17h6v-6" /></Svg>;
const IconAlert = <Svg><path d="M12 3l9 16H3L12 3z" /><path d="M12 9v5M12 17h.01" /></Svg>;
const IconTarget = <Svg><circle cx="12" cy="12" r="8.5" /><circle cx="12" cy="12" r="4.5" /><circle cx="12" cy="12" r="1" fill="currentColor" /></Svg>;
const IconClock = <Svg><circle cx="12" cy="12" r="8.5" /><path d="M12 7v5l3.2 2" /></Svg>;
const IconZap = <Svg><path d="M13 2 4 14h6l-1 8 9-12h-6l1-8z" /></Svg>;
const IconGlobe = <Svg><circle cx="12" cy="12" r="8.5" /><path d="M3.5 12h17M12 3.5c2.6 2.4 2.6 14.6 0 17M12 3.5c-2.6 2.4-2.6 14.6 0 17" /></Svg>;
const IconCheck = <Svg><path d="M4 12l6 6L20 6" /></Svg>;
const IconX = <Svg><path d="M6 6l12 12M18 6L6 18" /></Svg>;
const IconLock = <Svg><rect x="5" y="11" width="14" height="9" rx="2.2" /><path d="M8 11V7.5a4 4 0 018 0V11" /></Svg>;
/* ---------------------------------------------------------------
Shared shell: backgrounds, section rhythm, floating cards
--------------------------------------------------------------- */
type BgVariant = "grey" | "charcoal" | "graph" | "mesh" | "redglow" | "engine" | "logisticsgrid" | "white";
function Bg({ variant }: { variant: BgVariant }) {
return <div className={`sw-bg sw-bg--${variant}`} aria-hidden="true" />;
}
function Section({
id,
bg,
className = "",
children,
}: {
id: string;
bg: BgVariant;
className?: string;
children: React.ReactNode;
}) {
return (
<section id={id} className={`sw-sec sw-sec--${bg}${className ? ` ${className}` : ""}`}>
<Bg variant={bg} />
<div className="sw-in">{children}</div>
</section>
);
}
function GsapWordRevealText({
children,
className = "",
style,
tag: Tag = "div",
delay = 0,
}: {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
tag?: any;
delay?: number;
}) {
const containerRef = useRef<HTMLElement>(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
return;
}
const words = el.querySelectorAll(".gsap-word-inner");
if (!words.length) return;
gsap.set(words, { y: "115%", opacity: 0 });
const st = ScrollTrigger.create({
trigger: el,
start: "top 88%",
once: true,
onEnter: () => {
gsap.to(words, {
y: "0%",
opacity: 1,
duration: 0.75,
stagger: 0.035,
ease: "power3.out",
delay,
});
},
});
return () => st.kill();
}, [children, delay]);
const wrapWords = (node: React.ReactNode, keyPrefix = "w"): React.ReactNode => {
if (typeof node === "string") {
const words = node.split(" ");
return words.map((w, idx) => {
if (!w) return null;
return (
<span
key={`${keyPrefix}-${idx}`}
className="gsap-word-outer"
style={{
display: "inline-block",
overflow: "hidden",
verticalAlign: "bottom",
paddingBottom: "0.08em",
marginBottom: "-0.08em",
marginRight: "0.28em",
}}
>
<span className="gsap-word-inner" style={{ display: "inline-block" }}>
{w}
</span>
</span>
);
});
}
if (React.isValidElement(node)) {
const children = React.Children.map(
(node.props as any).children,
(child, i) => wrapWords(child, `${keyPrefix}-${i}`)
);
return React.cloneElement(node, { ...(node.props as object), children } as any);
}
if (Array.isArray(node)) {
return node.map((item, i) => wrapWords(item, `${keyPrefix}-${i}`));
}
return node;
};
return (
<Tag ref={containerRef} className={className} style={style}>
{wrapWords(children)}
</Tag>
);
}
function SectionHead({
n,
eyebrow = "Solutions",
title,
lede,
className = "",
darkText = false,
}: {
n?: string;
eyebrow?: string;
title: React.ReactNode;
lede: string;
className?: string;
darkText?: boolean;
}) {
const isCenter = className.includes("sw-head--center");
const cleanEyebrow = eyebrow.replace(/^\/|\/$/g, "").trim();
return (
<div className={`sw-head ${className}`}>
<ScrollReveal yOffset={16} triggerOnce>
<span className="sw-eyebrow sw-eyebrow--slash">/ {cleanEyebrow} /</span>
</ScrollReveal>
<motion.h2
className="sw-h"
style={darkText ? { color: "#111827" } : undefined}
initial={{ opacity: 0, y: 22, filter: "blur(10px)" }}
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
viewport={{ once: true, margin: "-10% 0px" }}
transition={{ duration: 0.65, delay: 0.08, ease: [0.16, 1, 0.3, 1] }}
>
{title}
</motion.h2>
<GsapWordRevealText
tag="p"
className="sw-lede"
style={{
...(darkText ? { color: "#4b5563" } : {}),
...(isCenter ? { marginLeft: "auto", marginRight: "auto", textAlign: "center" } : {}),
}}
>
{lede}
</GsapWordRevealText>
</div>
);
}
/** Hover-glow + subtle mouse-parallax tilt wrapper (Framer Motion). */
function TiltCard({ className = "", children }: { className?: string; children: React.ReactNode }) {
const reduce = useReducedMotion();
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(useTransform(y, [-40, 40], [7, -7]), { stiffness: 240, damping: 22 });
const rotateY = useSpring(useTransform(x, [-40, 40], [-7, 7]), { stiffness: 240, damping: 22 });
return (
<motion.div
className={`sw-tilt ${className}`}
style={reduce ? undefined : { rotateX, rotateY, transformPerspective: 900 }}
onMouseMove={(e) => {
if (reduce) return;
const r = e.currentTarget.getBoundingClientRect();
x.set(e.clientX - r.left - r.width / 2);
y.set(e.clientY - r.top - r.height / 2);
}}
onMouseLeave={() => {
x.set(0);
y.set(0);
}}
whileHover={{ y: -4 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
>
{children}
</motion.div>
);
}
/** Framer-Motion count-up (distinct from the GSAP CountUp used in Section 10). */
function FCount({ to, decimals = 0, suffix = "" }: { to: number; decimals?: number; suffix?: string }) {
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, margin: "-10% 0px" });
const mv = useMotionValue(0);
const [display, setDisplay] = useState((0).toFixed(decimals) + suffix);
useEffect(() => {
const unsub = mv.on("change", (v) => setDisplay(v.toFixed(decimals) + suffix));
return unsub;
}, [mv, decimals, suffix]);
useEffect(() => {
if (!inView) return;
const controls = animate(mv, to, { duration: 1.5, ease: [0.16, 1, 0.3, 1] });
return controls.stop;
}, [inView, to, mv]);
return <span ref={ref}>{display}</span>;
}
/** Wheel + ground-shadow primitives for VehicleArt — module-level (not
* declared inside a render body) so the React Compiler doesn't treat them
* as freshly-created components on every render. */
function VWheel({ cx, rim }: { cx: number; rim: string }) {
return (
<>
<circle cx={cx} cy="37" r="7" fill="#0a0a0a" />
<circle cx={cx} cy="37" r="7" fill="none" stroke={rim} strokeWidth="1.6" />
<circle cx={cx} cy="37" r="2.2" fill="#1c1c1c" />
</>
);
}
function VShadow() {
return <ellipse cx="40" cy="44.5" rx="30" ry="2.6" fill="rgba(0,0,0,0.4)" />;
}
function VDefs() {
return (
<defs>
<linearGradient id="swVBodyActive" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#ff4d61" />
<stop offset="1" stopColor="#a10f22" />
</linearGradient>
</defs>
);
}
/** Vehicle illustration built from primitives (filled body/glass/wheels + ground
* shadow) so it reads as a small rendered vehicle rather than a line icon. */
function VehicleArt({ type, active }: { type: "bike" | "ev" | "mini" | "van" | "truck"; active: boolean }) {
const body = active ? "url(#swVBodyActive)" : "#4b4b54";
const glass = active ? "rgba(255,255,255,0.5)" : "rgba(255,255,255,0.28)";
const wheelRim = active ? "rgba(226,53,74,0.55)" : "rgba(255,255,255,0.2)";
if (type === "bike") {
return (
<svg viewBox="0 0 80 48">
<VDefs /><VShadow />
<line x1="18" y1="37" x2="34" y2="17" stroke={body} strokeWidth="3.4" strokeLinecap="round" />
<line x1="34" y1="17" x2="50" y2="17" stroke={body} strokeWidth="3.4" strokeLinecap="round" />
<line x1="50" y1="17" x2="62" y2="37" stroke={body} strokeWidth="3.4" strokeLinecap="round" />
<line x1="34" y1="17" x2="30" y2="37" stroke={body} strokeWidth="3.4" strokeLinecap="round" />
<line x1="50" y1="17" x2="55" y2="11" stroke={body} strokeWidth="3.4" strokeLinecap="round" />
<rect x="29" y="13" width="10" height="4" rx="2" fill={body} />
<VWheel cx={18} rim={wheelRim} /><VWheel cx={62} rim={wheelRim} />
</svg>
);
}
if (type === "ev") {
return (
<svg viewBox="0 0 80 48">
<VDefs /><VShadow />
<rect x="12" y="30" width="44" height="8" rx="4" fill={body} />
<rect x="47" y="13" width="4" height="19" rx="2" fill={body} />
<rect x="43" y="11" width="13" height="3.4" rx="1.7" fill={body} />
<path d="M32 30l-4 7h4l-3 6 8-8h-4l3-5z" fill="#fff" opacity="0.85" />
<VWheel cx={18} rim={wheelRim} /><VWheel cx={52} rim={wheelRim} />
</svg>
);
}
if (type === "mini") {
return (
<svg viewBox="0 0 80 48">
<VDefs /><VShadow />
<rect x="8" y="19" width="17" height="15" rx="3" fill={body} />
<rect x="11" y="22" width="10" height="7" rx="1.5" fill={glass} />
<rect x="26" y="12" width="35" height="22" rx="2.5" fill={body} />
<VWheel cx={19} rim={wheelRim} /><VWheel cx={49} rim={wheelRim} />
</svg>
);
}
if (type === "van") {
return (
<svg viewBox="0 0 80 48">
<VDefs /><VShadow />
<rect x="8" y="13" width="58" height="21" rx="6" fill={body} />
<rect x="12" y="17" width="15" height="9" rx="2" fill={glass} />
<VWheel cx={21} rim={wheelRim} /><VWheel cx={55} rim={wheelRim} />
</svg>
);
}
return (
<svg viewBox="0 0 80 48">
<VDefs /><VShadow />
<rect x="4" y="15" width="17" height="19" rx="3" fill={body} />
<rect x="7" y="18" width="10" height="8" rx="1.5" fill={glass} />
<rect x="23" y="9" width="45" height="25" rx="2.5" fill={body} />
<VWheel cx={15} rim={wheelRim} /><VWheel cx={43} rim={wheelRim} /><VWheel cx={59} rim={wheelRim} />
</svg>
);
}
/* ============================================================
SECTION 01 — The AI Logistics Engine
Not a diagram, not a timeline: one dominant, always-alive engine
core. An order travels in, the engine visibly analyzes it, then
dispatches three coordinated outcomes at once, closing on delivery.
Answers "what happens after I place an order?" in one glance.
============================================================ */
function Sec01() {
const outputs = [
{ icon: IconDriver, label: "Fleet Driver", note: "Nearest available operator" },
{ icon: IconTruck, label: "Vehicle", note: "Bike, EV, van or truck" },
{ icon: IconRoute, label: "Route", note: "Optimized for time & cost" },
];
const readoutRows = ["Reads the order", "Picks the fleet driver & vehicle", "Plans the best route"];
const wrapRef = useRef<HTMLDivElement>(null);
const orderRef = useRef<HTMLDivElement>(null);
const c1Ref = useRef<HTMLSpanElement>(null);
const engineRowRef = useRef<HTMLDivElement>(null);
const coreTagRef = useRef<HTMLSpanElement>(null);
const readoutDotRefs = useRef<(HTMLSpanElement | null)[]>([]);
const readoutCheckRefs = useRef<(HTMLSpanElement | null)[]>([]);
const readoutRowRefs = useRef<(HTMLDivElement | null)[]>([]);
const fanRefs = useRef<(SVGPathElement | null)[]>([]);
const outNodeRefs = useRef<(HTMLDivElement | null)[]>([]);
const c2Ref = useRef<HTMLSpanElement>(null);
const trackingRef = useRef<HTMLDivElement>(null);
const c3Ref = useRef<HTMLSpanElement>(null);
const deliveredRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const wrap = wrapRef.current;
const order = orderRef.current;
const c1 = c1Ref.current;
const engineRow = engineRowRef.current;
const coreTag = coreTagRef.current;
const readoutDots = readoutDotRefs.current.filter(Boolean) as HTMLSpanElement[];
const readoutChecks = readoutCheckRefs.current.filter(Boolean) as HTMLSpanElement[];
const readoutRowsEl = readoutRowRefs.current.filter(Boolean) as HTMLDivElement[];
const fanLines = fanRefs.current.filter(Boolean) as SVGPathElement[];
const outNodes = outNodeRefs.current.filter(Boolean) as HTMLDivElement[];
const c2 = c2Ref.current;
const tracking = trackingRef.current;
const c3 = c3Ref.current;
const delivered = deliveredRef.current;
if (!wrap || !order || !c1 || !engineRow || !coreTag || fanLines.length === 0 || !c2 || !tracking || !c3 || !delivered) return;
const coreStatus = ["READING…", "CHOOSING…", "PLANNING…"];
const fanLens = fanLines.map((p) => p.getTotalLength());
gsap.set(fanLines, { strokeDasharray: (i: number) => fanLens[i], strokeDashoffset: (i: number) => fanLens[i] });
gsap.set(order, { opacity: 0, y: -14, scale: 0.9 });
gsap.set([c1, c2, c3], { "--fill": 0 } as gsap.TweenVars);
gsap.set(readoutDots, { opacity: 1 });
gsap.set(readoutChecks, { opacity: 0, scale: 0.4 });
gsap.set(readoutRowsEl, { opacity: 0.4 });
gsap.set(outNodes, { opacity: 0.28, y: 10, scale: 0.94 });
gsap.set(tracking, { opacity: 0.28, y: 10, scale: 0.94 });
gsap.set(delivered, { opacity: 0, scale: 0.7 });
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) {
gsap.set(order, { opacity: 1, y: 0, scale: 1 });
gsap.set([c1, c2, c3], { "--fill": 1 } as gsap.TweenVars);
gsap.set(fanLines, { strokeDashoffset: 0 });
gsap.set(readoutDots, { opacity: 0 });
gsap.set(readoutChecks, { opacity: 1, scale: 1 });
gsap.set(readoutRowsEl, { opacity: 1 });
gsap.set(outNodes, { opacity: 1, y: 0, scale: 1 });
gsap.set(tracking, { opacity: 1, y: 0, scale: 1 });
gsap.set(delivered, { opacity: 1, scale: 1 });
return;
}
const play = () => {
const tl = gsap.timeline();
// Order arrives, then travels into the engine.
tl.to(order, { opacity: 1, y: 0, scale: 1, duration: 0.45, ease: "back.out(2)" }, 0)
.to(c1, { "--fill": 1, duration: 0.5, ease: "power2.in" } as gsap.TweenVars, 0.5)
// The order stays on screen as the top of the flow (not a one-shot
// pulse that disappears) so the diagram reads top-to-bottom even
// to someone who arrives after the intro has already played.
// The engine visibly thinks: orbit + waveform speed up, and the
// readout ticks off analysing -> comparing -> selecting one at a time.
.add(() => engineRow.classList.add("is-active"), 1.05);
readoutRowsEl.forEach((row, i) => {
const at = 1.2 + i * 0.4;
// The core's own status readout — not just the side panel — cycles
// through what it's doing, so the "thinking" is legible even to
// someone glancing only at the core itself.
tl.call(() => { coreTag.textContent = coreStatus[i] ?? coreStatus[coreStatus.length - 1]; }, [], at)
.to(row, { opacity: 1, duration: 0.2 }, at)
.to(readoutDots[i], { opacity: 0, scale: 0.4, duration: 0.18 }, at + 0.16)
.to(readoutChecks[i], { opacity: 1, scale: 1, duration: 0.3, ease: "back.out(3)" }, at + 0.16);
});
const afterReadout = 1.2 + readoutRowsEl.length * 0.4 + 0.3;
tl.call(() => { coreTag.textContent = "DECIDED"; }, [], afterReadout - 0.15)
.call(() => { coreTag.textContent = "Doormile AI"; }, [], afterReadout + 0.5)
.add(() => engineRow.classList.remove("is-active"), afterReadout)
// Three coordinated outcomes dispatch together.
.to(fanLines, { strokeDashoffset: 0, duration: 0.5, stagger: 0.14, ease: "power2.out" }, afterReadout + 0.1)
.to(outNodes, { opacity: 1, y: 0, scale: 1, duration: 0.4, stagger: 0.14, ease: "back.out(2.2)" }, afterReadout + 0.1)
// Then live tracking, then delivered.
.to(c2, { "--fill": 1, duration: 0.4, ease: "power2.inOut" } as gsap.TweenVars, afterReadout + 0.75)
.to(tracking, { opacity: 1, y: 0, scale: 1, duration: 0.4, ease: "back.out(2.2)" }, afterReadout + 0.95)
.to(c3, { "--fill": 1, duration: 0.35, ease: "power2.inOut" } as gsap.TweenVars, afterReadout + 1.4)
.to(delivered, { opacity: 1, scale: 1, duration: 0.4, ease: "back.out(2.4)" }, afterReadout + 1.65);
};
const st = ScrollTrigger.create({ trigger: wrap, start: "top 75%", once: true, onEnter: play });
return () => st.kill();
}, [outputs.length, readoutRows.length]);
return (
<Section id="platform" bg="engine">
<SectionHead
n="01"
eyebrow="Order Processing"
title={<>What happens after a customer <span className="accent">places an order</span></>}
lede="One AI engine reads every order, decides the fleet driver, the vehicle and the route, then stays with the shipment until it's signed for."
/>
<div className="sw-eng" ref={wrapRef}>
<div className="sw-eng-order" ref={orderRef}>
<span className="sw-eng-order-ic">{IconBag}</span>
<span className="sw-eng-order-label">Customer Places Order</span>
<span className="sw-eng-order-note">Any channel, one order</span>
</div>
<span className="sw-eng-connector" ref={c1Ref}><span className="sw-eng-connector-fill" /></span>
<div className="sw-eng-engine-row" ref={engineRowRef}>
{/* A single, calm AI node — the Doormile mark plus a short status
label — instead of the earlier busy processor (orbiting dots,
corner brackets, radar sweep, waveform). Easier to read at a
glance: this is simply "the AI deciding". */}
<div className="sw-eng-core">
<div className="sw-eng-core-inner">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/images/doormile-mark.png" alt="" className="sw-eng-core-logo" loading="lazy" decoding="async" />
<span className="sw-eng-core-tag" ref={coreTagRef}>Doormile AI</span>
</div>
</div>
<div className="sw-eng-readout">
<span className="sw-eng-readout-title">Doormile AI Logistics Engine</span>
{readoutRows.map((r, i) => (
<div className="sw-eng-readout-row" ref={(el) => { readoutRowRefs.current[i] = el; }} key={r}>
<span className="sw-eng-readout-indicator">
<span className="sw-eng-readout-dot" ref={(el) => { readoutDotRefs.current[i] = el; }} />
<span className="sw-eng-readout-check" ref={(el) => { readoutCheckRefs.current[i] = el; }}>{IconCheck}</span>
</span>
<span className="sw-eng-readout-label">{r}</span>
</div>
))}
</div>
</div>
<svg className="sw-eng-fan" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
<path ref={(el) => { fanRefs.current[0] = el; }} d="M 50 0 Q 28 55 15 100" className="sw-eng-line--out" />
<path ref={(el) => { fanRefs.current[1] = el; }} d="M 50 0 L 50 100" className="sw-eng-line--out" />
<path ref={(el) => { fanRefs.current[2] = el; }} d="M 50 0 Q 72 55 85 100" className="sw-eng-line--out" />
</svg>
<div className="sw-eng-outputs">
{outputs.map((o, i) => (
<div className="sw-eng-out" ref={(el) => { outNodeRefs.current[i] = el; }} key={o.label}>
<span className="sw-eng-out-ic">{o.icon}</span>
<span className="sw-eng-out-label">{o.label}</span>
<span className="sw-eng-out-note">{o.note}</span>
</div>
))}
</div>
<span className="sw-eng-connector" ref={c2Ref}><span className="sw-eng-connector-fill" /></span>
<div className="sw-eng-tracking" ref={trackingRef}>
<span className="sw-eng-out-ic">{IconPin}</span>
<span className="sw-eng-out-label">Live Tracking</span>
<span className="sw-eng-out-note">Visible to everyone, live</span>
</div>
<span className="sw-eng-connector" ref={c3Ref}><span className="sw-eng-connector-fill" /></span>
<div className="sw-eng-delivered" ref={deliveredRef}>
<span className="sw-eng-delivered-ic">{IconBoxCheck}</span>
<span>Delivered</span>
<em>Confirmed at the door</em>
</div>
</div>
</Section>
);
}
/** Premium filled (not outline) illustrations for the five pickup
* checkpoints — two-tone red/steel shapes rather than thin-stroke icons. */
function PickupIcon({ kind }: { kind: "request" | "driver" | "arrival" | "otp" | "collected" }) {
if (kind === "request") {
return (
<svg viewBox="0 0 32 32">
<rect x="5" y="12" width="22" height="15" rx="2" fill="#C01227" />
<path d="M5 12l11-6 11 6" fill="none" stroke="#fff" strokeWidth="1.6" strokeLinejoin="round" />
<rect x="14" y="12" width="4" height="15" fill="rgba(255,255,255,0.5)" />
</svg>
);
}
if (kind === "driver") {
return (
<svg viewBox="0 0 32 32">
<path d="M16 4a8 8 0 00-8 8c0 6 8 16 8 16s8-10 8-16a8 8 0 00-8-8z" fill="#C01227" />
<circle cx="16" cy="12" r="3.4" fill="#fff" />
</svg>
);
}
if (kind === "arrival") {
return (
<svg viewBox="0 0 32 32">
<path d="M5 15l11-9 11 9" fill="none" stroke="#C01227" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<rect x="8" y="14" width="16" height="13" rx="1.5" fill="#1c1c1c" stroke="rgba(255,255,255,0.2)" />
<rect x="14" y="19" width="4" height="8" fill="#C01227" />
</svg>
);
}
if (kind === "otp") {
return (
<svg viewBox="0 0 32 32">
<rect x="9" y="4" width="14" height="24" rx="3" fill="#1c1c1c" stroke="rgba(255,255,255,0.25)" />
<circle cx="12.5" cy="16" r="1.4" fill="#E2354A" />
<circle cx="16" cy="16" r="1.4" fill="#E2354A" />
<circle cx="19.5" cy="16" r="1.4" fill="#E2354A" />
</svg>
);
}
return (
<svg viewBox="0 0 32 32">
<rect x="5" y="12" width="22" height="15" rx="2" fill="#3a3a3a" />
<path d="M5 12l11-6 11 6" fill="none" stroke="rgba(255,255,255,0.3)" strokeWidth="1.6" />
<circle cx="24" cy="10" r="7" fill="#C01227" />
<path d="M21 10l2 2 4-4" stroke="#fff" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** One wheel: a hub + realistic 5-spoke rim so rotation is actually visible
* (a plain circle would spin invisibly). `sw-van-wheel` is rotated directly
* every frame in Sec02 — transformBox/transformOrigin make the CSS
* transform rotate around the hub, not the SVG's (0,0) origin. */
/** `front` marks the wheel Sec02 gives an extra small steering rotation to
* on top of its continuous spin — the van always travels leading with this
* wheel (see the flip/tilt math in Sec02, which orients the sprite so
* cx=100 always faces the direction of travel). */
function VanWheel({ cx, front }: { cx: number; front?: boolean }) {
return (
<g className={`sw-van-wheel${front ? " sw-van-wheel--front" : ""}`} style={{ transformBox: "fill-box", transformOrigin: "center" }}>
<circle cx={cx} cy="50" r="12" fill="#0a0a0a" />
<circle cx={cx} cy="50" r="12" fill="none" stroke="url(#swVanRim)" strokeWidth="2" />
<circle cx={cx} cy="50" r="7.4" fill="#1b1b1e" stroke="rgba(255,255,255,0.28)" strokeWidth="0.8" />
{/* Spoke offsets are precomputed (not Math.cos/sin at render time) —
trig can round to a different last digit between the server and
client JS engines, which React's hydration check flags as a
mismatch even though it's visually meaningless. */}
{[
{ dx: 6.6, dy: 0 },
{ dx: 2.04, dy: 6.28 },
{ dx: -5.34, dy: 3.88 },
{ dx: -5.34, dy: -3.88 },
{ dx: 2.04, dy: -6.28 },
].map(({ dx, dy }) => (
<line
key={`${dx}-${dy}`}
x1={cx}
y1="50"
x2={cx + dx}
y2={50 + dy}
stroke="rgba(220,222,228,0.85)"
strokeWidth="1.6"
strokeLinecap="round"
/>
))}
<circle cx={cx} cy="50" r="2.1" fill="#4a4a50" />
</g>
);
}
/** Shared gradient/filter defs for VanArt — module scope so they aren't
* redeclared (and re-IDed) on every render. */
function VanDefs() {
return (
<defs>
<linearGradient id="swVanBody" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#ff5166" />
<stop offset="0.45" stopColor="#d81b32" />
<stop offset="1" stopColor="#8c0f1f" />
</linearGradient>
<linearGradient id="swVanCab" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#f6f6f8" />
<stop offset="0.55" stopColor="#dcdce0" />
<stop offset="1" stopColor="#aeaeb6" />
</linearGradient>
<linearGradient id="swVanGlass" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="rgba(255,255,255,0.9)" />
<stop offset="0.5" stopColor="rgba(150,185,210,0.45)" />
<stop offset="1" stopColor="rgba(18,26,34,0.65)" />
</linearGradient>
<linearGradient id="swVanRim" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#e8e9ee" />
<stop offset="1" stopColor="#8a8a92" />
</linearGradient>
<radialGradient id="swVanHeadlight" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stopColor="#fff9e0" />
<stop offset="1" stopColor="#ffd66b" />
</radialGradient>
</defs>
);
}
/** A premium, filled delivery-van illustration — body panels, cab glass with
* a reflection streak, side mirror, head/tail lights and realistic-rim
* wheels that actually spin (see VanWheel) — that physically rides the
* pickup route (see Sec02 below). Cargo box (rear) sits over cx=26, the cab
* and hood (front) over cx=100 — the sprite's native, unflipped pose noses
* toward cx=100, the side the flip/tilt math in Sec02 treats as the
* direction of travel. The ground shadow is rendered as a separate element
* outside this SVG (sw-pickup-van-shadow) so it can stay flat on the road
* while the body above it leans and bounces. Rim-light and contact-shadow
* are applied as a filter on .sw-van-svg (see CSS) rather than per-shape
* outlines, so every part of the silhouette gets the same edge without
* duplicating each path. */
function VanArt() {
return (
<svg viewBox="0 0 130 66" className="sw-van-svg">
<VanDefs />
{/* Cargo box (rear) */}
<rect x="6" y="10" width="76" height="40" rx="4" fill="url(#swVanBody)" />
<rect x="8" y="11.5" width="72" height="3.5" rx="1.75" fill="rgba(255,255,255,0.22)" />
<rect x="6" y="30" width="76" height="6" fill="rgba(255,255,255,0.94)" />
<line x1="14" y1="16" x2="14" y2="49" stroke="rgba(0,0,0,0.32)" strokeWidth="1.1" />
<rect x="10.5" y="30.8" width="2.6" height="4.4" rx="1.1" fill="rgba(0,0,0,0.4)" />
<rect x="6" y="15" width="4.2" height="8.5" rx="1.6" fill="#7a0e1a" />
{/* Cab + hood (front) */}
<path
d="M82 14 C93 14.5 101 17.5 108 22 C114.5 26 118.5 32 120.5 40 L120.5 50 L82 50 Z"
fill="url(#swVanCab)"
/>
<path
d="M91 21.5 C97.5 23.4 103 26.6 107.2 31.5 L106 38 L88.5 38 L88 25 Z"
fill="url(#swVanGlass)"
/>
<path d="M92 23.5 L96.5 22.4 L100 27.5 L94.5 29.5 Z" fill="rgba(255,255,255,0.55)" />
<rect x="103.5" y="24" width="6.5" height="3.2" rx="1.4" fill="#2a2a2e" transform="rotate(18 103.5 24)" />
{/* Lower body trim ties cab + cargo into one silhouette */}
<rect x="6" y="44" width="115" height="6" rx="2.4" fill="rgba(14,14,16,0.88)" />
{/* Front bumper + headlight */}
<rect x="113" y="40.5" width="8" height="9.5" rx="2" fill="#161618" />
<ellipse className="sw-van-headlight" cx="119.5" cy="42.5" rx="3.1" ry="3.6" fill="url(#swVanHeadlight)" />
{/* Wheels paint last so they sit in front of the body's lower trim,
not tucked behind it — they're the part of the vehicle closest to
the viewer in a side-profile illustration. */}
<VanWheel cx={26} />
<VanWheel cx={100} front />
</svg>
);
}
/* ============================================================
SECTION 02 — Smart Pickup Management
Not another progress timeline: an illustrated delivery van physically
drives a route drawn over the pickup photo, accelerating and decelerating
between five real checkpoints (AI-coordinated, not a generic list).
============================================================ */
/** Waypoints the van drives through, in section-relative percent, in the
* same order as `stops` below (Pickup Request -> ... -> Package Collected)
* so checkpoint reading order, road-progress direction and van travel
* direction all agree — left to right, like Google Maps navigation. The
* photo (see sw-pickup-img object-position below) is framed so the open
* road runs across its lower half with the van's open door at the left —
* these points trace a single road-perspective curve along that pavement
* (monotonic in both x and y: near/left is lower and closer, distant/right
* is higher and further out) rather than zig-zagging between arbitrary
* heights, so it reads as one route instead of a glitch line. Starts at the
* van's open door, where the destination pin sits and the delivery van
* departs from. Straight segments between them are interpolated directly as CSS
* left/top percentages (not CSS offset-path — bare path() coordinates are
* pixel units, not container-relative, so they don't scale with a
* responsive container). The whole group sits ~16 points higher than the
* raw pavement trace would put it (max y 68, not 84) so there's a real
* Photo -> Road -> Van -> whitespace -> section-end margin at the bottom
* instead of the lowest checkpoint's label crowding (or clipping against)
* the .sw-edge overflow:hidden boundary. */
const PICKUP_PTS = [
{ x: 18, y: 68 }, { x: 31, y: 64 }, { x: 49, y: 59 }, { x: 70, y: 53 }, { x: 91, y: 47 },
];
const PICKUP_DEST = { x: PICKUP_PTS[0].x, y: PICKUP_PTS[0].y - 15 };
/** Rounds each interior joint of a polyline into a short quadratic-curve
* corner (clamped to a fraction of the shorter adjacent segment so corners
* never overlap) — a believable turning radius instead of the sharp,
* decorative-looking angles a raw polyline draws at every waypoint. */
function roundedRouteD(points: { x: number; y: number }[], radius: number): string {
if (points.length < 3) return "M " + points.map((p) => `${p.x} ${p.y}`).join(" L ");
let d = `M ${points[0].x} ${points[0].y} `;
for (let i = 1; i < points.length - 1; i++) {
const prev = points[i - 1];
const curr = points[i];
const next = points[i + 1];
const v1x = curr.x - prev.x, v1y = curr.y - prev.y;
const v2x = next.x - curr.x, v2y = next.y - curr.y;
const len1 = Math.hypot(v1x, v1y);
const len2 = Math.hypot(v2x, v2y);
const r = Math.min(radius, len1 * 0.42, len2 * 0.42);
const p1x = curr.x - (v1x / len1) * r, p1y = curr.y - (v1y / len1) * r;
const p2x = curr.x + (v2x / len2) * r, p2y = curr.y + (v2y / len2) * r;
d += `L ${p1x} ${p1y} Q ${curr.x} ${curr.y} ${p2x} ${p2y} `;
}
const last = points[points.length - 1];
d += `L ${last.x} ${last.y}`;
return d;
}
/** The actual drawn/ridden road — rounded corners at every checkpoint so
* turns read as a real navigation route (Google Maps/Uber style) instead
* of an angular decorative polyline. Checkpoint markers still sit at the
* original sharp waypoints (PICKUP_PTS), which the rounded curve passes
* within `radius` units of. */
const PICKUP_ROAD_D = roundedRouteD(PICKUP_PTS, 4.5);
function Sec02() {
const stops = [
{ icon: <PickupIcon kind="request" />, label: "Pickup Request", note: "Order raised from any source" },
{ icon: <PickupIcon kind="driver" />, label: "Nearest Driver", note: "AI matches the closest rider" },
{ icon: <PickupIcon kind="arrival" />, label: "Driver Arrival", note: "Real-time ETA to origin" },
{ icon: <PickupIcon kind="otp" />, label: "OTP Verification", note: "Secure, tamper-proof handoff" },
{ icon: <PickupIcon kind="collected" />, label: "Package Collected", note: "Delivery lifecycle begins" },
];
const wrapRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const vanRef = useRef<HTMLDivElement>(null);
const vanBounceRef = useRef<HTMLDivElement>(null);
const vanRotRef = useRef<HTMLDivElement>(null);
const vanShadowRef = useRef<HTMLSpanElement>(null);
const routeProgressRef = useRef<SVGPathElement>(null);
const stopRefs = useRef<(HTMLDivElement | null)[]>([]);
const confirmRef = useRef<HTMLDivElement>(null);
const confirmLabelRef = useRef<HTMLSpanElement>(null);
const aiDotRef = useRef<HTMLSpanElement>(null);
const aiCheckRef = useRef<HTMLSpanElement>(null);
const aiTextRef = useRef<HTMLSpanElement>(null);
const successGlowRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const wrap = wrapRef.current;
const img = imgRef.current;
const van = vanRef.current;
const vanBounce = vanBounceRef.current;
const vanRot = vanRotRef.current;
const vanShadow = vanShadowRef.current;
const routeProgress = routeProgressRef.current;
const confirm = confirmRef.current;
const confirmLabel = confirmLabelRef.current;
const aiDot = aiDotRef.current;
const aiCheck = aiCheckRef.current;
const aiText = aiTextRef.current;
const successGlow = successGlowRef.current;
const stopEls = stopRefs.current.filter(Boolean) as HTMLDivElement[];
const wheelEls = van ? Array.from(van.querySelectorAll<SVGGElement>(".sw-van-wheel")) : [];
const frontWheelEl = van ? van.querySelector<SVGGElement>(".sw-van-wheel--front") : null;
if (!wrap || !img || !van || !vanBounce || !vanRot || !vanShadow || !routeProgress || !confirm || !confirmLabel || !aiDot || !aiCheck || !aiText || !successGlow || stopEls.length === 0) return;
const routeLen = routeProgress.getTotalLength();
gsap.set(routeProgress, { strokeDasharray: routeLen, strokeDashoffset: routeLen });
const activate = (i: number) => {
stopEls.forEach((el, k) => el.classList.toggle("is-active", k <= i));
};
// Sample the van's position AND heading directly off the rendered,
// rounded-corner road (same <path> the progress line draws) instead of
// computing straight-segment math separately — guarantees the wheels
// stay glued to the visible curve through the turns, not just at
// waypoints. Symmetric before/after sampling gives a stable tangent.
const sampleRoad = (pct: number) => {
const clamped = Math.max(0, Math.min(100, pct));
const len = (clamped / 100) * routeLen;
const eps = Math.max(0.4, routeLen * 0.015);
const ptA = routeProgress.getPointAtLength(Math.max(0, len - eps));
const ptB = routeProgress.getPointAtLength(Math.min(routeLen, len + eps));
const pt = routeProgress.getPointAtLength(len);
const dx = ptB.x - ptA.x, dy = ptB.y - ptA.y;
const flip = dx < 0 ? -1 : 1;
const tilt = Math.atan2(dy, Math.abs(dx)) * (180 / Math.PI);
return { x: pt.x, y: pt.y, tilt, flip };
};
// Real-vehicle motion is derived from actual elapsed time between frames
// (not the tween's nominal duration, since scrub time is scroll-driven
// and can move at any real-world speed): wheel spin, steering and the
// ground shadow all react to genuine instantaneous speed, so they
// naturally ease in with the tween's acceleration and settle to a stop
// as it brakes into each checkpoint.
let lastPct = 0;
let lastTs = typeof performance !== "undefined" ? performance.now() : 0;
let lastTilt = 0;
let wheelDeg = 0;
let movingUntil = 0;
const place = (pct: number) => {
const now = typeof performance !== "undefined" ? performance.now() : lastTs + 16;
const dtMs = Math.max(1, now - lastTs);
const dPct = pct - lastPct;
const speed = Math.abs(dPct) / dtMs; // % of route per ms
lastPct = pct;
lastTs = now;
const p = sampleRoad(pct);
van.style.left = `${p.x}%`;
van.style.top = `${p.y}%`;
// scaleX flips the sprite to face its direction of travel (it's drawn
// facing right-ish by default); rotate applies only the small road-
// slope tilt, never the raw heading angle — a full atan2(dy,dx)
// rotate() would flip the van upside-down whenever a route bends back
// on itself. A CSS transition on this element (see
// .sw-pickup-van-rot) eases the tilt change with a slight overshoot,
// reading as banking into the turn instead of snapping.
vanRot.style.transform = `scaleX(${p.flip}) rotate(${p.flip * p.tilt}deg)`;
wheelDeg += dPct * 22;
const turnRate = p.tilt - lastTilt;
lastTilt = p.tilt;
const steer = Math.max(-16, Math.min(16, turnRate * 5));
for (const w of wheelEls) w.style.transform = `rotate(${wheelDeg}deg)`;
if (frontWheelEl) frontWheelEl.style.transform = `rotate(${wheelDeg + steer}deg)`;
const moving = speed > 0.0015;
if (moving) movingUntil = now + 160; // small grace period so bounce/speed-lines don't flicker off between frames
const isMoving = now < movingUntil;
vanBounce.classList.toggle("is-moving", isMoving);
van.classList.toggle("is-moving", isMoving);
// The shadow lives outside the rotate/bounce chain so it stays flat on
// the road while the body above it leans and bobs — a real contact
// shadow, not one that's rigidly glued to the sprite. Motion blur is
// deliberately NOT applied here — it was making the van harder to
// see while moving, which is the opposite of the goal; the speed
// lines (CSS, see .sw-pickup-van-speedlines) carry the sense of
// speed instead, without ever softening the van itself.
const shadowSpread = Math.min(0.3, speed * 6);
vanShadow.style.opacity = String(0.85 - shadowSpread);
vanShadow.style.transform = `scaleX(${1 + shadowSpread})`;
routeProgress.style.strokeDashoffset = `${routeLen * (1 - pct / 100)}`;
};
const showToast = (label: string) => {
confirmLabel.textContent = label;
gsap.fromTo(confirm, { opacity: 0, scale: 0.4 }, { opacity: 1, scale: 1, duration: 0.2, ease: "back.out(3)" });
};
const finish = () => {
aiText.textContent = "AI Coordinated Pickup";
gsap.to(aiDot, { opacity: 0, duration: 0.2 });
gsap.to(aiCheck, { opacity: 1, scale: 1, duration: 0.35, ease: "back.out(3)" });
vanBounce.classList.remove("is-moving");
van.classList.remove("is-moving");
// A brief, subtle success flash across the frame — the completed
// route and checkmarks stay put, this just punctuates the moment.
// The van's own fade-out lives as a tween on the main scrubbed
// timeline (not here) so scrolling back up reverses it smoothly —
// an imperative one-shot gsap.to() in this callback doesn't
// un-happen on scroll-back the way a scrubbed tween does.
gsap.fromTo(successGlow, { opacity: 0 }, { opacity: 1, duration: 0.25, ease: "power1.out", yoyo: true, repeat: 1 });
};
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
gsap.set(confirm, { opacity: 0, scale: 0.4 });
gsap.set(aiCheck, { opacity: 0, scale: 0.4 });
gsap.set(successGlow, { opacity: 0 });
place(0);
if (reduced) {
place(100);
activate(stops.length - 1);
finish();
return;
}
const proxy = { v: 0 };
// A fixed timeline "budget" (10 units, arbitrary but easy to reason
// about in tenths) split across stages so scroll % always means the
// same thing: 0-20% idle at Pickup Request, 20/40/60/80% continuous
// driving between the four remaining checkpoints, 95% arrival, and a
// 95-100% exit beat — never "scroll 40%, van still at 0%" or "van
// already parked at 80% with 20% of empty scroll left to go".
const IDLE = 2; // 0 -> 20%: waiting at Pickup Request (CSS idle-breathe/headlight-blink below, no JS needed)
const LEG = 2; // 20->40, 40->60, 60->80
const LEG4 = 1.5; // 80 -> 95
const EXIT = 0.5; // 95 -> 100: glow, badge, package-loaded toast, van fade
const CP_HOLD = 0.3; // brief full stop at a checkpoint before departing again — skipped on the very first leg, since the idle stage already is that pause
// end at "bottom 84%" (not 42%) so the ride finishes while the section's
// top — and the "AI Coordinated Pickup" badge pinned there — clears the
// ~115px sticky header with real margin, instead of the finish state
// landing scrolled out from under the nav.
const tl = gsap.timeline({
scrollTrigger: { trigger: wrap, start: "top 82%", end: "bottom 84%", scrub: 0.7 },
});
tl.call(() => activate(0), [], 0);
const legs = [
{ from: 0, to: 25, start: IDLE, dur: LEG, hold: 0 },
{ from: 25, to: 50, start: IDLE + LEG, dur: LEG, hold: CP_HOLD },
{ from: 50, to: 75, start: IDLE + LEG * 2, dur: LEG, hold: CP_HOLD },
{ from: 75, to: 100, start: IDLE + LEG * 3, dur: LEG4, hold: CP_HOLD },
];
legs.forEach((leg, idx) => {
const driveStart = leg.start + leg.hold;
const driveDur = leg.dur - leg.hold;
const accelD = driveDur * 0.32, cruiseD = driveDur * 0.36, brakeD = driveDur * 0.32;
const accelEnd = leg.from + (leg.to - leg.from) * 0.28;
const cruiseEnd = leg.from + (leg.to - leg.from) * 0.72;
const arrival = leg.start + leg.dur;
// Depart, cruise, brake — three distinct phases per leg instead of one
// symmetric ease, so the ride reads as accelerate / hold speed / slow
// into the checkpoint rather than one smooth blob of motion.
tl.to(proxy, { v: accelEnd, duration: accelD, ease: "power2.in", onUpdate: () => place(proxy.v) }, driveStart)
.to(proxy, { v: cruiseEnd, duration: cruiseD, ease: "none", onUpdate: () => place(proxy.v) }, driveStart + accelD)
.to(proxy, { v: leg.to, duration: brakeD, ease: "power2.out", onUpdate: () => place(proxy.v) }, driveStart + accelD + cruiseD)
.call(() => activate(idx + 1), [], arrival);
if (idx === 2) {
// OTP Verification arrival (80%).
tl.call(() => showToast("OTP Verified"), [], arrival)
.to(confirm, { opacity: 0, duration: 0.2 }, arrival + 0.25);
}
});
// Exit beat, entirely inside the last 5% of scroll: arrive -> scan ->
// package loaded -> success glow + badge swap -> van fades. The fade is
// a normal tween on this SAME scrubbed timeline (not a one-shot
// gsap.to() outside it), so scrolling back up smoothly un-fades the
// van instead of leaving it permanently hidden.
const exitStart = IDLE + LEG * 3 + LEG4; // 9.5 -> 95%, matches the last leg's own arrival time
tl.call(() => showToast("Scanning package…"), [], exitStart)
.call(() => showToast("Package Loaded"), [], exitStart + 0.15)
.to(confirm, { opacity: 0, duration: 0.15 }, exitStart + 0.3)
.call(finish, [], exitStart + 0.3)
.to(van, { opacity: 0, duration: EXIT - 0.3, ease: "power1.in" }, exitStart + 0.3);
tl.to(img, { scale: 1.07, ease: "none", duration: tl.duration() }, 0);
return () => {
tl.scrollTrigger?.kill();
tl.kill();
};
}, [stops.length]);
return (
<Section id="pickup" bg="charcoal">
<SectionHead
n="02"
eyebrow="First Mile Pickup"
title={<>Smart <span className="accent">pickup</span> management</>}
lede="Every pickup request is received, validated, assigned and confirmed before a single mile is driven — so deliveries start the right way, every time."
/>
<div className="sw-edge sw-pickup" ref={wrapRef}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img ref={imgRef} src="/images/tab-pic-1.webp" alt="Doormile pickup in progress" className="sw-pickup-img" loading="lazy" decoding="async" />
<div className="sw-pickup-overlay" aria-hidden="true" />
<span className="sw-pickup-ai-badge">
<span className="sw-pickup-ai-indicator">
<span className="sw-livedot" ref={aiDotRef} />
<span className="sw-pickup-ai-check" ref={aiCheckRef}>{IconCheck}</span>
</span>
<span ref={aiTextRef}>AI Coordinating Pickup</span>
</span>
<div className="sw-pickup-confirm" ref={confirmRef} aria-hidden="true">{IconCheck}<span ref={confirmLabelRef}>Verified</span></div>
<svg className="sw-pickup-road" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
<path d={PICKUP_ROAD_D} className="sw-pickup-road-shadow" />
<path d={PICKUP_ROAD_D} className="sw-pickup-road-path" />
<path d={PICKUP_ROAD_D} className="sw-pickup-road-lane" />
{/* Full route, always shown, in neutral grey — the "not yet
travelled" state, Google Maps/Uber style. */}
<path d={PICKUP_ROAD_D} className="sw-pickup-road-remaining" />
{/* Same path, drawn on top in brand red via strokeDashoffset —
covers the grey up to the van's current position only, so the
road ahead of the van always stays grey instead of the whole
route reading as "done" at once. */}
<path ref={routeProgressRef} d={PICKUP_ROAD_D} className="sw-pickup-road-progress" />
</svg>
<div className="sw-pickup-dest" style={{ left: `${PICKUP_DEST.x}%`, top: `${PICKUP_DEST.y}%` }}>
{IconPin}
<span>Pickup Point</span>
</div>
{/* Static left/top (matching place(0) in the effect below) so the
van renders parked at Pickup Request in the server-rendered
markup itself — visible on first paint, not just once JS
hydrates and the effect runs a moment later. */}
<div className="sw-pickup-van" ref={vanRef} style={{ left: `${PICKUP_PTS[0].x}%`, top: `${PICKUP_PTS[0].y}%` }}>
<span className="sw-pickup-van-shadow" ref={vanShadowRef} aria-hidden="true" />
<span className="sw-pickup-van-speedlines" aria-hidden="true"><i /><i /><i /></span>
<div className="sw-pickup-van-bounce" ref={vanBounceRef}>
<div className="sw-pickup-van-rot" ref={vanRotRef}>
<VanArt />
</div>
</div>
</div>
<span className="sw-pickup-success-glow" ref={successGlowRef} aria-hidden="true" />
{stops.map((s, i) => (
<div className="sw-pickup-stop" style={{ left: `${PICKUP_PTS[i].x}%`, top: `${PICKUP_PTS[i].y}%` }} ref={(el) => { stopRefs.current[i] = el; }} key={s.label}>
<span className="sw-pickup-stop-icwrap">
<span className="sw-pickup-stop-ripple" aria-hidden="true" />
<span className="sw-pickup-stop-ic">{s.icon}</span>
<span className="sw-pickup-stop-badge" aria-hidden="true">{IconCheck}</span>
</span>
<span className="sw-pickup-stop-label">{s.label}</span>
<span className="sw-pickup-stop-note">{s.note}</span>
</div>
))}
</div>
</Section>
);
}
/* ============================================================
SECTION 03 — AI Vehicle Selection (split layout)
Richer vehicle illustrations. Hovering an order profile triggers a
brief "AI evaluating" scan sweep before the vehicle reselects.
============================================================ */
/** Rejection reason shown on each non-winning vehicle card, indexed
* [profileIndex][vehicleIndex] — "" for whichever vehicle that profile
* actually selects (never read, since that card gets .is-winner instead). */
const VEH_REJECT_REASONS: string[][] = [
["", "Not Needed", "Too Large", "Too Large", "Too Large"], // Small Parcel -> Bike
["Too Small", "Capacity", "", "Overkill", "Too Large"], // Retail Restock -> Mini Truck
["Too Small", "Capacity", "Capacity", "", "Overkill"], // Bulk Pallet -> Van
["Range Limit", "Range Limit", "Capacity", "Capacity", ""], // Long Haul -> Truck
];
const VEH_DECISION_STEPS = ["Analysing", "Comparing", "Filtering", "Selecting", "Dispatch Ready"];
const AI_VEHICLE_SELECTION_ITEMS = [
{ title: "Small Parcel", text: "Instantly routed to agile urban two-wheeler & EV bike fleets for zero-delay last-mile delivery. (2 kg · 4 km)" },
{ title: "Retail Restock", text: "Optimized for mid-capacity electric vans and urban cargo units with strict SLA time windows. (80 kg · 12 km)" },
{ title: "Bulk Pallet", text: "Matched to heavy-duty mini trucks and multi-pallet freight vehicles with route weight clearance. (600 kg · 30 km)" },
{ title: "Long Haul", text: "Assigned to primary long-distance transport trucks with real-time driver & route monitoring. (2 t · 140 km)" },
];
function Sec03() {
return (
<ConnectedLogisticsSection
id="vehicle-ai"
eyebrow="/ Vehicle Selection /"
title="AI vehicle selection"
imageSrc="/images/home2-pic-3.webp"
imageAlt="AI Vehicle Selection"
items={AI_VEHICLE_SELECTION_ITEMS}
/>
);
}
/* ============================================================
SECTION 04 — Smart Route Intelligence (AI Decision Engine)
A complete replacement for the earlier map/vehicle visualization. The
real value of the product is the AI *making the decision*, not a truck
driving — so this is an enterprise decision dashboard, not navigation.
Four candidate routes appear as glass cards; the AI scores them on live
metrics (ETA / distance / fuel / traffic) with values counting in and
bars filling, then the losers mute and one card wins, a "Best Route
Selected" confirmation lands, and a small dispatch beat runs. Self-
looping ~9s real-time timeline (the model this section has always used),
started once on scroll-in.
============================================================ */
type AdeRoute = { id: string; name: string; eta: number; dist: number; fuel: number; traffic: "Light" | "Moderate" | "Heavy"; score: number; reason: string | null; winner?: boolean };
/** Deterministic candidates — the winner (D) is genuinely best on every
* axis (lowest ETA, shortest distance, least fuel, light traffic, top
* score) so the AI's choice reads as obviously correct, not arbitrary. */
const ADE_ROUTES: AdeRoute[] = [
{ id: "A", name: "Route A", eta: 22, dist: 19.8, fuel: 71, traffic: "Heavy", score: 64, reason: "Heavy traffic" },
{ id: "B", name: "Route B", eta: 18, dist: 22.4, fuel: 66, traffic: "Moderate", score: 73, reason: "Longer distance" },
{ id: "C", name: "Route C", eta: 16, dist: 17.1, fuel: 70, traffic: "Light", score: 81, reason: "Higher fuel cost" },
{ id: "D", name: "Route D", eta: 13, dist: 15.6, fuel: 52, traffic: "Light", score: 97, reason: null, winner: true },
];
const ADE_WINNER = ADE_ROUTES.findIndex((r) => r.winner);
function Sec04() {
const panelRef = useRef<HTMLDivElement>(null);
const statusRef = useRef<HTMLSpanElement>(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const flowRef = useRef<HTMLDivElement>(null);
const resultRef = useRef<HTMLDivElement>(null);
const dispatchRef = useRef<HTMLDivElement>(null);
const dispatchBarRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const panel = panelRef.current;
const status = statusRef.current;
const cards = cardRefs.current.filter(Boolean) as HTMLDivElement[];
const flow = flowRef.current;
const result = resultRef.current;
const dispatch = dispatchRef.current;
const dispatchBar = dispatchBarRef.current;
if (!panel || !status || cards.length === 0 || !flow || !result || !dispatch || !dispatchBar) return;
// Per-card element handles — queried once (same technique the old
// section used with the van's wheels) so the timeline can drive each
// card's numbers/bars without a giant ref matrix.
const parts = cards.map((card) => ({
card,
eta: card.querySelector<HTMLElement>('[data-k="eta"]')!,
dist: card.querySelector<HTMLElement>('[data-k="dist"]')!,
fuel: card.querySelector<HTMLElement>('[data-k="fuel"]')!,
fuelBar: card.querySelector<HTMLElement>(".sw-ade-fuel-bar > i")!,
scoreVal: card.querySelector<HTMLElement>(".sw-ade-score-val")!,
scoreBar: card.querySelector<HTMLElement>(".sw-ade-score-bar > i")!,
traffic: card.querySelector<HTMLElement>(".sw-ade-traffic")!,
verdict: card.querySelector<HTMLElement>(".sw-ade-verdict")!,
}));
const blankNums = () => {
parts.forEach((p) => { p.eta.textContent = "—"; p.dist.textContent = "—"; p.fuel.textContent = "—"; p.scoreVal.textContent = "0"; });
};
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) {
gsap.set(cards, { opacity: 1, y: 0 });
parts.forEach((p, i) => {
const r = ADE_ROUTES[i];
p.eta.textContent = `${r.eta}`; p.dist.textContent = r.dist.toFixed(1); p.fuel.textContent = `${r.fuel}`; p.scoreVal.textContent = `${r.score}`;
gsap.set(p.fuelBar, { width: `${r.fuel}%` }); gsap.set(p.scoreBar, { width: `${r.score}%` });
gsap.set([p.traffic, p.verdict], { opacity: 1, y: 0 });
p.card.classList.toggle("is-winner", i === ADE_WINNER);
p.card.classList.toggle("is-muted", i !== ADE_WINNER);
});
gsap.set([flow, result, dispatch], { opacity: 1, y: 0 });
gsap.set(dispatchBar, { width: "100%" });
status.textContent = "Best route selected";
return;
}
const STATUS: [number, string][] = [
[0, "Initializing decision engine…"],
[0.6, "Generating route options…"],
[1.9, "Reading live traffic & road data…"],
[3.0, "Calculating fuel, distance & ETA…"],
[4.2, "Scoring 4 candidate routes…"],
[5.6, "Comparing against optimal thresholds…"],
[6.6, "Best route selected"],
[7.6, "Dispatching vehicle…"],
[8.9, "Shipment in transit"],
];
const tl = gsap.timeline({ paused: true, repeat: -1, repeatDelay: 1.1 });
tl.eventCallback("onUpdate", () => {
const t = tl.time();
let s = STATUS[0][1];
for (const [time, text] of STATUS) { if (t >= time) s = text; else break; }
status.textContent = s;
// "AI is reading this card" glow only during the scoring window.
const scoring = t >= 1.9 && t < 5.5;
cards.forEach((c) => c.classList.toggle("is-scoring", scoring));
});
// Count-up helper — adds a proxy tween straight to the timeline (fromTo
// so every loop restarts cleanly from 0), writing the rounded value to
// the element each frame.
const countTo = (el: HTMLElement, to: number, at: number, dur: number, dec = 0) => {
const o = { v: 0 };
tl.fromTo(o, { v: 0 }, { v: to, duration: dur, ease: "power2.out", onUpdate: () => { el.textContent = dec ? o.v.toFixed(dec) : `${Math.round(o.v)}`; } }, at);
};
// t=0 reset (re-runs every loop): clear classes + blank the numbers so
// Step 1 always begins from a clean slate.
tl.call(() => { cards.forEach((c) => c.classList.remove("is-scoring", "is-muted", "is-winner")); blankNums(); }, [], 0);
// Step 1 — the four route options appear.
tl.fromTo(cards, { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.5, stagger: 0.12, ease: "power3.out" }, 0.5);
// Step 2 — the AI evaluates each route: metrics count in, bars fill,
// score climbs, traffic pill lands.
parts.forEach((p, i) => {
const r = ADE_ROUTES[i];
const at = 1.9 + i * 0.22;
tl.fromTo(p.traffic, { opacity: 0 }, { opacity: 1, duration: 0.3 }, at);
countTo(p.eta, r.eta, at, 1.0);
countTo(p.dist, r.dist, at, 1.0, 1);
countTo(p.fuel, r.fuel, at, 1.0);
tl.fromTo(p.fuelBar, { width: "0%" }, { width: `${r.fuel}%`, duration: 1.0, ease: "power2.out" }, at);
countTo(p.scoreVal, r.score, at + 0.3, 1.3);
tl.fromTo(p.scoreBar, { width: "0%" }, { width: `${r.score}%`, duration: 1.3, ease: "power2.out" }, at + 0.3);
});
// Flow connector fades in as the compare begins.
tl.fromTo(flow, { opacity: 0 }, { opacity: 1, duration: 0.4 }, 5.3);
// Step 3 — compare: losers mute, the winner highlights, verdicts land.
tl.call(() => {
cards.forEach((c, i) => { c.classList.toggle("is-winner", i === ADE_WINNER); c.classList.toggle("is-muted", i !== ADE_WINNER); });
}, [], 5.6);
parts.forEach((p, i) => { tl.fromTo(p.verdict, { opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" }, 5.7 + i * 0.1); });
// Step 4 — the confirmation banner.
tl.fromTo(result, { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: 0.5, ease: "back.out(1.4)" }, 6.6);
// Step 5 — dispatch: truck + "Delivery Started" + a progress bar.
tl.fromTo(dispatch, { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.45, ease: "power2.out" }, 7.6);
tl.fromTo(dispatchBar, { width: "0%" }, { width: "100%", duration: 1.4, ease: "power2.inOut" }, 7.8);
const st = ScrollTrigger.create({ trigger: panel, start: "top 80%", once: true, onEnter: () => tl.play(0) });
return () => { st.kill(); tl.kill(); };
}, []);
return (
<Section id="routing" bg="white">
<div className="sw-qhead">
<ScrollReveal yOffset={16} triggerOnce>
<div className="sw-qhead-kicker">
<span className="sw-qhead-eyebrow">/ Smart Route Intelligence /</span>
<hr className="sw-qhead-divider" />
</div>
</ScrollReveal>
<ScrollReveal yOffset={20} delay={0.05} triggerOnce>
<div className="sw-qhead-stroke-title">
Smart Route Intelligence
</div>
</ScrollReveal>
<div className="sw-qhead-split">
<GsapWordRevealText
tag="h2"
className="sw-qhead-h2"
style={{ textTransform: "none" }}
delay={0.08}
>
Parallel Evaluation & Instant AI Dispatch
</GsapWordRevealText>
<div className="sw-qhead-desc">
<GsapWordRevealText tag="p" delay={0.16}>
Before a single dispatch is made, Doormile generates every possible route and lets the AI decide in real time.
</GsapWordRevealText>
<GsapWordRevealText tag="p" delay={0.24}>
Scoring each option on live traffic, fuel, distance, and ETA guarantees every order is committed to the single path that wins.
</GsapWordRevealText>
</div>
</div>
</div>
<div className="sw-ade" ref={panelRef}>
<div className="sw-ade-bar">
<span className="sw-ade-brand"><span className="sw-ade-brand-ic">{IconCpu}</span>AI Decision Engine</span>
<span className="sw-ade-live"><span className="sw-ade-livedot" aria-hidden="true" />LIVE</span>
</div>
<div className="sw-ade-status-row"><span className="sw-ade-status" ref={statusRef} /></div>
<div className="sw-ade-grid">
{ADE_ROUTES.map((r, i) => (
<div className="sw-ade-card" key={r.id} ref={(el) => { cardRefs.current[i] = el; }}>
<div className="sw-ade-card-top">
<span className="sw-ade-card-name">{r.name}</span>
<span className={`sw-ade-traffic sw-ade-traffic--${r.traffic.toLowerCase()}`}>{r.traffic}</span>
</div>
<div className="sw-ade-rows">
<div className="sw-ade-row"><span className="sw-ade-row-l">ETA</span><span className="sw-ade-row-v"><b data-k="eta"></b> min</span></div>
<div className="sw-ade-row"><span className="sw-ade-row-l">Distance</span><span className="sw-ade-row-v"><b data-k="dist"></b> km</span></div>
<div className="sw-ade-row"><span className="sw-ade-row-l">Fuel</span><span className="sw-ade-row-v"><b data-k="fuel"></b> %</span></div>
<div className="sw-ade-fuel-bar"><i /></div>
</div>
<div className="sw-ade-score">
<div className="sw-ade-score-head"><span>AI Score</span><span className="sw-ade-score-val">0</span></div>
<div className="sw-ade-score-bar"><i /></div>
</div>
<div className="sw-ade-verdict">
{r.winner
? <><span className="sw-ade-verdict-ic sw-ade-verdict-ic--win">{IconCheck}</span>Best match</>
: <><span className="sw-ade-verdict-ic">{IconX}</span>{r.reason}</>}
</div>
</div>
))}
</div>
<div className="sw-ade-flow" ref={flowRef}>
<span className="sw-ade-flow-arrow" aria-hidden="true" />
<span className="sw-ade-flow-chip">{IconCpu}AI Analysis</span>
<span className="sw-ade-flow-arrow" aria-hidden="true" />
</div>
<div className="sw-ade-result" ref={resultRef}>
<span className="sw-ade-result-badge"><span className="sw-ade-livedot sw-ade-livedot--green" aria-hidden="true" />{IconCheck}Best Route Selected</span>
<span className="sw-ade-result-sub">Route D · 13 min ETA · 52% fuel · Light traffic</span>
</div>
<div className="sw-ade-dispatch" ref={dispatchRef}>
<span className="sw-ade-dispatch-ic">{IconTruck}</span>
<div className="sw-ade-dispatch-body">
<span className="sw-ade-dispatch-text">Shipment In Transit</span>
<span className="sw-ade-dispatch-bar"><i ref={dispatchBarRef} /></span>
</div>
<span className="sw-ade-dispatch-eta">ETA 13 min</span>
</div>
</div>
</Section>
);
}
/* ============================================================
SECTION 05 — Quantum Decision Engine (near-full-viewport flagship)
============================================================ */
function Sec05() {
return (
<Section id="quantum" bg="white">
<div className="sw-qhead">
<ScrollReveal yOffset={16} triggerOnce>
<div className="sw-qhead-kicker">
<span className="sw-qhead-eyebrow">/ Quantum Decision Engine /</span>
<hr className="sw-qhead-divider" />
</div>
</ScrollReveal>
<ScrollReveal yOffset={20} delay={0.05} triggerOnce>
<div className="sw-qhead-stroke-title">
Quantum Decision Engine
</div>
</ScrollReveal>
<div className="sw-qhead-split">
<GsapWordRevealText
tag="h2"
className="sw-qhead-h2"
style={{ textTransform: "none" }}
delay={0.08}
>
Simulating Hundreds of Parallel Possibilities in Real Time
</GsapWordRevealText>
<div className="sw-qhead-desc">
<GsapWordRevealText tag="p" delay={0.16}>
Traditional systems calculate one static route and dispatch it sequentially. Doormile Quantum evaluates hundreds of route possibilities simultaneously in milliseconds.
</GsapWordRevealText>
<GsapWordRevealText tag="p" delay={0.24}>
Simulating every traffic, SLA, and fuel cost outcome in parallel guarantees every order is auto-dispatched along the single most efficient path.
</GsapWordRevealText>
</div>
</div>
</div>
<QuantumStage />
</Section>
);
}
/* ---------------------------------------------------------------
Phone chrome: a real iOS-style status bar (live JS clock, static
signal/5G/Wi-Fi/battery icons) and a glass notification banner.
Purely presentational additions to the Sec06 mockup — they don't
touch the map SVG or its native marker animation.
--------------------------------------------------------------- */
function PhoneStatusBar() {
// Rendered empty on the server / first paint so client and server markup
// agree, then filled in and re-synced on each minute boundary.
const [time, setTime] = useState("");
useEffect(() => {
const fmt = () => {
const d = new Date();
let h = d.getHours() % 12;
if (h === 0) h = 12;
return `${h}:${d.getMinutes().toString().padStart(2, "0")}`;
};
setTime(fmt());
let interval: ReturnType<typeof setInterval> | undefined;
const now = new Date();
const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
const timeout = setTimeout(() => {
setTime(fmt());
interval = setInterval(() => setTime(fmt()), 60000);
}, msToNextMinute);
return () => {
clearTimeout(timeout);
if (interval) clearInterval(interval);
};
}, []);
return (
<div className="sw-phone-statusbar" aria-hidden="true">
<span className="sw-phone-time">{time}</span>
<span className="sw-phone-status-icons">
<svg className="sw-phone-ic" viewBox="0 0 18 12" fill="currentColor">
<rect x="0" y="8" width="3" height="4" rx="1" />
<rect x="5" y="5.5" width="3" height="6.5" rx="1" />
<rect x="10" y="3" width="3" height="9" rx="1" />
<rect x="15" y="0.5" width="3" height="11.5" rx="1" />
</svg>
<span className="sw-phone-5g">5G</span>
<svg className="sw-phone-ic sw-phone-ic-wifi" viewBox="0 0 16 12" fill="currentColor">
<path d="M8 2.4c2.7 0 5.2 1 7.1 2.8L13.5 6.9C12 5.5 10.1 4.7 8 4.7S4 5.5 2.5 6.9L.9 5.2C2.8 3.4 5.3 2.4 8 2.4z" />
<path d="M8 6.6c1.5 0 2.9.6 3.9 1.6L8 12 4.1 8.2C5.1 7.2 6.5 6.6 8 6.6z" />
</svg>
<svg className="sw-phone-ic sw-phone-ic-batt" viewBox="0 0 27 13" fill="none">
<rect x="0.6" y="0.6" width="22" height="11.8" rx="3.4" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1.1" />
<rect x="2.2" y="2.2" width="17.5" height="8.6" rx="2" fill="currentColor" />
<path d="M24.6 4.2c1 .5 1 3.6 0 4.1z" fill="currentColor" fillOpacity="0.5" />
</svg>
</span>
</div>
);
}
function PhoneNotification() {
return (
<div className="sw-phone-notif">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/images/cropped-image-2.png" alt="Doormile Logo" className="sw-phone-notif-ic" loading="lazy" decoding="async" />
<div className="sw-phone-notif-body">
<div className="sw-phone-notif-top">
<span className="sw-phone-notif-app">Doormile</span>
<span className="sw-phone-notif-time">now</span>
</div>
<span className="sw-phone-notif-title">Your shipment is on the way</span>
<span className="sw-phone-notif-sub">Arriving in 12 min</span>
</div>
</div>
);
}
/* ---------------------------------------------------------------
Sec06 feature-card hero illustrations. Large, flat, brand-red-on-dark
SVGs (not tiny icons) — each is the visual hero of its card. Declared
at module scope so the React Compiler treats them as stable components.
--------------------------------------------------------------- */
function ArtTracking() {
return (
<svg viewBox="0 0 340 200" className="sw-ldi-svg" role="img" aria-label="Live map tracking">
<defs>
<linearGradient id="ldiTrackBg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="rgba(255,255,255,0.07)" /><stop offset="1" stopColor="rgba(255,255,255,0.015)" /></linearGradient>
<linearGradient id="ldiRoute" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stopColor="#C01227" /><stop offset="1" stopColor="#ff6a7d" /></linearGradient>
<radialGradient id="ldiPulse" cx="0.5" cy="0.5" r="0.5"><stop offset="0" stopColor="rgba(226,53,74,0.55)" /><stop offset="1" stopColor="rgba(226,53,74,0)" /></radialGradient>
</defs>
<rect x="0.5" y="0.5" width="339" height="199" rx="18" fill="url(#ldiTrackBg)" stroke="rgba(255,255,255,0.08)" />
<g stroke="rgba(255,255,255,0.05)" strokeWidth="1">
<path d="M0 50h340M0 100h340M0 150h340M85 0v200M170 0v200M255 0v200" />
</g>
<g stroke="rgba(255,255,255,0.06)" strokeWidth="9" strokeLinecap="round" fill="none">
<path d="M-10 130 H120 V-10" /><path d="M255 210 V70 H360" />
</g>
<path d="M42 162 C 96 150 96 96 150 96 S 232 66 300 40" fill="none" stroke="url(#ldiRoute)" strokeWidth="5" strokeLinecap="round" />
<path d="M42 162 C 96 150 96 96 150 96 S 232 66 300 40" fill="none" stroke="rgba(255,255,255,0.5)" strokeWidth="5" strokeLinecap="round" strokeDasharray="1 15" opacity="0.5" />
<circle cx="42" cy="162" r="7" fill="#0e0f12" stroke="#E2354A" strokeWidth="2.5" />
<circle cx="150" cy="96" r="28" fill="url(#ldiPulse)" />
<circle cx="150" cy="96" r="9.5" fill="#E2354A" stroke="#fff" strokeWidth="2.6" />
<path d="M300 20a17 17 0 00-17 17c0 12 17 27 17 27s17-15 17-27a17 17 0 00-17-17z" fill="#fff" />
<circle cx="300" cy="37" r="6.2" fill="#C01227" />
</svg>
);
}
function ArtVisibility() {
const nodes = [
{ x: 46, y: 40 }, { x: 294, y: 40 }, { x: 46, y: 160 }, { x: 294, y: 160 },
];
return (
<svg viewBox="0 0 340 200" className="sw-ldi-svg" role="img" aria-label="Shared operational visibility">
<defs>
<linearGradient id="ldiVisBg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="rgba(255,255,255,0.07)" /><stop offset="1" stopColor="rgba(255,255,255,0.015)" /></linearGradient>
<radialGradient id="ldiHub" cx="0.5" cy="0.5" r="0.5"><stop offset="0" stopColor="#ff6a7d" /><stop offset="1" stopColor="#C01227" /></radialGradient>
<radialGradient id="ldiHubGlow" cx="0.5" cy="0.5" r="0.5"><stop offset="0" stopColor="rgba(226,53,74,0.5)" /><stop offset="1" stopColor="rgba(226,53,74,0)" /></radialGradient>
</defs>
<rect x="0.5" y="0.5" width="339" height="199" rx="18" fill="url(#ldiVisBg)" stroke="rgba(255,255,255,0.08)" />
<g stroke="rgba(226,53,74,0.4)" strokeWidth="1.6" strokeDasharray="2 6">
{nodes.map((n, i) => <line key={i} x1="170" y1="100" x2={n.x} y2={n.y} />)}
</g>
{nodes.map((n, i) => (
<g key={i}>
<circle cx={n.x} cy={n.y} r="20" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.12)" />
<circle cx={n.x} cy={n.y - 5} r="6" fill="rgba(255,255,255,0.55)" />
<path d={`M${n.x - 10} ${n.y + 13}c0 -7 4.5 -11 10 -11s10 4 10 11z`} fill="rgba(255,255,255,0.35)" />
</g>
))}
<circle cx="170" cy="100" r="52" fill="url(#ldiHubGlow)" />
<circle cx="170" cy="100" r="30" fill="url(#ldiHub)" />
<circle cx="170" cy="100" r="30" fill="none" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" />
<circle cx="158" cy="100" r="3.4" fill="#fff" /><circle cx="170" cy="100" r="3.4" fill="#fff" /><circle cx="182" cy="100" r="3.4" fill="#fff" />
</svg>
);
}
function ArtProof() {
return (
<svg viewBox="0 0 340 200" className="sw-ldi-svg" role="img" aria-label="Verified proof of delivery">
<defs>
<linearGradient id="ldiProofBg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="rgba(255,255,255,0.07)" /><stop offset="1" stopColor="rgba(255,255,255,0.015)" /></linearGradient>
<linearGradient id="ldiBox" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#e8e9ee" /><stop offset="1" stopColor="#b7b9c2" /></linearGradient>
<linearGradient id="ldiBoxTop" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#f6f6f8" /><stop offset="1" stopColor="#d5d6dc" /></linearGradient>
<linearGradient id="ldiShield" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#ff6a7d" /><stop offset="1" stopColor="#C01227" /></linearGradient>
</defs>
<rect x="0.5" y="0.5" width="339" height="199" rx="18" fill="url(#ldiProofBg)" stroke="rgba(255,255,255,0.08)" />
{/* parcel */}
<path d="M78 92 L128 74 L188 92 L138 112 Z" fill="url(#ldiBoxTop)" />
<path d="M78 92 L138 112 L138 166 L78 146 Z" fill="url(#ldiBox)" />
<path d="M188 92 L138 112 L138 166 L188 146 Z" fill="#9a9ca6" />
<path d="M128 74 L128 128 L138 112 Z" fill="rgba(192,18,39,0.85)" />
<path d="M78 92 L138 112 L128 128 L78 108 Z" fill="rgba(0,0,0,0.08)" />
{/* OTP dots */}
<g>
<rect x="196" y="150" width="86" height="26" rx="8" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" />
<circle cx="212" cy="163" r="4" fill="#E2354A" /><circle cx="228" cy="163" r="4" fill="#E2354A" /><circle cx="244" cy="163" r="4" fill="rgba(255,255,255,0.3)" /><circle cx="260" cy="163" r="4" fill="rgba(255,255,255,0.3)" />
</g>
{/* shield check */}
<path d="M228 40l30 11v22c0 20-13 31-30 37-17-6-30-17-30-37V51l30-11z" fill="url(#ldiShield)" />
<path d="M228 40l30 11v22c0 20-13 31-30 37-17-6-30-17-30-37V51l30-11z" fill="none" stroke="rgba(255,255,255,0.4)" strokeWidth="1.4" />
<path d="M216 74l9 9 16-19" fill="none" stroke="#fff" strokeWidth="4.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/* ============================================================
SECTION 06 — Live Delivery Intelligence
Top-anchored heading, then a two-column showcase: a sticky live
phone mockup (unchanged) beside four scroll-revealed feature cards.
============================================================ */
function Sec06() {
// ETA still counts down live inside the phone's bottom card.
const [eta, setEta] = useState(24);
useEffect(() => {
const id = setInterval(() => setEta((v) => (v <= 4 ? 24 : v - 1)), 1400);
return () => clearInterval(id);
}, []);
const headingLines = [
<>Every shipment.</>,
<><span className="accent">Visible.</span></>,
<>In <span className="accent">real time.</span></>,
];
const features = [
{
art: <ArtTracking />,
title: "Live Tracking",
desc: "Follow every fleet driver on a live map from pickup to drop, with the exact location and route updating second by second.",
},
{
art: (
<div className="sw-ldi-card-art-stage">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="sw-ldi-svg-img"
src="/doorven.svg"
alt="Shipment journey — pickup request, fleet driver assignment, OTP verification and package collection"
/>
</div>
),
title: "Shipment Journey",
desc: "Track every milestone — from pickup request and fleet driver assignment to OTP verification and successful package collection — with complete real-time visibility.",
},
{
art: <ArtVisibility />,
title: "Shared Visibility",
desc: "Merchants, fleet drivers, operations teams and clients all watch the same shipment journey from a single source of truth.",
},
{
art: <ArtProof />,
title: "Proof of Delivery",
desc: "Secure OTP and photo confirmation close every shipment at the doorstep, giving tamper-proof evidence the parcel arrived.",
},
];
return (
<Section id="tracking" bg="grey">
<div className="sw-ldi">
<div className="sw-ldi-head">
<motion.span
className="sw-eyebrow sw-ldi-eyebrow"
initial={{ opacity: 0, y: 16, filter: "blur(8px)" }}
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
viewport={{ once: true, margin: "-12% 0px" }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
/ Live Logistics Intelligence /
</motion.span>
<h2 className="sw-ldi-h">
{headingLines.map((line, i) => (
<motion.span
className="sw-ldi-line"
key={i}
initial={{ opacity: 0, y: 26, filter: "blur(14px)" }}
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
viewport={{ once: true, margin: "-12% 0px" }}
transition={{ duration: 0.7, delay: 0.15 + i * 0.12, ease: [0.16, 1, 0.3, 1] }}
>
{line}
</motion.span>
))}
</h2>
<motion.p
className="sw-ldi-lede"
initial={{ opacity: 0, y: 18, filter: "blur(8px)" }}
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
viewport={{ once: true, margin: "-10% 0px" }}
transition={{ duration: 0.7, delay: 0.5, ease: [0.16, 1, 0.3, 1] }}
>
Every stakeholder sees the same shipment journey in real time from dispatch to
doorstep with live tracking, intelligent updates and complete operational visibility.
</motion.p>
</div>
<div className="sw-ldi-grid">
<div className="sw-ldi-phone-col">
<div className="sw-ldi-phone-sticky">
<div className="sw-phone">
<div className="sw-phone-screen">
<div className="sw-phone-island" />
<PhoneStatusBar />
<PhoneNotification />
<div className="sw-phone-map">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="sw-phone-track-svg"
src="/live-track.svg"
alt="Live shipment route with the fleet driver moving along the tracked path"
/>
<div className="sw-phone-badge">
<motion.span
className="sw-phone-badge-dot"
animate={{ scale: [1, 1.35, 1], opacity: [0.65, 1, 0.65] }}
transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut" }}
/>
LIVE
</div>
</div>
<div className="sw-phone-card">
<span className="sw-phone-card-label">Arriving in</span>
<span className="sw-phone-card-eta">
{eta}
<em>min</em>
</span>
</div>
</div>
</div>
</div>
</div>
<div className="sw-ldi-cards">
{features.map((f, i) => (
<motion.article
className="sw-ldi-card"
key={f.title}
initial={{ opacity: 0, y: 40, scale: 0.96 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once: true, margin: "-12% 0px" }}
transition={{ duration: 0.8, delay: i * 0.08, ease: [0.16, 1, 0.3, 1] }}
>
<div className="sw-ldi-card-art">{f.art}</div>
<h3 className="sw-ldi-card-title">{f.title}</h3>
<p className="sw-ldi-card-desc">{f.desc}</p>
</motion.article>
))}
</div>
</div>
</div>
</Section>
);
}
/* ============================================================
SECTION 07 — Door-to-Door Delivery Workflow
Edge-to-edge aerial banner. GSAP scrubs the timeline fill, moves a
travelling marker, and highlights the current stage.
============================================================ */
function Sec07() {
const stages = [
{ icon: IconBox, label: "Pickup", note: "Order collected from source" },
{ icon: IconLayers, label: "Sorting", note: "Smart sorting at hubs" },
{ icon: IconRoute, label: "Route Assignment", note: "AI assigns best route & vehicle" },
{ icon: IconTruck, label: "Transit", note: "Real-time monitoring" },
{ icon: IconPin, label: "Out For Delivery", note: "Fleet driver reaches destination" },
{ icon: IconBoxCheck, label: "Delivered", note: "Proof of delivery & confirmation" },
];
const wrapRef = useRef<HTMLDivElement>(null);
const stageRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const wrap = wrapRef.current;
if (!wrap) return;
const stageEls = stageRefs.current.filter(Boolean) as HTMLDivElement[];
const setActiveIndex = (idx: number) => {
stageEls.forEach((el, i) => el.classList.toggle("is-active", i <= idx));
};
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
gsap.set(wrap, { "--fill": 0 } as gsap.TweenVars);
if (reduced) {
gsap.set(wrap, { "--fill": 1 } as gsap.TweenVars);
setActiveIndex(stages.length - 1);
return;
}
const st = ScrollTrigger.create({
trigger: wrap,
start: "top 80%",
end: "bottom 45%",
scrub: 0.6,
onUpdate: (self) => {
gsap.set(wrap, { "--fill": self.progress } as gsap.TweenVars);
setActiveIndex(Math.min(stages.length - 1, Math.floor(self.progress * stages.length)));
},
});
return () => st.kill();
}, [stages.length]);
return (
<Section id="delivery-flow" bg="charcoal">
<SectionHead
n="07"
eyebrow="Door to Door Delivery"
title={<>Door-to-door <span className="accent">logistics</span>, simplified</>}
lede="One continuous lifecycle — from the moment an order is picked up to the moment it's signed for at the door — visible at every stage, for every stakeholder."
/>
<div className="sw-edge sw-flow" ref={wrapRef}>
<div className="sw-flow-bg" aria-hidden="true">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/images/miletruth-bg.webp" alt="" loading="lazy" decoding="async" />
</div>
<div className="sw-flow-rail">
{stages.map((s, i) => (
<div className="sw-flow-stage" ref={(el) => { stageRefs.current[i] = el; }} key={s.label}>
<span className="sw-flow-num">{String(i + 1).padStart(2, "0")}</span>
<span className="sw-flow-ic">{s.icon}</span>
<span className="sw-flow-label">{s.label}</span>
<span className="sw-flow-note">{s.note}</span>
</div>
))}
<div className="sw-flow-track" aria-hidden="true"><span className="sw-flow-track-fill" /><span className="sw-flow-marker" /></div>
</div>
</div>
</Section>
);
}
/* ============================================================
SECTION 08 — MileTruth™ AI (split layout)
The page's second (lightweight) 3D surface, paired beside the
sequential data -> AI -> recommendation -> improvement flow.
============================================================ */
function Sec08({ sectionId = "miletruth" }: { sectionId?: string } = {}) {
const flow = [
{ icon: IconDatabase, label: "Logistics Data", note: "Every completed trip" },
{ icon: IconCpu, label: "MileTruth AI", note: "Patterns & anomalies" },
{ icon: IconBulb, label: "Recommendations", note: "What to change" },
{ icon: IconTrendUp, label: "Improvement", note: "Measured lift" },
];
const feats = [
{ icon: IconShieldCheck, label: "SLA Monitoring" },
{ icon: IconTarget, label: "Demand Forecasting" },
{ icon: IconRoute, label: "Route & Fleet Optimization" },
{ icon: IconAlert, label: "Anomaly Detection" },
{ icon: IconTrendUp, label: "Performance Insights" },
];
return (
<Section id={sectionId} bg="mesh">
<div className="sw-split">
<div className="sw-mt-copy">
<SectionHead
n="08"
eyebrow="MileTruth AI"
title={<>MileTruth<span className="sw-tm"></span> <span className="accent">AI</span></>}
lede="MileTruth continuously analyzes every completed shipment — turning raw operational data into recommendations that make the next mile faster, cheaper and more reliable."
className="sw-head--split"
/>
<div className="sw-mt-flow">
{flow.map((f, i) => (
<React.Fragment key={f.label}>
<motion.div
className="sw-mt-step"
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10% 0px" }}
transition={{ duration: 0.5, delay: i * 0.12 }}
>
<span className="sw-mt-step-ic">{f.icon}</span>
<span className="sw-mt-step-label">{f.label}</span>
<span className="sw-mt-step-note">{f.note}</span>
</motion.div>
{i < flow.length - 1 && <span className="sw-veh-arrow" aria-hidden="true"></span>}
</React.Fragment>
))}
</div>
<div className="sw-mt-feats">
{feats.map((f) => (
<span className="sw-mt-feat" key={f.label}><span className="sw-mt-feat-ic">{f.icon}</span>{f.label}</span>
))}
</div>
</div>
<div className="sw-mt-canvas-col">
<div className="sw-mt-canvas"><MileTruthNetwork ariaLabel="MileTruth AI learning loop — shipment data feeds the AI core, which issues recommendations whose measured results feed the next shipment" /></div>
</div>
</div>
</Section>
);
}
/* ============================================================
SECTION 09 — Operations Dashboard
Premium glass panel, widened. Framer Motion counters, growing
charts, staggered reveals, and a live-scrolling activity feed.
============================================================ */
const FEED_EVENTS = [
"Order #48213 delivered — Chennai Zone 4",
"Fleet driver assigned — TN 09 CD 7790",
"SLA risk flagged — Route 12, rerouting",
"Pickup confirmed — OTP verified, Anna Nagar",
"EV charging stop scheduled — Fleet 3",
"98.5% on-time — last 24 hours",
];
function LiveFeed() {
const [rows, setRows] = useState(() => FEED_EVENTS.slice(0, 4));
const nextRef = useRef(4);
const slotRef = useRef(0);
useEffect(() => {
const id = setInterval(() => {
const slot = slotRef.current;
const next = nextRef.current;
setRows((current) => {
const updated = [...current];
updated[slot] = FEED_EVENTS[next % FEED_EVENTS.length];
return updated;
});
slotRef.current = (slot + 1) % 4;
nextRef.current = next + 1;
}, 2500);
return () => clearInterval(id);
}, []);
return (
<div className="sw-feed">
{rows.map((txt, i) => (
<div className="sw-feed-slot" key={i}>
<AnimatePresence initial={false} mode="wait">
<motion.div
key={txt}
className="sw-feed-row"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
>
<span className="sw-livedot" />{txt}
</motion.div>
</AnimatePresence>
</div>
))}
</div>
);
}
function Sec09() {
const [kpiValues, setKpiValues] = useState({
activeFleet: 482,
shipments: 3140,
sla: 98.5,
exceptions: 12,
});
const [statusMix, setStatusMix] = useState([68, 22, 10]);
const [bars, setBars] = useState([38, 52, 46, 61, 58, 72, 66]);
const [heat, setHeat] = useState([12, 34, 62, 48, 20, 8, 44, 70, 58, 30, 16, 52, 66, 40, 22, 10, 36, 60, 54, 28, 14, 46, 64, 38, 20, 8, 30, 56]);
const [hotCells, setHotCells] = useState<number[]>([]);
const [fleet, setFleet] = useState([
{ id: "TN 01 EV 1234", status: "On Route", eta: "12 min", tone: "moving" },
{ id: "TN 04 AB 5521", status: "Delayed", eta: "28 min", tone: "delayed" },
{ id: "TN 09 CD 7790", status: "Loading", eta: "—", tone: "loading" },
{ id: "TN 02 XY 3348", status: "Idle", eta: "—", tone: "idle" },
]);
useEffect(() => {
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) return;
const kpiId = setInterval(() => {
setKpiValues((v) => ({
activeFleet: Math.max(476, Math.min(489, v.activeFleet + (Math.random() > 0.5 ? 1 : -1) * (Math.random() > 0.72 ? 2 : 1))),
shipments: v.shipments + 1 + Math.floor(Math.random() * 3),
sla: Math.max(98.3, Math.min(98.7, Number((v.sla + (Math.random() > 0.5 ? 0.1 : -0.1)).toFixed(1)))),
exceptions: Math.max(9, Math.min(15, v.exceptions + (Math.random() > 0.72 ? (Math.random() > 0.5 ? 1 : -1) : 0))),
}));
}, 2600);
const mixId = setInterval(() => {
const delivered = 66 + Math.round(Math.random() * 4);
const delayed = 9 + Math.round(Math.random() * 2);
setStatusMix([delivered, 100 - delivered - delayed, delayed]);
}, 4200);
const barsId = setInterval(() => {
setBars((current) => current.map((value) => Math.max(32, Math.min(78, value + Math.round(Math.random() * 6 - 3)))));
}, 3200);
const heatId = setInterval(() => {
const changed = Array.from({ length: 4 }, () => Math.floor(Math.random() * 28));
setHotCells(changed);
setHeat((current) => current.map((value, index) => (
changed.includes(index)
? Math.max(8, Math.min(72, value + Math.round(Math.random() * 18 - 9)))
: value
)));
window.setTimeout(() => setHotCells([]), 900);
}, 2100);
const fleetStates = [
{ status: "On Route", tone: "moving" },
{ status: "Loading", tone: "loading" },
{ status: "Delayed", tone: "delayed" },
{ status: "Idle", tone: "idle" },
];
const fleetId = setInterval(() => {
setFleet((current) => {
const index = Math.floor(Math.random() * current.length);
const nextState = fleetStates[Math.floor(Math.random() * fleetStates.length)];
return current.map((row, rowIndex) => {
if (rowIndex !== index) return row;
const eta =
nextState.tone === "idle" || nextState.tone === "loading"
? "—"
: `${6 + Math.floor(Math.random() * 24)} min`;
return { ...row, ...nextState, eta };
});
});
}, 5200);
return () => {
clearInterval(kpiId);
clearInterval(mixId);
clearInterval(barsId);
clearInterval(heatId);
clearInterval(fleetId);
};
}, []);
const kpis = [
{ icon: IconTruck, value: kpiValues.activeFleet, label: "Active Fleet" },
{ icon: IconBox, value: kpiValues.shipments, label: "Shipments Today" },
{ icon: IconTarget, value: kpiValues.sla, decimals: 1, suffix: "%", label: "SLA Compliance" },
{ icon: IconAlert, value: kpiValues.exceptions, label: "Open Exceptions" },
];
const heatColor = (v: number) => `rgba(192,18,39,${0.08 + (v / 70) * 0.6})`;
const ringSegs = [
{ pct: statusMix[0], color: "var(--sw-red)" },
{ pct: statusMix[1], color: "rgba(17,24,39,0.3)" },
{ pct: statusMix[2], color: "rgba(17,24,39,0.12)" },
];
const circ = 2 * Math.PI * 15.9155;
let cum = 0;
return (
<Section id="ops-dashboard" bg="white">
<SectionHead
n="09"
eyebrow="Operations Dashboard"
title={<>Operations <span className="accent">dashboard</span></>}
lede="One control tower for every operations team — fleet, drivers, shipments, exceptions, analytics and reports, all live in a single view."
/>
<div className="sw-panel sw-dash">
<div className="sw-dash-chrome">
<span className="sw-dash-brand"><span className="sw-livedot" />Doormile Operations</span>
<span className="sw-dash-live">LIVE · Updated just now</span>
</div>
<div className="sw-dash-kpis">
{kpis.map((k, i) => (
<motion.div
className="sw-kpi-tile"
key={k.label}
initial={{ opacity: 0, y: 18 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10% 0px" }}
transition={{ duration: 0.5, delay: i * 0.08 }}
>
<span className="sw-kpi-tile-ic">{k.icon}</span>
<b className="sw-kpi-val"><FCount to={k.value} decimals={k.decimals ?? 0} suffix={k.suffix ?? ""} /></b>
<span className="sw-kpi-label">{k.label}</span>
</motion.div>
))}
</div>
<div className="sw-dash-body">
<motion.div className="sw-dash-panel" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10% 0px" }} transition={{ duration: 0.5 }}>
<span className="sw-dash-panel-h">Shipment Status Mix</span>
<div className="sw-ring-wrap">
<svg viewBox="0 0 36 36" className="sw-ring">
<circle cx="18" cy="18" r="15.9155" className="sw-ring-track" />
{ringSegs.map((s, i) => {
const segLen = (s.pct / 100) * circ;
const dash = `${segLen} ${circ - segLen}`;
const offset = -cum;
cum += segLen;
return (
<motion.circle key={i} cx="18" cy="18" r="15.9155" fill="none" stroke={s.color} strokeWidth="3.2"
animate={{ strokeDasharray: dash, strokeDashoffset: offset }}
transition={{ duration: 0.75, ease: [0.16, 1, 0.3, 1] }}
transform="rotate(-90 18 18)" />
);
})}
</svg>
<div className="sw-ring-center"><b>{kpiValues.sla.toFixed(1)}%</b><span>On-Time</span></div>
</div>
<div className="sw-ring-legend">
<span><i style={{ background: "var(--sw-red)" }} />Delivered</span>
<span><i style={{ background: "rgba(17,24,39,0.3)" }} />In Transit</span>
<span><i style={{ background: "rgba(17,24,39,0.12)" }} />Delayed</span>
</div>
</motion.div>
<motion.div className="sw-dash-panel" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10% 0px" }} transition={{ duration: 0.5, delay: 0.08 }}>
<span className="sw-dash-panel-h">Weekly Shipment Volume</span>
<div className="sw-bars">
{bars.map((h, i) => (
<motion.span
key={i}
className="sw-bar"
animate={{ height: `${h}%` }}
initial={{ scaleY: 0 }}
whileInView={{ scaleY: 1 }}
viewport={{ once: true, margin: "-10% 0px" }}
transition={{ duration: 0.75, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
/>
))}
</div>
</motion.div>
<motion.div className="sw-dash-panel" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10% 0px" }} transition={{ duration: 0.5, delay: 0.16 }}>
<span className="sw-dash-panel-h">Zone Demand Heatmap</span>
<div className="sw-heat">
{heat.map((v, i) => (
<motion.span
key={i}
className={`sw-heat-cell${hotCells.includes(i) ? " is-hot" : ""}`}
animate={{
backgroundColor: heatColor(v),
opacity: 1,
scale: hotCells.includes(i) ? 1.12 : 1,
}}
initial={{ opacity: 0, scale: 0.5 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: "-10% 0px" }}
transition={{ duration: 0.45, delay: i * 0.006 }}
/>
))}
</div>
</motion.div>
<motion.div className="sw-dash-panel sw-dash-panel--fleet" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10% 0px" }} transition={{ duration: 0.5, delay: 0.24 }}>
<span className="sw-dash-panel-h">Fleet Status</span>
<div className="sw-fleet-list">
{fleet.map((f) => (
<div className="sw-fleet-row" key={f.id}>
<span className={`sw-fleet-dot sw-fleet-dot--${f.tone}`} />
<span className="sw-fleet-id">{f.id}</span>
<span className="sw-fleet-status">{f.status}</span>
<span className="sw-fleet-eta">{f.eta}</span>
</div>
))}
</div>
</motion.div>
<motion.div className="sw-dash-panel sw-dash-panel--wide" initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10% 0px" }} transition={{ duration: 0.5, delay: 0.3 }}>
<span className="sw-dash-panel-h">Live Activity</span>
<LiveFeed />
</motion.div>
</div>
</div>
</Section>
);
}
/* ============================================================
SECTION 10 — Why Businesses Choose Doormile
Open grid of individually floating stat cards, wider canvas.
============================================================ */
function Sec10() {
const cards = [
{ icon: IconTarget, value: 99.9, decimals: 1, suffix: "%", label: "Dispatch Accuracy" },
{ icon: IconZap, value: 45, suffix: "ms", label: "Decision Time" },
{ icon: IconClock, text: "24/7", label: "Live Monitoring" },
{ icon: IconShieldCheck, value: 100, suffix: "%", label: "SLA First" },
{ icon: IconTrendDown, text: "Lower", label: "Operational Cost" },
{ icon: IconGlobe, text: "Enterprise", label: "Scale-Ready" },
];
return (
<Section id="why-doormile" bg="grey">
<SectionHead
n="10"
eyebrow="Enterprise Growth"
title={<>Why businesses choose <span className="accent">Doormile</span></>}
lede="An AI decision engine, SLA-first operations and enterprise-grade scale — built to move from pickup to doorstep without the guesswork."
/>
<div className="sw-kpi10-grid">
{cards.map((c) => (
<TiltCard className="sw-kpi10-card" key={c.label}>
<span className="sw-kpi10-shine" aria-hidden="true" />
<span className="sw-kpi10-ic">{c.icon}</span>
<b className="sw-kpi10-val">
{c.text ?? <CountUp end={c.value!} decimals={c.decimals} suffix={c.suffix} />}
</b>
<span className="sw-kpi10-label">{c.label}</span>
</TiltCard>
))}
</div>
</Section>
);
}
function SolutionsWorkflowShell({ children }: { children: React.ReactNode }) {
return (
<>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
<div id="sw-root">
<div className="sw-noise" aria-hidden="true" />
{children}
</div>
</>
);
}
/* ============================================================
Default export
============================================================ */
export default function SolutionsWorkflow() {
return (
<SolutionsWorkflowShell>
<Sec01 />
<Sec02 />
<Sec07 />
<Sec10 />
</SolutionsWorkflowShell>
);
}
function MigratedSolutionSection({ children }: { children: React.ReactNode }) {
return <SolutionsWorkflowShell>{children}</SolutionsWorkflowShell>;
}
export function SolutionAIVehicleSelection() {
return <MigratedSolutionSection><Sec03 /></MigratedSolutionSection>;
}
export function SolutionSmartRouteIntelligence() {
return <MigratedSolutionSection><Sec04 /></MigratedSolutionSection>;
}
export function SolutionQuantumDecisionEngine() {
return <MigratedSolutionSection><Sec05 /></MigratedSolutionSection>;
}
export function SolutionLiveLogisticsIntelligence() {
return <MigratedSolutionSection><Sec06 /></MigratedSolutionSection>;
}
export function SolutionMileTruthAI() {
return <MigratedSolutionSection><Sec08 sectionId="solution-miletruth-ai" /></MigratedSolutionSection>;
}
export function SolutionOperationsDashboard() {
return <MigratedSolutionSection><Sec09 /></MigratedSolutionSection>;
}
export function MigratedSolutionSections() {
return (
<SolutionsWorkflowShell>
<Sec03 />
<Sec04 />
<Sec05 />
<Sec06 />
<Sec08 sectionId="solution-miletruth-ai" />
<Sec09 />
</SolutionsWorkflowShell>
);
}
/* ============================================================
Styles
============================================================ */
const CSS = `
#sw-root {
--sw-red: #C01227;
--sw-red-bright: #E2354A;
--sw-line: rgba(255,255,255,0.09);
--sw-text-dim: rgba(255,255,255,0.68);
/* Shared inner-card padding — the same clamp() token .sw-panel already
uses for its own interior. Applied to .sw-in so every section's title,
heading, subtitle and other loose content sits clear of the card's
rounded edge instead of touching it, on the same single value site-wide
(see .sw-in and .sw-edge below). */
--sw-in-pad: clamp(16px, 3vw, 32px);
--fill: 0;
position: relative;
/* clip (not hidden) so it still contains horizontal overflow from the
section animations without creating a scroll container — the latter
would break the Sec06 sticky phone. */
overflow: clip;
/* No background color here: each section now paints its own colour
entirely within its inset, rounded .sw-bg card (see the section-rhythm
rule below), so the page's own white canvas shows through in the
gutters/gaps between cards — the same way it shows around the Hero
and Footer cards, instead of a dark fill masking the new spacing. */
}
#sw-root, #sw-root * { font-family: var(--font-manrope), "Manrope", sans-serif; box-sizing: border-box; }
#sw-root .sw-noise {
position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0.035; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
background-size: 180px 180px;
}
/* ---- Section rhythm + width (one shared card system, matching Hero/Footer) ----
Every section is its own floating card, using EXACTLY the outer-spacing
system Hero and Footer already use: a 20px (10px ≤1024px) left/right inset
+ a 24px (20px ≤767px) corner radius on the coloured surface (.sw-bg),
plus a single 40/32/24px (desktop/≤1024/≤767) gap between every stacked
card — Hero-to-Section-1, Section-to-Section, and Section-10-to-Footer
alike. One token pair drives all 10 sections; nothing here is
section-specific, so every section's outer spacing is identical. */
#sw-root .sw-sec { position: relative; padding: 96px 0; margin-top: 40px; overflow: clip; }
/* Left/right margin measured directly off the live Hero and Footer
containers (20px above the 1024px breakpoint, 10px at/below) — one
shared horizontal margin every section inherits through .sw-in, never
overridden per-section, so every section's edges land on the same
vertical grid as the Hero and Footer. */
#sw-root .sw-in { position: relative; z-index: 1; width: calc(100% - 40px); margin: 0 auto; padding: 0 var(--sw-in-pad); }
#sw-root .sw-bg { position: absolute; left: 20px; right: 20px; top: 0; bottom: 0; z-index: 0; border-radius: 24px; overflow: hidden; }
#sw-root #routing.sw-sec,
#sw-root #quantum.sw-sec { padding-top: clamp(24px, 3.5vw, 44px) !important; padding-bottom: clamp(24px, 3.5vw, 44px) !important; margin-top: 24px !important; }
#sw-root #quantum .sw-head,
#sw-root #routing .sw-head { margin-bottom: 24px !important; }
/* Poster-scale Header Layout for Quantum Engine (Matching "The Doormile Way") */
#sw-root .sw-qhead {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: clamp(24px, 3vw, 36px);
width: 100%;
}
#sw-root .sw-qhead-kicker {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
#sw-root .sw-qhead-eyebrow {
font-size: 13.5px;
font-weight: 600;
letter-spacing: 0.08em;
color: #6b7280;
margin-bottom: 14px;
text-transform: uppercase;
}
#sw-root .sw-qhead-divider {
width: 100%;
height: 1px;
border: none;
background-color: rgba(0, 0, 0, 0.12);
margin: 0 0 24px 0;
}
#sw-root .sw-qhead-stroke-title {
margin: 0 0 24px 0;
font-weight: 800;
letter-spacing: -0.01em !important;
line-height: 1.1;
color: #ffffff !important;
-webkit-text-stroke: 2px #dc2626;
font-size: clamp(34px, 4.8vw, 68px);
text-transform: none;
}
#sw-root .sw-qhead-split {
display: grid;
grid-template-columns: 1.15fr 1fr;
gap: clamp(28px, 4.5vw, 56px);
align-items: flex-start;
margin-top: 8px;
}
#sw-root .sw-qhead-h2 {
font-size: clamp(24px, 2.8vw, 36px);
font-weight: 800;
line-height: 1.35;
letter-spacing: -0.015em;
color: #111827;
margin: 0;
text-transform: none !important;
}
#sw-root .sw-qhead-desc {
display: flex;
flex-direction: column;
gap: 16px;
}
#sw-root .sw-qhead-desc p {
font-size: 15px;
line-height: 1.7;
color: #4b5563;
margin: 0;
font-weight: 450;
}
@media (max-width: 900px) {
#sw-root .sw-qhead-split {
grid-template-columns: 1fr;
gap: 16px;
}
}
#sw-root .sw-bg--grey { background: #121212; }
#sw-root .sw-bg--charcoal { background: #171717; }
#sw-root .sw-bg--charcoal::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(90% 60% at 50% 0%, rgba(255,255,255,0.035) 0%, transparent 60%);
}
/* The grid-line pattern fades out in a vignette (via mask-image on its own
::before layer, not on .sw-bg itself) so only the pattern softens toward
the edges — the card's own #141414 fill stays fully opaque right up to
its rounded corners, same as every other section's card. Masking .sw-bg
directly would fade the fill too, showing the page's white canvas through
the middle of the card instead of a clean rounded edge. */
#sw-root .sw-bg--graph { background: #141414; }
#sw-root .sw-bg--graph::before {
content: ''; position: absolute; inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px);
background-size: 54px 54px;
-webkit-mask-image: radial-gradient(85% 75% at 50% 35%, #000 45%, transparent 100%);
mask-image: radial-gradient(85% 75% at 50% 35%, #000 45%, transparent 100%);
}
#sw-root .sw-bg--mesh { background: #141414; }
#sw-root .sw-bg--mesh::before, #sw-root .sw-bg--mesh::after { content: ''; position: absolute; border-radius: 50%; }
#sw-root .sw-bg--mesh::before { width: 62vw; height: 62vw; left: -16vw; top: -20vw; background: radial-gradient(circle, rgba(192,18,39,0.26) 0%, transparent 70%); }
#sw-root .sw-bg--mesh::after { width: 48vw; height: 48vw; right: -12vw; bottom: -18vw; background: radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%); }
#sw-root .sw-bg--redglow { background: #121212; }
#sw-root .sw-bg--redglow::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(65% 60% at 50% 12%, rgba(226,53,74,0.36) 0%, transparent 70%),
radial-gradient(45% 45% at 85% 92%, rgba(192,18,39,0.2) 0%, transparent 70%);
}
/* Smooth, non-repeating "engine" ground — a soft vignette + a single
ambient glow centered behind the core. No grid, no tiling, no pattern. */
#sw-root .sw-bg--engine {
background:
radial-gradient(46% 40% at 50% 40%, rgba(226,53,74,0.16) 0%, transparent 72%),
radial-gradient(120% 90% at 50% 0%, #171414 0%, #101010 55%, #0c0c0c 100%);
}
/* A faint diagonal texture — just enough "logistics grid" ground under
Sec04's AI Decision Engine panel without competing with the glass cards
and animated metrics that sit on top of it. */
#sw-root .sw-bg--logisticsgrid {
background: #141414;
background-image:
repeating-linear-gradient(45deg, rgba(255,255,255,0.02) 0 2px, transparent 2px 26px),
repeating-linear-gradient(-45deg, rgba(192,18,39,0.028) 0 2px, transparent 2px 26px);
}
/* ---- Head ---- */
#sw-root .sw-head { margin-bottom: clamp(32px, 4vw, 56px); max-width: 820px; }
#sw-root .sw-head--center { max-width: 820px; margin-left: auto; margin-right: auto; text-align: center; }
#sw-root .sw-head--split { max-width: none; margin-bottom: clamp(30px, 3.4vw, 42px); }
#sw-root .sw-eyebrow {
display: inline-flex; align-items: center; gap: 10px;
color: var(--sw-red-bright) !important; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.16em; font-size: 12.5px; margin-bottom: 20px;
}
#sw-root .sw-head--center .sw-eyebrow { justify-content: center; }
#sw-root .sw-eyebrow b {
color: #fff !important; background: var(--sw-red); border-radius: 6px;
padding: 2px 7px; font-size: 11.5px; letter-spacing: 0.04em;
}
#sw-root .sw-eyebrow--slash {
color: #6b7280 !important;
font-weight: 500 !important;
font-size: 13.5px !important;
letter-spacing: 0.05em !important;
background: none !important;
padding: 0 !important;
}
#sw-root .sw-h {
color: #fff !important; font-weight: 800 !important;
font-size: clamp(30px, 4vw, 52px) !important; line-height: 1.14 !important;
letter-spacing: -0.02em; margin: 0 0 18px !important;
}
#sw-root .sw-h .accent { color: var(--sw-red-bright) !important; }
#sw-root .sw-tm { font-size: 0.5em; vertical-align: super; }
#sw-root .sw-lede { color: var(--sw-text-dim) !important; font-size: clamp(15.5px, 1.15vw, 19px) !important; line-height: 1.65 !important; margin: 0 !important; font-weight: 400 !important; max-width: 620px; }
#sw-root .sw-head--center .sw-lede,
#quantum .sw-head--center .sw-lede,
.sw-head--center .sw-lede {
margin-left: auto !important;
margin-right: auto !important;
text-align: center !important;
}
/* ---- White section overrides ---- */
#sw-root .sw-sec--white .sw-eyebrow,
#sw-root #quantum .sw-eyebrow,
#quantum .sw-eyebrow { color: var(--sw-red-bright) !important; }
#sw-root .sw-sec--white .sw-eyebrow b,
#sw-root #quantum .sw-eyebrow b,
#quantum .sw-eyebrow b { background: var(--sw-red); color: #fff !important; }
#sw-root .sw-sec--white h2,
#sw-root .sw-sec--white .sw-h,
#sw-root #quantum h2,
#sw-root #quantum .sw-h,
#quantum h2,
#quantum .sw-h,
div#sw-root section#quantum h2.sw-h,
.logico-front-end #quantum h2,
.elementor-59 #quantum h2 { color: #111827 !important; }
#sw-root .sw-sec--white h2 .accent,
#sw-root .sw-sec--white .sw-h .accent,
#sw-root #quantum h2 .accent,
#sw-root #quantum .sw-h .accent,
#quantum h2 .accent,
#quantum .sw-h .accent,
div#sw-root section#quantum h2.sw-h .accent,
.logico-front-end #quantum h2 .accent,
.elementor-59 #quantum h2 .accent { color: var(--sw-red-bright) !important; }
#sw-root .sw-sec--white p,
#sw-root .sw-sec--white .sw-lede,
#sw-root #quantum p,
#sw-root #quantum .sw-lede,
#quantum p,
#quantum .sw-lede,
div#sw-root section#quantum p.sw-lede,
.logico-front-end #quantum p,
.elementor-59 #quantum p { color: #4b5563 !important; }
/* ---- Split layout ---- */
#sw-root .sw-split { display: grid; grid-template-columns: minmax(300px, 0.86fr) minmax(0, 1.4fr); gap: clamp(36px, 5vw, 72px); align-items: center; }
#sw-root .sw-split--reverse { grid-template-columns: minmax(0, 1.3fr) minmax(300px, 0.9fr); }
#sw-root #miletruth .sw-split { grid-template-columns: minmax(320px, 1fr) minmax(0, 1.25fr); }
/* ---- Floating panel ---- */
#sw-root .sw-panel {
position: relative; width: 100%; border-radius: 24px;
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.012));
border: 1px solid var(--sw-line);
box-shadow: 0 50px 100px -50px rgba(0,0,0,0.7);
padding: clamp(16px, 3vw, 32px);
}
/* ---- Edge-to-edge (Hero-language) frame ----
These banners/media are deliberately meant to run flush to the section
card's own rounded edge (matching how Hero's banner image touches its own
card edge) — so they cancel out .sw-in's new inner padding with an equal
negative margin + width expansion, on the same --sw-in-pad token, instead
of adopting a padded, narrower frame like the rest of a section's content. */
#sw-root .sw-edge { position: relative; width: calc(100% + var(--sw-in-pad) * 2); margin: 0 calc(var(--sw-in-pad) * -1); border-radius: 24px; overflow: hidden; }
/* ---- Shared chip / arrow / livedot primitives ---- */
#sw-root .sw-veh-chip {
display: inline-flex; align-items: center; padding: 8px 16px; border-radius: 999px;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.11);
color: rgba(255,255,255,0.8); font-size: 13px; font-weight: 600; letter-spacing: 0.02em;
}
#sw-root .sw-veh-arrow { color: rgba(255,255,255,0.28); font-size: 20px; flex-shrink: 0; align-self: center; }
#sw-root .sw-livedot {
width: 8px; height: 8px; border-radius: 50%; background: var(--sw-red-bright);
box-shadow: 0 0 0 0 rgba(226,53,74,0.55); animation: swPulse 1.8s ease-out infinite; flex-shrink: 0; display: inline-block;
}
@keyframes swPulse { 0% { box-shadow: 0 0 0 0 rgba(226,53,74,0.55); } 70% { box-shadow: 0 0 0 9px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
@keyframes swSpin { to { transform: rotate(360deg); } }
/* ================= SECTION 01 — AI Logistics Engine ================= */
/* A robust flex-column stack (no fixed min-height, no absolute-% guessing) so
the diagram's height always matches its content — no dead space. */
#sw-root .sw-eng { display: flex; flex-direction: column; align-items: center; }
#sw-root .sw-eng-order {
display: flex; flex-direction: column; align-items: center; gap: 6px;
padding: 14px 24px; border-radius: 16px;
background: rgba(255,255,255,0.045); border: 1px solid rgba(255,255,255,0.14);
}
#sw-root .sw-eng-order-ic { width: 34px; height: 34px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: rgba(226,53,74,0.14); border: 1px solid rgba(226,53,74,0.35); color: var(--sw-red-bright); }
#sw-root .sw-eng-order-ic svg { width: 17px; height: 17px; }
#sw-root .sw-eng-order-label { color: #fff; font-weight: 700; font-size: 13.5px; }
#sw-root .sw-eng-order-note { color: rgba(255,255,255,0.5); font-size: 11px; }
/* Vertical connector: a short filled rail (no forced min-height container to fight). */
#sw-root .sw-eng-connector { position: relative; display: block; width: 2px; height: clamp(34px, 3.6vw, 52px); background: rgba(255,255,255,0.14); border-radius: 2px; margin: 6px 0; }
#sw-root .sw-eng-connector-fill { position: absolute; inset: 0; height: calc(var(--fill, 0) * 100%); background: linear-gradient(180deg, var(--sw-red), var(--sw-red-bright)); box-shadow: 0 0 10px rgba(226,53,74,0.7); border-radius: 2px; }
/* Engine row: core (processor panel, not a plain circle) + live decision readout. */
#sw-root .sw-eng-engine-row { display: flex; align-items: center; gap: clamp(24px, 3.2vw, 44px); margin: clamp(18px, 2.2vw, 26px) 0; }
#sw-root .sw-eng-core {
position: relative; flex-shrink: 0;
width: clamp(150px, 14vw, 188px); height: clamp(150px, 14vw, 188px); border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(120% 120% at 50% 22%, rgba(192,18,39,0.22), rgba(12,12,12,0.97) 72%);
border: 1px solid rgba(226,53,74,0.35);
box-shadow: 0 0 40px -14px rgba(192,18,39,0.45), inset 0 0 26px rgba(0,0,0,0.5);
transition: box-shadow 0.6s ease, border-color 0.6s ease;
}
/* A gentle brighten while the AI is "deciding" — a calm cue, not a strobe. */
#sw-root .sw-eng-engine-row.is-active .sw-eng-core { border-color: rgba(226,53,74,0.6); box-shadow: 0 0 56px -10px rgba(192,18,39,0.6), inset 0 0 26px rgba(0,0,0,0.5); }
/* A radar-style scan sweep inside the core — always turning slowly at
rest, spinning up sharply while "is-active" — so the core itself visibly
computes instead of just glowing brighter. Clipped to the core's own
rounded shape via overflow:hidden on this wrapper. */
#sw-root .sw-eng-core-scan { position: absolute; inset: 2px; border-radius: 25px; overflow: hidden; z-index: 1; opacity: 0.55; }
#sw-root .sw-eng-core-scan::before {
content: ''; position: absolute; inset: -60%;
background: conic-gradient(from 0deg, transparent 0deg, rgba(226,53,74,0.65) 28deg, transparent 68deg, transparent 360deg);
animation: swCoreScan 5s linear infinite;
}
#sw-root .sw-eng-engine-row.is-active .sw-eng-core-scan { opacity: 0.95; }
#sw-root .sw-eng-engine-row.is-active .sw-eng-core-scan::before { animation-duration: 0.9s; }
@keyframes swCoreScan { to { transform: rotate(360deg); } }
#sw-root .sw-eng-core-bracket { position: absolute; width: 22px; height: 22px; border-color: rgba(226,53,74,0.55); border-style: solid; border-width: 0; z-index: 2; }
#sw-root .sw-eng-core-bracket--tl { top: 10px; left: 10px; border-top-width: 2px; border-left-width: 2px; border-top-left-radius: 6px; }
#sw-root .sw-eng-core-bracket--tr { top: 10px; right: 10px; border-top-width: 2px; border-right-width: 2px; border-top-right-radius: 6px; }
#sw-root .sw-eng-core-bracket--bl { bottom: 10px; left: 10px; border-bottom-width: 2px; border-left-width: 2px; border-bottom-left-radius: 6px; }
#sw-root .sw-eng-core-bracket--br { bottom: 10px; right: 10px; border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: 6px; }
#sw-root .sw-eng-orbit { position: absolute; inset: 0; animation: swSpin 12s linear infinite; }
#sw-root .sw-eng-engine-row.is-active .sw-eng-orbit { animation-duration: 2.6s; }
#sw-root .sw-eng-orbit-dot {
position: absolute; top: 50%; left: 50%; width: 5px; height: 5px; margin: -2.5px;
border-radius: 50%; background: var(--sw-red-bright); box-shadow: 0 0 8px rgba(226,53,74,0.8); opacity: 0.7;
transform: rotate(calc(var(--i) * 45deg)) translateX(clamp(84px, 8vw, 104px));
}
#sw-root .sw-eng-core-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; gap: 9px; }
#sw-root .sw-eng-core-logo { width: clamp(52px, 4.6vw, 66px); height: auto; filter: drop-shadow(0 0 10px rgba(226,53,74,0.5)); }
#sw-root .sw-eng-core-wave { display: flex; align-items: flex-end; gap: 3px; height: 16px; }
#sw-root .sw-eng-core-wave span { width: 3px; border-radius: 2px; background: var(--sw-red-bright); animation: swWave 1.3s ease-in-out infinite; }
#sw-root .sw-eng-core-wave span:nth-child(1) { height: 40%; animation-delay: 0s; }
#sw-root .sw-eng-core-wave span:nth-child(2) { height: 80%; animation-delay: 0.12s; }
#sw-root .sw-eng-core-wave span:nth-child(3) { height: 100%; animation-delay: 0.24s; }
#sw-root .sw-eng-core-wave span:nth-child(4) { height: 65%; animation-delay: 0.36s; }
#sw-root .sw-eng-core-wave span:nth-child(5) { height: 45%; animation-delay: 0.48s; }
#sw-root .sw-eng-engine-row.is-active .sw-eng-core-wave span { animation-duration: 0.5s; }
@keyframes swWave { 0%, 100% { transform: scaleY(0.4); opacity: 0.6; } 50% { transform: scaleY(1); opacity: 1; } }
#sw-root .sw-eng-core-tag { color: rgba(255,255,255,0.5); font-size: 9.5px; font-weight: 800; letter-spacing: 0.14em; }
#sw-root .sw-eng-readout { display: flex; flex-direction: column; gap: 11px; padding: 18px 22px; border-radius: 16px; background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.1); min-width: clamp(200px, 20vw, 240px); }
#sw-root .sw-eng-readout-title { color: rgba(255,255,255,0.45); font-size: 10.5px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 2px; }
#sw-root .sw-eng-readout-row { display: flex; align-items: center; gap: 11px; opacity: 0.4; transition: opacity 0.3s ease; }
#sw-root .sw-eng-readout-indicator { position: relative; width: 18px; height: 18px; flex-shrink: 0; }
#sw-root .sw-eng-readout-dot { position: absolute; top: 5px; left: 5px; width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.3); }
#sw-root .sw-eng-readout-dot::after { content: ''; position: absolute; inset: -5px; border-radius: 50%; border: 1px solid rgba(226,53,74,0.5); animation: swReadoutPulse 1.3s ease-out infinite; }
@keyframes swReadoutPulse { 0% { transform: scale(0.6); opacity: 0.8; } 100% { transform: scale(1.7); opacity: 0; } }
#sw-root .sw-eng-readout-check { position: absolute; inset: 0; border-radius: 50%; background: var(--sw-red); color: #fff; display: flex; align-items: center; justify-content: center; opacity: 0; }
#sw-root .sw-eng-readout-check svg { width: 10px; height: 10px; stroke-width: 3; }
#sw-root .sw-eng-readout-label { color: #fff; font-size: 13px; font-weight: 600; }
/* Fan-out: a small, self-sized SVG (not a full-height overlay) linking the
engine to its three simultaneous outputs. */
#sw-root .sw-eng-fan { display: block; width: min(360px, 78%); height: clamp(44px, 4.6vw, 60px); }
#sw-root .sw-eng-fan path { fill: none; stroke: rgba(226,53,74,0.45); stroke-width: 2.2; vector-effect: non-scaling-stroke; }
#sw-root .sw-eng-outputs { display: flex; gap: clamp(14px, 2vw, 26px); flex-wrap: wrap; justify-content: center; }
#sw-root .sw-eng-out {
display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center;
width: 148px; padding: 16px 14px; border-radius: 16px;
background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.1);
}
#sw-root .sw-eng-out-ic { width: 42px; height: 42px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: rgba(226,53,74,0.12); border: 1px solid rgba(226,53,74,0.3); color: var(--sw-red-bright); }
#sw-root .sw-eng-out-ic svg { width: 20px; height: 20px; }
#sw-root .sw-eng-out-label { color: #fff; font-weight: 700; font-size: 13px; }
#sw-root .sw-eng-out-note { color: rgba(255,255,255,0.55); font-size: 11px; line-height: 1.35; }
#sw-root .sw-eng-tracking { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; padding: 16px 26px; border-radius: 16px; background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.1); }
#sw-root .sw-eng-delivered {
display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; border-radius: 999px;
background: rgba(10,10,10,0.85); border: 1px solid rgba(226,53,74,0.5); color: #fff; font-weight: 700; font-size: 13px;
}
#sw-root .sw-eng-delivered-ic { color: var(--sw-red-bright); display: flex; }
#sw-root .sw-eng-delivered-ic svg { width: 16px; height: 16px; }
#sw-root .sw-eng-delivered em { font-style: normal; color: rgba(255,255,255,0.55); font-weight: 500; }
@media (max-width: 767px) {
#sw-root .sw-eng-engine-row { flex-direction: column; gap: 20px; }
#sw-root .sw-eng-fan { display: none; }
}
/* ================= SECTION 02 — Pickup (van-driven) ================= */
/* Flush with the shared .sw-edge (width: 100%) — the same outer frame
every other full-width section's primary panel uses (01, 04, 05, 07,
09, 10). This used to carry its own margin + compensating width
reduction to inset the whole layered scene, which broke alignment with
every other section's outer edge. Section 05's canvas gets breathing
room at its own inner layer (.wf-scene), not on its outer frame
(.qs-frame) — that's a per-scene styling choice, not part of the
shared container, so it isn't reproduced here. */
#sw-root .sw-pickup { position: relative; min-height: clamp(460px, 44vw, 600px); overflow: hidden; border-radius: 16px; }
/* The source photo is square and the section is always wider than tall, so
object-fit: cover never crops horizontally here — only object-position's Y
matters. Biased down so the frame keeps the storefront (origin), the
parked van's open door (pickup point) AND the open pavement the animated
van actually drives across, instead of a crop that shows only the cargo
shelves. */
#sw-root .sw-pickup-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 80%; transform-origin: 45% 78%; will-change: transform; }
#sw-root .sw-pickup-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.32) 0%, rgba(0,0,0,0.42) 40%, rgba(0,0,0,0.9) 100%); }
#sw-root .sw-pickup-ai-badge {
position: absolute; top: 20px; left: 20px; z-index: 4; display: inline-flex; align-items: center; gap: 9px;
padding: 8px 14px; border-radius: 999px; background: rgba(10,10,10,0.72); border: 1px solid rgba(255,255,255,0.16);
color: #fff; font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; backdrop-filter: blur(8px);
}
#sw-root .sw-pickup-ai-indicator { position: relative; width: 8px; height: 8px; }
#sw-root .sw-pickup-ai-check { position: absolute; top: -5px; left: -5px; width: 18px; height: 18px; border-radius: 50%; background: var(--sw-red); color: #fff; display: flex; align-items: center; justify-content: center; opacity: 0; }
#sw-root .sw-pickup-ai-check svg { width: 10px; height: 10px; stroke-width: 3; }
#sw-root .sw-pickup-confirm { position: absolute; top: 18px; right: 20px; z-index: 4; display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 999px; background: rgba(10,10,10,0.88); border: 1px solid rgba(226,53,74,0.5); color: #fff; font-weight: 700; font-size: 13px; white-space: nowrap; }
#sw-root .sw-pickup-confirm svg { width: 16px; height: 16px; color: var(--sw-red-bright); flex-shrink: 0; }
/* A real road, not an abstract line: a dark asphalt band (with contact
shadow) carries a static white dashed lane marking, and the bright red
GPS-style route draws on top of it as the van advances — same shape
the reviewer asked for (subtle road texture + nav polyline). All strokes
use non-scaling-stroke so their screen-pixel width stays constant despite
the viewBox being stretched non-uniformly (preserveAspectRatio="none") to
the section's wide, short aspect ratio. */
#sw-root .sw-pickup-road { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; overflow: visible; }
#sw-root .sw-pickup-road-shadow { fill: none; stroke: rgba(0,0,0,0.5); stroke-width: 15; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; filter: blur(2px); }
#sw-root .sw-pickup-road-path { fill: none; stroke: rgba(12,12,14,0.62); stroke-width: 11; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
#sw-root .sw-pickup-road-lane { fill: none; stroke: rgba(255,255,255,0.5); stroke-width: 1.6; stroke-linecap: round; stroke-dasharray: 5 8; vector-effect: non-scaling-stroke; }
/* Nav-style route: the full path stays neutral grey (untravelled), and the
red progress line is drawn exactly on top of it up to the van's current
position only — so the road ahead of the van is always visibly "not
done yet" instead of the whole route reading as complete at once. */
#sw-root .sw-pickup-road-remaining { fill: none; stroke: rgba(255,255,255,0.4); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
/* No vector-effect here, unlike the other road layers: non-scaling-stroke
makes Chromium interpret stroke-dasharray/-dashoffset in screen-space
rather than the userspace units getTotalLength() returns, so the "fully
drawn" dash only ever covered a fraction of the path (invisible against
the dark asphalt before the grey remaining-route layer existed to expose
it). Plain scaling stroke keeps dasharray math and rendering in the same
coordinate space, so the small width below is in viewBox units, not px. */
#sw-root .sw-pickup-road-progress { fill: none; stroke: var(--sw-red-bright); stroke-width: 0.7; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 3px rgba(226,53,74,0.9)); }
#sw-root .sw-pickup-dest { position: absolute; z-index: 2; transform: translate(-50%, -100%); display: flex; flex-direction: column; align-items: center; gap: 2px; color: #fff; text-shadow: 0 2px 6px rgba(0,0,0,0.8); }
#sw-root .sw-pickup-dest svg { width: 26px; height: 26px; color: var(--sw-red-bright); filter: drop-shadow(0 0 8px rgba(226,53,74,0.7)); }
#sw-root .sw-pickup-dest span { font-size: 10.5px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; background: rgba(10,10,10,0.7); padding: 2px 8px; border-radius: 999px; }
/* The anchor point (left/top, set per-frame in JS) must land where the
wheels TOUCH the road — their bottom tangent, not the wheel's own center
— or the road line cuts through the middle of the wheels instead of
sitting under them. Wheel hubs are at y=50 with r=12 in a 66-tall
viewBox, so the contact line is y=62, i.e. 93.9% down — the transform
below (relative to the element's own box) does that lift and the
horizontal centering, instead of a negative-px margin, since the box is
now sized in %, not px. Sized to ~7% of the section width — proportional
to the real van visible in the background photo, not an oversized focal
prop. */
#sw-root .sw-pickup-van { position: absolute; top: 0; left: 0; width: 7%; aspect-ratio: 130 / 66; transform: translate(-50%, -93.9%); z-index: 6; }
/* A dedicated contact shadow, separate from the van body's rotate/bounce
chain, so it stays flat on the road while the body leans and bobs above
it — JS modulates opacity/scaleX with speed for a "following" feel.
Positioned right at the wheel-contact line (93.9%, see above). */
#sw-root .sw-pickup-van-shadow {
position: absolute; left: 4%; right: 4%; top: 90%; height: 12%;
background: radial-gradient(ellipse, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.28) 55%, transparent 78%);
filter: blur(2.5px); opacity: 0.85; transform-origin: center; z-index: 0;
}
/* A trailing streak, not a blur on the sprite itself — sense of speed
without ever softening the van's own silhouette. Anchored to the
trailing (left) edge since the route runs left-to-right and the sprite
noses right to match (see the flip logic in Sec02); fades in only while
actually moving. */
#sw-root .sw-pickup-van-speedlines { position: absolute; top: 34%; left: -10%; width: 26%; height: 30%; opacity: 0; transition: opacity 0.25s ease; pointer-events: none; z-index: 0; }
#sw-root .sw-pickup-van.is-moving .sw-pickup-van-speedlines { opacity: 1; }
#sw-root .sw-pickup-van-speedlines i { position: absolute; right: 0; height: 2.5px; border-radius: 2px; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.65)); font-style: normal; }
#sw-root .sw-pickup-van-speedlines i:nth-child(1) { top: 0; width: 100%; }
#sw-root .sw-pickup-van-speedlines i:nth-child(2) { top: 45%; width: 72%; opacity: 0.75; }
#sw-root .sw-pickup-van-speedlines i:nth-child(3) { top: 88%; width: 48%; opacity: 0.55; }
/* Suspension bounce: a small continuous vertical oscillation, only while
the van is actually moving (toggled in JS from real frame-to-frame
speed) — stops cleanly at rest instead of looping forever. */
#sw-root .sw-pickup-van-bounce { position: relative; z-index: 1; width: 100%; height: 100%; }
#sw-root .sw-pickup-van-bounce.is-moving { animation: swVanBounce 0.38s ease-in-out infinite; }
@keyframes swVanBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.4px); } }
/* Idle "alive" state — plain CSS, not JS/scroll-driven, so it plays the
instant the van's markup paints (even before hydration) and during every
checkpoint dwell, not just the very first wait at Pickup Request: a
slow breathing bob plus a headlight blink give the van presence before
it starts moving, instead of it looking like a static, inert sticker
parked on the road. */
#sw-root .sw-pickup-van:not(.is-moving) .sw-pickup-van-bounce { animation: swVanIdleBob 3.2s ease-in-out infinite; }
@keyframes swVanIdleBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-0.6px); } }
#sw-root .sw-pickup-van:not(.is-moving) .sw-van-headlight { animation: swVanHeadlightBlink 2.6s ease-in-out infinite; }
@keyframes swVanHeadlightBlink { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
/* Heading changes (set imperatively in JS) ease through this transition
instead of snapping — the overshoot in the curve reads as the van
banking into the turn before it settles straight. */
#sw-root .sw-pickup-van-rot { width: 100%; height: 100%; transition: transform 0.45s cubic-bezier(0.32, 1.45, 0.6, 1); }
/* Rim-light + contact shadow via filter (not per-shape outlines) so the
whole silhouette gets a consistent bright edge that separates it from
both the dark asphalt and the busy photo behind it. */
#sw-root .sw-van-svg { width: 100%; height: 100%; overflow: visible; filter: drop-shadow(0 0 1.5px rgba(255,255,255,0.95)) drop-shadow(0 0 4px rgba(255,255,255,0.35)) drop-shadow(0 4px 8px rgba(0,0,0,0.75)); }
/* A brief full-frame flash at "Package Collected" — punctuates the finish
without adding new persistent UI. */
#sw-root .sw-pickup-success-glow { position: absolute; inset: 0; z-index: 5; pointer-events: none; opacity: 0; background: radial-gradient(60% 60% at 18% 84%, rgba(226,53,74,0.4) 0%, transparent 70%); }
/* The card's own top:0 is pinned 24px above its waypoint (half the 48px icon
height) so the checkpoint icon's CENTER lands exactly on the path — not the
card's bottom edge, which would leave the icon floating well above it.
z-index sits above .sw-pickup-van (6): the van's body reaches upward from
its wheel-contact point, the same point each checkpoint icon is centered
on, so the two necessarily share screen space right as the van arrives —
the checkpoint card stays on top through that moment instead of getting
covered. */
#sw-root .sw-pickup-stop {
position: absolute; transform: translate(-50%, -24px); z-index: 7; width: 148px; padding-top: 58px;
display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center;
opacity: 0.34; transition: opacity 0.45s ease;
}
#sw-root .sw-pickup-stop.is-active { opacity: 1; }
#sw-root .sw-pickup-stop-icwrap { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 48px; height: 48px; }
#sw-root .sw-pickup-stop-ic {
position: relative; z-index: 1; width: 48px; height: 48px; border-radius: 14px; display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.09); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.18);
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
#sw-root .sw-pickup-stop-ic svg { width: 24px; height: 24px; }
#sw-root .sw-pickup-stop.is-active .sw-pickup-stop-ic { border-color: rgba(226,53,74,0.6); box-shadow: 0 0 24px -6px rgba(192,18,39,0.85); animation: swCheckpointPop 0.5s ease-out; }
#sw-root .sw-pickup-stop-ripple { position: absolute; top: 0; left: 0; width: 48px; height: 48px; border-radius: 14px; border: 2px solid rgba(226,53,74,0.6); opacity: 0; pointer-events: none; }
#sw-root .sw-pickup-stop.is-active .sw-pickup-stop-ripple { animation: swCheckpointRipple 0.7s ease-out; }
#sw-root .sw-pickup-stop-badge {
position: absolute; top: -5px; right: -5px; z-index: 2; width: 18px; height: 18px; border-radius: 50%;
background: var(--sw-red); color: #fff; display: flex; align-items: center; justify-content: center;
opacity: 0; transform: scale(0.4); transition: opacity 0.25s ease, transform 0.25s ease;
}
#sw-root .sw-pickup-stop-badge svg { width: 10px; height: 10px; stroke-width: 3; }
#sw-root .sw-pickup-stop.is-active .sw-pickup-stop-badge { opacity: 1; transform: scale(1); }
@keyframes swCheckpointPop { 0% { transform: scale(1); } 45% { transform: scale(1.26); } 100% { transform: scale(1); } }
@keyframes swCheckpointRipple { 0% { transform: scale(0.7); opacity: 0.9; } 100% { transform: scale(1.7); opacity: 0; } }
#sw-root .sw-pickup-stop-label { color: #fff; font-weight: 700; font-size: 13px; }
#sw-root .sw-pickup-stop-note { color: rgba(255,255,255,0.55); font-size: 11.5px; line-height: 1.4; }
@media (max-width: 700px) {
#sw-root .sw-pickup-confirm, #sw-root .sw-pickup-road, #sw-root .sw-pickup-van, #sw-root .sw-pickup-dest { display: none; }
#sw-root .sw-pickup { min-height: 0; display: flex; flex-wrap: wrap; justify-content: center; align-content: flex-start; gap: 16px; padding: 56px 16px 24px; width: 100%; margin: 0; }
#sw-root .sw-pickup-stop { position: relative; top: auto !important; left: auto !important; transform: none !important; padding-top: 0; width: 44%; opacity: 1 !important; }
#sw-root .sw-pickup-stop-icwrap { position: relative; margin: 0 auto; }
}
@media (prefers-reduced-motion: reduce) {
#sw-root .sw-pickup-stop.is-active .sw-pickup-stop-ic, #sw-root .sw-pickup-stop.is-active .sw-pickup-stop-ripple { animation: none; }
}
/* ================= SECTION 03 — Vehicle AI (split) ================= */
#sw-root #vehicle-ai { padding-top: clamp(24px, 3vw, 40px) !important; padding-bottom: clamp(24px, 3vw, 40px) !important; }
#sw-root #vehicle-ai .sw-split { align-items: stretch; gap: clamp(24px, 3.5vw, 48px); }
#sw-root .sw-veh-stage { position: relative; border-radius: 20px; overflow: hidden; border: 1px solid var(--sw-line); background: #0b0f17; padding: clamp(20px, 2.5vw, 32px) clamp(14px, 2vw, 24px); display: flex; flex-direction: column; justify-content: center; min-height: 100%; }
#sw-root .sw-veh-bg { position: absolute; inset: 0; z-index: 0; opacity: 0.14; pointer-events: none; }
#sw-root .sw-veh-bg img { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.85) contrast(1.1); }
#sw-root .sw-veh-scan { position: absolute; top: 0; bottom: 0; width: 60px; z-index: 2; background: linear-gradient(90deg, transparent, rgba(226,53,74,0.35), transparent); pointer-events: none; }
/* Decision timeline — Analysing -> Comparing -> Filtering -> Selecting ->
Dispatch Ready — advanced imperatively (see Sec03's evaluate()) so the
overall stage of the AI's reasoning is always legible above the
moment-to-moment status line and card choreography below it. */
#sw-root .sw-veh-timeline { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
#sw-root .sw-veh-tl-step { display: flex; align-items: center; gap: 6px; opacity: 0.38; transition: opacity 0.3s ease; }
#sw-root .sw-veh-tl-step:not(:last-child)::after { content: ''; width: 14px; height: 1px; background: rgba(255,255,255,0.18); margin-left: 6px; }
#sw-root .sw-veh-tl-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.3); transition: background-color 0.3s ease, box-shadow 0.3s ease; }
#sw-root .sw-veh-tl-label { font-size: 10px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: rgba(255,255,255,0.55); }
#sw-root .sw-veh-tl-step.is-done { opacity: 0.75; }
#sw-root .sw-veh-tl-step.is-done .sw-veh-tl-dot { background: rgba(226,53,74,0.65); }
#sw-root .sw-veh-tl-step.is-active { opacity: 1; }
#sw-root .sw-veh-tl-step.is-active .sw-veh-tl-dot { background: var(--sw-red-bright); box-shadow: 0 0 8px rgba(226,53,74,0.85); }
#sw-root .sw-veh-tl-step.is-active .sw-veh-tl-label { color: #fff; }
#sw-root .sw-veh-status { position: relative; z-index: 1; display: block; text-align: center; color: var(--sw-red-bright); font-weight: 700; font-size: 13.5px; letter-spacing: 0.02em; margin-bottom: clamp(22px, 2.6vw, 32px); min-height: 18px; }
#sw-root .sw-veh-row { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; }
#sw-root .sw-veh-card {
position: relative; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 16px 18px; border-radius: 18px;
background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.09); min-width: 106px; cursor: default;
transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease, filter 0.3s ease;
}
/* Rejection: a dark scrim + red X + reason ("Too Small", "Capacity"...)
pop in over the card and the whole card desaturates — each non-winner
gets this individually, staggered in evaluate(), so every rejection
reads as its own deliberate beat instead of five cards dimming at once. */
#sw-root .sw-veh-card-reject {
position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 5px; border-radius: 18px; background: rgba(8,8,9,0.62); opacity: 0; pointer-events: none;
}
#sw-root .sw-veh-card.is-rejected { filter: grayscale(0.9); }
#sw-root .sw-veh-card.is-rejected .sw-veh-card-reject { opacity: 1; animation: swVehRejectPop 0.35s ease-out; }
#sw-root .sw-veh-card-x { width: 20px; height: 20px; border-radius: 50%; background: rgba(226,53,74,0.2); border: 1px solid rgba(226,53,74,0.55); color: #ff5a6e; display: flex; align-items: center; justify-content: center; }
#sw-root .sw-veh-card-x svg { width: 10px; height: 10px; stroke-width: 3.4; }
#sw-root .sw-veh-card-reason { font-size: 9.5px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: #ff8a96; text-align: center; }
@keyframes swVehRejectPop { 0% { opacity: 0; transform: scale(0.72); } 65% { opacity: 1; transform: scale(1.08); } 100% { opacity: 1; transform: scale(1); } }
/* Winner: glow -> expand -> settle pulse (handled as GSAP scale tweens on
the card itself) -> lock, with this border/background/ring as the
persistent "selected" state once the pulse settles. */
#sw-root .sw-veh-card.is-winner {
border-color: rgba(226,53,74,0.55); background: rgba(192,18,39,0.12);
box-shadow: 0 0 0 1px rgba(226,53,74,0.25), 0 18px 40px -18px rgba(192,18,39,0.6);
animation: swVehWinnerRing 1.4s ease-out 1;
}
@keyframes swVehWinnerRing { 0% { box-shadow: 0 0 0 0 rgba(226,53,74,0.5), 0 18px 40px -18px rgba(192,18,39,0.6); } 100% { box-shadow: 0 0 0 16px rgba(226,53,74,0), 0 18px 40px -18px rgba(192,18,39,0.6); } }
#sw-root .sw-veh-art { width: 74px; height: 44px; display: block; }
#sw-root .sw-veh-label { font-size: 13px; font-weight: 700; color: #fff; }
#sw-root .sw-veh-check {
display: inline-flex; align-items: center; gap: 4px; font-size: 10px; font-weight: 800; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--sw-red-bright); opacity: 0; transform: scale(0.6); transition: opacity 0.3s ease, transform 0.3s ease;
}
#sw-root .sw-veh-card.is-winner .sw-veh-check { opacity: 1; transform: scale(1); }
#sw-root .sw-veh-lock { display: flex; }
#sw-root .sw-veh-lock svg { width: 11px; height: 11px; stroke-width: 2.2; }
#sw-root .sw-veh-profiles-label { display: block; font-size: 11.5px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(255,255,255,0.45); margin-bottom: 14px; }
#sw-root .sw-veh-profiles-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: clamp(22px, 2.6vw, 30px); }
#sw-root .sw-veh-chip--profile { flex-direction: column; align-items: flex-start; gap: 2px; cursor: pointer; border: none; transition: border-color 0.25s ease, background-color 0.25s ease; }
#sw-root .sw-veh-chip--profile em { font-style: normal; font-weight: 500; color: rgba(255,255,255,0.45); font-size: 11px; }
#sw-root .sw-veh-chip--profile.is-active { border: 1px solid rgba(226,53,74,0.5); background: rgba(192,18,39,0.14); color: #fff; }
#sw-root .sw-veh-factors { display: flex; gap: 10px; flex-wrap: wrap; }
/* Parameter chips (Weight, Dimensions, Distance...) light up one at a
time as the AI "checks" each one — glow while checking, a small
checkmark badge stays once checked — synced to evaluate()'s timeline
so the left-column chips and the right-column stage tell one story. */
#sw-root .sw-veh-chip--factor { position: relative; padding-right: 30px; }
#sw-root .sw-veh-chip--factor.is-checking { border-color: rgba(226,53,74,0.5); background: rgba(192,18,39,0.16); }
#sw-root .sw-veh-chip--factor.is-checked { border-color: rgba(226,53,74,0.32); }
#sw-root .sw-veh-chip-check {
position: absolute; right: 8px; top: 50%; width: 16px; height: 16px; border-radius: 50%; background: var(--sw-red); color: #fff;
display: flex; align-items: center; justify-content: center; opacity: 0; transform: translateY(-50%) scale(0.4);
transition: opacity 0.25s ease, transform 0.25s ease;
}
#sw-root .sw-veh-chip-check svg { width: 9px; height: 9px; stroke-width: 3.4; }
#sw-root .sw-veh-chip--factor.is-checked .sw-veh-chip-check { opacity: 1; transform: translateY(-50%) scale(1); }
@media (max-width: 991px) { #sw-root .sw-veh-row { overflow-x: auto; justify-content: flex-start; padding-bottom: 6px; } }
/* ================= SECTION 04 — AI Decision Engine ================= */
/* A complete replacement for the old map: an enterprise AI decision
dashboard. Dark glass cards on the section frame, live count-up metrics,
score bars, a winner-highlight compare beat, a confirmation banner and a
small dispatch step — all self-looping. */
#sw-root .sw-ade {
position: relative; border-radius: 24px; padding: clamp(18px, 2.6vw, 30px); overflow: hidden;
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.015));
border: 1px solid rgba(255,255,255,0.09);
box-shadow: 0 30px 70px -40px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.06);
}
/* A faint red glow anchored to the top edge so the panel reads as a lit
control surface, not a flat box. */
#sw-root .sw-ade::before {
content: ''; position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(120% 80% at 50% -12%, rgba(226,53,74,0.12), transparent 58%);
}
#sw-root .sw-ade > * { position: relative; z-index: 1; }
#sw-root .sw-ade-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
#sw-root .sw-ade-brand { display: inline-flex; align-items: center; gap: 9px; font-weight: 800; font-size: clamp(13px, 1.4vw, 15px); letter-spacing: 0.02em; color: #fff; }
#sw-root .sw-ade-brand-ic { width: 26px; height: 26px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; color: var(--sw-red-bright); background: rgba(226,53,74,0.14); border: 1px solid rgba(226,53,74,0.32); }
#sw-root .sw-ade-brand-ic svg { width: 15px; height: 15px; }
#sw-root .sw-ade-live { display: inline-flex; align-items: center; gap: 6px; font-size: 10px; font-weight: 800; letter-spacing: 0.14em; color: rgba(255,255,255,0.6); }
#sw-root .sw-ade-livedot { width: 7px; height: 7px; border-radius: 50%; background: var(--sw-red-bright); box-shadow: 0 0 0 0 rgba(226,53,74,0.5); animation: swPulse 1.8s ease-out infinite; flex-shrink: 0; }
#sw-root .sw-ade-livedot--green { background: #22c55e; box-shadow: 0 0 0 0 rgba(34,197,94,0.5); animation: swLiveGreen 1.8s ease-out infinite; }
@keyframes swLiveGreen { 0% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); } 70% { box-shadow: 0 0 0 8px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
#sw-root .sw-ade-status-row { min-height: 18px; margin-bottom: 16px; }
#sw-root .sw-ade-status { font-size: 12px; font-weight: 700; letter-spacing: 0.02em; color: var(--sw-red-bright); }
#sw-root .sw-ade-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(10px, 1.4vw, 16px); }
/* Each candidate is a dark glass card. Smooth transitions on every
property the compare beat touches (opacity/filter/border/shadow/
transform) so muting a loser and lifting the winner both glide. */
#sw-root .sw-ade-card {
display: flex; flex-direction: column; gap: 12px; padding: 16px; border-radius: 16px;
background: rgba(255,255,255,0.045); backdrop-filter: blur(14px) saturate(1.2); -webkit-backdrop-filter: blur(14px) saturate(1.2);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 14px 30px -18px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06);
transition: opacity 0.5s ease, filter 0.5s ease, border-color 0.5s ease, box-shadow 0.5s ease, transform 0.5s ease, background 0.5s ease;
}
#sw-root .sw-ade-card.is-scoring { border-color: rgba(226,53,74,0.32); box-shadow: 0 16px 34px -16px rgba(226,53,74,0.4), inset 0 1px 0 rgba(255,255,255,0.08); }
#sw-root .sw-ade-card.is-muted { opacity: 0.4; filter: saturate(0.55); }
#sw-root .sw-ade-card.is-winner {
border-color: rgba(226,53,74,0.7); transform: translateY(-4px);
background: linear-gradient(180deg, rgba(226,53,74,0.15), rgba(226,53,74,0.035));
box-shadow: 0 24px 46px -18px rgba(226,53,74,0.55), 0 0 0 1px rgba(226,53,74,0.3), inset 0 1px 0 rgba(255,255,255,0.1);
}
#sw-root .sw-ade-card-top { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
#sw-root .sw-ade-card-name { font-weight: 800; font-size: 14px; color: #fff; }
#sw-root .sw-ade-traffic { font-size: 9px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; padding: 3px 8px; border-radius: 999px; white-space: nowrap; }
#sw-root .sw-ade-traffic--light { color: #4ade80; background: rgba(34,197,94,0.14); border: 1px solid rgba(34,197,94,0.3); }
#sw-root .sw-ade-traffic--moderate { color: #fbbf24; background: rgba(251,191,36,0.14); border: 1px solid rgba(251,191,36,0.3); }
#sw-root .sw-ade-traffic--heavy { color: #f87171; background: rgba(248,113,113,0.14); border: 1px solid rgba(248,113,113,0.3); }
#sw-root .sw-ade-rows { display: flex; flex-direction: column; gap: 7px; }
#sw-root .sw-ade-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
#sw-root .sw-ade-row-l { font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(255,255,255,0.42); }
#sw-root .sw-ade-row-v { font-size: 11px; font-weight: 700; color: rgba(255,255,255,0.7); font-variant-numeric: tabular-nums; white-space: nowrap; }
#sw-root .sw-ade-row-v b { font-size: 15px; font-weight: 800; color: #fff; }
#sw-root .sw-ade-fuel-bar { height: 4px; border-radius: 999px; background: rgba(255,255,255,0.1); overflow: hidden; margin-top: 3px; }
#sw-root .sw-ade-fuel-bar > i { display: block; height: 100%; width: 0; border-radius: inherit; background: linear-gradient(90deg, var(--sw-red), var(--sw-red-bright)); }
#sw-root .sw-ade-score { display: flex; flex-direction: column; gap: 6px; padding-top: 11px; border-top: 1px solid rgba(255,255,255,0.09); margin-top: auto; }
#sw-root .sw-ade-score-head { display: flex; align-items: baseline; justify-content: space-between; font-size: 10px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: rgba(255,255,255,0.5); }
#sw-root .sw-ade-score-val { font-size: 20px; font-weight: 800; color: #fff; letter-spacing: 0; text-transform: none; font-variant-numeric: tabular-nums; transition: color 0.5s ease; }
#sw-root .sw-ade-card.is-winner .sw-ade-score-val { color: var(--sw-red-bright); }
#sw-root .sw-ade-score-bar { height: 6px; border-radius: 999px; background: rgba(255,255,255,0.1); overflow: hidden; }
#sw-root .sw-ade-score-bar > i { display: block; height: 100%; width: 0; border-radius: inherit; background: linear-gradient(90deg, rgba(255,255,255,0.4), rgba(255,255,255,0.72)); transition: background 0.5s ease; }
#sw-root .sw-ade-card.is-winner .sw-ade-score-bar > i { background: linear-gradient(90deg, var(--sw-red), var(--sw-red-bright)); box-shadow: 0 0 12px rgba(226,53,74,0.6); }
#sw-root .sw-ade-verdict { display: inline-flex; align-items: center; gap: 6px; font-size: 10.5px; font-weight: 800; color: rgba(255,255,255,0.5); transition: color 0.5s ease; }
#sw-root .sw-ade-verdict-ic { width: 15px; height: 15px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); flex-shrink: 0; }
#sw-root .sw-ade-verdict-ic svg { width: 8px; height: 8px; stroke-width: 3.4; }
#sw-root .sw-ade-verdict-ic--win { background: rgba(226,53,74,0.18); color: var(--sw-red-bright); }
#sw-root .sw-ade-card.is-winner .sw-ade-verdict { color: var(--sw-red-bright); }
/* The AI-analysis connector between the candidate grid and the result. */
#sw-root .sw-ade-flow { display: flex; align-items: center; justify-content: center; gap: 14px; margin: 20px 0 18px; }
#sw-root .sw-ade-flow-arrow { flex: 1; max-width: 200px; height: 1px; background: linear-gradient(90deg, transparent, rgba(226,53,74,0.5), transparent); }
#sw-root .sw-ade-flow-chip { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; letter-spacing: 0.04em; color: #fff; padding: 7px 14px; border-radius: 999px; background: rgba(226,53,74,0.12); border: 1px solid rgba(226,53,74,0.3); white-space: nowrap; }
#sw-root .sw-ade-flow-chip svg { width: 14px; height: 14px; color: var(--sw-red-bright); }
/* The confirmation banner — the decision the whole section builds to. */
#sw-root .sw-ade-result { display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap; padding: 16px; border-radius: 16px; background: linear-gradient(180deg, rgba(226,53,74,0.14), rgba(226,53,74,0.035)); border: 1px solid rgba(226,53,74,0.3); box-shadow: 0 18px 40px -22px rgba(226,53,74,0.5), inset 0 1px 0 rgba(255,255,255,0.08); }
#sw-root .sw-ade-result-badge { display: inline-flex; align-items: center; gap: 9px; font-size: clamp(14px, 1.6vw, 17px); font-weight: 800; color: #fff; }
#sw-root .sw-ade-result-badge svg { width: 18px; height: 18px; color: var(--sw-red-bright); }
#sw-root .sw-ade-result-sub { font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.6); font-variant-numeric: tabular-nums; }
/* The dispatch beat — arrow → vehicle → delivery started. */
#sw-root .sw-ade-dispatch { display: flex; align-items: center; gap: 14px; margin-top: 14px; padding: 14px 16px; border-radius: 14px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); }
#sw-root .sw-ade-dispatch-ic { width: 38px; height: 38px; flex-shrink: 0; border-radius: 11px; display: inline-flex; align-items: center; justify-content: center; color: #fff; background: linear-gradient(180deg, var(--sw-red-bright), #8c0f1f); box-shadow: 0 8px 18px -8px rgba(226,53,74,0.7); }
#sw-root .sw-ade-dispatch-ic svg { width: 20px; height: 20px; }
#sw-root .sw-ade-dispatch-body { flex: 1; display: flex; flex-direction: column; gap: 7px; min-width: 0; }
#sw-root .sw-ade-dispatch-text { font-size: 13px; font-weight: 800; color: #fff; }
#sw-root .sw-ade-dispatch-bar { height: 6px; border-radius: 999px; background: rgba(255,255,255,0.1); overflow: hidden; }
#sw-root .sw-ade-dispatch-bar > i { display: block; height: 100%; width: 0; border-radius: inherit; background: linear-gradient(90deg, var(--sw-red), var(--sw-red-bright)); box-shadow: 0 0 10px rgba(226,53,74,0.6); }
#sw-root .sw-ade-dispatch-eta { font-size: 11px; font-weight: 800; letter-spacing: 0.03em; color: rgba(255,255,255,0.55); white-space: nowrap; }
/* White & Red theme overrides for Sec04 (#routing inside white card) */
#sw-root .sw-sec--white h2,
#sw-root .sw-sec--white h2 *,
#sw-root .sw-sec--white .sw-h,
#sw-root .sw-sec--white .sw-h *,
#sw-root .sw-sec--white .gsap-word-outer,
#sw-root .sw-sec--white .gsap-word-inner,
#sw-root #routing h2,
#sw-root #routing h2 *,
#sw-root #routing .sw-h,
#sw-root #routing .sw-h *,
#sw-root #routing .gsap-word-outer,
#sw-root #routing .gsap-word-inner {
color: #111827 !important;
}
#sw-root .sw-sec--white h2 .accent,
#sw-root .sw-sec--white h2 *.accent,
#sw-root .sw-sec--white .sw-h .accent,
#sw-root .sw-sec--white .sw-h *.accent,
#sw-root .sw-sec--white .gsap-word-inner .accent,
#sw-root #routing h2 .accent,
#sw-root #routing h2 *.accent,
#sw-root #routing .sw-h .accent,
#sw-root #routing .sw-h *.accent,
#sw-root #routing .gsap-word-inner .accent {
color: #dc2626 !important;
}
#sw-root .sw-sec--white .sw-lede,
#sw-root .sw-sec--white .sw-lede *,
#sw-root #routing .sw-lede,
#sw-root #routing .sw-lede * {
color: #4b5563 !important;
}
#sw-root .sw-sec--white .sw-eyebrow,
#sw-root #routing .sw-eyebrow {
color: #6b7280 !important;
}
#sw-root #routing .sw-ade {
background: #f8fafc !important;
border: 1px solid #e2e8f0 !important;
box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.06) !important;
}
#sw-root #routing .sw-ade::before {
background: radial-gradient(120% 80% at 50% -12%, rgba(220, 38, 38, 0.08), transparent 60%) !important;
}
#sw-root #routing .sw-ade-brand,
#sw-root #routing .sw-ade-brand * {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-live,
#sw-root #routing .sw-ade-live * {
color: #475569 !important;
}
#sw-root #routing .sw-ade-status {
color: #dc2626 !important;
}
#sw-root #routing .sw-ade-card {
background: #ffffff !important;
border: 1px solid #cbd5e1 !important;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04) !important;
opacity: 1 !important;
filter: none !important;
}
#sw-root #routing .sw-ade-card.is-muted {
background: #f1f5f9 !important;
border-color: #e2e8f0 !important;
opacity: 0.85 !important;
filter: none !important;
}
#sw-root #routing .sw-ade-card-name,
#sw-root #routing .sw-ade-card-name * {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-row-l,
#sw-root #routing .sw-ade-row-l * {
color: #64748b !important;
}
#sw-root #routing .sw-ade-row-v,
#sw-root #routing .sw-ade-row-v * {
color: #334155 !important;
}
#sw-root #routing .sw-ade-row-v b,
#sw-root #routing .sw-ade-row-v b * {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-fuel-bar {
background: #e2e8f0 !important;
}
#sw-root #routing .sw-ade-score {
border-top-color: #e2e8f0 !important;
}
#sw-root #routing .sw-ade-score-head,
#sw-root #routing .sw-ade-score-head * {
color: #64748b !important;
}
#sw-root #routing .sw-ade-score-val {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-card.is-winner {
border: 2px solid #dc2626 !important;
background: #fff5f5 !important;
box-shadow: 0 14px 32px -10px rgba(220, 38, 38, 0.3) !important;
opacity: 1 !important;
}
#sw-root #routing .sw-ade-card.is-winner .sw-ade-card-name,
#sw-root #routing .sw-ade-card.is-winner .sw-ade-card-name * {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-card.is-winner .sw-ade-score-val {
color: #dc2626 !important;
}
#sw-root #routing .sw-ade-verdict,
#sw-root #routing .sw-ade-verdict * {
color: #64748b !important;
}
#sw-root #routing .sw-ade-verdict-ic {
background: #e2e8f0 !important;
color: #475569 !important;
}
#sw-root #routing .sw-ade-verdict-ic--win {
background: #fee2e2 !important;
color: #dc2626 !important;
}
#sw-root #routing .sw-ade-card.is-winner .sw-ade-verdict,
#sw-root #routing .sw-ade-card.is-winner .sw-ade-verdict * {
color: #dc2626 !important;
}
#sw-root #routing .sw-ade-flow-arrow {
background: linear-gradient(90deg, transparent, rgba(220,38,38,0.4), transparent) !important;
}
#sw-root #routing .sw-ade-flow-chip {
background: #fee2e2 !important;
border: 1px solid #fca5a5 !important;
color: #dc2626 !important;
}
#sw-root #routing .sw-ade-flow-chip * {
color: #dc2626 !important;
}
#sw-root #routing .sw-ade-result {
background: #fef2f2 !important;
border: 1px solid #fca5a5 !important;
box-shadow: 0 10px 24px -10px rgba(220, 38, 38, 0.2) !important;
}
#sw-root #routing .sw-ade-result-badge,
#sw-root #routing .sw-ade-result-badge * {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-result-sub,
#sw-root #routing .sw-ade-result-sub * {
color: #475569 !important;
}
#sw-root #routing .sw-ade-dispatch {
background: #ffffff !important;
border: 1px solid #cbd5e1 !important;
box-shadow: 0 4px 14px rgba(0,0,0,0.04) !important;
}
#sw-root #routing .sw-ade-dispatch-text,
#sw-root #routing .sw-ade-dispatch-text * {
color: #0f172a !important;
}
#sw-root #routing .sw-ade-dispatch-bar {
background: #e2e8f0 !important;
}
#sw-root #routing .sw-ade-dispatch-eta,
#sw-root #routing .sw-ade-dispatch-eta * {
color: #64748b !important;
}
@media (max-width: 860px) {
#sw-root .sw-ade-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
#sw-root .sw-ade-grid { grid-template-columns: 1fr; }
}
/* ================= SECTION 06 — Live Tracking (split) =================
Premium white iPhone-style live-tracking mockup. The device frame keeps
realistic proportions (thin bezels, Dynamic Island, soft shadow) and the
screen is pure white with soft radial lighting. The supplied SVG is a
self-animating map — its marker follows the route natively, so no JS path
animation is needed. */
#sw-root .sw-phone-col { display: flex; justify-content: center; }
#sw-root .sw-phone {
width: clamp(268px, 27vw, 326px); aspect-ratio: 300 / 652; flex-shrink: 0;
border-radius: clamp(44px, 4.4vw, 56px); padding: clamp(9px, 0.9vw, 12px);
background: linear-gradient(155deg, #24262b 0%, #0c0d10 46%, #050506 100%);
box-shadow:
0 70px 120px -46px rgba(0,0,0,0.78),
0 28px 50px -30px rgba(0,0,0,0.62),
inset 0 0 0 1.5px rgba(255,255,255,0.08),
inset 0 1px 1px rgba(255,255,255,0.14);
}
#sw-root .sw-phone-screen {
position: relative; width: 100%; height: 100%; overflow: hidden;
border-radius: clamp(34px, 3.5vw, 46px);
display: flex; flex-direction: column;
background:
radial-gradient(120% 78% at 50% 0%, #ffffff 0%, #f6f7f9 58%, #eef0f3 100%);
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.05);
}
#sw-root .sw-phone-island {
position: absolute; top: clamp(11px, 1.2vw, 15px); left: 50%; transform: translateX(-50%);
width: clamp(80px, 28%, 100px); height: clamp(24px, 2.5vw, 30px);
border-radius: 999px; background: #060607; z-index: 6;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
}
/* iOS status bar — live JS clock at left, static radios at right, flanking
the Dynamic Island. */
#sw-root .sw-phone-statusbar {
position: relative; z-index: 5; flex-shrink: 0;
display: flex; align-items: center; justify-content: space-between;
padding: clamp(15px, 1.7vw, 19px) clamp(20px, 2.3vw, 26px) clamp(3px, 0.5vw, 6px);
color: #0a0a0b;
}
#sw-root .sw-phone-time {
font-size: clamp(14px, 1.4vw, 16px); font-weight: 600; letter-spacing: 0.01em;
font-variant-numeric: tabular-nums; min-width: 3ch;
}
#sw-root .sw-phone-status-icons { display: inline-flex; align-items: center; gap: clamp(5px, 0.6vw, 7px); }
#sw-root .sw-phone-ic { display: block; height: clamp(10px, 1.05vw, 12px); width: auto; color: #0a0a0b; }
#sw-root .sw-phone-ic-wifi { height: clamp(11px, 1.1vw, 13px); }
#sw-root .sw-phone-ic-batt { height: clamp(12px, 1.2vw, 14px); }
#sw-root .sw-phone-5g { font-size: clamp(11px, 1.1vw, 12.5px); font-weight: 600; letter-spacing: 0.01em; }
/* Glass notification banner. */
#sw-root .sw-phone-notif {
flex-shrink: 0; z-index: 3;
margin: clamp(6px, 0.8vw, 9px) clamp(10px, 1.2vw, 14px) 0;
padding: clamp(9px, 1.05vw, 12px) clamp(11px, 1.25vw, 14px);
display: flex; align-items: center; gap: clamp(9px, 1vw, 12px);
border-radius: clamp(17px, 1.9vw, 22px);
background: rgba(255,255,255,0.72);
-webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px);
border: 1px solid rgba(255,255,255,0.7);
box-shadow: 0 12px 28px -14px rgba(15,20,30,0.3), 0 2px 6px -2px rgba(15,20,30,0.1), inset 0 1px 0 rgba(255,255,255,0.6);
}
#sw-root .sw-phone-notif-ic {
width: clamp(30px, 3.2vw, 38px); height: clamp(30px, 3.2vw, 38px); flex-shrink: 0;
border-radius: clamp(8px, 0.9vw, 11px); object-fit: contain; padding: 4px;
background: #fff; border: 1px solid rgba(0,0,0,0.06);
box-shadow: 0 2px 5px -2px rgba(15,20,30,0.25);
}
#sw-root .sw-phone-notif-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
#sw-root .sw-phone-notif-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
#sw-root .sw-phone-notif-app { font-size: clamp(11.5px, 1.15vw, 13px); font-weight: 700; color: #0a0a0b; letter-spacing: 0.005em; }
#sw-root .sw-phone-notif-time { font-size: clamp(10.5px, 1vw, 11.5px); font-weight: 500; color: #9aa1ac; }
#sw-root .sw-phone-notif-title { font-size: clamp(11.5px, 1.15vw, 13px); font-weight: 600; color: #1c1f24; line-height: 1.2; }
#sw-root .sw-phone-notif-sub { font-size: clamp(10.5px, 1.05vw, 12px); font-weight: 500; color: #6b7280; line-height: 1.2; }
#sw-root .sw-phone-map {
position: relative; flex: 1; min-height: 0; overflow: hidden;
display: flex; align-items: stretch; justify-content: center;
padding: clamp(9px, 1.1vw, 13px) clamp(10px, 1.2vw, 14px) clamp(8px, 1vw, 12px);
}
#sw-root .sw-phone-track-svg {
display: block; width: 100%; height: 100%; object-fit: cover;
border-radius: clamp(15px, 1.7vw, 20px);
background: linear-gradient(162deg, #f7f9fc 0%, #eceff4 100%);
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.05);
}
#sw-root .sw-phone-badge {
position: absolute; top: clamp(13px, 1.6vw, 18px); left: clamp(15px, 1.7vw, 20px);
display: inline-flex; align-items: center; gap: 7px; z-index: 3;
padding: 6px 12px 6px 10px; border-radius: 999px;
background: rgba(255,255,255,0.94);
-webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 1px solid rgba(0,0,0,0.06);
color: #0a0a0b; font-size: 11px; font-weight: 800; letter-spacing: 0.09em;
box-shadow: 0 8px 20px -8px rgba(15,20,30,0.35), 0 2px 6px -2px rgba(15,20,30,0.14);
}
#sw-root .sw-phone-badge-dot {
width: 8px; height: 8px; border-radius: 50%; background: #ff3b30; flex-shrink: 0;
box-shadow: 0 0 0 3px rgba(255,59,48,0.18), 0 0 8px 1px rgba(255,59,48,0.75);
}
#sw-root .sw-phone-card {
margin: clamp(2px, 0.4vw, 5px) clamp(12px, 1.4vw, 16px) clamp(14px, 1.6vw, 18px);
padding: clamp(15px, 1.7vw, 19px) clamp(16px, 1.9vw, 20px);
border-radius: clamp(22px, 2.4vw, 28px);
background: #ffffff; border: 1px solid rgba(0,0,0,0.07);
display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
box-shadow: 0 12px 30px -16px rgba(15,20,30,0.28), inset 0 1px 0 rgba(255,255,255,0.9);
}
#sw-root .sw-phone-card-label { color: #6b7280; font-size: clamp(12px, 1.2vw, 13.5px); font-weight: 600; letter-spacing: 0.01em; }
#sw-root .sw-phone-card-eta { color: #0a0a0b; font-size: clamp(26px, 3vw, 34px); font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
#sw-root .sw-phone-card-eta em { font-style: normal; font-size: clamp(12px, 1.2vw, 14px); font-weight: 600; color: #9aa1ac; margin-left: 5px; }
#sw-root .sw-track-cards { display: flex; flex-direction: column; gap: 14px; }
#sw-root .sw-track-card { display: flex; align-items: center; gap: 14px; padding: 16px 18px; border-radius: 18px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); }
#sw-root .sw-track-card-ic { width: 42px; height: 42px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: rgba(226,53,74,0.12); border: 1px solid rgba(226,53,74,0.3); color: var(--sw-red-bright); flex-shrink: 0; }
#sw-root .sw-track-card-ic svg { width: 20px; height: 20px; }
#sw-root .sw-track-card-label { display: block; color: #fff; font-weight: 700; font-size: 14px; }
#sw-root .sw-track-card-note { display: block; color: rgba(255,255,255,0.55); font-size: 12.5px; margin-top: 2px; }
/* ---- Section 06 rebuild: top heading + sticky phone / feature cards ---- */
#sw-root .sw-ldi-head { max-width: 1040px; margin-bottom: clamp(40px, 5vw, 64px); }
#sw-root .sw-ldi-eyebrow { margin-bottom: clamp(16px, 2vw, 22px); }
#sw-root .sw-ldi-h {
color: #fff; font-weight: 900; letter-spacing: -0.03em;
font-size: clamp(34px, 4.5vw, 64px); line-height: 1.15; margin: 0 0 clamp(16px, 2vw, 24px);
max-width: 1000px;
}
#sw-root .sw-ldi-line { display: inline-block; margin-right: 0.28em; }
#sw-root .sw-ldi-h .accent { color: var(--sw-red-bright); }
#sw-root .sw-ldi-lede {
color: var(--sw-text-dim); font-size: clamp(16px, 1.2vw, 19px); line-height: 1.65;
font-weight: 400; margin: 0; max-width: 780px;
}
#sw-root .sw-ldi-grid {
display: grid; grid-template-columns: minmax(280px, 0.82fr) minmax(0, 1.18fr);
gap: clamp(36px, 5vw, 80px); align-items: stretch;
}
#sw-root .sw-ldi-phone-col { position: relative; display: flex; justify-content: center; }
#sw-root .sw-ldi-phone-sticky {
position: sticky; top: clamp(88px, 12vh, 132px); align-self: start;
display: flex; justify-content: center; width: 100%;
}
#sw-root .sw-ldi-cards { display: flex; flex-direction: column; gap: clamp(28px, 3vw, 38px); }
#sw-root .sw-ldi-card {
position: relative; border-radius: clamp(22px, 2.4vw, 28px);
padding: clamp(20px, 2.2vw, 28px);
background: linear-gradient(180deg, rgba(255,255,255,0.055) 0%, rgba(255,255,255,0.02) 100%);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 24px 50px -30px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.06);
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1), border-color 0.4s ease, box-shadow 0.4s ease;
}
#sw-root .sw-ldi-card::after {
content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
background: radial-gradient(120% 80% at 50% 0%, rgba(226,53,74,0.12) 0%, rgba(226,53,74,0) 60%);
opacity: 0; transition: opacity 0.4s ease;
}
#sw-root .sw-ldi-card:hover {
transform: translateY(-6px);
border-color: rgba(226,53,74,0.4);
box-shadow: 0 34px 70px -34px rgba(0,0,0,0.8), 0 0 0 1px rgba(226,53,74,0.2), 0 18px 60px -30px rgba(226,53,74,0.35);
}
#sw-root .sw-ldi-card:hover::after { opacity: 1; }
#sw-root .sw-ldi-card-art {
position: relative; z-index: 1; width: 100%; margin-bottom: clamp(16px, 1.8vw, 22px);
border-radius: clamp(16px, 1.8vw, 20px); overflow: hidden;
}
#sw-root .sw-ldi-svg { display: block; width: 100%; height: auto; }
/* Supplied doorven.svg hero — a square SVG shown large and centered on its
own stage, contained (never cropped or distorted), with generous padding.
Loaded as <img> so its embedded SMIL animations keep running. */
#sw-root .sw-ldi-card-art-stage {
aspect-ratio: 4 / 3; width: 100%; overflow: hidden;
display: flex; align-items: center; justify-content: center;
padding: clamp(10px, 2vw, 22px);
border-radius: inherit;
background: linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.012) 100%);
border: 1px solid rgba(255,255,255,0.08);
}
#sw-root .sw-ldi-svg-img {
display: block; width: 100%; height: 100%;
object-fit: contain; object-position: center;
/* The artwork sits inside a large transparent 1080² canvas; zoom past the
built-in margin so the van itself is the hero (~60% of card height). The
empty margins overflow and are clipped by the stage. */
transform: scale(2.15); transform-origin: center;
}
#sw-root .sw-ldi-card-title {
position: relative; z-index: 1; color: #fff; font-weight: 800;
font-size: clamp(19px, 1.7vw, 24px); letter-spacing: -0.01em; margin: 0 0 8px;
}
#sw-root .sw-ldi-card-desc {
position: relative; z-index: 1; color: var(--sw-text-dim);
font-size: clamp(13.5px, 1.05vw, 15.5px); line-height: 1.55; margin: 0; max-width: 46ch;
}
@media (max-width: 991px) {
#sw-root .sw-ldi-grid { grid-template-columns: 1fr; gap: clamp(32px, 5vw, 48px); }
#sw-root .sw-ldi-phone-sticky { position: static; top: auto; }
}
@media (max-width: 767px) {
#sw-root .sw-ldi-cards { gap: 24px; }
}
/* ================= SECTION 07 — Delivery Flow ================= */
#sw-root .sw-flow { position: relative; min-height: clamp(380px, 30vw, 460px); }
/* Inset by the same clamp(16px, 3vw, 32px) every section's primary media
panel uses (see Section 05's canvas inset), instead of bleeding to the
section's own edge. */
#sw-root .sw-flow-bg { position: absolute; inset: clamp(16px, 3vw, 32px); border-radius: 16px; overflow: hidden; }
#sw-root .sw-flow-bg img { width: 100%; height: 100%; object-fit: cover; }
#sw-root .sw-flow-bg::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.8), rgba(0,0,0,0.93)); }
#sw-root .sw-flow-rail { position: relative; z-index: 1; display: flex; align-items: flex-start; justify-content: space-between; gap: 6px; padding: clamp(36px, 5vw, 60px) clamp(16px, 3vw, 32px); flex-wrap: wrap; row-gap: 30px; }
#sw-root .sw-flow-track { position: absolute; left: 4%; right: 4%; top: calc(clamp(36px, 5vw, 60px) + 22px); height: 2px; background: rgba(255,255,255,0.14); z-index: 0; border-radius: 2px; overflow: visible; }
#sw-root .sw-flow-track-fill { position: absolute; inset: 0; width: calc(var(--fill, 0) * 100%); background: linear-gradient(90deg, var(--sw-red), var(--sw-red-bright)); box-shadow: 0 0 10px rgba(226,53,74,0.7); border-radius: 2px; }
#sw-root .sw-flow-marker { position: absolute; top: 50%; left: calc(var(--fill, 0) * 100%); width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%; background: #fff; box-shadow: 0 0 14px 4px rgba(226,53,74,0.85); }
#sw-root .sw-flow-stage { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; width: 148px; opacity: 0.55; transition: opacity 0.4s ease; }
#sw-root .sw-flow-stage.is-active { opacity: 1; }
#sw-root .sw-flow-num { font-size: 11px; font-weight: 800; color: rgba(255,255,255,0.35); letter-spacing: 0.08em; }
#sw-root .sw-flow-ic {
width: 46px; height: 46px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
background: #0a0a0a; border: 1.5px solid rgba(226,53,74,0.4); color: rgba(226,53,74,0.7);
box-shadow: 0 0 0 5px #000; transition: border-color 0.4s ease, color 0.4s ease, box-shadow 0.4s ease, transform 0.4s ease;
}
#sw-root .sw-flow-stage.is-active .sw-flow-ic { border-color: var(--sw-red); color: var(--sw-red-bright); box-shadow: 0 0 0 5px #000, 0 0 24px -4px rgba(192,18,39,0.9); transform: scale(1.08); }
#sw-root .sw-flow-ic svg { width: 21px; height: 21px; }
#sw-root .sw-flow-label { color: #fff; font-weight: 700; font-size: 13.5px; }
#sw-root .sw-flow-note { color: rgba(255,255,255,0.55); font-size: 12px; line-height: 1.4; }
@media (max-width: 900px) { #sw-root .sw-flow-track { display: none; } }
/* ================= SECTION 08 — MileTruth AI (split + R3F) ================= */
/* One card, not three: the scene fills the frame directly, so there is no
inner rounded box nested inside the outer rounded box. */
#sw-root .sw-mt-canvas { position: relative; height: clamp(360px, 40vw, 520px); border-radius: 24px; overflow: hidden; border: 1px solid var(--sw-line); }
#sw-root .sw-mt-canvas .mtn-scene { border-radius: 0; height: 100%; min-height: 0; }
/* Grid, not wrapping flex — the arrows are real grid items in fixed tracks,
so a step can never drop to a second row and strand its arrow behind it. */
#sw-root .sw-mt-flow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr) auto) minmax(0, 1fr); align-items: start; gap: 6px; margin-bottom: clamp(24px, 2.8vw, 32px); }
#sw-root .sw-mt-step { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; text-align: center; }
#sw-root .sw-mt-flow .sw-veh-arrow { align-self: start; margin-top: 13px; }
#sw-root .sw-mt-step-note { font-size: 11px; font-weight: 500; color: var(--sw-text-dim); line-height: 1.35; }
/* Four steps abreast need ~548px: "Recommendations" alone is 114px and
cannot hyphenate. The copy column doesn't narrow with the viewport
(the split flips to one column at 991px, which makes it wider again),
so this keys off the column's own width rather than the screen's. */
#sw-root .sw-mt-copy { container-type: inline-size; }
@container (max-width: 548px) {
#sw-root .sw-mt-flow { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 10px; }
#sw-root .sw-mt-flow .sw-veh-arrow { display: none; }
#sw-root .sw-mt-step { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-areas: "ic label" "ic note"; column-gap: 10px; row-gap: 2px; text-align: left; }
#sw-root .sw-mt-step-ic { grid-area: ic; align-self: center; width: 42px; height: 42px; border-radius: 12px; }
#sw-root .sw-mt-step-label { grid-area: label; align-self: end; }
#sw-root .sw-mt-step-note { grid-area: note; align-self: start; }
}
@container (max-width: 360px) {
#sw-root .sw-mt-flow { grid-template-columns: 1fr; }
}
#sw-root .sw-mt-step-ic { width: 48px; height: 48px; border-radius: 14px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: var(--sw-red-bright); }
#sw-root .sw-mt-step-ic svg { width: 22px; height: 22px; }
#sw-root .sw-mt-step-label { font-size: 12.5px; font-weight: 700; color: #fff; }
#sw-root .sw-mt-feats { display: flex; gap: 10px; flex-wrap: wrap; border-top: 1px solid rgba(255,255,255,0.08); padding-top: clamp(20px, 2.5vw, 28px); }
#sw-root .sw-mt-feat { display: inline-flex; align-items: center; gap: 8px; padding: 9px 15px; border-radius: 999px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.8); font-size: 12.5px; font-weight: 600; }
#sw-root .sw-mt-feat-ic { color: var(--sw-red-bright); display: flex; }
#sw-root .sw-mt-feat-ic svg { width: 15px; height: 15px; }
/* ================= SECTION 09 — Ops Dashboard (White & Red Theme) ================= */
#sw-root .sw-bg--white {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
}
#sw-root .sw-bg--white::before {
content: ''; position: absolute; inset: 0;
background:
radial-gradient(65% 55% at 50% 0%, rgba(226, 53, 74, 0.08) 0%, transparent 70%),
radial-gradient(45% 45% at 90% 90%, rgba(192, 18, 39, 0.04) 0%, transparent 70%);
pointer-events: none;
}
#sw-root #ops-dashboard .sw-eyebrow--slash {
color: #dc2626 !important;
font-weight: 700 !important;
}
#sw-root #ops-dashboard .sw-h {
color: #111827 !important;
}
#sw-root #ops-dashboard .sw-h .accent {
color: #dc2626 !important;
}
#sw-root #ops-dashboard .sw-lede {
color: #4b5563 !important;
}
#sw-root #ops-dashboard .sw-dash {
background: #ffffff;
border: 1px solid rgba(220, 38, 38, 0.14);
box-shadow: 0 20px 40px -15px rgba(220, 38, 38, 0.07), 0 10px 25px -10px rgba(0, 0, 0, 0.05);
}
#sw-root #ops-dashboard .sw-dash-chrome {
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
#sw-root #ops-dashboard .sw-dash-brand {
color: #111827;
}
#sw-root #ops-dashboard .sw-dash-live {
color: #6b7280;
}
#sw-root #ops-dashboard .sw-livedot {
background: #dc2626;
box-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
}
#sw-root #ops-dashboard .sw-kpi-tile {
background: #fef2f2;
border: 1px solid rgba(220, 38, 38, 0.12);
transition: transform 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
#sw-root #ops-dashboard .sw-kpi-tile:hover {
transform: translateY(-2px);
border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 8px 20px -6px rgba(220, 38, 38, 0.15);
}
#sw-root #ops-dashboard .sw-kpi-tile-ic {
color: #dc2626;
}
#sw-root #ops-dashboard .sw-kpi-val {
color: #111827;
}
#sw-root #ops-dashboard .sw-kpi-label {
color: #6b7280;
}
#sw-root #ops-dashboard .sw-dash-panel {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.07);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
}
#sw-root #ops-dashboard .sw-dash-panel-h {
color: #4b5563;
}
#sw-root #ops-dashboard .sw-ring-track {
stroke: rgba(0, 0, 0, 0.08);
}
#sw-root #ops-dashboard .sw-ring-center b {
color: #111827;
}
#sw-root #ops-dashboard .sw-ring-center span {
color: #6b7280;
}
#sw-root #ops-dashboard .sw-ring-legend span {
color: #374151;
}
#sw-root #ops-dashboard .sw-bar {
background: linear-gradient(180deg, #ef4444, #dc2626);
}
#sw-root #ops-dashboard .sw-fleet-id {
color: #111827;
}
#sw-root #ops-dashboard .sw-fleet-status {
color: #4b5563;
}
#sw-root #ops-dashboard .sw-fleet-eta {
color: #6b7280;
}
#sw-root #ops-dashboard .sw-fleet-dot--moving {
background: #2563eb;
box-shadow: 0 0 8px rgba(37, 99, 235, 0.35);
}
#sw-root #ops-dashboard .sw-fleet-dot--idle {
background: #9ca3af;
box-shadow: none;
}
#sw-root #ops-dashboard .sw-feed-row {
background: #fdf2f2;
border: 1px solid rgba(220, 38, 38, 0.08);
color: #1f2937;
}
#sw-root .sw-dash-chrome { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-bottom: clamp(20px, 2.4vw, 28px); margin-bottom: clamp(20px, 2.4vw, 28px); border-bottom: 1px solid rgba(255,255,255,0.08); }
#sw-root .sw-dash-brand { display: inline-flex; align-items: center; gap: 9px; color: #fff; font-size: 14px; font-weight: 700; }
#sw-root .sw-dash-live { color: rgba(255,255,255,0.45); font-size: 12px; font-weight: 600; }
#sw-root .sw-dash-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: clamp(24px, 2.8vw, 32px); }
#sw-root .sw-kpi-tile { display: flex; flex-direction: column; gap: 6px; padding: 18px 20px; border-radius: 16px; background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.08); }
#sw-root .sw-kpi-tile-ic { color: var(--sw-red-bright); }
#sw-root .sw-kpi-tile-ic svg { width: 18px; height: 18px; }
#sw-root .sw-kpi-val { color: #fff; font-weight: 800; font-size: clamp(22px, 2.4vw, 28px); letter-spacing: -0.01em; }
#sw-root .sw-kpi-label { color: rgba(255,255,255,0.55); font-size: 12px; font-weight: 600; }
#sw-root .sw-dash-body { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
#sw-root .sw-dash-panel { background: rgba(255,255,255,0.025); border: 1px solid rgba(255,255,255,0.07); border-radius: 20px; padding: clamp(20px, 2.4vw, 26px); display: flex; flex-direction: column; gap: 16px; }
#sw-root .sw-dash-panel--wide { grid-column: span 2; }
#sw-root .sw-dash-panel-h { font-size: 11.5px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: rgba(255,255,255,0.45); }
#sw-root .sw-ring-wrap { position: relative; width: 128px; height: 128px; margin: 0 auto; }
#sw-root .sw-ring { width: 100%; height: 100%; }
#sw-root .sw-ring-track { fill: none; stroke: rgba(255,255,255,0.06); stroke-width: 3.2; }
#sw-root .sw-ring-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
#sw-root .sw-ring-center b { color: #fff; font-size: 17px; font-weight: 800; }
#sw-root .sw-ring-center span { color: rgba(255,255,255,0.5); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
#sw-root .sw-ring-legend { display: flex; flex-direction: column; gap: 8px; }
#sw-root .sw-ring-legend span { display: flex; align-items: center; gap: 8px; font-size: 12px; color: rgba(255,255,255,0.65); font-weight: 600; }
#sw-root .sw-ring-legend i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
#sw-root .sw-bars { display: flex; align-items: flex-end; gap: 10px; height: 128px; }
#sw-root .sw-bar { flex: 1; border-radius: 6px 6px 2px 2px; background: linear-gradient(180deg, var(--sw-red-bright), var(--sw-red)); opacity: 0.9; transform-origin: bottom; display: block; }
#sw-root .sw-heat { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
#sw-root .sw-heat-cell { aspect-ratio: 1; border-radius: 5px; }
#sw-root .sw-heat-cell.is-hot { box-shadow: 0 0 14px rgba(226,53,74,0.35); }
#sw-root .sw-fleet-list { display: flex; flex-direction: column; gap: 10px; }
#sw-root .sw-fleet-row { display: grid; grid-template-columns: 10px 1fr auto auto; align-items: center; gap: 10px; min-height: 24px; font-size: 12.5px; }
#sw-root .sw-fleet-dot { width: 8px; height: 8px; border-radius: 50%; animation: swFleetPulse 2.4s ease-in-out infinite; }
#sw-root .sw-fleet-dot--moving { background: #f4f4f6; box-shadow: 0 0 8px rgba(255,255,255,0.45); }
#sw-root .sw-fleet-dot--loading { background: #f1b84b; box-shadow: 0 0 8px rgba(241,184,75,0.48); animation-duration: 1.7s; }
#sw-root .sw-fleet-dot--delayed { background: var(--sw-red-bright); box-shadow: 0 0 9px rgba(226,53,74,0.7); animation-duration: 1.4s; }
#sw-root .sw-fleet-dot--idle { background: rgba(255,255,255,0.35); box-shadow: none; animation-duration: 3.2s; }
#sw-root .sw-fleet-id { color: #fff; font-weight: 700; }
#sw-root .sw-fleet-status { color: rgba(255,255,255,0.6); }
#sw-root .sw-fleet-eta { color: rgba(255,255,255,0.4); font-variant-numeric: tabular-nums; }
#sw-root .sw-feed { display: grid; grid-template-rows: repeat(4, 38px); gap: 10px; height: 182px; overflow: hidden; }
#sw-root .sw-feed-slot { position: relative; min-height: 38px; }
#sw-root .sw-feed-row { position: absolute; inset: 0; display: flex; align-items: center; gap: 10px; font-size: 13px; color: rgba(255,255,255,0.75); padding: 10px 14px; border-radius: 12px; background: rgba(255,255,255,0.03); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@keyframes swFleetPulse {
0%, 100% { transform: scale(1); opacity: 0.72; }
50% { transform: scale(1.25); opacity: 1; }
}
@media (max-width: 1200px) { #sw-root .sw-dash-kpis { grid-template-columns: repeat(2, 1fr); } #sw-root .sw-dash-body { grid-template-columns: 1fr 1fr; } #sw-root .sw-dash-panel--wide, #sw-root .sw-dash-panel--fleet { grid-column: span 2; } }
/* ================= SECTION 10 — Why Doormile ================= */
#sw-root .sw-kpi10-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(16px, 2vw, 22px); }
#sw-root .sw-tilt { will-change: transform; }
#sw-root .sw-kpi10-card {
position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 10px; padding: clamp(24px, 2.8vw, 32px); border-radius: 22px;
background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.09);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
#sw-root .sw-kpi10-card:hover { border-color: rgba(226,53,74,0.45); box-shadow: 0 24px 60px -30px rgba(192,18,39,0.55); }
#sw-root .sw-kpi10-shine { position: absolute; inset: -40%; background: linear-gradient(115deg, transparent 40%, rgba(255,255,255,0.05) 50%, transparent 60%); opacity: 0; transition: opacity 0.4s ease; pointer-events: none; }
#sw-root .sw-kpi10-card:hover .sw-kpi10-shine { opacity: 1; }
#sw-root .sw-kpi10-ic { color: var(--sw-red-bright); position: relative; }
#sw-root .sw-kpi10-ic svg { width: 26px; height: 26px; }
#sw-root .sw-kpi10-val { color: #fff; font-size: clamp(24px, 2.6vw, 32px); font-weight: 800; letter-spacing: -0.01em; position: relative; }
#sw-root .sw-kpi10-label { color: rgba(255,255,255,0.6); font-size: 13px; font-weight: 600; position: relative; }
@media (max-width: 860px) { #sw-root .sw-kpi10-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { #sw-root .sw-kpi10-grid { grid-template-columns: 1fr; } }
/* ================= Mobile: dedicated per-section rework ================= */
@media (max-width: 1024px) {
#sw-root .sw-sec { padding: 72px 0; margin-top: 32px; }
#sw-root .sw-in { width: calc(100% - 20px); }
#sw-root .sw-bg { left: 10px; right: 10px; }
#sw-root { margin-bottom: 32px; }
}
@media (max-width: 991px) {
#sw-root .sw-split, #sw-root .sw-split--reverse { grid-template-columns: 1fr; gap: 32px; }
#sw-root .sw-split--reverse .sw-mt-canvas-col { order: -1; }
#sw-root .sw-veh-stage { order: -1; }
#sw-root .sw-dash-body { grid-template-columns: 1fr; }
#sw-root .sw-dash-panel--wide, #sw-root .sw-dash-panel--fleet { grid-column: span 1 !important; }
}
@media (max-width: 767px) {
#sw-root { margin-bottom: 24px; --sw-in-pad: 16px; }
#sw-root .sw-sec { padding: 56px 0; margin-top: 24px; }
#sw-root .sw-panel, #sw-root .sw-edge, #sw-root .sw-ade, #sw-root .sw-veh-stage, #sw-root .sw-bg { border-radius: 20px; }
#sw-root .sw-panel { padding: 16px; }
#sw-root .sw-ade { padding: 16px; }
#sw-root .sw-dash { overflow: hidden; width: 100%; max-width: 100%; box-sizing: border-box; }
#sw-root .sw-dash-kpis { grid-template-columns: repeat(2, 1fr); gap: 10px; }
#sw-root .sw-kpi-tile { padding: 12px 14px; gap: 4px; }
#sw-root .sw-kpi-val { font-size: clamp(18px, 4.5vw, 24px); }
#sw-root .sw-kpi-label { font-size: 11px; }
#sw-root .sw-dash-body { grid-template-columns: 1fr !important; gap: 12px; }
#sw-root .sw-dash-panel, #sw-root .sw-dash-panel--wide, #sw-root .sw-dash-panel--fleet { grid-column: span 1 !important; padding: 16px 14px; gap: 14px; max-width: 100%; box-sizing: border-box; overflow: hidden; }
#sw-root .sw-bars { gap: 6px; max-width: 100%; }
#sw-root .sw-fleet-row { grid-template-columns: 8px 1fr auto auto; gap: 6px; font-size: 11.5px; }
#sw-root .sw-fleet-id, #sw-root .sw-fleet-status { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sw-root .sw-fleet-eta { white-space: nowrap; }
#sw-root .sw-feed-row { font-size: 12px; padding: 8px 10px; max-width: 100%; box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sw-root .sw-h { font-size: clamp(26px, 8vw, 34px) !important; }
#sw-root .sw-lede { max-width: none; }
/* The map fills via object-fit:cover, so the phone can keep realistic
tall proportions on mobile without the old square-art overflow hack. */
#sw-root .sw-phone { width: clamp(258px, 80vw, 320px); aspect-ratio: 300 / 624; }
#sw-root .sw-phone-map { padding: clamp(8px, 2.4vw, 12px) clamp(9px, 2.6vw, 13px) clamp(8px, 2.2vw, 11px); }
}
@media (max-width: 640px) {
#sw-root .sw-mt-canvas { height: clamp(300px, 62vw, 380px); }
}
@media (max-width: 480px) {
#sw-root .sw-dash-chrome { flex-wrap: wrap; gap: 6px; padding-bottom: 12px; margin-bottom: 16px; }
#sw-root .sw-dash-kpis { grid-template-columns: repeat(2, 1fr); gap: 8px; }
#sw-root .sw-kpi-tile { padding: 10px 12px; }
#sw-root .sw-ring-wrap { width: 108px; height: 108px; }
#sw-root .sw-heat { gap: 3px; }
}
@media (prefers-reduced-motion: reduce) {
#sw-root .sw-jnode-hero-ring, #sw-root .sw-livedot, #sw-root .sw-phone-badge-dot, #sw-root .sw-ade-livedot { animation: none !important; }
}
`;