Compare commits
14 Commits
b2d64bd335
...
bharath-ho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45b4e7a109 | ||
| 0ef51540e9 | |||
| 3d53f82e7b | |||
| e93785f2b6 | |||
| 263e03937f | |||
| bbe8f0d92b | |||
| 8862ad2cb3 | |||
| a16d51f2fa | |||
| ab67fec091 | |||
| 91841ba3f4 | |||
| 2f23f16634 | |||
| 7fb97a9ca6 | |||
| 3a16bf9267 | |||
| d5987b5dd1 |
0
3d_scene_final.jsx
Normal file
0
3d_scene_final.jsx
Normal file
@@ -14,6 +14,9 @@ const eslintConfig = defineConfig([
|
||||
"next-env.d.ts",
|
||||
// Vendored third-party JS shipped to /public is not ours to lint.
|
||||
"public/**",
|
||||
// Ported 3D experience (incl. the ~11.6k-line gltfjsx-generated model) — kept
|
||||
// as faithful .jsx/.js from the standalone app; not linted to ours rules.
|
||||
"src/modules/how-it-works-3d/**",
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
// Required by the How It Works 3D experience. React StrictMode double-invokes
|
||||
// mount/effects in dev, which tears down and re-creates the WebGL context of
|
||||
// the heavy 32MB scene mid-initialization — the context is lost ("THREE.
|
||||
// WebGLRenderer: Context Lost") and the canvas stays blank. This is a known
|
||||
// React-Three-Fiber + StrictMode incompatibility. Disabling it is a DEV-ONLY
|
||||
// change (production never runs StrictMode's double-mount) and does not affect
|
||||
// any other page's runtime behavior.
|
||||
reactStrictMode: false,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
};
|
||||
|
||||
97
package-lock.json
generated
97
package-lock.json
generated
@@ -8,26 +8,32 @@
|
||||
"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",
|
||||
"framer-motion": "^12.40.0",
|
||||
"gsap": "^3.15.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lenis": "^1.3.23",
|
||||
"maath": "^0.10.8",
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"three": "^0.171.0"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"three": "0.171.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/three": "^0.171.0",
|
||||
"@types/three": "^0.184.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"jest": "^30.4.2",
|
||||
@@ -722,6 +728,21 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"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",
|
||||
@@ -2267,6 +2288,17 @@
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-leaflet/core": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
|
||||
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/drei": {
|
||||
"version": "10.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
|
||||
@@ -2893,6 +2925,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
@@ -2986,6 +3025,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||
@@ -3044,17 +3093,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.171.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.171.0.tgz",
|
||||
"integrity": "sha512-oLuT1SAsT+CUg/wxUTFHo0K3NtJLnx9sJhZWQJp/0uXqFpzSk1hRHmvWvpaAWSfvx2db0lVKZ5/wV0I0isD2mQ==",
|
||||
"version": "0.184.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
|
||||
"integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": "*",
|
||||
"@webgpu/types": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~0.18.1"
|
||||
"meshoptimizer": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
@@ -3720,12 +3769,6 @@
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.70",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz",
|
||||
"integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -8215,6 +8258,12 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/lenis": {
|
||||
"version": "1.3.23",
|
||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz",
|
||||
@@ -8707,9 +8756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
|
||||
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
@@ -9652,6 +9701,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"dependencies": {
|
||||
"@react-leaflet/core": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-measure": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
|
||||
|
||||
11
package.json
11
package.json
@@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build:css": "bash scripts/build-css.sh",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"test": "jest",
|
||||
@@ -12,26 +13,32 @@
|
||||
"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",
|
||||
"framer-motion": "^12.40.0",
|
||||
"gsap": "^3.15.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lenis": "^1.3.23",
|
||||
"maath": "^0.10.8",
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"three": "^0.171.0"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"three": "0.171.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/three": "^0.171.0",
|
||||
"@types/three": "^0.184.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"jest": "^30.4.2",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
4622
public/css/custom-frontend.min.css
vendored
4622
public/css/custom-frontend.min.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
/* ── Blog page hero ── */
|
||||
.blog-hero {
|
||||
background: linear-gradient(160deg, #0c0c14 0%, #18050a 100%);
|
||||
padding: 120px 0 60px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.blog-hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 50% at 50% 60%, rgba(192,18,39,0.14) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.blog-hero-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 32px;
|
||||
}
|
||||
.blog-hero-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3.5px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.35);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.blog-hero-eyebrow::before,
|
||||
.blog-hero-eyebrow::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
.blog-hero h1 {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(28px, 4.5vw, 52px);
|
||||
font-weight: 900;
|
||||
color: #ffffff !important;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -1.5px;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
.blog-hero h1 span { color: #c01227; }
|
||||
.blog-hero p {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 15px;
|
||||
color: rgba(255,255,255,0.48);
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Blog grid section ── */
|
||||
.blog-section {
|
||||
background: #f8fafc;
|
||||
padding: 80px 0 100px;
|
||||
}
|
||||
.blog-container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
/* ── Blog grid ── */
|
||||
.dm-blog-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
/* ── Blog card ── */
|
||||
.dm-blog-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0,0,0,0.07);
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.05);
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dm-blog-card:hover {
|
||||
transform: translateY(-12px) scale(1.02);
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15), 0 0 20px rgba(192, 18, 39, 0.1);
|
||||
}
|
||||
.dm-blog-card-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
overflow: hidden;
|
||||
background: #eee;
|
||||
}
|
||||
.dm-blog-card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
.dm-blog-card:hover .dm-blog-card-image img {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
.dm-blog-card-body {
|
||||
padding: 20px 22px 24px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dm-blog-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dm-blog-category {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.8px;
|
||||
color: #c01227;
|
||||
background: rgba(192,18,39,0.07);
|
||||
border: 1px solid rgba(192,18,39,0.15);
|
||||
border-radius: 100px;
|
||||
padding: 3px 9px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dm-blog-date {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.dm-blog-card h3 {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.2px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.dm-blog-card p {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.65;
|
||||
margin: 0 0 18px;
|
||||
flex: 1;
|
||||
}
|
||||
.dm-blog-read-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #c01227;
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.2px;
|
||||
margin-top: auto;
|
||||
transition: gap 0.2s ease;
|
||||
}
|
||||
.dm-blog-read-more:hover { gap: 9px; }
|
||||
.dm-blog-read-more svg { flex-shrink: 0; }
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 1024px) {
|
||||
.dm-blog-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.dm-blog-grid { grid-template-columns: 1fr; }
|
||||
.blog-hero { padding: 110px 0 60px; }
|
||||
.blog-container { padding: 0 20px; }
|
||||
.blog-section { padding: 60px 0 80px; }
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
.tax-product_brand .brand-description {
|
||||
overflow: hidden;
|
||||
zoom:1}
|
||||
|
||||
.tax-product_brand .brand-description img.brand-thumbnail {
|
||||
width: 25%;
|
||||
float: right
|
||||
}
|
||||
|
||||
.tax-product_brand .brand-description .text {
|
||||
width: 72%;
|
||||
float: left
|
||||
}
|
||||
|
||||
.widget_brand_description img {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
margin: 0 0 1em
|
||||
}
|
||||
|
||||
ul.brand-thumbnails {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
clear: both;
|
||||
list-style: none
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:before {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table
|
||||
}
|
||||
|
||||
ul.brand-thumbnails li {
|
||||
float: left;
|
||||
margin: 0 3.8% 1em 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 22.05%
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.fluid-columns li {
|
||||
width: auto
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.first {
|
||||
clear: both
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.last {
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-1 li {
|
||||
width: 100%;
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-2 li {
|
||||
width: 48%
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-3 li {
|
||||
width: 30.75%
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-5 li {
|
||||
width: 16.95%
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-6 li {
|
||||
width: 13.5%
|
||||
}
|
||||
|
||||
.brand-thumbnails li img {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
ul.brand-thumbnails:not(.fluid-columns) li {
|
||||
width:48%!important
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.first {
|
||||
clear: none
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.last {
|
||||
margin-right: 3.8%
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(odd) {
|
||||
clear: both
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(even) {
|
||||
margin-right: 0
|
||||
}
|
||||
}
|
||||
|
||||
.brand-thumbnails-description li {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.brand-thumbnails-description li .term-thumbnail img {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.brand-thumbnails-description li .term-description {
|
||||
margin-top: 1em;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
#brands_a_z h3:target {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
ul.brands_index {
|
||||
list-style: none outside;
|
||||
overflow: hidden;
|
||||
zoom:1}
|
||||
|
||||
ul.brands_index li {
|
||||
float: left;
|
||||
margin: 0 2px 2px 0
|
||||
}
|
||||
|
||||
ul.brands_index li a,ul.brands_index li span {
|
||||
border: 1px solid #ccc;
|
||||
padding: 6px;
|
||||
line-height: 1em;
|
||||
float: left;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
ul.brands_index li span {
|
||||
border-color: #eee;
|
||||
color: #ddd
|
||||
}
|
||||
|
||||
ul.brands_index li a:hover {
|
||||
border-width: 2px;
|
||||
padding: 5px;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
ul.brands_index li a.active {
|
||||
border-width: 2px;
|
||||
padding: 5px
|
||||
}
|
||||
|
||||
div#brands_a_z a.top {
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
line-height: 1em;
|
||||
float: right;
|
||||
text-decoration: none;
|
||||
font-size: .8em
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
/* EV Section Styles */
|
||||
:root {
|
||||
--ev-primary: #111111;
|
||||
--ev-accent: #E31E24;
|
||||
--ev-accent-soft: rgba(227, 30, 36, 0.1);
|
||||
--ev-text-muted: #64748B;
|
||||
--ev-bg-light: #F8FAFC;
|
||||
--ev-card-bg: rgba(255, 255, 255, 0.8);
|
||||
--ev-radius-lg: 32px;
|
||||
--ev-radius-md: 20px;
|
||||
--ev-radius-sm: 12px;
|
||||
--ev-shadow-premium: 0 20px 50px rgba(0, 0, 0, 0.06);
|
||||
--ev-glass-border: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.ev-section {
|
||||
padding: 120px 0;
|
||||
background-color: #FDFDFD;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Background Blobs */
|
||||
.ev-section::before,
|
||||
.ev-section::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
z-index: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.ev-section::before {
|
||||
background: radial-gradient(circle, #E31E24 0%, transparent 70%);
|
||||
top: -200px;
|
||||
right: -200px;
|
||||
}
|
||||
|
||||
.ev-section::after {
|
||||
background: radial-gradient(circle, #3B82F6 0%, transparent 70%);
|
||||
bottom: -200px;
|
||||
left: -200px;
|
||||
}
|
||||
|
||||
.ev-container {
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ev-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1.2fr;
|
||||
gap: 80px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left Content */
|
||||
.ev-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 18px;
|
||||
border-radius: 100px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 32px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
color: #1E293B;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.ev-title {
|
||||
font-size: clamp(36px, 5vw, 64px);
|
||||
font-weight: 900;
|
||||
line-height: 1.05;
|
||||
color: var(--ev-primary);
|
||||
margin-bottom: 28px;
|
||||
letter-spacing: -1.5px;
|
||||
}
|
||||
|
||||
.ev-title .accent {
|
||||
color: var(--ev-accent);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ev-desc {
|
||||
font-size: 18px;
|
||||
color: var(--ev-text-muted);
|
||||
line-height: 1.7;
|
||||
max-width: 520px;
|
||||
margin-bottom: 56px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Feature Grid */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--ev-card-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
padding: 24px;
|
||||
border-radius: var(--ev-radius-md);
|
||||
border: 1px solid var(--ev-glass-border);
|
||||
box-shadow: var(--ev-shadow-premium);
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-12px) scale(1.02);
|
||||
border-color: rgba(227, 30, 36, 0.3);
|
||||
box-shadow: 0 30px 70px rgba(227, 30, 36, 0.2), 0 0 30px rgba(227, 30, 36, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon-box {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ev-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 13px;
|
||||
color: var(--ev-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Right Dashboard */
|
||||
.ev-dashboard {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: var(--ev-radius-lg);
|
||||
padding: 40px;
|
||||
box-shadow: 0 40px 100px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
color: var(--ev-primary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
padding: 6px 14px;
|
||||
border-radius: 100px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.live-indicator::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #10B981;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(0.95); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.6; }
|
||||
100% { transform: scale(0.95); opacity: 1; }
|
||||
}
|
||||
|
||||
.van-display {
|
||||
position: relative;
|
||||
margin: 60px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.van-image {
|
||||
width: 80%;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 30px 40px rgba(0,0,0,0.1));
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.van-display:hover .van-image {
|
||||
transform: scale(1.05) rotate(-2deg);
|
||||
}
|
||||
|
||||
/* Dashboard Overlays */
|
||||
.overlay-card {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 12px 18px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
z-index: 2;
|
||||
border: 1px solid rgba(255, 255, 255, 1);
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
|
||||
.overlay-card.v-status-1 { top: 0%; left: -10%; animation-delay: 0s; }
|
||||
.overlay-card.v-status-2 { top: -15%; right: 0%; animation-delay: 1s; }
|
||||
.overlay-card.v-status-3 { bottom: 10%; left: -5%; animation-delay: 2s; }
|
||||
.overlay-card.v-status-4 { bottom: -5%; right: -10%; animation-delay: 1.5s; }
|
||||
|
||||
.status-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #F1F5F9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overlay-card .info h4 {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.overlay-card .info p {
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
color: var(--ev-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #E2E8F0;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.progress-ring.success { border-color: #10B981; color: #10B981; }
|
||||
.progress-ring.warning { border-color: #F59E0B; color: #F59E0B; }
|
||||
|
||||
/* Stats Row */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.stat-metric {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
padding: 20px 10px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid #F1F5F9;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-metric:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--ev-accent);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
color: var(--ev-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: var(--ev-text-muted);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 1280px) {
|
||||
.ev-grid { gap: 40px; }
|
||||
.overlay-card.v-status-1 { left: 0; }
|
||||
.overlay-card.v-status-2 { right: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.ev-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.ev-content {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ev-desc {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ev-dashboard {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ev-section { padding: 80px 0; }
|
||||
.feature-grid { grid-template-columns: 1fr; }
|
||||
.stats-row { grid-template-columns: 1fr 1fr; }
|
||||
.dashboard-card { padding: 30px 20px; }
|
||||
.ev-title { font-size: 40px; }
|
||||
.overlay-card { transform: scale(0.8); }
|
||||
.v-status-1, .v-status-2, .v-status-3, .v-status-4 { position: static; margin-bottom: 10px; }
|
||||
.van-display { margin: 20px 0; }
|
||||
}
|
||||
/* EV2 Section - Premium Redesign */
|
||||
:root {
|
||||
--ev2-primary: #0A0A0B;
|
||||
--ev2-accent: #E31E24;
|
||||
--ev2-accent-glow: rgba(227, 30, 36, 0.4);
|
||||
--ev2-text: #FFFFFF;
|
||||
--ev2-text-muted: #A1A1AA;
|
||||
--ev2-card-bg: rgba(23, 23, 23, 0.7);
|
||||
--ev2-border: rgba(255, 255, 255, 0.1);
|
||||
--ev2-radius: 24px;
|
||||
}
|
||||
|
||||
.ev2-section {
|
||||
padding: 120px 0;
|
||||
background-color: var(--ev2-primary);
|
||||
color: var(--ev2-text);
|
||||
font-family: 'Outfit', 'Manrope', sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Abstract Background Elements */
|
||||
.ev2-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
right: -10%;
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
background: radial-gradient(circle, var(--ev2-accent-glow) 0%, transparent 70%);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ev2-container {
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 0 30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ev2-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 80px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left Content */
|
||||
.ev2-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 100px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
background: rgba(227, 30, 36, 0.1);
|
||||
color: var(--ev2-accent);
|
||||
border: 1px solid rgba(227, 30, 36, 0.2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.ev2-badge::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--ev2-accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--ev2-accent);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.5); opacity: 0.5; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.ev2-title {
|
||||
font-size: clamp(40px, 6vw, 72px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
margin-bottom: 30px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ev2-title .glow {
|
||||
color: var(--ev2-accent);
|
||||
display: block;
|
||||
text-shadow: 0 0 30px var(--ev2-accent-glow);
|
||||
}
|
||||
|
||||
.ev2-desc {
|
||||
font-size: 20px;
|
||||
color: var(--ev2-text-muted);
|
||||
line-height: 1.6;
|
||||
max-width: 580px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* Feature Stack */
|
||||
.ev2-features {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ev2-feature-item {
|
||||
padding: 24px;
|
||||
background: var(--ev2-card-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--ev2-border);
|
||||
border-radius: 20px;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.ev2-feature-item:hover {
|
||||
transform: translateY(-8px);
|
||||
border-color: rgba(227, 30, 36, 0.4);
|
||||
background: rgba(23, 23, 23, 0.9);
|
||||
}
|
||||
|
||||
.ev2-icon-wrap {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
color: var(--ev2-accent);
|
||||
}
|
||||
|
||||
.ev2-feature-item h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.ev2-feature-item p {
|
||||
font-size: 14px;
|
||||
color: var(--ev2-text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Right Side - Visual Interface */
|
||||
.ev2-visual-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ev2-main-card {
|
||||
background: linear-gradient(135deg, #171719 0%, #0A0A0B 100%);
|
||||
border-radius: 32px;
|
||||
padding: 40px;
|
||||
border: 1px solid var(--ev2-border);
|
||||
box-shadow: 0 40px 100px rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ev2-main-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(227, 30, 36, 0.05) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ev2-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.ev2-tagline {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: var(--ev2-accent);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ev2-id {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.ev2-status-pill {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10B981;
|
||||
padding: 6px 14px;
|
||||
border-radius: 100px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ev2-image-box {
|
||||
position: relative;
|
||||
margin: 30px 0;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ev2-image-box img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.8s ease;
|
||||
}
|
||||
|
||||
.ev2-main-card:hover .ev2-image-box img {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Floating HUD elements */
|
||||
.ev2-hud {
|
||||
position: absolute;
|
||||
padding: 15px;
|
||||
background: rgba(10, 10, 11, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.hud-1 {
|
||||
top: 30%;
|
||||
right: -20px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.hud-2 {
|
||||
bottom: 20%;
|
||||
left: -20px;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.hud-label {
|
||||
font-size: 10px;
|
||||
color: var(--ev2-text-muted);
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.hud-value {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hud-progress {
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.hud-bar {
|
||||
height: 100%;
|
||||
background: var(--ev2-accent);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 10px var(--ev2-accent);
|
||||
}
|
||||
|
||||
/* Bottom Metrics */
|
||||
.ev2-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.metric-box {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.m-val {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.m-label {
|
||||
font-size: 11px;
|
||||
color: var(--ev2-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 1024px) {
|
||||
.ev2-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 60px;
|
||||
}
|
||||
|
||||
.ev2-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ev2-desc {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ev2-features {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ev2-visual-wrap {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hud-1, .hud-2 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ev2-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ev2-title {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.ev2-metrics {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,464 +0,0 @@
|
||||
/* EV Premium Section - High-End SaaS UI */
|
||||
:root {
|
||||
--evp-bg: #030303;
|
||||
--evp-card-bg: rgba(20, 20, 22, 0.6);
|
||||
--evp-accent: #E31E24;
|
||||
--evp-accent-glow: rgba(227, 30, 36, 0.4);
|
||||
--evp-success: #10B981;
|
||||
--evp-info: #3B82F6;
|
||||
--evp-text: #FFFFFF;
|
||||
--evp-text-dim: #A1A1AA;
|
||||
--evp-border: rgba(255, 255, 255, 0.08);
|
||||
--evp-glass-border: rgba(255, 255, 255, 0.12);
|
||||
--evp-radius-lg: 32px;
|
||||
--evp-radius-md: 20px;
|
||||
--evp-font: 'Manrope', -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.evp-section {
|
||||
padding: 140px 0;
|
||||
background-color: #1f1f1f;
|
||||
color: var(--evp-text);
|
||||
font-family: var(--evp-font);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ─── BACKGROUND EFFECTS ─── */
|
||||
.evp-bg-aura {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border-radius: 50%;
|
||||
filter: blur(150px);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.aura-red {
|
||||
background: radial-gradient(circle, var(--evp-accent) 0%, transparent 70%);
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.logico-front-end h4:not([class*=logico-title-h]){
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
|
||||
.aura-blue {
|
||||
background: radial-gradient(circle, var(--evp-info) 0%, transparent 70%);
|
||||
bottom: -100px;
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
.evp-grid-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
mask-image: radial-gradient(circle at center, black, transparent 80%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.evp-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.evp-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 80px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ─── LEFT: CONTENT & HERO ─── */
|
||||
.evp-content {
|
||||
flex: 1;
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
.evp-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(227, 30, 36, 0.1);
|
||||
border: 1px solid rgba(227, 30, 36, 0.2);
|
||||
border-radius: 100px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--evp-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.evp-tag .status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--evp-accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--evp-accent);
|
||||
animation: evp-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes evp-pulse {
|
||||
0% { transform: scale(0.95); opacity: 1; }
|
||||
50% { transform: scale(1.3); opacity: 0.5; }
|
||||
100% { transform: scale(0.95); opacity: 1; }
|
||||
}
|
||||
|
||||
.evp-title {
|
||||
font-size: clamp(40px, 5vw, 68px);
|
||||
line-height: 1.05;
|
||||
font-weight: 800;
|
||||
letter-spacing: -2px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.evp-title span {
|
||||
display: block;
|
||||
color: var(--evp-accent);
|
||||
filter: drop-shadow(0 0 20px var(--evp-accent-glow));
|
||||
}
|
||||
|
||||
.evp-desc {
|
||||
font-size: 20px;
|
||||
color: var(--evp-text-dim);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 50px;
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
/* Feature Cards Stack */
|
||||
.evp-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.evp-feature-card {
|
||||
padding: 30px;
|
||||
background: var(--evp-card-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--evp-glass-border);
|
||||
border-radius: var(--evp-radius-md);
|
||||
transition: all 0.4s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.evp-feature-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, transparent 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
.evp-feature-card:hover {
|
||||
transform: translateY(-5px) scale(1.02);
|
||||
border-color: rgba(227, 30, 36, 0.4);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.evp-feature-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.evp-icon-box {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
color: var(--evp-accent);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.evp-feature-card h3 {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.evp-feature-card p {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ─── RIGHT: VISUAL DASHBOARD ─── */
|
||||
.evp-visual {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.evp-dashboard {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, transparent 100%);
|
||||
border: 1px solid var(--evp-border);
|
||||
border-radius: 40px;
|
||||
padding: 50px;
|
||||
position: relative;
|
||||
box-shadow: 0 50px 100px rgba(0,0,0,0.8);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.evp-dashboard::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 40px;
|
||||
padding: 1px;
|
||||
background: linear-gradient(to bottom right, rgba(255,255,255,0.1), transparent, rgba(255,255,255,0.05));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.evp-db-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.evp-db-title {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: var(--evp-text-dim);
|
||||
}
|
||||
|
||||
.evp-live-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--evp-success);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.evp-db-id {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.evp-van-stage {
|
||||
position: relative;
|
||||
margin: 40px 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.evp-van-image {
|
||||
width: 110%;
|
||||
height: auto;
|
||||
/* margin-left: -5%; */
|
||||
filter: drop-shadow(0 20px 30px rgba(0,0,0,0.5));
|
||||
animation: evp-float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes evp-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-15px); }
|
||||
}
|
||||
|
||||
/* Floating HUD Cards */
|
||||
.evp-hud-card {
|
||||
position: absolute;
|
||||
background: rgba(15, 15, 18, 0.85);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
|
||||
pointer-events: all;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.evp-hud-card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.hud-battery {
|
||||
top: 55%;
|
||||
left: -40px;
|
||||
min-width: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.hud-location {
|
||||
top: 15%;
|
||||
right: -30px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.hud-route {
|
||||
bottom: 30%;
|
||||
right: -50px;
|
||||
min-width: 170px;
|
||||
background: linear-gradient(135deg, rgba(20, 20, 24, 0.9), rgba(10, 10, 12, 0.9));
|
||||
}
|
||||
|
||||
.evp-progress-svg {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.evp-progress-circle-bg {
|
||||
fill: none;
|
||||
stroke: rgba(255,255,255,0.05);
|
||||
stroke-width: 4;
|
||||
}
|
||||
|
||||
.evp-progress-circle {
|
||||
fill: none;
|
||||
stroke: var(--evp-success);
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 113;
|
||||
stroke-dashoffset: 20; /* Example for 82% */
|
||||
transition: stroke-dashoffset 1s ease-out;
|
||||
}
|
||||
|
||||
.hud-val-large {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hud-label-small {
|
||||
font-size: 10px;
|
||||
color: var(--evp-text-dim);
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Route Visual in HUD */
|
||||
.hud-route-path {
|
||||
margin-top: 10px;
|
||||
height: 30px;
|
||||
background-image: radial-gradient(circle, rgba(255,255,255,0.2) 1px, transparent 1px);
|
||||
background-size: 8px 8px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hud-route-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
width: 60%;
|
||||
height: 2px;
|
||||
background: var(--evp-accent);
|
||||
box-shadow: 0 0 10px var(--evp-accent);
|
||||
}
|
||||
|
||||
/* Dashboard Bottom Metrics */
|
||||
.evp-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
border-top: 1px solid var(--evp-border);
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.m-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.m-label {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
color: var(--evp-text-dim);
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.m-value {
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ─── RESPONSIVENESS ─── */
|
||||
@media (max-width: 1300px) {
|
||||
.evp-layout { gap: 40px; }
|
||||
.hud-battery { left: 0; }
|
||||
.hud-location { right: 0; }
|
||||
.hud-route { right: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.evp-layout {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.evp-content {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.evp-desc {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.evp-features {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.evp-visual {
|
||||
margin-top: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.evp-section { padding: 80px 0; }
|
||||
.evp-features { grid-template-columns: 1fr; }
|
||||
.evp-metrics { grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.evp-dashboard { padding: 30px 20px; }
|
||||
.evp-hud-card { display: contents; }
|
||||
.evp-title { font-size: 38px; }
|
||||
.evp-db-id { font-size: 24px; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,562 +0,0 @@
|
||||
/*
|
||||
* section-miletruth.css
|
||||
* Extends the existing ev-section & evp-section patterns.
|
||||
* Only adds what the existing CSS files don't already provide.
|
||||
*/
|
||||
|
||||
/* ── Override ev-section font to match site Manrope ── */
|
||||
.ev-section,
|
||||
.ev-section .ev-title,
|
||||
.ev-section .ev-desc,
|
||||
.ev-section .ev-badge,
|
||||
.ev-section .feature-card h3,
|
||||
.ev-section .feature-card p {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
}
|
||||
|
||||
/* ── Remove the decorative blobs (they show as colored arcs at section edges) ── */
|
||||
.ev-section::before,
|
||||
.ev-section::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Column widths: image always gets the large column ──
|
||||
Edge (non-rev): content first → col1 small, image → col2 large
|
||||
Impact/Fulfillment (rev): image first → col1 large, content → col2 small ── */
|
||||
.ev-section .ev-grid {
|
||||
grid-template-columns: 0.42fr 2.2fr;
|
||||
}
|
||||
.ev-section .ev-grid.ev-grid--rev {
|
||||
grid-template-columns: 2.2fr 0.42fr;
|
||||
}
|
||||
.ev-section .ev-grid,
|
||||
.ev-section .ev-grid.ev-grid--rev {
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── Widen the container on this page to give images more room ── */
|
||||
.ev-section .ev-container {
|
||||
max-width: 1440px;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
/* ── Slightly smaller body text so images feel dominant ── */
|
||||
.ev-section .ev-desc {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.65;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
/* ── Hero slider dark background (no Elementor CSS on this page) ── */
|
||||
/* .miletruth-hero .content-slider-wrapper {
|
||||
background: linear-gradient(160deg, #0c0c14 0%, #18050a 100%);
|
||||
} */
|
||||
|
||||
/* ── Remove the default 90px top margin so hero starts at y:0, covering the
|
||||
white gap that would otherwise show behind the transparent header ── */
|
||||
.miletruth-hero .content-slider .slide-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.miletruth-hero .content-slider .slide-content-inner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 980px;
|
||||
padding: 0 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.miletruth-hero .content-slider-item-heading,
|
||||
.miletruth-hero .content-slider-item-heading .heading-content {
|
||||
color: rgba(255, 255, 255, 0.92) !important;
|
||||
font-size: 72px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.miletruth-hero .content-slider-item-text,
|
||||
.miletruth-hero .content-slider-item-text .text-content,
|
||||
.miletruth-hero .content-slider-item-text p {
|
||||
color: rgba(255, 255, 255, 0.72) !important;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content-wrapper.miletruth-hero {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
/* ── Image-left layouts: image is first child in HTML, large column is first ── */
|
||||
.ev-grid--rev {
|
||||
grid-template-columns: 2.2fr 0.42fr;
|
||||
}
|
||||
/* No order override needed — HTML source order already places image first (left, large) */
|
||||
|
||||
|
||||
/* ── Dark section override on ev-section ── */
|
||||
.ev-section--dark {
|
||||
background: #1a1a1f !important;
|
||||
}
|
||||
.ev-section--dark .ev-title { color: #ffffff; }
|
||||
.ev-section--dark .ev-desc { color: rgba(255, 255, 255, 0.55); }
|
||||
.ev-section--dark .ev-badge {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-color: rgba(255,255,255,0.12);
|
||||
color: rgba(255,255,255,0.7);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ev-section--dark .feature-card {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-color: rgba(255,255,255,0.08);
|
||||
}
|
||||
.ev-section--dark .feature-card:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-color: rgba(227,30,36,0.3);
|
||||
}
|
||||
.ev-section--dark .feature-card h3 { color: #ffffff; }
|
||||
.ev-section--dark .feature-card p { color: rgba(255,255,255,0.5); }
|
||||
.ev-section--dark .feature-icon-box {
|
||||
background: rgba(255,255,255,0.07);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ── Picture card (pitch image container) ── */
|
||||
.mr-pic-card {
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 40px 100px rgba(0, 0, 0, 0.09);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
background: #fff;
|
||||
}
|
||||
.mr-pic-card--dark {
|
||||
border-color: rgba(255,255,255,0.08);
|
||||
box-shadow: 0 40px 80px rgba(0,0,0,0.45);
|
||||
}
|
||||
.mr-pic-card img {
|
||||
width: 100%; height: auto; display: block;
|
||||
transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
.mr-pic-card:hover img { transform: scale(1.025); }
|
||||
|
||||
/* ── Stats strip ── */
|
||||
.mr-stats-strip {
|
||||
padding: 72px 0;
|
||||
background: #F8FAFC;
|
||||
border-top: 1px solid rgba(0,0,0,0.05);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.mr-stats-grid {
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.07);
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.05);
|
||||
}
|
||||
.mr-stat-item {
|
||||
padding: 44px 32px;
|
||||
text-align: center;
|
||||
border-right: 1px solid rgba(0,0,0,0.07);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.mr-stat-item:last-child { border-right: none; }
|
||||
.mr-stat-item:hover { background: rgba(192,18,39,0.03); }
|
||||
.mr-stat-num {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(36px, 4.5vw, 58px);
|
||||
font-weight: 900;
|
||||
color: #111;
|
||||
letter-spacing: -2px;
|
||||
line-height: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.mr-stat-num span { color: #c01227; }
|
||||
.mr-stat-lbl {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
/* ── CTA section ── */
|
||||
.mr-cta-section {
|
||||
padding: 130px 0;
|
||||
background: #0d0304;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
.mr-cta-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 65% 55% at 50% 50%, rgba(192,18,39,0.16) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.mr-cta-inner {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.mr-cta-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3.5px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.35);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.mr-cta-eyebrow::before,
|
||||
.mr-cta-eyebrow::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 28px; height: 1px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
.mr-cta-inner h2 {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(34px, 5vw, 64px);
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -2px;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 22px;
|
||||
}
|
||||
.mr-cta-inner h2 span { color: #c01227; }
|
||||
.mr-cta-inner p {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 18px;
|
||||
color: rgba(255,255,255,0.48);
|
||||
line-height: 1.75;
|
||||
margin: 0 0 52px;
|
||||
}
|
||||
.mr-cta-btns {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* Primary CTA button */
|
||||
.mr-cta-btns .logico-button {
|
||||
border-radius: 100px !important;
|
||||
}
|
||||
|
||||
/* ── List style matching ev-section checklist ── */
|
||||
.ev-checklist {
|
||||
list-style: none !important;
|
||||
margin: 0 0 48px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.ev-checklist li {
|
||||
list-style: none !important;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.ev-checklist li::marker {
|
||||
content: '' !important;
|
||||
display: none !important;
|
||||
}
|
||||
.ev-checklist--dark li { color: rgba(255,255,255,0.75); }
|
||||
|
||||
/* .logico-front-end ul li:before (vendor-theme-core.css) injects a fontello icon on
|
||||
every li — override it with higher specificity + !important so only our SVG shows */
|
||||
.logico-front-end .ev-checklist li::before,
|
||||
.ev-checklist li::before {
|
||||
content: '' !important;
|
||||
font: unset !important;
|
||||
position: relative !important;
|
||||
display: block !important;
|
||||
width: 20px; height: 20px; min-width: 20px;
|
||||
border-radius: 6px;
|
||||
background: rgba(192,18,39,0.08) !important;
|
||||
border: 1.5px solid rgba(192,18,39,0.2);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='8' viewBox='0 0 10 8' fill='none'%3E%3Cpath d='M1 4l2.5 2.5L9 1' stroke='%23c01227' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
|
||||
background-size: 10px; background-repeat: no-repeat; background-position: center;
|
||||
margin-top: 2px; flex-shrink: 0;
|
||||
top: 0 !important; left: 0 !important;
|
||||
}
|
||||
.ev-checklist--dark li::before {
|
||||
background-color: rgba(192,18,39,0.08) !important;
|
||||
border-color: rgba(192,18,39,0.2) !important;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c01227' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 7 17l-5-5'/%3E%3Cpath d='m22 10-7.5 7.5L13 16'/%3E%3C/svg%3E") !important;
|
||||
background-size: 14px;
|
||||
}
|
||||
|
||||
/* ── Responsive extras ── */
|
||||
@media (max-width: 1024px) {
|
||||
.mr-stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mr-stat-item:nth-child(even) { border-right: none; }
|
||||
.mr-stat-item:nth-child(n+3) { border-top: 1px solid rgba(0,0,0,0.07); }
|
||||
}
|
||||
@media (max-width: 680px) {
|
||||
.mr-stats-strip { padding: 52px 0; }
|
||||
.mr-stats-grid { padding: 0 20px; grid-template-columns: 1fr 1fr; border-radius: 16px; }
|
||||
.mr-stat-item { padding: 28px 16px; }
|
||||
.mr-stat-item:nth-child(odd) { border-right: 1px solid rgba(0,0,0,0.07); }
|
||||
.mr-cta-section { padding: 80px 0; }
|
||||
.mr-cta-inner { padding: 0 20px; }
|
||||
.mr-cta-btns { flex-direction: column; align-items: center; }
|
||||
}
|
||||
|
||||
/* Additional responsive image fixes for MileTruth page */
|
||||
@media (max-width: 1200px) {
|
||||
.mr-pic-card img {
|
||||
max-height: 520px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
.miletruth-hero .content-slider .slide-content-inner {
|
||||
max-width: 860px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-heading,
|
||||
.miletruth-hero .content-slider-item-heading .heading-content {
|
||||
font-size: 60px;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-text,
|
||||
.miletruth-hero .content-slider-item-text .text-content,
|
||||
.miletruth-hero .content-slider-item-text p {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.mr-pic-card {
|
||||
border-radius: 24px;
|
||||
}
|
||||
.mr-pic-card img {
|
||||
max-height: 480px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.miletruth-hero .content-slider .slide-content-inner {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-heading,
|
||||
.miletruth-hero .content-slider-item-heading .heading-content {
|
||||
font-size: 50px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-text,
|
||||
.miletruth-hero .content-slider-item-text .text-content,
|
||||
.miletruth-hero .content-slider-item-text p {
|
||||
font-size: 18px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ev-section .ev-grid,
|
||||
.ev-grid--rev {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.mr-pic-card {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.mr-pic-card img {
|
||||
max-height: 360px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.miletruth-hero .content-slider .slide-content-inner {
|
||||
max-width: 620px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0 22px;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-heading,
|
||||
.miletruth-hero .content-slider-item-heading .heading-content {
|
||||
font-size: 40px;
|
||||
line-height: 1.12;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-text,
|
||||
.miletruth-hero .content-slider-item-text .text-content,
|
||||
.miletruth-hero .content-slider-item-text p {
|
||||
font-size: 16px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.miletruth-hero .content-slider.nav-h-position-right .owl-nav,
|
||||
.miletruth-hero .content-slider.nav-h-position-right .slider-footer {
|
||||
right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mr-pic-card img {
|
||||
max-height: 240px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.mr-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.mr-stat-item {
|
||||
padding: 20px;
|
||||
border-right: none !important;
|
||||
border-top: 1px solid rgba(0,0,0,0.07);
|
||||
}
|
||||
.mr-stat-num {
|
||||
font-size: clamp(28px, 8vw, 40px);
|
||||
}
|
||||
.miletruth-hero .content-slider .slide-content-inner {
|
||||
max-width: 100%;
|
||||
padding: 0 18px;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-heading,
|
||||
.miletruth-hero .content-slider-item-heading .heading-content {
|
||||
font-size: 32px;
|
||||
line-height: 1.14;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-text,
|
||||
.miletruth-hero .content-slider-item-text .text-content,
|
||||
.miletruth-hero .content-slider-item-text p {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-text {
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
.miletruth-hero .content-slider.nav-h-position-right .owl-nav,
|
||||
.miletruth-hero .content-slider.nav-h-position-right .slider-footer {
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive tweaks specifically for dark variant to ensure readable layout */
|
||||
@media (max-width: 1024px) {
|
||||
.ev-section--dark {
|
||||
padding: 80px 0;
|
||||
}
|
||||
.ev-section--dark .ev-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.ev-section--dark .ev-title,
|
||||
.ev-section--dark .ev-desc {
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
.ev-section--dark .feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ev-section--dark {
|
||||
padding: 60px 0;
|
||||
}
|
||||
.ev-section--dark .ev-title { font-size: clamp(28px, 7vw, 40px); }
|
||||
.ev-section--dark .ev-badge { margin-left: auto; margin-right: auto; }
|
||||
.ev-section--dark .feature-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-color: rgba(255,255,255,0.06);
|
||||
}
|
||||
}
|
||||
|
||||
/* MileTruth workflow carousel pagination */
|
||||
.miletruth-workflow-heading {
|
||||
color: #ffffff;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(26px, 3vw, 44px);
|
||||
font-weight: 800;
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.elementor-63 .elementor-element.elementor-element-0a76e77 .testimonial-text p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.elementor-63 .elementor-element.elementor-element-0a76e77 .miletruth-workflow-progress,
|
||||
.miletruth-workflow-progress.slider-footer.slider-footer-position-after {
|
||||
margin-top: 18px !important;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .slider-footer-content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .slider-pagination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .slider-progress-wrapper {
|
||||
color: #ffffff !important;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .slider-progress-current,
|
||||
.miletruth-workflow-progress .slider-progress-all {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .owl-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .owl-dots .owl-dot,
|
||||
.miletruth-workflow-progress .owl-dots button.owl-dot {
|
||||
width: 43px;
|
||||
height: 2px;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0;
|
||||
background: rgba(255,255,255,0.9) !important;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .owl-dots .owl-dot span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.miletruth-workflow-progress .owl-dots .owl-dot.active,
|
||||
.miletruth-workflow-progress .owl-dots button.owl-dot.active {
|
||||
height: 3px;
|
||||
background: #C01227 !important;
|
||||
}
|
||||
18156
public/css/site.css
Normal file
18156
public/css/site.css
Normal file
File diff suppressed because it is too large
Load Diff
2000
public/css/vendor/vendor-elementor-base.css
vendored
2000
public/css/vendor/vendor-elementor-base.css
vendored
File diff suppressed because it is too large
Load Diff
2282
public/css/vendor/vendor-elementor-custom.min.css
vendored
2282
public/css/vendor/vendor-elementor-custom.min.css
vendored
File diff suppressed because it is too large
Load Diff
7724
public/css/vendor/vendor-elementor-generated-globals.css
vendored
7724
public/css/vendor/vendor-elementor-generated-globals.css
vendored
File diff suppressed because it is too large
Load Diff
25
public/css/vendor/vendor-elementor-hfe.css
vendored
25
public/css/vendor/vendor-elementor-hfe.css
vendored
@@ -1,25 +0,0 @@
|
||||
.footer-width-fixer {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.ehf-template-genesis.ehf-header .site-header .wrap,.ehf-template-genesis.ehf-footer .site-footer .wrap,.ehf-template-generatepress.ehf-header .site-header .inside-header {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
max-width: 100%
|
||||
}
|
||||
|
||||
.ehf-template-generatepress.ehf-header .site-header,.ehf-template-generatepress.ehf-footer .site-footer {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
background-color: transparent!important
|
||||
}
|
||||
|
||||
.bhf-hidden {
|
||||
display: none
|
||||
}
|
||||
|
||||
.ehf-header #masthead {
|
||||
z-index: 99;
|
||||
position: relative
|
||||
}
|
||||
979
public/css/vendor/vendor-global-overrides.css
vendored
979
public/css/vendor/vendor-global-overrides.css
vendored
@@ -1,979 +0,0 @@
|
||||
/*
|
||||
Theme Name: Logico Child
|
||||
Description: Logico Child Theme
|
||||
Theme URI: https://example.com/themes/logico
|
||||
Author: Artureanec
|
||||
Author URI: https://example.com
|
||||
Template: logico
|
||||
Version: 1.0.0
|
||||
License: GNU General Public License version 3.0
|
||||
License URI: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
Text Domain: logico-child
|
||||
*/
|
||||
|
||||
/* =Theme customization starts here
|
||||
------------------------------------------------------- */
|
||||
|
||||
/* Hide mobile-specific logo by default */
|
||||
.logo-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MOBILE & TABLET HEADER REFINEMENT (< 1024px)
|
||||
Transition to mobile pill-shape earlier to prevent desktop overlap/wrapping
|
||||
============================================================ */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
|
||||
/* Logo toggling */
|
||||
.logo-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.logo-mobile {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 1. Header Wrapper (Outer): Position & Spacing */
|
||||
.elementor-5180 .elementor-element.elementor-element-466de1b {
|
||||
position: fixed !important;
|
||||
top: 30px !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
height: auto !important;
|
||||
z-index: 10000 !important;
|
||||
background: transparent !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 2. The Header Box / Card (Inner) */
|
||||
.elementor-5180 .elementor-element.elementor-element-e052838 {
|
||||
margin: 0 10px auto !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
background-color: #ffffff !important;
|
||||
border-radius: 25px !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08) !important;
|
||||
pointer-events: all;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* 3. Main Row Layout Container */
|
||||
.elementor-5180 .elementor-element.elementor-element-d681ece {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
width: 100% !important;
|
||||
padding: 12px 25px !important;
|
||||
min-height: 70px !important;
|
||||
box-sizing: border-box !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
/* 4. Logo Container (Left) */
|
||||
.elementor-5180 .elementor-element.elementor-element-472172e {
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 5. Menu Container (Right / Hamburger) */
|
||||
.elementor-5180 .elementor-element.elementor-element-e44ee7e {
|
||||
flex: 0 0 auto !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-end !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 6. Logo Widget Adjustments */
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d {
|
||||
width: auto !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .hfe-site-logo .hfe-site-logo-container img {
|
||||
margin: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-mobile {
|
||||
width: 180px !important;
|
||||
height: auto !important;
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
/* 7. Menu Trigger (Hamburger) alignment */
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .menu-trigger {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-end !important;
|
||||
padding: 5px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Hamburger icon customization */
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .hamburger {
|
||||
width: 30px !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .hamburger span {
|
||||
background-color: #1f1f1f !important;
|
||||
height: 2px !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-only elements */
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .header-menu-container,
|
||||
.elementor-5180 .elementor-element.elementor-element-2f31137,
|
||||
.elementor-5180 .elementor-element.elementor-element-f961133 {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MEDIUM DESKTOP OPTIMIZATION (1025px - 1480px)
|
||||
Prevents logo/menu overlap on medium screens
|
||||
============================================================ */
|
||||
@media (min-width: 1025px) and (max-width: 1480px) {
|
||||
/* Reduce logo size slightly */
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-desktop {
|
||||
width: 130px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* Force single line and tighten spacing */
|
||||
.elementor-5180 .main-menu {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .main-menu > li > a {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
/* Reduce container spacing */
|
||||
.elementor-5180 .elementor-element.elementor-element-d681ece {
|
||||
padding-left: 15px !important;
|
||||
padding-right: 15px !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
|
||||
/* Ensure the menu pill container stays compact */
|
||||
.header-menu-container {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
max-width: 750px !important;
|
||||
}
|
||||
|
||||
/* Scale down the CTA button */
|
||||
.elementor-5180 .logico-small-button {
|
||||
padding: 10px 20px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet Scaling (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-mobile {
|
||||
width: 210px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Standard Mobile Scaling */
|
||||
@media (max-width: 767px) {
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-mobile {
|
||||
width: 175px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra Small Devise Optimization */
|
||||
@media (max-width: 480px) {
|
||||
.elementor-5180 .elementor-element.elementor-element-466de1b {
|
||||
top: 15px !important;
|
||||
padding-left: 15px !important;
|
||||
padding-right: 15px !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-d681ece {
|
||||
padding: 6px 20px !important;
|
||||
min-height: 55px !important; /* Smaller height for small screens */
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-mobile {
|
||||
width: 155px !important; /* Scaled down for tiny screens */
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HOME SECTION IMAGE RESPONSIVENESS (wp-image-4481)
|
||||
============================================================ */
|
||||
|
||||
/* Fix for Tablet and Mobile */
|
||||
@media (max-width: 1024px) {
|
||||
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
|
||||
width: 100% !important;
|
||||
max-width: 450px !important;
|
||||
/* Prevent oversized/zoomed appearance on tablets */
|
||||
height: auto !important;
|
||||
aspect-ratio: auto !important;
|
||||
/* Maintain original proportions */
|
||||
object-fit: contain !important;
|
||||
/* Ensure no cropping of important content */
|
||||
margin: 0 auto !important;
|
||||
/* Centering */
|
||||
display: block !important;
|
||||
padding-bottom: 0px !important;
|
||||
/* Spacing below the image */
|
||||
}
|
||||
|
||||
/* Ensure parent container centers the image */
|
||||
.elementor-element.elementor-element-99768ba {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Specific scaling for small mobile phones */
|
||||
@media (max-width: 767px) {
|
||||
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
|
||||
max-width: 90% !important;
|
||||
/* Reduce scale on small screens */
|
||||
margin: 0 auto !important;
|
||||
border-radius: 18px !important;
|
||||
/* Consistent rounded edges */
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
WHY CHOOSE DOORMILE SECTION RESPONSIVENESS (< 1020px)
|
||||
============================================================ */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ============================================================
|
||||
PROBLEM SECTION - Responsive Layout Fix
|
||||
Targets: "Fragmented Logistics is Broken" section only
|
||||
No button styles included.
|
||||
============================================================ */
|
||||
|
||||
/* Prevent horizontal overflow on the outer container */
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"],
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"] > .e-con-inner {
|
||||
overflow-x: hidden !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* Full width with padding so content doesn't touch screen edges */
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"] > .e-con-inner {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
}
|
||||
|
||||
/* The 4-icon stat grid: wrap into 2 columns on tablet */
|
||||
.elementor-element.elementor-element-e09c20e {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 24px !important;
|
||||
}
|
||||
|
||||
/* Description text: prevent overflow */
|
||||
.elementor-element.elementor-element-5a5c397,
|
||||
.elementor-element.elementor-element-07cd509 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-07cd509 p {
|
||||
width: 100% !important;
|
||||
overflow-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Single column on small mobile */
|
||||
.elementor-element.elementor-element-e09c20e {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HEADER SCROLL VISIBILITY
|
||||
Home page: hidden until scroll. All other pages: always visible.
|
||||
============================================================ */
|
||||
|
||||
/* Home page: header fades in after scroll (JS adds .header-visible-scrolled) */
|
||||
.is-home-page .header-hide-until-scroll {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
transform: translateY(-20px);
|
||||
transition: opacity 0.5s ease, visibility 0.5s ease, transform 0.5s ease !important;
|
||||
}
|
||||
|
||||
.is-home-page .header-hide-until-scroll.header-visible-scrolled {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
/* Non-home pages: always show header */
|
||||
body:not(.is-home-page) .header-hide-until-scroll {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PROBLEM SECTION - Responsive Text & Layout Fix
|
||||
============================================================ */
|
||||
|
||||
/* Prevent overflow at the section container level */
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"] {
|
||||
overflow-x: hidden !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"] > .e-con-inner {
|
||||
max-width: 1480px !important;
|
||||
width: 100% !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* @media (min-width: 1300px) {
|
||||
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"] > .e-con-inner {
|
||||
max-width: 1300px !important;
|
||||
padding: 0px 50px 10px !important;
|
||||
}
|
||||
} */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"] > .e-con-inner {
|
||||
max-width: 100% !important;
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
}
|
||||
|
||||
/* Left column (heading + stats + text): full width */
|
||||
.elementor-element.elementor-element-03db5d7 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Stats grid: 2 columns on tablet */
|
||||
.elementor-element.elementor-element-e09c20e {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 20px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Description text block: prevent overflow */
|
||||
.elementor-element.elementor-element-5a5c397 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-07cd509 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Force all text to wrap properly */
|
||||
.section-shrink-custom p,
|
||||
.section-shrink-custom h3,
|
||||
.section-shrink-custom span {
|
||||
max-width: 100% !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Single column stats on small phones */
|
||||
.elementor-element.elementor-element-e09c20e {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PAGE LOADER - Must cover header on all viewports
|
||||
Header uses z-index: 10000 so loader needs higher
|
||||
============================================================ */
|
||||
.page-loader-container {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CONNECTED LOGISTICS - India Map Image Sizing
|
||||
Widen the map column to fill the gap on the right side
|
||||
============================================================ */
|
||||
@media (min-width: 1021px) {
|
||||
/* Image column: force wider */
|
||||
|
||||
|
||||
/* Text column: take the remaining space */
|
||||
|
||||
|
||||
/* Map image: fill the wider container */
|
||||
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
INDUSTRY SOLUTIONS - Hover Effect Override
|
||||
Image fills box, content shows on hover with #c01227 shade.
|
||||
============================================================ */
|
||||
|
||||
.elementor-element.elementor-element-b891c78,
|
||||
.elementor-element.elementor-element-9b933db,
|
||||
.elementor-element.elementor-element-280b0dc {
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
height: 620px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-content: flex-end !important;
|
||||
align-items: flex-start !important;
|
||||
text-align: left !important;
|
||||
padding: 40px 40px 80px 40px !important;
|
||||
|
||||
|
||||
border-radius: 20px !important;
|
||||
background-color: #f7f7f7 !important;
|
||||
transition: all 0.4s ease !important;
|
||||
}
|
||||
|
||||
|
||||
/* Make image fill the entire card box */
|
||||
.elementor-element.elementor-element-b891c78 .elementor-widget-image,
|
||||
.elementor-element.elementor-element-9b933db .elementor-widget-image,
|
||||
.elementor-element.elementor-element-280b0dc .elementor-widget-image {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78 .elementor-widget-image img,
|
||||
.elementor-element.elementor-element-9b933db .elementor-widget-image img,
|
||||
.elementor-element.elementor-element-280b0dc .elementor-widget-image img {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 20px !important;
|
||||
transition: transform 0.6s ease !important;
|
||||
}
|
||||
|
||||
/* Persistent bottom overlay before hover #c01227 */
|
||||
.elementor-element.elementor-element-b891c78::after,
|
||||
.elementor-element.elementor-element-9b933db::after,
|
||||
.elementor-element.elementor-element-280b0dc::after {
|
||||
content: "" !important;
|
||||
position: absolute !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 120px !important;
|
||||
background-image: linear-gradient(to top, rgba(34, 34, 34, 0.9), rgba(192, 18, 39, 0)) !important;
|
||||
z-index: 1 !important;
|
||||
opacity: 1 !important;
|
||||
border-radius: 0 0 20px 20px !important;
|
||||
transition: opacity 0.4s ease !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover::after,
|
||||
.elementor-element.elementor-element-9b933db:hover::after,
|
||||
.elementor-element.elementor-element-280b0dc:hover::after {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* Hover background shade overlay #c01227 */
|
||||
|
||||
.elementor-element.elementor-element-b891c78::before,
|
||||
.elementor-element.elementor-element-9b933db::before,
|
||||
.elementor-element.elementor-element-280b0dc::before {
|
||||
content: "" !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background-image: linear-gradient(to top, rgba(192, 18, 39, 0.95), rgba(192, 18, 39, 0.2)) !important;
|
||||
|
||||
z-index: 2 !important;
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.4s ease !important;
|
||||
border-radius: 20px !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover::before,
|
||||
.elementor-element.elementor-element-9b933db:hover::before,
|
||||
.elementor-element.elementor-element-280b0dc:hover::before {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Hide content until hover (explicitly target text and lists) */
|
||||
.elementor-element.elementor-element-b891c78 .elementor-widget-text-editor,
|
||||
.elementor-element.elementor-element-9b933db .elementor-widget-text-editor,
|
||||
.elementor-element.elementor-element-280b0dc .elementor-widget-text-editor,
|
||||
.elementor-element.elementor-element-b891c78 .elementor-widget-divider,
|
||||
.elementor-element.elementor-element-9b933db .elementor-widget-divider,
|
||||
.elementor-element.elementor-element-280b0dc .elementor-widget-divider,
|
||||
.elementor-element.elementor-element-b891c78 .hover-list-content,
|
||||
.elementor-element.elementor-element-9b933db .hover-list-content,
|
||||
.elementor-element.elementor-element-280b0dc .hover-list-content {
|
||||
position: relative !important;
|
||||
z-index: 3 !important;
|
||||
opacity: 0 !important;
|
||||
transform: translateY(20px) !important;
|
||||
transition: opacity 0.4s ease, transform 0.4s ease !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover .elementor-widget-text-editor,
|
||||
.elementor-element.elementor-element-9b933db:hover .elementor-widget-text-editor,
|
||||
.elementor-element.elementor-element-280b0dc:hover .elementor-widget-text-editor,
|
||||
.elementor-element.elementor-element-b891c78:hover .elementor-widget-divider,
|
||||
.elementor-element.elementor-element-9b933db:hover .elementor-widget-divider,
|
||||
.elementor-element.elementor-element-280b0dc:hover .elementor-widget-divider,
|
||||
.elementor-element.elementor-element-b891c78:hover .hover-list-content,
|
||||
.elementor-element.elementor-element-9b933db:hover .hover-list-content,
|
||||
.elementor-element.elementor-element-280b0dc:hover .hover-list-content {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Force text to be white and clean */
|
||||
.elementor-element.elementor-element-b891c78 .logico-title,
|
||||
.elementor-element.elementor-element-9b933db .logico-title,
|
||||
.elementor-element.elementor-element-280b0dc .logico-title,
|
||||
.elementor-element.elementor-element-b891c78 p,
|
||||
.elementor-element.elementor-element-9b933db p,
|
||||
.elementor-element.elementor-element-280b0dc p {
|
||||
position: relative !important;
|
||||
z-index: 5 !important;
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78 .logico-title,
|
||||
.elementor-element.elementor-element-9b933db .logico-title,
|
||||
.elementor-element.elementor-element-280b0dc .logico-title {
|
||||
color: #fff !important;
|
||||
text-shadow: 0px 2px 10px rgba(255, 255, 255, 0.4) !important;
|
||||
font-size: 24px !important;
|
||||
font-weight: 700 !important;
|
||||
margin-bottom: 15px !important;
|
||||
text-align: left !important;
|
||||
transition: all 0.3s ease !important;
|
||||
position: relative !important;
|
||||
padding-left: 0px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover .logico-title,
|
||||
.elementor-element.elementor-element-9b933db:hover .logico-title,
|
||||
.elementor-element.elementor-element-280b0dc:hover .logico-title {
|
||||
padding-left: 45px !important;
|
||||
}
|
||||
|
||||
|
||||
/* White Circle Arrow Icon Wrapper */
|
||||
.title-icon {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) scale(0.6) !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
background-color: #ffffff !important;
|
||||
border-radius: 50% !important;
|
||||
color: #000000 !important;
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover .title-icon,
|
||||
.elementor-element.elementor-element-9b933db:hover .title-icon,
|
||||
.elementor-element.elementor-element-280b0dc:hover .title-icon {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(-50%) scale(1) !important;
|
||||
}
|
||||
|
||||
|
||||
.title-icon svg {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover .logico-title,
|
||||
.elementor-element.elementor-element-9b933db:hover .logico-title,
|
||||
.elementor-element.elementor-element-280b0dc:hover .logico-title,
|
||||
.elementor-element.elementor-element-b891c78:hover p,
|
||||
.elementor-element.elementor-element-9b933db:hover p,
|
||||
.elementor-element.elementor-element-280b0dc:hover p {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover .elementor-divider-separator,
|
||||
.elementor-element.elementor-element-9b933db:hover .elementor-divider-separator,
|
||||
.elementor-element.elementor-element-280b0dc:hover .elementor-divider-separator {
|
||||
border-top-color: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Lists styling on Hover */
|
||||
.hover-list-content {
|
||||
position: relative !important;
|
||||
z-index: 5 !important;
|
||||
opacity: 0 !important;
|
||||
transform: translateY(20px) !important;
|
||||
transition: opacity 0.4s ease 0.1s, transform 0.4s ease 0.1s !important; /* staggered entry */
|
||||
|
||||
width: 100% !important;
|
||||
margin-top: 25px !important;
|
||||
margin-bottom: 20px !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-b891c78:hover .hover-list-content,
|
||||
.elementor-element.elementor-element-9b933db:hover .hover-list-content,
|
||||
.elementor-element.elementor-element-280b0dc:hover .hover-list-content {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
margin-bottom: 25px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
.list-section:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.list-section-title {
|
||||
color: #e2e8f0 !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 1.5px !important;
|
||||
margin-bottom: 12px !important;
|
||||
text-transform: uppercase !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.hover-items-list {
|
||||
list-style: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.hover-items-list li {
|
||||
display: flex !important;
|
||||
justify-content: flex-start !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 12px !important;
|
||||
margin-bottom: 12px !important;
|
||||
color: #ffffff !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.hover-items-list li::before {
|
||||
content: "•" !important;
|
||||
color: #ffffff !important;
|
||||
font-size: 20px !important;
|
||||
line-height: 1 !important;
|
||||
margin-top: -3px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.hover-items-list li:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.hover-items-list li span {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Push headers to bottom */
|
||||
.elementor-element.elementor-element-b891c78 .elementor-widget-logico_heading,
|
||||
.elementor-element.elementor-element-9b933db .elementor-widget-logico_heading,
|
||||
.elementor-element.elementor-element-280b0dc .elementor-widget-logico_heading {
|
||||
position: absolute !important;
|
||||
bottom: 30px !important;
|
||||
left: 30px !important;
|
||||
z-index: 10 !important;
|
||||
margin: 0 !important;
|
||||
text-align: left !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
FOOTER CONTACT LINKS - Contrast Fix
|
||||
============================================================ */
|
||||
.elementor-6585 a,
|
||||
.elementor-6585 a::before,
|
||||
.elementor-6585 a::after {
|
||||
color: #FFFFFF !important;
|
||||
text-decoration: none !important;
|
||||
transition: all 0.4s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Apply red hover color only to simple links, avoiding buttons */
|
||||
.elementor-6585 a:not(.logico-alter-button):hover {
|
||||
color: #c01227 !important;
|
||||
}
|
||||
|
||||
.elementor-6585 a.logico-alter-button:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
/* Specific fix for phone and email links to ensure no default theme underlines */
|
||||
.elementor-element-87be926 a,
|
||||
.elementor-element-ba67644 a {
|
||||
border-bottom: none !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SOCIAL ICONS VISIBILITY FIX
|
||||
Ensures both font-based and SVG icons are visible and properly sized.
|
||||
Targeting the common .wrapper-socials class used across the site.
|
||||
============================================================ */
|
||||
.e-font-icon-svg {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.elementor-social-icon svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.fab, .fas, .far {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.wrapper-socials {
|
||||
list-style: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.wrapper-socials li {
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.wrapper-socials li:before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.wrapper-socials a {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: inherit;
|
||||
text-decoration: none !important;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.wrapper-socials a:hover {
|
||||
background-color: var(--logico-accent-color, #c01227);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.wrapper-socials a svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Global visibility enforcement for Elementor and other social icons */
|
||||
.elementor-social-icon,
|
||||
.e-font-icon-svg,
|
||||
.wrapper-socials i,
|
||||
.wrapper-socials svg {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Ensure SVG icons have at least some size if not specified */
|
||||
svg.e-font-icon-svg,
|
||||
.elementor-social-icon svg {
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ============================================================
|
||||
REDESIGNED STEP CARDS (How It Works) - BLACK TEMPLATE
|
||||
============================================================ */
|
||||
.dm-step-card {
|
||||
background: #111111;
|
||||
border: 1px solid #222222;
|
||||
border-radius: 25px;
|
||||
padding: 35px;
|
||||
height: 100%;
|
||||
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.dm-step-card:hover {
|
||||
border-color: #c01227;
|
||||
background: #161616;
|
||||
box-shadow: 0 30px 60px rgba(192, 18, 39, 0.3), 0 0 30px rgba(192, 18, 39, 0.15), 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||
transform: translateY(-10px) scale(1.02);
|
||||
}
|
||||
|
||||
.dm-step-card__image {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dm-step-card__image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 20px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.dm-step-card:hover .dm-step-card__image img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dm-step-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dm-step-card__num {
|
||||
color: #c01227;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
letter-spacing: 1.5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dm-step-card__title {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.dm-step-card__text {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #aaaaaa;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dm-step-card__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dm-step-card__list li {
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
font-size: 14px;
|
||||
color: #dddddd;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dm-step-card__list li::before {
|
||||
content: "\f061"; /* FontAwesome arrow-right */
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1px;
|
||||
color: #c01227;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'fontello';
|
||||
src: url(/fonts/fontello.woff2?98791691) format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
523
public/css/vendor/vendor-icons-fontello.css
vendored
523
public/css/vendor/vendor-icons-fontello.css
vendored
@@ -1,523 +0,0 @@
|
||||
[class^="icon-"]:before,[class*=" icon-"]:before {
|
||||
font-family: 'fontello';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
speak: never;
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1em;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
}
|
||||
|
||||
.icon-arrow-down:before {
|
||||
content: '\e800'
|
||||
}
|
||||
|
||||
.icon-arrow-right:before {
|
||||
content: '\e801'
|
||||
}
|
||||
|
||||
.icon-arrow-left:before {
|
||||
content: '\e802'
|
||||
}
|
||||
|
||||
.icon-arrow-top:before {
|
||||
content: '\e803'
|
||||
}
|
||||
|
||||
.icon-arrow-x-l-down:before {
|
||||
content: '\e804'
|
||||
}
|
||||
|
||||
.icon-arrow-x-r-down:before {
|
||||
content: '\e805'
|
||||
}
|
||||
|
||||
.icon-arrow-x-l-top:before {
|
||||
content: '\e806'
|
||||
}
|
||||
|
||||
.icon-button-arrow-x-l-down:before {
|
||||
content: '\e807'
|
||||
}
|
||||
|
||||
.icon-button-arrow-x-r-down:before {
|
||||
content: '\e808'
|
||||
}
|
||||
|
||||
.icon-button-arrow-x-l-top:before {
|
||||
content: '\e809'
|
||||
}
|
||||
|
||||
.icon-arrow-x-r-top:before {
|
||||
content: '\e80a'
|
||||
}
|
||||
|
||||
.icon-button-arrow-x-r-top:before {
|
||||
content: '\e80b'
|
||||
}
|
||||
|
||||
.icon-chevron-large-down:before {
|
||||
content: '\e80c'
|
||||
}
|
||||
|
||||
.icon-chevron-large-left:before {
|
||||
content: '\e80d'
|
||||
}
|
||||
|
||||
.icon-chevron-large-top:before {
|
||||
content: '\e80e'
|
||||
}
|
||||
|
||||
.icon-chevron-large-right:before {
|
||||
content: '\e80f'
|
||||
}
|
||||
|
||||
.icon-chevron-medium-down:before {
|
||||
content: '\e810'
|
||||
}
|
||||
|
||||
.icon-chevron-medium-left:before {
|
||||
content: '\e811'
|
||||
}
|
||||
|
||||
.icon-chevron-medium-right:before {
|
||||
content: '\e812'
|
||||
}
|
||||
|
||||
.icon-chevron-medium-top:before {
|
||||
content: '\e813'
|
||||
}
|
||||
|
||||
.icon-chevron-small-left:before {
|
||||
content: '\e814'
|
||||
}
|
||||
|
||||
.icon-chevron-small-top:before {
|
||||
content: '\e815'
|
||||
}
|
||||
|
||||
.icon-chevron-small-down:before {
|
||||
content: '\e816'
|
||||
}
|
||||
|
||||
.icon-chevron-small-right:before {
|
||||
content: '\e817'
|
||||
}
|
||||
|
||||
.icon-goods-export-1:before {
|
||||
content: '\e819'
|
||||
}
|
||||
|
||||
.icon-goods-export-2:before {
|
||||
content: '\e81a'
|
||||
}
|
||||
|
||||
.icon-goods-export-3:before {
|
||||
content: '\e81b'
|
||||
}
|
||||
|
||||
.icon-goods-export-4:before {
|
||||
content: '\e81c'
|
||||
}
|
||||
|
||||
.icon-goods-export-5:before {
|
||||
content: '\e81d'
|
||||
}
|
||||
|
||||
.icon-goods-export-6:before {
|
||||
content: '\e81e'
|
||||
}
|
||||
|
||||
.icon-goods-export-7:before {
|
||||
content: '\e81f'
|
||||
}
|
||||
|
||||
.icon-goods-export-8:before {
|
||||
content: '\e820'
|
||||
}
|
||||
|
||||
.icon-goods-export-9:before {
|
||||
content: '\e821'
|
||||
}
|
||||
|
||||
.icon-goods-export-10:before {
|
||||
content: '\e822'
|
||||
}
|
||||
|
||||
.icon-goods-export-11:before {
|
||||
content: '\e823'
|
||||
}
|
||||
|
||||
.icon-goods-export-12:before {
|
||||
content: '\e824'
|
||||
}
|
||||
|
||||
.icon-goods-export-13:before {
|
||||
content: '\e825'
|
||||
}
|
||||
|
||||
.icon-goods-export-14:before {
|
||||
content: '\e826'
|
||||
}
|
||||
|
||||
.icon-goods-export-15:before {
|
||||
content: '\e827'
|
||||
}
|
||||
|
||||
.icon-goods-export-16:before {
|
||||
content: '\e828'
|
||||
}
|
||||
|
||||
.icon-goods-export-17:before {
|
||||
content: '\e829'
|
||||
}
|
||||
|
||||
.icon-goods-export-18:before {
|
||||
content: '\e82a'
|
||||
}
|
||||
|
||||
.icon-goods-export-19:before {
|
||||
content: '\e82b'
|
||||
}
|
||||
|
||||
.icon-goods-export-20:before {
|
||||
content: '\e82c'
|
||||
}
|
||||
|
||||
.icon-goods-export-21:before {
|
||||
content: '\e82d'
|
||||
}
|
||||
|
||||
.icon-goods-export-22:before {
|
||||
content: '\e82e'
|
||||
}
|
||||
|
||||
.icon-goods-export-23:before {
|
||||
content: '\e82f'
|
||||
}
|
||||
|
||||
.icon-goods-export-24:before {
|
||||
content: '\e830'
|
||||
}
|
||||
|
||||
.icon-goods-export-25:before {
|
||||
content: '\e831'
|
||||
}
|
||||
|
||||
.icon-goods-export-26:before {
|
||||
content: '\e832'
|
||||
}
|
||||
|
||||
.icon-goods-export-27:before {
|
||||
content: '\e833'
|
||||
}
|
||||
|
||||
.icon-goods-export-28:before {
|
||||
content: '\e834'
|
||||
}
|
||||
|
||||
.icon-goods-export-29:before {
|
||||
content: '\e835'
|
||||
}
|
||||
|
||||
.icon-goods-export-30:before {
|
||||
content: '\e836'
|
||||
}
|
||||
|
||||
.icon-goods-export-31:before {
|
||||
content: '\e837'
|
||||
}
|
||||
|
||||
.icon-goods-export-32:before {
|
||||
content: '\e838'
|
||||
}
|
||||
|
||||
.icon-light-container:before {
|
||||
content: '\e839'
|
||||
}
|
||||
|
||||
.icon-light-delivery:before {
|
||||
content: '\e83a'
|
||||
}
|
||||
|
||||
.icon-light-directions:before {
|
||||
content: '\e83b'
|
||||
}
|
||||
|
||||
.icon-light-storage:before {
|
||||
content: '\e83c'
|
||||
}
|
||||
|
||||
.icon-triangle-arrow-down:before {
|
||||
content: '\e83d'
|
||||
}
|
||||
|
||||
.icon-triangle-arrow-left:before {
|
||||
content: '\e83e'
|
||||
}
|
||||
|
||||
.icon-triangle-arrow-right:before {
|
||||
content: '\e83f'
|
||||
}
|
||||
|
||||
.icon-triangle-arrow-top:before {
|
||||
content: '\e840'
|
||||
}
|
||||
|
||||
.icon-air-delivery:before {
|
||||
content: '\e841'
|
||||
}
|
||||
|
||||
.icon-call:before {
|
||||
content: '\e842'
|
||||
}
|
||||
|
||||
.icon-cart:before {
|
||||
content: '\e843'
|
||||
}
|
||||
|
||||
.icon-calendar:before {
|
||||
content: '\e844'
|
||||
}
|
||||
|
||||
.icon-close:before {
|
||||
content: '\e845'
|
||||
}
|
||||
|
||||
.icon-container:before {
|
||||
content: '\e846'
|
||||
}
|
||||
|
||||
.icon-coupon:before {
|
||||
content: '\e847'
|
||||
}
|
||||
|
||||
.icon-crop:before {
|
||||
content: '\e848'
|
||||
}
|
||||
|
||||
.icon-download:before {
|
||||
content: '\e849'
|
||||
}
|
||||
|
||||
.icon-download-alter:before {
|
||||
content: '\e84a'
|
||||
}
|
||||
|
||||
.icon-eye:before {
|
||||
content: '\e84b'
|
||||
}
|
||||
|
||||
.icon-file-doc:before {
|
||||
content: '\e84c'
|
||||
}
|
||||
|
||||
.icon-file-file:before {
|
||||
content: '\e84d'
|
||||
}
|
||||
|
||||
.icon-file-pdf:before {
|
||||
content: '\e84e'
|
||||
}
|
||||
|
||||
.icon-garland:before {
|
||||
content: '\e84f'
|
||||
}
|
||||
|
||||
.icon-font:before {
|
||||
content: '\e850'
|
||||
}
|
||||
|
||||
.icon-half-logo:before {
|
||||
content: '\e851'
|
||||
}
|
||||
|
||||
.icon-logo:before {
|
||||
content: '\e852'
|
||||
}
|
||||
|
||||
.icon-location:before {
|
||||
content: '\e853'
|
||||
}
|
||||
|
||||
.icon-hiring:before {
|
||||
content: '\e854'
|
||||
}
|
||||
|
||||
.icon-mail:before {
|
||||
content: '\e855'
|
||||
}
|
||||
|
||||
.icon-package-delivery:before {
|
||||
content: '\e856'
|
||||
}
|
||||
|
||||
.icon-paint:before {
|
||||
content: '\e857'
|
||||
}
|
||||
|
||||
.icon-print:before {
|
||||
content: '\e858'
|
||||
}
|
||||
|
||||
.icon-resize:before {
|
||||
content: '\e85a'
|
||||
}
|
||||
|
||||
.icon-sidebar:before {
|
||||
content: '\e85b'
|
||||
}
|
||||
|
||||
.icon-search:before {
|
||||
content: '\e85c'
|
||||
}
|
||||
|
||||
.icon-side-menu-black:before {
|
||||
content: '\e85d'
|
||||
}
|
||||
|
||||
.icon-side-menu-light:before {
|
||||
content: '\e85e'
|
||||
}
|
||||
|
||||
.icon-star-light:before {
|
||||
content: '\e861'
|
||||
}
|
||||
|
||||
.icon-star:before {
|
||||
content: '\e862'
|
||||
}
|
||||
|
||||
.icon-supply-chain:before {
|
||||
content: '\e863'
|
||||
}
|
||||
|
||||
.icon-tap:before {
|
||||
content: '\e864'
|
||||
}
|
||||
|
||||
.icon-terms:before {
|
||||
content: '\e865'
|
||||
}
|
||||
|
||||
.icon-translate:before {
|
||||
content: '\e866'
|
||||
}
|
||||
|
||||
.icon-trolley:before {
|
||||
content: '\e867'
|
||||
}
|
||||
|
||||
.icon-update:before {
|
||||
content: '\e868'
|
||||
}
|
||||
|
||||
.icon-user:before {
|
||||
content: '\e869'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-container:before {
|
||||
content: '\e86a'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-conveyor:before {
|
||||
content: '\e86b'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-delivery:before {
|
||||
content: '\e86c'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-hand-loader:before {
|
||||
content: '\e86d'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-loader:before {
|
||||
content: '\e86e'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-loader-in-work:before {
|
||||
content: '\e86f'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-manual-loader-box:before {
|
||||
content: '\e870'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-manual-transportation:before {
|
||||
content: '\e871'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-scales:before {
|
||||
content: '\e872'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-scanner:before {
|
||||
content: '\e873'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-palett:before {
|
||||
content: '\e874'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-security:before {
|
||||
content: '\e875'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-storage:before {
|
||||
content: '\e876'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-truck:before {
|
||||
content: '\e877'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-warehouse:before {
|
||||
content: '\e878'
|
||||
}
|
||||
|
||||
.icon-solid-warehousing-autoloader:before {
|
||||
content: '\e879'
|
||||
}
|
||||
|
||||
.icon-check:before {
|
||||
content: '\e87a'
|
||||
}
|
||||
|
||||
.icon-check-alter:before {
|
||||
content: '\e87b'
|
||||
}
|
||||
|
||||
.icon-quote-left:before {
|
||||
content: '\e87c'
|
||||
}
|
||||
|
||||
.icon-quote-right:before {
|
||||
content: '\e87d'
|
||||
}
|
||||
|
||||
.icon-quote-right-light:before {
|
||||
content: '\e87e'
|
||||
}
|
||||
|
||||
.icon-play:before {
|
||||
content: '\e8a2'
|
||||
}
|
||||
|
||||
.icon-play-active:before {
|
||||
content: '\e8a3'
|
||||
}
|
||||
|
||||
.icon-icon-rounded:before {
|
||||
content: '\e8f9'
|
||||
}
|
||||
|
||||
.icon-icon-angular:before {
|
||||
content: '\e8fa'
|
||||
}
|
||||
200
public/css/vendor/vendor-layout-main.css
vendored
200
public/css/vendor/vendor-layout-main.css
vendored
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
Theme Name: Logico Child
|
||||
Description: Logico Child Theme
|
||||
Theme URI: https://example.com/themes/logico
|
||||
Author: Artureanec
|
||||
Author URI: https://example.com
|
||||
Template: logico
|
||||
Version: 1.0.0
|
||||
License: GNU General Public License version 3.0
|
||||
License URI: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
Text Domain: logico-child
|
||||
*/
|
||||
|
||||
/* =Theme customization starts here
|
||||
------------------------------------------------------- */
|
||||
|
||||
/* Hide mobile-specific logo by default */
|
||||
.logo-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MOBILE & TABLET HEADER REFINEMENT (< 1024px)
|
||||
Transition to mobile pill-shape earlier to prevent desktop overlap/wrapping
|
||||
============================================================ */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
|
||||
/* Logo toggling */
|
||||
.logo-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.logo-mobile {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 1. Header Wrapper (Outer): Position & Spacing */
|
||||
.elementor-5180 .elementor-element.elementor-element-466de1b {
|
||||
position: fixed !important;
|
||||
top: 30px !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
height: auto !important;
|
||||
z-index: 10000 !important;
|
||||
background: transparent !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 2. The Header Box / Card (Inner) */
|
||||
.elementor-5180 .elementor-element.elementor-element-e052838 {
|
||||
margin: 0 10px auto !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
background-color: #ffffff !important;
|
||||
border-radius: 25px !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08) !important;
|
||||
pointer-events: all;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* 3. Main Row Layout Container */
|
||||
.elementor-5180 .elementor-element.elementor-element-d681ece {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
width: 100% !important;
|
||||
padding: 12px 25px !important;
|
||||
min-height: 70px !important;
|
||||
box-sizing: border-box !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
/* 4. Logo Container (Left) */
|
||||
.elementor-5180 .elementor-element.elementor-element-472172e {
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 5. Menu Container (Right / Hamburger) */
|
||||
.elementor-5180 .elementor-element.elementor-element-e44ee7e {
|
||||
flex: 0 0 auto !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-end !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 6. Logo Widget Adjustments */
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d {
|
||||
width: auto !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .hfe-site-logo .hfe-site-logo-container img {
|
||||
margin: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-mobile {
|
||||
width: 180px !important;
|
||||
height: auto !important;
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
/* 7. Menu Trigger (Hamburger) alignment */
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .menu-trigger {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-end !important;
|
||||
padding: 5px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Hamburger icon customization */
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .hamburger {
|
||||
width: 30px !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .hamburger span {
|
||||
background-color: #1f1f1f !important;
|
||||
height: 2px !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-only elements */
|
||||
.elementor-5180 .elementor-element.elementor-element-0b7bf6f .header-menu-container,
|
||||
.elementor-5180 .elementor-element.elementor-element-2f31137,
|
||||
.elementor-5180 .elementor-element.elementor-element-f961133 {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MEDIUM DESKTOP OPTIMIZATION (1025px - 1480px)
|
||||
Prevents logo/menu overlap on medium screens
|
||||
============================================================ */
|
||||
@media (min-width: 1025px) and (max-width: 1480px) {
|
||||
/* Reduce logo size slightly */
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-desktop {
|
||||
width: 130px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* Force single line and tighten spacing */
|
||||
.elementor-5180 .main-menu {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.elementor-5180 .main-menu > li > a {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
/* Reduce container spacing */
|
||||
.elementor-5180 .elementor-element.elementor-element-d681ece {
|
||||
padding-left: 15px !important;
|
||||
padding-right: 15px !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
|
||||
/* Ensure the menu pill container stays compact */
|
||||
.header-menu-container {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
max-width: 750px !important;
|
||||
}
|
||||
|
||||
/* Scale down the CTA button */
|
||||
.elementor-5180 .logico-small-button {
|
||||
padding: 10px 20px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet Scaling (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.elementor-5180 .elementor-element.elementor-element-846e53d .logo-mobile {
|
||||
width: 210px !important;
|
||||
}
|
||||
}
|
||||
|
||||
73
public/css/vendor/vendor-responsive-laptops.css
vendored
73
public/css/vendor/vendor-responsive-laptops.css
vendored
@@ -1,73 +0,0 @@
|
||||
/* ============================================================
|
||||
LAPTOP RESPONSIVENESS FIX (1025px – 1520px)
|
||||
Enforces desktop layouts strictly within laptop constraints.
|
||||
Leaves Mobile (<1024px) and Ultrawide (>1520px) UNTOUCHED.
|
||||
============================================================ */
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1520px) {
|
||||
|
||||
/* Hero Slider - Force Desktop Alignment */
|
||||
.logico-content-slider-widget .slide-content-inner {
|
||||
text-align: left !important;
|
||||
align-items: flex-start !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.logico-content-slider-widget .content-slider-item-heading,
|
||||
.logico-content-slider-widget .content-slider-item-text {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Restore Flex & Grid container directions to desktop state */
|
||||
/* .e-con.e-parent,
|
||||
.e-con.e-child {
|
||||
--flex-direction: var(--flex-direction) !important;
|
||||
--container-widget-width: var(--container-widget-width) !important;
|
||||
--container-widget-height: var(--container-widget-height) !important;
|
||||
} */
|
||||
|
||||
/* Keep the .e-con-inner container widths at desktop-equivalent max widths */
|
||||
.e-con-boxed>.e-con-inner {
|
||||
max-width: min(var(--container-max-width, 1480px), 100%) !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
/* ─── Problem Section Specific Fixes ─── */
|
||||
.elementor-element.section-shrink-custom[data-id="30fd9d1"]>.e-con-inner {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
max-width: 1480px !important;
|
||||
width: 100% !important;
|
||||
gap: 40px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-03db5d7 {
|
||||
flex: 1 1 0% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-e09c20e {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(4, 1fr) !important;
|
||||
gap: 24px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-5a5c397 {
|
||||
flex: 0 0 auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* ─── Connected Logistics Section Fixes ─── */
|
||||
.elementor-element.elementor-element-9ffed33 {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
28884
public/css/vendor/vendor-theme-core.css
vendored
28884
public/css/vendor/vendor-theme-core.css
vendored
File diff suppressed because one or more lines are too long
BIN
public/images/ev-paradox.png
Normal file
BIN
public/images/ev-paradox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 999 KiB |
BIN
public/models/3d_scene_final.glb
Normal file
BIN
public/models/3d_scene_final.glb
Normal file
Binary file not shown.
39
purgecss.config.cjs
Normal file
39
purgecss.config.cjs
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* PurgeCSS config for consolidating the migrated WordPress/Elementor CSS.
|
||||
* Content = rendered static HTML (authoritative class list) + component source
|
||||
* (catches runtime-toggled classes that appear as string literals, e.g. "active",
|
||||
* "is-active", "animated", and the body-class strings in src/lib/bodyClasses.ts).
|
||||
*
|
||||
* Conservative by design: keyframes / @font-face / CSS variables are kept (PurgeCSS
|
||||
* defaults), and a safelist guards classes added by JS at runtime.
|
||||
*/
|
||||
module.exports = {
|
||||
css: ["public/css/site.css"],
|
||||
content: [
|
||||
"out/**/*.html",
|
||||
"src/**/*.{tsx,ts,jsx,js}",
|
||||
],
|
||||
// Keep classes toggled/added at runtime by GSAP/React state and header/menu logic.
|
||||
safelist: {
|
||||
standard: [
|
||||
"active",
|
||||
"is-active",
|
||||
"is-hiding",
|
||||
"animated",
|
||||
"header-visible-scrolled",
|
||||
"dm-header-scrolled",
|
||||
"header-visible",
|
||||
"current-menu-item",
|
||||
"current_page_item",
|
||||
"current-menu-ancestor",
|
||||
"menu-open",
|
||||
"loaded",
|
||||
],
|
||||
greedy: [
|
||||
/^elementor-.*-animation/, // animation utility classes applied on scroll
|
||||
/animated$/,
|
||||
/^swiper-/, // carousel runtime classes
|
||||
/^owl-/, // carousel runtime classes
|
||||
],
|
||||
},
|
||||
};
|
||||
48
scripts/build-css.sh
Executable file
48
scripts/build-css.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Consolidate the formerly-WordPress/Elementor vendor CSS into one purged, renamed file.
|
||||
# - Concatenates the load-bearing CSS in the SAME order they were <link>-ed (cascade-preserving).
|
||||
# - Normalizes relative ../../fonts and ../../images url() refs to absolute (location-independent).
|
||||
# - PurgeCSS strips selectors not present in the rendered HTML or component source.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
OUT="public/css/site.css"
|
||||
PURGED_DIR="public/css/.purged"
|
||||
|
||||
# Exact <link> order from the old layout.tsx <head> (cascade matters).
|
||||
FILES=(
|
||||
"public/css/vendor/vendor-elementor-generated-globals.css"
|
||||
"public/css/vendor/vendor-elementor-base.css"
|
||||
"public/css/vendor/vendor-elementor-custom.min.css"
|
||||
"public/css/vendor/vendor-theme-core.css"
|
||||
"public/css/vendor/vendor-global-overrides.css"
|
||||
"public/css/vendor/vendor-layout-main.css"
|
||||
"public/css/vendor/vendor-responsive-laptops.css"
|
||||
"public/css/vendor/vendor-elementor-hfe.css"
|
||||
"public/css/vendor/vendor-icons-fontello-load.css"
|
||||
"public/css/vendor/vendor-icons-fontello.css"
|
||||
"public/css/custom-frontend.min.css"
|
||||
"public/css/all-inlined-head-styles.css"
|
||||
)
|
||||
|
||||
: > "$OUT"
|
||||
for f in "${FILES[@]}"; do
|
||||
printf '\n/* === %s === */\n' "$f" >> "$OUT"
|
||||
cat "$f" >> "$OUT"
|
||||
done
|
||||
|
||||
# Normalize relative asset paths so the combined file works from any location.
|
||||
sed -i '' -E 's#\.\./\.\./fonts/#/fonts/#g; s#\.\./\.\./images/#/images/#g' "$OUT"
|
||||
|
||||
RAW_BYTES=$(wc -c < "$OUT" | tr -d ' ')
|
||||
|
||||
# Purge (config points css: ['public/css/site.css']); output keeps the basename.
|
||||
rm -rf "$PURGED_DIR"
|
||||
npx purgecss --config purgecss.config.cjs --output "$PURGED_DIR"
|
||||
mv "$PURGED_DIR/site.css" "$OUT"
|
||||
rm -rf "$PURGED_DIR"
|
||||
|
||||
PURGED_BYTES=$(wc -c < "$OUT" | tr -d ' ')
|
||||
echo "---"
|
||||
echo "Raw combined: $RAW_BYTES bytes"
|
||||
echo "After purge: $PURGED_BYTES bytes -> $OUT"
|
||||
@@ -27,7 +27,10 @@ import Lenis from "lenis";
|
||||
* Re-evaluates on every route change: the effect cleanup destroys the previous
|
||||
* instance and re-inits on the next route.
|
||||
*/
|
||||
const DISABLED_ROUTES: string[] = [];
|
||||
// /how-it-works runs its own tuned Lenis inside the embedded 3D experience
|
||||
// (src/modules/how-it-works-3d); the global instance is gated off there so two
|
||||
// Lenis instances don't fight over the same document scroll.
|
||||
const DISABLED_ROUTES: string[] = ["/how-it-works"];
|
||||
|
||||
export default function SmoothScroll() {
|
||||
const pathname = usePathname();
|
||||
|
||||
116
src/app/blog/[slug]/page.tsx
Normal file
116
src/app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import SingleBlog from "@/components/sections/SingleBlog";
|
||||
import BlogPostFooter from "@/components/sections/BlogPostFooter";
|
||||
import { getPostBySlug, getAllSlugs, SITE_URL } from "@/data/blog";
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
// Required for `output: "export"` — prerender every post at build time.
|
||||
export function generateStaticParams(): Params[] {
|
||||
return getAllSlugs().map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<Params>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = getPostBySlug(slug);
|
||||
|
||||
if (!post) {
|
||||
return { title: "Article Not Found – Doormile" };
|
||||
}
|
||||
|
||||
const url = `${SITE_URL}/blog/${post.slug}`;
|
||||
const image = `${SITE_URL}${post.image}`;
|
||||
|
||||
return {
|
||||
title: `${post.title} – Doormile`,
|
||||
description: post.excerpt,
|
||||
keywords: [post.category, "last-mile logistics", "EV fleet", "MileTruth", "route optimisation"],
|
||||
authors: [{ name: post.author }],
|
||||
alternates: { canonical: url },
|
||||
openGraph: {
|
||||
type: "article",
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
url,
|
||||
siteName: "Doormile",
|
||||
images: [{ url: image, alt: post.title }],
|
||||
publishedTime: new Date(`${post.date}T00:00:00Z`).toISOString(),
|
||||
authors: [post.author],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<Params>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = getPostBySlug(slug);
|
||||
|
||||
if (!post) notFound();
|
||||
|
||||
const url = `${SITE_URL}/blog/${post.slug}`;
|
||||
|
||||
const articleSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: post.title,
|
||||
description: post.excerpt,
|
||||
image: [`${SITE_URL}${post.image}`],
|
||||
datePublished: new Date(`${post.date}T00:00:00Z`).toISOString(),
|
||||
dateModified: new Date(`${post.date}T00:00:00Z`).toISOString(),
|
||||
author: { "@type": "Organization", name: post.author, url: SITE_URL },
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "Doormile",
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: `${SITE_URL}/images/cropped-image-2.png`,
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: { "@type": "WebPage", "@id": url },
|
||||
articleSection: post.category,
|
||||
};
|
||||
|
||||
const breadcrumbSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: "Home", item: SITE_URL },
|
||||
{ "@type": "ListItem", position: 2, name: "Blog", item: `${SITE_URL}/blog` },
|
||||
{ "@type": "ListItem", position: 3, name: post.title, item: url },
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||
/>
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<SingleBlog post={post} />
|
||||
<BlogPostFooter slug={post.slug} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
121
src/app/cookie-policy/page.tsx
Normal file
121
src/app/cookie-policy/page.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import LegalDocument, { ContactLink, type LegalSection } from "@/components/sections/LegalDocument";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Cookie Policy – Doormile",
|
||||
description:
|
||||
"How Doormile uses cookies and similar technologies when you visit our website, and how you can manage them.",
|
||||
};
|
||||
|
||||
const sections: LegalSection[] = [
|
||||
{
|
||||
heading: "What Are Cookies?",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Cookies are small text files stored on your device that help websites remember information about your visit and improve the browsing experience.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Why We Use Cookies",
|
||||
blocks: [
|
||||
{ type: "p", text: "We use cookies to:" },
|
||||
{
|
||||
type: "ul",
|
||||
items: [
|
||||
"Enable essential website functionality",
|
||||
"Improve website performance",
|
||||
"Analyze visitor behavior and traffic patterns",
|
||||
"Remember user preferences",
|
||||
"Enhance overall user experience",
|
||||
"Support website security",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Types of Cookies We Use",
|
||||
blocks: [
|
||||
{ type: "h3", text: "Essential Cookies" },
|
||||
{
|
||||
type: "p",
|
||||
text: "These cookies are required for core website functionality and security features. The website may not function correctly without them.",
|
||||
},
|
||||
{ type: "h3", text: "Performance Cookies" },
|
||||
{
|
||||
type: "p",
|
||||
text: "These cookies collect information about how visitors interact with the website and help us improve performance and usability.",
|
||||
},
|
||||
{ type: "h3", text: "Analytics Cookies" },
|
||||
{
|
||||
type: "p",
|
||||
text: "Analytics cookies help us understand website traffic, visitor engagement, popular content, and user journeys.",
|
||||
},
|
||||
{ type: "h3", text: "Functional Cookies" },
|
||||
{
|
||||
type: "p",
|
||||
text: "These cookies remember user preferences such as language, region, and other customization settings.",
|
||||
},
|
||||
{ type: "h3", text: "Third-Party Cookies" },
|
||||
{
|
||||
type: "p",
|
||||
text: "Some third-party services integrated into our website, including analytics and performance monitoring tools, may place cookies on your device.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Managing Cookies",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Most web browsers allow users to control, block, or delete cookies through browser settings. Please note that disabling cookies may impact certain website features and functionality.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Cookie Consent",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Where required by applicable law, visitors may be presented with cookie consent options when accessing the website.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Policy Updates",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "We may revise this Cookie Policy periodically to reflect changes in technology, regulations, or business practices. Updated versions will be published on this page.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Contact Us",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: <>If you have questions regarding this Cookie Policy or our use of cookies, please contact us through our <ContactLink />.</>,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function CookiePolicyPage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<LegalDocument
|
||||
title="Cookie Policy"
|
||||
lastUpdated="June 2026"
|
||||
intro="This Cookie Policy explains how Doormile uses cookies and similar technologies when you visit our website."
|
||||
sections={sections}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,76 @@ html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
GLOBAL SPACING SYSTEM
|
||||
A single shared vertical-rhythm scale so every section uses
|
||||
consistent top/bottom spacing instead of ad-hoc values.
|
||||
Fluid (clamp) so it scales down gracefully on small screens.
|
||||
--space-section → standard section (≈80px)
|
||||
--space-section-lg → large/feature section (≈100px)
|
||||
--space-hero-gap → gap from hero to the first section below
|
||||
Apply via .dm-section / .dm-section-lg, or reference the vars
|
||||
directly in component styles.
|
||||
============================================================ */
|
||||
:root {
|
||||
--space-section: clamp(40px, 5vw, 64px);
|
||||
--space-section-lg: clamp(52px, 6vw, 80px);
|
||||
--space-hero-gap: clamp(36px, 4.5vw, 64px);
|
||||
}
|
||||
|
||||
.dm-section {
|
||||
padding-top: var(--space-section) !important;
|
||||
padding-bottom: var(--space-section) !important;
|
||||
}
|
||||
|
||||
.dm-section-lg {
|
||||
padding-top: var(--space-section-lg) !important;
|
||||
padding-bottom: var(--space-section-lg) !important;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Hero → first-section gap fixes
|
||||
The "Doormile Way" container (.elementor-element-88745f4) carries a
|
||||
150px top margin from the shared Elementor kit (intended as mid-page
|
||||
spacing on Home, where it sits deep in the stack). On the About page
|
||||
it is the FIRST section under the hero, so that 150px reads as an
|
||||
oversized empty gap. Scope the reduction to the About page only
|
||||
(.elementor-86) so Home's mid-page rhythm is untouched.
|
||||
------------------------------------------------------------ */
|
||||
.elementor-86 .elementor-element.elementor-element-88745f4 {
|
||||
margin-top: var(--space-hero-gap) !important;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
HOME PAGE — unified section rhythm
|
||||
The shared Elementor kit stamps several stacked sections with a
|
||||
150px top margin. On Home that makes "The Problem", the marquee,
|
||||
"Connected Logistics" and "The Doormile Way" float far below the
|
||||
section above them, while Stats and the EV card sit tight — an
|
||||
uneven, broken rhythm. Normalize EVERY post-hero section to one
|
||||
consistent gap so the page reads Hero ↓ Section ↓ Section evenly.
|
||||
|
||||
Scoped to `.elementor.elementor-61` — the Home page root uniquely
|
||||
carries BOTH classes, so this never leaks onto other pages that
|
||||
reuse these components (e.g. The Doormile Way on About-us, whose
|
||||
root is `.elementor.elementor-86`).
|
||||
|
||||
Hero (741f56c) and the Stats band (9b26234) directly beneath it are
|
||||
intentionally left tight and untouched.
|
||||
------------------------------------------------------------ */
|
||||
/* Sections, in render order: 30fd9d1 The Problem · b62c0b3 Marquee ·
|
||||
89a0ca1 Connected Logistics · 88745f4 The Doormile Way · bbc6760 EV ·
|
||||
3b4a7cc Industry Solutions. */
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-30fd9d1,
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-b62c0b3,
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-89a0ca1,
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-88745f4,
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-bbc6760,
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-3b4a7cc {
|
||||
margin-top: var(--space-section) !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Lenis global smooth scroll (src/animations/SmoothScroll.tsx). These classes are
|
||||
only present on routes/devices where Lenis is active; on touch devices and with
|
||||
prefers-reduced-motion Lenis is off and native scroll-behavior:smooth (above) applies. */
|
||||
@@ -87,12 +157,15 @@ body {
|
||||
#masthead .elementor-element.elementor-element-0b7bf6f .header-menu-container .main-menu > li > a {
|
||||
padding-top: 15px !important;
|
||||
padding-bottom: 16px !important;
|
||||
padding-left: 14px !important;
|
||||
padding-right: 14px !important;
|
||||
font-size: 15px !important;
|
||||
line-height: 1.2 !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
#masthead .header-menu-container .main-menu {
|
||||
gap: 18px !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
#masthead .elementor-element.elementor-element-cabdb09 a.header-button {
|
||||
@@ -450,8 +523,8 @@ body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 30px;
|
||||
width: 90%;
|
||||
margin: 30px auto;
|
||||
width: 100%;
|
||||
margin: 30px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
@@ -579,12 +652,17 @@ body {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* Default card title — single typography token shared by every card (FMCG,
|
||||
Pharmaceutical, Enterprise & B2B). FMCG is the reference: small, clean,
|
||||
bottom-left, consistent weight. Kept compact so longer labels read at the
|
||||
same size/weight as FMCG instead of looking heavier. */
|
||||
.industry-card-default-title {
|
||||
color: #ffffff !important;
|
||||
font-size: 28px !important;
|
||||
font-size: 22px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.2 !important;
|
||||
margin: 0 !important;
|
||||
letter-spacing: -0.5px !important;
|
||||
letter-spacing: -0.3px !important;
|
||||
text-transform: none !important;
|
||||
font-family: var(--font-manrope), 'Manrope', system-ui, -apple-system, sans-serif !important;
|
||||
}
|
||||
@@ -730,9 +808,27 @@ body {
|
||||
letter-spacing: -0.5px !important;
|
||||
}
|
||||
|
||||
.elementor-element-3b4a7cc > .e-con-inner {
|
||||
/* Industry Solutions: match the Meet Crew (OurTeam) container EXACTLY. The
|
||||
e-con-boxed inner otherwise spans edge-to-edge because the vendor
|
||||
"display: var(--display)" resolves to the invalid `inline` fallback, so its
|
||||
max-width is ignored. The previous rule here only set padding (never
|
||||
max-width / margin / display), which is why the divider line stretched almost
|
||||
full-viewport and the heading hugged the left edge. Force flex and pin the
|
||||
inner to the shared 1480px inset so the label divider, heading and card grid
|
||||
all share the same left/right edges as the rest of the page. */
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-3b4a7cc {
|
||||
display: flex;
|
||||
}
|
||||
.elementor.elementor-61 .elementor-element.elementor-element-3b4a7cc > .e-con-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1480px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: clamp(20px, 4vw, 50px) !important;
|
||||
padding-right: clamp(20px, 4vw, 50px) !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Custom premium animations keyframes */
|
||||
@@ -805,7 +901,9 @@ body {
|
||||
width falls below 1536px). Match the home breakpoint instead. */
|
||||
@media (max-width: 840px) {
|
||||
.custom-standard-hero-container {
|
||||
padding: 10px 10px 10px 10px !important;
|
||||
/* No top gap on mobile: the hero card sits flush to the top so the floating
|
||||
navbar rests directly on the hero instead of leaving a band above it. */
|
||||
padding: 0 10px 10px 10px !important;
|
||||
}
|
||||
.custom-standard-hero-card {
|
||||
height: 600px !important;
|
||||
@@ -814,3 +912,22 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Consistent flush hero across every page on mobile/tablet — the floating navbar
|
||||
(position:absolute; top:5px) should rest directly on the hero with no band
|
||||
above it. Home / How It Works / Solutions share .elementor-element-741f56c;
|
||||
MileTruth uses .miletruth-hero-container. (About / Contact / Blog already use
|
||||
.custom-standard-hero-container, handled above.) */
|
||||
@media (max-width: 1024px) {
|
||||
/* The e-con applies top padding via the --padding-top / --padding-block-start
|
||||
vars (site.css sets them to 10px at a higher specificity), so override the
|
||||
vars — not just padding-top — to actually zero the gap. */
|
||||
.elementor-element.elementor-element-741f56c {
|
||||
--padding-top: 0px !important;
|
||||
--padding-block-start: 0px !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.miletruth-hero-container {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from "react";
|
||||
import HowItWorksHero from "../../components/sections/HowItWorksHero";
|
||||
import Miles3 from "../../components/sections/Miles3";
|
||||
import WhyChooseDoormile from "../../components/sections/WhyChooseDoormile";
|
||||
import TheDoormileWay from "../../components/sections/TheDoormileWay";
|
||||
import Experience3DLoader from "@/modules/how-it-works-3d/Experience3DLoader";
|
||||
|
||||
export const metadata = {
|
||||
title: "How It Works – Doormile",
|
||||
@@ -16,9 +14,10 @@ export default function HowItWorksPage() {
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
||||
<HowItWorksHero />
|
||||
<Miles3 />
|
||||
<WhyChooseDoormile />
|
||||
<TheDoormileWay />
|
||||
{/* The first/mid/last-mile story is now told by the scroll-driven 3D
|
||||
experience, which replaces the former Miles3 / WhyChooseDoormile /
|
||||
TheDoormileWay content sections on this page. */}
|
||||
<Experience3DLoader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ const inter = Inter({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Doormile — Last-Mile Logistics Intelligence",
|
||||
title: "Doormile — Delivering Trust. Beyond Boundaries",
|
||||
description: "Doormile powers last-mile logistics with MileTruth™ AI, providing connected miles, SLA protection, and carrier management.",
|
||||
icons: {
|
||||
icon: "/images/cropped-image-2.png",
|
||||
@@ -71,19 +71,13 @@ export default function RootLayout({
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
|
||||
/>
|
||||
{/* Load WordPress & Elementor compiled styles from public folder */}
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-elementor-generated-globals.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-elementor-base.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-elementor-custom.min.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-theme-core.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-global-overrides.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-layout-main.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-responsive-laptops.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-elementor-hfe.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-icons-fontello-load.css" />
|
||||
<link rel="stylesheet" href="/css/vendor/vendor-icons-fontello.css" />
|
||||
<link rel="stylesheet" href="/css/custom-frontend.min.css" />
|
||||
<link rel="stylesheet" href="/css/all-inlined-head-styles.css" />
|
||||
{/*
|
||||
Consolidated site styles. Generated by `npm run build:css`
|
||||
(scripts/build-css.sh): the legacy WordPress/Elementor vendor CSS is
|
||||
concatenated in cascade order and purged of unused selectors via
|
||||
purgecss.config.cjs. ~2.86 MB of vendor CSS -> ~560 KB, one request.
|
||||
*/}
|
||||
<link rel="stylesheet" href="/css/site.css" />
|
||||
</head>
|
||||
{/*
|
||||
Production DOM (index.php + header.php):
|
||||
|
||||
@@ -10,7 +10,7 @@ import EVSection from "@/components/sections/EVSection";
|
||||
import IndustrySolutions from "@/components/sections/IndustrySolutions";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Doormile — Last-Mile Logistics Intelligence",
|
||||
title: "Doormile — Delivering Trust. Beyond Boundaries",
|
||||
description:
|
||||
"Doormile helps logistics companies track every mile with MileTruth™ AI. Real-time SLA protection and connected miles visibility.",
|
||||
};
|
||||
|
||||
146
src/app/privacy-policy/page.tsx
Normal file
146
src/app/privacy-policy/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import LegalDocument, { ContactLink, type LegalSection } from "@/components/sections/LegalDocument";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy – Doormile",
|
||||
description:
|
||||
"How Doormile collects, uses, stores, and protects your information when you visit our website, interact with our services, or communicate with us.",
|
||||
};
|
||||
|
||||
const sections: LegalSection[] = [
|
||||
{
|
||||
heading: "Information We Collect",
|
||||
blocks: [
|
||||
{ type: "p", text: "We may collect the following information:" },
|
||||
{
|
||||
type: "ul",
|
||||
items: [
|
||||
"Full name",
|
||||
"Email address",
|
||||
"Phone number",
|
||||
"Company name",
|
||||
"Job title",
|
||||
"Information submitted through contact forms",
|
||||
"Service inquiry details",
|
||||
"Website usage data and analytics",
|
||||
"Browser, device, and IP information",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "How We Use Your Information",
|
||||
blocks: [
|
||||
{ type: "p", text: "We use your information to:" },
|
||||
{
|
||||
type: "ul",
|
||||
items: [
|
||||
"Respond to inquiries and support requests",
|
||||
"Provide information about our services",
|
||||
"Improve website performance and user experience",
|
||||
"Analyze usage trends and platform effectiveness",
|
||||
"Maintain security and prevent unauthorized access",
|
||||
"Communicate service updates and business information",
|
||||
"Comply with legal and regulatory requirements",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Information Sharing",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Doormile does not sell, rent, or trade personal information. Information may be shared with trusted service providers that assist with website hosting, analytics, communications, and operational support, subject to appropriate confidentiality and security obligations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Data Security",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "We implement industry-standard administrative, technical, and organizational safeguards designed to protect personal information from unauthorized access, disclosure, alteration, or destruction.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Data Retention",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "We retain information only for as long as necessary to fulfill the purposes described in this policy, comply with legal obligations, resolve disputes, and enforce agreements.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Cookies and Tracking Technologies",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "We use cookies and similar technologies to improve website functionality, measure performance, understand user behavior, and enhance user experience.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Your Rights",
|
||||
blocks: [
|
||||
{ type: "p", text: "Depending on applicable laws, you may have the right to:" },
|
||||
{
|
||||
type: "ul",
|
||||
items: [
|
||||
"Access your personal information",
|
||||
"Request correction of inaccurate data",
|
||||
"Request deletion of personal information",
|
||||
"Restrict or object to certain processing activities",
|
||||
"Withdraw consent where applicable",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Third-Party Links",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Our website may contain links to third-party websites. We are not responsible for the privacy practices or content of external websites.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Policy Updates",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "We may update this Privacy Policy periodically. Any changes will be posted on this page with a revised effective date.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Contact Us",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: <>For privacy-related questions or requests, please contact us through our <ContactLink />.</>,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<LegalDocument
|
||||
title="Privacy Policy"
|
||||
lastUpdated="June 2026"
|
||||
intro="At Doormile, we are committed to protecting your privacy and maintaining the security of the information you share with us. This Privacy Policy outlines how we collect, use, store, and protect your information when you visit our website, interact with our services, or communicate with us."
|
||||
sections={sections}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
src/app/terms-of-service/page.tsx
Normal file
140
src/app/terms-of-service/page.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import LegalDocument, { ContactLink, type LegalSection } from "@/components/sections/LegalDocument";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Terms of Service – Doormile",
|
||||
description:
|
||||
"The Terms of Service governing your access to and use of the Doormile website and related services.",
|
||||
};
|
||||
|
||||
const sections: LegalSection[] = [
|
||||
{
|
||||
heading: "Acceptance of Terms",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "By accessing this website, you acknowledge that you have read, understood, and agreed to these Terms of Service and all applicable laws and regulations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Permitted Use",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "You agree to use the website and services only for lawful purposes and in accordance with these terms.",
|
||||
},
|
||||
{ type: "p", text: "You agree not to:" },
|
||||
{
|
||||
type: "ul",
|
||||
items: [
|
||||
"Violate applicable laws or regulations",
|
||||
"Attempt unauthorized access to systems or networks",
|
||||
"Interfere with website functionality or security",
|
||||
"Distribute malicious software or harmful code",
|
||||
"Misrepresent your identity or organization",
|
||||
"Use website content without authorization",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Intellectual Property Rights",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "All content, technology, software, graphics, trademarks, logos, text, designs, and other materials available on this website are the property of Doormile or its licensors and are protected by applicable intellectual property laws.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "User Submissions",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Any information submitted through forms, inquiries, or communications must be accurate and lawful. Users are responsible for the content they submit.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Service Availability",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "While we strive to maintain uninterrupted access, we do not guarantee that the website or services will always be available, secure, or error-free.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Disclaimer of Warranties",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: 'The website and services are provided on an "as is" and "as available" basis without warranties of any kind, whether express or implied.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Limitation of Liability",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "To the maximum extent permitted by law, Doormile shall not be liable for any indirect, incidental, special, consequential, or punitive damages arising from or related to the use of the website or services.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Indemnification",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "Users agree to indemnify and hold harmless Doormile, its employees, partners, and affiliates from claims arising from misuse of the website or violation of these terms.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Modifications",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "We reserve the right to update or modify these Terms of Service at any time. Continued use of the website following updates constitutes acceptance of the revised terms.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Governing Law",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: "These Terms shall be governed by and interpreted in accordance with applicable laws and regulations in the jurisdictions where Doormile conducts business.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Contact Us",
|
||||
blocks: [
|
||||
{
|
||||
type: "p",
|
||||
text: <>Questions regarding these Terms of Service may be submitted through our <ContactLink />.</>,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function TermsOfServicePage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<LegalDocument
|
||||
title="Terms of Service"
|
||||
lastUpdated="June 2026"
|
||||
intro="These Terms of Service govern your access to and use of the Doormile website and related services. By accessing or using our website, you agree to comply with these terms."
|
||||
sections={sections}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
src/components/blog/BlogSearch.tsx
Normal file
122
src/components/blog/BlogSearch.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { blogPosts } from "@/data/blog";
|
||||
|
||||
/**
|
||||
* Client-side blog search. The site is a static export, so there is no search
|
||||
* server — we filter the known posts in the browser and link straight to the
|
||||
* matching /blog/[slug] routes.
|
||||
*/
|
||||
export default function BlogSearch() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const results = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
return blogPosts
|
||||
.filter(
|
||||
(p) =>
|
||||
p.title.toLowerCase().includes(q) ||
|
||||
p.category.toLowerCase().includes(q) ||
|
||||
p.excerpt.toLowerCase().includes(q)
|
||||
)
|
||||
.slice(0, 6);
|
||||
}, [query]);
|
||||
|
||||
// Close the results panel on outside click.
|
||||
useEffect(() => {
|
||||
function onDocClick(e: MouseEvent) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", onDocClick);
|
||||
return () => document.removeEventListener("mousedown", onDocClick);
|
||||
}, []);
|
||||
|
||||
const showPanel = open && query.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="dm-blog-search" ref={containerRef}>
|
||||
<form
|
||||
role="search"
|
||||
className="dm-blog-search-form"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="dm-blog-search-input" className="dm-sr-only">
|
||||
Search articles
|
||||
</label>
|
||||
<input
|
||||
id="dm-blog-search-input"
|
||||
type="search"
|
||||
className="dm-blog-search-input"
|
||||
placeholder="Search articles…"
|
||||
value={query}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
aria-expanded={showPanel}
|
||||
aria-controls="dm-blog-search-results"
|
||||
/>
|
||||
<span className="dm-blog-search-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</span>
|
||||
</form>
|
||||
|
||||
{showPanel && (
|
||||
<div
|
||||
id="dm-blog-search-results"
|
||||
className="dm-blog-search-results"
|
||||
role="listbox"
|
||||
>
|
||||
{results.length === 0 ? (
|
||||
<p className="dm-blog-search-empty">
|
||||
No articles match “{query.trim()}”.
|
||||
</p>
|
||||
) : (
|
||||
<ul>
|
||||
{results.map((p) => (
|
||||
<li key={p.slug} role="option" aria-selected="false">
|
||||
<Link
|
||||
href={`/blog/${p.slug}`}
|
||||
className="dm-blog-search-result"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<span className="dm-blog-search-result-cat">
|
||||
{p.category}
|
||||
</span>
|
||||
<span className="dm-blog-search-result-title">
|
||||
{p.title}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
src/components/blog/BlogSidebar.tsx
Normal file
100
src/components/blog/BlogSidebar.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
getRecentPosts,
|
||||
getCategories,
|
||||
formatDate,
|
||||
type BlogPost,
|
||||
} from "@/data/blog";
|
||||
import BlogSearch from "./BlogSearch";
|
||||
|
||||
/**
|
||||
* Sticky single-post sidebar: Search, Recent Posts, Categories and a CTA card.
|
||||
* Styling lives in SingleBlog's scoped <style> block (dm-blog-* classes).
|
||||
*/
|
||||
export default function BlogSidebar({ current }: { current?: BlogPost }) {
|
||||
const recent = getRecentPosts(5)
|
||||
.filter((p) => p.slug !== current?.slug)
|
||||
.slice(0, 4);
|
||||
const categories = getCategories();
|
||||
|
||||
return (
|
||||
<aside className="dm-blog-sidebar" aria-label="Blog sidebar">
|
||||
{/* Search */}
|
||||
<section className="dm-blog-widget">
|
||||
<h2 className="dm-blog-widget-title">Search</h2>
|
||||
<BlogSearch />
|
||||
</section>
|
||||
|
||||
{/* Recent Posts */}
|
||||
<section className="dm-blog-widget">
|
||||
<h2 className="dm-blog-widget-title">Recent Posts</h2>
|
||||
<ul className="dm-blog-recent">
|
||||
{recent.map((p) => (
|
||||
<li key={p.slug}>
|
||||
<Link href={`/blog/${p.slug}`} className="dm-blog-recent-item">
|
||||
<span className="dm-blog-recent-thumb">
|
||||
<Image
|
||||
src={p.image}
|
||||
alt={p.title}
|
||||
fill
|
||||
sizes="62px"
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
</span>
|
||||
<span className="dm-blog-recent-meta">
|
||||
<span className="dm-blog-recent-title">{p.title}</span>
|
||||
<time dateTime={p.date} className="dm-blog-recent-date">
|
||||
{formatDate(p.date)}
|
||||
</time>
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Categories */}
|
||||
<section className="dm-blog-widget">
|
||||
<h2 className="dm-blog-widget-title">Categories</h2>
|
||||
<ul className="dm-blog-categories">
|
||||
{categories.map((c) => (
|
||||
<li key={c.name}>
|
||||
<Link href="/blog" className="dm-blog-category-item">
|
||||
<span>{c.name}</span>
|
||||
<span className="dm-blog-category-count">{c.count}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* CTA Card */}
|
||||
<section className="dm-blog-widget dm-blog-cta-card">
|
||||
<h2 className="dm-blog-cta-title">Ready to optimise your fleet?</h2>
|
||||
<p className="dm-blog-cta-text">
|
||||
See how MileTruth™ AI cuts distance, vehicles and emissions — without
|
||||
missing an SLA.
|
||||
</p>
|
||||
<Link href="/contact" className="dm-blog-cta-btn">
|
||||
Contact Us
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
</section>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useHeaderUI } from "./HeaderUIProvider";
|
||||
/**
|
||||
* Production: `<div class="body-overlay"></div>` is a direct child of body
|
||||
* (rendered by both index.php line 6 and header.php line 5 — production has two; we render one).
|
||||
* CSS (in vendor-theme-core.css and elementor-frontend-inline-css.css) styles it as fixed-position fullscreen overlay.
|
||||
* CSS (consolidated into /public/css/site.css) styles it as fixed-position fullscreen overlay.
|
||||
*/
|
||||
export default function BodyOverlay() {
|
||||
const { isMenuOpen, isSidebarOpen, closeAll } = useHeaderUI();
|
||||
|
||||
@@ -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) {
|
||||
@@ -107,7 +130,12 @@ export default function Footer() {
|
||||
<div className="elementor-element elementor-element-df89993 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="df89993" data-element_type="container" data-e-type="container">
|
||||
<div className="elementor-element elementor-element-69b6892 elementor-widget elementor-widget-logico_heading" data-id="69b6892" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title">Call Center</div>
|
||||
<div className="logico-title dm-foot-label">
|
||||
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z" />
|
||||
</svg>
|
||||
<span>Call Center</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-87be926 elementor-widget elementor-widget-text-editor" data-id="87be926" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
|
||||
@@ -119,7 +147,13 @@ export default function Footer() {
|
||||
<div className="elementor-element elementor-element-f5d8e63 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="f5d8e63" data-element_type="container" data-e-type="container">
|
||||
<div className="elementor-element elementor-element-774e540 elementor-widget elementor-widget-logico_heading" data-id="774e540" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title">Our Location</div>
|
||||
<div className="logico-title dm-foot-label">
|
||||
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
<span>Our Location</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-9c1cf03 elementor-widget elementor-widget-text-editor" data-id="9c1cf03" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
|
||||
@@ -135,7 +169,13 @@ export default function Footer() {
|
||||
<div className="elementor-element elementor-element-a96d151 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="a96d151" data-element_type="container" data-e-type="container">
|
||||
<div className="elementor-element elementor-element-37e647f elementor-widget elementor-widget-logico_heading" data-id="37e647f" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title">Email</div>
|
||||
<div className="logico-title dm-foot-label">
|
||||
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="m22 7-10 5L2 7" />
|
||||
</svg>
|
||||
<span>Email</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-ba67644 elementor-widget elementor-widget-text-editor" data-id="ba67644" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
|
||||
@@ -149,7 +189,16 @@ export default function Footer() {
|
||||
<div className="elementor-element elementor-element-9ba4b82 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9ba4b82" data-element_type="container" data-e-type="container">
|
||||
<div className="elementor-element elementor-element-e9a5d79 elementor-widget elementor-widget-logico_heading" data-id="e9a5d79" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title">Social network</div>
|
||||
<div className="logico-title dm-foot-label">
|
||||
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
<circle cx="6" cy="12" r="3" />
|
||||
<circle cx="18" cy="19" r="3" />
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
||||
</svg>
|
||||
<span>Social network</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-a6bccba elementor-shape-square elementor-grid-0 elementor-widget elementor-widget-social-icons" data-id="a6bccba" data-element_type="widget" data-e-type="widget" data-widget_type="social-icons.default">
|
||||
@@ -285,12 +334,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>
|
||||
@@ -498,6 +547,97 @@ export default function Footer() {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
/* Compact footer link columns on phones (<=480px). The kit only sets
|
||||
column --width at min-width:481px, so below that all four blocks
|
||||
fall back to 100% and stack into one very tall column. Put the
|
||||
logo+social full-width on top, then lay the three link groups out
|
||||
in a 2-up grid (matching the tablet 47% intent) so the footer is
|
||||
roughly half as tall. */
|
||||
/* Inline icon beside each contact label (Call Center / Our Location /
|
||||
Email / Social network). */
|
||||
.elementor-6585 .dm-foot-label {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.elementor-6585 .dm-foot-label .dm-foot-ic {
|
||||
flex: 0 0 auto;
|
||||
color: #C01227;
|
||||
}
|
||||
|
||||
/* ── Compact mobile contact footer (<=600px) ──
|
||||
On phones the upper contact section stacked into one very tall
|
||||
column with 50px margins between every block. Tighten the heading,
|
||||
collapse those stack margins, and pair Email + Social side-by-side
|
||||
(both are short) so the footer is far shorter without losing the
|
||||
full-width address readability. */
|
||||
@media (max-width: 600px) {
|
||||
/* Trim the band's oversized top/bottom padding on phones. */
|
||||
.elementor-6585 .elementor-element.elementor-element-b29b8fc > .e-con-inner {
|
||||
padding-top: 40px !important;
|
||||
padding-bottom: 40px !important;
|
||||
}
|
||||
.elementor-6585 .elementor-element.elementor-element-687d944 .logico-title {
|
||||
font-size: clamp(24px, 6.6vw, 32px) !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
.elementor-6585 .dm-foot-label .dm-foot-ic { width: 14px; height: 14px; }
|
||||
/* The Call Center / Our Location group carries a 10px side padding
|
||||
the Email / Social group doesn't, leaving its labels indented
|
||||
10px out of line with everything else — zero it so all four
|
||||
contact blocks share the same left edge as the heading. */
|
||||
.elementor-6585 .elementor-element.elementor-element-2631b42 {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
/* All four contact blocks stack in one left-aligned column with a
|
||||
consistent gap (Social network sits below Email). */
|
||||
.elementor-6585 .elementor-element.elementor-element-2631b42,
|
||||
.elementor-6585 .elementor-element.elementor-element-f5d8e63,
|
||||
.elementor-6585 .elementor-element.elementor-element-645be8d,
|
||||
.elementor-6585 .elementor-element.elementor-element-9ba4b82 {
|
||||
margin-top: 22px !important;
|
||||
}
|
||||
.elementor-6585 .elementor-element.elementor-element-645be8d > .e-con-inner {
|
||||
flex-direction: column !important;
|
||||
gap: 0 !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
.elementor-6585 .elementor-element.elementor-element-a96d151,
|
||||
.elementor-6585 .elementor-element.elementor-element-9ba4b82 {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.elementor-6585 .elementor-element.elementor-element-3f1ba7a {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 36px 16px !important;
|
||||
}
|
||||
.elementor-6585 .elementor-element.elementor-element-64e2e81 {
|
||||
flex: 0 0 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.elementor-6585 .elementor-element.elementor-element-5b73dd3,
|
||||
.elementor-6585 .elementor-element.elementor-element-451f15c,
|
||||
.elementor-6585 .elementor-element.elementor-element-44a1f5d {
|
||||
flex: 1 1 calc(50% - 8px) !important;
|
||||
width: calc(50% - 8px) !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
/* Allow long links (e.g. "Women entrepreneurship") to wrap inside
|
||||
the narrow columns instead of overflowing. */
|
||||
.elementor-6585 .logico-custom-menu-widget li a {
|
||||
white-space: normal !important;
|
||||
}
|
||||
/* Trim the large social-icons top gap so the brand block stays tight. */
|
||||
.elementor-6585 .elementor-element.elementor-element-e4e6486 > .elementor-widget-container {
|
||||
margin-top: 24px !important;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
@@ -449,7 +449,7 @@ export default function Header() {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="header-mobile-button">
|
||||
<Link className="logico-alter-button" href="/contact" target="_blank">Get in Touch</Link>
|
||||
<Link className="logico-alter-button" href="/contact">Get in Touch</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -470,7 +470,7 @@ export default function Header() {
|
||||
>
|
||||
<div className="elementor-widget-container">
|
||||
<div className="header-button-container">
|
||||
<Link href="/contact" target="_blank" className="header-button header-button-animation-fade">
|
||||
<Link href="/contact" className="header-button header-button-animation-fade">
|
||||
Contact Us
|
||||
</Link>
|
||||
</div>
|
||||
@@ -480,8 +480,15 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/* Inline <style> block — 1:1 translation of header.php lines 600-627 */}
|
||||
{/* Inline <style> block — 1:1 translation of header.php lines 600-627.
|
||||
suppressHydrationWarning: this is a static, deterministic CSS string,
|
||||
but as a client component its dangerouslySetInnerHTML is diffed during
|
||||
hydration. A stale prebuilt out/ or a CSS-injecting browser extension
|
||||
can make the server HTML differ from the client bundle, which React
|
||||
refuses to patch — suppressing avoids a false console error for a node
|
||||
whose content never depends on render-time state. */}
|
||||
<style
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
#masthead .elementor-element.elementor-element-466de1b {
|
||||
@@ -512,8 +519,10 @@ export default function Header() {
|
||||
Force position:fixed once scrolled past 50px so the header stays in viewport. */
|
||||
#masthead .elementor-element.elementor-element-466de1b.dm-header-scrolled {
|
||||
position: fixed !important;
|
||||
background: #4b4b4baa !important;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18) !important;
|
||||
background: rgba(26, 26, 26, 0.92) !important;
|
||||
-webkit-backdrop-filter: blur(14px) !important;
|
||||
backdrop-filter: blur(14px) !important;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22) !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
@@ -522,11 +531,117 @@ export default function Header() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Desktop navbar layout (3-section balanced grid) ──
|
||||
The earlier Mac-M1 fix used justify-content:space-between, which
|
||||
on wide screens flung the logo and nav to opposite edges and
|
||||
left a large asymmetric gap between them. Replace it with a
|
||||
responsive three-column grid so the nav stays optically centred
|
||||
with equal space on both sides, the logo group sits left and the
|
||||
Contact button right — proportional at every desktop width.
|
||||
|
||||
The logo group (472172e) and nav (e44ee7e) are nested inside
|
||||
e052838 › d681ece, while the Contact button (f961133) is their
|
||||
uncle. Those two wrappers carry no visual styling on desktop
|
||||
(their background/padding live only in the <1024px mobile block),
|
||||
so display:contents flattens them and promotes all three logical
|
||||
sections to be direct items of one header grid. The grid only
|
||||
applies >=1025px, leaving the mobile pill layout untouched. */
|
||||
@media (min-width: 1025px) {
|
||||
#masthead .elementor-element.elementor-element-466de1b {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr auto 1fr !important;
|
||||
align-items: center !important;
|
||||
column-gap: clamp(16px, 2vw, 32px) !important;
|
||||
/* Equal left/right safe areas pull the logo and Contact
|
||||
button inward off the viewport edges. Because the inset
|
||||
is symmetric, the 1fr side columns shrink equally and the
|
||||
centred nav does not shift. */
|
||||
padding-inline: clamp(24px, 3vw, 48px) !important;
|
||||
/* Consistent top/bottom breathing room inside the bar; with
|
||||
align-items:center every section stays vertically centred. */
|
||||
padding-block: clamp(8px, 1vw, 16px) !important;
|
||||
}
|
||||
|
||||
#masthead .elementor-element.elementor-element-e052838,
|
||||
#masthead .elementor-element.elementor-element-d681ece {
|
||||
display: contents !important;
|
||||
}
|
||||
|
||||
/* navbar-left: menu/grid icon + logo */
|
||||
#masthead .elementor-element.elementor-element-472172e {
|
||||
justify-self: start !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: clamp(16px, 1.5vw, 28px) !important;
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
/* Logo never shrinks — keeps a clean edge against the nav. */
|
||||
#masthead .elementor-element.elementor-element-846e53d {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
/* navbar-center: navigation menu, optically centred. */
|
||||
#masthead .elementor-element.elementor-element-e44ee7e {
|
||||
justify-self: center !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* navbar-right: Contact button, aligned to the right edge. */
|
||||
#masthead .elementor-element.elementor-element-f961133 {
|
||||
justify-self: end !important;
|
||||
}
|
||||
}
|
||||
|
||||
#masthead .header-menu-container .main-menu > li.active > a:before {
|
||||
background-color: #ffffff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
#masthead .header-menu-container .main-menu > li.active > a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
#masthead .header-menu-container .main-menu > li > a:focus,
|
||||
#masthead .header-menu-container .main-menu > li > a:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Responsive logo adjustment on mobile/tablet */
|
||||
@media (max-width: 1024px) {
|
||||
#masthead .elementor-element.elementor-element-846e53d .hfe-site-logo .hfe-site-logo-container img {
|
||||
width: 150px !important;
|
||||
height: auto !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
#masthead .header-menu-container {
|
||||
display: none !important;
|
||||
}
|
||||
#masthead .menu-trigger {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Sticky header on scroll for mobile/tablet too — the desktop
|
||||
rule lived only in the min-width:1025px block, so on phones the
|
||||
absolutely-positioned navbar scrolled away with the page. Pin it
|
||||
to the top once scrolled past 50px, matching desktop behaviour. */
|
||||
#masthead .elementor-element.elementor-element-466de1b.dm-header-scrolled {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#masthead .elementor-element.elementor-element-846e53d .hfe-site-logo .hfe-site-logo-container img {
|
||||
width: 130px !important;
|
||||
margin-left: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* The theme reveals the mobile slide-in menu via Elementor's
|
||||
body[data-elementor-device-mode="mobile"] rules, which are set by
|
||||
Elementor's frontend JS — that JS isn't shipped in this Next port,
|
||||
@@ -539,6 +654,34 @@ export default function Header() {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* The bundled 'fontello' subset is missing the search (\\e85c) and
|
||||
close (\\e845) glyphs, so those two icons render as empty tofu
|
||||
boxes. Replace them with inline SVGs via CSS masks so they paint
|
||||
in the current text colour (matching the working cart/user icons)
|
||||
and respond to hover. */
|
||||
#masthead .header-icon.search-link .search-trigger-icon::before,
|
||||
#masthead .header-icon.menu-close .menu-close-icon::before {
|
||||
content: "" !important;
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
}
|
||||
#masthead .header-icon.search-link .search-trigger-icon::before {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='16.5' y1='16.5' x2='21' y2='21'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='16.5' y1='16.5' x2='21' y2='21'/%3E%3C/svg%3E");
|
||||
}
|
||||
#masthead .header-icon.menu-close .menu-close-icon::before {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cline x1='5' y1='5' x2='19' y2='19'/%3E%3Cline x1='19' y1='5' x2='5' y2='19'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cline x1='5' y1='5' x2='19' y2='19'/%3E%3Cline x1='19' y1='5' x2='5' y2='19'/%3E%3C/svg%3E");
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -79,11 +79,10 @@ export default function LoadingScreen() {
|
||||
<Image
|
||||
src="/images/preloader.png"
|
||||
alt="Doormile"
|
||||
width={200}
|
||||
height={38}
|
||||
width={325}
|
||||
height={239}
|
||||
priority
|
||||
className="dm-loader__logo"
|
||||
style={{ width: "auto", height: "auto" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -100,8 +99,8 @@ export default function LoadingScreen() {
|
||||
will-change: opacity;
|
||||
}
|
||||
.dm-loader.is-hiding { opacity: 0; pointer-events: none; }
|
||||
.dm-loader__pulse { animation: dmLoaderPulse 1.5s linear infinite; }
|
||||
.dm-loader__logo { width: clamp(140px, 18vw, 200px); height: auto; }
|
||||
.dm-loader__pulse { animation: dmLoaderPulse 1.5s linear infinite; display: grid; place-items: center; }
|
||||
.dm-loader__logo { display: block; margin: 0 auto; width: clamp(120px, 32vw, 180px); height: auto; }
|
||||
@keyframes dmLoaderPulse {
|
||||
50% { transform: scale(0.85); }
|
||||
100% { transform: scale(1); }
|
||||
|
||||
@@ -5,7 +5,7 @@ import dynamic from "next/dynamic";
|
||||
import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { P, STRATEGIES, ENGINE_STEPS, CONSTRAINT_LIST, STRATEGY_SCORES } from "./theme";
|
||||
import { P, STRATEGIES, WINNER_INDEX, ENGINE_STEPS, CONSTRAINT_LIST, STRATEGY_SCORES } from "./theme";
|
||||
|
||||
const LogisticsBrainCanvas = dynamic(() => import("./LogisticsBrainCanvas"), { ssr: false });
|
||||
|
||||
@@ -24,23 +24,7 @@ function Counter({ mv }: { mv: MotionValue<number> }) {
|
||||
return <span ref={ref}>{Math.round(mv.get())}</span>;
|
||||
}
|
||||
|
||||
/** True only while a card's own opacity window is open (with a tiny buffer).
|
||||
* Lets us keep future/past story cards out of the DOM — and off the compositor
|
||||
* (each has `will-change`) — until their beat is actually on screen, so no
|
||||
* workflow state is rendered before activation. Visually identical, since a
|
||||
* card outside its window is opacity:0 anyway. */
|
||||
function useInWindow(mv: MotionValue<number>, threshold = 0.01): boolean {
|
||||
// `mv` is an external mutable store (a MotionValue). useTransform `.set()`s its
|
||||
// output synchronously while the PARENT renders, so a plain `.on("change") -> setState`
|
||||
// updates this component during the parent's render (React warns). useSyncExternalStore
|
||||
// is built for exactly this: it reads a snapshot and reconciles store-changes-during-
|
||||
// render safely. The snapshot is a primitive boolean, so it never re-renders needlessly.
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => mv.on("change", onStoreChange),
|
||||
() => mv.get() > threshold,
|
||||
() => mv.get() > threshold,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/** Active step index from scroll progress (−1 before the engine starts). */
|
||||
function stepFromProgress(p: number): number {
|
||||
@@ -69,33 +53,51 @@ function StepRail({ active }: { active: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** One cross-fading workflow card pinned to the lower-left. */
|
||||
/**
|
||||
* One workflow card that travels through the story. The outer anchor pins it to
|
||||
* its stage position (Left / Center / Right / Center-Hero); the inner motion card
|
||||
* slides + scales into that anchor in lockstep with scroll, so as the camera moves
|
||||
* through the stages the card visibly moves with the narrative instead of sitting
|
||||
* fixed in one corner.
|
||||
*/
|
||||
function StoryCard({
|
||||
step,
|
||||
index,
|
||||
pos,
|
||||
opacity,
|
||||
y,
|
||||
x,
|
||||
scale,
|
||||
num,
|
||||
kicker,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
step: number;
|
||||
index: number;
|
||||
pos: "left" | "center" | "right" | "hero";
|
||||
opacity: MotionValue<number>;
|
||||
y: MotionValue<number>;
|
||||
x: MotionValue<number>;
|
||||
scale: MotionValue<number>;
|
||||
num: string;
|
||||
kicker: string;
|
||||
title: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
// Don't mount this beat's card until its cross-fade window opens.
|
||||
if (!useInWindow(opacity)) return null;
|
||||
// Don't mount this beat's card until its step is active.
|
||||
if (step !== index) return null;
|
||||
return (
|
||||
<motion.div className="dm-lb-card-story" style={{ opacity, y }}>
|
||||
<div className="dm-lb-card-story__head">
|
||||
<span className="dm-lb-pillar__num">{num}</span>
|
||||
<span className="dm-lb-pillar__kicker">{kicker}</span>
|
||||
</div>
|
||||
<h3 className="dm-lb-pillar__title">{title}</h3>
|
||||
{children}
|
||||
</motion.div>
|
||||
<div className={`dm-lb-card-anchor is-${pos}`}>
|
||||
<motion.div className="dm-lb-card-story" style={{ opacity, y, x, scale }}>
|
||||
<div className="dm-lb-card-story__head">
|
||||
<span className="dm-lb-pillar__num">{num}</span>
|
||||
<span className="dm-lb-pillar__kicker">{kicker}</span>
|
||||
</div>
|
||||
<h3 className="dm-lb-pillar__title">{title}</h3>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,6 +196,22 @@ export default function LogisticsBrainSection({ connected = false }: { connected
|
||||
const p5o = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [0, 1, 1, 0]);
|
||||
const p5y = useTransform(scroll, [0.75, 0.79], [26, 0]);
|
||||
|
||||
// Horizontal slide + scale per beat — same windows as the opacity above, so the
|
||||
// card glides between its stage anchors (Left → Center → Right → Left → Center-Hero)
|
||||
// in lockstep with the camera. Each card enters from the direction of the previous
|
||||
// stage and drifts toward the next as it leaves, reading as one continuous travel.
|
||||
const p1x = useTransform(scroll, [0.135, 0.165, 0.255, 0.275], [-52, 0, 0, 52]);
|
||||
const p1s = useTransform(scroll, [0.135, 0.165, 0.255, 0.275], [0.965, 1, 1, 0.965]);
|
||||
const p2x = useTransform(scroll, [0.29, 0.32, 0.415, 0.435], [-52, 0, 0, 52]);
|
||||
const p2s = useTransform(scroll, [0.29, 0.32, 0.415, 0.435], [0.965, 1, 1, 0.965]);
|
||||
const p3x = useTransform(scroll, [0.45, 0.48, 0.575, 0.595], [-52, 0, 0, -52]);
|
||||
const p3s = useTransform(scroll, [0.45, 0.48, 0.575, 0.595], [0.965, 1, 1, 0.965]);
|
||||
const p4x = useTransform(scroll, [0.61, 0.64, 0.715, 0.735], [52, 0, 0, 52]);
|
||||
const p4s = useTransform(scroll, [0.61, 0.64, 0.715, 0.735], [0.965, 1, 1, 0.965]);
|
||||
// Hero (final selection): scales up a touch and holds center as it settles.
|
||||
const p5x = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [-52, 0, 0, 0]);
|
||||
const p5s = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [0.97, 1.05, 1.05, 1.0]);
|
||||
|
||||
const finaleOpacity = useTransform(scroll, [P.finale - 0.02, P.finale + 0.04], [0, 1]);
|
||||
const finaleY = useTransform(scroll, [P.finale - 0.02, P.finale + 0.06], [40, 0]);
|
||||
const taglineOpacity = useTransform(scroll, [P.finale + 0.04, P.finale + 0.1], [0, 1]);
|
||||
@@ -229,18 +247,18 @@ export default function LogisticsBrainSection({ connected = false }: { connected
|
||||
<span className="dm-lb-arrow">↓</span>
|
||||
</motion.div>
|
||||
|
||||
{/* STEP 01 — Generate Routes */}
|
||||
<StoryCard opacity={p1o} y={p1y} num="01" kicker="Generate Routes" title="We create many delivery plans at once">
|
||||
{/* STEP 01 — Generate Routes (card anchored LEFT) */}
|
||||
<StoryCard step={step} index={0} pos="left" opacity={p1o} y={p1y} x={p1x} scale={p1s} num="01" kicker="Generate Routes" title="We create many delivery plans at once">
|
||||
<div className="dm-lb-chips">
|
||||
{STRATEGIES.map((s) => (
|
||||
<span key={s} className="dm-lb-chip">{s}</span>
|
||||
{STRATEGIES.map((s, i) => (
|
||||
<span key={s} className={`dm-lb-chip${i === WINNER_INDEX ? " dm-lb-chip--active" : ""}`}>{s}</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="dm-lb-pillar__foot">6 different ways to deliver all 59 orders — generated in milliseconds.</p>
|
||||
</StoryCard>
|
||||
|
||||
{/* STEP 02 — Check Constraints (the EV paradox) */}
|
||||
<StoryCard opacity={p2o} y={p2y} num="02" kicker="Check Constraints" title="Every plan must respect real-world limits">
|
||||
{/* STEP 02 — Check Constraints (card anchored CENTER) */}
|
||||
<StoryCard step={step} index={1} pos="center" opacity={p2o} y={p2y} x={p2x} scale={p2s} num="02" kicker="Check Constraints" title="Every plan must respect real-world limits">
|
||||
<ul className="dm-lb-constraints">
|
||||
{CONSTRAINT_LIST.map((c) => (
|
||||
<li key={c.label}>
|
||||
@@ -253,8 +271,8 @@ export default function LogisticsBrainSection({ connected = false }: { connected
|
||||
<p className="dm-lb-pillar__stat"><strong>59/59</strong> delivered <em>vs 34/59 when battery limits are ignored</em></p>
|
||||
</StoryCard>
|
||||
|
||||
{/* STEP 03 — Score & Compare (the leaderboard) */}
|
||||
<StoryCard opacity={p3o} y={p3y} num="03" kicker="Score & Compare" title="Each plan is scored by total delivery cost">
|
||||
{/* STEP 03 — Score & Compare (card anchored RIGHT) */}
|
||||
<StoryCard step={step} index={2} pos="right" opacity={p3o} y={p3y} x={p3x} scale={p3s} num="03" kicker="Score & Compare" title="Each plan is scored by total delivery cost">
|
||||
<ul className="dm-lb-board">
|
||||
{STRATEGY_SCORES.map((s) => (
|
||||
<li key={s.name} className={s.win ? "is-win" : ""}>
|
||||
@@ -266,8 +284,8 @@ export default function LogisticsBrainSection({ connected = false }: { connected
|
||||
</ul>
|
||||
</StoryCard>
|
||||
|
||||
{/* STEP 04 — Guarantee On-Time */}
|
||||
<StoryCard opacity={p4o} y={p4y} num="04" kicker="Guarantee On-Time" title="Any plan even 1 minute late is rejected">
|
||||
{/* STEP 04 — Guarantee On-Time (card anchored LEFT) */}
|
||||
<StoryCard step={step} index={3} pos="left" opacity={p4o} y={p4y} x={p4x} scale={p4s} num="04" kicker="Guarantee On-Time" title="Any plan even 1 minute late is rejected">
|
||||
<div className="dm-lb-sla">
|
||||
<span className="dm-lb-sla__badge">⏱️ On-time only</span>
|
||||
<span className="dm-lb-sla__x">✕ Late plan → dropped</span>
|
||||
@@ -275,8 +293,8 @@ export default function LogisticsBrainSection({ connected = false }: { connected
|
||||
<p className="dm-lb-pillar__foot">We only keep plans that hit every promised delivery window.</p>
|
||||
</StoryCard>
|
||||
|
||||
{/* STEP 05 — Pick & Dispatch */}
|
||||
<StoryCard opacity={p5o} y={p5y} num="05" kicker="Pick & Dispatch" title="The winning plan is sent to the fleet">
|
||||
{/* STEP 05 — Pick & Dispatch (card anchored CENTER, hero) */}
|
||||
<StoryCard step={step} index={4} pos="hero" opacity={p5o} y={p5y} x={p5x} scale={p5s} num="05" kicker="Pick & Dispatch" title="The winning plan is sent to the fleet">
|
||||
<div className="dm-lb-winner">✓ Multi-Trip selected — lowest cost, zero delays</div>
|
||||
<div className="dm-lb-chips">
|
||||
<span className="dm-lb-chip">EV Bikes</span>
|
||||
@@ -393,62 +411,76 @@ const styles = `
|
||||
.dm-lb-arrow { font-size: 18px; animation: dmLbBob 1.8s ease-in-out infinite; }
|
||||
@keyframes dmLbBob { 0%,100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(6px); opacity: 1; } }
|
||||
|
||||
/* ---- Lower-left workflow card (glass panel, cross-fades per step) ---- */
|
||||
.dm-lb-card-story { position: absolute; left: clamp(18px, 4vw, 56px); bottom: clamp(26px, 7vh, 64px);
|
||||
width: min(440px, 84vw); pointer-events: auto; will-change: opacity, transform;
|
||||
/* ---- Story card: a premium light-glass panel that TRAVELS between stage
|
||||
anchors. The anchor pins the stage position; the inner card slides/scales into
|
||||
it (Left → Center → Right → Left → Center-Hero) in lockstep with scroll. ---- */
|
||||
.dm-lb-card-anchor { position: absolute; bottom: clamp(26px, 7vh, 64px); z-index: 6; pointer-events: none; }
|
||||
.dm-lb-card-anchor.is-left { left: clamp(18px, 4vw, 56px); }
|
||||
.dm-lb-card-anchor.is-right { right: clamp(18px, 4vw, 56px); }
|
||||
.dm-lb-card-anchor.is-center,
|
||||
.dm-lb-card-anchor.is-hero { left: 50%; transform: translateX(-50%); }
|
||||
/* Hero (final selection) sits a little higher + centred so it reads as the payoff. */
|
||||
.dm-lb-card-anchor.is-hero { bottom: clamp(40px, 9vh, 92px); }
|
||||
|
||||
.dm-lb-card-story { position: relative; width: min(440px, 84vw); pointer-events: auto;
|
||||
will-change: opacity, transform; transform-origin: bottom center;
|
||||
padding: 18px 20px; border-radius: 18px;
|
||||
background: rgba(14,8,10,0.9); border: 1px solid rgba(226,53,66,0.22);
|
||||
/* backdrop blur removed — this card cross-fades/translates on scroll, so the blur
|
||||
was recomputed every frame; a near-opaque fill keeps the look at no per-frame cost. */
|
||||
box-shadow: 0 24px 64px -30px rgba(0,0,0,0.92); }
|
||||
/* Premium light glass — clean SaaS surface, brand red used only as a top accent. */
|
||||
background: rgba(255,255,255,0.94);
|
||||
border: 1px solid rgba(15,23,42,0.08); border-top: 3px solid #C01227;
|
||||
box-shadow: 0 28px 70px -34px rgba(15,23,42,0.45); }
|
||||
.dm-lb-card-anchor.is-hero .dm-lb-card-story { width: min(480px, 88vw);
|
||||
box-shadow: 0 38px 92px -34px rgba(192,18,39,0.4); }
|
||||
.dm-lb-card-story__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.dm-lb-pillar__num { font-size: 12px; font-weight: 700; letter-spacing: 0.1em; color: #ffffff;
|
||||
background: linear-gradient(135deg, #E2354A, #C01227); border-radius: 7px; padding: 3px 8px; }
|
||||
.dm-lb-pillar__kicker { font-size: clamp(11px, 1.1vw, 13px); font-weight: 700; letter-spacing: 0.18em;
|
||||
text-transform: uppercase; color: #F2667A; }
|
||||
.dm-lb .dm-lb-pillar__title { margin: 0 0 12px !important; padding: 0 !important; color: #fbf5f6 !important;
|
||||
text-transform: uppercase; color: #C01227; }
|
||||
.dm-lb .dm-lb-pillar__title { margin: 0 0 12px !important; padding: 0 !important; color: #0f172a !important;
|
||||
font-weight: 700 !important; text-transform: none !important; letter-spacing: -0.015em !important;
|
||||
font-size: clamp(17px, 1.9vw, 24px) !important; line-height: 1.18 !important;
|
||||
text-shadow: 0 0 30px rgba(192,18,39,0.3) !important; }
|
||||
.dm-lb-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
|
||||
.dm-lb-chip { font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em; color: #f1dadd;
|
||||
padding: 4px 11px; border-radius: 999px; background: rgba(192,18,39,0.12);
|
||||
border: 1px solid rgba(226,53,66,0.30); white-space: nowrap; }
|
||||
.dm-lb-pillar__foot { margin: 0; font-size: clamp(12px, 1.1vw, 13.5px); line-height: 1.45; color: rgba(236,224,226,0.72); }
|
||||
.dm-lb-pillar__stat { margin: 6px 0 0; font-size: clamp(12.5px, 1.2vw, 15px); color: rgba(236,224,226,0.78); }
|
||||
.dm-lb-pillar__stat strong { color: #4ade80; font-weight: 800; font-size: 1.25em; text-shadow: 0 0 20px rgba(34,197,94,0.5); }
|
||||
.dm-lb-pillar__stat em { font-style: normal; color: rgba(230,218,220,0.55); }
|
||||
font-size: clamp(17px, 1.9vw, 24px) !important; line-height: 1.18 !important; }
|
||||
.dm-lb-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 10px; }
|
||||
/* Strategy pills — white pills, soft border + light shadow, brand-red active state. */
|
||||
.dm-lb-chip { font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em; color: #334155;
|
||||
padding: 5px 12px; border-radius: 999px; background: #ffffff;
|
||||
border: 1px solid rgba(15,23,42,0.1); box-shadow: 0 1px 2px rgba(15,23,42,0.06); white-space: nowrap; }
|
||||
.dm-lb-chip--active { color: #ffffff; background: linear-gradient(135deg, #E2354A, #C01227);
|
||||
border-color: transparent; box-shadow: 0 6px 16px -6px rgba(192,18,39,0.5); }
|
||||
.dm-lb-pillar__foot { margin: 0; font-size: clamp(12px, 1.1vw, 13.5px); line-height: 1.45; color: #475569; }
|
||||
.dm-lb-pillar__stat { margin: 6px 0 0; font-size: clamp(12.5px, 1.2vw, 15px); color: #475569; }
|
||||
.dm-lb-pillar__stat strong { color: #16a34a; font-weight: 800; font-size: 1.25em; }
|
||||
.dm-lb-pillar__stat em { font-style: normal; color: #94a3b8; }
|
||||
|
||||
/* Constraints checklist (step 02) */
|
||||
.dm-lb-constraints { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 7px; }
|
||||
.dm-lb-constraints li { display: flex; align-items: center; gap: 9px; }
|
||||
.dm-lb-constraints__icon { font-size: 14px; width: 20px; text-align: center; }
|
||||
.dm-lb-constraints__label { font-size: 13px; font-weight: 700; color: #fbeff0; min-width: 84px; }
|
||||
.dm-lb-constraints__note { font-size: 12px; color: rgba(232,222,224,0.6); }
|
||||
.dm-lb-constraints__label { font-size: 13px; font-weight: 700; color: #0f172a; min-width: 84px; }
|
||||
.dm-lb-constraints__note { font-size: 12px; color: #64748b; }
|
||||
|
||||
/* Scored leaderboard (step 03) */
|
||||
.dm-lb-board { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
||||
.dm-lb-board li { display: grid; grid-template-columns: 104px 1fr 26px; align-items: center; gap: 9px; }
|
||||
.dm-lb-board__name { font-size: 11.5px; font-weight: 600; color: rgba(234,226,228,0.68); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
.dm-lb-board li.is-win .dm-lb-board__name { color: #fff; font-weight: 800; }
|
||||
.dm-lb-board__name { font-size: 11.5px; font-weight: 600; color: #64748b; display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
.dm-lb-board li.is-win .dm-lb-board__name { color: #0f172a; font-weight: 800; }
|
||||
.dm-lb-board__tag { font-size: 8px; font-weight: 800; letter-spacing: 0.08em; color: #fff;
|
||||
background: linear-gradient(135deg,#E2354A,#C01227); padding: 2px 5px; border-radius: 5px; }
|
||||
.dm-lb-board__track { height: 7px; border-radius: 999px; background: rgba(255,255,255,0.08); overflow: hidden; }
|
||||
.dm-lb-board__fill { display: block; height: 100%; border-radius: 999px; background: rgba(150,150,165,0.5); }
|
||||
.dm-lb-board li.is-win .dm-lb-board__fill { background: linear-gradient(90deg,#E2354A,#C01227); box-shadow: 0 0 12px rgba(226,53,66,0.6); }
|
||||
.dm-lb-board__score { font-size: 12px; font-weight: 700; color: rgba(234,226,228,0.68); text-align: right; }
|
||||
.dm-lb-board li.is-win .dm-lb-board__score { color: #fff; }
|
||||
.dm-lb-board__track { height: 7px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
|
||||
.dm-lb-board__fill { display: block; height: 100%; border-radius: 999px; background: rgba(100,116,139,0.45); }
|
||||
.dm-lb-board li.is-win .dm-lb-board__fill { background: linear-gradient(90deg,#E2354A,#C01227); box-shadow: 0 0 12px rgba(226,53,66,0.4); }
|
||||
.dm-lb-board__score { font-size: 12px; font-weight: 700; color: #64748b; text-align: right; }
|
||||
.dm-lb-board li.is-win .dm-lb-board__score { color: #0f172a; }
|
||||
|
||||
/* SLA badges (step 04) */
|
||||
.dm-lb-sla { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.dm-lb-sla__badge { font-size: 12px; font-weight: 700; color: #86efac; background: rgba(34,197,94,0.1);
|
||||
border: 1px solid rgba(34,197,94,0.32); padding: 6px 12px; border-radius: 999px; }
|
||||
.dm-lb-sla__x { font-size: 12px; font-weight: 700; color: #fca5a5; background: rgba(239,68,68,0.1);
|
||||
border: 1px solid rgba(239,68,68,0.32); padding: 6px 12px; border-radius: 999px; }
|
||||
.dm-lb-sla__badge { font-size: 12px; font-weight: 700; color: #15803d; background: rgba(34,197,94,0.1);
|
||||
border: 1px solid rgba(34,197,94,0.3); padding: 6px 12px; border-radius: 999px; }
|
||||
.dm-lb-sla__x { font-size: 12px; font-weight: 700; color: #b91c1c; background: rgba(239,68,68,0.08);
|
||||
border: 1px solid rgba(239,68,68,0.28); padding: 6px 12px; border-radius: 999px; }
|
||||
|
||||
/* Winner banner (step 05) */
|
||||
.dm-lb-winner { font-size: 13.5px; font-weight: 700; color: #fff; margin-bottom: 10px; padding: 9px 13px; border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(192,18,39,0.24), rgba(34,197,94,0.16)); border: 1px solid rgba(226,53,66,0.4); }
|
||||
.dm-lb-winner { font-size: 13.5px; font-weight: 700; color: #0f172a; margin-bottom: 10px; padding: 9px 13px; border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(192,18,39,0.08), rgba(34,197,94,0.08)); border: 1px solid rgba(226,53,66,0.32); }
|
||||
|
||||
/* ---- Finale: KPI cards ---- */
|
||||
.dm-lb-finale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0 20px; }
|
||||
@@ -478,7 +510,15 @@ const styles = `
|
||||
.dm-lb { height: 400vh; }
|
||||
.dm-lb-kpis { gap: 12px; }
|
||||
.dm-lb-kpi { min-width: 96px; padding: 14px 14px; }
|
||||
.dm-lb-card-story { left: 0; right: 0; margin: 0 auto; width: calc(100% - 28px); bottom: clamp(20px, 5vh, 44px); padding: 14px 16px; }
|
||||
/* On phones every stage collapses to one centred, full-width position — the
|
||||
horizontal travel only reads on wider screens. */
|
||||
.dm-lb-card-anchor,
|
||||
.dm-lb-card-anchor.is-left,
|
||||
.dm-lb-card-anchor.is-right,
|
||||
.dm-lb-card-anchor.is-center,
|
||||
.dm-lb-card-anchor.is-hero { left: 50%; right: auto; transform: translateX(-50%); bottom: clamp(20px, 5vh, 44px); }
|
||||
.dm-lb-card-story,
|
||||
.dm-lb-card-anchor.is-hero .dm-lb-card-story { width: calc(100vw - 28px); padding: 14px 16px; }
|
||||
.dm-lb-board li { grid-template-columns: 88px 1fr 24px; }
|
||||
.dm-lb-constraints__note { display: none; }
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function makeRouteCurve(i: number): THREE.CatmullRomCurve3 {
|
||||
* route is invalid, the brain recalculates, a charging station rises and a new
|
||||
* green optimized route lights up.
|
||||
*/
|
||||
function Routes({ progress }: Props) {
|
||||
function Routes({ progress, isMobile = false }: Props) {
|
||||
const eased = useRef(0);
|
||||
|
||||
const tubeMats = useRef<(THREE.MeshBasicMaterial | null)[]>([]);
|
||||
@@ -295,6 +295,10 @@ function Routes({ progress }: Props) {
|
||||
{labelPos.map((pos, i) => {
|
||||
const isWinner = i === WINNER;
|
||||
const isReject = i === REJECT_INDEX;
|
||||
// Mobile clarity: hide ALL floating strategy labels (Multi-Trip, EV-Aware,
|
||||
// Balanced, Best, Time-Aware …) so the small canvas shows only the routing
|
||||
// nodes/network — these screen-space labels overlap badly at 320–390px.
|
||||
if (isMobile) return null;
|
||||
const dotColor = isWinner || isReject ? C.red : ROUTE_COLORS[i];
|
||||
return (
|
||||
<Html key={`lbl${i}`} position={[pos.x, pos.y, pos.z]} center zIndexRange={[30, 0]} style={{ pointerEvents: "none" }}>
|
||||
@@ -342,13 +346,16 @@ function Routes({ progress }: Props) {
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{/* Recharge hub label (the "Kitchen / Recharge" in the EV paradox) */}
|
||||
<Html position={[stationPos.x, 1.7, stationPos.z]} center zIndexRange={[30, 0]} style={{ pointerEvents: "none" }}>
|
||||
<div ref={stationLabelRef} style={{ ...labelBase, border: "1px solid rgba(34,197,94,0.65)", boxShadow: "0 0 18px rgba(34,197,94,0.45)" }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: C.green, boxShadow: `0 0 8px ${C.green}` }} />
|
||||
Recharge Hub
|
||||
</div>
|
||||
</Html>
|
||||
{/* Recharge hub label (the "Kitchen / Recharge" in the EV paradox).
|
||||
Hidden on mobile to keep the small canvas free of overlapping overlays. */}
|
||||
{!isMobile && (
|
||||
<Html position={[stationPos.x, 1.7, stationPos.z]} center zIndexRange={[30, 0]} style={{ pointerEvents: "none" }}>
|
||||
<div ref={stationLabelRef} style={{ ...labelBase, border: "1px solid rgba(34,197,94,0.65)", boxShadow: "0 0 18px rgba(34,197,94,0.45)" }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: C.green, boxShadow: `0 0 8px ${C.green}` }} />
|
||||
Recharge Hub
|
||||
</div>
|
||||
</Html>
|
||||
)}
|
||||
|
||||
{/* EV scooter */}
|
||||
<group ref={scooter} visible={false}>
|
||||
|
||||
22
src/components/map/ContactMapEmbed.tsx
Normal file
22
src/components/map/ContactMapEmbed.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* ContactMapEmbed
|
||||
* ---------------------------------------------------------------------------
|
||||
* Client boundary that lazy-loads the Leaflet map. `ssr: false` keeps Leaflet
|
||||
* out of the server bundle and off the critical render path; the skeleton fills
|
||||
* the host container's fixed height so there is zero layout shift (CLS) while
|
||||
* the map chunk loads.
|
||||
*/
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import styles from "./OfficeMap.module.css";
|
||||
|
||||
const OfficeMap = dynamic(() => import("./OfficeMap"), {
|
||||
ssr: false,
|
||||
loading: () => <div className={styles.skeleton} role="presentation" aria-hidden="true" />,
|
||||
});
|
||||
|
||||
export default function ContactMapEmbed() {
|
||||
return <OfficeMap />;
|
||||
}
|
||||
281
src/components/map/OfficeMap.module.css
Normal file
281
src/components/map/OfficeMap.module.css
Normal file
@@ -0,0 +1,281 @@
|
||||
/* ===========================================================================
|
||||
Office satellite map — scoped styles.
|
||||
All Leaflet global classes are namespaced under `.root` via :global() so this
|
||||
module cannot leak into the rest of the app and vice-versa.
|
||||
Colours/radii reference the app's theme (dark surface, #C01227 brand red).
|
||||
The map fills its container 100% — no hard-coded layout values live here.
|
||||
=========================================================================== */
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* inherit the host container's rounded corners so tiles/controls clip cleanly */
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
background: #0b0b0b;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0b0b0b;
|
||||
font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* Visible keyboard focus ring on the map viewport */
|
||||
.map:focus-visible {
|
||||
outline: 2px solid #c01227;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* ---- Office navigation buttons (fly-to controls) ---- */
|
||||
.controls {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
/* Span the full width (with side insets) instead of left:50%+auto-width:
|
||||
an absolutely-positioned auto-width box anchored at left:50% can only grow
|
||||
to 50% of the map, which on a narrow phone forced the city buttons to stack
|
||||
vertically. With left/right insets the row uses the full width and centres. */
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
z-index: 600; /* above tiles + markers; popups open lower so they never collide */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
pointer-events: none; /* let the row be transparent to drags; buttons re-enable */
|
||||
}
|
||||
.controlBtn {
|
||||
pointer-events: auto;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(15, 15, 17, 0.82);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
color: #f5f5f5;
|
||||
font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1;
|
||||
padding: 9px 16px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease,
|
||||
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
|
||||
}
|
||||
.controlBtn:hover {
|
||||
background: rgba(192, 18, 39, 0.9);
|
||||
border-color: #c01227;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.controlBtn:focus-visible {
|
||||
outline: 2px solid #ffffff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.controlBtnActive,
|
||||
.controlBtnActive:hover {
|
||||
background: #c01227;
|
||||
border-color: #c01227;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 6px 18px rgba(192, 18, 39, 0.45);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.controls {
|
||||
top: 12px;
|
||||
gap: 5px;
|
||||
/* Keep all three city buttons on a single line — shrink them (below) so the
|
||||
row fits instead of wrapping to two lines on narrow phones. */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.controlBtn {
|
||||
font-size: 11px;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Branded marker pin ---- */
|
||||
.markerIcon {
|
||||
/* divIcon resets — keep only the pin's own drop shadow */
|
||||
background: transparent;
|
||||
border: 0;
|
||||
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.55));
|
||||
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.markerIcon:hover,
|
||||
.markerIcon:focus-visible {
|
||||
transform: translateY(-3px) scale(1.06);
|
||||
}
|
||||
|
||||
/* ---- Themed Leaflet internals (scoped to this map only) ---- */
|
||||
.root :global(.leaflet-container) {
|
||||
background: #0b0b0b;
|
||||
}
|
||||
|
||||
/* Zoom + attribution controls — dark, on-theme */
|
||||
.root :global(.leaflet-bar) {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.root :global(.leaflet-bar a) {
|
||||
background: rgba(15, 15, 17, 0.92);
|
||||
color: #f5f5f5;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12);
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
.root :global(.leaflet-bar a:hover) {
|
||||
background: #c01227;
|
||||
color: #ffffff;
|
||||
}
|
||||
.root :global(.leaflet-bar a:focus-visible) {
|
||||
outline: 2px solid #c01227;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.root :global(.leaflet-control-attribution) {
|
||||
background: rgba(10, 10, 10, 0.72);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
font-size: 11px;
|
||||
border-radius: 6px 0 0 0;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
.root :global(.leaflet-control-attribution a) {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* Popup — dark card matching the app surfaces */
|
||||
.root :global(.leaflet-popup-content-wrapper) {
|
||||
background: #141416;
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.root :global(.leaflet-popup-content) {
|
||||
margin: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.root :global(.leaflet-popup-tip) {
|
||||
background: #141416;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.root :global(.leaflet-popup-close-button) {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.root :global(.leaflet-popup-close-button:hover) {
|
||||
color: #ffffff;
|
||||
}
|
||||
.root :global(.leaflet-popup-content .office-popup__name) {
|
||||
display: block;
|
||||
}
|
||||
.root :global(.leaflet-popup-content .office-popup__dot) {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
background: #c01227;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background:
|
||||
linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0) 30%,
|
||||
rgba(255, 255, 255, 0.05) 50%,
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
),
|
||||
#101012;
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
from { background-position: 200% 0; }
|
||||
to { background-position: -200% 0; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skeleton { animation: none; }
|
||||
.markerIcon { transition: none; }
|
||||
}
|
||||
|
||||
/* ---- Graceful error fallback ---- */
|
||||
.errorOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background: #101012;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
||||
}
|
||||
.errorTitle {
|
||||
font-size: clamp(15px, 2.4vw, 18px);
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
.errorText {
|
||||
font-size: 13px;
|
||||
max-width: 38ch;
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.errorList {
|
||||
list-style: none;
|
||||
margin: 4px 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
.errorList li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #f1f1f1;
|
||||
}
|
||||
.errorList li::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #c01227;
|
||||
}
|
||||
|
||||
/* ---- Screen-reader-only office list (semantic fallback) ---- */
|
||||
.srOnly {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
264
src/components/map/OfficeMap.tsx
Normal file
264
src/components/map/OfficeMap.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* OfficeMap
|
||||
* ---------------------------------------------------------------------------
|
||||
* Client-only Leaflet satellite map (Esri World Imagery) rendering the three
|
||||
* Doormile office markers, plus a row of "jump to office" buttons that fly the
|
||||
* map to a selected office's coordinates and open its popup.
|
||||
*
|
||||
* Loaded via a `ssr:false` dynamic import so Leaflet (which touches `window`)
|
||||
* never runs on the server and cannot cause hydration mismatches. Layout/spacing
|
||||
* is owned by the host container (see ContactMap).
|
||||
*/
|
||||
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import styles from "./OfficeMap.module.css";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import L from "leaflet";
|
||||
import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "react-leaflet";
|
||||
|
||||
import {
|
||||
ESRI_WORLD_IMAGERY,
|
||||
MAP_FIT_MAX_ZOOM,
|
||||
MAP_FIT_PADDING,
|
||||
MAP_FOCUS_ZOOM,
|
||||
MAP_INITIAL_CENTER,
|
||||
MAP_INITIAL_ZOOM,
|
||||
OFFICE_LOCATIONS,
|
||||
type LatLng,
|
||||
} from "./offices";
|
||||
|
||||
type MapStatus = "loading" | "ready" | "error";
|
||||
|
||||
/** A request to focus a specific office. `nonce` lets the same office be re-selected. */
|
||||
type FocusTarget = { id: string; nonce: number };
|
||||
|
||||
/** Build the branded SVG pin once (module scope is fine — this file is client-only). */
|
||||
function createMarkerIcon(): L.DivIcon {
|
||||
return L.divIcon({
|
||||
className: styles.markerIcon,
|
||||
html: `
|
||||
<svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
||||
<path d="M15 0C6.72 0 0 6.72 0 15c0 10.5 13.06 23.86 13.62 24.42a1.95 1.95 0 0 0 2.76 0C16.94 38.86 30 25.5 30 15 30 6.72 23.28 0 15 0Z" fill="#C01227"/>
|
||||
<path d="M15 1.5C7.54 1.5 1.5 7.54 1.5 15c0 9.6 12.3 22.4 12.83 22.94a.95.95 0 0 0 1.34 0C16.2 37.4 28.5 24.6 28.5 15 28.5 7.54 22.46 1.5 15 1.5Z" fill="none" stroke="#ffffff" stroke-width="1.2" stroke-opacity="0.85"/>
|
||||
<circle cx="15" cy="15" r="5.4" fill="#ffffff"/>
|
||||
</svg>
|
||||
`,
|
||||
iconSize: [30, 40],
|
||||
iconAnchor: [15, 40],
|
||||
popupAnchor: [0, -36],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Imperative map effects that need the Leaflet instance:
|
||||
* - fit the viewport to every marker (with edge padding)
|
||||
* - keep that framing correct across resizes / lazy reveals
|
||||
* - fly to a single office when one is selected via the buttons
|
||||
*/
|
||||
function MapController({
|
||||
positions,
|
||||
focus,
|
||||
markerRefs,
|
||||
}: {
|
||||
positions: LatLng[];
|
||||
focus: FocusTarget | null;
|
||||
markerRefs: React.RefObject<Record<string, L.Marker>>;
|
||||
}) {
|
||||
const map = useMap();
|
||||
|
||||
// Latest focus, read inside the (stable) resize handler without resubscribing.
|
||||
const focusRef = useRef(focus);
|
||||
useEffect(() => {
|
||||
focusRef.current = focus;
|
||||
}, [focus]);
|
||||
|
||||
const fitAll = useCallback(() => {
|
||||
if (positions.length === 0) return;
|
||||
map.invalidateSize();
|
||||
map.fitBounds(L.latLngBounds(positions), {
|
||||
padding: MAP_FIT_PADDING,
|
||||
maxZoom: MAP_FIT_MAX_ZOOM,
|
||||
});
|
||||
}, [map, positions]);
|
||||
|
||||
// Initial fit, after the container has its final size.
|
||||
useEffect(() => {
|
||||
const raf = requestAnimationFrame(fitAll);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [fitAll]);
|
||||
|
||||
// Re-measure on container resize; only re-frame all markers when nothing is
|
||||
// focused, so resizing while zoomed into one office doesn't jump the view away.
|
||||
useEffect(() => {
|
||||
const container = map.getContainer();
|
||||
let raf = 0;
|
||||
const observer = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => {
|
||||
map.invalidateSize();
|
||||
if (!focusRef.current) {
|
||||
map.fitBounds(L.latLngBounds(positions), {
|
||||
padding: MAP_FIT_PADDING,
|
||||
maxZoom: MAP_FIT_MAX_ZOOM,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(container);
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [map, positions]);
|
||||
|
||||
// Fly to the selected office, then open its popup once movement settles.
|
||||
useEffect(() => {
|
||||
if (!focus) return;
|
||||
const office = OFFICE_LOCATIONS.find((item) => item.id === focus.id);
|
||||
if (!office) return;
|
||||
|
||||
map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 });
|
||||
|
||||
const marker = markerRefs.current[office.id];
|
||||
if (!marker) return;
|
||||
const openPopup = () => marker.openPopup();
|
||||
map.once("moveend", openPopup);
|
||||
return () => {
|
||||
map.off("moveend", openPopup);
|
||||
};
|
||||
}, [map, focus, markerRefs]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function OfficeMap() {
|
||||
const icon = useMemo(() => createMarkerIcon(), []);
|
||||
const positions = useMemo<LatLng[]>(
|
||||
() => OFFICE_LOCATIONS.map((office) => office.position),
|
||||
[],
|
||||
);
|
||||
const markerRefs = useRef<Record<string, L.Marker>>({});
|
||||
|
||||
const [focus, setFocus] = useState<FocusTarget | null>(null);
|
||||
const focusOffice = useCallback((id: string) => {
|
||||
setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }));
|
||||
}, []);
|
||||
|
||||
const [status, setStatus] = useState<MapStatus>("loading");
|
||||
const loadedRef = useRef(false);
|
||||
const errorCountRef = useRef(0);
|
||||
|
||||
const handleTileLoad = useCallback(() => {
|
||||
loadedRef.current = true;
|
||||
setStatus("ready");
|
||||
}, []);
|
||||
|
||||
// Only surface an error if tiles never render (network/CORS/down). Once any
|
||||
// tile load succeeds the map is considered healthy and stays that way.
|
||||
const handleTileError = useCallback(() => {
|
||||
errorCountRef.current += 1;
|
||||
if (!loadedRef.current && errorCountRef.current >= 6) setStatus("error");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
if (!loadedRef.current) setStatus("error");
|
||||
}, 12_000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.root} role="region" aria-label="Map of Doormile office locations">
|
||||
{/* Semantic, always-available fallback for assistive tech + no-JS/SEO. */}
|
||||
<ul className={styles.srOnly}>
|
||||
{OFFICE_LOCATIONS.map((office) => (
|
||||
<li key={office.id}>
|
||||
{office.name} — latitude {office.position[0]}, longitude {office.position[1]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Jump-to-office navigation buttons. */}
|
||||
<div className={styles.controls} role="group" aria-label="Jump to an office location">
|
||||
{OFFICE_LOCATIONS.map((office) => {
|
||||
const isActive = focus?.id === office.id;
|
||||
return (
|
||||
<button
|
||||
key={office.id}
|
||||
type="button"
|
||||
className={`${styles.controlBtn} ${isActive ? styles.controlBtnActive : ""}`}
|
||||
aria-pressed={isActive}
|
||||
aria-label={`Show ${office.name} on the map`}
|
||||
onClick={() => focusOffice(office.id)}
|
||||
>
|
||||
{office.city}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<MapContainer
|
||||
className={styles.map}
|
||||
center={MAP_INITIAL_CENTER}
|
||||
zoom={MAP_INITIAL_ZOOM}
|
||||
scrollWheelZoom={false}
|
||||
zoomControl={false}
|
||||
attributionControl={false}
|
||||
worldCopyJump
|
||||
>
|
||||
{/* Keep zoom controls clear of the top-row buttons. */}
|
||||
<ZoomControl position="bottomleft" />
|
||||
|
||||
<TileLayer
|
||||
url={ESRI_WORLD_IMAGERY.url}
|
||||
attribution={ESRI_WORLD_IMAGERY.attribution}
|
||||
maxZoom={ESRI_WORLD_IMAGERY.maxZoom}
|
||||
updateWhenIdle
|
||||
keepBuffer={2}
|
||||
eventHandlers={{ load: handleTileLoad, tileerror: handleTileError }}
|
||||
/>
|
||||
|
||||
{OFFICE_LOCATIONS.map((office) => (
|
||||
<Marker
|
||||
key={office.id}
|
||||
position={office.position}
|
||||
icon={icon}
|
||||
keyboard
|
||||
title={office.name}
|
||||
alt={office.name}
|
||||
eventHandlers={{ click: () => focusOffice(office.id) }}
|
||||
ref={(instance) => {
|
||||
if (instance) markerRefs.current[office.id] = instance;
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<span className="office-popup__name">
|
||||
<span className="office-popup__dot" aria-hidden="true" />
|
||||
{office.name}
|
||||
</span>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
|
||||
<MapController positions={positions} focus={focus} markerRefs={markerRefs} />
|
||||
</MapContainer>
|
||||
|
||||
{status === "error" && (
|
||||
<div className={styles.errorOverlay} role="alert">
|
||||
<p className={styles.errorTitle}>Map could not be loaded</p>
|
||||
<p className={styles.errorText}>
|
||||
Please check your connection. Our offices are located in:
|
||||
</p>
|
||||
<ul className={styles.errorList}>
|
||||
{OFFICE_LOCATIONS.map((office) => (
|
||||
<li key={office.id}>{office.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
src/components/map/offices.ts
Normal file
59
src/components/map/offices.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Office location data + map tile configuration for the Contact section map.
|
||||
*
|
||||
* Kept dependency-free (no Leaflet runtime import) so it can be safely consumed
|
||||
* by both server and client modules. Types model `[lat, lng]` tuples that are
|
||||
* directly compatible with Leaflet's `LatLngExpression`.
|
||||
*/
|
||||
|
||||
/** A `[latitude, longitude]` coordinate pair. */
|
||||
export type LatLng = [number, number];
|
||||
|
||||
export interface OfficeLocation {
|
||||
/** Stable, unique key (used for React keys + analytics). */
|
||||
readonly id: string;
|
||||
/** Short city label, shown on the navigation buttons. */
|
||||
readonly city: string;
|
||||
/** Human-readable label shown in the marker popup + a11y fallback. */
|
||||
readonly name: string;
|
||||
/** `[latitude, longitude]`. */
|
||||
readonly position: LatLng;
|
||||
}
|
||||
|
||||
/** The three permanent office markers, ordered north-to-south is irrelevant — bounds are auto-fit. */
|
||||
export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
||||
{ id: "coimbatore", city: "Coimbatore", name: "Coimbatore Office", position: [11.0168, 76.9558] },
|
||||
{ id: "bengaluru", city: "Bengaluru", name: "Bengaluru Office", position: [12.9716, 77.5946] },
|
||||
{ id: "hyderabad", city: "Hyderabad", name: "Hyderabad Office", position: [17.385, 78.4867] },
|
||||
];
|
||||
|
||||
export interface TileLayerConfig {
|
||||
readonly url: string;
|
||||
readonly attribution: string;
|
||||
readonly maxZoom: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Esri "World Imagery" satellite basemap.
|
||||
* Attribution is mandatory per Esri's terms of use.
|
||||
* @see https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9
|
||||
*/
|
||||
export const ESRI_WORLD_IMAGERY: TileLayerConfig = {
|
||||
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||
attribution:
|
||||
'Imagery © <a href="https://www.esri.com/" target="_blank" rel="noopener noreferrer">Esri</a>, Maxar, Earthstar Geographics & the GIS User Community',
|
||||
maxZoom: 19,
|
||||
};
|
||||
|
||||
/** Padding (in px) applied when auto-fitting bounds so markers never touch the edges. */
|
||||
export const MAP_FIT_PADDING: LatLng = [50, 50];
|
||||
|
||||
/** Cap the auto-fit zoom so two close offices don't zoom the map in too far. */
|
||||
export const MAP_FIT_MAX_ZOOM = 7;
|
||||
|
||||
/** City-level zoom used when a single office is selected via the nav buttons. */
|
||||
export const MAP_FOCUS_ZOOM = 13;
|
||||
|
||||
/** Initial center/zoom (roughly the centroid of the offices) used before bounds are fit. */
|
||||
export const MAP_INITIAL_CENTER: LatLng = [14.0, 77.7];
|
||||
export const MAP_INITIAL_ZOOM = 5;
|
||||
@@ -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,81 @@ 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: pulled ~33% closer (radius 18→12) and lower, centred on the depot
|
||||
// with a tighter fov so the route hub fills the small frame and the empty grid
|
||||
// around it is cropped out — the depot → routes → vehicles story reads at 320px.
|
||||
mobile: { radiusStart: 16, radiusEnd: 12, heightStart: 8.5, heightEnd: 6, lookAtY: 2.3, fov: 58 },
|
||||
};
|
||||
|
||||
/** 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 +97,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 +109,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>
|
||||
@@ -451,13 +557,18 @@ const styles = `
|
||||
/* ===== FLOATING CARD — the only colored surface ===== */
|
||||
.dm-opt-card {
|
||||
position: absolute !important;
|
||||
top: 96px !important;
|
||||
/* 20px top inset to match the Workflow 2 / 3 connected cards (.dm-lb-card /
|
||||
.dm-st-card), so the gap above Workflow 1 (between the Control Tower stats
|
||||
bar and this card) is the same small, consistent seam used at every other
|
||||
workflow transition — not the old oversized 96px band. The header below is
|
||||
offset on its own to clear the fixed navbar (see .dm-opt-head). */
|
||||
top: 20px !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) */
|
||||
border-radius: 42px 42px 0 0 !important;
|
||||
border-radius: 28px 28px 0 0 !important;
|
||||
overflow: hidden !important;
|
||||
// background: linear-gradient(165deg, #06101f 0%, #020617 35%, #040d1c 70%, #030a18 100%) !important;
|
||||
// border: 0px solid ${rgba("#ffffff", 0.08)} !important;
|
||||
@@ -499,20 +610,20 @@ const styles = `
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.dm-opt-card {
|
||||
top: 96px !important;
|
||||
top: 20px !important;
|
||||
left: 20px !important;
|
||||
right: 20px !important;
|
||||
bottom: 0 !important;
|
||||
border-radius: 42px 42px 0 0 !important;
|
||||
border-radius: 28px 28px 0 0 !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dm-opt-card {
|
||||
top: 86px !important;
|
||||
top: 10px !important;
|
||||
left: 10px !important;
|
||||
right: 10px !important;
|
||||
bottom: 0 !important;
|
||||
border-radius: 28px 28px 0 0 !important;
|
||||
border-radius: 20px 20px 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,7 +719,13 @@ const styles = `
|
||||
|
||||
/* ===== HEADER — compact, no dead space ===== */
|
||||
.dm-opt-head {
|
||||
position: absolute; top: clamp(18px, 3vh, 36px); left: 50%;
|
||||
/* Offset below the site's fixed navbar (~104px desktop / ~100px mobile when
|
||||
.dm-header-scrolled is active). The card now sits at top:20px (was 96px), so
|
||||
the navbar clearance lives here on the header itself — mirroring how the
|
||||
Workflow 2 / 3 sections offset their inner header while the card stays at 20px.
|
||||
This keeps the eyebrow/title clear of the navbar (z-index 10000) without
|
||||
re-introducing the oversized top gap above Workflow 1. */
|
||||
position: absolute; top: clamp(96px, 12vh, 116px); left: 50%;
|
||||
transform: translateX(-50%); width: min(640px, 90vw); text-align: center;
|
||||
}
|
||||
.dm-opt-eyebrow {
|
||||
@@ -882,6 +999,100 @@ 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;
|
||||
/* Top gap separates this card from the hero stats bar above; sides stay 10px
|
||||
and bottom stays flush so the Performance card butts against it below. */
|
||||
margin: 16px 10px 0;
|
||||
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; }
|
||||
|
||||
@@ -1,73 +1,11 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ScrollReveal } from "@/animations/Reveal";
|
||||
import { blogPosts } from "@/data/blog";
|
||||
|
||||
export default function BlogGrid() {
|
||||
const blogs = [
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "How AI Is Transforming Last-Mile EV Delivery",
|
||||
excerpt: "Machine learning and real-time data are reshaping how fleets plan, dispatch, and adapt — making every kilometre smarter than the last.",
|
||||
category: "Technology",
|
||||
image: "/images/blog-post-pic-17.png",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "The EV Paradox: Solving Range Anxiety for Urban Fleets",
|
||||
excerpt: "Electric vehicles promise sustainability, but battery constraints introduce a new routing challenge. Here's how MileTruth™ AI solves it before dispatch.",
|
||||
category: "EV Fleet",
|
||||
image: "/images/blog-post-pic-18-840x840.jpg",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "42% Less Distance: Insights from Our Hyderabad Hub",
|
||||
excerpt: "A detailed look at how Doormile's MileTruth routing engine delivered measurable efficiency gains — fewer vehicles, less fuel, and zero SLA misses.",
|
||||
category: "Case Study",
|
||||
image: "/images/blog-post-pic-15.png",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "MileTruth™ AI — 10 Stages to Smarter Dispatch",
|
||||
excerpt: "From order ingestion to final route output in under 45ms — a technical walkthrough of the ten-stage pipeline at the heart of our routing engine.",
|
||||
category: "MileTruth",
|
||||
image: "/images/blog-post-pic-31.png",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "Why Mathematical Precision Beats Heuristics in Routing",
|
||||
excerpt: "Most routing tools guess. We calculate. Powered by Google OR-Tools, MileTruth evaluates six parallel strategy universes to select the optimal route every time.",
|
||||
category: "Technology",
|
||||
image: "/images/blog-post-pic-14.jpeg",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "Fleet Reduction Without Compromising Delivery Volume",
|
||||
excerpt: "Deploying 37% fewer vehicles while handling the same order volumes isn't a trade-off — it's the result of smarter routing intelligence applied at every dispatch.",
|
||||
category: "Fleet Management",
|
||||
image: "/images/blog-post-pic-8.jpeg",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "Building a Greener City: The Future of Urban Logistics",
|
||||
excerpt: "Cities are demanding cleaner delivery. We explore how AI-powered EV fleets and optimised routing create a path to zero-emission last-mile logistics at city scale.",
|
||||
category: "Sustainability",
|
||||
image: "/images/blog-post-pic-6.jpeg",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "How Doormile Maintains 99.9% SLA Compliance at Scale",
|
||||
excerpt: "Hitting SLA targets 99.9% of the time isn't luck — it's the product of ETA pre-validation, real-time rebalancing, and a routing engine built with delivery reliability as its first constraint.",
|
||||
category: "Operations",
|
||||
image: "/images/blog-post-pic-4.jpeg",
|
||||
},
|
||||
{
|
||||
date: "Apl.06/2025",
|
||||
title: "Battery Simulation: The Secret to EV Route Pre-Validation",
|
||||
excerpt: "Before a single rider leaves the hub, MileTruth™ simulates every route against real charge capacity — eliminating mid-route failures and protecting your fulfillment rate.",
|
||||
category: "EV Fleet",
|
||||
image: "/images/blog-post-pic-3.jpeg",
|
||||
},
|
||||
];
|
||||
const blogs = blogPosts;
|
||||
|
||||
return (
|
||||
<div className="elementor-element elementor-element-c70681e e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-parent" data-id="c70681e" data-element_type="container" data-e-type="container">
|
||||
@@ -114,24 +52,6 @@ export default function BlogGrid() {
|
||||
border-color: rgba(192, 18, 39, 0.2) !important;
|
||||
}
|
||||
|
||||
.custom-blog-date {
|
||||
font-size: 12px !important;
|
||||
font-weight: 700 !important;
|
||||
color: #94a3b8 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 1px !important;
|
||||
margin-bottom: 8px !important;
|
||||
display: block !important;
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
}
|
||||
|
||||
.custom-blog-divider {
|
||||
width: 100% !important;
|
||||
height: 1px !important;
|
||||
background: rgba(0, 0, 0, 0.08) !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.custom-blog-title {
|
||||
font-size: 20px !important;
|
||||
font-weight: 800 !important;
|
||||
@@ -157,18 +77,47 @@ export default function BlogGrid() {
|
||||
font-weight: 500 !important;
|
||||
color: #64748b !important;
|
||||
line-height: 1.6 !important;
|
||||
margin: 0 0 24px 0 !important;
|
||||
margin: 0 0 18px 0 !important;
|
||||
text-transform: none !important;
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
}
|
||||
|
||||
/* Bottom block pinned to the card base — keeps Read More + image at the
|
||||
same vertical position across cards with different text lengths. */
|
||||
.custom-blog-bottom {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
margin-top: auto !important;
|
||||
}
|
||||
|
||||
.custom-blog-readmore {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
align-self: flex-start !important;
|
||||
gap: 6px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 800 !important;
|
||||
color: #c01227 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
margin: 0 0 24px 0 !important;
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
}
|
||||
|
||||
.custom-blog-readmore-arrow {
|
||||
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) !important;
|
||||
}
|
||||
|
||||
.custom-blog-card:hover .custom-blog-readmore-arrow {
|
||||
transform: translateX(5px) !important;
|
||||
}
|
||||
|
||||
.custom-blog-img-container {
|
||||
position: relative !important;
|
||||
width: 100% !important;
|
||||
aspect-ratio: 4 / 3 !important;
|
||||
border-radius: 20px !important;
|
||||
overflow: hidden !important;
|
||||
margin-top: auto !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
@@ -197,44 +146,61 @@ export default function BlogGrid() {
|
||||
<div className="custom-blog-grid">
|
||||
|
||||
{blogs.map((blog, i) => (
|
||||
<ScrollReveal key={i} delay={(i % 3) * 0.08} duration={0.8} yOffset={35}>
|
||||
<div className="custom-blog-card">
|
||||
<ScrollReveal key={blog.slug} delay={(i % 3) * 0.08} duration={0.8} yOffset={35}>
|
||||
<Link href={`/blog/${blog.slug}`} className="custom-blog-card" style={{ textDecoration: "none" }}>
|
||||
{/* Text Block at Top */}
|
||||
<div className="flex flex-col">
|
||||
{/* Date */}
|
||||
<span className="custom-blog-date">
|
||||
{blog.date}
|
||||
</span>
|
||||
|
||||
{/* Thin divider line */}
|
||||
<div className="custom-blog-divider" />
|
||||
|
||||
{/* Bold Title */}
|
||||
<h3 className="custom-blog-title">
|
||||
{blog.title}
|
||||
</h3>
|
||||
|
||||
|
||||
{/* Description Excerpt */}
|
||||
<p className="custom-blog-excerpt">
|
||||
{blog.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Image at Bottom */}
|
||||
<div className="custom-blog-img-container">
|
||||
<Image
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
{/* Category Badge overlay */}
|
||||
<span className="custom-blog-badge">
|
||||
{blog.category}
|
||||
{/* Bottom block: Read more + image, pinned to the card
|
||||
base so Read More aligns across every card regardless
|
||||
of title / excerpt length. */}
|
||||
<div className="custom-blog-bottom">
|
||||
{/* Read more affordance (whole card is the link) */}
|
||||
<span className="custom-blog-readmore">
|
||||
Read More
|
||||
<svg
|
||||
className="custom-blog-readmore-arrow"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{/* Image */}
|
||||
<div className="custom-blog-img-container">
|
||||
<Image
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
{/* Category Badge overlay */}
|
||||
<span className="custom-blog-badge">
|
||||
{blog.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
|
||||
|
||||
276
src/components/sections/BlogPostFooter.tsx
Normal file
276
src/components/sections/BlogPostFooter.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ScrollReveal } from "@/animations/Reveal";
|
||||
import { getAdjacentPosts, getRelatedPosts } from "@/data/blog";
|
||||
|
||||
export default function BlogPostFooter({ slug }: { slug: string }) {
|
||||
const { prev, next } = getAdjacentPosts(slug);
|
||||
const related = getRelatedPosts(slug, 3);
|
||||
|
||||
return (
|
||||
<section className="dm-blog-footer" aria-label="More articles">
|
||||
<style dangerouslySetInnerHTML={{ __html: STYLES }} />
|
||||
|
||||
<div className="dm-blog-footer-inner">
|
||||
{/* Previous / Next */}
|
||||
{(prev || next) && (
|
||||
<nav className="dm-prevnext" aria-label="Article navigation">
|
||||
{prev ? (
|
||||
<Link href={`/blog/${prev.slug}`} className="dm-prevnext-card dm-prevnext-prev">
|
||||
<span className="dm-prevnext-thumb">
|
||||
<Image src={prev.image} alt={prev.title} fill sizes="80px" style={{ objectFit: "cover" }} />
|
||||
</span>
|
||||
<span className="dm-prevnext-text">
|
||||
<span className="dm-prevnext-label">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="19" y1="12" x2="5" y2="12" /><polyline points="12 19 5 12 12 5" />
|
||||
</svg>
|
||||
Previous
|
||||
</span>
|
||||
<span className="dm-prevnext-cat">{prev.category}</span>
|
||||
<span className="dm-prevnext-title">{prev.title}</span>
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className="dm-prevnext-placeholder" />
|
||||
)}
|
||||
{next ? (
|
||||
<Link href={`/blog/${next.slug}`} className="dm-prevnext-card dm-prevnext-next">
|
||||
<span className="dm-prevnext-text">
|
||||
<span className="dm-prevnext-label">
|
||||
Next
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className="dm-prevnext-cat">{next.category}</span>
|
||||
<span className="dm-prevnext-title">{next.title}</span>
|
||||
</span>
|
||||
<span className="dm-prevnext-thumb">
|
||||
<Image src={next.image} alt={next.title} fill sizes="80px" style={{ objectFit: "cover" }} />
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className="dm-prevnext-placeholder" />
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* Related Articles */}
|
||||
{related.length > 0 && (
|
||||
<div className="dm-related">
|
||||
<h2 className="dm-related-heading">Related Articles</h2>
|
||||
<div className="dm-related-grid">
|
||||
{related.map((post, i) => (
|
||||
<ScrollReveal key={post.slug} delay={i * 0.08} duration={0.7} yOffset={30}>
|
||||
<Link href={`/blog/${post.slug}`} className="dm-related-card">
|
||||
<div className="dm-related-img">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
fill
|
||||
sizes="(max-width: 700px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
<span className="dm-related-badge">{post.category}</span>
|
||||
</div>
|
||||
<div className="dm-related-body">
|
||||
<h3 className="dm-related-card-title">{post.title}</h3>
|
||||
<p className="dm-related-card-excerpt">{post.excerpt}</p>
|
||||
<span className="dm-related-readmore">
|
||||
Read More
|
||||
<svg className="dm-related-readmore-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact CTA banner */}
|
||||
<div className="dm-blog-contact-cta">
|
||||
<div className="dm-blog-contact-cta-content">
|
||||
<span className="dm-blog-contact-eyebrow">Let's talk logistics</span>
|
||||
<h2 className="dm-blog-contact-title">
|
||||
Ready to move smarter with Doormile?
|
||||
</h2>
|
||||
<p className="dm-blog-contact-sub">
|
||||
Tell us about your fleet and routes — we'll show you where the
|
||||
distance, vehicles and emissions are hiding.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/contact" className="dm-blog-contact-btn">
|
||||
Get in Touch
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const STYLES = `
|
||||
.dm-blog-footer {
|
||||
font-family: var(--font-manrope), sans-serif; --dm-red: #c01227; --dm-red-hover: #e31d32;
|
||||
/* The global theme applies 72px top/bottom section padding — strip it so the
|
||||
inner container is the single source of vertical rhythm (no double gap). */
|
||||
padding: 0 !important;
|
||||
}
|
||||
/* Neutralize the global theme's 120/80/60px UPPERCASE heading rules */
|
||||
.dm-blog-footer :where(h1, h2, h3, h4, h5, h6) {
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
text-transform: none !important;
|
||||
font-style: normal !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
/* Neutralize the theme's .elementor-kit-5 a (red color + underline) */
|
||||
.dm-blog-footer a { text-decoration: none !important; }
|
||||
/* Shared content container — mirrors SingleBlog's .dm-blog-wrap (same 1280px
|
||||
max-width + 20→40px horizontal padding) so Prev/Next, Related and the CTA
|
||||
align to the exact same left/right edges as the article body above.
|
||||
Vertical rhythm: ~64px from the article end to the Prev/Next divider, then a
|
||||
consistent ~64–72px section→section gap (no 120px+ voids). */
|
||||
.dm-blog-footer-inner {
|
||||
max-width: 1280px; margin: 0 auto;
|
||||
/* Compact vertical rhythm on an 8px system. Top padding sets the
|
||||
article→Prev/Next gap (~24–32px); the inter-section gap sets the
|
||||
Prev/Next→Related gap (~32–48px). No large arbitrary voids. */
|
||||
/* Minimal bottom padding — the global site footer already contributes its
|
||||
own 20px top inset, so the CTA banner sits close to it without a void. */
|
||||
padding: clamp(24px, 3vw, 32px) clamp(20px, 4vw, 40px) clamp(8px, 1.5vw, 16px);
|
||||
display: flex; flex-direction: column; gap: clamp(32px, 4vw, 48px);
|
||||
}
|
||||
|
||||
/* Prev / Next */
|
||||
.dm-prevnext {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
|
||||
/* Halved from 40px: tight divider→cards spacing without crowding. */
|
||||
padding-top: clamp(16px, 2vw, 24px); border-top: 1px solid rgba(15,23,42,0.08);
|
||||
}
|
||||
@media (max-width: 640px) { .dm-prevnext { grid-template-columns: 1fr; } }
|
||||
.dm-prevnext-placeholder { display: block; }
|
||||
.dm-prevnext-card {
|
||||
display: flex; gap: 16px; align-items: center; padding: 16px;
|
||||
background: #fff; border: 1px solid rgba(15,23,42,0.09); border-radius: 22px;
|
||||
text-decoration: none; transition: transform .3s ease, box-shadow .3s ease, border-color .3s ease;
|
||||
}
|
||||
.dm-prevnext-card:hover {
|
||||
transform: translateY(-4px); border-color: rgba(192,18,39,0.2);
|
||||
box-shadow: 0 16px 34px rgba(192,18,39,0.10);
|
||||
}
|
||||
.dm-prevnext-thumb {
|
||||
position: relative; flex: 0 0 80px; width: 80px; height: 80px;
|
||||
border-radius: 16px; overflow: hidden; background: #f1f5f9;
|
||||
}
|
||||
.dm-prevnext-text { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
||||
.dm-prevnext-next { text-align: right; }
|
||||
.dm-prevnext-next .dm-prevnext-text { align-items: flex-end; }
|
||||
.dm-prevnext-label {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; color: var(--dm-red);
|
||||
}
|
||||
.dm-prevnext-cat { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .5px; }
|
||||
.dm-prevnext-title {
|
||||
font-size: 15.5px; font-weight: 700; color: #1e293b; line-height: 1.4;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
transition: color .2s ease;
|
||||
}
|
||||
.dm-prevnext-card:hover .dm-prevnext-title { color: var(--dm-red); }
|
||||
|
||||
/* Related */
|
||||
.dm-related-heading {
|
||||
font-size: clamp(22px, 2.2vw, 28px) !important; font-weight: 850 !important; letter-spacing: -.4px !important;
|
||||
line-height: 1.25 !important; color: #0f172a !important; margin: 0 0 24px;
|
||||
}
|
||||
.dm-related-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px;
|
||||
}
|
||||
@media (max-width: 1024px) { .dm-related-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 700px) { .dm-related-grid { grid-template-columns: 1fr; gap: 24px; } }
|
||||
|
||||
.dm-related-card {
|
||||
display: flex; flex-direction: column; height: 100%;
|
||||
background: #fff; border: 1px solid rgba(15,23,42,0.09); border-radius: 22px;
|
||||
overflow: hidden; box-shadow: 0 4px 24px rgba(15,23,42,0.05); text-decoration: none;
|
||||
transition: transform .4s cubic-bezier(0.2,0.8,0.2,1), box-shadow .4s ease, border-color .4s ease;
|
||||
}
|
||||
.dm-related-card:hover {
|
||||
transform: translateY(-8px); box-shadow: 0 22px 44px rgba(192,18,39,0.13);
|
||||
border-color: rgba(192,18,39,0.2);
|
||||
}
|
||||
.dm-related-img {
|
||||
position: relative; width: 100%; aspect-ratio: 16 / 10; overflow: hidden; background: #f1f5f9;
|
||||
}
|
||||
.dm-related-img img { transition: transform .5s cubic-bezier(0.2,0.8,0.2,1); }
|
||||
.dm-related-card:hover .dm-related-img img { transform: scale(1.05); }
|
||||
.dm-related-badge {
|
||||
position: absolute; top: 14px; left: 14px; z-index: 5; background: var(--dm-red); color: #fff;
|
||||
font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 1.2px;
|
||||
padding: 5px 11px; border-radius: 8px; box-shadow: 0 4px 12px rgba(192,18,39,0.25);
|
||||
}
|
||||
.dm-related-body { display: flex; flex-direction: column; flex: 1; padding: 22px; }
|
||||
.dm-related-card-title {
|
||||
font-size: 17px !important; font-weight: 800 !important; color: #1e293b !important; line-height: 1.4 !important;
|
||||
letter-spacing: -.2px !important; margin: 0 0 10px;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
transition: color .2s ease;
|
||||
}
|
||||
.dm-related-card:hover .dm-related-card-title { color: var(--dm-red); }
|
||||
.dm-related-card-excerpt {
|
||||
font-size: 13.5px; font-weight: 500; color: #64748b; line-height: 1.6; margin: 0 0 18px;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.dm-related-readmore {
|
||||
margin-top: auto; display: inline-flex; align-items: center; gap: 7px;
|
||||
font-size: 12.5px; font-weight: 800; color: var(--dm-red);
|
||||
text-transform: uppercase; letter-spacing: .6px;
|
||||
}
|
||||
.dm-related-readmore-arrow { transition: transform .3s cubic-bezier(0.2,0.8,0.2,1); }
|
||||
.dm-related-card:hover .dm-related-readmore-arrow { transform: translateX(5px); }
|
||||
|
||||
/* Contact CTA banner */
|
||||
.dm-blog-contact-cta {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d1417 100%);
|
||||
border-radius: 30px; padding: clamp(32px, 4vw, 56px);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.dm-blog-contact-cta::after {
|
||||
content: ""; position: absolute; right: -80px; top: -80px; width: 300px; height: 300px;
|
||||
background: radial-gradient(circle, rgba(192,18,39,0.40), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.dm-blog-contact-cta-content { position: relative; z-index: 1; max-width: 640px; }
|
||||
.dm-blog-contact-eyebrow {
|
||||
display: inline-block; font-size: 12px; font-weight: 800; text-transform: uppercase;
|
||||
letter-spacing: 1.4px; color: #ff8088; margin-bottom: 14px;
|
||||
}
|
||||
.dm-blog-contact-title {
|
||||
font-size: clamp(22px, 2.2vw, 28px) !important; font-weight: 800 !important; line-height: 1.25 !important;
|
||||
letter-spacing: -.3px !important; color: #ffffff !important; margin: 0 0 12px; text-wrap: balance;
|
||||
}
|
||||
.dm-blog-contact-sub {
|
||||
font-size: 15.5px; line-height: 1.65; color: #e2e2e2; margin: 0; font-weight: 450;
|
||||
}
|
||||
.dm-blog-contact-btn {
|
||||
position: relative; z-index: 1; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 10px;
|
||||
background: var(--dm-red); color: #fff !important; font-size: 15px; font-weight: 700;
|
||||
padding: 16px 32px; border-radius: 16px; text-decoration: none;
|
||||
box-shadow: 0 10px 26px rgba(192,18,39,0.34);
|
||||
transition: background .2s ease, transform .2s ease;
|
||||
}
|
||||
.dm-blog-contact-btn:hover { background: var(--dm-red-hover); transform: translateY(-2px); }
|
||||
@media (max-width: 720px) {
|
||||
.dm-blog-contact-cta { flex-direction: column; align-items: flex-start; gap: 26px; }
|
||||
.dm-blog-contact-btn { width: 100%; }
|
||||
}
|
||||
`;
|
||||
@@ -10,6 +10,7 @@ export default function BlogsHero() {
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
font-size: clamp(34px, 5.5vw, 68px) !important;
|
||||
font-weight: 850 !important;
|
||||
line-height: 1.08 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: -1.5px !important;
|
||||
margin: 0 !important;
|
||||
@@ -30,22 +31,11 @@ export default function BlogsHero() {
|
||||
{/* Title / Heading for Blogs */}
|
||||
<div style={{ textAlign: "center", color: "#fff", zIndex: 5 }}>
|
||||
<h1 className="blogs-hero-title">
|
||||
Our <span style={{ color: "#C01227" }}>Blogs</span>
|
||||
Delivering Trust.<br />
|
||||
<span style={{ color: "#C01227" }}>Beyond Boundaries</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="elementor-element elementor-element-91be79f elementor-widget__width-auto elementor-absolute elementor-widget elementor-widget-logico_breadcrumbs"
|
||||
style={{ position: "absolute", bottom: "40px", left: "50%", transform: "translateX(-50%)", zIndex: 10 }}
|
||||
>
|
||||
<div className="elementor-widget-container">
|
||||
<nav className="breadcrumbs" style={{ background: "rgba(255, 255, 255, 0.1)", backdropFilter: "blur(10px)", padding: "10px 24px", borderRadius: "30px", border: "1px solid rgba(255, 255, 255, 0.15)" }}>
|
||||
<Link href="/" style={{ color: "#fff", fontWeight: 600 }}>Home</Link>
|
||||
<span className="delimiter" style={{ color: "rgba(255, 255, 255, 0.6)", margin: "0 8px" }}>/</span>
|
||||
<span className="current" style={{ color: "#C01227", fontWeight: 700 }}>Blogs</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function CompetitiveEdge() {
|
||||
<div className="advantage-eyebrow-container">
|
||||
<span className="advantage-eyebrow">/ DoorMile wins/</span>
|
||||
</div>
|
||||
<h2 className="moat-heading" data-text="WHERE DOORMILE SITS AND WHY IT WINS">WHERE DOORMILE SITS AND WHY IT WINS</h2>
|
||||
<h2 className="moat-heading" data-text="WHERE DOORMILE WINS">WHERE DOORMILE WINS</h2>
|
||||
<p className="moat-desc">
|
||||
A side-by-side technical capabilities comparison showing how operational fleet ownership and dynamic AI planning disrupt basic aggregators.
|
||||
</p>
|
||||
@@ -498,26 +498,79 @@ export default function CompetitiveEdge() {
|
||||
.moat-heading {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Capability Matrix table gains responsive horizontal swipe scrolls */
|
||||
/* On phones the 5-column table can't fit, so transform each row into a
|
||||
stacked card: capability as the card title, then one "label: value"
|
||||
line per competitor. Column labels are injected via nth-of-type so no
|
||||
markup changes are needed (every row has the same column order). */
|
||||
@media (max-width: 600px) {
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
min-width: 720px; /* Forces optimal reading width swipe trail */
|
||||
.comparison-table { min-width: 0; width: 100%; }
|
||||
.comparison-table thead { display: none; }
|
||||
.comparison-table tbody,
|
||||
.comparison-table tr,
|
||||
.comparison-table td { display: block; width: auto; }
|
||||
|
||||
.comparison-table tr {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0,0,0,0.07);
|
||||
border-radius: 16px;
|
||||
padding: 6px 16px 8px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 16px 18px;
|
||||
font-size: 0.9rem;
|
||||
/* Capability = card title (icon + name), no label */
|
||||
.comparison-table td.capability-cell {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
font-size: 1.02rem;
|
||||
padding: 12px 0 12px;
|
||||
border-bottom: 1.5px solid #eef0f4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.capability-cell {
|
||||
font-size: 0.98rem;
|
||||
/* Value cells = "Label .... value" rows */
|
||||
.comparison-table td:not(.capability-cell) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f4f5f7;
|
||||
font-size: 0.95rem;
|
||||
text-align: right;
|
||||
}
|
||||
.comparison-table tr td:last-child { border-bottom: none; }
|
||||
|
||||
.comparison-table td.col-highlight::before { content: "Doormile"; }
|
||||
.comparison-table td:nth-of-type(3)::before { content: "Aggregators"; }
|
||||
.comparison-table td:nth-of-type(4)::before { content: "Local Couriers"; }
|
||||
.comparison-table td:nth-of-type(5)::before { content: "Software Platforms"; }
|
||||
.comparison-table td:not(.capability-cell)::before {
|
||||
font-weight: 700;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #8a8f9d;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Neutralise the desktop highlight column styling inside the card */
|
||||
.comparison-table td.col-highlight {
|
||||
background: transparent !important;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
animation: none !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
@@ -8,6 +8,78 @@ import { ScrollReveal, Magnetic } from "@/animations/Reveal";
|
||||
export default function ConnectedLogistics() {
|
||||
return (
|
||||
<div className="elementor-element elementor-element-89a0ca1 e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="89a0ca1" data-element_type="container" data-e-type="container">
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
/* Ensure the inner content wrapper is responsive and never overflows */
|
||||
.elementor-element-fdb2e58 {
|
||||
width: 100% !important;
|
||||
max-width: 650px !important;
|
||||
}
|
||||
|
||||
/* Make heading font size fluid and responsive */
|
||||
.elementor-element-7500280 .logico-title {
|
||||
font-size: clamp(26px, 3.5vw, 54px) !important;
|
||||
line-height: 1.25em !important;
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
/* Make sure paragraphs are responsive on all screen widths */
|
||||
.elementor-element-165dfa5 {
|
||||
width: 100% !important;
|
||||
max-width: min(619px, 100%) !important;
|
||||
}
|
||||
.elementor-element-3888a1e {
|
||||
width: 100% !important;
|
||||
max-width: min(526px, 100%) !important;
|
||||
}
|
||||
|
||||
/* Desktop/Laptop (min-width: 1025px) column width and flex rules */
|
||||
@media (min-width: 1025px) {
|
||||
.elementor-element-9ffed33 {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.elementor-element-96343ba,
|
||||
.elementor-element-71c3e1d {
|
||||
width: 50% !important;
|
||||
max-width: 50% !important;
|
||||
flex: 1 1 50% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* Force columns to stack vertically on mobile/tablet */
|
||||
.elementor-element-9ffed33 {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
/* Force both children to be full width */
|
||||
.elementor-element-96343ba,
|
||||
.elementor-element-71c3e1d {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Show image at the bottom or top depending on order */
|
||||
.elementor-element-96343ba {
|
||||
margin-top: 30px !important;
|
||||
}
|
||||
|
||||
/* Override desktop fixed widths on mobile/tablet */
|
||||
.elementor-element-165dfa5,
|
||||
.elementor-element-3888a1e {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
<div className="elementor-element elementor-element-9ffed33 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9ffed33" data-element_type="container" data-e-type="container" data-settings="{"background_background":"classic"}">
|
||||
|
||||
{/* Image side */}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import React from "react";
|
||||
import ContactMapEmbed from "@/components/map/ContactMapEmbed";
|
||||
|
||||
/**
|
||||
* Contact / Location section.
|
||||
*
|
||||
* The section's layout (20px side padding, 40px top margin, the 25px top-rounded
|
||||
* card that flows into the footer, and the 500px / 360px responsive heights) is
|
||||
* preserved verbatim from the original Elementor markup so there is no visual
|
||||
* regression. Only the *content* of the embed has changed: the Google Maps
|
||||
* iframe is replaced by an interactive Leaflet satellite map (ContactMapEmbed).
|
||||
*/
|
||||
export default function ContactMap() {
|
||||
return (
|
||||
<div className="elementor-element elementor-element-7304a53 e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="7304a53" data-element_type="container" data-e-type="container">
|
||||
@@ -7,36 +17,47 @@ export default function ContactMap() {
|
||||
.elementor-element-7304a53 {
|
||||
--padding-left: 20px;
|
||||
--padding-right: 20px;
|
||||
--margin-top: 40px;
|
||||
--margin-top: 12px;
|
||||
--margin-bottom: 0px;
|
||||
/* Reduce the large gap above the map (was 40px). */
|
||||
margin-top: 12px !important;
|
||||
padding-top: 0 !important;
|
||||
/* Real side padding so the map is inset like the hero/footer sections
|
||||
(the --padding-* vars only work with elementor-frontend.css, which
|
||||
isn't loaded, so the map was going edge-to-edge). Matches
|
||||
.custom-standard-hero-container: 20px desktop / 10px mobile. */
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
box-sizing: border-box;
|
||||
/* The section computes to display:inline here (the --display:flex var
|
||||
isn't mapped without elementor-frontend.css), so its padding never
|
||||
constrained the block child. Force flex so the padding insets the map. */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.elementor-element-7304a53 .elementor-custom-embed {
|
||||
border-radius: 25px 25px 0 0;
|
||||
/* Rounded on all corners so the map reads as a self-contained card
|
||||
(bottom was square before, leaving a hard edge above the footer gap). */
|
||||
border-radius: 25px;
|
||||
overflow: hidden;
|
||||
background: #ededed;
|
||||
background: #0b0b0b;
|
||||
line-height: 0;
|
||||
}
|
||||
.elementor-element-7304a53 .elementor-custom-embed iframe {
|
||||
display: block;
|
||||
filter: grayscale(100%);
|
||||
@media (max-width: 840px) {
|
||||
.elementor-element-7304a53 {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.elementor-element-7304a53 .elementor-custom-embed { height: 360px !important; }
|
||||
}
|
||||
` }} />
|
||||
<div className="elementor-element elementor-element-5a3eed4 elementor-widget elementor-widget-google_maps" data-id="5a3eed4" data-element_type="widget" data-e-type="widget" data-widget_type="google_maps.default">
|
||||
<div className="elementor-element elementor-element-5a3eed4 elementor-widget" data-id="5a3eed4" data-element_type="widget" data-e-type="widget">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="elementor-custom-embed" style={{ width: "100%", height: "500px" }}>
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3806.1918122409634!2d78.35579498480733!3d17.45053110831999!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3bcb93b8c5a049b3%3A0x6f4b5999fccad985!2sJayabheri%20Enclave%2C%20Gachibowli%2C%20Hyderabad%2C%20Telangana!5e0!3m2!1sen!2sin!4v1778663239768!5m2!1sen!2sin"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen={true}
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Doormile Location Map"
|
||||
/>
|
||||
<ContactMapEmbed />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,34 +6,226 @@ export default function ContactsHero() {
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.contacts-hero-custom {
|
||||
background-color: #1f1f1f !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.78) 55%, rgba(0, 0, 0, 0.95) 100%), url('/images/home2-slide-1.jpg') !important;
|
||||
background-color: #0b0b0b !important;
|
||||
background-image: url('/images/home2-banner-3.jpg') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
|
||||
.contacts-hero-card-custom {
|
||||
position: relative !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
flex-direction: column !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.contacts-hero-bg-overlay {
|
||||
position: absolute !important;
|
||||
inset: 0 !important;
|
||||
/* Lighter wash so the red van / sunset stays vivid like the reference,
|
||||
while keeping the centered heading readable. */
|
||||
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.42) 55%, rgba(0, 0, 0, 0.62) 100%) !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
/* Ambient Glowing Orbs */
|
||||
.contacts-hero-glow-red {
|
||||
position: absolute !important;
|
||||
bottom: -15% !important;
|
||||
right: -10% !important;
|
||||
width: min(500px, 75vw) !important;
|
||||
height: min(500px, 75vw) !important;
|
||||
background: radial-gradient(circle, rgba(192, 18, 39, 0.24) 0%, rgba(192, 18, 39, 0) 70%) !important;
|
||||
filter: blur(70px) !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 1 !important;
|
||||
animation: float-glow 10s ease-in-out infinite alternate !important;
|
||||
}
|
||||
|
||||
.contacts-hero-glow-blue {
|
||||
position: absolute !important;
|
||||
top: -15% !important;
|
||||
left: -10% !important;
|
||||
width: min(450px, 60vw) !important;
|
||||
height: min(450px, 60vw) !important;
|
||||
background: radial-gradient(circle, rgba(0, 150, 255, 0.06) 0%, rgba(0, 150, 255, 0) 70%) !important;
|
||||
filter: blur(75px) !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
/* Abstract Tech Grid */
|
||||
.contacts-hero-grid {
|
||||
position: absolute !important;
|
||||
inset: 0 !important;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px) !important;
|
||||
background-size: 50px 50px !important;
|
||||
opacity: 0.8 !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
/* Center content — card frame removed (no background, border, blur or
|
||||
shadow); the text sits directly on the hero background. */
|
||||
.contacts-hero-glass-card {
|
||||
position: relative !important;
|
||||
z-index: 3 !important;
|
||||
background: transparent !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
max-width: 820px !important;
|
||||
width: 90% !important;
|
||||
box-shadow: none !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.contacts-hero-glass-card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Spaced kicker */
|
||||
.contacts-hero-kicker {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
|
||||
.contacts-hero-kicker-line {
|
||||
display: block !important;
|
||||
width: 24px !important;
|
||||
height: 1.5px !important;
|
||||
background: #C01227 !important;
|
||||
border-radius: 1px !important;
|
||||
}
|
||||
|
||||
.contacts-hero-kicker-text {
|
||||
font-size: 13px !important;
|
||||
font-weight: 850 !important;
|
||||
letter-spacing: 4px !important;
|
||||
color: #C01227 !important;
|
||||
text-transform: uppercase !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
/* Bold modern typography */
|
||||
.contacts-hero-title {
|
||||
font-size: clamp(34px, 5.2vw, 62px) !important;
|
||||
font-weight: 850 !important;
|
||||
line-height: 1.15 !important;
|
||||
color: #ffffff !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: -1.8px !important;
|
||||
margin: 0 0 20px 0 !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
.contacts-hero-title-highlight {
|
||||
background: linear-gradient(135deg, #ffffff 40%, #c01227 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Description text */
|
||||
.contacts-hero-desc {
|
||||
font-size: clamp(15px, 1.22vw, 18px) !important;
|
||||
line-height: 1.6 !important;
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto 36px auto !important;
|
||||
font-weight: 500 !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
/* Breadcrumb capsule */
|
||||
.contacts-hero-breadcrumbs {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 10px !important;
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
padding: 8px 22px !important;
|
||||
border-radius: 30px !important;
|
||||
transition: background 0.3s, border-color 0.3s !important;
|
||||
}
|
||||
|
||||
.contacts-hero-breadcrumbs:hover {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.contacts-breadcrumb-link {
|
||||
color: rgba(255, 255, 255, 0.65) !important;
|
||||
text-decoration: none !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
transition: color 0.2s !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
.contacts-breadcrumb-link:hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.contacts-breadcrumb-del {
|
||||
color: rgba(255, 255, 255, 0.3) !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.contacts-breadcrumb-cur {
|
||||
color: #C01227 !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 700 !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
@keyframes float-glow {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
100% { transform: translate(-15px, -25px) scale(1.08); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contacts-hero-glass-card {
|
||||
padding: 0 16px !important;
|
||||
width: 95% !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
<div className="custom-standard-hero-container">
|
||||
<div className="custom-standard-hero-card contacts-hero-custom">
|
||||
<div className="e-con-inner" style={{ position: "relative", width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column" }}>
|
||||
<div className="elementor-element elementor-element-ea205b5 elementor-widget elementor-widget-logico_page_title_line" style={{ textAlign: "center", zIndex: 5 }}>
|
||||
<div className="elementor-widget-container">
|
||||
<h1 className="page-title logico-title-h2" style={{ color: "#fff", fontSize: "clamp(34px, 5.5vw, 68px)", fontWeight: 850, textTransform: "uppercase", letterSpacing: "-1.5px", margin: 0 }}>
|
||||
Contacts
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="contacts-hero-card-custom">
|
||||
<div className="contacts-hero-bg-overlay"></div>
|
||||
<div className="contacts-hero-grid"></div>
|
||||
<div className="contacts-hero-glow-red"></div>
|
||||
<div className="contacts-hero-glow-blue"></div>
|
||||
|
||||
<div
|
||||
className="elementor-element elementor-element-3d13f28 elementor-widget__width-auto elementor-absolute elementor-widget elementor-widget-logico_breadcrumbs"
|
||||
style={{ position: "absolute", bottom: "40px", left: "50%", transform: "translateX(-50%)", zIndex: 10 }}
|
||||
>
|
||||
<div className="elementor-widget-container">
|
||||
<nav className="breadcrumbs" style={{ background: "rgba(255, 255, 255, 0.1)", backdropFilter: "blur(10px)", padding: "10px 24px", borderRadius: "30px", border: "1px solid rgba(255, 255, 255, 0.15)" }}>
|
||||
<Link href="/" style={{ color: "#fff", fontWeight: 600 }}>Home</Link>
|
||||
<span className="delimiter" style={{ color: "rgba(255, 255, 255, 0.6)", margin: "0 8px" }}>/</span>
|
||||
<span className="current" style={{ color: "#C01227", fontWeight: 700 }}>Contacts</span>
|
||||
</nav>
|
||||
<div className="contacts-hero-glass-card">
|
||||
<div className="contacts-hero-kicker">
|
||||
<span className="contacts-hero-kicker-line"></span>
|
||||
<span className="contacts-hero-kicker-text">24/7 support & sales</span>
|
||||
<span className="contacts-hero-kicker-line"></span>
|
||||
</div>
|
||||
|
||||
<h1 className="contacts-hero-title">
|
||||
Get In <span className="contacts-hero-title-highlight">Touch</span>
|
||||
</h1>
|
||||
|
||||
<p className="contacts-hero-desc">
|
||||
Have questions about our smart delivery network, pricing plans, or partner ecosystem? Let's build the future of logistics together.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,3 +234,4 @@ export default function ContactsHero() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export default function EVLogisticSection() {
|
||||
|
||||
.ev-logistic-kicker {
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 2.1429em !important;
|
||||
letter-spacing: 0px !important; /* Expands to 3px on scroll */
|
||||
text-transform: lowercase !important;
|
||||
@@ -408,16 +408,19 @@ export default function EVLogisticSection() {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
/* Base rules above use !important (flex-direction:row, the 58/42%
|
||||
column widths), so these overrides MUST also use !important or the
|
||||
grid never stacks and the image/text stay squished side-by-side. */
|
||||
.ev-logistic-body-grid {
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
flex-direction: column !important;
|
||||
gap: 50px !important;
|
||||
}
|
||||
|
||||
.ev-logistic-image-col {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
min-height: auto;
|
||||
justify-content: center; /* Center layout on mobile */
|
||||
flex: 1 1 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-height: auto !important;
|
||||
justify-content: center !important; /* Center layout on mobile */
|
||||
}
|
||||
|
||||
.ev-logistic-image-wrapper {
|
||||
@@ -428,8 +431,8 @@ export default function EVLogisticSection() {
|
||||
}
|
||||
|
||||
.ev-logistic-content-col {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
flex: 1 1 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ev-logistic-title {
|
||||
@@ -464,7 +467,7 @@ export default function EVLogisticSection() {
|
||||
>
|
||||
{/* Top Header Row with / features / kicker */}
|
||||
<div className="ev-logistic-header">
|
||||
<div className="ev-logistic-kicker">/ features /</div>
|
||||
<div className="ev-logistic-kicker">/ Build Electric Vehicles /</div>
|
||||
</div>
|
||||
|
||||
<div className="ev-logistic-body-grid">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,11 @@ import React, { useState, useEffect } from "react";
|
||||
export default function HowItWorksHero() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
// Auto-slide every 6 seconds
|
||||
// Auto-slide every 7 seconds — slower, more readable, professional pacing
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveSlide((prev) => (prev === 0 ? 1 : 0));
|
||||
}, 6000);
|
||||
}, 7000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
@@ -19,7 +19,9 @@ export default function HowItWorksHero() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.howits-hero-custom-bg.elementor-repeater-item-3264830,
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.png') !important;
|
||||
@@ -57,6 +59,10 @@ export default function HowItWorksHero() {
|
||||
width: 100% !important;
|
||||
max-width: 1000px !important;
|
||||
margin: 0 auto !important;
|
||||
/* site.css gives the inner content-box sizing + 20px padding,
|
||||
which makes it 40px wider than its parent and overflows.
|
||||
Force border-box so width:100% actually fits. */
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .content-slider-item-heading,
|
||||
@@ -77,11 +83,23 @@ export default function HowItWorksHero() {
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .text-content {
|
||||
text-align: center !important;
|
||||
max-width: 800px !important;
|
||||
max-width: 820px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* Larger, more readable hero subtitle on large/4K screens */
|
||||
.elementor-element.elementor-element-6c7cbcb .content-slider-item-text p {
|
||||
font-size: clamp(16px, 1.35vw, 23px) !important;
|
||||
line-height: 1.65 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
/* Match Home's hero frame so the floating navbar sits with the same
|
||||
breathing space. Home (.elementor-61) frames the hero card at 20px;
|
||||
this page falls through to the shared kit's 32px base. */
|
||||
.elementor-element.elementor-element-741f56c {
|
||||
padding: 20px !important;
|
||||
}
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-carousel.owl-theme .content-item {
|
||||
height: 800px !important;
|
||||
min-height: 800px !important;
|
||||
@@ -98,6 +116,12 @@ export default function HowItWorksHero() {
|
||||
border-radius: 25px !important;
|
||||
}
|
||||
|
||||
/* Collapse the stage-outer to the slide height so no empty
|
||||
black space is left below the hero card on mobile/tablet. */
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-stage-outer {
|
||||
height: 620px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-carousel.owl-theme .content-item {
|
||||
width: 100% !important;
|
||||
min-height: 620px !important;
|
||||
@@ -121,59 +145,151 @@ export default function HowItWorksHero() {
|
||||
border-radius: 22px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-stage-outer {
|
||||
height: 560px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-carousel.owl-theme .content-item {
|
||||
min-height: 560px !important;
|
||||
height: 560px !important;
|
||||
border-radius: 22px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .slide-content {
|
||||
/* Center BOTH slides' content. Use the .elementor prefix +
|
||||
repeater-item classes so these beat the per-slide rules in
|
||||
site.css (slide 1 has no centering there; slide 2 carries an
|
||||
asymmetric top margin). */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .slide-content {
|
||||
min-height: 560px !important;
|
||||
padding-left: 22px !important;
|
||||
padding-right: 22px !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .content-slider-item-heading {
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-3264830 .slide-content-inner,
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-6867061 .slide-content-inner {
|
||||
margin: 0 auto !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(38px, 11vw, 48px) !important;
|
||||
line-height: 1.05 !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
align-items: center !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .content-slider-item-heading,
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .content-slider-item-text,
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .text-content {
|
||||
text-align: center !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
/* Smaller, fitting heading so it never overflows the card edge.
|
||||
site.css forced clamp(38px,11vw,48px) which clipped at 320px. */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-3264830 .content-slider-item-heading,
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-6867061 .content-slider-item-heading {
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(26px, 8vw, 34px) !important;
|
||||
line-height: 1.12 !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
<div className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="741f56c" data-element_type="container" data-e-type="container">
|
||||
<div className="elementor-element elementor-element-6c7cbcb elementor-widget elementor-widget-logico_content_slider" data-id="6c7cbcb" data-element_type="widget" data-e-type="widget" data-widget_type="logico_content_slider.default">
|
||||
/* Slide counter ("01/02"): keep it bottom-RIGHT, aligned with the
|
||||
hero text padding (22px) and lifted cleanly off the edge. */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .slider-footer {
|
||||
text-align: right !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .slider-footer .slider-pagination {
|
||||
justify-content: flex-end !important;
|
||||
margin-left: 22px !important;
|
||||
margin-right: 22px !important;
|
||||
}
|
||||
/* The wide dot navigation eats the right side and shoves "01/02" to
|
||||
the centre. Hide it on mobile (matching the home hero) so the
|
||||
counter sits cleanly on the right. */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .owl-dots {
|
||||
display: none !important;
|
||||
}
|
||||
/* Hide the prev/next arrows on mobile (they overlap the counter) —
|
||||
slides still auto-rotate, matching the home hero. */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .owl-nav {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent"
|
||||
data-id="741f56c"
|
||||
data-element_type="container"
|
||||
data-e-type="container"
|
||||
>
|
||||
<div
|
||||
className="elementor-element elementor-element-6c7cbcb elementor-widget elementor-widget-logico_content_slider"
|
||||
data-id="6c7cbcb"
|
||||
data-element_type="widget"
|
||||
data-e-type="widget"
|
||||
data-widget_type="logico_content_slider.default"
|
||||
>
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-content-slider-widget">
|
||||
<div className="content-slider-wrapper">
|
||||
<div className="content-slider-container">
|
||||
<div className="content-slider owl-carousel owl-theme nav-view-vertical nav-h-position-right nav-v-position-bottom owl-loaded owl-drag">
|
||||
|
||||
<div className="owl-stage-outer" style={{ position: "relative", overflow: "hidden", height: "800px" }}>
|
||||
<div className="owl-stage" style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
|
||||
<div
|
||||
className="owl-stage-outer"
|
||||
style={{
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
height: "800px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="owl-stage"
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Slide 1 */}
|
||||
<div
|
||||
<div
|
||||
className={`owl-item ${activeSlide === 0 ? "active" : ""}`}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
opacity: activeSlide === 0 ? 1 : 0,
|
||||
visibility: activeSlide === 0 ? "visible" : "hidden",
|
||||
transition: "opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
|
||||
zIndex: activeSlide === 0 ? 2 : 1
|
||||
visibility:
|
||||
activeSlide === 0 ? "visible" : "hidden",
|
||||
transition:
|
||||
"opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
|
||||
zIndex: activeSlide === 0 ? 2 : 1,
|
||||
}}
|
||||
>
|
||||
<div className="content-item slider-item elementor-repeater-item-3264830 slide-style-standard howits-hero-custom-bg">
|
||||
<div className="slide-content">
|
||||
<div className="slide-content-inner">
|
||||
<h1 className="content-slider-item-heading logico-content-wrapper-1">
|
||||
<span className="heading-content">One Journey. Complete<br />Control.</span>
|
||||
<span className="heading-content">
|
||||
One Journey. Complete
|
||||
<br />
|
||||
Control.
|
||||
</span>
|
||||
</h1>
|
||||
<div className="content-slider-item-text logico-content-wrapper-2">
|
||||
<div className="text-content">
|
||||
<p>See how Doormile connects first, mid, and last mile into a seamless delivery experience powered by MileTruth™ AI.</p>
|
||||
<p>
|
||||
See how Doormile connects first, mid, and
|
||||
last mile into a seamless delivery
|
||||
experience powered by MileTruth™ AI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +298,7 @@ export default function HowItWorksHero() {
|
||||
</div>
|
||||
|
||||
{/* Slide 2 */}
|
||||
<div
|
||||
<div
|
||||
className={`owl-item ${activeSlide === 1 ? "active" : ""}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -190,51 +306,147 @@ export default function HowItWorksHero() {
|
||||
left: 0,
|
||||
width: "100%",
|
||||
opacity: activeSlide === 1 ? 1 : 0,
|
||||
visibility: activeSlide === 1 ? "visible" : "hidden",
|
||||
transition: "opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
|
||||
zIndex: activeSlide === 1 ? 2 : 1
|
||||
visibility:
|
||||
activeSlide === 1 ? "visible" : "hidden",
|
||||
transition:
|
||||
"opacity 0.8s ease-in-out, visibility 0.8s ease-in-out",
|
||||
zIndex: activeSlide === 1 ? 2 : 1,
|
||||
}}
|
||||
>
|
||||
<div className="content-item slider-item elementor-repeater-item-6867061 slide-style-standard howits-hero-custom-bg">
|
||||
<div className="slide-content">
|
||||
<div className="slide-content-inner">
|
||||
<h1 className="content-slider-item-heading logico-content-wrapper-1">
|
||||
<span className="heading-content">A New Freight<br />Experience</span>
|
||||
<span className="heading-content">
|
||||
A New Logisitics
|
||||
<br />
|
||||
Experience
|
||||
</span>
|
||||
</h1>
|
||||
<div className="content-slider-item-text logico-content-wrapper-2">
|
||||
<div className="text-content">
|
||||
<p>See how Doormile connects first, mid, and last mile into a seamless delivery experience powered by MileTruth™ AI.</p>
|
||||
<p>
|
||||
See how Doormile connects first, mid, and
|
||||
last mile into a seamless delivery
|
||||
experience powered by MileTruth™ AI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation controls (prev/next) — matches the home hero slider */}
|
||||
<div className="owl-nav">
|
||||
<button
|
||||
type="button"
|
||||
className="owl-next"
|
||||
onClick={() =>
|
||||
setActiveSlide((prev) => (prev === 0 ? 1 : 0))
|
||||
}
|
||||
aria-label="Next"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="owl-prev"
|
||||
onClick={() =>
|
||||
setActiveSlide((prev) => (prev === 0 ? 1 : 0))
|
||||
}
|
||||
aria-label="Previous"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress indicators */}
|
||||
<div className="slider-footer slider-footer-position-after slider-footer-width-full slider-footer-view-inside">
|
||||
<div className="slider-footer-content">
|
||||
<div className="slider-pagination" style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "10px" }}>
|
||||
<div className="slider-progress-wrapper" style={{ marginRight: "15px" }}>
|
||||
<span className="slider-progress-current">{activeSlide === 0 ? "01" : "02"}</span>
|
||||
/
|
||||
<span className="slider-progress-all">02</span>
|
||||
<div
|
||||
className="slider-pagination"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="slider-progress-wrapper"
|
||||
style={{
|
||||
marginRight: "35px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
color: "#FFFFFF",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
<span className="slider-progress-current">
|
||||
{activeSlide === 0 ? "01" : "02"}
|
||||
</span>
|
||||
{" / "}
|
||||
<span
|
||||
className="slider-progress-all"
|
||||
style={{ opacity: 0.6 }}
|
||||
>
|
||||
02
|
||||
</span>
|
||||
</div>
|
||||
{/* Progress line — red bar slides to match the active slide (mirrors the home hero) */}
|
||||
<div
|
||||
style={{
|
||||
width: "80px",
|
||||
height: "2px",
|
||||
background: "rgba(255, 255, 255, 0.2)",
|
||||
position: "relative",
|
||||
borderRadius: "1px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: activeSlide === 0 ? "0" : "50%",
|
||||
width: "50%",
|
||||
height: "100%",
|
||||
background: "#c01227",
|
||||
transition: "left 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="owl-dots owl-dots-6c7cbcb" style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
type="button"
|
||||
role="button"
|
||||
<div
|
||||
className="owl-dots owl-dots-6c7cbcb"
|
||||
style={{ display: "none" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="button"
|
||||
className={`owl-dot ${activeSlide === 0 ? "active" : ""}`}
|
||||
onClick={() => handleDotClick(0)}
|
||||
>
|
||||
<span></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="button"
|
||||
<button
|
||||
type="button"
|
||||
role="button"
|
||||
className={`owl-dot ${activeSlide === 1 ? "active" : ""}`}
|
||||
onClick={() => handleDotClick(1)}
|
||||
>
|
||||
@@ -244,7 +456,6 @@ export default function HowItWorksHero() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,11 @@ export default function IndexHero() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-slide every 6 seconds
|
||||
// Auto-slide every 7 seconds — slower, more readable, professional pacing
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveSlide((prev) => (prev === 0 ? 1 : 0));
|
||||
}, 6000);
|
||||
}, 7000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
@@ -52,13 +52,114 @@ export default function IndexHero() {
|
||||
|
||||
return (
|
||||
<div className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="741f56c" data-element_type="container" data-e-type="container">
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
/* Fluid responsive font size override for hero headings */
|
||||
.logico-content-slider-widget .content-slider-item-heading {
|
||||
font-size: clamp(30px, 5.5vw, 80px) !important;
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
/* Prevent horizontal overflow on slider and stage containers */
|
||||
.logico-content-slider-widget,
|
||||
.content-slider-wrapper,
|
||||
.content-slider-container,
|
||||
.content-slider,
|
||||
.owl-stage-outer,
|
||||
.owl-stage,
|
||||
.owl-item,
|
||||
.slider-item,
|
||||
.slide-content,
|
||||
.slide-content-inner {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Force word wrapping and responsive spacing for heading and text */
|
||||
.logico-content-slider-widget .content-slider-item-heading,
|
||||
.logico-content-slider-widget .content-slider-item-heading span,
|
||||
.logico-content-slider-widget .content-slider-item-heading .heading-content {
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.logico-content-slider-widget .content-slider-item-heading {
|
||||
padding-left: 15px !important;
|
||||
padding-right: 15px !important;
|
||||
}
|
||||
|
||||
.logico-content-slider-widget .text-content {
|
||||
width: 100% !important;
|
||||
max-width: min(780px, 100%) !important;
|
||||
box-sizing: border-box !important;
|
||||
padding-left: 15px !important;
|
||||
padding-right: 15px !important;
|
||||
}
|
||||
|
||||
/* Larger, more readable hero subtitle on large/4K screens */
|
||||
.logico-content-slider-widget .content-slider-item-text p {
|
||||
font-size: clamp(16px, 1.35vw, 23px) !important;
|
||||
line-height: 1.65 !important;
|
||||
}
|
||||
|
||||
/* Responsive slider heights */
|
||||
.logico-content-slider-widget .owl-stage-outer {
|
||||
height: 800px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.logico-content-slider-widget .owl-stage-outer {
|
||||
height: 600px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.logico-content-slider-widget .owl-stage-outer {
|
||||
height: 520px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 660px) {
|
||||
.logico-content-slider-widget .content-slider-item-heading {
|
||||
font-size: clamp(20px, 7vw, 32px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Slide counter ("01/02" + progress line): site.css pushes it ~80px in
|
||||
from the right and only 9px off the bottom on mobile, so it sits in an
|
||||
awkward spot. Pin it cleanly to the bottom-right with even padding. */
|
||||
@media (max-width: 767px) {
|
||||
.elementor-61 .elementor-element.elementor-element-6c7cbcb .slider-footer {
|
||||
text-align: right !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
.elementor-61 .elementor-element.elementor-element-6c7cbcb .slider-footer .slider-pagination {
|
||||
justify-content: flex-end !important;
|
||||
margin-left: 22px !important;
|
||||
margin-right: 22px !important;
|
||||
}
|
||||
.elementor-61 .elementor-element.elementor-element-6c7cbcb .slider-progress-wrapper {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
/* The prev/next arrows sit in the bottom-right corner and overlap the
|
||||
counter once it's pinned right. Hide them on mobile — slides
|
||||
auto-rotate and the counter shows progress. */
|
||||
.elementor-61 .elementor-element.elementor-element-6c7cbcb .owl-nav {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
<div className="elementor-element elementor-element-6c7cbcb elementor-widget elementor-widget-logico_content_slider" data-id="6c7cbcb" data-element_type="widget" data-e-type="widget" data-widget_type="logico_content_slider.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-content-slider-widget">
|
||||
<div className="content-slider-wrapper">
|
||||
<div className="content-slider-container">
|
||||
<div className="content-slider owl-carousel owl-theme nav-view-vertical nav-h-position-right nav-v-position-bottom owl-loaded owl-drag" ref={containerRef}>
|
||||
<div className="owl-stage-outer" style={{ position: "relative", overflow: "hidden", height: "800px" }}>
|
||||
<div className="owl-stage-outer" style={{ position: "relative", overflow: "hidden" }}>
|
||||
<div className="owl-stage" style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
|
||||
{/* Slide 1 */}
|
||||
|
||||
@@ -11,14 +11,14 @@ 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 industry-section-label" 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 dangerouslySetInnerHTML={{ __html: `
|
||||
/* Minimal section label — matches the "/ Doormile Approach /" pattern */
|
||||
.industry-section-label {
|
||||
width: 100%;
|
||||
}
|
||||
.industry-section-label > .elementor-widget-container {
|
||||
max-width: 1740px;
|
||||
max-width: 100%;
|
||||
margin: 12px 0 50px 0;
|
||||
padding: 0 0 14px 0;
|
||||
border-style: solid;
|
||||
@@ -60,17 +60,9 @@ export default function IndustrySolutions() {
|
||||
data-element_type="widget"
|
||||
data-e-type="widget"
|
||||
data-widget_type="logico_heading.default"
|
||||
style={{marginLeft: "50px"}}
|
||||
>
|
||||
<div className="elementor-widget-container" style={{ margin: "30px 0 0 0"}}>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@media (min-width: 1024px) {
|
||||
.industry-title-single-line {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
<h3 className="logico-title industry-title-single-line" style={{ fontSize: "clamp(28px, 3.5vw, 48px)", lineHeight: "1.1", fontWeight: 800, textTransform: "uppercase" }}>
|
||||
<h3 className="logico-title" style={{ fontSize: "clamp(28px, 3.5vw, 48px)", lineHeight: "1.1", fontWeight: 800, textTransform: "uppercase", maxWidth: "900px" }}>
|
||||
<ScrollReveal delay={0.05} duration={0.8} yOffset={25}>
|
||||
Smart solutions built exclusively for your <span style={{ color: "#c01227" }}>industry</span>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -305,6 +305,17 @@ const CSS = `
|
||||
#ind-stack .istk__media { flex: none; min-height: 0; height: 300px; }
|
||||
#ind-stack .istk__content { max-width: none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#ind-stack .istk { padding: 10px 14px; }
|
||||
#ind-stack .istk__card { padding: 22px 20px; border-radius: 24px; }
|
||||
#ind-stack .istk__media { height: 240px; }
|
||||
#ind-stack .istk__title { margin-bottom: 16px !important; }
|
||||
#ind-stack .istk__desc { margin: 0 0 24px; }
|
||||
/* Two equal-width pills that always fit the card — no horizontal overflow. */
|
||||
#ind-stack .istk__tabs { gap: 8px; margin-bottom: 22px; }
|
||||
#ind-stack .istk__tab { flex: 1 1 0; min-width: 0; padding: 12px 8px; text-align: center; font-size: 13px; }
|
||||
#ind-stack .istk__list li { margin-bottom: 18px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#ind-stack .istk__list li { animation: none !important; opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
@@ -252,8 +252,19 @@ export default function IntelligenceGrid() {
|
||||
/* Buttery-Smooth Hardware-Accelerated 3D AI Logistics Timeline Styles */
|
||||
.roadmap-hero-section {
|
||||
position: relative;
|
||||
background: #09090b !important;
|
||||
width: 100%;
|
||||
background: #09090b !important;
|
||||
/* Match the site-wide wide-band gutter EXACTLY. Measured against the
|
||||
footer band (#b29b8fc) and the sibling "Women" band
|
||||
(#women-entrepreneurship), both of which sit at a 20px left/right
|
||||
gutter. The parent (#bbc6760) contributes no padding here (the
|
||||
vendor padding vars resolve to 0), so the band carries the 20px
|
||||
gutter itself via calc(100% - 40px) + auto margins:
|
||||
at a 1910px viewport this renders left:20 right:1890 width:1870,
|
||||
identical to the footer band. */
|
||||
width: calc(100% - 40px);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 24px;
|
||||
padding: 100px 40px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
@@ -299,7 +310,9 @@ export default function IntelligenceGrid() {
|
||||
}
|
||||
|
||||
.roadmap-hero-section .container {
|
||||
max-width: 1320px;
|
||||
/* Fill the wide section so the timeline + cards span most of the
|
||||
available width (no narrow content cap). */
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
@@ -683,6 +696,7 @@ export default function IntelligenceGrid() {
|
||||
/* Responsive Constraints */
|
||||
@media (max-width: 1024px) {
|
||||
.roadmap-hero-section {
|
||||
width: calc(100% - 40px); /* keep the 20px site-standard band gutter */
|
||||
padding: 80px 24px;
|
||||
}
|
||||
|
||||
@@ -698,23 +712,91 @@ export default function IntelligenceGrid() {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.roadmap-hero-section {
|
||||
padding: 60px 16px;
|
||||
}
|
||||
|
||||
.roadmap-grid-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
width: calc(100% - 24px); /* tighter 12px gutter so the band reads edge-to-edge on phones */
|
||||
padding: 52px 16px 44px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
.vision-main-subtitle { margin-bottom: 40px !important; }
|
||||
|
||||
/* Progress header → compact, centered summary card */
|
||||
.roadmap-track-container {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
padding: 20px 18px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.roadmap-track-label { font-size: 12px; letter-spacing: 1.5px; }
|
||||
.timeline-horizontal-wrapper { width: 100%; }
|
||||
.roadmap-complete-pct-container { justify-content: center; }
|
||||
|
||||
.timeline-horizontal-wrapper {
|
||||
width: 100%;
|
||||
/* ── Vertical timeline: the four stacked cards become a connected
|
||||
journey (2026 → 2030) with a coloured rail + phase node dots. ── */
|
||||
.roadmap-grid-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
}
|
||||
.roadmap-grid-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 16px;
|
||||
bottom: 16px;
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, #eab308 0%, #22c55e 34%, #3b82f6 67%, #C8102E 100%);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.roadmap-col-card { padding: 24px 22px; }
|
||||
/* node dot sits on the rail, aligned with the year number */
|
||||
.roadmap-col-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: 28px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #09090b;
|
||||
border: 2px solid #C8102E;
|
||||
box-shadow: 0 0 0 4px #09090b, 0 0 12px rgba(200, 16, 46, 0.7);
|
||||
z-index: 3;
|
||||
}
|
||||
.roadmap-col-card[data-card="2026"]::before {
|
||||
border-color: #eab308;
|
||||
box-shadow: 0 0 0 4px #09090b, 0 0 12px rgba(234, 179, 8, 0.6);
|
||||
}
|
||||
.roadmap-col-card[data-card="2027"]::before {
|
||||
border-color: #22c55e;
|
||||
box-shadow: 0 0 0 4px #09090b, 0 0 12px rgba(34, 197, 94, 0.6);
|
||||
}
|
||||
.roadmap-col-card[data-card="2028"]::before {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 4px #09090b, 0 0 12px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
.roadmap-col-card[data-card="2030"]::before {
|
||||
border-color: #ffffff;
|
||||
background: #C8102E;
|
||||
box-shadow: 0 0 0 4px #09090b, 0 0 16px rgba(200, 16, 46, 0.9);
|
||||
}
|
||||
/* the 2030 card's sweeping border (::after) would sit over the rail
|
||||
gutter — keep it from bleeding past the card's rounded edge */
|
||||
.glowing-vision-card::after { inset: -1px; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.roadmap-hero-section { padding: 44px 14px 38px; }
|
||||
.roadmap-hero-section .intelligence-grid-title { font-size: clamp(28px, 8.4vw, 38px) !important; letter-spacing: -1px !important; }
|
||||
.roadmap-grid-container { padding-left: 28px; }
|
||||
.roadmap-grid-container::before { left: 5px; }
|
||||
.roadmap-col-card { padding: 22px 18px; }
|
||||
.roadmap-col-card::before { left: -28px; top: 26px; }
|
||||
.year-num { font-size: 23px; }
|
||||
.card-heading { font-size: 16px; }
|
||||
.card-text { font-size: 13px; }
|
||||
}
|
||||
`}} />
|
||||
|
||||
@@ -794,9 +876,10 @@ export default function IntelligenceGrid() {
|
||||
const is2030 = card.year === 2030;
|
||||
const isActive = activeYear === card.year;
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
key={card.year}
|
||||
onMouseEnter={() => setActiveYear(card.year)}
|
||||
onClick={() => setActiveYear(card.year)}
|
||||
className={`roadmap-col-card ${is2030 ? "glowing-vision-card" : ""} ${isActive ? "active" : ""}`}
|
||||
data-card={card.year}
|
||||
>
|
||||
|
||||
284
src/components/sections/LegalDocument.tsx
Normal file
284
src/components/sections/LegalDocument.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
/**
|
||||
* Shared layout for the legal pages (Privacy Policy, Terms of Service, Cookie
|
||||
* Policy) so all three stay pixel-consistent. Content is passed in as structured
|
||||
* blocks; this component owns the hero, the desktop sticky table of contents and
|
||||
* the 800px reading column described in the design spec.
|
||||
*/
|
||||
|
||||
export type LegalBlock =
|
||||
| { type: "p"; text: React.ReactNode }
|
||||
| { type: "lead"; text: React.ReactNode }
|
||||
| { type: "h3"; text: string }
|
||||
| { type: "ul"; items: React.ReactNode[] };
|
||||
|
||||
export type LegalSection = {
|
||||
heading: string;
|
||||
blocks: LegalBlock[];
|
||||
};
|
||||
|
||||
export type LegalDocumentProps = {
|
||||
title: string;
|
||||
lastUpdated: string;
|
||||
intro: React.ReactNode;
|
||||
sections: LegalSection[];
|
||||
};
|
||||
|
||||
/** Stable, URL-safe id from a section heading (for TOC anchors). */
|
||||
function slug(heading: string): string {
|
||||
return heading
|
||||
.toLowerCase()
|
||||
.replace(/&/g, "and")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
}
|
||||
|
||||
function Block({ block }: { block: LegalBlock }) {
|
||||
switch (block.type) {
|
||||
case "lead":
|
||||
return <p className="dm-legal__lead">{block.text}</p>;
|
||||
case "p":
|
||||
return <p className="dm-legal__p">{block.text}</p>;
|
||||
case "h3":
|
||||
return <h3 className="dm-legal__h3">{block.text}</h3>;
|
||||
case "ul":
|
||||
return (
|
||||
<ul className="dm-legal__ul">
|
||||
{block.items.map((item, i) => (
|
||||
<li key={i}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function LegalDocument({ title, lastUpdated, intro, sections }: LegalDocumentProps) {
|
||||
return (
|
||||
<section className="dm-legal" aria-label={title}>
|
||||
<div className="dm-legal__wrap">
|
||||
{/* Desktop sticky table of contents */}
|
||||
<aside className="dm-legal__toc" aria-label="On this page">
|
||||
<div className="dm-legal__toc-inner">
|
||||
<p className="dm-legal__toc-label">On this page</p>
|
||||
<nav>
|
||||
<ul>
|
||||
{sections.map((s) => (
|
||||
<li key={s.heading}>
|
||||
<a href={`#${slug(s.heading)}`}>{s.heading}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="dm-legal__main">
|
||||
<header className="dm-legal__hero">
|
||||
<h1 className="dm-legal__title">{title}</h1>
|
||||
<p className="dm-legal__updated">Last Updated: {lastUpdated}</p>
|
||||
<p className="dm-legal__lead">{intro}</p>
|
||||
</header>
|
||||
|
||||
<article className="dm-legal__content">
|
||||
{sections.map((s) => (
|
||||
<section key={s.heading} id={slug(s.heading)} className="dm-legal__section">
|
||||
<h2 className="dm-legal__h2">{s.heading}</h2>
|
||||
{s.blocks.map((b, i) => (
|
||||
<Block key={i} block={b} />
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{styles}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders the recurring "contact us through our Contact page" link. */
|
||||
export function ContactLink() {
|
||||
return (
|
||||
<Link href="/contact" className="dm-legal__link">
|
||||
Contact page
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = `
|
||||
.dm-legal {
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
/* Top padding clears the fixed/absolute floating navbar (~104px) + breathing room. */
|
||||
padding: clamp(132px, 16vh, 184px) 0 clamp(72px, 10vw, 96px);
|
||||
font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.dm-legal__wrap {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 0 clamp(20px, 5vw, 40px);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* ---- Sticky table of contents (desktop only) ---- */
|
||||
.dm-legal__toc { display: none; }
|
||||
|
||||
@media (min-width: 1080px) {
|
||||
.dm-legal__wrap {
|
||||
grid-template-columns: 248px minmax(0, 800px);
|
||||
justify-content: center;
|
||||
gap: 64px;
|
||||
}
|
||||
.dm-legal__toc { display: block; }
|
||||
}
|
||||
|
||||
.dm-legal__toc-inner { position: sticky; top: 128px; }
|
||||
.dm-legal__toc-label {
|
||||
margin: 0 0 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.dm-legal__toc nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.dm-legal__toc nav a {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
padding-left: 12px;
|
||||
transition: color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.dm-legal__toc nav a:hover { color: #c01227; border-left-color: #c01227; }
|
||||
|
||||
/* ---- Reading column (max 800px) ---- */
|
||||
.dm-legal__main { min-width: 0; max-width: 800px; }
|
||||
|
||||
.dm-legal__hero { margin-bottom: clamp(40px, 6vw, 56px); }
|
||||
.dm-legal__title {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
/* !important / text-transform override the global ".elementor-kit-5 h1"
|
||||
(uppercase + kit font-size) so headings render in normal case at the spec sizes. */
|
||||
font-size: clamp(40px, 6vw, 60px) !important;
|
||||
font-weight: 800 !important;
|
||||
line-height: 1.08 !important;
|
||||
letter-spacing: -0.02em !important;
|
||||
text-transform: none !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
.dm-legal__updated {
|
||||
margin: 16px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dm-legal__lead {
|
||||
margin: 24px 0 0;
|
||||
font-size: clamp(18px, 1.6vw, 20px);
|
||||
line-height: 1.7;
|
||||
color: #475569;
|
||||
}
|
||||
.dm-legal__hero .dm-legal__lead { margin-top: 24px; }
|
||||
|
||||
/* ---- Sections ---- */
|
||||
.dm-legal__content { display: flex; flex-direction: column; }
|
||||
.dm-legal__section { padding-top: 32px; }
|
||||
.dm-legal__section:first-child { padding-top: 0; }
|
||||
|
||||
.dm-legal__h2 {
|
||||
margin: 0 0 16px !important;
|
||||
padding: 0 !important;
|
||||
font-size: clamp(24px, 3vw, 32px) !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.2 !important;
|
||||
letter-spacing: -0.01em !important;
|
||||
text-transform: none !important;
|
||||
color: #0f172a !important;
|
||||
/* Offset anchor jumps so the heading isn't hidden under the fixed navbar. */
|
||||
scroll-margin-top: 120px;
|
||||
}
|
||||
.dm-legal__h3 {
|
||||
margin: 24px 0 8px !important;
|
||||
padding: 0 !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.35 !important;
|
||||
text-transform: none !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
.dm-legal__p {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.7;
|
||||
color: #334155;
|
||||
}
|
||||
.dm-legal__p:last-child { margin-bottom: 0; }
|
||||
|
||||
.dm-legal__ul { margin: 4px 0 16px; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; }
|
||||
.dm-legal__ul li {
|
||||
position: relative;
|
||||
padding-left: 26px;
|
||||
font-size: 18px;
|
||||
line-height: 1.7;
|
||||
color: #334155;
|
||||
}
|
||||
/* Extra .dm-legal prefix raises specificity above the global theme rule
|
||||
".logico-front-end ul li:before" (a fontello glyph), so our clean red dot
|
||||
replaces the inherited checkmark marker. */
|
||||
.dm-legal .dm-legal__ul li::before {
|
||||
content: "" !important;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 0.62em;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #c01227;
|
||||
font-size: 0;
|
||||
}
|
||||
/* TOC links are inside a <ul> too — suppress the same inherited glyph marker. */
|
||||
.dm-legal .dm-legal__toc nav li::before { content: none !important; }
|
||||
|
||||
.dm-legal__link { color: #c01227; font-weight: 600; text-decoration: none; border-bottom: 1px solid rgba(192,18,39,0.35); }
|
||||
.dm-legal__link:hover { border-bottom-color: #c01227; }
|
||||
|
||||
@media (max-width: 1079px) {
|
||||
.dm-legal__main { margin: 0 auto; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Navbar visibility — LEGAL PAGES ONLY.
|
||||
This <style> block is rendered exclusively by LegalDocument, which only mounts
|
||||
on /privacy-policy, /terms-of-service and /cookie-policy. So although the rule
|
||||
targets the global #masthead, it is physically absent from every other page's
|
||||
HTML and cannot affect them — no global CSS file is touched.
|
||||
|
||||
Why it's needed: the desktop logo and nav links are white and the navbar bar is
|
||||
transparent at the top of the page. Over the dark heroes used elsewhere that's
|
||||
fine, but on these white pages the navbar vanishes. Give the bar the same dark
|
||||
glass fill the rest of the site already shows on scroll. Desktop only — the
|
||||
mobile logo is black-on-transparent and already legible here.
|
||||
============================================================ */
|
||||
@media (min-width: 1025px) {
|
||||
#masthead .elementor-element.elementor-element-466de1b {
|
||||
background: rgba(40, 40, 40, 0.75) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16) !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -99,11 +99,11 @@ export default function MileTruthHero() {
|
||||
.miletruth-hero .content-slider-item-text p {
|
||||
color: rgba(255, 255, 255, 0.72) !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
font-size: clamp(16px, 1.8vw, 20px) !important;
|
||||
font-size: clamp(17px, 1.6vw, 23px) !important;
|
||||
font-weight: 500 !important;
|
||||
line-height: 1.6 !important;
|
||||
line-height: 1.65 !important;
|
||||
margin: 0 auto !important;
|
||||
max-width: 800px;
|
||||
max-width: 820px;
|
||||
}
|
||||
.miletruth-hero .slide-content {
|
||||
display: flex !important;
|
||||
@@ -116,6 +116,14 @@ export default function MileTruthHero() {
|
||||
padding: 0 40px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
.miletruth-hero .slide-content,
|
||||
.miletruth-hero .slide-content-inner {
|
||||
/* These carry inherited 20px padding from the slider theme with
|
||||
content-box sizing, so width:100% + padding overflowed the viewport
|
||||
by ~40px and pushed the hero off-center. Force border-box so the
|
||||
padding is contained and the block stays centered. */
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
.miletruth-hero .slide-content-inner {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
@@ -284,17 +292,42 @@ export default function MileTruthHero() {
|
||||
.miletruth-hero .elementor-element-86f3204 {
|
||||
padding: 0 12px;
|
||||
}
|
||||
/* Collapse the nested horizontal padding (was 40px + 40px on top of the
|
||||
12px wrapper) — at 320px that left only ~136px for text and the long
|
||||
heading words overflowed, causing horizontal scroll + off-center look. */
|
||||
.miletruth-content {
|
||||
padding: 0 14px !important;
|
||||
}
|
||||
.miletruth-hero .slide-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.miletruth-hero .elementor-element-8e5c81e {
|
||||
padding: 30px 0 !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
.miletruth-hero .elementor-element-8e5c81e > .e-con-inner {
|
||||
padding: 0 20px !important;
|
||||
}
|
||||
.miletruth-hero .elementor-element-628123a {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 36px !important;
|
||||
/* Single column on mobile → center each stat in the card */
|
||||
justify-items: center !important;
|
||||
}
|
||||
.miletruth-hero .elementor-counter {
|
||||
align-items: center !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
.miletruth-hero .elementor-counter-number-wrapper {
|
||||
justify-content: center !important;
|
||||
}
|
||||
.miletruth-hero .content-slider-item-heading,
|
||||
.miletruth-hero .content-slider-item-heading .heading-content {
|
||||
font-size: 32px !important;
|
||||
font-size: clamp(26px, 8vw, 32px) !important;
|
||||
letter-spacing: -0.5px !important;
|
||||
/* never let a single long word push past the viewport */
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Miles3() {
|
||||
/* =====================================================================
|
||||
How It Works · "Doormile connects first, mid, and last mile" section
|
||||
Self-contained recreation of the original Elementor design
|
||||
(source: public/css/sections/section-miles3.css + kit-5 typography).
|
||||
(originally Elementor section CSS, now inlined below + in /public/css/site.css).
|
||||
Explicit flex/grid values because the Elementor framework reads them
|
||||
from CSS custom properties that aren't set in this rebuild.
|
||||
===================================================================== */
|
||||
@@ -25,7 +25,7 @@ export default function Miles3() {
|
||||
margin: 20px 20px 0 20px;
|
||||
background-color: #1F1F1F;
|
||||
border-radius: 25px 25px 0 0;
|
||||
padding: 150px 0 0 0;
|
||||
padding: 90px 0 0 0;
|
||||
}
|
||||
/* Boxed inner — centered, original content width */
|
||||
.elementor-element-c36a604 > .e-con-inner {
|
||||
@@ -98,6 +98,11 @@ export default function Miles3() {
|
||||
while keeping the 70px row-gap. */
|
||||
.elementor-element-4add972 {
|
||||
display: grid;
|
||||
/* Elementor's ".e-con.e-grid" (specificity 0,2,0) sets grid-template-columns
|
||||
from this CSS var, which OUT-RANKS a plain ".elementor-element-4add972"
|
||||
(0,1,0) rule — so the responsive breakpoints below MUST drive the var,
|
||||
not grid-template-columns directly, or the grid never collapses on mobile. */
|
||||
--e-con-grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-flow: row;
|
||||
gap: 70px 60px;
|
||||
@@ -118,12 +123,14 @@ export default function Miles3() {
|
||||
}
|
||||
.elementor-element-4add972 .elementor-icon svg {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.elementor-element-74687fb > .elementor-widget-container { padding: 22px 0 0 0; }
|
||||
.elementor-element-74687fb .elementor-icon svg { height: 139px; }
|
||||
.elementor-element-fd9c57e .elementor-icon svg { height: 158px; }
|
||||
.elementor-element-74687fb .elementor-icon svg { max-height: 139px; }
|
||||
.elementor-element-fd9c57e .elementor-icon svg { max-height: 158px; }
|
||||
.elementor-element-fbb1628 > .elementor-widget-container { padding: 25px 0 0 0; }
|
||||
.elementor-element-fbb1628 .elementor-icon svg { height: 128px; }
|
||||
.elementor-element-fbb1628 .elementor-icon svg { max-height: 128px; }
|
||||
|
||||
/* Card titles (First / Mid / Last Mile) */
|
||||
.elementor-element-d310968 > .elementor-widget-container,
|
||||
@@ -163,15 +170,15 @@ export default function Miles3() {
|
||||
.logico-front-end .elementor-element-63a9de5 .logico-title { font-size: clamp(40px, 5vw, 60px); }
|
||||
}
|
||||
@media (max-width: 1020px) {
|
||||
.elementor-element-c36a604 { margin: 15px 15px 0 15px; padding: 100px 0 0 0; }
|
||||
.elementor-element-c36a604 { margin: 15px 15px 0 15px; padding: 76px 0 60px 0; }
|
||||
.elementor-element-77d1265 { padding: 0 30px; }
|
||||
.elementor-element-4add972 { grid-template-columns: repeat(2, 1fr); gap: 50px 40px; }
|
||||
.elementor-element-4add972 { --e-con-grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr); gap: 50px 40px; }
|
||||
.logico-front-end .elementor-element-63a9de5 .logico-title { font-size: clamp(34px, 6vw, 52px); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.elementor-element-c36a604 { margin: 12px 12px 0 12px; border-radius: 20px 20px 0 0; padding: 70px 0 0 0; }
|
||||
.elementor-element-c36a604 { margin: 12px 12px 0 12px; border-radius: 20px 20px 0 0; padding: 70px 0 50px 0; }
|
||||
.elementor-element-77d1265 { padding: 0 22px; }
|
||||
.elementor-element-4add972 { grid-template-columns: 1fr; gap: 56px; }
|
||||
.elementor-element-4add972 { --e-con-grid-template-columns: 1fr; grid-template-columns: 1fr; gap: 56px; }
|
||||
.logico-front-end .elementor-element-63a9de5 .logico-title { font-size: clamp(30px, 9vw, 44px); }
|
||||
}
|
||||
`}} />
|
||||
|
||||
@@ -8,11 +8,7 @@ export default function OurTeam() {
|
||||
position: "COO & Operational Specialist",
|
||||
image: "/images/Investor.png",
|
||||
},
|
||||
{
|
||||
name: "Fazul Ilahi",
|
||||
position: "CTO & Technology Specialist",
|
||||
image: "/images/Fazul.png",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Parthiban",
|
||||
position: "CGO & Growth Specialist",
|
||||
@@ -22,6 +18,11 @@ export default function OurTeam() {
|
||||
name: "Aravinth",
|
||||
position: "CFO & Finance Specialist",
|
||||
image: "/images/Aravinth.png",
|
||||
},
|
||||
{
|
||||
name: "Fazul Ilahi",
|
||||
position: "CTO & Technology Specialist",
|
||||
image: "/images/Fazul.png",
|
||||
},
|
||||
{
|
||||
name: "Suriya Kumar",
|
||||
@@ -48,6 +49,9 @@ export default function OurTeam() {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: clamp(20px, 4vw, 50px) !important;
|
||||
padding-right: clamp(20px, 4vw, 50px) !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Breathing room above and below the "Meet our the best crew" heading. */
|
||||
@@ -65,38 +69,40 @@ export default function OurTeam() {
|
||||
filter: grayscale(0%);
|
||||
}
|
||||
|
||||
/* Self-contained layout (does not rely on the cached vendor CSS). */
|
||||
/* Grid: three columns that wrap, with tightened row/column gaps. */
|
||||
/* Grid: five columns that wrap, with tightened row/column gaps. */
|
||||
.team-listing-wrapper.team-grid-listing {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -16px -40px;
|
||||
}
|
||||
.team-listing-wrapper.team-grid-listing .team-item-wrapper {
|
||||
width: 33.3333%;
|
||||
width: 20%;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 1020px) {
|
||||
@media (max-width: 1200px) {
|
||||
.team-listing-wrapper.team-grid-listing .team-item-wrapper { width: 33.3333%; }
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.team-listing-wrapper.team-grid-listing .team-item-wrapper { width: 50%; }
|
||||
}
|
||||
@media (max-width: 660px) {
|
||||
@media (max-width: 480px) {
|
||||
.team-listing-wrapper.team-grid-listing .team-item-wrapper { width: 100%; }
|
||||
}
|
||||
|
||||
/* Card: photo on the LEFT, name/position on the RIGHT (side by side). */
|
||||
|
||||
/* Card: photo on top, name/position at the bottom (down to image). */
|
||||
.team-listing-wrapper.team-grid-listing .team-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
/* Compact portrait photo. Extra .team-item in the selector raises the
|
||||
specificity so it beats the cached vendor rule (width: 45.65%). */
|
||||
.team-listing-wrapper.team-grid-listing .team-item .team-item-media {
|
||||
flex-shrink: 0;
|
||||
width: 160px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.team-listing-wrapper.team-grid-listing .team-item .post-media {
|
||||
@@ -113,7 +119,7 @@ export default function OurTeam() {
|
||||
object-fit: cover;
|
||||
}
|
||||
.team-listing-wrapper.team-grid-listing .team-item .team-item-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
`}} />
|
||||
@@ -128,7 +134,7 @@ export default function OurTeam() {
|
||||
|
||||
<div style={{ alignSelf: "flex-start", width: "100%" }} className="elementor-element elementor-element-c46350e elementor-widget__width-initial elementor-widget elementor-widget-logico_heading" data-id="c46350e" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<h3 className="logico-title" style={{ textAlign: "left" }}>Meet our the best crew</h3>
|
||||
<h3 className="logico-title" style={{ textAlign: "left" }}>Meet crew</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +142,7 @@ export default function OurTeam() {
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-team-listing-widget">
|
||||
<div className="archive-listing">
|
||||
<div className="archive-listing-wrapper team-listing-wrapper team-grid-listing columns-3">
|
||||
<div className="archive-listing-wrapper team-listing-wrapper team-grid-listing columns-5">
|
||||
|
||||
{teamMembers.map((member, i) => (
|
||||
<div key={i} className="team-item-wrapper post-2866 team type-team status-publish has-post-thumbnail hentry">
|
||||
@@ -159,10 +165,7 @@ export default function OurTeam() {
|
||||
<div className="team-item-position" dangerouslySetInnerHTML={{ __html: member.position }}></div>
|
||||
<div className="team-item-socials">
|
||||
<ul className="team-socials wrapper-socials">
|
||||
<li><a href="https://www.facebook.com/" target="_blank" rel="noreferrer" className="fab fa-facebook-f"></a></li>
|
||||
<li><a href="https://x.com/" target="_blank" rel="noreferrer" className="fab fa-x-twitter"></a></li>
|
||||
<li><a href="https://www.linkedin.com/" target="_blank" rel="noreferrer" className="fab fa-linkedin-in"></a></li>
|
||||
<li><a href="https://www.youtube.com/" target="_blank" rel="noreferrer" className="fab fa-youtube"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
472
src/components/sections/SingleBlog.tsx
Normal file
472
src/components/sections/SingleBlog.tsx
Normal file
@@ -0,0 +1,472 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ScrollReveal } from "@/animations/Reveal";
|
||||
import BlogSidebar from "@/components/blog/BlogSidebar";
|
||||
import {
|
||||
type BlogPost,
|
||||
type ContentBlock,
|
||||
formatDate,
|
||||
estimateReadingTime,
|
||||
} from "@/data/blog";
|
||||
|
||||
function ContentRenderer({ block }: { block: ContentBlock }) {
|
||||
switch (block.type) {
|
||||
case "paragraph":
|
||||
return <p className="dm-article-p">{block.text}</p>;
|
||||
case "heading":
|
||||
return block.level === 2 ? (
|
||||
<h2 className="dm-article-h2">{block.text}</h2>
|
||||
) : (
|
||||
<h3 className="dm-article-h3">{block.text}</h3>
|
||||
);
|
||||
case "list":
|
||||
return block.ordered ? (
|
||||
<ol className="dm-article-ol">
|
||||
{block.items.map((it, i) => (
|
||||
<li key={i}>{it}</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<ul className="dm-article-ul">
|
||||
{block.items.map((it, i) => (
|
||||
<li key={i}>{it}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
case "quote":
|
||||
return (
|
||||
<blockquote className="dm-article-quote">
|
||||
<p>{block.text}</p>
|
||||
{block.cite && <cite>— {block.cite}</cite>}
|
||||
</blockquote>
|
||||
);
|
||||
case "image":
|
||||
return (
|
||||
<figure className="dm-article-figure">
|
||||
<span className="dm-article-figure-img">
|
||||
<Image
|
||||
src={block.src}
|
||||
alt={block.alt}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 760px"
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
</span>
|
||||
{block.caption && <figcaption>{block.caption}</figcaption>}
|
||||
</figure>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SingleBlog({ post }: { post: BlogPost }) {
|
||||
const readingTime = estimateReadingTime(post);
|
||||
|
||||
// Banner background: a subtle dark overlay over the article's featured image,
|
||||
// light enough to let the photograph remain the primary visual element.
|
||||
const bannerStyle = {
|
||||
backgroundImage: `url(${post.image})`,
|
||||
"--hero-overlay":
|
||||
"linear-gradient(180deg, rgba(0,0,0,0.38) 0%, rgba(0,0,0,0.46) 55%, rgba(0,0,0,0.60) 100%)",
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<article className="dm-single-blog">
|
||||
<style dangerouslySetInnerHTML={{ __html: STYLES }} />
|
||||
|
||||
{/* ── Full-width page banner (image + badge + title only) ──────── */}
|
||||
<div className="custom-standard-hero-container">
|
||||
<div className="custom-standard-hero-card dm-banner-card" style={bannerStyle}>
|
||||
<div className="e-con-inner dm-banner-inner">
|
||||
<span className="dm-banner-category">{post.category}</span>
|
||||
<h1 className="dm-banner-title">{post.title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Article meta bar (breadcrumb + author / date / reading time) ── */}
|
||||
<div className="dm-blog-wrap">
|
||||
<div className="dm-meta-bar">
|
||||
<nav className="dm-meta-breadcrumb" aria-label="Breadcrumb">
|
||||
<ol>
|
||||
<li>
|
||||
<Link href="/">Home</Link>
|
||||
</li>
|
||||
<li aria-hidden="true" className="dm-meta-sep">/</li>
|
||||
<li>
|
||||
<Link href="/blog">Blog</Link>
|
||||
</li>
|
||||
<li aria-hidden="true" className="dm-meta-sep">/</li>
|
||||
<li aria-current="page" className="dm-meta-current">
|
||||
{post.title}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div className="dm-meta-items">
|
||||
<span className="dm-meta-item dm-meta-author">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
{post.author}
|
||||
</span>
|
||||
<span className="dm-meta-item">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" /><line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
<time dateTime={post.date}>{formatDate(post.date)}</time>
|
||||
</span>
|
||||
<span className="dm-meta-item">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" /><polyline points="12 7 12 12 15 14" />
|
||||
</svg>
|
||||
{readingTime} min read
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content + sidebar ──────────────────────────────────────── */}
|
||||
<div className="dm-blog-layout">
|
||||
<div className="dm-blog-main">
|
||||
<p className="dm-blog-intro">{post.intro}</p>
|
||||
|
||||
<div className="dm-article-body">
|
||||
{post.content.map((block, i) => (
|
||||
<ScrollReveal key={i} delay={0} duration={0.6} yOffset={20}>
|
||||
<ContentRenderer block={block} />
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dm-blog-aside-wrap">
|
||||
<BlogSidebar current={post} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
const STYLES = `
|
||||
.dm-single-blog {
|
||||
--dm-red: #c01227;
|
||||
--dm-red-hover: #e31d32;
|
||||
--dm-radius-card: 22px;
|
||||
--dm-radius-img: 20px;
|
||||
--dm-radius-badge: 8px;
|
||||
--dm-shadow-card: 0 4px 24px rgba(15, 23, 42, 0.05);
|
||||
--dm-border: 1px solid rgba(15, 23, 42, 0.09);
|
||||
--dm-space-p: 24px;
|
||||
--dm-space-h: 32px;
|
||||
--dm-space-img: 32px;
|
||||
--dm-space-quote: 40px;
|
||||
--dm-sticky-top: 138px;
|
||||
--dm-measure: min(1100px, 100%);
|
||||
font-family: var(--font-manrope), sans-serif;
|
||||
}
|
||||
|
||||
/* Heading normalization — beat the global theme's .elementor-kit-5 h1–h6
|
||||
(120/80/60px UPPERCASE) rules with !important on our own classes. */
|
||||
.dm-single-blog :where(h1, h2, h3, h4, h5, h6) {
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
text-transform: none !important;
|
||||
font-style: normal !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
/* Neutralize the theme's .elementor-kit-5 a (red color + underline,
|
||||
specificity 0-1-1) so blog links keep our colors and never get underlined. */
|
||||
.dm-single-blog a { text-decoration: none !important; }
|
||||
|
||||
/* ── Page banner — tall (homepage-scale); only badge + title inside ── */
|
||||
/* Compound selector (specificity 20) + !important beats the global 800px
|
||||
single-class height rules so the blog banner can use viewport heights. */
|
||||
.custom-standard-hero-card.dm-banner-card {
|
||||
height: 90vh !important;
|
||||
min-height: 85vh !important;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.custom-standard-hero-card.dm-banner-card { height: 80vh !important; min-height: 75vh !important; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.custom-standard-hero-card.dm-banner-card { height: 78vh !important; min-height: 72vh !important; }
|
||||
}
|
||||
|
||||
.dm-banner-inner {
|
||||
position: relative; width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
text-align: center; gap: clamp(22px, 2.6vw, 34px);
|
||||
padding: clamp(48px, 8vh, 96px) clamp(20px, 5vw, 48px);
|
||||
}
|
||||
|
||||
.dm-banner-category {
|
||||
display: inline-block; background: var(--dm-red); color: #fff;
|
||||
font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 1.6px;
|
||||
padding: 9px 18px; border-radius: 999px; box-shadow: 0 8px 22px rgba(192,18,39,0.45);
|
||||
}
|
||||
.dm-banner-title {
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
font-size: clamp(34px, 5vw, 60px) !important; font-weight: 850 !important;
|
||||
line-height: 1.16 !important; letter-spacing: -1.2px !important;
|
||||
color: #ffffff !important; margin: 0; max-width: 820px;
|
||||
text-wrap: balance; text-shadow: 0 2px 30px rgba(0,0,0,0.38);
|
||||
}
|
||||
@media (max-width: 1024px) { .dm-banner-title { font-size: clamp(32px, 6vw, 48px) !important; max-width: 90%; } }
|
||||
@media (max-width: 600px) { .dm-banner-title { font-size: clamp(28px, 8vw, 38px) !important; max-width: 90%; } }
|
||||
|
||||
/* ── Content wrap — begins immediately below the banner ── */
|
||||
/* Shared content container: the SAME max-width + horizontal padding is used
|
||||
by BlogPostFooter (.dm-blog-footer-inner) so the article body, headings,
|
||||
images, Prev/Next, Related Articles and the CTA banner all align to one
|
||||
grid with identical left/right edges. Keep both in sync. */
|
||||
.dm-blog-wrap {
|
||||
max-width: 1280px; margin: 0 auto;
|
||||
/* 20px mobile padding floor → 40px on desktop; matches the footer container. */
|
||||
padding: clamp(14px, 2vw, 26px) clamp(20px, 4vw, 40px) 0;
|
||||
}
|
||||
|
||||
/* ── Article meta bar (directly under the hero) ── */
|
||||
.dm-meta-bar {
|
||||
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px 24px;
|
||||
padding: clamp(20px, 2.6vw, 30px) 0; margin-bottom: clamp(26px, 3vw, 44px);
|
||||
border-bottom: 1px solid rgba(15,23,42,0.10);
|
||||
}
|
||||
.dm-meta-breadcrumb ol {
|
||||
list-style: none; display: flex; flex-wrap: wrap; align-items: center;
|
||||
gap: 8px; margin: 0; padding: 0; font-size: 13px; font-weight: 600; color: #64748b;
|
||||
}
|
||||
.dm-meta-breadcrumb a { color: #64748b !important; text-decoration: none; transition: color .2s ease; }
|
||||
.dm-meta-breadcrumb a:hover { color: var(--dm-red) !important; }
|
||||
.dm-meta-sep { color: #cbd5e1; }
|
||||
.dm-meta-current {
|
||||
color: #0f172a; font-weight: 700;
|
||||
max-width: min(40ch, 46vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 600px) { .dm-meta-current { max-width: 56vw; } }
|
||||
.dm-meta-items { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; }
|
||||
.dm-meta-item {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
font-size: 13.5px; font-weight: 600; color: #475569;
|
||||
}
|
||||
.dm-meta-item svg { color: var(--dm-red); flex: 0 0 auto; }
|
||||
.dm-meta-author { color: #0f172a; font-weight: 700; }
|
||||
@media (max-width: 600px) { .dm-meta-bar { gap: 12px; } }
|
||||
.dm-blog-layout {
|
||||
display: grid; grid-template-columns: minmax(0,1fr) 320px;
|
||||
/* Tighter gap gives the reading column more room next to the 320px sidebar. */
|
||||
gap: clamp(28px, 3vw, 56px); align-items: start;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.dm-blog-layout { grid-template-columns: 1fr; gap: 48px; }
|
||||
/* Single-column (tablet/mobile): cap the reading column and centre it so the
|
||||
article never sits left-aligned with a large empty right gutter. */
|
||||
.dm-blog-main { max-width: 900px; margin-inline: auto; }
|
||||
}
|
||||
.dm-blog-main { min-width: 0; }
|
||||
|
||||
/* ── Intro lead ── */
|
||||
.dm-blog-intro {
|
||||
max-width: var(--dm-measure);
|
||||
font-size: clamp(18px, 1.5vw, 20px); line-height: 1.65; font-weight: 500;
|
||||
color: #475569; margin: 0 0 clamp(26px, 3vw, 38px); padding-left: 20px;
|
||||
border-left: 3px solid var(--dm-red);
|
||||
}
|
||||
|
||||
/* ── Article body ── */
|
||||
.dm-article-body { max-width: var(--dm-measure); }
|
||||
.dm-article-p {
|
||||
font-size: 18px !important; line-height: 1.8 !important; color: #334155; font-weight: 450;
|
||||
margin: 0 0 var(--dm-space-p);
|
||||
}
|
||||
.dm-article-h2 {
|
||||
font-size: clamp(23px, 2vw, 30px) !important; font-weight: 800 !important; letter-spacing: -.3px !important;
|
||||
color: #0f172a !important; line-height: 1.3 !important; margin: 48px 0 var(--dm-space-h);
|
||||
text-wrap: balance;
|
||||
}
|
||||
/* Each article block is wrapped in its OWN ScrollReveal <div>, so a bare
|
||||
:first-child rule matched every heading (each is the only child of its
|
||||
wrapper) and zeroed its top margin — collapsing the gap above every
|
||||
section heading. Scope the reset to only the article body's first block. */
|
||||
.dm-article-body > :first-child :where(.dm-article-h2, .dm-article-h3),
|
||||
.dm-article-body > .dm-article-h2:first-child,
|
||||
.dm-article-body > .dm-article-h3:first-child { margin-top: 0; }
|
||||
.dm-article-h3 {
|
||||
font-size: clamp(19px, 1.5vw, 23px) !important; font-weight: 800 !important; letter-spacing: -.2px !important;
|
||||
color: #1e293b !important; line-height: 1.34 !important; margin: 40px 0 18px;
|
||||
text-wrap: balance;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dm-article-p { font-size: 16px !important; }
|
||||
.dm-article-ul li, .dm-article-ol li { font-size: 15.5px; }
|
||||
.dm-article-h2 { font-size: clamp(22px, 6vw, 28px) !important; margin-top: 38px; }
|
||||
.dm-article-h3 { font-size: clamp(18px, 5vw, 22px) !important; }
|
||||
}
|
||||
|
||||
.dm-article-ul, .dm-article-ol {
|
||||
/* Top margin (was 0) separates the list from the paragraph above it;
|
||||
matching bottom margin keeps it clear of the next heading/paragraph.
|
||||
Slightly larger than --dm-space-p so the list reads as its own block. */
|
||||
margin: clamp(26px, 2.4vw, 32px) 0 clamp(26px, 2.4vw, 32px);
|
||||
padding-left: 2px; list-style: none;
|
||||
display: flex; flex-direction: column; gap: 15px;
|
||||
}
|
||||
/* Prefixed with .dm-article-body so these beat the theme's global
|
||||
".logico-front-end ul li:before" fontello-glyph bullet (specificity 0,1,3),
|
||||
which otherwise replaces our clean red dot with a misaligned checkmark glyph
|
||||
and adds its own 1.7em indent. */
|
||||
.dm-article-body .dm-article-ul li, .dm-article-body .dm-article-ol li {
|
||||
position: relative; padding-left: 34px;
|
||||
font-size: 17px; line-height: 1.65; color: #334155; font-weight: 450;
|
||||
}
|
||||
.dm-article-body .dm-article-ul li::before {
|
||||
content: ""; position: absolute; left: 7px; top: 10px;
|
||||
width: 8px; height: 8px; border-radius: 50%; background: var(--dm-red);
|
||||
/* kill any inherited fontello glyph from the global rule */
|
||||
font-size: 0; line-height: 0;
|
||||
}
|
||||
.dm-article-ol { counter-reset: dm-li; }
|
||||
.dm-article-body .dm-article-ol li { counter-increment: dm-li; }
|
||||
.dm-article-body .dm-article-ol li::before {
|
||||
content: counter(dm-li); position: absolute; left: 0; top: 0;
|
||||
width: 24px; height: 24px; border-radius: 7px;
|
||||
background: rgba(192,18,39,0.10); color: var(--dm-red);
|
||||
font-size: 12px; font-weight: 800; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
/* ── Quote ── */
|
||||
.dm-article-quote {
|
||||
position: relative; margin: var(--dm-space-quote) 0; padding: 30px 32px 30px 64px;
|
||||
background: linear-gradient(135deg, #fbf2f3 0%, #fdf7f8 100%);
|
||||
border-left: 4px solid var(--dm-red); border-radius: 0 18px 18px 0;
|
||||
box-shadow: 0 8px 28px rgba(192, 18, 39, 0.06);
|
||||
}
|
||||
.dm-article-quote::before {
|
||||
content: "\\201C"; position: absolute; left: 22px; top: 8px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: 64px; line-height: 1; color: rgba(192, 18, 39, 0.28); font-weight: 700;
|
||||
}
|
||||
.dm-article-quote p {
|
||||
font-size: clamp(19px, 2vw, 23px); line-height: 1.5; font-weight: 600;
|
||||
font-style: italic; color: #1e293b; margin: 0;
|
||||
}
|
||||
.dm-article-quote cite {
|
||||
display: block; margin-top: 16px; font-size: 13px; font-weight: 700;
|
||||
font-style: normal; color: var(--dm-red); text-transform: uppercase; letter-spacing: .6px;
|
||||
}
|
||||
@media (max-width: 600px) { .dm-article-quote { padding: 26px 22px; } .dm-article-quote::before { display: none; } }
|
||||
|
||||
/* ── Images ── */
|
||||
.dm-article-figure { margin: var(--dm-space-img) 0; }
|
||||
.dm-article-figure-img {
|
||||
position: relative; display: block; width: 100%; aspect-ratio: 16 / 9;
|
||||
border-radius: var(--dm-radius-img); overflow: hidden; box-shadow: 0 14px 34px rgba(15,23,42,0.10);
|
||||
}
|
||||
.dm-article-figure figcaption {
|
||||
margin-top: 14px; font-size: 13.5px; color: #94a3b8; font-weight: 500;
|
||||
text-align: center; font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.dm-blog-aside-wrap { position: relative; }
|
||||
.dm-blog-sidebar {
|
||||
display: flex; flex-direction: column; gap: 18px;
|
||||
position: sticky; top: var(--dm-sticky-top);
|
||||
}
|
||||
@media (max-width: 1024px) { .dm-blog-sidebar { position: static; gap: 20px; } }
|
||||
|
||||
.dm-blog-widget {
|
||||
background: #fff; border: var(--dm-border); border-radius: var(--dm-radius-card);
|
||||
padding: 20px; box-shadow: var(--dm-shadow-card);
|
||||
}
|
||||
.dm-blog-widget-title {
|
||||
font-size: 13px !important; font-weight: 700 !important; text-transform: uppercase !important;
|
||||
letter-spacing: .8px !important; line-height: 1.35 !important; color: #0f172a !important;
|
||||
margin: 0 0 15px; padding: 0 0 12px;
|
||||
border-bottom: 1px solid rgba(15,23,42,0.08);
|
||||
overflow: visible; white-space: normal; word-break: normal;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.dm-sr-only {
|
||||
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
||||
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
||||
}
|
||||
.dm-blog-search { position: relative; }
|
||||
.dm-blog-search-form { position: relative; }
|
||||
.dm-blog-search-input {
|
||||
width: 100%; height: 46px; border: 1.5px solid #e2e8f0; border-radius: 12px;
|
||||
padding: 0 42px 0 15px; font-size: 14px; font-family: inherit; color: #0f172a;
|
||||
background: #f8fafc; transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
|
||||
}
|
||||
.dm-blog-search-input::placeholder { color: #9c9c9c; }
|
||||
.dm-blog-search-input:focus {
|
||||
outline: none; border-color: var(--dm-red); background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(192,18,39,0.11);
|
||||
}
|
||||
.dm-blog-search-icon {
|
||||
position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
|
||||
color: #94a3b8; pointer-events: none; display: flex;
|
||||
}
|
||||
.dm-blog-search-results {
|
||||
position: absolute; z-index: 20; top: calc(100% + 8px); left: 0; right: 0;
|
||||
background: #fff; border: 1px solid rgba(15,23,42,0.10); border-radius: 14px;
|
||||
box-shadow: 0 18px 42px rgba(15,23,42,0.14); overflow: hidden;
|
||||
}
|
||||
.dm-blog-search-results ul { list-style: none; margin: 0; padding: 6px; }
|
||||
.dm-blog-search-result {
|
||||
display: flex; flex-direction: column; gap: 3px; padding: 10px 12px;
|
||||
border-radius: 10px; text-decoration: none; transition: background .15s ease;
|
||||
}
|
||||
.dm-blog-search-result:hover { background: #f8fafc; }
|
||||
.dm-blog-search-result-cat {
|
||||
font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .8px; color: var(--dm-red);
|
||||
}
|
||||
.dm-blog-search-result-title { font-size: 13.5px; font-weight: 600; color: #1e293b; line-height: 1.35; }
|
||||
.dm-blog-search-empty { margin: 0; padding: 14px 12px; font-size: 13px; color: #64748b; }
|
||||
|
||||
/* Recent posts */
|
||||
.dm-blog-recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 15px; }
|
||||
.dm-blog-recent-item { display: flex; gap: 13px; align-items: center; text-decoration: none; }
|
||||
.dm-blog-recent-thumb {
|
||||
position: relative; flex: 0 0 62px; width: 62px; height: 62px;
|
||||
border-radius: 13px; overflow: hidden; background: #f1f5f9;
|
||||
}
|
||||
.dm-blog-recent-meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.dm-blog-recent-title {
|
||||
font-size: 13.5px; font-weight: 700; color: #1e293b; line-height: 1.35;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
transition: color .2s ease;
|
||||
}
|
||||
.dm-blog-recent-item:hover .dm-blog-recent-title { color: var(--dm-red); }
|
||||
.dm-blog-recent-date { font-size: 11.5px; font-weight: 600; color: #94a3b8; }
|
||||
|
||||
/* Categories */
|
||||
.dm-blog-categories { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
|
||||
.dm-blog-category-item {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
||||
padding: 11px 2px; text-decoration: none; font-size: 14px; font-weight: 600; color: #334155 !important;
|
||||
border-bottom: 1px solid rgba(15,23,42,0.06); transition: color .2s ease, padding-left .2s ease;
|
||||
}
|
||||
.dm-blog-categories li:last-child .dm-blog-category-item { border-bottom: none; }
|
||||
.dm-blog-category-item:hover { color: var(--dm-red); padding-left: 6px; }
|
||||
.dm-blog-category-item > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dm-blog-category-count {
|
||||
flex: 0 0 auto; font-size: 11px; font-weight: 800; color: #94a3b8; background: #f1f5f9;
|
||||
min-width: 24px; height: 22px; border-radius: 7px; display: inline-flex;
|
||||
align-items: center; justify-content: center; padding: 0 7px;
|
||||
}
|
||||
|
||||
/* CTA card */
|
||||
.dm-blog-cta-card { background: #1f1f1f; border-color: #1f1f1f; }
|
||||
.dm-blog-cta-title { font-size: 18px !important; font-weight: 800 !important; color: #fff !important; line-height: 1.32 !important; margin: 0 0 10px; letter-spacing: -.2px !important; }
|
||||
.dm-blog-cta-text { font-size: 13.5px; line-height: 1.6; color: #c7c7c7; margin: 0 0 20px; font-weight: 450; }
|
||||
.dm-blog-cta-btn {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: var(--dm-red); color: #fff !important; font-size: 13.5px; font-weight: 700;
|
||||
padding: 12px 22px; border-radius: 12px; text-decoration: none;
|
||||
transition: background .2s ease, transform .2s ease;
|
||||
}
|
||||
.dm-blog-cta-btn:hover { background: var(--dm-red-hover); transform: translateY(-2px); }
|
||||
`;
|
||||
@@ -5,11 +5,11 @@ import React, { useState, useEffect } from "react";
|
||||
export default function SolutionsHero() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
// Auto-slide every 6 seconds
|
||||
// Auto-slide every 7 seconds — slower, more readable, professional pacing
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveSlide((prev) => (prev === 0 ? 1 : 0));
|
||||
}, 6000);
|
||||
}, 7000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
@@ -57,6 +57,10 @@ export default function SolutionsHero() {
|
||||
width: 100% !important;
|
||||
max-width: 1000px !important;
|
||||
margin: 0 auto !important;
|
||||
/* Contain the slider theme's inherited 20px padding (content-box) so
|
||||
width:100% + padding doesn't overflow the viewport and push the
|
||||
heading off-center on mobile. */
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .content-slider-item-heading,
|
||||
@@ -77,11 +81,23 @@ export default function SolutionsHero() {
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .text-content {
|
||||
text-align: center !important;
|
||||
max-width: 800px !important;
|
||||
max-width: 820px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* Larger, more readable hero subtitle on large/4K screens */
|
||||
.elementor-element.elementor-element-6c7cbcb .content-slider-item-text p {
|
||||
font-size: clamp(16px, 1.35vw, 23px) !important;
|
||||
line-height: 1.65 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
/* Match Home's hero frame so the floating navbar sits with the same
|
||||
breathing space. Home (.elementor-61) frames the hero card at 20px;
|
||||
this page falls through to the shared kit's 32px base. */
|
||||
.elementor-element.elementor-element-741f56c {
|
||||
padding: 20px !important;
|
||||
}
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-carousel.owl-theme .content-item {
|
||||
height: 800px !important;
|
||||
min-height: 800px !important;
|
||||
@@ -98,6 +114,12 @@ export default function SolutionsHero() {
|
||||
border-radius: 25px !important;
|
||||
}
|
||||
|
||||
/* The stage-outer carries an inline height:800px; collapse it to the
|
||||
slide height so there is no dead empty space below the hero card. */
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-stage-outer {
|
||||
height: 620px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-carousel.owl-theme .content-item {
|
||||
width: 100% !important;
|
||||
min-height: 620px !important;
|
||||
@@ -121,6 +143,10 @@ export default function SolutionsHero() {
|
||||
border-radius: 22px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-stage-outer {
|
||||
height: 560px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-6c7cbcb .owl-carousel.owl-theme .content-item {
|
||||
min-height: 560px !important;
|
||||
height: 560px !important;
|
||||
@@ -139,6 +165,28 @@ export default function SolutionsHero() {
|
||||
line-height: 1.05 !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
/* Pin the slide counter ("01/02") cleanly bottom-RIGHT — matching the
|
||||
Home and How-It-Works heroes. site.css parks it ~80px in from the
|
||||
right (looks left/centred), and the wide dot navigation eats the
|
||||
right side, so hide the dots on mobile and right-align the counter. */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .slider-footer {
|
||||
text-align: right !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .slider-footer .slider-pagination {
|
||||
justify-content: flex-end !important;
|
||||
margin-left: 22px !important;
|
||||
margin-right: 22px !important;
|
||||
}
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .owl-dots {
|
||||
display: none !important;
|
||||
}
|
||||
/* Hide the prev/next arrows on mobile (they overlap the counter) —
|
||||
slides still auto-rotate, matching the home hero. */
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .owl-nav {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
@@ -214,16 +262,47 @@ export default function SolutionsHero() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation controls (prev/next) — matches the home hero slider */}
|
||||
<div className="owl-nav">
|
||||
<button
|
||||
type="button"
|
||||
className="owl-next"
|
||||
onClick={() => setActiveSlide((prev) => (prev === 0 ? 1 : 0))}
|
||||
aria-label="Next"
|
||||
style={{ cursor: "pointer", border: "none", outline: "none" }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="owl-prev"
|
||||
onClick={() => setActiveSlide((prev) => (prev === 0 ? 1 : 0))}
|
||||
aria-label="Previous"
|
||||
style={{ cursor: "pointer", border: "none", outline: "none" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress indicators */}
|
||||
<div className="slider-footer slider-footer-position-after slider-footer-width-full slider-footer-view-inside">
|
||||
<div className="slider-footer-content">
|
||||
<div className="slider-pagination" style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "10px" }}>
|
||||
<div className="slider-progress-wrapper" style={{ marginRight: "15px" }}>
|
||||
<span className="slider-progress-current">{activeSlide === 0 ? "01" : "02"}</span>
|
||||
/
|
||||
<span className="slider-progress-all">02</span>
|
||||
<div className="slider-progress-wrapper" style={{ marginRight: "35px", display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
|
||||
<div style={{ fontSize: "16px", fontWeight: 600, color: "#FFFFFF", marginBottom: "4px" }}>
|
||||
<span className="slider-progress-current">{activeSlide === 0 ? "01" : "02"}</span>
|
||||
{" / "}
|
||||
<span className="slider-progress-all" style={{ opacity: 0.6 }}>02</span>
|
||||
</div>
|
||||
{/* Progress line — red bar slides to match the active slide (mirrors the home hero) */}
|
||||
<div style={{ width: "80px", height: "2px", background: "rgba(255, 255, 255, 0.2)", position: "relative", borderRadius: "1px", overflow: "hidden" }}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: activeSlide === 0 ? "0" : "50%",
|
||||
width: "50%",
|
||||
height: "100%",
|
||||
background: "#c01227",
|
||||
transition: "left 0.3s ease"
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="owl-dots owl-dots-6c7cbcb" style={{ display: "flex", gap: "8px" }}>
|
||||
<div className="owl-dots owl-dots-6c7cbcb" style={{ display: "none" }}>
|
||||
<button
|
||||
type="button"
|
||||
role="button"
|
||||
|
||||
@@ -2,35 +2,57 @@ import React from "react";
|
||||
import Image from "next/image";
|
||||
import { ScrollReveal } from "../../animations/Reveal";
|
||||
|
||||
type Stat = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type Stage = {
|
||||
img: string;
|
||||
label: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
desc: string;
|
||||
points: string[];
|
||||
stats?: Stat[];
|
||||
};
|
||||
|
||||
const STAGES: Stage[] = [
|
||||
{
|
||||
img: "/images/first-mile-approach.jpg",
|
||||
label: "01 / First Mile",
|
||||
title: "Origin to Hub",
|
||||
desc: "We collect freight directly from your facility, optimise vehicle assignment in real time, and consolidate loads for maximum efficiency before they reach the hub.",
|
||||
label: "Stage 01",
|
||||
title: "First Mile Warehouse",
|
||||
subtitle: "Consolidation & Prep",
|
||||
desc: "Incoming shipments are securely loaded, checked, and queued for transfer in our high-capacity fulfillment centers.",
|
||||
points: ["AI-scheduled pickups", "Dynamic load consolidation", "Yard & dock management"],
|
||||
stats: [
|
||||
{ value: "14,250", label: "Parcels Processed" },
|
||||
{ value: "99.98%", label: "Sorting Accuracy" }
|
||||
]
|
||||
},
|
||||
{
|
||||
img: "/images/mid-mile-approach.jpg",
|
||||
label: "02 / Mid Mile",
|
||||
title: "Hub to Hub Transit",
|
||||
desc: "Freight moves between hubs on optimised line-haul routes. Real-time tracking, cross-docking, and SLA monitoring keep every shipment on schedule.",
|
||||
label: "Stage 02",
|
||||
title: "Mid Mile Transit",
|
||||
subtitle: "Hub-to-Hub Transport",
|
||||
desc: "Freight is routed dynamically through our network of strategically located hubs using automated sortation and linehaul scheduling.",
|
||||
points: ["Optimised line-haul routing", "Cross-docking & sortation", "Live SLA monitoring"],
|
||||
stats: [
|
||||
{ value: "1,240+", label: "Daily Line-Hauls" },
|
||||
{ value: "98.5%", label: "SLA Adherence" }
|
||||
]
|
||||
},
|
||||
{
|
||||
img: "/images/last-mile-approach.jpg",
|
||||
label: "03 / Last Mile",
|
||||
title: "Hub to Doorstep",
|
||||
desc: "The final and most complex phase. We optimise multi-stop routes, deliver within precise windows, and capture digital proof of delivery at every door.",
|
||||
label: "Stage 03",
|
||||
title: "Last Mile Delivery",
|
||||
subtitle: "Hub to Doorstep",
|
||||
desc: "The final handoff. Our routing engine optimizes multi-stop itineraries to deliver parcels directly to customers' doorsteps in record time.",
|
||||
points: ["Multi-stop route optimisation", "Precise delivery windows", "Digital proof of delivery"],
|
||||
stats: [
|
||||
{ value: "450K+", label: "Happy Deliveries" },
|
||||
{ value: "2.8 Hours", label: "Average Turnaround" }
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
@@ -40,18 +62,12 @@ export default function WhyChooseDoormile() {
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
/* =====================================================================
|
||||
"Why Businesses Choose Doormile" — First / Mid / Last Mile stage cards.
|
||||
Dark rounded card on the white page (consistent with the Miles3
|
||||
section), each stage shown with a photo, numbered red label, title,
|
||||
description and a red-checkmark feature list.
|
||||
Card titles are <h3>; theme-core forces a dark color on bare headings
|
||||
(.logico-front-end h3:not([class*=logico-title-h]) @ (0,2,1)), so the
|
||||
white title rule is prefixed to outrank it.
|
||||
===================================================================== */
|
||||
.wcd-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
margin: -250px 20px 20px 20px; /* Snug pull-up overlap to touch Miles3 columns without covering their text! */
|
||||
margin: -250px 20px 20px 20px;
|
||||
background-color: #1F1F1F;
|
||||
border-radius: 0 0 25px 25px;
|
||||
padding: 50px 0 110px;
|
||||
@@ -63,7 +79,6 @@ export default function WhyChooseDoormile() {
|
||||
padding: 0 50px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* Centered header block (eyebrow + heading) with a faint map backdrop */
|
||||
.wcd-head {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
@@ -78,16 +93,16 @@ export default function WhyChooseDoormile() {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 75%; /* Shifted down to the bottom of the header block to overlap the top of the cards */
|
||||
top: 75%;
|
||||
transform: translateX(-50%);
|
||||
width: min(1180px, 95%);
|
||||
aspect-ratio: 2 / 1;
|
||||
background: url('/images/bg-map.png') center / contain no-repeat;
|
||||
opacity: 0.06; /* Elegant faint visibility */
|
||||
filter: invert(1); /* Invert dark map dots to white/light-gray to make them visible on the #1F1F1F background */
|
||||
opacity: 0.06;
|
||||
filter: invert(1);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
animation: wcd-float 20s ease-in-out infinite; /* Premium floating map animation */
|
||||
animation: wcd-float 20s ease-in-out infinite;
|
||||
}
|
||||
.wcd-card-wrapper {
|
||||
display: flex;
|
||||
@@ -127,18 +142,23 @@ export default function WhyChooseDoormile() {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Premium Glassmorphism & Card Interaction */
|
||||
.wcd-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
transition: border-color 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
box-shadow 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
.wcd-card:hover {
|
||||
border-color: #c01227 !important;
|
||||
box-shadow: 0 10px 30px rgba(192, 18, 39, 0.25) !important;
|
||||
box-shadow: 0 20px 40px rgba(192, 18, 39, 0.15), inset 0 0 20px rgba(255, 255, 255, 0.02) !important;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
.wcd-card-media {
|
||||
@@ -152,82 +172,157 @@ export default function WhyChooseDoormile() {
|
||||
transition: transform 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
.wcd-card:hover .wcd-card-media img {
|
||||
transform: scale(1.06);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Body Data Container (Unified Div) */
|
||||
.wcd-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* Modern Pill Badge for Label */
|
||||
.wcd-card-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
padding: 6px 14px;
|
||||
background: rgba(192, 18, 39, 0.08);
|
||||
border: 1px solid rgba(192, 18, 39, 0.25);
|
||||
border-radius: 100px;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif;
|
||||
font-size: 14px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #c01227;
|
||||
color: #ff3344;
|
||||
margin: 0 0 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.wcd-card:hover .wcd-card-label {
|
||||
background: rgba(192, 18, 39, 0.18);
|
||||
border-color: #c01227;
|
||||
box-shadow: 0 0 10px rgba(192, 18, 39, 0.15);
|
||||
}
|
||||
|
||||
.logico-front-end .wcd-section h3.wcd-card-title {
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif;
|
||||
font-size: 32px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #FFFFFF;
|
||||
-webkit-text-fill-color: #FFFFFF;
|
||||
margin: 0 0 22px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.wcd-card-subtitle {
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
margin: 0 0 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.wcd-card-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||
margin: 0 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wcd-card-desc {
|
||||
font-size: 17px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 0 34px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.wcd-card-points {
|
||||
list-style: none;
|
||||
margin: auto 0 0;
|
||||
margin: 0 0 30px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
.wcd-card-points li {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
.wcd-section .wcd-card-points li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding-left: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
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);
|
||||
.wcd-section .wcd-card-points li::before {
|
||||
content: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wcd-card-points .wcd-check {
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 0.08em;
|
||||
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);
|
||||
}
|
||||
|
||||
/* Integrated Stats Section */
|
||||
.wcd-card-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: auto;
|
||||
padding-top: 24px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.wcd-stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wcd-stat-value {
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #FFFFFF;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.wcd-card:hover .wcd-stat-value {
|
||||
color: #ff3344;
|
||||
}
|
||||
.wcd-stat-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@media (max-width: 1020px) {
|
||||
.wcd-section { margin: -150px 15px 15px 15px; padding: 40px 0 80px; }
|
||||
.wcd-section { margin: 0 15px 15px 15px; padding: 40px 0 80px; }
|
||||
.wcd-inner { padding: 0 30px; }
|
||||
.wcd-grid { grid-template-columns: 1fr; gap: 24px; }
|
||||
.wcd-card-body { padding: 32px; }
|
||||
.logico-front-end .wcd-section h3.wcd-card-title { font-size: 28px; }
|
||||
.logico-front-end .wcd-section h3.wcd-card-title { font-size: 24px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.wcd-section { margin: -100px 12px 12px 12px; border-radius: 0 0 20px 20px; padding: 30px 0 64px; }
|
||||
.wcd-section { margin: 0 12px 12px 12px; border-radius: 0 0 20px 20px; padding: 30px 0 64px; }
|
||||
.wcd-inner { padding: 0 20px; }
|
||||
.wcd-card-body { padding: 24px; }
|
||||
}
|
||||
@@ -260,15 +355,52 @@ export default function WhyChooseDoormile() {
|
||||
sizes="(max-width: 1020px) 100vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
{/* Single Parent Div for All Data Content */}
|
||||
<div className="wcd-card-body">
|
||||
<div className="wcd-card-label">{stage.label}</div>
|
||||
|
||||
<h3 className="wcd-card-title">{stage.title}</h3>
|
||||
|
||||
{stage.subtitle && (
|
||||
<div className="wcd-card-subtitle">{stage.subtitle}</div>
|
||||
)}
|
||||
|
||||
<div className="wcd-card-divider" />
|
||||
|
||||
<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>
|
||||
|
||||
{/* Integrated Statistics Grid */}
|
||||
{stage.stats && stage.stats.length > 0 && (
|
||||
<div className="wcd-card-stats">
|
||||
{stage.stats.map((stat) => (
|
||||
<div key={stat.label} className="wcd-stat-item">
|
||||
<span className="wcd-stat-value">{stat.value}</span>
|
||||
<span className="wcd-stat-label">{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -179,6 +179,21 @@ export default function WomenSection() {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
/* The banner image (bg-header-women.png) is very wide (≈2.48:1) and the
|
||||
woman — the subject of this section — sits on the right. On narrow
|
||||
screens "cover" center-crops her out, so shift the focal point right
|
||||
to keep her face in frame. */
|
||||
@media (max-width: 1024px) {
|
||||
.elementor-element.elementor-element-7da6646:not(.elementor-motion-effects-element-type-background) {
|
||||
background-position: 78% center !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.elementor-element.elementor-element-7da6646:not(.elementor-motion-effects-element-type-background) {
|
||||
background-position: 82% center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stack the two columns below the desktop breakpoint so the box group
|
||||
always gets full width and stays centered — never clipped. */
|
||||
@media (max-width: 1024px) {
|
||||
@@ -187,11 +202,46 @@ export default function WomenSection() {
|
||||
}
|
||||
.elementor-element.elementor-element-2ed47f3 { grid-template-columns: 1fr !important; }
|
||||
#ws-stories { max-width: 640px; margin: 0 auto; }
|
||||
/* Align the heading, image and stats/cards to one shared left/right
|
||||
edge — the heading + image widgets carried extra inset that left
|
||||
them staggered (80px / 70px / 60px) against the stats column. */
|
||||
#women-entrepreneurship .elementor-element-36efec7,
|
||||
#women-entrepreneurship .elementor-element-b2c956f {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
#women-entrepreneurship .elementor-element-778840d,
|
||||
#women-entrepreneurship .elementor-element-bbfb67f,
|
||||
#women-entrepreneurship .elementor-element-778840d > .elementor-widget-container,
|
||||
#women-entrepreneurship .elementor-element-bbfb67f > .elementor-widget-container {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#ws-stories .ws__stats { grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||||
#ws-stories .ws__stat { padding: 14px 10px; }
|
||||
#ws-stories .ws__stat-label { font-size: 11px !important; letter-spacing: 0.02em; }
|
||||
/* KPI stat cards stack one-per-row on mobile. Inside each card the big
|
||||
number sits on the left and the label on the right, vertically
|
||||
centered, so every stat reads as a clean full-width row instead of
|
||||
three cramped columns. */
|
||||
#ws-stories .ws__stats { grid-template-columns: 1fr; gap: 12px; }
|
||||
#ws-stories .ws__stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 20px 22px;
|
||||
min-width: 0;
|
||||
}
|
||||
#ws-stories .ws__stat-num { font-size: clamp(30px, 9vw, 40px); }
|
||||
#ws-stories .ws__stat-label {
|
||||
margin-top: 0;
|
||||
font-size: 13px !important;
|
||||
letter-spacing: 0.02em;
|
||||
text-align: right;
|
||||
min-height: 0;
|
||||
}
|
||||
#ws-stories .ws__cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
`}} />
|
||||
@@ -265,7 +315,7 @@ export default function WomenSection() {
|
||||
<div className="elementor-element elementor-element-90cc867 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="90cc867" data-element_type="container" data-e-type="container">
|
||||
<div className="elementor-element elementor-element-24c0280 elementor-widget__width-inherit elementor-widget elementor-widget-logico_heading" data-id="24c0280" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title">/ Success Stories /</div>
|
||||
<div className="logico-title">/ Women Empowerment /</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,243 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import OptimizationSection from "../optimization/OptimizationSection";
|
||||
import React from "react";
|
||||
import EVSection, { EVStat, EVBadge, EVSlide, EVCardsTheme } from "./EVSection";
|
||||
import WorkflowScene from "./WorkflowScene";
|
||||
|
||||
/* Cyan / electric-blue — matches the Optimization Engine scene palette. */
|
||||
const THEME: EVCardsTheme = {
|
||||
accent: "#00E5FF",
|
||||
accent2: "#3B82F6",
|
||||
glow: "rgba(0,229,255,0.22)",
|
||||
};
|
||||
|
||||
/**
|
||||
* Workflow 1 — Performance (hybrid split-screen).
|
||||
*
|
||||
* Keeps the premium EVSection chrome (banner → floating card → dark section →
|
||||
* stat bar) but converts the body into a split layout:
|
||||
* • Left — the PRODUCTION Optimization Engine Three.js scene (the same
|
||||
* OptimizationCanvas used by OptimizationSection: depot, trucks,
|
||||
* route optimization, shaders, particles). One instance, mounted
|
||||
* compactly instead of as a multi-viewport pinned scroll.
|
||||
* • Right — lightweight auto-rotating cards (4s / 600ms fade+slide).
|
||||
*
|
||||
* This preserves the 3D storytelling while dramatically cutting page height.
|
||||
*/
|
||||
const SLIDES: EVSlide[] = [
|
||||
{
|
||||
status: "Optimization Running",
|
||||
title: "Route Optimization",
|
||||
value: 42,
|
||||
suffix: "%",
|
||||
metricLabel: "Distance Saved",
|
||||
kpis: ["Route optimization active", "37% fewer vehicles required", "SLA compliance 99.9%"],
|
||||
desc: "AI selects the most efficient delivery paths across every zone, cutting unnecessary travel and fuel and battery consumption.",
|
||||
},
|
||||
{
|
||||
status: "Fleet Balancing",
|
||||
title: "Distance Reduction",
|
||||
value: 37,
|
||||
suffix: "%",
|
||||
metricLabel: "Fewer Vehicles",
|
||||
kpis: ["Load balancing engaged", "Same volume, leaner fleet", "Lower maintenance & staffing"],
|
||||
desc: "Intelligent load balancing fulfils the same order volume with a leaner, better-utilised fleet — fewer miles, fewer vehicles.",
|
||||
},
|
||||
{
|
||||
status: "Dispatch Active",
|
||||
title: "Fleet Efficiency",
|
||||
value: 31,
|
||||
suffix: "%",
|
||||
metricLabel: "Lower Operating Cost",
|
||||
kpis: ["Higher fleet utilisation", "Predictable operations", "Reduced fuel & overhead"],
|
||||
desc: "Smart grouping and dispatch keep operations smooth and predictable while reducing maintenance and staffing cost.",
|
||||
},
|
||||
{
|
||||
status: "SLA Safe",
|
||||
title: "SLA Performance",
|
||||
value: 99.9,
|
||||
decimals: 1,
|
||||
suffix: "%",
|
||||
metricLabel: "On-Time Delivery",
|
||||
kpis: ["Real-time route correction", "Consistent delivery windows", "100% order fulfilment"],
|
||||
desc: "Real-time routing keeps deliveries on time across all zones, sustaining high customer satisfaction and SLA performance.",
|
||||
},
|
||||
];
|
||||
|
||||
const BADGES: EVBadge[] = [
|
||||
{ value: "-42%", label: "DISTANCE SAVED" },
|
||||
{ value: "-37%", label: "FEWER VEHICLES" },
|
||||
];
|
||||
|
||||
const STATS: EVStat[] = [
|
||||
{ value: 42, suffix: "%", label: "Distance Saved" },
|
||||
{ value: 28, suffix: "%", label: "Faster Routes" },
|
||||
{ value: 31, suffix: "%", label: "Lower Cost" },
|
||||
{ value: 99.9, decimals: 1, suffix: "%", label: "On-Time" },
|
||||
];
|
||||
|
||||
export default function Workflow1() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
title: "PERFORMANCE",
|
||||
text: "Our AI-powered routing system reduces unnecessary travel by selecting the most efficient delivery paths across the city. This helps lower fuel and battery consumption while improving delivery speed and operational efficiency. Businesses can complete more deliveries in less time with significantly reduced logistics costs."
|
||||
},
|
||||
{
|
||||
title: "PERFORMANCE",
|
||||
text: "The optimization engine intelligently groups and balances deliveries, allowing the same order volume to be fulfilled with fewer vehicles. This improves fleet utilization, reduces maintenance and staffing costs, and increases overall delivery efficiency. Even with fewer vehicles, the platform maintains smooth and reliable operations."
|
||||
},
|
||||
{
|
||||
title: "PERFORMANCE",
|
||||
text: "Real-time route optimization ensures predictable and on-time deliveries across all delivery zones. By reducing delays and improving route planning, businesses can maintain high customer satisfaction and strong SLA performance. The system delivers lower operational costs while consistently maintaining 100% order fulfillment."
|
||||
}
|
||||
];
|
||||
|
||||
// Auto-advance the carousel every 5s, infinite loop. Keyed on activeSlide so any
|
||||
// manual selection resets the timer; pauses while the user hovers the card.
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
const id = setTimeout(() => {
|
||||
setActiveSlide((prev) => (prev + 1) % slides.length);
|
||||
}, 5000);
|
||||
return () => clearTimeout(id);
|
||||
}, [activeSlide, paused, slides.length]);
|
||||
|
||||
return (
|
||||
<section className="dm-wf1" aria-label="Workflow 1 — Impact of Optimisation & Performance">
|
||||
|
||||
{/* ── Top sub-section: the full interactive "Impact of Optimisation" experience ── */}
|
||||
<OptimizationSection />
|
||||
|
||||
{/* ── 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)}>
|
||||
{/* 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">
|
||||
<path
|
||||
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
|
||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
|
||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Quotes & Text Content */}
|
||||
<div className="dm-workflow-right">
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
|
||||
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
||||
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
||||
</svg>
|
||||
|
||||
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
|
||||
|
||||
<div className="dm-workflow-text-container">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.p
|
||||
key={activeSlide}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: "easeInOut" }}
|
||||
className="dm-workflow-text"
|
||||
>
|
||||
{slides[activeSlide].text}
|
||||
</motion.p>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="dm-workflow-nav">
|
||||
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
|
||||
<div className="dm-workflow-bars">
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
|
||||
onClick={() => setActiveSlide(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: styles }} />
|
||||
</section>
|
||||
<EVSection
|
||||
ariaLabel="Workflow 1 — Performance"
|
||||
gapTop
|
||||
gapBottom
|
||||
bannerImage="/images/home3-slide-1.jpg"
|
||||
cardTitle="OPTIMIZE EVERY MILE"
|
||||
cardSubtitle="Cut travel distance, reduce operating cost, and improve fleet productivity across every route."
|
||||
eyebrow="/ Performance /"
|
||||
titleLead="SMARTER ROUTES. "
|
||||
titleAccent="LOWER COSTS."
|
||||
mediaSlot={<WorkflowScene variant="optimization" ariaLabel="Live route optimization engine" />}
|
||||
slides={SLIDES}
|
||||
cardsHeading="Performance Insight"
|
||||
cardsTheme={THEME}
|
||||
badges={BADGES}
|
||||
stats={STATS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = `
|
||||
/* ============================================================
|
||||
Workflow 1 = ONE container:
|
||||
├─ Impact of Optimisation (full interactive OptimizationSection)
|
||||
└─ Performance (content card, flush + colour-matched)
|
||||
The Performance card is pulled up to butt against the optimisation
|
||||
card's flat bottom and shares its dark-navy surface, so the two
|
||||
read as a single continuous container with no gap / no break.
|
||||
============================================================ */
|
||||
.dm-wf1 {
|
||||
position: relative;
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
|
||||
/* Cancel the global "section { padding: 6rem 0 }" (custom-frontend.min.css): both
|
||||
this wrapper and the nested .dm-opt are sections, so that 96px top+bottom stacked
|
||||
into large empty bands above / between the workflows. These are full-bleed pinned
|
||||
experiences whose cards butt together via their own insets — no section padding. */
|
||||
.dm-wf1, .dm-wf1 .dm-opt { padding-top: 0; padding-bottom: 0; }
|
||||
|
||||
/* 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 20px 0;
|
||||
background: linear-gradient(180deg, #030a18 0%, #06101f 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-top: none;
|
||||
border-radius: 0 0 42px 42px;
|
||||
/* No shadow: this card is flush under the optimisation card and merges with it as one
|
||||
continuous container — a shadow here would re-introduce a dark band at the seam. */
|
||||
box-shadow: none;
|
||||
padding: 36px 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dm-workflow-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 440px;
|
||||
}
|
||||
.dm-workflow-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 8px 24px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
.dm-workflow-right {
|
||||
flex: 1.2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
.dm-workflow-quote { margin-bottom: 5px; }
|
||||
|
||||
.dm-workflow-title {
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC !important;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dm-workflow-text-container { min-height: 110px; width: 100%; }
|
||||
.dm-workflow-text {
|
||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
color: #A3A3A3;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.dm-workflow-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
align-self: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.dm-workflow-counter {
|
||||
font-family: var(--font-space-grotesk), sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #737373;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.dm-workflow-bars { display: flex; gap: 8px; }
|
||||
.dm-workflow-bar {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.dm-workflow-bar.is-active { background: #C01227; }
|
||||
.dm-workflow-bar:hover { background: rgba(255, 255, 255, 0.35); }
|
||||
.dm-workflow-bar.is-active:hover { background: #C01227; }
|
||||
|
||||
/* ── Responsive — keep insets/radius aligned to the optimisation card ── */
|
||||
@media (max-width: 1024px) {
|
||||
.dm-wf1-card {
|
||||
margin: 0 20px 0;
|
||||
border-radius: 0 0 42px 42px;
|
||||
padding: 44px 44px;
|
||||
gap: 44px;
|
||||
}
|
||||
.dm-workflow-title { font-size: 32px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.dm-wf1-card {
|
||||
margin: 0 10px 0;
|
||||
border-radius: 0 0 28px 28px;
|
||||
padding: 36px 28px;
|
||||
gap: 36px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dm-workflow-left { max-width: 280px; }
|
||||
.dm-workflow-right { width: 100%; }
|
||||
.dm-workflow-title { font-size: 28px; }
|
||||
.dm-workflow-text-container { min-height: auto; }
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,243 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import LogisticsBrainSection from "../logisticsbrain/LogisticsBrainSection";
|
||||
import React from "react";
|
||||
import EVSection, { EVStat, EVBadge, EVSlide, EVCardsTheme } from "./EVSection";
|
||||
import WorkflowScene from "./WorkflowScene";
|
||||
|
||||
/* Red / crimson / orange — matches the Routing Engine (logistics brain) scene. */
|
||||
const THEME: EVCardsTheme = {
|
||||
accent: "#E2354A",
|
||||
accent2: "#F59E0B",
|
||||
glow: "rgba(226,53,74,0.24)",
|
||||
};
|
||||
|
||||
/**
|
||||
* Workflow 2 — Innovation (hybrid split-screen).
|
||||
*
|
||||
* Keeps the premium EVSection chrome but converts the body into a split layout:
|
||||
* • Left — the PRODUCTION Routing Engine Three.js scene (the same
|
||||
* LogisticsBrainCanvas used by LogisticsBrainSection: city nodes,
|
||||
* buildings, multi-route generation, constraint evaluation,
|
||||
* network/brain animation). One instance, mounted compactly.
|
||||
* • Right — lightweight auto-rotating cards (4s / 600ms fade+slide).
|
||||
*
|
||||
* Preserves the 3D storytelling while dramatically cutting page height.
|
||||
*/
|
||||
const SLIDES: EVSlide[] = [
|
||||
{
|
||||
status: "Generating Routes",
|
||||
title: "Generate Routes",
|
||||
value: 6,
|
||||
suffix: " plans",
|
||||
metricLabel: "Route Plans Generated",
|
||||
kpis: ["Parallel strategies explored", "59 orders in scope", "Real-time combinations"],
|
||||
desc: "The Parallel Universe Engine evaluates many routing strategies at once for every dispatch window, exploring route combinations in real time.",
|
||||
},
|
||||
{
|
||||
status: "Constraints Passed",
|
||||
title: "Check Constraints",
|
||||
value: 5,
|
||||
metricLabel: "Constraints Evaluated",
|
||||
kpis: ["Battery aware", "Capacity & distance checked", "Powered by Google OR-Tools"],
|
||||
desc: "Battery, distance, capacity and time are first-class inputs — battery-aware simulation solves the EV routing challenge.",
|
||||
},
|
||||
{
|
||||
status: "Scoring Routes",
|
||||
title: "Score & Compare",
|
||||
value: 12,
|
||||
suffix: "+",
|
||||
metricLabel: "Strategies Compared",
|
||||
kpis: ["Ranked by total cost", "SLA protected", "Real-time ETA validation"],
|
||||
desc: "Every plan is benchmarked in parallel and ranked by total cost, with sub-45ms inference at production scale.",
|
||||
},
|
||||
{
|
||||
status: "Delivery Ready",
|
||||
title: "Select Best Plan",
|
||||
value: 45,
|
||||
suffix: "ms",
|
||||
metricLabel: "Decision Latency",
|
||||
kpis: ["Late plans rejected", "Best plan locked in", "Dispatched to the fleet"],
|
||||
desc: "Late plans are rejected automatically and the highest-performing, SLA-first plan is locked in and dispatched.",
|
||||
},
|
||||
];
|
||||
|
||||
const BADGES: EVBadge[] = [
|
||||
{ value: "45ms", label: "INFERENCE" },
|
||||
{ value: "100%", label: "SLA-FIRST" },
|
||||
];
|
||||
|
||||
const STATS: EVStat[] = [
|
||||
{ value: 45, suffix: "ms", label: "Inference" },
|
||||
{ value: 12, suffix: "+", label: "Strategies" },
|
||||
{ value: 99.9, decimals: 1, suffix: "%", label: "SLA Met" },
|
||||
{ value: 24, suffix: "/7", label: "Adaptive" },
|
||||
];
|
||||
|
||||
export default function Workflow2() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
title: "INNOVATION",
|
||||
text: "Our Parallel Universe Engine simultaneously evaluates multiple routing strategies to identify the most efficient delivery plan for every dispatch window. By simulating different route combinations in real time, the system ensures faster, smarter, and more cost-effective logistics decisions. This enables businesses to maintain high operational accuracy while adapting dynamically to changing delivery conditions."
|
||||
},
|
||||
{
|
||||
title: "INNOVATION",
|
||||
text: "The platform solves the EV routing challenge through intelligent battery-aware simulations and advanced optimization logic powered by Google OR-Tools. It balances delivery efficiency, charging constraints, and SLA priorities to maximize fleet performance without compromising reliability. This creates a scalable and future-ready logistics system designed for both traditional and EV fleets."
|
||||
},
|
||||
{
|
||||
title: "INNOVATION",
|
||||
text: "With sub-45ms inference latency and real-time ETA validation, the engine delivers instant routing decisions with exceptional precision. Multiple strategy universes are benchmarked in parallel to consistently select the best-performing route configuration. The result is highly reliable, SLA-first delivery operations with improved customer experience and operational consistency."
|
||||
}
|
||||
];
|
||||
|
||||
// Auto-advance the carousel every 5s, infinite loop. Keyed on activeSlide so any
|
||||
// manual selection resets the timer; pauses while the user hovers the card.
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
const id = setTimeout(() => {
|
||||
setActiveSlide((prev) => (prev + 1) % slides.length);
|
||||
}, 5000);
|
||||
return () => clearTimeout(id);
|
||||
}, [activeSlide, paused, slides.length]);
|
||||
|
||||
return (
|
||||
<section className="dm-wf2" aria-label="Workflow 2 — How Our Logistics Brain Works & Innovation">
|
||||
|
||||
{/* ── Top sub-section: the complete "How Our Logistics Brain Works" experience ── */}
|
||||
<LogisticsBrainSection connected />
|
||||
|
||||
{/* ── 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)}>
|
||||
{/* 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">
|
||||
<path
|
||||
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
|
||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
|
||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Quotes & Text Content */}
|
||||
<div className="dm-workflow-right">
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
|
||||
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
||||
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
||||
</svg>
|
||||
|
||||
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
|
||||
|
||||
<div className="dm-workflow-text-container">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.p
|
||||
key={activeSlide}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: "easeInOut" }}
|
||||
className="dm-workflow-text"
|
||||
>
|
||||
{slides[activeSlide].text}
|
||||
</motion.p>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="dm-workflow-nav">
|
||||
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
|
||||
<div className="dm-workflow-bars">
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
|
||||
onClick={() => setActiveSlide(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: styles }} />
|
||||
</section>
|
||||
<EVSection
|
||||
ariaLabel="Workflow 2 — Innovation"
|
||||
gapBottom
|
||||
bannerImage="/images/mid-mile-approach.jpg"
|
||||
cardTitle="CHOOSE THE BEST PLAN"
|
||||
cardSubtitle="Analyze thousands of route possibilities and automatically select the most efficient delivery strategy."
|
||||
eyebrow="/ Innovation /"
|
||||
titleLead="MANY STRATEGIES. "
|
||||
titleAccent="ONE BEST PLAN."
|
||||
mediaSlot={<WorkflowScene variant="logistics" ariaLabel="Live multi-route logistics brain" />}
|
||||
slides={SLIDES}
|
||||
cardsHeading="AI Decision Engine"
|
||||
cardsTheme={THEME}
|
||||
badges={BADGES}
|
||||
stats={STATS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = `
|
||||
/* ============================================================
|
||||
Workflow 2 = ONE container:
|
||||
├─ How Our Logistics Brain Works (full LogisticsBrainSection)
|
||||
└─ Innovation (content card, flush + colour-matched)
|
||||
The Innovation card is pulled up to butt against the logistics-brain
|
||||
card's flat bottom and shares its dark red/black surface, so the two
|
||||
read as a single continuous container with no gap / no break — the
|
||||
same connected storytelling structure used in Workflow 1
|
||||
(Impact of Optimisation → Performance).
|
||||
============================================================ */
|
||||
.dm-wf2 {
|
||||
position: relative;
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
|
||||
/* Cancel the global "section { padding: 6rem 0 }" (custom-frontend.min.css): both
|
||||
this wrapper and the nested .dm-lb are sections, so that 96px top+bottom stacked
|
||||
into large empty bands above / between the workflows. These are full-bleed pinned
|
||||
experiences whose cards butt together via their own insets — no section padding. */
|
||||
.dm-wf2, .dm-wf2 .dm-lb { padding-top: 0; padding-bottom: 0; }
|
||||
|
||||
/* 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 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;
|
||||
border-radius: 0 0 28px 28px;
|
||||
/* No shadow: this card is flush under the logistics-brain card and merges with it as one
|
||||
continuous container — a shadow here would re-introduce a dark band at the seam. */
|
||||
box-shadow: none;
|
||||
padding: 36px 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dm-workflow-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 440px;
|
||||
}
|
||||
.dm-workflow-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 8px 24px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
.dm-workflow-right {
|
||||
flex: 1.2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
.dm-workflow-quote { margin-bottom: 5px; }
|
||||
|
||||
.dm-workflow-title {
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC !important;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dm-workflow-text-container { min-height: 110px; width: 100%; }
|
||||
.dm-workflow-text {
|
||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
color: #A3A3A3;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.dm-workflow-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
align-self: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.dm-workflow-counter {
|
||||
font-family: var(--font-space-grotesk), sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #737373;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.dm-workflow-bars { display: flex; gap: 8px; }
|
||||
.dm-workflow-bar {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.dm-workflow-bar.is-active { background: #C01227; }
|
||||
.dm-workflow-bar:hover { background: rgba(255, 255, 255, 0.35); }
|
||||
.dm-workflow-bar.is-active:hover { background: #C01227; }
|
||||
|
||||
/* ── Responsive — keep insets/radius aligned to the logistics-brain card ── */
|
||||
@media (max-width: 1024px) {
|
||||
.dm-wf2-card {
|
||||
padding: 44px 44px;
|
||||
gap: 44px;
|
||||
}
|
||||
.dm-workflow-title { font-size: 32px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dm-wf2-card {
|
||||
margin: 0 10px 0;
|
||||
border-radius: 0 0 20px 20px;
|
||||
padding: 36px 28px;
|
||||
gap: 36px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dm-workflow-left { max-width: 280px; }
|
||||
.dm-workflow-right { width: 100%; }
|
||||
.dm-workflow-title { font-size: 28px; }
|
||||
.dm-workflow-text-container { min-height: auto; }
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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,15 +25,36 @@ export default function Workflow3() {
|
||||
}
|
||||
];
|
||||
|
||||
// Auto-advance the carousel every 5s, infinite loop. Keyed on activeSlide so any
|
||||
// manual selection resets the timer; pauses while the user hovers the card.
|
||||
// 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(() => {
|
||||
if (paused) return;
|
||||
setActiveSlide(0);
|
||||
}, []);
|
||||
|
||||
// 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(() => {
|
||||
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);
|
||||
}, 5000);
|
||||
}, 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">
|
||||
@@ -43,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">
|
||||
@@ -74,7 +97,7 @@ export default function Workflow3() {
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: "easeInOut" }}
|
||||
transition={{ duration: 0.7, ease: "easeInOut" }}
|
||||
className="dm-workflow-text"
|
||||
>
|
||||
{slides[activeSlide].text}
|
||||
@@ -118,7 +141,7 @@ const styles = `
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
|
||||
/* Cancel the global "section { padding: 6rem 0 }" (custom-frontend.min.css): both
|
||||
/* Cancel the global "section { padding: 6rem 0 }" (consolidated into /public/css/site.css): both
|
||||
this wrapper and the nested .dm-st are sections, so that 96px top+bottom stacked
|
||||
into large empty bands above / between the workflows. These are full-bleed pinned
|
||||
experiences whose cards butt together via their own insets — no section padding. */
|
||||
@@ -177,11 +200,12 @@ const styles = `
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dm-workflow-text-container { min-height: 110px; width: 100%; }
|
||||
.dm-workflow-text-container { min-height: 150px; width: 100%; }
|
||||
.dm-workflow-text {
|
||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
font-size: 21px;
|
||||
line-height: 1.75;
|
||||
letter-spacing: 0.01em;
|
||||
color: #A3A3A3;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
@@ -221,18 +245,38 @@ const styles = `
|
||||
@media (max-width: 1024px) {
|
||||
.dm-wf3-card { padding: 44px 44px; gap: 44px; }
|
||||
.dm-workflow-title { font-size: 32px; }
|
||||
.dm-workflow-text { font-size: 19px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
/* Mobile: compact card so it never exceeds ~500px (was ~850px from the full
|
||||
desktop chevron + long paragraph). Smaller chevron, tighter spacing and a
|
||||
line-clamped paragraph keep the workflow state readable without a long scroll. */
|
||||
.dm-wf3-card {
|
||||
margin: 0 10px 0;
|
||||
/* Bottom gap separates this last workflow card from the contact section below. */
|
||||
margin: 0 10px 16px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
padding: 36px 28px;
|
||||
gap: 36px;
|
||||
padding: 26px 22px;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dm-workflow-left { max-width: 280px; }
|
||||
.dm-workflow-right { width: 100%; }
|
||||
.dm-workflow-title { font-size: 28px; }
|
||||
.dm-workflow-left { max-width: 128px; }
|
||||
.dm-workflow-right { width: 100%; gap: 12px; }
|
||||
.dm-workflow-quote { margin-bottom: 2px; }
|
||||
.dm-workflow-title { font-size: 22px; }
|
||||
.dm-workflow-text-container { min-height: auto; }
|
||||
.dm-workflow-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dm-workflow-nav { margin-top: 4px; }
|
||||
}
|
||||
@media (max-width: 390px) {
|
||||
.dm-workflow-left { max-width: 108px; }
|
||||
.dm-workflow-title { font-size: 20px; }
|
||||
.dm-workflow-text { font-size: 14px; -webkit-line-clamp: 4; }
|
||||
}
|
||||
`;
|
||||
|
||||
180
src/components/sections/WorkflowScene.tsx
Normal file
180
src/components/sections/WorkflowScene.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
/* ============================================================
|
||||
WorkflowScene — compact, self-driven mount of the PRODUCTION
|
||||
Three.js scenes (OptimizationCanvas / LogisticsBrainCanvas).
|
||||
|
||||
These are the EXACT same scene components used by the full
|
||||
OptimizationSection / LogisticsBrainSection experiences — same
|
||||
shaders, assets, particles, camera rig and animation system.
|
||||
The only difference here is how `progress` is driven: instead
|
||||
of a multi-viewport pinned scroll (which made the workflows
|
||||
extremely tall), the scene idles in its "settled" narrative
|
||||
band with a gentle autonomous oscillation so it stays alive
|
||||
inside the split-screen media panel.
|
||||
|
||||
Exactly ONE canvas instance is created per workflow — no
|
||||
duplicate scenes, no duplicate render loops. The render loop
|
||||
is paused (frameloop="never") whenever the panel is off-screen.
|
||||
============================================================ */
|
||||
|
||||
// Client-only, code-split so the heavy 3D bundle never blocks first paint.
|
||||
const OptimizationCanvas = dynamic(() => import("../optimization/OptimizationCanvas"), { ssr: false });
|
||||
const LogisticsBrainCanvas = dynamic(() => import("../logisticsbrain/LogisticsBrainCanvas"), { ssr: false });
|
||||
|
||||
type Variant = "optimization" | "logistics";
|
||||
|
||||
/** Per-scene background (matches each Canvas's own <color attach="background">)
|
||||
* so there is no flash before the WebGL context paints. */
|
||||
const SCENE_BG: Record<Variant, string> = {
|
||||
optimization: "#020617",
|
||||
logistics: "#08080c",
|
||||
};
|
||||
|
||||
/** Settled narrative band each scene idles within. The optimization scene reads
|
||||
* best inside its post-optimize band (mirrors the production mobile idle); the
|
||||
* logistics scene around its score/network phase where city + routes + brain
|
||||
* are all alive. Held at a single static value under reduced-motion. */
|
||||
const IDLE: Record<Variant, { center: number; amp: number; speed: number; still: number }> = {
|
||||
optimization: { center: 0.78, amp: 0.14, speed: 0.5, still: 0.85 },
|
||||
logistics: { center: 0.5, amp: 0.13, speed: 0.4, still: 0.52 },
|
||||
};
|
||||
|
||||
export default function WorkflowScene({
|
||||
variant,
|
||||
ariaLabel,
|
||||
}: {
|
||||
variant: Variant;
|
||||
ariaLabel?: string;
|
||||
}) {
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const progressRef = useRef(IDLE[variant].still);
|
||||
|
||||
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);
|
||||
|
||||
// Responsive + reduced-motion flags (same breakpoints as the production sections).
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Mount the heavy scene a little before it enters; keep the render loop gated
|
||||
// to actual visibility so it never burns frames off-screen.
|
||||
useEffect(() => {
|
||||
const el = wrapRef.current;
|
||||
if (!el) return;
|
||||
const mountIo = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) {
|
||||
setMountScene(true);
|
||||
setSceneActive(true);
|
||||
mountIo.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "60% 0px" },
|
||||
);
|
||||
const activeIo = new IntersectionObserver(
|
||||
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
|
||||
{ rootMargin: "10% 0px" },
|
||||
);
|
||||
mountIo.observe(el);
|
||||
activeIo.observe(el);
|
||||
return () => {
|
||||
mountIo.disconnect();
|
||||
activeIo.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Drive `progress` autonomously: a slow sine within the settled band keeps the
|
||||
// hologram breathing. Runs only while visible; held static under reduced-motion.
|
||||
useEffect(() => {
|
||||
const cfg = IDLE[variant];
|
||||
if (reduced) {
|
||||
progressRef.current = cfg.still;
|
||||
return;
|
||||
}
|
||||
if (!sceneActive) return;
|
||||
let raf = 0;
|
||||
const tick = () => {
|
||||
const t = performance.now() / 1000;
|
||||
progressRef.current = cfg.center + Math.sin(t * cfg.speed) * cfg.amp;
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [variant, reduced, sceneActive]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapRef}
|
||||
className="wf-scene"
|
||||
role="img"
|
||||
aria-label={ariaLabel ?? "DoorMile 3D logistics scene"}
|
||||
style={{ background: SCENE_BG[variant] }}
|
||||
>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.wf-scene {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
/* Compact, fixed-ratio media panel — replaces the multi-viewport pinned
|
||||
scroll experience so the workflow is dramatically shorter. */
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wf-scene__canvas { position: absolute; inset: 0; }
|
||||
.wf-scene__canvas canvas { display: block; width: 100% !important; height: 100% !important; }
|
||||
@media (max-width: 991px) {
|
||||
.wf-scene { aspect-ratio: 16 / 10; }
|
||||
}
|
||||
/* Phones: shorter scene panel (~17% less tall than the old 4/3) so the
|
||||
card + KPI bar are reached sooner. The closer mobile camera framing
|
||||
keeps the depot/routes/vehicles readable in the reduced height. */
|
||||
@media (max-width: 480px) {
|
||||
.wf-scene { aspect-ratio: 16 / 10; }
|
||||
}
|
||||
`}} />
|
||||
<div className="wf-scene__canvas">
|
||||
{mountScene &&
|
||||
(variant === "optimization" ? (
|
||||
<OptimizationCanvas
|
||||
progress={progressRef}
|
||||
reduced={reduced}
|
||||
isMobile={isMobile}
|
||||
isTablet={isTablet}
|
||||
active={sceneActive}
|
||||
/>
|
||||
) : (
|
||||
<LogisticsBrainCanvas
|
||||
progress={progressRef}
|
||||
reduced={reduced}
|
||||
isMobile={isMobile}
|
||||
active={sceneActive}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,23 +12,6 @@ const StrategyCanvas = dynamic(() => import("./StrategyCanvas"), { ssr: false })
|
||||
/** Center of each stage's scroll window (0…1). */
|
||||
const CENTER = (i: number) => i / (N - 1);
|
||||
|
||||
/** True only while a card's own opacity window is open (tiny buffer). Keeps
|
||||
* not-yet-reached stage cards out of the DOM / off the compositor until their
|
||||
* stage is on screen — no future workflow state is rendered before activation.
|
||||
* Visually identical: a card outside its window is opacity:0 regardless. */
|
||||
function useInWindow(mv: MotionValue<number>, threshold = 0.01): boolean {
|
||||
// `mv` is an external mutable store (a MotionValue). useTransform `.set()`s its
|
||||
// output synchronously while the PARENT renders, so a plain `.on("change") -> setState`
|
||||
// updates this component during the parent's render (React warns). useSyncExternalStore
|
||||
// is built for exactly this: it reads a snapshot and reconciles store-changes-during-
|
||||
// render safely. The snapshot is a primitive boolean, so it never re-renders needlessly.
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => mv.on("change", onStoreChange),
|
||||
() => mv.get() > threshold,
|
||||
() => mv.get() > threshold,
|
||||
);
|
||||
}
|
||||
|
||||
/** Persistent top rail: the 5 stages, current one highlighted. */
|
||||
function StageRail({ active }: { active: number }) {
|
||||
return (
|
||||
@@ -54,11 +37,13 @@ function StageCard({
|
||||
i,
|
||||
scroll,
|
||||
side,
|
||||
active,
|
||||
children,
|
||||
}: {
|
||||
i: number;
|
||||
scroll: MotionValue<number>;
|
||||
side: "left" | "right";
|
||||
active: number;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const c = CENTER(i);
|
||||
@@ -67,8 +52,8 @@ function StageCard({
|
||||
const opacity = useTransform(scroll, [c - 0.1, c - 0.05, c + 0.05, c + 0.1], [0, 1, 1, 0]);
|
||||
const y = useTransform(scroll, [c - 0.1, c - 0.05], [34, 0]);
|
||||
const s = STAGES[i];
|
||||
// Only mount the card while its stage's cross-fade window is open.
|
||||
if (!useInWindow(opacity)) return null;
|
||||
// Only mount the card while its stage is active.
|
||||
if (active !== i) return null;
|
||||
return (
|
||||
<motion.div
|
||||
className={`dm-st-card-story is-${side}`}
|
||||
@@ -202,7 +187,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
detail. Each: short title + one lead line + a couple of key chips. */}
|
||||
|
||||
{/* STAGE 01 — INPUT (green) */}
|
||||
<StageCard i={0} scroll={scroll} side="left">
|
||||
<StageCard i={0} scroll={scroll} side="left" active={active}>
|
||||
<h3 className="dm-st-pillar__title">Orders & riders enter the system</h3>
|
||||
<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">
|
||||
@@ -213,7 +198,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 02 — PARALLEL EXECUTION (purple) */}
|
||||
<StageCard i={1} scroll={scroll} side="right">
|
||||
<StageCard i={1} scroll={scroll} side="right" active={active}>
|
||||
<h3 className="dm-st-pillar__title">Six strategies, evaluated in parallel</h3>
|
||||
<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">
|
||||
@@ -224,7 +209,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 03 — SMART OPTIMIZATION (blue) */}
|
||||
<StageCard i={2} scroll={scroll} side="left">
|
||||
<StageCard i={2} scroll={scroll} side="left" active={active}>
|
||||
<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">
|
||||
@@ -235,7 +220,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 04 — PERFORMANCE GRADING (orange) */}
|
||||
<StageCard i={3} scroll={scroll} side="right">
|
||||
<StageCard i={3} scroll={scroll} side="right" active={active}>
|
||||
<h3 className="dm-st-pillar__title">Every strategy is scored</h3>
|
||||
<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">
|
||||
@@ -246,7 +231,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
</StageCard>
|
||||
|
||||
{/* STAGE 05 — STRATEGY COMPARISON (red, hero) */}
|
||||
<StageCard i={4} scroll={scroll} side="right">
|
||||
<StageCard i={4} scroll={scroll} side="right" active={active}>
|
||||
<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">
|
||||
@@ -433,8 +418,13 @@ const styles = `
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dm-st { height: 420vh; }
|
||||
/* Full-width, bottom-anchored story card. Bound its height to the viewport and
|
||||
let it scroll internally so a tall stage card (Command Center / Winner) can
|
||||
never be clipped off the top of a short phone screen — the active workflow
|
||||
state always stays fully visible. */
|
||||
.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; }
|
||||
bottom: clamp(18px, 4vh, 40px); padding: 15px 16px;
|
||||
max-height: 52vh; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dm-st-arrow { animation: none !important; }
|
||||
|
||||
531
src/data/blog.ts
Normal file
531
src/data/blog.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* Central blog data module — single source of truth for the listing page,
|
||||
* the /blog/[slug] route, the sidebar, related posts and prev/next nav.
|
||||
*
|
||||
* The site is a static export (next.config.ts → output: "export"), so all of
|
||||
* this is plain data resolved at build time. No CMS / no runtime fetching.
|
||||
*/
|
||||
|
||||
export const SITE_URL = "https://www.doormile.com";
|
||||
|
||||
export type ContentBlock =
|
||||
| { type: "paragraph"; text: string }
|
||||
| { type: "heading"; level: 2 | 3; text: string }
|
||||
| { type: "list"; ordered?: boolean; items: string[] }
|
||||
| { type: "quote"; text: string; cite?: string }
|
||||
| { type: "image"; src: string; alt: string; caption?: string };
|
||||
|
||||
export interface BlogPost {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
category: string;
|
||||
image: string;
|
||||
date: string; // ISO (YYYY-MM-DD)
|
||||
author: string;
|
||||
intro: string;
|
||||
content: ContentBlock[];
|
||||
}
|
||||
|
||||
const AUTHOR = "Doormile Team";
|
||||
|
||||
/**
|
||||
* Shared rich-content template for non-flagship posts. Seeded with the post's
|
||||
* own title / category so every element type (h2, h3, lists, quote, image)
|
||||
* renders and every route is complete. Flagship posts below override `content`
|
||||
* with fully authored copy.
|
||||
*/
|
||||
function templateContent(post: {
|
||||
title: string;
|
||||
category: string;
|
||||
image: string;
|
||||
}): ContentBlock[] {
|
||||
return [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: `In last-mile logistics, the difference between a good day and a missed SLA is rarely a single dramatic failure — it is the quiet accumulation of small inefficiencies. ${post.title} looks at how Doormile turns those margins into measurable advantage, and why a precision-first approach consistently outperforms guesswork on the road.`,
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Why this matters for modern fleets",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Every additional kilometre carries cost: fuel or charge, rider hours, vehicle wear, and the risk of a late delivery. When routing decisions are made on intuition or static rules, those costs compound across hundreds of stops. Treating the route as a solvable optimisation problem — not a best guess — is what separates scalable operations from ones that simply add more vehicles.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Fewer vehicles deployed for the same delivery volume",
|
||||
"Lower cost-per-drop through tighter, smarter sequencing",
|
||||
"Predictable ETAs that protect customer trust and SLA targets",
|
||||
"A cleaner, lower-emission footprint per parcel delivered",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "From data to decision",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: `Doormile's MileTruth™ engine ingests orders, constraints and live conditions, then evaluates the routing problem across parallel strategy universes before committing to a plan. The result is a dispatch decision grounded in mathematics rather than heuristics — validated before a single rider leaves the hub.`,
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: post.image,
|
||||
alt: post.title,
|
||||
caption: `${post.category} — operational intelligence applied at the point of dispatch.`,
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "We don't guess the route. We calculate it — and we prove it works before the wheels start turning.",
|
||||
cite: "Doormile Operations",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Putting it into practice",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The teams that benefit most treat routing intelligence as core infrastructure, not an afterthought. Start by measuring your current cost-per-drop and SLA adherence, then let a precision engine reveal where distance, time and capacity are being lost. The gains are rarely theoretical — they show up directly in the next dispatch cycle.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
"Benchmark today's distance, fleet size and on-time rate.",
|
||||
"Feed real constraints — capacity, windows, charge — into the engine.",
|
||||
"Validate routes against real-world conditions before dispatch.",
|
||||
"Measure the delta, then scale the approach across hubs.",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Smarter routing is not about working harder on the road — it is about making the right decision before the journey begins. That is the foundation every Doormile deployment is built on.",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface SeedPost {
|
||||
title: string;
|
||||
excerpt: string;
|
||||
category: string;
|
||||
image: string;
|
||||
date: string;
|
||||
intro: string;
|
||||
content?: ContentBlock[]; // present only for flagship posts
|
||||
}
|
||||
|
||||
const seeds: SeedPost[] = [
|
||||
// ── Flagship 1 ───────────────────────────────────────────────────────────
|
||||
{
|
||||
title: "How AI Is Transforming Last-Mile EV Delivery",
|
||||
excerpt:
|
||||
"Machine learning and real-time data are reshaping how fleets plan, dispatch, and adapt — making every kilometre smarter than the last.",
|
||||
category: "Technology",
|
||||
image: "/images/blog-post-pic-17.png",
|
||||
date: "2025-10-02",
|
||||
intro:
|
||||
"The last mile has always been logistics' most expensive and least predictable stretch. Add electric vehicles to the mix and the problem sharpens: now every route must respect not just time and capacity, but battery range. Artificial intelligence is what turns that constraint into an advantage.",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "For decades, last-mile delivery was planned the way it was a generation ago — dispatchers, spreadsheets, and hard-won intuition. That approach scales poorly, and it breaks entirely when you electrify the fleet. EVs introduce a moving constraint that no static plan can absorb: a vehicle's remaining range changes with load, terrain, traffic and temperature, all at once.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "The shift from rules to learning",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Traditional routing tools rely on fixed rules: nearest-stop-first, fixed zones, manual overrides. They are fast to set up and brittle in practice. Machine-learning-driven systems instead learn from outcomes — every completed delivery, every delay, every charge cycle becomes training signal that sharpens the next decision.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Demand forecasting that anticipates volume spikes before they hit the hub",
|
||||
"Travel-time models trained on the city's real traffic, not generic averages",
|
||||
"Battery-draw prediction tuned to each vehicle class and load profile",
|
||||
"Continuous feedback that improves accuracy with every dispatch",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "Real-time adaptation",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The real unlock is not planning — it is replanning. When a road closes, an order is added, or a vehicle's charge drops faster than expected, an AI-driven system re-optimises in milliseconds and reroutes the affected vehicles without a human in the loop. The plan stays optimal even as reality refuses to hold still.",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: "/images/ev-paradox.png",
|
||||
alt: "Electric delivery vehicle routing visualisation",
|
||||
caption:
|
||||
"AI continuously re-evaluates range, load and traffic to keep every EV route feasible.",
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "An electric fleet is only as good as the intelligence that routes it. The battery sets the limit — the algorithm decides whether you ever reach it.",
|
||||
cite: "Doormile Engineering",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "What it means for operators",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "For fleet operators, the payoff is concrete: fewer vehicles covering the same ground, near-zero range-related failures, and ETAs accurate enough to commit to. AI does not replace the operator — it removes the guesswork, so the operator can run a larger, cleaner, more reliable fleet with the same team.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
"Capture real operational data — deliveries, delays, charge cycles.",
|
||||
"Let models learn your city's actual travel and demand patterns.",
|
||||
"Validate every route against live battery capacity before dispatch.",
|
||||
"Re-optimise continuously as conditions change through the day.",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The fleets pulling ahead are not the ones with the most vehicles — they are the ones with the smartest kilometre. That is the promise AI brings to last-mile EV delivery, and it is already on the road.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Flagship 2 ───────────────────────────────────────────────────────────
|
||||
{
|
||||
title: "42% Less Distance: Insights from Our Hyderabad Hub",
|
||||
excerpt:
|
||||
"A detailed look at how Doormile's MileTruth routing engine delivered measurable efficiency gains — fewer vehicles, less fuel, and zero SLA misses.",
|
||||
category: "Case Study",
|
||||
image: "/images/blog-post-pic-15.png",
|
||||
date: "2025-09-18",
|
||||
intro:
|
||||
"Numbers settle arguments. When we deployed MileTruth™ at our Hyderabad hub, the goal was simple: prove that precision routing changes the economics of last-mile delivery. The result — a 42% reduction in total distance travelled — did exactly that.",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Hyderabad is a demanding test bed: dense urban cores, sprawling new suburbs, unpredictable traffic and tight delivery windows. If a routing approach works here, it works almost anywhere. We ran it side by side against the hub's existing manual-plus-rules dispatch process over a sustained period, holding order volume constant.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "The baseline",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Before MileTruth, the hub planned routes the conventional way — zones drawn by experience, sequences set by dispatchers, adjustments made on the fly. It worked, but it left distance on the table every single day, and that distance translated directly into fuel, hours and vehicles.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Zone-based allocation that ignored cross-zone efficiencies",
|
||||
"Manual sequencing that couldn't evaluate every alternative",
|
||||
"No pre-validation of ETAs against real travel times",
|
||||
"Reactive rather than predictive handling of disruptions",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "What changed",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "MileTruth treated the day's deliveries as one large optimisation problem rather than a set of independent zones. It evaluated routing strategies in parallel, selected the optimal plan against real constraints, and validated every ETA before dispatch. The same orders, the same city — a fundamentally tighter plan.",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: "/images/last-mile-approach.jpg",
|
||||
alt: "Hyderabad delivery hub routing analysis",
|
||||
caption:
|
||||
"Consolidating the day's deliveries into a single optimisation removed redundant cross-town travel.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "The results",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"42% reduction in total distance travelled across the hub",
|
||||
"37% fewer vehicles required for the same delivery volume",
|
||||
"Zero SLA misses across the measured deployment window",
|
||||
"Proportional drop in fuel cost and per-parcel emissions",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "Fewer vehicles, less fuel, zero missed SLAs — and not by working the team harder. By making a better decision before the wheels turned.",
|
||||
cite: "Hyderabad Hub Operations",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "Why it generalises",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The Hyderabad gains were not a quirk of one city. The inefficiencies MileTruth removed — redundant travel, conservative sequencing, unvalidated ETAs — exist in nearly every manual operation. The engine simply makes them visible, then eliminates them. That is why the same approach now anchors deployments well beyond this hub.",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "A 42% cut in distance is not a rounding error — it is a structural change in what the operation costs to run. And it came from intelligence, not additional resources.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Flagship 3 ───────────────────────────────────────────────────────────
|
||||
{
|
||||
title: "MileTruth™ AI — 10 Stages to Smarter Dispatch",
|
||||
excerpt:
|
||||
"From order ingestion to final route output in under 45ms — a technical walkthrough of the ten-stage pipeline at the heart of our routing engine.",
|
||||
category: "MileTruth",
|
||||
image: "/images/blog-post-pic-31.png",
|
||||
date: "2025-09-05",
|
||||
intro:
|
||||
"Behind every Doormile dispatch is a pipeline that turns raw orders into a validated, optimal route in under 45 milliseconds. This is how MileTruth™ does it — ten stages, each one removing a source of error before the next begins.",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Speed and correctness are usually a trade-off. MileTruth is engineered to deliver both: a routing decision fast enough to feel instant, yet rigorous enough to commit a fleet to. The secret is a staged pipeline where each step has a single responsibility and hands clean, validated data to the next.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "The ten stages",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
"Ingestion — orders, constraints and fleet state are normalised on arrival.",
|
||||
"Validation — addresses, time windows and capacities are checked and geocoded.",
|
||||
"Demand modelling — volume and service-time estimates are attached to each stop.",
|
||||
"Travel-time estimation — real-world, time-of-day travel matrices are built.",
|
||||
"Constraint assembly — capacity, range, windows and rules are encoded.",
|
||||
"Strategy generation — multiple routing universes are explored in parallel.",
|
||||
"Optimisation — the solver searches for the minimum-cost feasible plan.",
|
||||
"Battery / range validation — EV routes are checked against real charge capacity.",
|
||||
"ETA pre-validation — promised times are verified before any commitment.",
|
||||
"Output — the final, validated route is emitted to dispatch.",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "Why staging matters",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Collapsing these steps into one monolithic calculation is how most tools accumulate hidden errors. By isolating each concern, MileTruth catches a bad address before it reaches the solver, and an infeasible battery plan before it reaches a rider. Each stage is independently testable, observable and fast.",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: "/images/blog-post-pic-31.png",
|
||||
alt: "MileTruth routing pipeline diagram",
|
||||
caption:
|
||||
"Ten focused stages turn raw orders into a validated route in well under 45 milliseconds.",
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "Each stage exists to delete a category of mistake. By the time a route reaches dispatch, the questionable decisions have already been ruled out.",
|
||||
cite: "MileTruth Engineering",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Parallel strategy universes",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Stage six is where MileTruth diverges from conventional routers. Rather than committing to one heuristic, it generates several distinct routing strategies simultaneously — each a complete candidate plan — and lets the optimiser select the best. Powered by a mathematical solver, it evaluates trade-offs no dispatcher could hold in their head.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Multiple candidate plans evaluated, not a single best guess",
|
||||
"Mathematical optimisation instead of fixed heuristics",
|
||||
"Range and ETA validated inside the loop, not bolted on after",
|
||||
"Sub-45ms output that keeps dispatch genuinely real-time",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Ten stages, one outcome: a route you can trust enough to commit a fleet to — calculated, validated, and delivered before a dispatcher could finish reading the order list.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Template-backed posts ────────────────────────────────────────────────
|
||||
{
|
||||
title: "The EV Paradox: Solving Range Anxiety for Urban Fleets",
|
||||
excerpt:
|
||||
"Electric vehicles promise sustainability, but battery constraints introduce a new routing challenge. Here's how MileTruth™ AI solves it before dispatch.",
|
||||
category: "EV Fleet",
|
||||
image: "/images/ev-paradox.png",
|
||||
date: "2025-08-21",
|
||||
intro:
|
||||
"Electric fleets promise cleaner cities and lower running costs — but they trade one problem for another. Range becomes a hard constraint on every route, and range anxiety becomes an operational risk. Solving it before dispatch is the whole game.",
|
||||
},
|
||||
{
|
||||
title: "Why Mathematical Precision Beats Heuristics in Routing",
|
||||
excerpt:
|
||||
"Most routing tools guess. We calculate. Powered by Google OR-Tools, MileTruth evaluates six parallel strategy universes to select the optimal route every time.",
|
||||
category: "Technology",
|
||||
image: "/images/blog-post-pic-14.jpeg",
|
||||
date: "2025-08-07",
|
||||
intro:
|
||||
"Heuristics are fast to build and easy to trust — until they quietly cost you a vehicle a day. Mathematical optimisation asks more of the engine and gives more back: provably better routes, every dispatch, at scale.",
|
||||
},
|
||||
{
|
||||
title: "Fleet Reduction Without Compromising Delivery Volume",
|
||||
excerpt:
|
||||
"Deploying 37% fewer vehicles while handling the same order volumes isn't a trade-off — it's the result of smarter routing intelligence applied at every dispatch.",
|
||||
category: "Fleet Management",
|
||||
image: "/images/blog-post-pic-8.jpeg",
|
||||
date: "2025-07-24",
|
||||
intro:
|
||||
"Cutting your fleet usually means cutting capacity — unless the kilometres you remove were never necessary in the first place. Smarter routing reclaims that wasted distance and turns it into headroom.",
|
||||
},
|
||||
{
|
||||
title: "Building a Greener City: The Future of Urban Logistics",
|
||||
excerpt:
|
||||
"Cities are demanding cleaner delivery. We explore how AI-powered EV fleets and optimised routing create a path to zero-emission last-mile logistics at city scale.",
|
||||
category: "Sustainability",
|
||||
image: "/images/blog-post-pic-6.jpeg",
|
||||
date: "2025-07-10",
|
||||
intro:
|
||||
"Zero-emission delivery is no longer a marketing line — it is becoming a regulatory expectation. The path there runs through two changes at once: electrifying the fleet, and routing it intelligently enough to make electrification viable.",
|
||||
},
|
||||
{
|
||||
title: "How Doormile Maintains 99.9% SLA Compliance at Scale",
|
||||
excerpt:
|
||||
"Hitting SLA targets 99.9% of the time isn't luck — it's the product of ETA pre-validation, real-time rebalancing, and a routing engine built with delivery reliability as its first constraint.",
|
||||
category: "Operations",
|
||||
image: "/images/last-mile-approach.jpg",
|
||||
date: "2025-06-26",
|
||||
intro:
|
||||
"An SLA you hit 99.9% of the time is not an average you got lucky on — it is a system designed so that missing is the exception, not the risk. Reliability, it turns out, is an engineering decision made long before dispatch.",
|
||||
},
|
||||
{
|
||||
title: "Battery Simulation: The Secret to EV Route Pre-Validation",
|
||||
excerpt:
|
||||
"Before a single rider leaves the hub, MileTruth™ simulates every route against real charge capacity — eliminating mid-route failures and protecting your fulfillment rate.",
|
||||
category: "EV Fleet",
|
||||
image: "/images/blog-post-pic-3.jpeg",
|
||||
date: "2025-06-12",
|
||||
intro:
|
||||
"A stranded EV is not just a late delivery — it is a vehicle out of service, a customer let down, and a recovery cost. Simulating the route against real charge capacity before dispatch is how you make sure it never happens.",
|
||||
},
|
||||
];
|
||||
|
||||
function slugify(title: string): string {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/™/g, "")
|
||||
.replace(/&/g, " and ")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
export const blogPosts: BlogPost[] = seeds.map((s) => ({
|
||||
slug: slugify(s.title),
|
||||
title: s.title,
|
||||
excerpt: s.excerpt,
|
||||
category: s.category,
|
||||
image: s.image,
|
||||
date: s.date,
|
||||
author: AUTHOR,
|
||||
intro: s.intro,
|
||||
content:
|
||||
s.content ??
|
||||
templateContent({ title: s.title, category: s.category, image: s.image }),
|
||||
}));
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function getPostBySlug(slug: string): BlogPost | undefined {
|
||||
return blogPosts.find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
export function getAllSlugs(): string[] {
|
||||
return blogPosts.map((p) => p.slug);
|
||||
}
|
||||
|
||||
/** Same-category posts first (most recent), then fill by recency. Excludes self. */
|
||||
export function getRelatedPosts(slug: string, n = 3): BlogPost[] {
|
||||
const current = getPostBySlug(slug);
|
||||
if (!current) return blogPosts.slice(0, n);
|
||||
const others = blogPosts.filter((p) => p.slug !== slug);
|
||||
const byDateDesc = (a: BlogPost, b: BlogPost) => b.date.localeCompare(a.date);
|
||||
const sameCat = others
|
||||
.filter((p) => p.category === current.category)
|
||||
.sort(byDateDesc);
|
||||
const rest = others
|
||||
.filter((p) => p.category !== current.category)
|
||||
.sort(byDateDesc);
|
||||
return [...sameCat, ...rest].slice(0, n);
|
||||
}
|
||||
|
||||
/** Previous / next by array order (publication sequence). */
|
||||
export function getAdjacentPosts(slug: string): {
|
||||
prev: BlogPost | null;
|
||||
next: BlogPost | null;
|
||||
} {
|
||||
const i = blogPosts.findIndex((p) => p.slug === slug);
|
||||
if (i === -1) return { prev: null, next: null };
|
||||
return {
|
||||
prev: i > 0 ? blogPosts[i - 1] : null,
|
||||
next: i < blogPosts.length - 1 ? blogPosts[i + 1] : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRecentPosts(n = 4): BlogPost[] {
|
||||
return [...blogPosts].sort((a, b) => b.date.localeCompare(a.date)).slice(0, n);
|
||||
}
|
||||
|
||||
export function getCategories(): { name: string; count: number }[] {
|
||||
const counts = new Map<string, number>();
|
||||
for (const p of blogPosts) {
|
||||
counts.set(p.category, (counts.get(p.category) ?? 0) + 1);
|
||||
}
|
||||
return Array.from(counts, ([name, count]) => ({ name, count })).sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDate(iso: string): string {
|
||||
// Parse as UTC to keep static output deterministic across build environments.
|
||||
const d = new Date(`${iso}T00:00:00Z`);
|
||||
return d.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
|
||||
export function estimateReadingTime(post: BlogPost): number {
|
||||
const wordsFromBlocks = post.content.reduce((sum, b) => {
|
||||
if (b.type === "paragraph" || b.type === "quote") {
|
||||
return sum + b.text.trim().split(/\s+/).length;
|
||||
}
|
||||
if (b.type === "heading") return sum + b.text.trim().split(/\s+/).length;
|
||||
if (b.type === "list") {
|
||||
return sum + b.items.join(" ").trim().split(/\s+/).length;
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
const total = wordsFromBlocks + post.intro.trim().split(/\s+/).length;
|
||||
return Math.max(1, Math.round(total / 200));
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
165
src/modules/how-it-works-3d/Experience3D.jsx
Normal file
165
src/modules/how-it-works-3d/Experience3D.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react'
|
||||
import Experience from './components/Experience'
|
||||
import ScrollRig from './components/ScrollRig'
|
||||
import Navbar from './components/ui/Navbar'
|
||||
import FirstMile from './components/sections/FirstMile'
|
||||
import MidMile from './components/sections/MidMile'
|
||||
import LastMile from './components/sections/LastMile'
|
||||
import Analytics from './components/sections/Analytics'
|
||||
import { useSceneStore } from './store/useSceneStore'
|
||||
import './styles/experience.css'
|
||||
|
||||
import Lenis from 'lenis'
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
/**
|
||||
* Experience3D
|
||||
* ---------------------------------------------------------------------------
|
||||
* The full scroll-driven 3D logistics story, ported from the standalone Vite
|
||||
* app's App.jsx and embedded as the body of the How It Works page (below the
|
||||
* existing Elementor hero, above the global Footer).
|
||||
*
|
||||
* Two integration changes vs. the standalone app:
|
||||
* 1. Self-managed fixed pin. The site has a fixed header and an ancestor with
|
||||
* `overflow:hidden`, both of which break CSS `position: sticky`. So this is
|
||||
* a tall `position:relative` section (`.dm-hiw-3d`, its height supplied by
|
||||
* the 900vh ScrollRig spacer) with an absolutely-positioned `.dm-hiw-3d-stage`
|
||||
* toggled absolute(top) → fixed → absolute(bottom) via the ScrollTrigger pin
|
||||
* state — the same approach the site's other 3D sections use (StrategySection).
|
||||
* 2. The global Lenis is disabled on `/how-it-works` (SmoothScroll.tsx) so the
|
||||
* experience runs its own tuned Lenis here without a second instance fighting
|
||||
* it. The internal "Scroll to start" Hero overlay is dropped because the page
|
||||
* keeps the Elementor HowItWorksHero above this section.
|
||||
*/
|
||||
export default function Experience3D() {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const setLenis = useSceneStore((state) => state.setLenis)
|
||||
|
||||
const containerRef = useRef(null)
|
||||
const [pinState, setPinState] = useState('before')
|
||||
// Defer mounting the WebGL Canvas until the section nears the viewport. This
|
||||
// mirrors the site's other 3D sections (StrategySection's `mountScene`): besides
|
||||
// saving the heavy 32MB scene until needed, it keeps the Canvas out of React
|
||||
// StrictMode's initial synchronous double-mount, which otherwise creates and
|
||||
// immediately loses the WebGL context in dev ("THREE.WebGLRenderer: Context Lost"),
|
||||
// leaving a blank canvas. Once mounted it stays mounted.
|
||||
const [mountScene, setMountScene] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) {
|
||||
setMountScene(true)
|
||||
io.disconnect()
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200% 0px' }, // mount well before it scrolls into view
|
||||
)
|
||||
io.observe(el)
|
||||
return () => io.disconnect()
|
||||
}, [])
|
||||
|
||||
// Refresh ScrollTrigger when the scene actually mounts. WebGL canvas mounting
|
||||
// can block the main thread and shift elements, so a refresh here is critical.
|
||||
useEffect(() => {
|
||||
if (mountScene) {
|
||||
const timer = setTimeout(() => {
|
||||
ScrollTrigger.refresh()
|
||||
}, 150)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [mountScene])
|
||||
|
||||
// Own Lenis instance (global Lenis is gated off for this route).
|
||||
useEffect(() => {
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
lerp: 0.08,
|
||||
syncTouch: true,
|
||||
})
|
||||
|
||||
setLenis(lenis)
|
||||
lenis.on('scroll', ScrollTrigger.update)
|
||||
|
||||
// Drive Lenis using GSAP's ticker to ensure synchronization with ScrollTrigger
|
||||
const tickerCb = (time) => lenis.raf(time * 1000)
|
||||
gsap.ticker.add(tickerCb)
|
||||
gsap.ticker.lagSmoothing(0)
|
||||
ScrollTrigger.refresh()
|
||||
|
||||
return () => {
|
||||
gsap.ticker.remove(tickerCb)
|
||||
lenis.destroy()
|
||||
setLenis(null)
|
||||
}
|
||||
}, [setLenis])
|
||||
|
||||
// 3D references shared between R3F and the GSAP scroll system.
|
||||
const truckRef = useRef(null)
|
||||
|
||||
const wheelRefs = React.useMemo(() => [
|
||||
{ current: null }, // FR
|
||||
{ current: null }, // FL
|
||||
{ current: null }, // RL
|
||||
{ current: null }, // RR
|
||||
], [])
|
||||
|
||||
const dashboardRefs = React.useMemo(() => ({
|
||||
bars: [
|
||||
{ current: null }, { current: null }, { current: null },
|
||||
{ current: null }, { current: null }, { current: null }
|
||||
],
|
||||
floorBars: [
|
||||
{ current: null }, { current: null }, { current: null },
|
||||
{ current: null }, { current: null }
|
||||
],
|
||||
pieQuarters: [
|
||||
{ current: null }, { current: null }, { current: null }, { current: null }
|
||||
]
|
||||
}), [])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`dm-hiw-3d is-${pinState}`}>
|
||||
{/* Pinned stage: canvas + HTML overlays. Stays fixed across the scroll. */}
|
||||
<div className="dm-hiw-3d-stage">
|
||||
<div
|
||||
className="canvas-wrapper"
|
||||
style={{
|
||||
opacity: scrollProgress >= 0.92 ? 0.85 : 1.0,
|
||||
transition: 'opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
}}
|
||||
>
|
||||
{mountScene && (
|
||||
<Experience
|
||||
truckRef={truckRef}
|
||||
wheelRefs={wheelRefs}
|
||||
dashboardRefs={dashboardRefs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* In-experience section navigation */}
|
||||
<Navbar />
|
||||
|
||||
{/* Story stage text panels (revealed at their scroll ranges) */}
|
||||
<div className="sections-overlay-container">
|
||||
<FirstMile active={scrollProgress >= 0.02 && scrollProgress < 0.14} />
|
||||
<MidMile active={scrollProgress >= 0.38 && scrollProgress < 0.50} />
|
||||
<LastMile active={scrollProgress >= 0.80 && scrollProgress < 0.92} />
|
||||
<Analytics active={scrollProgress >= 0.94} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GSAP scroll system: 900vh in-flow spacer that gives the section its
|
||||
height, drives scroll progress, and reports pin state. */}
|
||||
<ScrollRig dashboardRefs={dashboardRefs} onPinState={setPinState} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
src/modules/how-it-works-3d/Experience3DLoader.tsx
Normal file
20
src/modules/how-it-works-3d/Experience3DLoader.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
/**
|
||||
* Client-only loader for the 3D How It Works experience.
|
||||
*
|
||||
* `ssr: false` is required: the experience uses React Three Fiber, a Zustand
|
||||
* store, Lenis, and `window`/`AudioContext` — all client-only. The 100vh
|
||||
* placeholder reserves space so the page doesn't jump while the (large) GLB
|
||||
* scene and WebGL bundle load.
|
||||
*/
|
||||
const Experience3D = dynamic(() => import("./Experience3D"), {
|
||||
ssr: false,
|
||||
loading: () => <div style={{ minHeight: "100vh" }} aria-hidden />,
|
||||
});
|
||||
|
||||
export default function Experience3DLoader() {
|
||||
return <Experience3D />;
|
||||
}
|
||||
21
src/modules/how-it-works-3d/animations/cameraTimeline.js
Normal file
21
src/modules/how-it-works-3d/animations/cameraTimeline.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import gsap from 'gsap'
|
||||
|
||||
// Optional GSAP timeline utility to animate custom camera effects (like micro-shake)
|
||||
export const playCameraTransition = (camera, target, duration = 1.0) => {
|
||||
if (!camera) return
|
||||
|
||||
const tl = gsap.timeline()
|
||||
|
||||
// Add a subtle drift to camera position to make it feel organic and premium
|
||||
tl.to(camera.position, {
|
||||
x: '+=0.3',
|
||||
y: '+=0.1',
|
||||
z: '-=0.2',
|
||||
duration: duration,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
ease: 'power1.inOut',
|
||||
})
|
||||
|
||||
return tl
|
||||
}
|
||||
24
src/modules/how-it-works-3d/animations/dashboardAnimation.js
Normal file
24
src/modules/how-it-works-3d/animations/dashboardAnimation.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { clamp } from '../utils/helpers'
|
||||
|
||||
export const animateDashboard = (bars, pieQuarters, progress) => {
|
||||
// progress is 0 at scrollProgress = 0.75, and 1 at scrollProgress = 1.0
|
||||
// Scale bar charts on their Y axis with a staggered effect
|
||||
bars.forEach((barRef, index) => {
|
||||
if (barRef.current) {
|
||||
const delay = index * 0.08
|
||||
const scaleY = clamp((progress - delay) / 0.5, 0, 1)
|
||||
|
||||
// Interpolate scale Y
|
||||
barRef.current.scale.y = scaleY
|
||||
}
|
||||
})
|
||||
|
||||
// Rotate pie chart quarters around their local Y axis
|
||||
pieQuarters.forEach((quarterRef, index) => {
|
||||
if (quarterRef.current) {
|
||||
// Rotate based on progress (offset each slice slightly for dynamic feeling)
|
||||
const rotationSpeed = 2 + index * 0.5
|
||||
quarterRef.current.rotation.y = -0.709 + progress * Math.PI * 2 * rotationSpeed
|
||||
}
|
||||
})
|
||||
}
|
||||
18
src/modules/how-it-works-3d/animations/truckTimeline.js
Normal file
18
src/modules/how-it-works-3d/animations/truckTimeline.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import gsap from 'gsap'
|
||||
|
||||
// Play a subtle engine idle vibration when the truck is active
|
||||
export const playTruckEngineVibration = (truckGroup, isActive = true) => {
|
||||
if (!truckGroup) return null
|
||||
|
||||
if (isActive) {
|
||||
return gsap.to(truckGroup.position, {
|
||||
y: '+=0.015',
|
||||
duration: 0.08,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'sine.inOut',
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
14
src/modules/how-it-works-3d/animations/wheelAnimation.js
Normal file
14
src/modules/how-it-works-3d/animations/wheelAnimation.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const animateWheels = (wheelRefs, rotation) => {
|
||||
if (!wheelRefs || wheelRefs.length === 0) return
|
||||
|
||||
wheelRefs.forEach((wheelRef, index) => {
|
||||
if (wheelRef.current) {
|
||||
// Y-axis is the axle for these wheel meshes.
|
||||
// Odd indices (1, 3) are left side wheels; even indices (0, 2) are right side wheels.
|
||||
// Since left-side wheel groups are rotated 180 degrees in GLTF to face outward,
|
||||
// we invert the spin direction for one side so they all roll forward together.
|
||||
const direction = (index % 2 === 0) ? 1 : -1
|
||||
wheelRef.current.rotation.y = rotation * direction
|
||||
}
|
||||
})
|
||||
}
|
||||
48
src/modules/how-it-works-3d/components/CameraRig.jsx
Normal file
48
src/modules/how-it-works-3d/components/CameraRig.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
import { useCameraAnimation } from '../hooks/useCameraAnimation'
|
||||
import { easing } from 'maath'
|
||||
|
||||
export default function CameraRig() {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const { position: targetPosition, target: lookAtTarget } = useCameraAnimation(scrollProgress)
|
||||
|
||||
// Track the current focus point of the camera in a ref so we can interpolate it smoothly
|
||||
const currentLookAt = useRef(new THREE.Vector3(19.7, 4.4, -31.08))
|
||||
|
||||
useFrame((state, delta) => {
|
||||
const { camera } = state
|
||||
|
||||
// maath's easing.damp3 divides by delta internally; a delta of 0 (coincident
|
||||
// or first frames) yields NaN that poisons the damper and would push the
|
||||
// camera to NaN — blanking the whole scene. Clamp delta to a safe range.
|
||||
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
|
||||
|
||||
// Smoothly damp the camera position towards the target position
|
||||
easing.damp3(camera.position, targetPosition, 0.35, dt)
|
||||
|
||||
// Smoothly damp the camera focus target (lookAt)
|
||||
easing.damp3(currentLookAt.current, lookAtTarget, 0.25, dt)
|
||||
|
||||
// Defensive recovery: if anything upstream produced a non-finite value, snap
|
||||
// back to the target so the camera never gets stuck at NaN (black screen).
|
||||
if (!Number.isFinite(camera.position.x)) camera.position.copy(targetPosition)
|
||||
if (!Number.isFinite(currentLookAt.current.x)) currentLookAt.current.copy(lookAtTarget)
|
||||
|
||||
// Apply lookAt orientation using the interpolated target vector
|
||||
camera.lookAt(currentLookAt.current)
|
||||
|
||||
// Responsive aspect ratio adjustments: increase FOV on portrait screens to zoom out and keep truck & buildings in frame
|
||||
const aspect = state.size.width / state.size.height
|
||||
if (aspect < 1.0) {
|
||||
camera.fov = Math.min(75, 45 / Math.sqrt(aspect))
|
||||
} else {
|
||||
camera.fov = 45
|
||||
}
|
||||
camera.updateProjectionMatrix()
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
143
src/modules/how-it-works-3d/components/Experience.jsx
Normal file
143
src/modules/how-it-works-3d/components/Experience.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { Canvas, useFrame } from '@react-three/fiber'
|
||||
import { Environment, SoftShadows } from '@react-three/drei'
|
||||
import * as THREE from 'three'
|
||||
import { Model as SceneModel } from '../models/Scene3D'
|
||||
import CameraRig from './CameraRig'
|
||||
import TruckAnimation from './TruckAnimation'
|
||||
import StreetLights from './StreetLights'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
|
||||
const dayBgColor = new THREE.Color('#f5f5f7')
|
||||
const nightBgColor = new THREE.Color('#010103') // Pitch black sky with a tiny touch of midnight slate
|
||||
|
||||
const dayAmbientColor = new THREE.Color('#ffffff')
|
||||
const nightAmbientColor = new THREE.Color('#000000') // Pitch black ambient
|
||||
|
||||
const dayDirColor = new THREE.Color('#ffffff')
|
||||
const nightDirColor = new THREE.Color('#000000') // Pitch black sun/moon directional light
|
||||
|
||||
const tempColor = new THREE.Color()
|
||||
|
||||
// Dynamic lighting rig that centers the shadow frustum on the moving truck
|
||||
const SceneLighting = React.memo(function SceneLighting({ truckRef }) {
|
||||
const dirLightRef = useRef()
|
||||
const ambientLightRef = useRef()
|
||||
const targetRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (dirLightRef.current && targetRef.current) {
|
||||
dirLightRef.current.target = targetRef.current
|
||||
}
|
||||
}, [])
|
||||
|
||||
useFrame((state) => {
|
||||
// 1. Center shadows on the truck
|
||||
if (dirLightRef.current && targetRef.current && truckRef.current) {
|
||||
const truckPos = new THREE.Vector3()
|
||||
truckRef.current.getWorldPosition(truckPos)
|
||||
|
||||
targetRef.current.position.copy(truckPos)
|
||||
targetRef.current.updateMatrixWorld()
|
||||
|
||||
dirLightRef.current.position.set(truckPos.x + 10, truckPos.y + 20, truckPos.z + 10)
|
||||
}
|
||||
|
||||
// 2. Day-to-Night transition calculations (disabled: keeping day view throughout the scroll)
|
||||
const nightFactor = 0
|
||||
|
||||
// 3. Mutate scene background color & environment intensity
|
||||
if (state.scene) {
|
||||
state.scene.background = tempColor.lerpColors(dayBgColor, nightBgColor, nightFactor)
|
||||
state.scene.environmentIntensity = 1.0 - nightFactor * 1.0 // Fades completely to 0.0
|
||||
}
|
||||
|
||||
// 4. Update lights properties
|
||||
if (ambientLightRef.current) {
|
||||
ambientLightRef.current.intensity = 0.45 - nightFactor * 0.45 // Fades completely to 0.0
|
||||
ambientLightRef.current.color.lerpColors(dayAmbientColor, nightAmbientColor, nightFactor)
|
||||
}
|
||||
|
||||
if (dirLightRef.current) {
|
||||
dirLightRef.current.intensity = 1.5 - nightFactor * 1.5 // Fades completely to 0.0
|
||||
dirLightRef.current.color.lerpColors(dayDirColor, nightDirColor, nightFactor)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
<ambientLight ref={ambientLightRef} intensity={0.45} />
|
||||
<directionalLight
|
||||
ref={dirLightRef}
|
||||
castShadow
|
||||
position={[10, 20, 10]}
|
||||
intensity={1.5}
|
||||
shadow-mapSize-width={2048}
|
||||
shadow-mapSize-height={2048}
|
||||
shadow-camera-far={100}
|
||||
shadow-camera-left={-35}
|
||||
shadow-camera-right={35}
|
||||
shadow-camera-top={35}
|
||||
shadow-camera-bottom={-35}
|
||||
shadow-bias={-0.0001}
|
||||
/>
|
||||
<object3D ref={targetRef} />
|
||||
</group>
|
||||
)
|
||||
})
|
||||
|
||||
export default React.memo(function Experience({ dashboardRefs, wheelRefs, truckRef }) {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0 }}>
|
||||
<Canvas
|
||||
shadows
|
||||
// Cap the device-pixel-ratio: uncapped, a retina display renders this
|
||||
// heavy 32MB scene into a 2x (or 3x) framebuffer, multiplying GPU memory
|
||||
// and risking WebGL context loss. [1, 1.5] keeps it crisp but bounded —
|
||||
// matching the dpr caps the site's other R3F canvases use.
|
||||
dpr={[1, 1.5]}
|
||||
camera={{ position: [32, 12, -18], fov: 45 }}
|
||||
gl={{ antialias: true, powerPreference: 'high-performance' }}
|
||||
>
|
||||
<color attach="background" args={['#f5f5f7']} />
|
||||
|
||||
{/* Soft shadows */}
|
||||
<SoftShadows size={10} samples={12} focus={1.0} />
|
||||
|
||||
{/* Dynamic ambient and shadow-tracking directional lights */}
|
||||
<SceneLighting truckRef={truckRef} />
|
||||
|
||||
{/* Focused street lights along the road */}
|
||||
<StreetLights />
|
||||
|
||||
{/* Environment preset */}
|
||||
<Environment preset="city" />
|
||||
|
||||
{/* Main 3D logistics scene model */}
|
||||
<SceneModel
|
||||
dashboardRefs={dashboardRefs}
|
||||
truckRef={truckRef}
|
||||
wheelRefs={wheelRefs}
|
||||
/>
|
||||
|
||||
{/* Delivery truck model animation controller */}
|
||||
<TruckAnimation truckRef={truckRef} wheelRefs={wheelRefs} />
|
||||
|
||||
{/* Dynamic camera rig with damping and target interpolation */}
|
||||
<CameraRig />
|
||||
|
||||
{/* Post-processing (EffectComposer/Bloom/Vignette) intentionally omitted.
|
||||
@react-three/postprocessing's EffectComposer reads
|
||||
`renderer.getContextAttributes().alpha` while initializing its buffers;
|
||||
under Next dev's React StrictMode the canvas's WebGL context is torn
|
||||
down and re-created, so that read hits a null context and throws
|
||||
"Cannot read properties of null (reading 'alpha')", crashing the whole
|
||||
scene. Dropping the composer renders the scene directly (lighting +
|
||||
shadows + environment carry the look). To re-add Bloom later, set
|
||||
`reactStrictMode: false` in next.config.ts and restore a Bloom-only
|
||||
composer. */}
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
106
src/modules/how-it-works-3d/components/ScrollRig.jsx
Normal file
106
src/modules/how-it-works-3d/components/ScrollRig.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
import { animateDashboard } from '../animations/dashboardAnimation'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
export default function ScrollRig({ dashboardRefs, onPinState }) {
|
||||
const setScrollProgress = useSceneStore((state) => state.setScrollProgress)
|
||||
const setActiveSection = useSceneStore((state) => state.setActiveSection)
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
const containerRef = useRef(null)
|
||||
const activeSectionRef = useRef(0)
|
||||
const pinStateRef = useRef('before')
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current
|
||||
if (!element) return
|
||||
|
||||
// Create the ScrollTrigger to track the scrolling progress of the 900vh height container
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: 'top top',
|
||||
end: 'bottom bottom',
|
||||
scrub: 2.5, // Even slower, weightier scroll follow for premium feel
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => {
|
||||
const progress = self.progress
|
||||
setScrollProgress(progress)
|
||||
|
||||
// Report pin state so the parent toggles the stage between
|
||||
// absolute(top) → fixed → absolute(bottom). Mirrors StrategySection.
|
||||
const ns = progress <= 0.0002 ? 'before' : progress >= 0.9998 ? 'after' : 'pinned'
|
||||
if (ns !== pinStateRef.current) {
|
||||
pinStateRef.current = ns
|
||||
onPinState?.(ns)
|
||||
}
|
||||
|
||||
// Determine the active stage section
|
||||
// Section 0 (First Mile): 0% to 12%
|
||||
// Section 1 (Mid Mile): 12% to 50%
|
||||
// Section 2 (Last Mile): 50% to 76%
|
||||
// Section 3 (Analytics): 76% to 100%
|
||||
let section = 0
|
||||
if (progress >= 0.92) {
|
||||
section = 3
|
||||
} else if (progress >= 0.50) {
|
||||
section = 2
|
||||
} else if (progress >= 0.12) {
|
||||
section = 1
|
||||
}
|
||||
|
||||
if (section !== activeSectionRef.current) {
|
||||
activeSectionRef.current = section
|
||||
}
|
||||
|
||||
setActiveSection(section)
|
||||
|
||||
// Trigger dashboard animations inside R3F when entering the analytics stage (progress >= 0.92)
|
||||
if (dashboardRefs) {
|
||||
if (progress >= 0.92) {
|
||||
const dashboardProgress = (progress - 0.92) / 0.08
|
||||
animateDashboard(
|
||||
dashboardRefs.bars || [],
|
||||
dashboardRefs.pieQuarters || [],
|
||||
dashboardProgress
|
||||
)
|
||||
} else {
|
||||
// Keep reset when out of analytics section
|
||||
animateDashboard(
|
||||
dashboardRefs.bars || [],
|
||||
dashboardRefs.pieQuarters || [],
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
const refreshTimeout = setTimeout(() => {
|
||||
ScrollTrigger.refresh()
|
||||
}, 150)
|
||||
|
||||
return () => {
|
||||
trigger.kill()
|
||||
clearTimeout(refreshTimeout)
|
||||
}
|
||||
}, [setScrollProgress, setActiveSection, dashboardRefs, lenis, onPinState])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="scroll-trigger-trigger"
|
||||
style={{
|
||||
// In normal flow so it gives the `.dm-hiw-3d` section its 900vh height
|
||||
// (the footer follows cleanly after it). The pinned stage is a separate
|
||||
// absolutely/fixed-positioned sibling.
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '900vh', // Optimized scroll length for faster, smoother travel
|
||||
pointerEvents: 'none', // Allow interacting with the R3F Canvas underneath
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
93
src/modules/how-it-works-3d/components/StreetLights.jsx
Normal file
93
src/modules/how-it-works-3d/components/StreetLights.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
|
||||
// The exact calculated world coordinates of the 10 street light heads in the scene
|
||||
const streetLightsData = [
|
||||
{ pos: [0, 4.2, -4.56], target: [0, 0, -4.56] },
|
||||
{ pos: [9.113, 4.2, 0.944], target: [9.113, 0, 0.944] },
|
||||
{ pos: [-10.158, 4.2, -9.874], target: [-10.158, 0, -9.874] },
|
||||
{ pos: [3.513, 4.2, 9.195], target: [3.513, 0, 9.195] },
|
||||
{ pos: [3.96, 4.2, -21.17], target: [3.96, 0, -21.17] },
|
||||
{ pos: [12.25, 4.2, -16.7], target: [12.25, 0, -16.7] },
|
||||
{ pos: [3.052, 4.2, -12.335], target: [3.052, 0, -12.335] },
|
||||
{ pos: [-2.03, 4.2, -16.89], target: [-2.03, 0, -16.89] },
|
||||
{ pos: [-27.151, 3.98, -9], target: [-27.151, 0, -9] }
|
||||
]
|
||||
|
||||
const bulbOffColor = new THREE.Color('#333333')
|
||||
const bulbOnColor = new THREE.Color('#ffdf6d')
|
||||
const emissiveOffColor = new THREE.Color('#000000')
|
||||
const emissiveOnColor = new THREE.Color('#ffdf6d')
|
||||
|
||||
function SingleStreetLight({ pos, targetPos }) {
|
||||
const lightRef = useRef()
|
||||
const targetRef = useRef()
|
||||
const bulbRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (lightRef.current && targetRef.current) {
|
||||
lightRef.current.target = targetRef.current
|
||||
lightRef.current.target.updateMatrixWorld()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useFrame(() => {
|
||||
// Day-to-Night factor (disabled: streetlights stay off)
|
||||
const nightFactor = 0
|
||||
|
||||
// Smoothly scale spotlights intensity
|
||||
if (lightRef.current) {
|
||||
lightRef.current.intensity = nightFactor * 12.0
|
||||
}
|
||||
|
||||
// Interpolate light bulb material colors to simulate glowing filament
|
||||
if (bulbRef.current) {
|
||||
bulbRef.current.material.color.lerpColors(bulbOffColor, bulbOnColor, nightFactor)
|
||||
bulbRef.current.material.emissive.lerpColors(emissiveOffColor, emissiveOnColor, nightFactor)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Spotlight casting cone of light downward */}
|
||||
<spotLight
|
||||
ref={lightRef}
|
||||
position={pos}
|
||||
intensity={0}
|
||||
distance={12}
|
||||
angle={Math.PI / 4.5}
|
||||
penumbra={0.6}
|
||||
decay={1.2}
|
||||
color="#ffdf6d"
|
||||
castShadow={false} // Disabled for peak frame rate, main shadow is cast by directionalLight
|
||||
/>
|
||||
{/* Glowing bulb mesh placed exactly at the light coordinates */}
|
||||
<mesh ref={bulbRef} position={pos}>
|
||||
<sphereGeometry args={[0.16, 16, 16]} />
|
||||
<meshStandardMaterial
|
||||
color="#333333"
|
||||
emissive="#000000"
|
||||
emissiveIntensity={3.5}
|
||||
roughness={0.1}
|
||||
/>
|
||||
</mesh>
|
||||
<object3D ref={targetRef} position={targetPos} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(function StreetLights() {
|
||||
return (
|
||||
<group>
|
||||
{streetLightsData.map((light, index) => (
|
||||
<SingleStreetLight
|
||||
key={index}
|
||||
pos={light.pos}
|
||||
targetPos={light.target}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
})
|
||||
182
src/modules/how-it-works-3d/components/TruckAnimation.jsx
Normal file
182
src/modules/how-it-works-3d/components/TruckAnimation.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
import { useTruckMovement } from '../hooks/useTruckMovement'
|
||||
import { animateWheels } from '../animations/wheelAnimation'
|
||||
import { easing } from 'maath'
|
||||
import { truckPath } from '../curves/truckPath'
|
||||
|
||||
export default function TruckAnimation({ truckRef, wheelRefs }) {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const activeSection = useSceneStore((state) => state.activeSection)
|
||||
const setTruckProgress = useSceneStore((state) => state.setTruckProgress)
|
||||
|
||||
const { truckProgress } = useTruckMovement(scrollProgress)
|
||||
|
||||
const initialized = useRef(false)
|
||||
|
||||
// Sync truck progress to the global store
|
||||
useEffect(() => {
|
||||
setTruckProgress(truckProgress)
|
||||
}, [truckProgress, setTruckProgress])
|
||||
|
||||
// Float trackers for 1D progress and direction detection
|
||||
const dampedProgressRef = useRef(0)
|
||||
const lastScrollProgressRef = useRef(0)
|
||||
const isReversingRef = useRef(false)
|
||||
|
||||
// Tracker for smooth 180-degree yaw rotation (prevents glitches by pivoting Y rotation angle directly)
|
||||
const extraRotationRef = useRef(0)
|
||||
|
||||
// Track wheel rotation accumulation
|
||||
const accumulatedRotationRef = useRef(0)
|
||||
const lastDampedProgressRef = useRef(0)
|
||||
useFrame((state, delta) => {
|
||||
|
||||
if (!truckRef.current) return
|
||||
|
||||
// r3f can emit delta === 0 (coincident frames, the first frame, or after a
|
||||
// long main-thread block while the 32MB scene parses). maath's easing.damp
|
||||
// divides by delta internally, so a 0 yields NaN/Infinity that poisons the
|
||||
// damper's stored velocity — and from then on truckPath.getPoint(NaN) throws
|
||||
// "Cannot read properties of undefined (reading 'x')". Clamp delta to a safe
|
||||
// positive range before any damping.
|
||||
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
|
||||
|
||||
// Detect scroll direction changes from the actual page scroll progress
|
||||
const deltaScroll = scrollProgress - lastScrollProgressRef.current
|
||||
if (deltaScroll < -0.0001) {
|
||||
isReversingRef.current = true
|
||||
} else if (deltaScroll > 0.0001) {
|
||||
isReversingRef.current = false
|
||||
}
|
||||
lastScrollProgressRef.current = scrollProgress
|
||||
|
||||
// Ensure correct parent-child structure and orientation for the truck (runs reactively on re-renders)
|
||||
const innerGroup = truckRef.current.children[0]
|
||||
if (innerGroup && truckRef.current.children.length > 1) {
|
||||
const siblings = [...truckRef.current.children].slice(1)
|
||||
siblings.forEach((sibling) => {
|
||||
innerGroup.attach(sibling)
|
||||
})
|
||||
|
||||
innerGroup.rotation.set(0, -Math.PI / 2, 0)
|
||||
|
||||
// Disable frustum culling on all child meshes so the truck/shadow is always visible
|
||||
truckRef.current.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
child.frustumCulled = false
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Run one-time state initialization for progress trackers
|
||||
if (!initialized.current) {
|
||||
dampedProgressRef.current = truckProgress
|
||||
dampedProgressRef.current_velocity = 0
|
||||
lastDampedProgressRef.current = truckProgress
|
||||
lastScrollProgressRef.current = scrollProgress
|
||||
isReversingRef.current = false
|
||||
extraRotationRef.current = 0
|
||||
extraRotationRef.current_velocity = 0
|
||||
|
||||
const position = truckPath.getPoint(dampedProgressRef.current)
|
||||
let lookAtTargetVector
|
||||
if (dampedProgressRef.current >= 0.99) {
|
||||
const tangent = truckPath.getTangent(1.0)
|
||||
const endPoint = truckPath.getPoint(1.0)
|
||||
lookAtTargetVector = new THREE.Vector3().copy(endPoint).addScaledVector(tangent, 1.0)
|
||||
} else {
|
||||
const ahead = Math.min(dampedProgressRef.current + 0.01, 1.0)
|
||||
lookAtTargetVector = truckPath.getPoint(ahead)
|
||||
}
|
||||
|
||||
truckRef.current.position.copy(position)
|
||||
if (truckRef.current.position.distanceToSquared(lookAtTargetVector) > 0.0001) {
|
||||
truckRef.current.lookAt(lookAtTargetVector)
|
||||
}
|
||||
|
||||
initialized.current = true
|
||||
}
|
||||
|
||||
// Smoothly damp the 1D progress scalar along the curve path
|
||||
easing.damp(dampedProgressRef, 'current', truckProgress, 0.30, dt)
|
||||
|
||||
// Defensive: keep the spline parameter a finite value in [0,1]. getPoint(NaN)
|
||||
// or an out-of-range t reads an undefined curve point and throws.
|
||||
if (!Number.isFinite(dampedProgressRef.current)) {
|
||||
dampedProgressRef.current = truckProgress
|
||||
if (dampedProgressRef.__damp) dampedProgressRef.__damp = {} // clear any poisoned velocity
|
||||
}
|
||||
dampedProgressRef.current = THREE.MathUtils.clamp(dampedProgressRef.current, 0, 1)
|
||||
|
||||
// Evaluate the 3D position and orientation directly on the spline curve
|
||||
const position = truckPath.getPoint(dampedProgressRef.current)
|
||||
|
||||
let lookAtTargetVector
|
||||
if (dampedProgressRef.current >= 0.99) {
|
||||
const tangent = truckPath.getTangent(1.0)
|
||||
const endPoint = truckPath.getPoint(1.0)
|
||||
lookAtTargetVector = new THREE.Vector3().copy(endPoint).addScaledVector(tangent, 1.0)
|
||||
} else {
|
||||
const ahead = Math.min(dampedProgressRef.current + 0.01, 1.0)
|
||||
lookAtTargetVector = truckPath.getPoint(ahead)
|
||||
}
|
||||
|
||||
// Update position and base forward rotation directly (ensures 100% spline compliance, zero corner cutting)
|
||||
truckRef.current.position.copy(position)
|
||||
if (truckRef.current.position.distanceToSquared(lookAtTargetVector) > 0.0001) {
|
||||
truckRef.current.lookAt(lookAtTargetVector)
|
||||
}
|
||||
|
||||
// Determine target extra rotation:
|
||||
// - 0 radians when moving forward
|
||||
// - Math.PI radians (180 degrees) when reversing
|
||||
// We disable U-turns at the extreme start and end of the path to keep the truck stable at warehouse/delivery spots
|
||||
let targetExtraRotation = 0
|
||||
if (dampedProgressRef.current > 0.05 && dampedProgressRef.current < 0.95) {
|
||||
if (isReversingRef.current) {
|
||||
targetExtraRotation = Math.PI
|
||||
}
|
||||
}
|
||||
|
||||
// Smoothly damp the extra rotation angle directly (prevents pitch/roll glitches or 3D target collapse)
|
||||
easing.damp(extraRotationRef, 'current', targetExtraRotation, 0.20, dt)
|
||||
|
||||
// Defensive: reset extra rotation if it becomes NaN
|
||||
if (!Number.isFinite(extraRotationRef.current)) {
|
||||
extraRotationRef.current = targetExtraRotation
|
||||
extraRotationRef.current_velocity = 0
|
||||
if (extraRotationRef.__damp) extraRotationRef.__damp = {}
|
||||
}
|
||||
|
||||
// Apply the yaw pivot around the local vertical axis
|
||||
truckRef.current.rotateY(extraRotationRef.current)
|
||||
|
||||
// Calculate progress delta for wheels and audio
|
||||
const deltaDamped = Math.abs(dampedProgressRef.current - lastDampedProgressRef.current)
|
||||
lastDampedProgressRef.current = dampedProgressRef.current
|
||||
|
||||
// Accumulate wheel rotation based on absolute movement delta so they always roll forward locally
|
||||
const isMoving = dampedProgressRef.current > 0.001 && dampedProgressRef.current < 0.999
|
||||
if (isMoving) {
|
||||
accumulatedRotationRef.current += deltaDamped * 250 // spinFactor
|
||||
}
|
||||
|
||||
// Spin wheels
|
||||
animateWheels(wheelRefs, accumulatedRotationRef.current)
|
||||
|
||||
|
||||
|
||||
// Add engine vibration to the inner group to prevent coordinate pollution on the root group
|
||||
if (truckRef.current.children && truckRef.current.children[0]) {
|
||||
const innerGroup = truckRef.current.children[0]
|
||||
innerGroup.position.y = Math.sin(state.clock.getElapsedTime() * 45) * 0.003
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
|
||||
export default function Analytics({ active }) {
|
||||
return (
|
||||
<RevealCard active={active} id="analytics-section">
|
||||
<div className="section-badge">Workflow</div>
|
||||
<h2 className="section-title">Doormile Insights</h2>
|
||||
<h3 className="section-subtitle">3-Mile Logistics Ecosystem</h3>
|
||||
|
||||
<div className="workflow-steps">
|
||||
<div className="workflow-step">
|
||||
<div className="step-number-container">
|
||||
<span className="step-number">01</span>
|
||||
<div className="step-line"></div>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h4 className="step-title">First Mile</h4>
|
||||
<p className="step-description">Incoming shipments are securely loaded, checked, and consolidated at initial fulfillment hubs.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workflow-step">
|
||||
<div className="step-number-container">
|
||||
<span className="step-number">02</span>
|
||||
<div className="step-line"></div>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h4 className="step-title">Mid Mile</h4>
|
||||
<p className="step-description">Consolidated goods travel between primary distribution nodes via optimized express transit corridors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workflow-step">
|
||||
<div className="step-number-container">
|
||||
<span className="step-number">03</span>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h4 className="step-title">Last Mile</h4>
|
||||
<p className="step-description">Local delivery units organize doorstep routes to transport packages to final customers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RevealCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import { sections } from '../../constants/sectionConfig'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
|
||||
export default function FirstMile({ active }) {
|
||||
const config = sections[0]
|
||||
return (
|
||||
<RevealCard active={active} id="first-mile-section">
|
||||
<div className="section-badge">Stage 01</div>
|
||||
<h2 className="section-title">{config.title}</h2>
|
||||
<h3 className="section-subtitle">{config.subtitle}</h3>
|
||||
<p className="section-description">{config.description}</p>
|
||||
<div className="section-metrics">
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">14,250</span>
|
||||
<span className="metric-label">Parcels Processed</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">99.98%</span>
|
||||
<span className="metric-label">Sorting Accuracy</span>
|
||||
</div>
|
||||
</div>
|
||||
</RevealCard>
|
||||
)
|
||||
}
|
||||
52
src/modules/how-it-works-3d/components/sections/LastMile.jsx
Normal file
52
src/modules/how-it-works-3d/components/sections/LastMile.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { sections } from '../../constants/sectionConfig'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
import { progressToScrollY } from '../../utils/helpers'
|
||||
|
||||
export default function LastMile({ active }) {
|
||||
const config = sections[2]
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
|
||||
const handleClose = () => {
|
||||
// Smoothly scroll to 97% progress, which is inside the Analytics Dashboard section.
|
||||
// Relative to the experience spacer (the section sits below the page hero).
|
||||
lenis?.scrollTo(progressToScrollY(0.97), { duration: 1.5 })
|
||||
}
|
||||
|
||||
return (
|
||||
<RevealCard active={active} id="last-mile-section">
|
||||
<div className="section-badge">Stage 03</div>
|
||||
<h2 className="section-title">{config.title}</h2>
|
||||
<h3 className="section-subtitle">{config.subtitle}</h3>
|
||||
<p className="section-description">{config.description}</p>
|
||||
<div className="section-metrics">
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">12.5 min</span>
|
||||
<span className="metric-label">Avg. Delivery window</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">99.4%</span>
|
||||
<span className="metric-label">On-Time Rate</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="section-close-btn" onClick={handleClose}>
|
||||
View Analytics
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ marginLeft: '6px' }}
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</RevealCard>
|
||||
)
|
||||
}
|
||||
52
src/modules/how-it-works-3d/components/sections/MidMile.jsx
Normal file
52
src/modules/how-it-works-3d/components/sections/MidMile.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { sections } from '../../constants/sectionConfig'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
import { progressToScrollY } from '../../utils/helpers'
|
||||
|
||||
export default function MidMile({ active }) {
|
||||
const config = sections[1]
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
|
||||
const handleClose = () => {
|
||||
// Smoothly scroll to 57.5% progress, which is just after the truck resumes moving (at 57%).
|
||||
// Relative to the experience spacer (the section sits below the page hero).
|
||||
lenis?.scrollTo(progressToScrollY(0.575), { duration: 1.5 })
|
||||
}
|
||||
|
||||
return (
|
||||
<RevealCard active={active} id="mid-mile-section">
|
||||
<div className="section-badge">Stage 02</div>
|
||||
<h2 className="section-title">{config.title}</h2>
|
||||
<h3 className="section-subtitle">{config.subtitle}</h3>
|
||||
<p className="section-description">{config.description}</p>
|
||||
<div className="section-metrics">
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">4.2 hr</span>
|
||||
<span className="metric-label">Avg. Transit Time</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">220 kw</span>
|
||||
<span className="metric-label">Solar Output (Self-powered)</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="section-close-btn" onClick={handleClose}>
|
||||
Continue Journey
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ marginLeft: '6px' }}
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</RevealCard>
|
||||
)
|
||||
}
|
||||
22
src/modules/how-it-works-3d/components/ui/Hero.jsx
Normal file
22
src/modules/how-it-works-3d/components/ui/Hero.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
|
||||
export default function Hero() {
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
const handleScrollToStart = () => {
|
||||
// Scroll down to the first active transition point
|
||||
lenis?.scrollTo(window.innerHeight * 0.5, { duration: 1.5 })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hero-overlay" id="home-hero">
|
||||
{/* Dynamic mouse scrolling indicator */}
|
||||
<div className="scroll-indicator" onClick={handleScrollToStart}>
|
||||
<div className="mouse-frame">
|
||||
<div className="mouse-dot" />
|
||||
</div>
|
||||
<div className="scroll-text">Scroll to start</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
src/modules/how-it-works-3d/components/ui/Navbar.jsx
Normal file
38
src/modules/how-it-works-3d/components/ui/Navbar.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
import { progressToScrollY } from '../../utils/helpers'
|
||||
|
||||
export default function Navbar() {
|
||||
const activeSection = useSceneStore((state) => state.activeSection)
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
|
||||
const handleNavClick = (index) => {
|
||||
// Map index (0, 1, 2, 3) to the stable parking progress percentages (0.0, 0.38, 0.76, 0.97).
|
||||
const sectionFractions = [0, 0.38, 0.76, 0.97]
|
||||
const targetProgress = sectionFractions[index]
|
||||
// Relative to the experience spacer (the section sits below the page hero).
|
||||
lenis?.scrollTo(progressToScrollY(targetProgress), { duration: 1.5 })
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ label: 'First Mile', index: 0 },
|
||||
{ label: 'Mid Mile', index: 1 },
|
||||
{ label: 'Last Mile', index: 2 },
|
||||
{ label: 'Analytics', index: 3 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="side-navigation" id="main-navbar">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.index}
|
||||
onClick={() => handleNavClick(item.index)}
|
||||
className={`side-nav-item ${activeSection === item.index ? 'active' : ''}`}
|
||||
>
|
||||
<span className="side-nav-label">{item.label}</span>
|
||||
<span className="side-nav-dot" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
src/modules/how-it-works-3d/components/ui/RevealCard.jsx
Normal file
89
src/modules/how-it-works-3d/components/ui/RevealCard.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import gsap from 'gsap'
|
||||
|
||||
export default function RevealCard({ children, active, id, className = "" }) {
|
||||
const cardRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const card = cardRef.current
|
||||
if (!card) return
|
||||
|
||||
// Find all target children inside the card to create a staggered entrance
|
||||
const animTargets = card.querySelectorAll(
|
||||
'.section-badge, .section-title, .section-subtitle, .section-description, .section-metrics, .section-close-btn, .workflow-step'
|
||||
)
|
||||
|
||||
const isAnalytics = id === 'analytics-section'
|
||||
|
||||
if (active) {
|
||||
// Clean up any ongoing animations first
|
||||
gsap.killTweensOf([card, animTargets])
|
||||
|
||||
// Animate card container in
|
||||
gsap.to(card, {
|
||||
xPercent: isAnalytics ? -50 : 0,
|
||||
yPercent: isAnalytics ? -50 : 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.85,
|
||||
ease: 'power4.out',
|
||||
})
|
||||
|
||||
// Stagger child elements reveal
|
||||
gsap.fromTo(
|
||||
animTargets,
|
||||
{
|
||||
y: 15,
|
||||
opacity: 0
|
||||
},
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 0.6,
|
||||
stagger: 0.08,
|
||||
ease: 'power3.out',
|
||||
delay: 0.1, // brief delay to let card body expand first
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Kill active tweens
|
||||
gsap.killTweensOf([card, animTargets])
|
||||
|
||||
// Animate card container out
|
||||
gsap.to(card, {
|
||||
xPercent: isAnalytics ? -50 : 0,
|
||||
yPercent: isAnalytics ? -50 : 0,
|
||||
y: isAnalytics ? 18 : 20,
|
||||
scale: 0.96,
|
||||
opacity: 0,
|
||||
duration: 0.5,
|
||||
ease: 'power3.inOut',
|
||||
})
|
||||
|
||||
// Smoothly hide child elements
|
||||
gsap.to(animTargets, {
|
||||
y: 10,
|
||||
opacity: 0,
|
||||
duration: 0.35,
|
||||
ease: 'power2.in',
|
||||
})
|
||||
}
|
||||
}, [active, id])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
id={id}
|
||||
className={`section-panel ${active ? 'active' : ''} ${className}`}
|
||||
style={{
|
||||
opacity: 0,
|
||||
transform: id === 'analytics-section'
|
||||
? 'translate(-50%, -50%) translateY(18px) scale(0.96)'
|
||||
: 'translateY(20px) scale(0.96)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
src/modules/how-it-works-3d/constants/cameraPositions.js
Normal file
35
src/modules/how-it-works-3d/constants/cameraPositions.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
// Premium Apple-inspired cinematic keyframes looking directly at the front of each building
|
||||
export const cameraPositions = [
|
||||
{
|
||||
// Stage 01: First Mile Warehouse (Front-on view of loading bays, lowered target to center truck)
|
||||
progress: 0.0,
|
||||
position: new THREE.Vector3(19.727, 7.5, -14.0),
|
||||
target: new THREE.Vector3(19.727, 2.0, -31.02),
|
||||
},
|
||||
{
|
||||
// Transition 01: Highway Cruise (Looking down at the highway joining road)
|
||||
progress: 0.25,
|
||||
position: new THREE.Vector3(0.0, 12.0, -12.0),
|
||||
target: new THREE.Vector3(6.447, 2.0, -19.06),
|
||||
},
|
||||
{
|
||||
// Stage 02: Mid Mile Hub (Front-on view of loading bays, lowered target to center truck)
|
||||
progress: 0.5,
|
||||
position: new THREE.Vector3(-19.146, 6.5, 10.0),
|
||||
target: new THREE.Vector3(-19.146, 1.5, -6.00),
|
||||
},
|
||||
{
|
||||
// Stage 03: Last Mile Delivery Center (Front-on view of local hub, lowered target to center truck)
|
||||
progress: 0.75,
|
||||
position: new THREE.Vector3(19.263, 5.5, 27.0),
|
||||
target: new THREE.Vector3(19.263, 1.2, 4.0),
|
||||
},
|
||||
{
|
||||
// Stage 04: Centralized Dashboard (Front-on view of the analytics monitor screen)
|
||||
progress: 1.0,
|
||||
position: new THREE.Vector3(-13.5, 5.0, 31.0),
|
||||
target: new THREE.Vector3(-7.7, 3.8, 25.4),
|
||||
},
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user