update code and styles
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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="{"_position":"absolute"}" 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). */
|
||||
|
||||
@@ -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(" ");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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's fastest-growing logistics network.
|
||||
</p>
|
||||
<div className="we-cta-btns">
|
||||
<Link href="/contact" className="btn-we-primary" style={{ textDecoration: "none" }}>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 & 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'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 & 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 & 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 02–05) — 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; }
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user