update code and styles

This commit is contained in:
2026-06-03 19:19:56 +05:30
parent 6eea5636fb
commit 4ba08fc400
21 changed files with 939 additions and 593 deletions

3
.gitignore vendored
View File

@@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# Local backups of pre-optimized GLB source models (not served)
.glb-originals/

View File

@@ -24248,7 +24248,7 @@ img.wp-smiley,
background: rgba(192, 32, 42, 0.13);
}
.init-content {}
/* .init-content {} */
.init-title {
font-family: 'Playfair Display', serif;
@@ -31925,9 +31925,6 @@ img.wp-smiley,
--widgets-spacing-column: 40px;
}
{
}
h1.page-title {
display: var(--page-title-display);
}
@@ -37683,9 +37680,6 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
--widgets-spacing-column: 40px;
}
{
}
h1.page-title {
display: var(--page-title-display);
}

View File

@@ -33,7 +33,29 @@ export default function SmoothScroll() {
// Mouse/desktop only — touch devices already have good native momentum.
const isPointerFine = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (routeDisabled || prefersReduced || !isPointerFine) return;
let hashTimer: ReturnType<typeof setTimeout>;
if (routeDisabled || prefersReduced || !isPointerFine) {
if (!window.location.hash) {
window.scrollTo(0, 0);
} else {
const scrollToHash = () => {
try {
const target = document.querySelector(window.location.hash) as HTMLElement | null;
if (target) {
target.scrollIntoView();
}
} catch (err) {
console.warn(err);
}
};
scrollToHash();
hashTimer = setTimeout(scrollToHash, 100);
}
return () => {
if (hashTimer) clearTimeout(hashTimer);
};
}
gsap.registerPlugin(ScrollTrigger);
@@ -45,6 +67,24 @@ export default function SmoothScroll() {
smoothWheel: true,
});
if (!window.location.hash) {
lenis.scrollTo(0, { immediate: true });
window.scrollTo(0, 0);
} else {
const scrollToHash = () => {
try {
const target = document.querySelector(window.location.hash) as HTMLElement | null;
if (target) {
lenis.scrollTo(target, { immediate: true });
}
} catch (err) {
console.warn(err);
}
};
scrollToHash();
hashTimer = setTimeout(scrollToHash, 100);
}
lenis.on("scroll", ScrollTrigger.update);
const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is seconds, Lenis wants ms
gsap.ticker.add(tickerCb);
@@ -52,6 +92,7 @@ export default function SmoothScroll() {
ScrollTrigger.refresh();
return () => {
if (hashTimer) clearTimeout(hashTimer);
gsap.ticker.remove(tickerCb);
lenis.destroy();
};

View File

@@ -2,6 +2,7 @@
import React, { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { ScrollReveal } from "@/animations/Reveal";
export default function Footer() {
@@ -79,7 +80,7 @@ export default function Footer() {
<div className="elementor-element elementor-element-9990148 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9990148" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-8899bdf elementor-absolute elementor-widget elementor-widget-image" data-id="8899bdf" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_position&quot;:&quot;absolute&quot;}" data-widget_type="image.default">
<div className="elementor-widget-container">
<img width="965" height="474" src="/images/bg-map.png" className="attachment-full size-full wp-image-1148" alt="" />
<Image width={965} height={474} src="/images/bg-map.png" className="attachment-full size-full wp-image-1148" alt="" />
</div>
</div>
<div className="elementor-element elementor-element-a0e7516 elementor-widget elementor-widget-logico_heading" data-id="a0e7516" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
@@ -327,7 +328,7 @@ export default function Footer() {
<div className="elementor-element elementor-element-b5c897d elementor-widget elementor-widget-image" data-id="b5c897d" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
<div className="elementor-widget-container">
<Link href="/">
<img width="300" height="57" src="/images/logo-slogan.png" style={{ width: "280px", height: "auto" }} className="attachment-full size-full wp-image-5851" alt="Doormile Tagline" />
<Image width={300} height={57} src="/images/logo-slogan.png" style={{ width: "280px", height: "auto" }} className="attachment-full size-full wp-image-5851" alt="Doormile Tagline" />
</Link>
</div>
</div>
@@ -468,6 +469,11 @@ export default function Footer() {
.elementor-6585 .elementor-element.elementor-element-3f1ba7a .logico-custom-menu-widget {
font-family: var(--font-manrope), system-ui, -apple-system, "Segoe UI", sans-serif;
}
/* Prevent footer custom navigation menu items from wrapping on hover,
which causes layout shifts, height changes, and cursor flickering loops. */
.elementor-6585 .logico-custom-menu-widget li a {
white-space: nowrap !important;
}
/* The divider widget is a flex child that should grow via Elementor's
--container-widget-flex-grow variable, but the base rule wiring that
variable to flex-grow lives in elementor-frontend.css (not loaded). */

View File

@@ -7,7 +7,7 @@
* Menu open/close + sidebar state is read from HeaderUIProvider so BodyOverlay (sibling at body level) can react.
*/
import { useEffect, useState, useRef } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
@@ -48,7 +48,7 @@ export default function Header() {
// - on doc.ready: $('.header-hide-until-scroll').addClass('header-visible-scrolled')
// - on scroll: toggleClass('dm-header-scrolled', scrollTop > 50)
const [visibleScrolled, setVisibleScrolled] = useState(false);
const isScrolledRef = useRef(false);
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handle = requestAnimationFrame(() => {
@@ -63,17 +63,7 @@ export default function Header() {
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
const scrolled = window.scrollY > 50;
if (isScrolledRef.current !== scrolled) {
isScrolledRef.current = scrolled;
const headerEl = document.querySelector(".header-hide-until-scroll");
if (headerEl) {
if (scrolled) {
headerEl.classList.add("dm-header-scrolled");
} else {
headerEl.classList.remove("dm-header-scrolled");
}
}
}
setIsScrolled(scrolled);
rafId = null;
});
};
@@ -99,7 +89,7 @@ export default function Header() {
"e-parent",
"header-hide-until-scroll",
visibleScrolled ? "header-visible-scrolled" : "",
isScrolledRef.current ? "dm-header-scrolled" : "",
isScrolled ? "dm-header-scrolled" : "",
]
.filter(Boolean)
.join(" ");

View File

@@ -287,7 +287,7 @@ const styles = `
.dm-lb.is-after .dm-lb-sticky { position: absolute; top: auto; bottom: 0; }
.dm-lb-card {
position: absolute !important; inset: 16px !important;
position: absolute !important; inset: 20px !important;
border-radius: 28px !important; overflow: hidden !important;
background: radial-gradient(120% 100% at 50% 0%, #12090c 0%, #0a070a 55%, #060507 100%) !important;
border: 1px solid rgba(192,18,39,0.16) !important;
@@ -300,7 +300,7 @@ const styles = `
the section's bottom so the Innovation card below butts directly against it, reading
as one continuous container — mirrors the Optimisation → Performance seam in Workflow 1. */
.dm-lb.is-connected .dm-lb-card {
top: 16px !important; left: 16px !important; right: 16px !important; bottom: 0 !important;
top: 20px !important; left: 20px !important; right: 20px !important; bottom: 0 !important;
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
}
@media (max-width: 767px) {

View File

@@ -434,9 +434,9 @@ const styles = `
/* ===== FLOATING CARD — the only colored surface ===== */
.dm-opt-card {
position: absolute !important;
top: 110px !important;
left: 40px !important;
right: 40px !important;
top: 96px !important;
left: 20px !important;
right: 20px !important;
bottom: 0 !important;
/* flat bottom + flush to container so the Performance card butts directly
against it, reading as one continuous container (home-page technique) */

View File

@@ -12,7 +12,7 @@ export default function AboutCTA() {
</h2>
<p className="we-cta-sub">
Join our Women Entrepreneurship program and become part of <br />
India's fastest-growing logistics network.
India&apos;s fastest-growing logistics network.
</p>
<div className="we-cta-btns">
<Link href="/contact" className="btn-we-primary" style={{ textDecoration: "none" }}>

View File

@@ -1,7 +1,6 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
import gsap from "gsap";
import { ShimmerText } from "@/animations/Reveal";

View File

@@ -11,7 +11,45 @@ export default function IndustrySolutions() {
<div className="elementor-element elementor-element-f64bd88 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="f64bd88" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-5ed2dbb e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5ed2dbb" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-c8162c4 elementor-widget elementor-widget-logico_heading" data-id="c8162c4" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default" style={{paddingLeft: "50px"}}>
<div className="elementor-element elementor-element-c8162c4 elementor-widget elementor-widget-logico_heading industry-section-label" data-id="c8162c4" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default" style={{paddingLeft: "50px"}}>
<style dangerouslySetInnerHTML={{ __html: `
/* Minimal section label — matches the "/ Doormile Approach /" pattern */
.industry-section-label {
width: 100%;
}
.industry-section-label > .elementor-widget-container {
max-width: 1740px;
margin: 12px 0 50px 0;
padding: 0 0 14px 0;
border-style: solid;
border-width: 0 0 1px 0;
border-color: rgba(17, 17, 17, 0.15);
}
.industry-section-label .logico-title {
font-size: 14px;
font-weight: 500;
line-height: 2.1429em;
letter-spacing: 2px;
text-transform: uppercase;
color: #111111;
}
@media (max-width: 1024px) {
.industry-section-label > .elementor-widget-container {
margin-bottom: 36px;
padding-bottom: 12px;
}
}
@media (max-width: 767px) {
.industry-section-label > .elementor-widget-container {
margin-bottom: 28px;
padding-bottom: 10px;
}
.industry-section-label .logico-title {
font-size: 12px;
letter-spacing: 1.5px;
}
}
`}} />
<div className="elementor-widget-container">
<div className="logico-title">/ Industry Solutions /</div>
</div>

View File

@@ -102,14 +102,15 @@ export default function IndustryWorldMap() {
buildDots();
};
const cityPx = () => CITIES.map(([cx, cy]) => ({ x: cx * w, y: cy * h }));
type Point = { x: number; y: number };
const cityPx = (): Point[] => CITIES.map(([cx, cy]) => ({ x: cx * w, y: cy * h }));
const ctrl = (p0: { x: number; y: number }, p1: { x: number; y: number }) => {
const ctrl = (p0: Point, p1: Point): Point => {
const mx = (p0.x + p1.x) / 2, my = (p0.y + p1.y) / 2;
const lift = Math.hypot(p1.x - p0.x, p1.y - p0.y) * 0.28;
return { x: mx, y: my - lift };
};
const bezier = (p0: any, c: any, p1: any, t: number) => {
const bezier = (p0: Point, c: Point, p1: Point, t: number): Point => {
const u = 1 - t;
return {
x: u * u * p0.x + 2 * u * t * c.x + t * t * p1.x,

View File

@@ -168,7 +168,7 @@ export default function MileTruthHero() {
margin: -25px 0 0 0 !important;
background-color: #1F1F1F !important;
border-radius: 25px !important;
padding: 80px 0 !important;
padding: 40px 0 !important;
position: relative !important;
z-index: 1 !important;
}
@@ -252,8 +252,7 @@ export default function MileTruthHero() {
padding: 120px 0;
}
.miletruth-hero .elementor-element-8e5c81e {
padding: 80px
0 !important;
padding: 40px 0 !important;
}
.miletruth-hero .elementor-element-628123a {
grid-template-columns: repeat(2, 1fr) !important;
@@ -284,7 +283,7 @@ export default function MileTruthHero() {
padding: 0 12px;
}
.miletruth-hero .elementor-element-8e5c81e {
padding: 60px 0 !important;
padding: 30px 0 !important;
border-radius: 20px !important;
}
.miletruth-hero .elementor-element-628123a {

View File

@@ -2,7 +2,7 @@
import React from "react";
import Link from "next/link";
import { ScrollReveal, CountUp, StaggerChildren } from "@/animations/Reveal";
import { CountUp, StaggerChildren } from "@/animations/Reveal";
export default function StatsBar() {
return (

View File

@@ -196,7 +196,7 @@ export default function WomenSection() {
}
`}} />
<div className="elementor-element elementor-element-bbc6760 e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="bbc6760" data-element_type="container" data-e-type="container" style={{ backgroundColor: "#1f1f1f", width: "calc(100% - 40px)", marginLeft: "20px", marginRight: "20px", borderRadius: "25px", overflow: "hidden" }}>
<div id="women-entrepreneurship" className="elementor-element elementor-element-bbc6760 e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="bbc6760" data-element_type="container" data-e-type="container" style={{ backgroundColor: "#1f1f1f", width: "calc(100% - 40px)", marginLeft: "20px", marginRight: "20px", borderRadius: "25px" }}>
<div
className="elementor-element elementor-element-13a7637 elementor-widget__width-auto elementor-absolute elementor-widget elementor-widget-logico_decorative_block"
data-id="13a7637"

View File

@@ -102,15 +102,15 @@ const styles = `
============================================================ */
.dm-wf1 {
position: relative;
margin: 0 auto 24px;
margin: 0 auto 0;
}
/* Performance card — aligned to the optimisation card (40px side insets),
/* Performance card — aligned to the optimisation card (20px side insets),
navy-matched, flat top, rounded bottom, pulled up to close the seam. */
.dm-wf1-card {
position: relative;
z-index: 2;
margin: 0 40px 0;
margin: 0 20px 0;
background: linear-gradient(180deg, #030a18 0%, #06101f 100%);
border: 1px solid rgba(255, 255, 255, 0.05);
border-top: none;

View File

@@ -104,15 +104,15 @@ const styles = `
============================================================ */
.dm-wf2 {
position: relative;
margin: 0 auto 24px;
margin: 0 auto 0;
}
/* Innovation card — aligned to the logistics-brain card (16px side insets),
/* Innovation card — aligned to the logistics-brain card (20px side insets),
red/black-matched, flat top, rounded bottom, pulled up to close the seam. */
.dm-wf2-card {
position: relative;
z-index: 2;
margin: 0 16px 0;
margin: 0 20px 0;
background: radial-gradient(120% 100% at 50% 0%, #12090c 0%, #0a070a 55%, #060507 100%);
border: 1px solid rgba(192, 18, 39, 0.16);
border-top: none;

View File

@@ -98,19 +98,19 @@ const styles = `
Workflow 3 = ONE container:
├─ Happier Riders. Higher Fulfillment. (full StrategySection — 3D)
└─ Strategy (content card, flush, pulled up)
The Strategy card aligns to the 3D card's 16px side insets, butts against
The Strategy card aligns to the 3D card's 20px side insets, butts against
its flat bottom and rounds the bottom corners, so the two read as a single
continuous container — same technique as Workflow 1 & 2.
============================================================ */
.dm-wf3 {
position: relative;
margin: 0 auto 24px;
margin: 0 auto 0;
}
.dm-wf3-card {
position: relative;
z-index: 2;
margin: 0 16px 0;
margin: 0 20px 0;
background: #181818;
border: 1px solid rgba(255, 255, 255, 0.06);
border-top: none;

File diff suppressed because it is too large Load Diff

View File

@@ -154,7 +154,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
<div className="dm-st-card">
{mountScene && (
<div className="dm-st-canvas">
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} />
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} stage={active} />
</div>
)}
@@ -171,127 +171,61 @@ export default function StrategySection({ connected = false }: { connected?: boo
<span className="dm-st-arrow"></span>
</motion.div>
{/* STAGE 01 — INPUT (green): orders + riders enter the system */}
{/* Side cards are now light text anchors — the 3D world carries the
detail. Each: short title + one lead line + a couple of key chips. */}
{/* STAGE 01 — INPUT (green) */}
<StageCard i={0} scroll={scroll} side="left">
<h3 className="dm-st-pillar__title">Orders &amp; riders enter the system</h3>
<div className="dm-st-grid2">
<div className="dm-st-tile">
<span className="dm-st-tile__ico">📄</span>
<span className="dm-st-tile__num">59</span>
<span className="dm-st-tile__lbl">Orders</span>
<span className="dm-st-tile__sub">Order intake</span>
</div>
<div className="dm-st-tile">
<span className="dm-st-tile__ico">🧑</span>
<span className="dm-st-tile__num">4</span>
<span className="dm-st-tile__lbl">Total Riders</span>
<span className="dm-st-tile__sub">Available fleet</span>
</div>
<p className="dm-st-anchor__lead">Orders are uploaded and matched against the available fleet, ready for assignment.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">59 Orders</span>
<span className="dm-st-anchor__chip">4 Riders</span>
<span className="dm-st-anchor__chip">Fleet ready</span>
</div>
<div className="dm-st-file">
<span className="dm-st-file__ico"></span>
<span className="dm-st-file__name">orders.csv</span>
<span className="dm-st-file__meta">59 rows · uploaded</span>
<span className="dm-st-file__ok"></span>
</div>
<ul className="dm-st-riders">
{[
{ a: "A", name: "Rider A", v: "EV Bike", cap: "12 orders" },
{ a: "B", name: "Rider B", v: "Auto", cap: "18 orders" },
{ a: "C", name: "Rider C", v: "Cargo Truck", cap: "20 orders" },
{ a: "D", name: "Rider D", v: "EV Van", cap: "9 orders" },
].map((r) => (
<li key={r.a} className="dm-st-rider">
<span className="dm-st-rider__av">{r.a}<i /></span>
<span className="dm-st-rider__name">{r.name}</span>
<span className="dm-st-rider__veh">{r.v}</span>
<span className="dm-st-rider__cap">{r.cap}</span>
</li>
))}
</ul>
</StageCard>
{/* STAGE 02 — PARALLEL EXECUTION (purple): 6 strategies at once */}
{/* STAGE 02 — PARALLEL EXECUTION (purple) */}
<StageCard i={1} scroll={scroll} side="right">
<h3 className="dm-st-pillar__title">Six strategies, evaluated in parallel</h3>
<div className="dm-st-engines">
<div className="dm-st-engine">
<div className="dm-st-engine__head"><span className="dm-st-engine__name">Legacy Engine</span><span className="dm-st-engine__tag">Baseline</span></div>
<div className="dm-st-pills">
{["Proximity", "Balanced", "Fuel Saver"].map((p) => <span key={p} className="dm-st-pill">{p}</span>)}
</div>
</div>
<div className="dm-st-engine is-unified">
<div className="dm-st-engine__head"><span className="dm-st-engine__name">Unified Engine</span><span className="dm-st-engine__tag dm-st-engine__tag--u">MileTruth</span></div>
<div className="dm-st-pills">
{["EV Aware", "Multi Trip", "Time Aware"].map((p) => <span key={p} className="dm-st-pill dm-st-pill--u">{p}</span>)}
</div>
</div>
<p className="dm-st-anchor__lead">The AI runs every routing strategy at the same time legacy baselines and MileTruth&apos;s unified engine.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">EV Aware</span>
<span className="dm-st-anchor__chip">Multi Trip</span>
<span className="dm-st-anchor__chip">+4 more</span>
</div>
<p className="dm-st-foot"><span className="dm-st-livedot" /> All 6 strategies run at the same time</p>
</StageCard>
{/* STAGE 03 — SMART OPTIMIZATION (blue): validation pipeline */}
{/* STAGE 03 — SMART OPTIMIZATION (blue) */}
<StageCard i={2} scroll={scroll} side="left">
<h3 className="dm-st-pillar__title">Routes validated &amp; optimized</h3>
<div className="dm-st-pipe">
{[
{ ico: "⚙️", name: "VRP Optimizer", desc: "Google OR-Tools solver" },
{ ico: "🔋", name: "Battery Simulation", desc: "EV range & charging feasibility" },
{ ico: "⏱️", name: "SLA Validator", desc: "ETA vs promised window" },
].map((m, j) => (
<React.Fragment key={m.name}>
{j > 0 && <span className="dm-st-pipe__arrow"><i /></span>}
<div className="dm-st-pipe__node">
<span className="dm-st-pipe__ico">{m.ico}</span>
<span className="dm-st-pipe__name">{m.name}</span>
<span className="dm-st-pipe__desc">{m.desc}</span>
</div>
</React.Fragment>
))}
<h3 className="dm-st-pillar__title">Routes optimized &amp; validated</h3>
<p className="dm-st-anchor__lead">Every route is solved for distance, then checked against battery range and delivery SLAs.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">Optimize</span>
<span className="dm-st-anchor__chip">Battery</span>
<span className="dm-st-anchor__chip">SLA</span>
</div>
</StageCard>
{/* STAGE 04 — PERFORMANCE GRADING (orange): every strategy scored */}
{/* STAGE 04 — PERFORMANCE GRADING (orange) */}
<StageCard i={3} scroll={scroll} side="right">
<h3 className="dm-st-pillar__title">Every strategy is scored</h3>
<div className="dm-st-stars"><span className="dm-st-stars__on"></span><span className="dm-st-stars__off"></span><span className="dm-st-stars__txt">4.5 / 5 grade</span></div>
<ul className="dm-st-metrics">
{[
{ name: "Fulfillment Rate", score: "88%", w: 88, status: "Good", desc: "Orders delivered vs total" },
{ name: "SLA Compliance", score: "95%", w: 95, status: "Pass", desc: "On-time within window" },
{ name: "Efficiency Score", score: "92", w: 92, status: "Strong", desc: "Distance & fleet usage" },
{ name: "Battery & Route", score: "OK", w: 100, status: "Feasible", desc: "EV range respected" },
].map((m) => (
<li key={m.name} className="dm-st-metric">
<span className="dm-st-metric__name">{m.name}</span>
<span className="dm-st-metric__bar"><i style={{ width: `${m.w}%` }} /></span>
<span className="dm-st-metric__score">{m.score}</span>
<span className="dm-st-metric__status"> {m.status}</span>
<span className="dm-st-metric__desc">{m.desc}</span>
</li>
))}
</ul>
<p className="dm-st-anchor__lead">Each strategy is graded live on fulfillment, SLA compliance, efficiency and battery feasibility.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">Grade A</span>
<span className="dm-st-anchor__chip">88% Fulfillment</span>
<span className="dm-st-anchor__chip">95% SLA</span>
</div>
</StageCard>
{/* STAGE 05 — STRATEGY COMPARISON (red, hero): the winner */}
{/* STAGE 05 — STRATEGY COMPARISON (red, hero) */}
<StageCard i={4} scroll={scroll} side="right">
<div className="dm-st-winner">
<span className="dm-st-winner__eyebrow">Best strategy recommendation</span>
<div className="dm-st-winner__name"><span className="dm-st-trophy">🏆</span> EV Aware</div>
<span className="dm-st-winner__grade">High Performance Grade</span>
</div>
<div className="dm-st-hero">
<div className="dm-st-hero__ring">
<span className="dm-st-hero__pct">88%</span>
<span className="dm-st-hero__sub">Score</span>
</div>
<ul className="dm-st-wins">
<li><strong>52/59</strong> Orders Fulfilled</li>
<li><strong>88%</strong> Performance Score</li>
<li><strong>3</strong> SLA Violations</li>
<li><strong>A</strong> Performance Grade</li>
</ul>
<h3 className="dm-st-pillar__title">Happier riders. Higher fulfillment.</h3>
<p className="dm-st-anchor__lead">EV Aware wins the best fulfillment with feasible, battery-safe routes for every rider.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip dm-st-anchor__chip--win">🏆 EV Aware</span>
<span className="dm-st-anchor__chip">88% Score</span>
<span className="dm-st-anchor__chip">52/59 Fulfilled</span>
</div>
</StageCard>
</div>
@@ -303,13 +237,13 @@ export default function StrategySection({ connected = false }: { connected?: boo
}
const styles = `
.dm-st { position: relative; height: 620vh; background: transparent; }
.dm-st { position: relative; height: 720vh; background: transparent; }
.dm-st-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; }
.dm-st.is-pinned .dm-st-sticky { position: fixed; top: 0; left: 0; }
.dm-st.is-after .dm-st-sticky { position: absolute; top: auto; bottom: 0; }
.dm-st-card {
position: absolute !important; inset: 16px !important;
position: absolute !important; inset: 20px !important;
border-radius: 28px !important; overflow: hidden !important;
background: radial-gradient(120% 100% at 50% 0%, #ffffff 0%, #eef1f6 60%, #e6eaf2 100%) !important;
border: 1px solid rgba(15,23,42,0.08) !important;
@@ -321,7 +255,7 @@ const styles = `
/* Connected mode (inside Workflow 3): flatten the card's bottom so the Strategy
content card below butts directly against it — same seam as Workflow 1 & 2. */
.dm-st.is-connected .dm-st-card {
top: 16px !important; left: 16px !important; right: 16px !important; bottom: 0 !important;
top: 20px !important; left: 20px !important; right: 20px !important; bottom: 0 !important;
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
}
@media (max-width: 767px) {
@@ -414,90 +348,48 @@ const styles = `
.dm-st3d-chip__txt b { font-size: 12.5px; font-weight: 800; color: #0f172a; }
.dm-st3d-chip__txt { font-size: 10.5px; color: #475569; }
/* STAGE 01 — Input */
.dm-st-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.dm-st-tile { position: relative; background: color-mix(in srgb, var(--c) 7%, white); border: 1px solid color-mix(in srgb, var(--c) 22%, white);
border-radius: 14px; padding: 12px 14px; display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; align-items: center; }
.dm-st-tile__ico { grid-row: 1 / 3; font-size: 24px; }
.dm-st-tile__num { font-size: 26px; font-weight: 800; color: #0f172a; line-height: 1; }
.dm-st-tile__lbl { font-size: 12px; font-weight: 700; color: #334155; }
.dm-st-tile__sub { grid-column: 1 / 3; margin-top: 4px; font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
.dm-st-file { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; padding: 9px 12px; border-radius: 12px;
background: rgba(15,23,42,0.04); border: 1px dashed rgba(15,23,42,0.18); }
.dm-st-file__ico { font-size: 15px; }
.dm-st-file__name { font-size: 13px; font-weight: 700; color: #0f172a; }
.dm-st-file__meta { font-size: 11.5px; color: #64748b; }
.dm-st-file__ok { margin-left: auto; font-size: 12px; font-weight: 800; color: #22C55E; }
.dm-st-riders { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.dm-st-rider { display: grid; grid-template-columns: 26px 1fr auto auto; align-items: center; gap: 9px;
padding: 6px 8px; border-radius: 10px; background: rgba(15,23,42,0.03); }
.dm-st-rider__av { position: relative; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 800; color: #fff; background: linear-gradient(135deg, var(--c), color-mix(in srgb, var(--c) 60%, #1e293b)); }
.dm-st-rider__av i { position: absolute; right: -1px; bottom: -1px; width: 8px; height: 8px; border-radius: 50%; background: #22C55E; border: 2px solid #fff; }
.dm-st-rider__name { font-size: 12.5px; font-weight: 700; color: #0f172a; }
.dm-st-rider__veh { font-size: 11px; font-weight: 600; color: #475569; padding: 2px 8px; border-radius: 999px; background: rgba(15,23,42,0.06); }
.dm-st-rider__cap { font-size: 11px; color: #64748b; white-space: nowrap; }
/* Generic themed 3D chips (stages 0205) — colour comes from --tc per element */
.dm-st3d-tag, .dm-st3d-score {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
pointer-events: none; user-select: none; white-space: nowrap; transition: opacity 0.2s linear; }
.dm-st3d-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 700; color: #0f172a;
background: rgba(255,255,255,0.9); border: 1px solid color-mix(in srgb, var(--tc, #8B5CF6) 55%, white);
border-radius: 999px; padding: 5px 11px; box-shadow: 0 8px 20px -12px var(--tc, #8B5CF6); backdrop-filter: blur(8px); }
.dm-st3d-tag b { font-weight: 800; color: var(--tc, #0f172a); }
.dm-st3d-tag.is-u { background: color-mix(in srgb, var(--tc) 14%, white); border-color: var(--tc); }
.dm-st3d-tag.is-muted { opacity: 0.82; border-style: dashed; }
.dm-st3d-tag.is-win { border-color: var(--tc); box-shadow: 0 10px 26px -10px var(--tc); }
.dm-st3d-score { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #0f172a;
background: rgba(255,255,255,0.92); border: 1px solid color-mix(in srgb, var(--tc, #0f172a) 45%, white);
border-radius: 12px; padding: 6px 13px; box-shadow: 0 10px 26px -12px var(--tc, #0f172a); backdrop-filter: blur(8px); }
.dm-st3d-score b { font-size: 16px; font-weight: 800; color: var(--tc, #0f172a); }
.dm-st3d-score.is-win { border-color: var(--tc); }
/* STAGE 02 — Parallel execution */
.dm-st-engines { display: grid; gap: 10px; }
.dm-st-engine { padding: 11px 12px; border-radius: 14px; background: rgba(15,23,42,0.03); border: 1px solid rgba(15,23,42,0.07); }
.dm-st-engine.is-unified { background: color-mix(in srgb, var(--c) 8%, white); border-color: color-mix(in srgb, var(--c) 28%, white); }
.dm-st-engine__head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.dm-st-engine__name { font-size: 13px; font-weight: 800; color: #0f172a; }
.dm-st-engine__tag { font-size: 9.5px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: #64748b;
background: rgba(15,23,42,0.07); padding: 2px 7px; border-radius: 6px; }
.dm-st-engine__tag--u { color: #fff; background: var(--c); }
.dm-st-pills { display: flex; flex-wrap: wrap; gap: 7px; }
.dm-st-pill { font-size: 12px; font-weight: 700; color: #334155; padding: 6px 12px; border-radius: 999px;
background: rgba(255,255,255,0.9); border: 1px solid rgba(15,23,42,0.12); }
.dm-st-pill--u { color: #0f172a; background: color-mix(in srgb, var(--c) 14%, white); border-color: color-mix(in srgb, var(--c) 40%, white); }
/* Light side-card anchor — the 3D world now carries the detail */
.dm-st-anchor__lead { margin: 0 0 14px; font-size: clamp(13px, 1.2vw, 15px); line-height: 1.55; color: #475569; }
.dm-st-anchor__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.dm-st-anchor__chip { font-size: 12px; font-weight: 700; color: #334155; padding: 6px 12px; border-radius: 999px;
background: color-mix(in srgb, var(--c) 9%, white); border: 1px solid color-mix(in srgb, var(--c) 30%, white); }
.dm-st-anchor__chip--win { color: #fff; background: linear-gradient(90deg, #C01227, #E2354A); border-color: transparent; }
/* STAGE 03 — Optimization pipeline */
.dm-st-pipe { display: grid; gap: 0; }
.dm-st-pipe__node { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 11px; align-items: center;
padding: 11px 14px; border-radius: 14px; background: rgba(255,255,255,0.92);
border: 1px solid color-mix(in srgb, var(--c) 30%, white); box-shadow: 0 8px 22px -14px var(--c); }
.dm-st-pipe__ico { grid-row: 1 / 3; font-size: 22px; }
.dm-st-pipe__name { font-size: 13.5px; font-weight: 800; color: #0f172a; }
.dm-st-pipe__desc { font-size: 11.5px; color: #64748b; }
.dm-st-pipe__arrow { display: flex; align-items: center; justify-content: center; height: 22px; }
.dm-st-pipe__arrow i { position: relative; width: 2px; height: 22px; background: linear-gradient(180deg, color-mix(in srgb, var(--c) 50%, transparent), var(--c)); overflow: visible; }
.dm-st-pipe__arrow i::after { content: ""; position: absolute; left: 50%; top: -4px; width: 6px; height: 6px; border-radius: 50%;
background: var(--c); box-shadow: 0 0 8px var(--c); transform: translateX(-50%); animation: dmStFlow 1.4s linear infinite; }
@keyframes dmStFlow { 0% { top: -4px; opacity: 0; } 20% { opacity: 1; } 100% { top: 22px; opacity: 0; } }
/* STAGE 04 — Performance grading */
.dm-st-stars { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; }
.dm-st-stars__on { color: var(--c); letter-spacing: 2px; font-size: 16px; }
.dm-st-stars__off { color: rgba(15,23,42,0.18); font-size: 16px; }
.dm-st-stars__txt { font-size: 12px; font-weight: 700; color: #475569; margin-left: 4px; }
.dm-st-metrics { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.dm-st-metric { display: grid; grid-template-columns: 1fr 70px auto; grid-template-rows: auto auto; column-gap: 10px; row-gap: 3px; align-items: center; }
.dm-st-metric__name { font-size: 12.5px; font-weight: 700; color: #0f172a; }
.dm-st-metric__bar { grid-column: 1 / 2; grid-row: 2; height: 6px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
.dm-st-metric__bar i { display: block; height: 100%; border-radius: 999px; background: var(--c); }
.dm-st-metric__score { grid-column: 2; grid-row: 1 / 3; font-size: 17px; font-weight: 800; color: #0f172a; text-align: right; }
.dm-st-metric__status { grid-column: 3; grid-row: 1 / 3; font-size: 11px; font-weight: 800; color: #16a34a;
background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.3); padding: 3px 8px; border-radius: 999px; white-space: nowrap; }
.dm-st-metric__desc { grid-column: 1 / 2; grid-row: 1; font-size: 10.5px; color: #64748b; align-self: end; display: none; }
/* STAGE 05 — Strategy comparison (hero) */
.dm-st-winner { margin-bottom: 14px; }
.dm-st-winner__eyebrow { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: #64748b; }
.dm-st-winner__name { display: flex; align-items: center; gap: 10px; margin: 4px 0; font-size: clamp(24px, 2.8vw, 34px); font-weight: 800; color: #0f172a; letter-spacing: -0.02em; }
.dm-st-winner__grade { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 0.04em; color: #fff;
background: linear-gradient(90deg, #C01227, #E2354A); padding: 4px 11px; border-radius: 999px; }
.dm-st-trophy { font-size: 32px; filter: drop-shadow(0 8px 18px rgba(192,18,39,0.4)); animation: dmStFloat 3s ease-in-out infinite; }
@keyframes dmStFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
.dm-st-hero { display: flex; align-items: center; gap: 20px; }
.dm-st-hero__ring { position: relative; flex-shrink: 0; width: 96px; height: 96px; border-radius: 50%; display: flex; flex-direction: column;
align-items: center; justify-content: center; background: conic-gradient(#C01227 88%, rgba(15,23,42,0.1) 0); }
.dm-st-hero__ring::after { content: ""; position: absolute; inset: 9px; border-radius: 50%; background: #fff; }
.dm-st-hero__pct { position: relative; z-index: 1; font-size: 24px; font-weight: 800; color: #C01227; }
.dm-st-hero__sub { position: relative; z-index: 1; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: #64748b; }
.dm-st-wins { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.dm-st-wins li { font-size: 13px; color: #334155; display: flex; align-items: baseline; gap: 8px; }
.dm-st-wins strong { color: #C01227; font-weight: 800; min-width: 48px; }
/* In-world Command Center KPI card + Winner card (drei <Html>, faded by proximity) */
.dm-st3d-kpi, .dm-st3d-winner3d {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
pointer-events: none; user-select: none; transition: opacity 0.2s linear; }
.dm-st3d-kpi { display: flex; flex-direction: column; gap: 5px; width: 132px; padding: 9px 12px; border-radius: 12px;
background: rgba(255,255,255,0.95); border: 1px solid color-mix(in srgb, var(--tc, #F59E0B) 40%, white);
box-shadow: 0 10px 26px -14px var(--tc, #F59E0B); backdrop-filter: blur(8px); }
.dm-st3d-kpi__n { font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
.dm-st3d-kpi__v { font-size: 24px; font-weight: 800; color: #0f172a; line-height: 1; }
.dm-st3d-kpi__v i { font-size: 14px; font-weight: 700; color: var(--tc, #F59E0B); font-style: normal; margin-left: 1px; }
.dm-st3d-kpi__bar { height: 6px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
.dm-st3d-kpi__bar i { display: block; height: 100%; border-radius: 999px; background: var(--tc, #F59E0B); }
.dm-st3d-winner3d { display: flex; flex-direction: column; gap: 3px; width: 184px; padding: 13px 15px; border-radius: 14px;
background: rgba(255,255,255,0.96); border: 1px solid rgba(192,18,39,0.4); box-shadow: 0 16px 40px -16px rgba(192,18,39,0.6); backdrop-filter: blur(8px); }
.dm-st3d-winner3d__top { font-size: 10.5px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #C01227; }
.dm-st3d-winner3d__name { font-size: 26px; font-weight: 800; color: #0f172a; letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 4px; }
.dm-st3d-winner3d__row { font-size: 12px; color: #475569; }
.dm-st3d-winner3d__row b { color: #C01227; font-weight: 800; margin-right: 4px; }
@media (max-width: 1000px) {
.dm-st-rail__title { display: none; }
@@ -505,12 +397,11 @@ const styles = `
.dm-st-rail__line { width: 9px; }
}
@media (max-width: 767px) {
.dm-st { height: 560vh; }
.dm-st { height: 640vh; }
.dm-st-card-story { left: 0 !important; right: 0 !important; margin: 0 auto; width: calc(100% - 28px);
bottom: clamp(18px, 4vh, 40px); padding: 15px 16px; }
.dm-st-rider__cap { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.dm-st-arrow, .dm-st-trophy { animation: none !important; }
.dm-st-arrow { animation: none !important; }
}
`;

View File

@@ -1,4 +1,4 @@
import { STAGES, N, Z_SPACING, X_OFFSET, stagePosition, samplePath } from "./theme";
import { STAGES, N, D_SPACING, X_SWAY, districtPosition, samplePath, cameraFor } from "./theme";
describe("strategy/theme — stage data", () => {
it("exposes five stages and a matching N", () => {
@@ -17,45 +17,51 @@ describe("strategy/theme — stage data", () => {
});
});
describe("strategy/theme — stagePosition()", () => {
it("alternates x sign by parity and steps z by -Z_SPACING", () => {
expect(stagePosition(0)).toEqual([-X_OFFSET, -0.45, -0]);
expect(stagePosition(1)).toEqual([X_OFFSET, 0.55, -Z_SPACING]);
expect(stagePosition(2)).toEqual([-X_OFFSET, -0.45, -2 * Z_SPACING]);
expect(stagePosition(3)).toEqual([X_OFFSET, 0.55, -3 * Z_SPACING]);
describe("strategy/theme — districtPosition()", () => {
it("sits on the floor (y=0), sways x by parity, steps z by -D_SPACING", () => {
expect(districtPosition(0)).toEqual([-X_SWAY, 0, -0]);
expect(districtPosition(1)).toEqual([X_SWAY, 0, -D_SPACING]);
expect(districtPosition(2)).toEqual([-X_SWAY, 0, -2 * D_SPACING]);
expect(districtPosition(3)).toEqual([X_SWAY, 0, -3 * D_SPACING]);
});
it("places even stages left and odd stages right", () => {
it("places even districts left and odd districts right", () => {
for (let i = 0; i < N; i++) {
const [x] = stagePosition(i);
const [x] = districtPosition(i);
if (i % 2 === 0) expect(x).toBeLessThan(0);
else expect(x).toBeGreaterThan(0);
}
});
it("marches monotonically into -Z", () => {
for (let i = 1; i < N; i++) {
expect(districtPosition(i)[2]).toBeLessThan(districtPosition(i - 1)[2]);
}
});
});
describe("strategy/theme — samplePath()", () => {
it("equals stagePosition() at integer indices", () => {
it("equals districtPosition() at integer indices", () => {
for (let i = 0; i < N; i++) {
const sampled = samplePath(i);
const exact = stagePosition(i);
const exact = districtPosition(i);
sampled.forEach((v, k) => expect(v).toBeCloseTo(exact[k], 10));
}
});
it("linearly interpolates between adjacent stages", () => {
it("linearly interpolates between adjacent districts", () => {
const [x, y, z] = samplePath(0.5);
expect(x).toBeCloseTo(0, 10); // midpoint of -2.5 and +2.5
expect(y).toBeCloseTo(0.05, 10); // midpoint of -0.45 and 0.55
expect(z).toBeCloseTo(-Z_SPACING / 2, 10);
expect(x).toBeCloseTo(0, 10); // midpoint of -X_SWAY and +X_SWAY
expect(y).toBeCloseTo(0, 10);
expect(z).toBeCloseTo(-D_SPACING / 2, 10);
});
it("clamps indices below 0 to the first stage", () => {
expect(samplePath(-3)).toEqual(stagePosition(0));
it("clamps indices below 0 to the first district", () => {
expect(samplePath(-3)).toEqual(districtPosition(0));
});
it("clamps indices above N-1 to the last stage", () => {
expect(samplePath(99)).toEqual(stagePosition(N - 1));
it("clamps indices above N-1 to the last district", () => {
expect(samplePath(99)).toEqual(districtPosition(N - 1));
});
it("never produces NaN across a fine sweep", () => {
@@ -65,3 +71,22 @@ describe("strategy/theme — samplePath()", () => {
}
});
});
describe("strategy/theme — cameraFor()", () => {
it("keeps the camera behind (+Z) and above (+Y) the active district", () => {
for (let i = 0; i < N; i++) {
const { pos, look } = cameraFor(i);
const [, , dz] = districtPosition(i);
expect(pos[2]).toBeGreaterThan(dz); // camera is back from the district
expect(pos[1]).toBeGreaterThan(look[1]); // camera looks down
expect(look[2]).toBeLessThan(pos[2]); // look target leads forward
}
});
it("never produces NaN across a fine sweep", () => {
for (let p = 0; p <= 1.0001; p += 0.05) {
const { pos, look } = cameraFor(p * (N - 1));
for (const v of [...pos, ...look]) expect(Number.isNaN(v)).toBe(false);
}
});
});

View File

@@ -1,6 +1,8 @@
// Shared data + geometry for the "Strategy" 3D scroll-storytelling section.
// Five themed stages laid out along a zig-zag path in 3D. The camera travels
// stage-to-stage as a normalized scroll progress (0→1) advances.
// The five stages are five DISTRICTS of one continuous logistics world laid out
// along the camera's forward (Z) axis. As normalized scroll progress (0→1)
// advances, the camera dollies forward through the world: Intake dock → Strategy
// network → City route map → Command center → Winner podium.
export type StageTheme = {
n: string; // "01"
@@ -19,25 +21,43 @@ export const STAGES: StageTheme[] = [
export const N = STAGES.length;
// Zig-zag layout constants (world units).
export const Z_SPACING = 6.4;
export const X_OFFSET = 2.5;
// Forward-journey layout (world units). Each district sits on the floor (y≈0) at
// an increasing depth; a slight alternating x sway gives the dolly some rhythm
// without ever turning the world into a zig-zag of separate panels.
export const D_SPACING = 13; // depth between districts
export const X_SWAY = 1.4; // gentle left/right sway between districts
/** Resting world position of stage `i` along the zig-zag path. */
export function stagePosition(i: number): [number, number, number] {
const x = (i % 2 === 0 ? -1 : 1) * X_OFFSET;
const y = i % 2 === 0 ? -0.45 : 0.55;
const z = -i * Z_SPACING;
return [x, y, z];
/** Resting world position (floor anchor) of district `i` along the journey. */
export function districtPosition(i: number): [number, number, number] {
const x = (i % 2 === 0 ? -1 : 1) * X_SWAY;
const z = -i * D_SPACING;
return [x, 0, z];
}
/** Continuous position sampled at a fractional stage index (lerp between stages). */
/** Continuous floor position sampled at a fractional district index. */
export function samplePath(idx: number): [number, number, number] {
const clamped = Math.max(0, Math.min(N - 1, idx));
const f = Math.floor(clamped);
const c = Math.min(N - 1, f + 1);
const t = clamped - f;
const a = stagePosition(f);
const b = stagePosition(c);
const a = districtPosition(f);
const b = districtPosition(c);
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}
/**
* Camera dolly + look target for a fractional district index. The camera rides
* behind and above the active district and looks slightly forward+down, so the
* district fills the frame while the next one eases into view ahead.
*/
export function cameraFor(idx: number): {
pos: [number, number, number];
look: [number, number, number];
} {
const [tx, ty, tz] = samplePath(idx);
// Look target leads a little forward (deeper into Z) and sits at eye height.
const look: [number, number, number] = [tx * 0.55, ty + 1.05, tz - 1.6];
// Camera sits back (+Z) and up (+Y) from the active district.
const pos: [number, number, number] = [tx * 0.7, ty + 4.0, tz + 8.2];
return { pos, look };
}