update screen fix

This commit is contained in:
2026-06-05 17:40:56 +05:30
parent 2f23f16634
commit 91841ba3f4
12 changed files with 438 additions and 93 deletions

10
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "doormile-next",
"version": "0.1.0",
"dependencies": {
"@emailjs/browser": "^4.4.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@react-three/postprocessing": "^3.0.4",
@@ -722,6 +723,15 @@
"node": ">=18"
}
},
"node_modules/@emailjs/browser": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@emailjs/browser/-/browser-4.4.1.tgz",
"integrity": "sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",

View File

@@ -13,6 +13,7 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@emailjs/browser": "^4.4.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@react-three/postprocessing": "^3.0.4",

View File

@@ -2663,7 +2663,7 @@ iframe {
margin-bottom: 0
}
.wp-block-group.has-background {
.dm-block-group.has-background {
border-radius: var(--logico-radius-medium,0)
}
@@ -5487,11 +5487,11 @@ body:not(.block-editor-page) .content-wrapper .widget p {
margin-top: -.25em
}
.sidebar .widget-wrapper>.wp-block-title:not(:last-child),.sidebar .wp-block-group>.wp-block-title:not(:last-child) {
.sidebar .widget-wrapper>.wp-block-title:not(:last-child),.sidebar .dm-block-group>.wp-block-title:not(:last-child) {
margin: 0 0 .95em
}
.sidebar .widget-wrapper>.wp-block-title:first-child h1,.sidebar .widget-wrapper>.wp-block-title:first-child h2,.sidebar .widget-wrapper>.wp-block-title:first-child h3,.sidebar .widget-wrapper>.wp-block-title:first-child h4,.sidebar .widget-wrapper>.wp-block-title:first-child h5,.sidebar .widget-wrapper>.wp-block-title:first-child h6,.sidebar .wp-block-group>.wp-block-title:first-child h1,.sidebar .wp-block-group>.wp-block-title:first-child h2,.sidebar .wp-block-group>.wp-block-title:first-child h3,.sidebar .wp-block-group>.wp-block-title:first-child h4,.sidebar .wp-block-group>.wp-block-title:first-child h5,.sidebar .wp-block-group>.wp-block-title:first-child h6 {
.sidebar .widget-wrapper>.wp-block-title:first-child h1,.sidebar .widget-wrapper>.wp-block-title:first-child h2,.sidebar .widget-wrapper>.wp-block-title:first-child h3,.sidebar .widget-wrapper>.wp-block-title:first-child h4,.sidebar .widget-wrapper>.wp-block-title:first-child h5,.sidebar .widget-wrapper>.wp-block-title:first-child h6,.sidebar .dm-block-group>.wp-block-title:first-child h1,.sidebar .dm-block-group>.wp-block-title:first-child h2,.sidebar .dm-block-group>.wp-block-title:first-child h3,.sidebar .dm-block-group>.wp-block-title:first-child h4,.sidebar .dm-block-group>.wp-block-title:first-child h5,.sidebar .dm-block-group>.wp-block-title:first-child h6 {
margin: 0!important;
padding: 0 1.5em 1.05em 0;
border-bottom: solid 1px;
@@ -5501,7 +5501,7 @@ body:not(.block-editor-page) .content-wrapper .widget p {
font: 600 normal 20px/1.25em var(--logico-body-font-family)
}
.sidebar .widget-wrapper>.wp-block-title:first-child h1:after,.sidebar .widget-wrapper>.wp-block-title:first-child h2:after,.sidebar .widget-wrapper>.wp-block-title:first-child h3:after,.sidebar .widget-wrapper>.wp-block-title:first-child h4:after,.sidebar .widget-wrapper>.wp-block-title:first-child h5:after,.sidebar .widget-wrapper>.wp-block-title:first-child h6:after,.sidebar .wp-block-group>.wp-block-title:first-child h1:after,.sidebar .wp-block-group>.wp-block-title:first-child h2:after,.sidebar .wp-block-group>.wp-block-title:first-child h3:after,.sidebar .wp-block-group>.wp-block-title:first-child h4:after,.sidebar .wp-block-group>.wp-block-title:first-child h5:after,.sidebar .wp-block-group>.wp-block-title:first-child h6:after {
.sidebar .widget-wrapper>.wp-block-title:first-child h1:after,.sidebar .widget-wrapper>.wp-block-title:first-child h2:after,.sidebar .widget-wrapper>.wp-block-title:first-child h3:after,.sidebar .widget-wrapper>.wp-block-title:first-child h4:after,.sidebar .widget-wrapper>.wp-block-title:first-child h5:after,.sidebar .widget-wrapper>.wp-block-title:first-child h6:after,.sidebar .dm-block-group>.wp-block-title:first-child h1:after,.sidebar .dm-block-group>.wp-block-title:first-child h2:after,.sidebar .dm-block-group>.wp-block-title:first-child h3:after,.sidebar .dm-block-group>.wp-block-title:first-child h4:after,.sidebar .dm-block-group>.wp-block-title:first-child h5:after,.sidebar .dm-block-group>.wp-block-title:first-child h6:after {
content: '\e80a';
display: block;
position: absolute;
@@ -8089,11 +8089,11 @@ h1:where(.wp-block-heading).has-background,
/*# sourceURL=local */
/* STYLE BLOCK 8 */
.wp-block-group {
.dm-block-group {
box-sizing: border-box
}
:where(.wp-block-group.wp-block-group-is-layout-constrained) {
:where(.dm-block-group.dm-block-group-is-layout-constrained) {
position: relative
}

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import emailjs from "@emailjs/browser";
import { ScrollReveal } from "@/animations/Reveal";
export default function Footer() {
@@ -45,15 +46,37 @@ export default function Footer() {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear any stale success/error message once the user resumes editing.
if (formStatus === "success" || formStatus === "error") setFormStatus("idle");
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// EmailJS is fully client-side — only the public-safe credentials are used
// (Service ID, Template ID, Public Key). No private/secret key, no backend route.
const serviceId = process.env.NEXT_PUBLIC_EMAILJS_SERVICE_ID;
const templateId = process.env.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID;
const publicKey = process.env.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY;
if (!serviceId || !templateId || !publicKey) {
console.error("EmailJS env vars are missing — set NEXT_PUBLIC_EMAILJS_* in .env.local");
setFormStatus("error");
return;
}
setFormStatus("submitting");
try {
// Simulate API submission
console.log("Footer contact form submitted:", formData);
await new Promise((resolve) => setTimeout(resolve, 1000));
await emailjs.send(
serviceId,
templateId,
{
name: formData.fullName,
email: formData.email,
subject: formData.subject,
message: formData.message,
},
publicKey,
);
setFormStatus("success");
setFormData({ fullName: "", email: "", subject: "", message: "" });
} catch (err) {
@@ -285,12 +308,12 @@ export default function Footer() {
</button>
{formStatus === "success" && (
<div style={{ color: "#4caf50", marginTop: "10px", fontSize: "14px" }}>
Message sent successfully!
Message sent successfully.
</div>
)}
{formStatus === "error" && (
<div style={{ color: "#f44336", marginTop: "10px", fontSize: "14px" }}>
Something went wrong. Please try again.
Failed to send message. Please try again.
</div>
)}
</div>

View File

@@ -145,7 +145,7 @@ export default function Header() {
<div className="slide-sidebar-content">
<div id="block-37" className="widget widget_block">
<div className="widget-wrapper">
<div className="wp-block-group is-layout-constrained wp-block-group-is-layout-constrained">
<div className="dm-block-group is-layout-constrained dm-block-group-is-layout-constrained">
<figure className="wp-block-image size-full is-resized">
<Image
width={305}

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import React, { useRef, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import { COLORS } from "./constants";
@@ -15,40 +15,78 @@ type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
isMobile?: boolean;
isTablet?: boolean;
/** Pause the render loop when the section is scrolled off-screen. */
active?: boolean;
};
/**
* Device-specific camera framing. The mobile/tablet scene renders into a much
* smaller, near-square block than the desktop full-screen card, so the camera
* is pulled back, raised, and widened (higher fov) to keep every node in frame
* without clipping. `lerp(start, end, progress)` eases from the chaotic wide
* view to the settled framing as the narrative progresses.
*/
type CameraFraming = {
radiusStart: number;
radiusEnd: number;
heightStart: number;
heightEnd: number;
lookAtY: number;
fov: number;
};
const FRAMING: Record<"desktop" | "tablet" | "mobile", CameraFraming> = {
desktop: { radiusStart: 17, radiusEnd: 13, heightStart: 9, heightEnd: 6.5, lookAtY: 2.4, fov: 50 },
tablet: { radiusStart: 19, radiusEnd: 15, heightStart: 9.5, heightEnd: 7, lookAtY: 2.6, fov: 54 },
mobile: { radiusStart: 22, radiusEnd: 18, heightStart: 11, heightEnd: 8, lookAtY: 3, fov: 62 },
};
/** Slow cinematic camera move from a high chaotic view to a settled framing. */
function CameraRig({ progress }: { progress: React.RefObject<number> }) {
function CameraRig({ progress, framing }: { progress: React.RefObject<number>; framing: CameraFraming }) {
const eased = useRef(0);
const camera = useThree((s) => s.camera);
// Re-frame on device change (orientation / breakpoint crossing): the fov is a
// construction-time prop on <Canvas>, so update it imperatively here.
useEffect(() => {
if ("fov" in camera) {
(camera as THREE_PerspectiveCamera).fov = framing.fov;
(camera as THREE_PerspectiveCamera).updateProjectionMatrix();
}
}, [camera, framing.fov]);
useFrame((state, dt) => {
const p = progress.current ?? 0;
eased.current = damp(eased.current, p, 1.5, dt);
const e = eased.current;
const t = state.clock.elapsedTime;
const radius = lerp(17, 13, e);
const radius = lerp(framing.radiusStart, framing.radiusEnd, e);
const angle = lerp(-0.5, 0.45, e) + t * 0.02;
const height = lerp(9, 6.5, e) + Math.sin(t * 0.4) * 0.3;
const height = lerp(framing.heightStart, framing.heightEnd, e) + Math.sin(t * 0.4) * 0.3;
const cam = state.camera;
cam.position.x = Math.sin(angle) * radius;
cam.position.z = Math.cos(angle) * radius;
cam.position.y = height;
cam.lookAt(0, 2.4, 0);
cam.lookAt(0, framing.lookAtY, 0);
});
return null;
}
function OptimizationCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) {
// Minimal structural type so we can set fov without importing three's types here.
type THREE_PerspectiveCamera = { fov: number; updateProjectionMatrix: () => void };
function OptimizationCanvas({ progress, reduced = false, isMobile = false, isTablet = false, active = true }: Props) {
const cityCount = isMobile ? 48 : 90;
const framing = isMobile ? FRAMING.mobile : isTablet ? FRAMING.tablet : FRAMING.desktop;
return (
<Canvas
flat
dpr={[1, isMobile || reduced ? 1.25 : 1.5]}
camera={{ position: [0, 9, 19], fov: 50, near: 0.1, far: 120 }}
camera={{ position: [0, framing.heightStart, framing.radiusStart], fov: framing.fov, near: 0.1, far: 120 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"}
>
@@ -56,7 +94,7 @@ function OptimizationCanvas({ progress, reduced = false, isMobile = false, activ
<fog attach="fog" args={[COLORS.bg, 18, 52]} />
<ambientLight intensity={0.6} />
<CameraRig progress={progress} />
<CameraRig progress={progress} framing={framing} />
<HologramCity progress={progress} count={cityCount} reduced={reduced} />
<RouteSystem progress={progress} reduced={reduced} isMobile={isMobile} />
<VehicleFleet progress={progress} reduced={reduced} />
@@ -68,10 +106,10 @@ function OptimizationCanvas({ progress, reduced = false, isMobile = false, activ
<EffectComposer multisampling={isMobile ? 0 : 2}>
<Bloom
mipmapBlur
intensity={isMobile ? 0.7 : 1.0}
intensity={isMobile ? 0.5 : 1.0}
luminanceThreshold={0.15}
luminanceSmoothing={0.04}
radius={isMobile ? 0.6 : 0.75}
radius={isMobile ? 0.55 : 0.75}
kernelSize={KernelSize.MEDIUM}
/>
</EffectComposer>

View File

@@ -109,6 +109,46 @@ const LiveInsightBar = React.memo(function LiveInsightBar() {
);
});
/** Inner content of the "Without Optimization" panel — shared by the desktop
* (scroll-reactive motion.aside) and mobile (static aside) layouts. */
function WithoutPanelBody() {
return (
<>
<div className="dm-opt-panel__badge">
<span className="dm-opt-pulse dm-opt-pulse--red" /> System: Congested
</div>
<h3>Without Optimization</h3>
<ul>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> Chaotic overlapping routes</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> Duplicate &amp; idle trips</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> 8 vehicles required</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> 23 delivery delays</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> +18% cost overrun</li>
</ul>
</>
);
}
/** Inner content of the "With Doormile AI" panel — shared by both layouts. */
function WithPanelBody() {
return (
<>
<div className="dm-opt-panel__badge dm-opt-panel__badge--good">
<span className="dm-opt-pulse dm-opt-pulse--green" /> System: Optimized
</div>
<h3>With Doormile AI</h3>
<ul>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Optimized route clusters</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Intelligent vehicle assignment</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Multi-trip &amp; EV planning</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Zero delivery delays</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> 18% cost saved</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Carbon footprint reduced</li>
</ul>
</>
);
}
export default function OptimizationSection() {
const containerRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
@@ -119,21 +159,30 @@ export default function OptimizationSection() {
const [mountScene, setMountScene] = useState(false);
const [sceneActive, setSceneActive] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [isTablet, setIsTablet] = useState(false);
const [reduced, setReduced] = useState(false);
// Final-state metrics value for the mobile stack: a constant MotionValue so
// MetricsPanel renders the optimized numbers without scroll-driven counting.
const staticFinal = useMotionValue(1);
// Environment detection (client only).
useEffect(() => {
const mqMobile = window.matchMedia("(max-width: 767px)");
const mqTablet = window.matchMedia("(min-width: 768px) and (max-width: 1024px)");
const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)");
const sync = () => {
setIsMobile(mqMobile.matches);
setIsTablet(mqTablet.matches);
setReduced(mqReduce.matches);
};
sync();
mqMobile.addEventListener("change", sync);
mqTablet.addEventListener("change", sync);
mqReduce.addEventListener("change", sync);
return () => {
mqMobile.removeEventListener("change", sync);
mqTablet.removeEventListener("change", sync);
mqReduce.removeEventListener("change", sync);
};
}, []);
@@ -176,6 +225,10 @@ export default function OptimizationSection() {
useEffect(() => {
const el = containerRef.current;
if (!el) return;
// Mobile renders a non-pinned vertical stack (see the `isMobile` branch in
// render): no ScrollTrigger pin/scrub at all. Bail before creating one so
// pinState stays "before" and the section keeps its natural auto height.
if (isMobile) return;
gsap.registerPlugin(ScrollTrigger);
// NOTE: global Lenis (src/animations/SmoothScroll.tsx) is active on this
@@ -213,7 +266,29 @@ export default function OptimizationSection() {
clearTimeout(refresh);
st.kill();
};
}, [scroll]);
}, [scroll, isMobile]);
// Mobile ambient loop: with no scroll scrub, gently oscillate the shared
// progress inside the *optimized* band so the hologram stays "alive" and
// coherent with the final metrics. Held static under reduced-motion.
useEffect(() => {
if (!isMobile) return;
if (reduced) {
progressRef.current = 0.85;
return;
}
if (!sceneActive) return;
let raf = 0;
let start = 0;
const tick = (ts: number) => {
if (!start) start = ts;
const t = (ts - start) / 1000;
progressRef.current = 0.76 + Math.sin(t * 0.5) * 0.16; // ~0.60 → 0.92
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [isMobile, reduced, sceneActive]);
// Overlay reactions to scroll (no React re-render — direct DOM updates).
const leftOpacity = useTransform(scroll, [0.3, 0.55], [1, 0.32]);
@@ -231,9 +306,59 @@ export default function OptimizationSection() {
return (
<section
ref={containerRef}
className={`dm-opt is-${pinState}`}
className={`dm-opt is-${pinState}${isMobile ? " dm-opt--mobile" : ""}`}
aria-label="AI Logistics Optimization"
>
{/* ===== MOBILE: non-pinned vertical stack ===== */}
{isMobile && (
<div className="dm-opt-mobile">
<header className="dm-opt-mhead">
<div className="dm-opt-eyebrow">
<span className="dm-opt-dot" /> Doormile AI Control Tower
</div>
<h2>AI Logistics Optimization Engine</h2>
<p>
Watch Doormile&apos;s AI engine transform chaotic logistics into precision-optimized delivery networks reducing distance, fleet size, delays, and cost.
</p>
</header>
{/* 1. Without Optimization */}
<aside className="dm-opt-panel dm-opt-panel--bad dm-opt-mpanel">
<WithoutPanelBody />
</aside>
{/* 2. 3D Visualization (ambient) */}
<div className="dm-opt-mobile__scene">
{mountScene && (
<div className="dm-opt-canvas">
<OptimizationCanvas
progress={progressRef}
reduced={reduced}
isMobile
active={sceneActive}
/>
</div>
)}
<span className="dm-opt-mobile__scene-tag">
<span className="dm-opt-dot" /> Live AI optimization
</span>
</div>
{/* 3. With Doormile AI */}
<aside className="dm-opt-panel dm-opt-panel--good dm-opt-mpanel">
<WithPanelBody />
</aside>
{/* 4. Metrics — final optimized values, 2-col grid */}
<div className="dm-opt-mfoot">
<MetricsPanel scroll={staticFinal} />
<LiveInsightBar />
</div>
</div>
)}
{/* ===== DESKTOP / TABLET: pinned scroll experience ===== */}
{!isMobile && (
<div className="dm-opt-sticky">
<div className="dm-opt-card">
{/* Static backdrop (also the canvas loading state) */}
@@ -246,6 +371,7 @@ export default function OptimizationSection() {
progress={progressRef}
reduced={reduced}
isMobile={isMobile}
isTablet={isTablet}
// Only run the render loop while the section is actually pinned
// (filling the viewport). At a workflow seam two sections can both
// satisfy their activeIo margin; without the pin gate their two
@@ -375,35 +501,14 @@ export default function OptimizationSection() {
className="dm-opt-panel dm-opt-panel--bad"
style={{ opacity: leftOpacity, filter: leftFilter }}
>
<div className="dm-opt-panel__badge">
<span className="dm-opt-pulse dm-opt-pulse--red" /> System: Congested
</div>
<h3>Without Optimization</h3>
<ul>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> Chaotic overlapping routes</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> Duplicate &amp; idle trips</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> 8 vehicles required</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> 23 delivery delays</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> +18% cost overrun</li>
</ul>
<WithoutPanelBody />
</motion.aside>
<motion.aside
className="dm-opt-panel dm-opt-panel--good"
style={{ opacity: rightOpacity }}
>
<div className="dm-opt-panel__badge dm-opt-panel__badge--good">
<span className="dm-opt-pulse dm-opt-pulse--green" /> System: Optimized
</div>
<h3>With Doormile AI</h3>
<ul>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Optimized route clusters</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Intelligent vehicle assignment</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Multi-trip &amp; EV planning</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Zero delivery delays</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> 18% cost saved</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Carbon footprint reduced</li>
</ul>
<WithPanelBody />
</motion.aside>
</div>
@@ -417,6 +522,7 @@ export default function OptimizationSection() {
</div>
</div>
</div>
)}
<style>{styles}</style>
</section>
@@ -893,6 +999,98 @@ const styles = `
.dm-opt-insight__text { font-size: 8.5px; }
.dm-opt-insight__sep { height: 10px; }
}
/* ===== MOBILE STACKED LAYOUT (<=767px) — non-pinned vertical flow =====
Rendered by the isMobile branch as a normal-flow .dm-opt-mobile container
(header → Without → 3D → With → metrics). These rules come after the block
above so they win for the elements that actually exist on mobile. */
@media (max-width: 767px) {
/* Un-pin: natural height, no fixed sticky, no 200/230vh scroll runway.
(.dm-opt--mobile out-specifies the base .dm-opt height rules.) */
.dm-opt.dm-opt--mobile { height: auto; }
.dm-opt-mobile {
position: relative;
margin: 0 10px;
padding: 24px 13px 22px;
display: flex;
flex-direction: column;
gap: 14px;
box-sizing: border-box;
background: linear-gradient(180deg, #06101f 0%, #020617 55%, #030a18 100%);
border: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: none;
/* Flat bottom + flush so the Performance card (.dm-wf1-card) butts directly
against it as one continuous container (matches Workflow1 ≤767 styles). */
border-radius: 20px 20px 0 0;
overflow: hidden;
}
/* Header */
.dm-opt-mhead { text-align: center; padding: 0 4px; }
.dm-opt-mhead .dm-opt-eyebrow { font-size: 10px; }
.dm-opt-mhead h2 {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
margin: 10px 0 6px !important; padding: 0 !important; color: #F8FAFC !important;
font-weight: 700 !important; text-transform: none !important;
font-size: clamp(20px, 6.2vw, 26px) !important; line-height: 1.15 !important;
letter-spacing: -0.015em !important;
}
.dm-opt-mhead p {
margin: 0 auto !important; padding: 0 !important; color: ${COLORS.textDim} !important;
max-width: 40ch; font-size: 12.5px !important; line-height: 1.5 !important;
}
/* Comparison panels → full-width static cards, fully visible, readable.
Trim the heavy glow (box-shadow) but keep the colored border for identity. */
.dm-opt-mobile .dm-opt-panel {
width: 100%; box-sizing: border-box;
opacity: 1 !important; filter: none !important;
padding: 15px 16px; border-radius: 16px; box-shadow: none;
}
.dm-opt-mobile .dm-opt-panel h3 {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
font-size: 16px !important; margin: 9px 0 10px !important;
}
.dm-opt-mobile .dm-opt-panel ul { gap: 8px; }
.dm-opt-mobile .dm-opt-panel li { font-size: 12.5px !important; line-height: 1.35 !important; }
.dm-opt-mobile .dm-opt-marker { width: 18px; height: 18px; font-size: 10px; border-radius: 6px; }
.dm-opt-mobile .dm-opt-panel__badge { font-size: 9.5px; padding: 4px 9px; }
/* 3D visualization block — contained, ~40vh, premium but not dominant. */
.dm-opt-mobile__scene {
position: relative; width: 100%; height: 40vh; min-height: 260px; max-height: 360px;
border-radius: 16px; overflow: hidden;
background: radial-gradient(120% 90% at 50% 30%, ${rgba(COLORS.cyan, 0.06)} 0%, ${COLORS.bg} 70%);
border: 1px solid ${rgba(COLORS.cyan, 0.14)};
}
.dm-opt-mobile__scene .dm-opt-canvas { position: absolute; inset: 0; z-index: 0; }
.dm-opt-mobile__scene-tag {
position: absolute; left: 10px; top: 10px; z-index: 1;
display: inline-flex; align-items: center; gap: 6px;
font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 700;
color: #E2E8F0; padding: 4px 9px; border-radius: 999px;
background: ${rgba(COLORS.ink, 0.72)}; border: 1px solid ${rgba(COLORS.cyan, 0.22)};
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
/* Metrics → 2-col grid (5th card spans full width), final optimized values,
readable sizing so nothing truncates. */
.dm-opt-mfoot { padding: 0; }
.dm-opt-mobile .dm-opt-metrics {
grid-template-columns: repeat(2, 1fr); gap: 8px; max-width: none;
}
.dm-opt-mobile .dm-opt-metric { padding: 12px 13px 11px; border-radius: 12px; }
.dm-opt-mobile .dm-opt-metric:last-child { grid-column: 1 / -1; }
.dm-opt-mobile .dm-opt-metric__label { font-size: 10px; letter-spacing: 0.02em; }
.dm-opt-mobile .dm-opt-metric__value { font-size: clamp(20px, 6.5vw, 26px); }
/* Insight bar wraps instead of overflowing. */
.dm-opt-mobile .dm-opt-insight {
flex-wrap: wrap; max-width: none; gap: 6px 10px; padding: 8px 14px; margin-top: 4px;
}
.dm-opt-mobile .dm-opt-insight__text { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.dm-opt-pulse { animation: none; }
.dm-opt-metric { animation: none; opacity: 1; transform: none; }

View File

@@ -195,28 +195,37 @@ export default function WhyChooseDoormile() {
flex-direction: column;
gap: 16px;
}
.wcd-card-points li {
position: relative;
padding-left: 30px;
.wcd-section .wcd-card-points li {
/* Flex row so the check icon and its label always sit on the same line.
Scoped with .wcd-section to outrank the global ".logico-front-end ul li"
theme rule, which adds 1.7em padding + a fontello bullet icon. */
display: flex;
align-items: flex-start;
gap: 12px;
padding-left: 0;
font-size: 16px;
font-weight: 700;
line-height: 1.3;
color: #FFFFFF;
}
.wcd-card-points li::before {
content: "";
position: absolute;
left: 0;
top: 0.18em;
width: 14px;
height: 9px;
border-left: 2px solid #c01227;
border-bottom: 2px solid #c01227;
transform: rotate(-45deg) scale(1);
/* Suppress the theme's default fontello list bullet
(.logico-front-end ul li:before) so only our circle-check SVG renders. */
.wcd-section .wcd-card-points li::before {
content: none;
display: none;
}
/* Clean circle-check feature icon (inline SVG, see markup below) — replaces
the old border-based chevron. Brand red with thin, rounded strokes. */
.wcd-card-points .wcd-check {
flex: 0 0 auto;
width: 18px;
height: 18px;
margin-top: 0.12em;
color: #c01227;
transition: transform 0.3s ease;
}
.wcd-card:hover .wcd-card-points li::before {
transform: rotate(-45deg) scale(1.1);
.wcd-card:hover .wcd-card-points .wcd-check {
transform: scale(1.1);
}
@media (max-width: 1020px) {
@@ -266,7 +275,22 @@ export default function WhyChooseDoormile() {
<p className="wcd-card-desc">{stage.desc}</p>
<ul className="wcd-card-points">
{stage.points.map((point) => (
<li key={point}>{point}</li>
<li key={point}>
<svg
className="wcd-check"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
<span>{point}</span>
</li>
))}
</ul>
</div>

View File

@@ -1,12 +1,14 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import OptimizationSection from "../optimization/OptimizationSection";
export default function Workflow1() {
const [activeSlide, setActiveSlide] = useState(0);
const [paused, setPaused] = useState(false);
const [inView, setInView] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const slides = [
{
@@ -23,21 +25,36 @@ export default function Workflow1() {
}
];
// Always begin the storytelling sequence from slide 1 (01/03) on mount — never
// preserve a previous slide index across remounts / route changes back to MileTruth.
// Always begin on slide 1 (01/03) on mount. Scrolling away and back does NOT reset
// (the component stays mounted) — only a fresh page load / route change back to
// MileTruth re-mounts and restarts at slide 1.
useEffect(() => {
setActiveSlide(0);
}, []);
// Auto-advance the carousel every 4s, infinite loop. Keyed on activeSlide so any
// manual selection resets the timer; pauses while the user hovers the card.
// Autoplay is gated on visibility: it starts only once the slider card scrolls into
// view (not on page load) and stops when it leaves — without touching activeSlide,
// so returning to the section resumes from wherever it was, never snapping to slide 1.
useEffect(() => {
if (paused) return;
const el = cardRef.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => setInView(entry.isIntersecting),
{ threshold: 0.35 }
);
io.observe(el);
return () => io.disconnect();
}, []);
// Auto-advance every 10s, looping — but only while the card is in view and the user
// isn't hovering it. Keyed on activeSlide so a manual jump restarts the 10s dwell.
useEffect(() => {
if (!inView || paused) return;
const id = setTimeout(() => {
setActiveSlide((prev) => (prev + 1) % slides.length);
}, 4000);
}, 10000);
return () => clearTimeout(id);
}, [activeSlide, paused, slides.length]);
}, [activeSlide, inView, paused, slides.length]);
return (
<section className="dm-wf1" aria-label="Workflow 1 — Impact of Optimisation & Performance">
@@ -47,7 +64,7 @@ export default function Workflow1() {
{/* ── Bottom sub-section: Performance content, flush + colour-matched to the
optimisation section above so the whole workflow reads as one container ── */}
<div className="dm-wf1-card" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
<div className="dm-wf1-card" ref={cardRef} onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">

View File

@@ -1,12 +1,14 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import LogisticsBrainSection from "../logisticsbrain/LogisticsBrainSection";
export default function Workflow2() {
const [activeSlide, setActiveSlide] = useState(0);
const [paused, setPaused] = useState(false);
const [inView, setInView] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const slides = [
{
@@ -23,21 +25,36 @@ export default function Workflow2() {
}
];
// Always begin the storytelling sequence from slide 1 (01/03) on mount — never
// preserve a previous slide index across remounts / route changes back to MileTruth.
// Always begin on slide 1 (01/03) on mount. Scrolling away and back does NOT reset
// (the component stays mounted) — only a fresh page load / route change back to
// MileTruth re-mounts and restarts at slide 1.
useEffect(() => {
setActiveSlide(0);
}, []);
// Auto-advance the carousel every 4s, infinite loop. Keyed on activeSlide so any
// manual selection resets the timer; pauses while the user hovers the card.
// Autoplay is gated on visibility: it starts only once the slider card scrolls into
// view (not on page load) and stops when it leaves — without touching activeSlide,
// so returning to the section resumes from wherever it was, never snapping to slide 1.
useEffect(() => {
if (paused) return;
const el = cardRef.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => setInView(entry.isIntersecting),
{ threshold: 0.35 }
);
io.observe(el);
return () => io.disconnect();
}, []);
// Auto-advance every 10s, looping — but only while the card is in view and the user
// isn't hovering it. Keyed on activeSlide so a manual jump restarts the 10s dwell.
useEffect(() => {
if (!inView || paused) return;
const id = setTimeout(() => {
setActiveSlide((prev) => (prev + 1) % slides.length);
}, 4000);
}, 10000);
return () => clearTimeout(id);
}, [activeSlide, paused, slides.length]);
}, [activeSlide, inView, paused, slides.length]);
return (
<section className="dm-wf2" aria-label="Workflow 2 — How Our Logistics Brain Works & Innovation">
@@ -47,7 +64,7 @@ export default function Workflow2() {
{/* ── Bottom sub-section: Innovation content, flush + colour-matched to the
logistics-brain card above so the whole workflow reads as one container ── */}
<div className="dm-wf2-card" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
<div className="dm-wf2-card" ref={cardRef} onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">

View File

@@ -1,12 +1,14 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import StrategySection from "../strategy/StrategySection";
export default function Workflow3() {
const [activeSlide, setActiveSlide] = useState(0);
const [paused, setPaused] = useState(false);
const [inView, setInView] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const slides = [
{
@@ -23,21 +25,36 @@ export default function Workflow3() {
}
];
// Always begin the storytelling sequence from slide 1 (01/03) on mount — never
// preserve a previous slide index across remounts / route changes back to MileTruth.
// Always begin on slide 1 (01/03) on mount. Scrolling away and back does NOT reset
// (the component stays mounted) — only a fresh page load / route change back to
// MileTruth re-mounts and restarts at slide 1.
useEffect(() => {
setActiveSlide(0);
}, []);
// Auto-advance the carousel every 4s, infinite loop. Keyed on activeSlide so any
// manual selection resets the timer; pauses while the user hovers the card.
// Autoplay is gated on visibility: it starts only once the slider card scrolls into
// view (not on page load) and stops when it leaves — without touching activeSlide,
// so returning to the section resumes from wherever it was, never snapping to slide 1.
useEffect(() => {
if (paused) return;
const el = cardRef.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => setInView(entry.isIntersecting),
{ threshold: 0.35 }
);
io.observe(el);
return () => io.disconnect();
}, []);
// Auto-advance every 10s, looping — but only while the card is in view and the user
// isn't hovering it. Keyed on activeSlide so a manual jump restarts the 10s dwell.
useEffect(() => {
if (!inView || paused) return;
const id = setTimeout(() => {
setActiveSlide((prev) => (prev + 1) % slides.length);
}, 4000);
}, 10000);
return () => clearTimeout(id);
}, [activeSlide, paused, slides.length]);
}, [activeSlide, inView, paused, slides.length]);
return (
<section className="dm-wf3" aria-label="Workflow 3 — Happier Riders. Higher Fulfillment. & Strategy">
@@ -49,7 +66,7 @@ export default function Workflow3() {
{/* ── Bottom sub-section: Strategy content, flush + pulled up to butt against
the 3D card's flat bottom so the whole workflow reads as one container —
the same connected structure used in Workflow 1 & 2 ── */}
<div className="dm-wf3-card" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
<div className="dm-wf3-card" ref={cardRef} onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">

View File

@@ -11,7 +11,7 @@
/** Base classes shared by every page — safe to SSR before route is known. */
export const SHARED_BODY_CLASSES =
"wp-singular page-template-default page wp-theme-logico wp-child-theme-logico-child theme-logico woocommerce-no-js ehf-header ehf-footer ehf-template-logico ehf-stylesheet-logico-child logico-front-end logico-theme-style-rounded elementor-default elementor-kit-5 elementor-page";
"wp-singular page-template-default page wp-theme-logico wp-child-theme-logico-child theme-logico ehf-header ehf-footer ehf-template-logico ehf-stylesheet-logico-child logico-front-end logico-theme-style-rounded elementor-default elementor-kit-5 elementor-page";
const SHARED = SHARED_BODY_CLASSES;
@@ -19,7 +19,7 @@ const ROUTE_CLASSES: Record<string, string> = {
"/": `home-page ${SHARED} page-id-61 elementor-page-61 is-home-page`,
// PHP source quirk: how-it-works omits elementor-kit-5
"/how-it-works":
"wp-singular page-template-default page page-id-59 wp-theme-logico wp-child-theme-logico-child theme-logico woocommerce-no-js ehf-header ehf-footer ehf-template-logico ehf-stylesheet-logico-child logico-front-end logico-theme-style-rounded elementor-default elementor-page elementor-page-59",
"wp-singular page-template-default page page-id-59 wp-theme-logico wp-child-theme-logico-child theme-logico ehf-header ehf-footer ehf-template-logico ehf-stylesheet-logico-child logico-front-end logico-theme-style-rounded elementor-default elementor-page elementor-page-59",
"/miletruth": `${SHARED} page-id-59 elementor-page-59`,
"/solutions": `${SHARED} page-id-59 elementor-page-59`,
// PHP source quirk: about-us.php has `home` class (upstream bug, preserved)