update screen fix
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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 & 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'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 & 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 & 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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user