feat: add CTAButton component with customizable variants, sizes, and arrows
Some checks are pending
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

This commit is contained in:
Najjar\NajjarV02 2026-05-21 12:57:56 +04:00
parent 79f7f9e579
commit 7458818c62
31 changed files with 2852 additions and 1023 deletions

View File

@ -6,6 +6,7 @@ const nextConfig = {
},
trailingSlash: true,
reactStrictMode: true,
allowedDevOrigins: ['127.0.0.1', 'localhost', '10.255.254.66'],
};
export default nextConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

View File

@ -1,39 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu BellaBot delivery robot silhouette">
<defs>
<radialGradient id="bg" cx="50%" cy="45%" r="60%">
<stop offset="0%" stop-color="#3a55c4" stop-opacity="0.35"/>
<stop offset="60%" stop-color="#0a0a0c" stop-opacity="0"/>
</radialGradient>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FBFBFD"/>
<stop offset="100%" stop-color="#8891C7"/>
</linearGradient>
<linearGradient id="screen" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1c1b21"/>
<stop offset="100%" stop-color="#0a0a0c"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#DEE0F0"/>
<stop offset="100%" stop-color="#273F94"/>
</linearGradient>
</defs>
<rect width="600" height="600" fill="url(#bg)"/>
<g transform="translate(300 480)">
<ellipse cx="0" cy="0" rx="160" ry="14" fill="#000" opacity="0.55"/>
</g>
<g transform="translate(300 100)">
<rect x="-120" y="0" width="240" height="120" rx="36" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-100" y="20" width="200" height="80" rx="22" fill="url(#screen)"/>
<circle cx="-44" cy="60" r="14" fill="url(#accent)"/>
<circle cx="44" cy="60" r="14" fill="url(#accent)"/>
<path d="M -30 88 Q 0 78 30 88" stroke="url(#accent)" stroke-width="4" fill="none" stroke-linecap="round"/>
<rect x="-140" y="140" width="280" height="14" rx="6" fill="#5a5e7a"/>
<rect x="-128" y="170" width="256" height="50" rx="12" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-128" y="240" width="256" height="50" rx="12" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-128" y="310" width="256" height="50" rx="12" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-100" y="370" width="200" height="14" rx="6" fill="#3a3d52"/>
<circle cx="-72" cy="384" r="20" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
<circle cx="72" cy="384" r="20" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
</g>
<text x="50%" y="96%" text-anchor="middle" fill="#8891C7" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU BELLABOT</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -1,31 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu CC1 cleaning robot silhouette">
<defs>
<radialGradient id="bg" cx="50%" cy="55%" r="60%">
<stop offset="0%" stop-color="#3a55c4" stop-opacity="0.32"/>
<stop offset="60%" stop-color="#0a0a0c" stop-opacity="0"/>
</radialGradient>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FBFBFD"/>
<stop offset="100%" stop-color="#8891C7"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#DEE0F0"/>
<stop offset="100%" stop-color="#273F94"/>
</linearGradient>
</defs>
<rect width="600" height="600" fill="url(#bg)"/>
<g transform="translate(300 360)">
<ellipse cx="0" cy="120" rx="200" ry="20" fill="#000" opacity="0.55"/>
<rect x="-180" y="-180" width="360" height="240" rx="40" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-160" y="-160" width="160" height="100" rx="18" fill="#0a0a0c"/>
<rect x="-150" y="-150" width="140" height="14" rx="6" fill="url(#accent)" opacity="0.9"/>
<rect x="-150" y="-130" width="100" height="10" rx="4" fill="#5a5e7a"/>
<rect x="-150" y="-110" width="120" height="10" rx="4" fill="#5a5e7a"/>
<circle cx="120" cy="-110" r="34" fill="#1c1b21" stroke="#5a5e7a" stroke-width="2"/>
<circle cx="120" cy="-110" r="14" fill="url(#accent)"/>
<rect x="-170" y="80" width="340" height="30" rx="8" fill="#0a0a0c"/>
<circle cx="-130" cy="110" r="22" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
<circle cx="130" cy="110" r="22" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
</g>
<text x="50%" y="96%" text-anchor="middle" fill="#8891C7" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU CC1 CLEANING ROBOT</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,35 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu KettyBot service robot silhouette">
<defs>
<radialGradient id="bg" cx="50%" cy="45%" r="60%">
<stop offset="0%" stop-color="#3a55c4" stop-opacity="0.32"/>
<stop offset="60%" stop-color="#0a0a0c" stop-opacity="0"/>
</radialGradient>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FBFBFD"/>
<stop offset="100%" stop-color="#8891C7"/>
</linearGradient>
<linearGradient id="screen" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0a0a0c"/>
<stop offset="100%" stop-color="#3a3d52"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#DEE0F0"/>
<stop offset="100%" stop-color="#3a55c4"/>
</linearGradient>
</defs>
<rect width="600" height="600" fill="url(#bg)"/>
<g transform="translate(300 110)">
<rect x="-110" y="0" width="220" height="280" rx="28" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-92" y="22" width="184" height="240" rx="18" fill="url(#screen)"/>
<g transform="translate(0 110)">
<circle cx="-30" cy="0" r="12" fill="url(#accent)"/>
<circle cx="30" cy="0" r="12" fill="url(#accent)"/>
<path d="M -22 30 Q 0 18 22 30" stroke="url(#accent)" stroke-width="3" fill="none" stroke-linecap="round"/>
</g>
<rect x="-130" y="300" width="260" height="14" rx="6" fill="#5a5e7a"/>
<path d="M -120 320 L 120 320 L 100 460 L -100 460 Z" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<circle cx="-70" cy="460" r="22" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
<circle cx="70" cy="460" r="22" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
</g>
<text x="50%" y="96%" text-anchor="middle" fill="#8891C7" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU KETTYBOT</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,29 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu D-Series industrial delivery robot silhouette">
<defs>
<radialGradient id="bg" cx="50%" cy="55%" r="60%">
<stop offset="0%" stop-color="#3a55c4" stop-opacity="0.32"/>
<stop offset="60%" stop-color="#0a0a0c" stop-opacity="0"/>
</radialGradient>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FBFBFD"/>
<stop offset="100%" stop-color="#6a73a5"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#DEE0F0"/>
<stop offset="100%" stop-color="#3a55c4"/>
</linearGradient>
</defs>
<rect width="600" height="600" fill="url(#bg)"/>
<g transform="translate(300 380)">
<ellipse cx="0" cy="120" rx="200" ry="18" fill="#000" opacity="0.55"/>
<rect x="-180" y="-220" width="360" height="320" rx="36" fill="url(#body)" stroke="#3a3d52" stroke-width="2"/>
<rect x="-160" y="-200" width="320" height="40" rx="10" fill="#0a0a0c"/>
<rect x="-160" y="-140" width="320" height="80" rx="12" fill="#1c1b21"/>
<rect x="-160" y="-40" width="320" height="80" rx="12" fill="#1c1b21"/>
<rect x="-160" y="60" width="320" height="30" rx="10" fill="url(#accent)" opacity="0.85"/>
<rect x="-180" y="100" width="360" height="20" rx="8" fill="#0a0a0c"/>
<circle cx="-130" cy="120" r="26" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
<circle cx="130" cy="120" r="26" fill="#0a0a0c" stroke="#5a5e7a" stroke-width="2"/>
</g>
<text x="50%" y="96%" text-anchor="middle" fill="#8891C7" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU D-SERIES</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -266,53 +266,264 @@ html {
::-webkit-scrollbar-thumb { background: rgba(136, 145, 199, 0.28); border-radius: var(--radius-sm); }
::-webkit-scrollbar-thumb:hover { background: rgba(39, 63, 148, 0.7); }
/* === Buttons === */
.btn {
/* === CTA Button System === */
/* Reusable across all sections primary | secondary | ghost | link, sizes sm | md | lg.
Arrow handled via .cta-arrow capsule; data-arrow="up-right" rotates on hover, "right" translates X. */
.cta-btn {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.625rem;
padding: 0.875rem 1.75rem;
gap: 0.65rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.08em;
font-family: inherit;
font-weight: 800;
letter-spacing: 0.22em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.16,1,0.3,1), box-shadow 0.25s ease, background 0.25s ease, color 0.25s ease;
border: 1px solid transparent;
white-space: nowrap;
isolation: isolate;
overflow: hidden;
border: 1px solid transparent;
transition: transform 0.3s cubic-bezier(0.16,1,0.3,1), box-shadow 0.3s, background 0.3s, border-color 0.3s, color 0.3s;
-webkit-user-select: none;
user-select: none;
}
.btn:hover { transform: translateY(-1px); }
.btn-primary {
background: linear-gradient(135deg, #3a55c4 0%, #273F94 55%, #1a2e6e 100%);
color: #FBFBFD;
box-shadow: 0 8px 28px rgba(39, 63, 148, 0.45), inset 0 1px 0 rgba(222, 224, 240, 0.25);
.cta-btn:focus-visible {
outline: 2px solid #DEE0F0;
outline-offset: 3px;
}
.btn-primary:hover {
box-shadow: 0 12px 38px rgba(58, 85, 196, 0.6), inset 0 1px 0 rgba(222, 224, 240, 0.35);
.cta-btn:disabled,
.cta-btn[aria-disabled='true'] {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.btn-ghost {
background: rgba(222, 224, 240, 0.04);
color: #FBFBFD;
border-color: rgba(199, 207, 230, 0.28);
backdrop-filter: blur(12px);
/* sizes */
.cta-sm { padding: 0.62rem 1.05rem; font-size: 0.64rem; letter-spacing: 0.22em; }
.cta-md { padding: 0.95rem 1.45rem; font-size: 0.74rem; }
.cta-lg { padding: 1.05rem 1.7rem; font-size: 0.8rem; letter-spacing: 0.24em; }
/* shared arrow capsule */
.cta-arrow {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(222, 224, 240, 0.12);
border: 1px solid rgba(222, 224, 240, 0.22);
color: currentColor;
transition: transform 0.35s, background 0.35s, border-color 0.35s;
flex-shrink: 0;
}
.btn-ghost:hover {
background: rgba(39, 63, 148, 0.16);
border-color: rgba(136, 145, 199, 0.6);
color: #DEE0F0;
.cta-sm .cta-arrow { width: 20px; height: 20px; }
.cta-md .cta-arrow { width: 24px; height: 24px; }
.cta-lg .cta-arrow { width: 26px; height: 26px; }
/* PRIMARY — filled gradient, strongest emphasis */
.cta-primary {
background: linear-gradient(135deg, #5a76e8 0%, #4a66d8 35%, #2f4cb4 70%, #273F94 100%);
color: #FFFFFF;
box-shadow:
0 24px 56px rgba(39, 63, 148, 0.55),
0 0 0 1px rgba(222, 224, 240, 0.22),
0 0 32px rgba(74, 102, 216, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.24);
}
.cta-primary::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, transparent 30%, rgba(255, 255, 255, 0.2) 50%, transparent 70%);
transform: translateX(-100%);
transition: transform 0.7s ease;
pointer-events: none;
}
.cta-primary:hover {
transform: translateY(-2px);
box-shadow:
0 30px 66px rgba(39, 63, 148, 0.7),
0 0 0 1px rgba(222, 224, 240, 0.32),
0 0 44px rgba(74, 102, 216, 0.55),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.cta-primary:hover::before { transform: translateX(100%); }
.cta-primary .cta-arrow {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.24);
}
.cta-primary:hover .cta-arrow {
background: rgba(255, 255, 255, 0.32);
border-color: rgba(255, 255, 255, 0.4);
}
.btn-outline {
/* SECONDARY — glassmorphism, supporting actions */
.cta-secondary {
border-color: rgba(222, 224, 240, 0.55);
background:
linear-gradient(135deg, rgba(58, 85, 196, 0.16), rgba(14, 13, 18, 0.55) 60%),
rgba(14, 13, 18, 0.55);
color: #FFFFFF;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
box-shadow:
0 14px 32px rgba(0, 0, 0, 0.55),
inset 0 1px 0 rgba(222, 224, 240, 0.22),
0 0 0 1px rgba(58, 85, 196, 0.18);
}
.cta-secondary::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, transparent 30%, rgba(222, 224, 240, 0.16) 50%, transparent 70%);
transform: translateX(-100%);
transition: transform 0.7s ease;
pointer-events: none;
}
.cta-secondary:hover {
border-color: rgba(222, 224, 240, 0.82);
background:
linear-gradient(135deg, rgba(74, 102, 216, 0.3), rgba(20, 22, 38, 0.62) 60%),
rgba(20, 22, 38, 0.62);
transform: translateY(-2px);
box-shadow:
0 22px 46px rgba(0, 0, 0, 0.62),
inset 0 1px 0 rgba(222, 224, 240, 0.3),
0 0 0 1px rgba(58, 85, 196, 0.5),
0 0 32px rgba(58, 85, 196, 0.4);
}
.cta-secondary:hover::before { transform: translateX(100%); }
/* GHOST — outline, low-emphasis action */
.cta-ghost {
border-color: rgba(222, 224, 240, 0.28);
background: transparent;
color: #ECEEF6;
}
.cta-ghost:hover {
border-color: rgba(222, 224, 240, 0.6);
background: rgba(58, 85, 196, 0.14);
color: #FFFFFF;
transform: translateY(-2px);
}
.cta-ghost .cta-arrow {
background: rgba(222, 224, 240, 0.08);
border-color: rgba(222, 224, 240, 0.2);
}
.cta-ghost:hover .cta-arrow {
background: rgba(58, 85, 196, 0.22);
border-color: rgba(222, 224, 240, 0.4);
}
/* LINK — text-only for minor inline links */
.cta-link {
padding: 0.45rem 0;
border: none;
background: transparent;
color: #DEE0F0;
border-color: rgba(136, 145, 199, 0.55);
font-size: 0.72rem;
letter-spacing: 0.2em;
border-bottom: 1px solid transparent;
border-radius: 0;
white-space: nowrap;
font-weight: 700;
}
.cta-link:hover {
color: #FFFFFF;
border-bottom-color: rgba(222, 224, 240, 0.55);
transform: translateX(2px);
}
.cta-link .cta-arrow {
width: auto;
height: auto;
padding: 0;
background: transparent;
border: none;
}
.cta-link:hover .cta-arrow { transform: translate(3px, -1px); }
/* arrow motion per direction */
.cta-btn[data-arrow='up-right']:hover .cta-arrow { transform: rotate(45deg); }
.cta-btn[data-arrow='right']:hover .cta-arrow { transform: translateX(3px); }
.cta-link[data-arrow='up-right']:hover .cta-arrow,
.cta-link[data-arrow='right']:hover .cta-arrow { transform: translate(3px, -1px); }
/* full-width on small screens for primary CTA wrappers */
.cta-btn.cta-full {
width: 100%;
justify-content: space-between;
}
/* === Legacy .btn aliases — kept for backward compat, route to new system === */
.btn {
display: inline-flex;
align-items: center;
gap: 0.65rem;
padding: 0.95rem 1.45rem;
border-radius: 999px;
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.22em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
transition: transform 0.3s cubic-bezier(0.16,1,0.3,1), box-shadow 0.3s, background 0.3s, color 0.3s, border-color 0.3s;
}
.btn:hover { transform: translateY(-2px); }
.btn-primary {
background: linear-gradient(135deg, #5a76e8 0%, #4a66d8 35%, #2f4cb4 70%, #273F94 100%);
color: #FFFFFF;
box-shadow:
0 24px 56px rgba(39, 63, 148, 0.55),
0 0 0 1px rgba(222, 224, 240, 0.22),
0 0 32px rgba(74, 102, 216, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.24);
}
.btn-primary:hover {
box-shadow:
0 30px 66px rgba(39, 63, 148, 0.7),
0 0 0 1px rgba(222, 224, 240, 0.32),
0 0 44px rgba(74, 102, 216, 0.55),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.btn-ghost {
background:
linear-gradient(135deg, rgba(58, 85, 196, 0.16), rgba(14, 13, 18, 0.55) 60%),
rgba(14, 13, 18, 0.55);
color: #FFFFFF;
border-color: rgba(222, 224, 240, 0.55);
backdrop-filter: blur(14px);
box-shadow:
0 14px 32px rgba(0, 0, 0, 0.55),
inset 0 1px 0 rgba(222, 224, 240, 0.22),
0 0 0 1px rgba(58, 85, 196, 0.18);
}
.btn-ghost:hover {
border-color: rgba(222, 224, 240, 0.82);
background:
linear-gradient(135deg, rgba(74, 102, 216, 0.3), rgba(20, 22, 38, 0.62) 60%),
rgba(20, 22, 38, 0.62);
color: #FFFFFF;
box-shadow:
0 22px 46px rgba(0, 0, 0, 0.62),
inset 0 1px 0 rgba(222, 224, 240, 0.3),
0 0 0 1px rgba(58, 85, 196, 0.5),
0 0 32px rgba(58, 85, 196, 0.4);
}
.btn-outline {
background: transparent;
color: #ECEEF6;
border-color: rgba(222, 224, 240, 0.28);
}
.btn-outline:hover {
background: rgba(39, 63, 148, 0.14);
background: rgba(58, 85, 196, 0.14);
border-color: rgba(222, 224, 240, 0.6);
color: #FFFFFF;
}
/* === Layout helpers === */

View File

@ -4,7 +4,7 @@ import { FooterAndContact } from '@/components/FooterAndContact';
import { Hero3DRobotics } from '@/components/robotics/Hero3DRobotics';
import { BrandShowcase } from '@/components/robotics/BrandShowcase';
import { CategoryShowcaseScroll } from '@/components/robotics/CategoryShowcaseScroll';
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
import { FeaturedRobotsShowcase } from '@/components/robotics/FeaturedRobotsShowcase';
import { IndustryUseCases } from '@/components/robotics/IndustryUseCases';
import { DemoCTA } from '@/components/robotics/DemoCTA';
import { ConfigureCTA } from '@/components/robotics/ConfigureCTA';
@ -83,7 +83,7 @@ export default function HomePage() {
</div>
</MotionSection>
<MotionSection style={{ padding: 'clamp(1.5rem, 3vw, 2.5rem) 0 clamp(3rem, 6vw, 5rem)' }} id="categories">
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="categories">
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<SectionHeading
eyebrow="Robot categories"
@ -97,21 +97,11 @@ export default function HomePage() {
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="featured">
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<SectionHeading
eyebrow="Featured robots"
title="Available exclusively in the UAE through YS Lootah Robotics."
description="A live snapshot of robots ready to demo at our Dubai showroom or deploy at your venue. Request a UAE quotation or book a live demo."
eyebrow="Dubai showroom"
title="Robots you can see, test, and deploy now."
description="Four exhibits on our Dubai showroom floor — step in, request a live demo, or ask for a price."
/>
<div
style={{
display: 'grid',
gap: '1.25rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
}}
>
{FEATURED_ROBOTS.map((r, idx) => (
<RobotProductCard key={r.id} robot={r} priority={idx === 0} />
))}
</div>
<FeaturedRobotsShowcase robots={FEATURED_ROBOTS} />
</div>
</MotionSection>
@ -124,9 +114,9 @@ export default function HomePage() {
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="services">
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<SectionHeading
eyebrow="Our solutions"
title="Intelligent robotics across the full lifecycle."
description="From autonomous robotics and intelligent automation to motion programming, SDKs, and remote diagnostics a complete robotics partner for UAE businesses."
eyebrow="UAE deployment"
title="Real robots, deployed and supported across the UAE."
description="From Dubai showroom demo to live venue deployment — procurement, configuration, installation, training, and ongoing support, handled by one local team."
/>
<ServicesGrid />
</div>
@ -147,9 +137,9 @@ export default function HomePage() {
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="how-it-works">
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<SectionHeading
eyebrow="How we work"
title="From inquiry to live deployment."
description="Three steps from your first message to a robot running in your venue fully supported by our Dubai team."
eyebrow="Deployment timeline"
title="Five steps from procurement to support."
description="The robotics implementation journey — what changes at every stage, in order."
/>
<HowItWorks />
</div>

View File

@ -177,10 +177,7 @@ export default async function RobotDetailPage({ params }: { params: Promise<Para
</MotionSection>
<MotionSection>
<ConfigureCTA
title={`Configure ${robot.name} for your business.`}
description="Choose persona, attire, color, and accessories visualize your deployment before requesting a quotation."
/>
<ConfigureCTA />
</MotionSection>
{related.length > 0 && (

View File

@ -24,8 +24,8 @@ const BRAND_VISUALS: Record<RobotBrand, BrandVisual> = {
description:
'Service, delivery, cleaning, and hospitality robotics available exclusively in the UAE through YS Lootah Robotics.',
chips: ['Service', 'Delivery', 'Cleaning', 'Hospitality'],
primary: { src: '/images/robots/pudu-bellabot.svg', alt: 'Pudu BellaBot delivery robot' },
secondary: { src: '/images/robots/pudu-kettybot.svg', alt: 'Pudu KettyBot service robot' },
primary: { src: '/images/robots/pudu-bellabot.png', alt: 'Pudu BellaBot multi-tray delivery robot' },
secondary: { src: '/images/robots/pudu-kettybot.png', alt: 'Pudu KettyBot Pro service robot with built-in advertising display' },
},
};

View File

@ -1,22 +1,18 @@
'use client';
import { useRef, useEffect, useState, useCallback } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { motion, useScroll, useTransform } from 'framer-motion';
import { ArrowRight, Bot, Footprints, Coffee, Truck, Sparkles, Hotel, Search, Factory } from 'lucide-react';
import { motion } from 'framer-motion';
import { ArrowUpRight, Bot, Footprints, Coffee, Truck, Sparkles, Hotel, Search, Factory } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import Link from 'next/link';
const ease = [0.16, 1, 0.3, 1] as const;
type Category = {
id: string;
num: string;
name: string;
brand: 'Unitree' | 'Pudu Robotics' | 'Unitree + Pudu';
brandAccent: string;
models: string;
description: string;
useCases: string[];
image: string;
imageAlt: string;
chips: [string, string, string];
href: string;
Icon: LucideIcon;
};
@ -24,649 +20,296 @@ type Category = {
const CATEGORIES: Category[] = [
{
id: 'humanoid',
num: '01',
name: 'Humanoid Robots',
brand: 'Unitree',
brandAccent: '#DEE0F0',
models: 'G1 · H2 · R1',
description:
'Bipedal humanoid platforms for events, education, research, and customer-facing experiences across the UAE.',
useCases: ['Events & activations', 'Education & STEM', 'Research labs', 'Concierge & reception'],
image: '/images/robots/unitree-g1.png',
imageAlt: 'Unitree G1 humanoid robot',
description: 'Bipedal humanoid platforms for concierge, events, and research.',
chips: ['Events', 'Concierge', 'Research'],
href: '/robots/?category=humanoid',
Icon: Bot,
},
{
id: 'quadruped',
num: '02',
name: 'Quadruped Robots',
brand: 'Unitree',
brandAccent: '#DEE0F0',
models: 'Go2 · B2 · A2',
description:
'Agile four-legged robots built for inspection, security patrol, and field mobility in any terrain.',
useCases: ['Facility inspection', 'Security patrol', 'Terrain mobility', 'Robotics research'],
image: '/images/robots/unitree-go2.png',
imageAlt: 'Unitree Go2 quadruped robot',
description: 'Four-legged mobility for inspection, security, and field robotics.',
chips: ['Inspection', 'Security', 'Outdoor'],
href: '/robots/?category=quadruped',
Icon: Footprints,
},
{
id: 'service',
num: '03',
name: 'Service Robots',
brand: 'Pudu Robotics',
brandAccent: '#8891C7',
models: 'KettyBot · BellaBot',
description:
'Greeting, guiding, and customer-interaction robots designed for restaurants, hotels, and retail venues.',
useCases: ['Restaurant greeting', 'Customer guidance', 'Retail activation', 'Hospitality service'],
image: '/images/robots/pudu-kettybot.svg',
imageAlt: 'Pudu KettyBot service robot',
description: 'Greeting, guidance, and customer interaction for restaurants and retail.',
chips: ['Restaurants', 'Retail', 'Reception'],
href: '/robots/?category=service',
Icon: Coffee,
},
{
id: 'delivery',
num: '04',
name: 'Delivery Robots',
brand: 'Pudu Robotics',
brandAccent: '#8891C7',
models: 'BellaBot · D-Series',
description:
'Multi-tray autonomous delivery robots for hotels, restaurants, hospitals, and back-of-house operations.',
useCases: ['Hotel room service', 'Restaurant delivery', 'Hospital logistics', 'Back-of-house transport'],
image: '/images/robots/pudu-bellabot.svg',
imageAlt: 'Pudu BellaBot delivery robot',
description: 'Multi-tray autonomous delivery for hotels, dining, and back-of-house.',
chips: ['Hotels', 'Dining', 'Logistics'],
href: '/robots/?category=delivery',
Icon: Truck,
},
{
id: 'cleaning',
num: '05',
name: 'Cleaning Robots',
brand: 'Pudu Robotics',
brandAccent: '#8891C7',
models: 'CC1 · Commercial Cleaning',
description:
'Autonomous commercial cleaning platforms that sweep, scrub, mop, and vacuum across large venues.',
useCases: ['Shopping malls', 'Airports & transit', 'Hotels & resorts', 'Corporate offices'],
image: '/images/robots/pudu-cc1.svg',
imageAlt: 'Pudu CC1 cleaning robot',
description: 'Sweep, scrub, mop, vacuum across malls, airports, and large venues.',
chips: ['Malls', 'Airports', 'Hotels'],
href: '/robots/?category=cleaning',
Icon: Sparkles,
},
{
id: 'hospitality',
num: '06',
name: 'Hospitality Robots',
brand: 'Pudu Robotics',
brandAccent: '#8891C7',
models: 'BellaBot · KettyBot',
description:
'Premium guest-facing robots for hotels, resorts, and events — designed to enhance every guest moment.',
useCases: ['Hotel concierge', 'Resort service', 'Event activations', 'Guest experience'],
image: '/images/robots/pudu-bellabot.svg',
imageAlt: 'Pudu BellaBot hospitality robot',
description: 'Premium guest-facing robotics for hotels, resorts, and events.',
chips: ['Hotels', 'Resorts', 'Events'],
href: '/robots/?category=hospitality',
Icon: Hotel,
},
{
id: 'inspection',
num: '07',
name: 'Inspection Robots',
brand: 'Unitree',
brandAccent: '#DEE0F0',
models: 'Go2 · B2 · AS2',
description:
'Quadruped inspection platforms for energy, utilities, and industrial sites — equipped with rich sensor payloads.',
useCases: ['Energy & utilities', 'Industrial sites', 'Infrastructure', 'Security patrols'],
image: '/images/robots/unitree-b2.png',
imageAlt: 'Unitree B2 industrial quadruped robot',
description: 'Industrial quadrupeds with sensor payloads for energy and utilities.',
chips: ['Energy', 'Utilities', 'Patrol'],
href: '/robots/?category=inspection',
Icon: Search,
},
{
id: 'commercial',
num: '08',
name: 'Commercial Automation',
brand: 'Unitree + Pudu',
brandAccent: '#3a55c4',
models: 'Multi-brand portfolio',
description:
'Mixed robotics fleets for warehouses, smart buildings, and enterprise operations across the UAE.',
useCases: ['Warehouses', 'Smart buildings', 'Enterprise venues', 'Mixed-fleet deployments'],
image: '/images/robots/unitree-as2.png',
imageAlt: 'Unitree AS2 autonomous system for commercial automation',
description: 'Mixed robotics fleets for warehouses, smart buildings, and enterprises.',
chips: ['Warehouses', 'Smart buildings', 'Fleets'],
href: '/robots/?category=commercial',
Icon: Factory,
},
];
export function CategoryShowcaseScroll() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 900);
check();
window.addEventListener('resize', check);
return () => window.removeEventListener('resize', check);
}, []);
if (isMobile) return <CategoryStack />;
return <CategorySticky />;
}
function CategorySticky() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end end'],
});
const [activeIdx, setActiveIdx] = useState(0);
const manualLockUntil = useRef<number>(0);
useEffect(() => {
return scrollYProgress.on('change', (v) => {
/* honor manual click for a short window so scroll doesn't override */
if (Date.now() < manualLockUntil.current) return;
const idx = Math.min(
CATEGORIES.length - 1,
Math.max(0, Math.floor(v * CATEGORIES.length))
);
setActiveIdx(idx);
});
}, [scrollYProgress]);
const handlePick = useCallback((i: number) => {
/* swap in place, no scroll. Lock scroll listener for 1.2s. */
manualLockUntil.current = Date.now() + 1200;
setActiveIdx(i);
}, []);
/* progress bar height */
const progressHeight = useTransform(scrollYProgress, [0, 1], ['0%', '100%']);
return (
<div
ref={containerRef}
style={{
position: 'relative',
/* ~50vh per category scroll budget on top of sticky height */
height: `calc(100svh + ${CATEGORIES.length * 50}vh)`,
}}
>
<div
style={{
position: 'sticky',
top: '5.5rem',
/* fill the entire viewport — no void below the panel */
height: 'calc(100svh - 6.5rem)',
display: 'grid',
gridTemplateColumns: 'minmax(0, 0.95fr) minmax(0, 1.05fr)',
gap: 'clamp(1.5rem, 3vw, 2.5rem)',
alignItems: 'stretch',
}}
>
{/* LEFT — sticky text column with progress + active category */}
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '0.25rem 0', minWidth: 0 }}>
{/* progress rail */}
<div
style={{
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 2,
background: 'rgba(136,145,199,0.15)',
borderRadius: 1,
overflow: 'hidden',
}}
aria-hidden
>
<motion.div
style={{
width: '100%',
height: progressHeight,
background: 'linear-gradient(180deg, #DEE0F0, #3a55c4)',
boxShadow: '0 0 12px rgba(58,85,196,0.6)',
}}
/>
</div>
{/* category list */}
<div style={{ paddingLeft: '1.25rem', display: 'flex', flexDirection: 'column', gap: '0.4rem', maxHeight: '32vh', overflow: 'hidden' }}>
<span className="eyebrow" style={{ marginBottom: '0.5rem' }}>Robot categories · 8</span>
{CATEGORIES.map((c, i) => (
<button
key={c.id}
type="button"
onClick={() => handlePick(i)}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.55rem',
padding: '0.4rem 0',
background: 'transparent',
border: 'none',
cursor: 'pointer',
textAlign: 'left',
color: i === activeIdx ? '#FBFBFD' : '#6a73a5',
fontSize: i === activeIdx ? '1rem' : '0.85rem',
fontWeight: i === activeIdx ? 600 : 500,
letterSpacing: '-0.005em',
transition: 'color 0.3s, font-size 0.3s',
}}
>
<span
aria-hidden
style={{
width: i === activeIdx ? 18 : 8,
height: 1,
background: i === activeIdx ? '#DEE0F0' : '#3a3d52',
transition: 'width 0.3s, background 0.3s',
}}
/>
<span style={{ fontSize: '0.65rem', letterSpacing: '0.18em', color: '#6a73a5' }}>
{String(i + 1).padStart(2, '0')}
</span>
{c.name}
</button>
))}
</div>
{/* active category copy */}
<div style={{ paddingLeft: '1.25rem', marginTop: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<motion.h2
key={`title-${activeIdx}`}
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: [0.16, 1, 0.3, 1] }}
style={{
margin: 0,
fontSize: 'clamp(1.6rem, 3.4vw, 2.4rem)',
fontWeight: 300,
lineHeight: 1.1,
letterSpacing: '-0.03em',
}}
>
<span className="text-gradient" style={{ fontWeight: 500 }}>
{CATEGORIES[activeIdx].name}
</span>
</motion.h2>
<motion.p
key={`desc-${activeIdx}`}
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.05, ease: [0.16, 1, 0.3, 1] }}
style={{
margin: 0,
color: '#DEE0F0',
fontSize: 'clamp(0.92rem, 1.7vw, 1rem)',
lineHeight: 1.65,
maxWidth: 500,
}}
>
{CATEGORIES[activeIdx].description}
</motion.p>
<motion.div
key={`cta-${activeIdx}`}
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.12, ease: [0.16, 1, 0.3, 1] }}
style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}
>
<Link
href={CATEGORIES[activeIdx].href}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.4rem',
padding: '0.7rem 1.2rem',
borderRadius: 999,
background: 'linear-gradient(135deg, #3a55c4 0%, #273F94 55%, #1a2e6e 100%)',
color: '#FBFBFD',
fontSize: '0.72rem',
fontWeight: 700,
letterSpacing: '0.16em',
textTransform: 'uppercase',
textDecoration: 'none',
boxShadow: '0 12px 32px rgba(39,63,148,0.45)',
}}
>
Explore robots
<ArrowRight size={13} />
</Link>
<Link
href="/book-demo/"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.4rem',
padding: '0.7rem 1.2rem',
borderRadius: 999,
border: '1px solid rgba(222,224,240,0.28)',
background: 'rgba(255,255,255,0.04)',
color: '#DEE0F0',
fontSize: '0.72rem',
fontWeight: 700,
letterSpacing: '0.16em',
textTransform: 'uppercase',
textDecoration: 'none',
}}
>
Book demo
<ArrowRight size={13} />
</Link>
</motion.div>
</div>
</div>
{/* RIGHT — visual stage */}
<div
style={{
position: 'relative',
borderRadius: 24,
overflow: 'hidden',
background:
`radial-gradient(ellipse 70% 50% at 50% 60%, rgba(58,85,196,0.28) 0%, transparent 65%), linear-gradient(180deg, rgba(28,27,33,0.85), rgba(8,8,10,0.97))`,
border: '1px solid rgba(222,224,240,0.14)',
boxShadow: '0 30px 90px rgba(0,0,0,0.7), inset 0 1px 0 rgba(222,224,240,0.06)',
}}
>
{/* metallic edge */}
<div
aria-hidden
style={{
position: 'absolute',
inset: 0,
borderRadius: 24,
padding: 1,
background:
'linear-gradient(135deg, rgba(222,224,240,0.32), transparent 45%, rgba(58,85,196,0.22))',
WebkitMask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)',
WebkitMaskComposite: 'xor',
maskComposite: 'exclude',
pointerEvents: 'none',
}}
/>
{/* grid */}
<div
aria-hidden
style={{
position: 'absolute',
inset: 0,
backgroundImage:
'linear-gradient(rgba(199, 207, 230,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(199, 207, 230,0.06) 1px, transparent 1px)',
backgroundSize: '36px 36px',
maskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 30%, transparent 80%)',
WebkitMaskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 30%, transparent 80%)',
pointerEvents: 'none',
}}
/>
{/* spotlight */}
<div
aria-hidden
style={{
position: 'absolute',
top: '5%',
left: '50%',
transform: 'translateX(-50%)',
width: '70%',
height: '70%',
background:
'radial-gradient(ellipse 60% 70% at 50% 35%, rgba(58,85,196,0.45) 0%, transparent 65%)',
filter: 'blur(14px)',
pointerEvents: 'none',
}}
/>
{/* top floating brand chip */}
<div
style={{
position: 'absolute',
top: '1.25rem',
left: '1.25rem',
zIndex: 5,
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4rem 0.85rem',
borderRadius: 999,
background: 'rgba(14,13,18,0.7)',
border: `1px solid ${CATEGORIES[activeIdx].brandAccent}55`,
color: CATEGORIES[activeIdx].brandAccent,
fontSize: '0.65rem',
fontWeight: 800,
letterSpacing: '0.24em',
textTransform: 'uppercase',
backdropFilter: 'blur(8px)',
transition: 'border-color 0.4s, color 0.4s',
}}
>
<span style={{ width: 6, height: 6, borderRadius: 999, background: CATEGORIES[activeIdx].brandAccent }} />
{CATEGORIES[activeIdx].brand}
</div>
{/* top-right index */}
<div
style={{
position: 'absolute',
top: '1.25rem',
right: '1.25rem',
zIndex: 5,
fontSize: 'clamp(2rem, 4vw, 3rem)',
fontWeight: 800,
color: 'transparent',
WebkitTextStroke: '1px rgba(222,224,240,0.32)',
letterSpacing: '-0.04em',
lineHeight: 1,
}}
>
{String(activeIdx + 1).padStart(2, '0')}
<span style={{ color: '#6a73a5', WebkitTextStroke: 0, fontSize: '0.7rem', letterSpacing: '0.2em', marginLeft: 4 }}>
/ {String(CATEGORIES.length).padStart(2, '0')}
</span>
</div>
{/* product stage — cross-fade per category */}
<div style={{ position: 'absolute', inset: 0 }}>
{CATEGORIES.map((c, i) => (
<motion.div
key={c.id}
aria-hidden={i !== activeIdx}
initial={false}
animate={{
opacity: i === activeIdx ? 1 : 0,
scale: i === activeIdx ? 1 : 0.94,
filter: i === activeIdx ? 'blur(0px)' : 'blur(6px)',
}}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
padding: 'clamp(1.5rem, 4vw, 3rem)',
paddingBottom: '32%',
}}
>
<div
style={{
position: 'relative',
width: '100%',
height: '100%',
filter:
'drop-shadow(0 32px 50px rgba(0,0,0,0.78)) drop-shadow(0 0 32px rgba(58,85,196,0.45))',
}}
>
<Image
src={c.image}
alt={c.imageAlt}
fill
sizes="(max-width: 768px) 90vw, 620px"
style={{ objectFit: 'contain', objectPosition: 'center bottom' }}
priority={i === 0}
/>
</div>
</motion.div>
))}
</div>
{/* floor line */}
<div
aria-hidden
style={{
position: 'absolute',
left: '6%',
right: '6%',
bottom: '32%',
height: 1,
background: 'linear-gradient(90deg, transparent, rgba(222,224,240,0.55), transparent)',
pointerEvents: 'none',
}}
/>
{/* floor reflection */}
{CATEGORIES.map((c, i) => (
<motion.div
key={`r-${c.id}`}
aria-hidden
initial={false}
animate={{ opacity: i === activeIdx ? 0.3 : 0 }}
transition={{ duration: 0.7 }}
style={{
position: 'absolute',
left: 'clamp(1.5rem, 4vw, 3rem)',
right: 'clamp(1.5rem, 4vw, 3rem)',
bottom: '6%',
height: '22%',
transform: 'scaleY(-1)',
transformOrigin: 'center top',
maskImage: 'linear-gradient(to bottom, rgba(0,0,0,0.8), transparent 78%)',
WebkitMaskImage: 'linear-gradient(to bottom, rgba(0,0,0,0.8), transparent 78%)',
pointerEvents: 'none',
filter: 'blur(1.4px)',
}}
>
<Image
src={c.image}
alt=""
fill
sizes="(max-width: 768px) 90vw, 620px"
style={{ objectFit: 'contain', objectPosition: 'center top' }}
/>
</motion.div>
))}
{/* bottom info bar: models + use cases */}
<section className="cats" aria-label="Robot categories">
<div className="cats-grid">
{CATEGORIES.map((c, i) => (
<motion.div
key={`info-${activeIdx}`}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.08, ease: [0.16, 1, 0.3, 1] }}
style={{
position: 'absolute',
bottom: '1rem',
left: '1rem',
right: '1rem',
zIndex: 5,
padding: '0.875rem 1.1rem',
borderRadius: 16,
background: 'rgba(14,13,18,0.78)',
border: '1px solid rgba(222,224,240,0.16)',
backdropFilter: 'blur(14px)',
display: 'flex',
flexDirection: 'column',
gap: '0.55rem',
}}
key={c.id}
initial={{ opacity: 0, y: 18 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-8%' }}
transition={{ duration: 0.55, ease, delay: 0.05 * i }}
className="cat-wrap"
>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem' }}>
<span style={{ fontSize: '0.62rem', color: '#8891C7', letterSpacing: '0.24em', textTransform: 'uppercase', fontWeight: 700 }}>
Featured models
</span>
<span style={{ fontSize: '0.85rem', color: '#FBFBFD', fontWeight: 600 }}>
{CATEGORIES[activeIdx].models}
</span>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{CATEGORIES[activeIdx].useCases.map((u) => (
<span
key={u}
style={{
fontSize: '0.66rem',
padding: '0.22rem 0.55rem',
borderRadius: 999,
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(222,224,240,0.18)',
color: '#D7DBEA',
letterSpacing: '0.04em',
}}
>
{u}
<Link href={c.href} className="cat" aria-label={`Explore ${c.name}`}>
{/* TOP: number left, icon right */}
<div className="cat-top">
<span className="cat-num">{c.num}</span>
<span className="cat-icon" aria-hidden>
<c.Icon size={16} />
</span>
))}
</div>
</motion.div>
</div>
</div>
</div>
);
}
</div>
function CategoryStack() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{CATEGORIES.map((c, i) => (
<Link
key={c.id}
href={c.href}
style={{
position: 'relative',
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) 130px',
gap: '0.75rem',
padding: '1rem',
borderRadius: 18,
background:
`radial-gradient(ellipse 70% 80% at 100% 0%, ${c.brandAccent}1F, transparent 60%), linear-gradient(135deg, rgba(28,27,33,0.85), rgba(8,8,10,0.96))`,
border: `1px solid ${c.brandAccent}33`,
color: '#FBFBFD',
textDecoration: 'none',
overflow: 'hidden',
minHeight: 150,
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', minWidth: 0 }}>
<span style={{ fontSize: '0.6rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#8891C7', fontWeight: 700 }}>
{String(i + 1).padStart(2, '0')} · {c.brand}
</span>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 700, letterSpacing: '-0.01em' }}>
{c.name}
</h3>
<p style={{ margin: 0, color: '#D7DBEA', fontSize: '0.82rem', lineHeight: 1.5 }}>
{c.description}
</p>
<span style={{ marginTop: 'auto', display: 'inline-flex', alignItems: 'center', gap: '0.35rem', color: c.brandAccent, fontSize: '0.68rem', letterSpacing: '0.18em', textTransform: 'uppercase', fontWeight: 700 }}>
Explore
<ArrowRight size={12} />
</span>
</div>
<div
style={{
position: 'relative',
borderRadius: 12,
overflow: 'hidden',
background:
`radial-gradient(ellipse 70% 50% at 50% 60%, ${c.brandAccent}22 0%, transparent 65%), linear-gradient(180deg, rgba(28,27,33,0.7), rgba(10,10,12,0.95))`,
border: '1px solid rgba(222,224,240,0.08)',
}}
>
<Image
src={c.image}
alt={c.imageAlt}
fill
sizes="130px"
style={{ objectFit: 'contain', padding: '0.5rem' }}
/>
</div>
</Link>
))}
</div>
{/* MIDDLE: title + description */}
<div className="cat-mid">
<h3 className="cat-title">{c.name}</h3>
<p className="cat-desc">{c.description}</p>
</div>
{/* BOTTOM: chips + CTA */}
<div className="cat-bot">
<ul className="cat-chips" aria-label={`${c.name} use cases`}>
{c.chips.map((chip) => (
<li key={chip} className="cat-chip">{chip}</li>
))}
</ul>
<span className="cat-cta">
<span>Explore</span>
<span className="cat-cta-arrow" aria-hidden>
<ArrowUpRight size={12} />
</span>
</span>
</div>
<span className="cat-hover-glow" aria-hidden />
</Link>
</motion.div>
))}
</div>
<style jsx>{`
.cats {
position: relative;
}
.cats-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.85rem;
}
.cat-wrap { display: flex; }
.cat {
position: relative;
isolation: isolate;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 1rem;
width: 100%;
padding: 1.25rem 1.25rem 1.1rem;
border-radius: 18px;
border: 1px solid rgba(222, 224, 240, 0.1);
background: linear-gradient(180deg, rgba(20, 19, 26, 0.7), rgba(8, 8, 12, 0.92));
color: #FFFFFF;
text-decoration: none;
overflow: hidden;
transition: border-color 0.35s, transform 0.35s, box-shadow 0.35s, background 0.35s;
}
.cat:hover {
border-color: rgba(74, 102, 216, 0.45);
transform: translateY(-3px);
box-shadow:
0 22px 50px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(74, 102, 216, 0.28),
0 0 32px rgba(58, 85, 196, 0.22);
}
.cat-hover-glow {
position: absolute;
top: -40%;
right: -30%;
width: 80%;
height: 100%;
background: radial-gradient(ellipse 50% 60% at 50% 50%, rgba(74, 102, 216, 0.18), transparent 70%);
filter: blur(40px);
opacity: 0;
transition: opacity 0.4s;
z-index: -1;
pointer-events: none;
}
.cat:hover .cat-hover-glow { opacity: 1; }
/* TOP ROW */
.cat-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.cat-num {
font-size: 0.62rem;
letter-spacing: 0.3em;
font-weight: 800;
color: #6f78a2;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.cat-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
border: 1px solid rgba(74, 102, 216, 0.35);
background: rgba(58, 85, 196, 0.14);
color: #DEE0F0;
transition: transform 0.4s, background 0.4s, border-color 0.4s, box-shadow 0.4s;
}
.cat:hover .cat-icon {
transform: scale(1.06);
background: rgba(74, 102, 216, 0.28);
border-color: rgba(74, 102, 216, 0.6);
box-shadow: 0 0 18px rgba(74, 102, 216, 0.45);
color: #FFFFFF;
}
/* MIDDLE */
.cat-mid {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cat-title {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.005em;
color: #FFFFFF;
}
.cat-desc {
margin: 0;
color: #B5BDDB;
font-size: 0.84rem;
line-height: 1.55;
}
/* BOTTOM */
.cat-bot {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.cat-chips {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.cat-chip {
font-size: 0.58rem;
letter-spacing: 0.08em;
padding: 0.28rem 0.6rem;
border-radius: 999px;
border: 1px solid rgba(222, 224, 240, 0.14);
background: rgba(14, 13, 18, 0.45);
color: #B5BDDB;
font-weight: 600;
}
.cat-cta {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.64rem;
letter-spacing: 0.26em;
text-transform: uppercase;
font-weight: 800;
color: #DEE0F0;
}
.cat-cta-arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 999px;
border: 1px solid rgba(74, 102, 216, 0.35);
background: rgba(58, 85, 196, 0.14);
color: #DEE0F0;
transition: transform 0.35s, background 0.35s, border-color 0.35s;
}
.cat:hover .cat-cta-arrow {
transform: translate(2px, -2px);
background: rgba(74, 102, 216, 0.32);
border-color: rgba(74, 102, 216, 0.7);
color: #FFFFFF;
}
.cat:hover .cat-cta { color: #FFFFFF; }
/* RESPONSIVE GRID */
@media (min-width: 640px) {
.cats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.95rem; }
}
@media (min-width: 1024px) {
.cats-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.05rem; }
}
`}</style>
</section>
);
}

View File

@ -1,67 +1,196 @@
'use client';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { Sparkles } from 'lucide-react';
import { CTAButton } from '@/components/ui/CTAButton';
type Props = {
title?: string;
description?: string;
href?: string;
ctaLabel?: string;
};
const ease = [0.16, 1, 0.3, 1] as const;
export function ConfigureCTA({
title = 'Configure a robotics solution for your business.',
description = 'Build a tailored robot choose persona, attire, color, accessories, and accessories. Visualize before you request a quote.',
href = '/configure/',
ctaLabel = 'Start configuration',
}: Props) {
export function ConfigureCTA() {
return (
<div
style={{
position: 'relative',
display: 'grid',
gap: 'clamp(1.5rem, 3vw, 2.5rem)',
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
alignItems: 'center',
padding: 'clamp(1.75rem, 4vw, 2.75rem)',
borderRadius: '1.5rem',
background:
'radial-gradient(ellipse 80% 80% at 100% 0%, rgba(39, 63, 148,0.16), transparent 60%), linear-gradient(135deg, rgba(15, 12, 8,0.85), rgba(5, 5, 5,0.95))',
border: '1px solid rgba(39, 63, 148,0.28)',
overflow: 'hidden',
boxShadow: '0 20px 80px rgba(0, 0, 0,0.55)',
}}
>
<div
<section className="cfg" aria-label="3D configurator call to action">
<div className="cfg-bg" aria-hidden />
<motion.div
className="cfg-glow"
aria-hidden
style={{
position: 'absolute',
inset: 0,
background:
'linear-gradient(rgba(39, 63, 148,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(39, 63, 148,0.05) 1px, transparent 1px)',
backgroundSize: '40px 40px',
maskImage: 'radial-gradient(ellipse 60% 80% at 80% 30%, #000 30%, transparent 80%)',
WebkitMaskImage: 'radial-gradient(ellipse 60% 80% at 80% 30%, #000 30%, transparent 80%)',
pointerEvents: 'none',
}}
animate={{ opacity: [0.55, 0.95, 0.55], scale: [1, 1.05, 1] }}
transition={{ duration: 9, repeat: Infinity, ease: 'easeInOut' }}
/>
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: '0.875rem' }}>
<span className="eyebrow">3D Configurator · Unitree G1 · Dubai</span>
<h3 style={{ margin: 0, fontSize: 'clamp(1.4rem, 3vw, 2rem)', lineHeight: 1.15, fontWeight: 400, letterSpacing: '-0.02em' }}>
<span className="text-gradient" style={{ fontWeight: 500 }}>{title}</span>
</h3>
<p style={{ margin: 0, color: '#DEE0F0', fontSize: '0.95rem', lineHeight: 1.65 }}>{description}</p>
</div>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '0.75rem', flexWrap: 'wrap' }}>
<Link href={href} className="btn btn-primary">
{ctaLabel}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" 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>
<Link href="/book-demo/" className="btn btn-ghost">Book a demo</Link>
</div>
</div>
<div className="cfg-grid-tex" aria-hidden />
<div className="cfg-vignette" aria-hidden />
<motion.div
className="cfg-body"
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-15%' }}
transition={{ duration: 0.7, ease }}
>
<div className="cfg-copy">
<span className="cfg-eyebrow">
<span className="cfg-eyebrow-pulse" />
3D Configurator · Dubai
</span>
<h3 className="cfg-title">
<span className="text-gradient">Configure a robotics solution for your business.</span>
</h3>
<p className="cfg-desc">
Build a tailored robot experience choose persona, attire, color, accessories, language, and venue use case before requesting a quote.
</p>
<p className="cfg-note">
<Sparkles size={11} />
<span>Available for Unitree G1 and selected robotics solutions.</span>
</p>
</div>
<div className="cfg-actions">
<CTAButton href="/configure/" variant="primary" size="lg" arrow="up-right">
Start configuration
</CTAButton>
<CTAButton href="/book-demo/" variant="secondary" size="lg" arrow="right">
Book a demo
</CTAButton>
</div>
</motion.div>
<style jsx>{`
.cfg {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: 22px;
border: 1px solid rgba(222, 224, 240, 0.12);
background:
radial-gradient(circle at 86% 0%, rgba(58, 85, 196, 0.16), transparent 55%),
linear-gradient(180deg, #0a0a0d 0%, #050507 100%);
padding: clamp(1.5rem, 3.5vw, 2.5rem) clamp(1.25rem, 3vw, 2.5rem);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(222, 224, 240, 0.06);
}
.cfg-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 16% 50%, rgba(58, 85, 196, 0.18), transparent 55%),
radial-gradient(circle at 88% 60%, rgba(136, 145, 199, 0.12), transparent 55%);
z-index: 0;
pointer-events: none;
}
.cfg-glow {
position: absolute;
top: -20%;
right: -10%;
width: 50%;
height: 140%;
background: radial-gradient(ellipse 60% 60% at 50% 50%, rgba(74, 102, 216, 0.42), transparent 70%);
filter: blur(50px);
z-index: 0;
pointer-events: none;
}
.cfg-grid-tex {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(199, 207, 230, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(199, 207, 230, 0.04) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(ellipse 70% 80% at 80% 50%, #000 30%, transparent 85%);
-webkit-mask-image: radial-gradient(ellipse 70% 80% at 80% 50%, #000 30%, transparent 85%);
z-index: 0;
pointer-events: none;
}
.cfg-vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 100% 70% at 50% 115%, rgba(0, 0, 0, 0.45), transparent 55%);
z-index: 0;
pointer-events: none;
}
.cfg-body {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: clamp(1rem, 2vw, 1.5rem);
align-items: center;
}
.cfg-copy {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 640px;
}
.cfg-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.55rem;
font-size: 0.6rem;
letter-spacing: 0.32em;
text-transform: uppercase;
font-weight: 800;
color: #8891C7;
}
.cfg-eyebrow-pulse {
width: 8px;
height: 8px;
border-radius: 999px;
background: #DEE0F0;
box-shadow: 0 0 14px rgba(222, 224, 240, 0.9);
animation: cfg-pulse 2.2s ease-in-out infinite;
}
@keyframes cfg-pulse {
0%, 100% { opacity: 0.4; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.1); }
}
.cfg-title {
margin: 0;
font-size: clamp(1.7rem, 4vw, 2.6rem);
font-weight: 300;
line-height: 1.06;
letter-spacing: -0.04em;
color: #FFFFFF;
}
.cfg-title :global(.text-gradient) { font-weight: 600; }
.cfg-desc {
margin: 0;
color: #F0F2F8;
font-size: clamp(0.95rem, 1.55vw, 1.05rem);
line-height: 1.6;
}
.cfg-note {
margin: 0.15rem 0 0;
display: inline-flex;
align-items: center;
gap: 0.45rem;
color: #8891C7;
font-size: 0.72rem;
letter-spacing: 0.04em;
}
.cfg-note :global(svg) { color: #DEE0F0; }
.cfg-actions {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: center;
}
@media (min-width: 900px) {
.cfg-body {
grid-template-columns: minmax(0, 1.4fr) auto;
gap: clamp(1.5rem, 3vw, 2.5rem);
}
.cfg-actions { justify-content: flex-end; }
}
@media (max-width: 520px) {
.cfg-actions :global(.cta-btn) { width: 100%; justify-content: space-between; }
}
@media (prefers-reduced-motion: reduce) {
.cfg-glow { animation: none !important; }
.cfg-eyebrow-pulse { animation: none !important; }
}
`}</style>
</section>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import { PremiumButton } from '@/components/ui/PremiumButton';
import { CTAButton } from '@/components/ui/CTAButton';
type Props = {
title?: string;
@ -62,8 +62,8 @@ export function DemoCTA({
{description}
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', justifyContent: 'center', marginTop: '0.5rem' }}>
<PremiumButton href={primaryHref}>{primaryLabel}</PremiumButton>
<PremiumButton href={secondaryHref} variant="ghost">{secondaryLabel}</PremiumButton>
<CTAButton href={primaryHref} variant="primary" size="lg" arrow="up-right">{primaryLabel}</CTAButton>
<CTAButton href={secondaryHref} variant="secondary" size="lg" arrow="right">{secondaryLabel}</CTAButton>
</div>
</div>
</div>

View File

@ -113,8 +113,8 @@ export function ExclusiveAccessSection() {
brand="Pudu Robotics"
tag="Service · Delivery · Cleaning"
accent="#8891C7"
primary={{ src: '/images/robots/pudu-bellabot.svg', alt: 'Pudu BellaBot delivery robot' }}
secondary={{ src: '/images/robots/pudu-kettybot.svg', alt: 'Pudu KettyBot service robot' }}
primary={{ src: '/images/robots/pudu-bellabot.png', alt: 'Pudu BellaBot multi-tray delivery robot' }}
secondary={{ src: '/images/robots/pudu-kettybot.png', alt: 'Pudu KettyBot Pro service robot with built-in advertising display' }}
/>
<TerritoryTile />
<ShowroomTile />

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +1,250 @@
'use client';
const STEPS = [
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { Boxes, Cog, ClipboardCheck, GraduationCap, Wrench } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
const ease = [0.16, 1, 0.3, 1] as const;
type Step = { n: string; title: string; body: string; Icon: LucideIcon; accent: string };
const STEPS: Step[] = [
{
n: '01',
title: 'Discover',
body: 'Tell us about your venue and use case. We recommend a brand and model humanoid, quadruped, service, delivery, cleaning, or commercial.',
title: 'Procurement',
body: 'Exclusive UAE access to Unitree & Pudu lineups.',
Icon: Boxes,
accent: '#DEE0F0',
},
{
n: '02',
title: 'Demo & configure',
body: 'Book a live demo at our Dubai showroom or your venue. Configure your robots persona, attire, and accessories in our 3D configurator.',
accent: '#273F94',
title: 'Configuration',
body: 'Persona, attire, routes, languages, and brand fit.',
Icon: Cog,
accent: '#A6B2D8',
},
{
n: '03',
title: 'Deploy & support',
body: 'We handle procurement, setup, training, and ongoing service across the UAE so your team can run, not maintain.',
accent: '#8891C7',
title: 'Deployment',
body: 'Venue mapping, installation, and live commissioning.',
Icon: ClipboardCheck,
accent: '#7FD6D0',
},
{
n: '04',
title: 'Training',
body: 'Staff onboarding and operations handover.',
Icon: GraduationCap,
accent: '#F5C089',
},
{
n: '05',
title: 'Support',
body: 'Remote diagnostics and on-site service in the UAE.',
Icon: Wrench,
accent: '#C9B7E0',
},
];
export function HowItWorks() {
const ref = useRef<HTMLDivElement | null>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start 85%', 'end 25%'],
});
const lineHeight = useTransform(scrollYProgress, [0, 1], ['0%', '100%']);
return (
<div
style={{
position: 'relative',
display: 'grid',
gap: '1rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
}}
>
{STEPS.map((s, i) => (
<div
key={s.n}
className="card"
style={{
position: 'relative',
padding: 'clamp(1.5rem, 3vw, 2rem)',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
overflow: 'hidden',
}}
>
<span
aria-hidden
style={{
position: 'absolute',
top: '-1.5rem',
right: '-1rem',
fontSize: 'clamp(6rem, 12vw, 9rem)',
fontWeight: 800,
color: 'transparent',
WebkitTextStroke: `1px ${s.accent}33`,
letterSpacing: '-0.05em',
lineHeight: 1,
pointerEvents: 'none',
userSelect: 'none',
}}
<div className="tl" ref={ref}>
<div className="tl-track" aria-hidden>
<motion.span className="tl-track-fill" style={{ height: lineHeight }} />
</div>
<ol className="tl-list" role="list">
{STEPS.map((s, i) => (
<motion.li
key={s.n}
className="tl-step"
style={{ ['--tl-acc' as string]: s.accent }}
initial={{ opacity: 0, x: -24 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: '-15%' }}
transition={{ duration: 0.6, ease, delay: 0.08 * i }}
>
{s.n}
</span>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
color: s.accent,
fontSize: '0.7rem',
letterSpacing: '0.22em',
textTransform: 'uppercase',
fontWeight: 700,
}}
>
<span style={{ width: 18, height: 1, background: s.accent }} />
Step {i + 1}
</div>
<h3 style={{ margin: 0, fontSize: '1.35rem', fontWeight: 600, letterSpacing: '-0.01em' }}>{s.title}</h3>
<p style={{ margin: 0, color: '#DEE0F0', lineHeight: 1.65, fontSize: '0.95rem' }}>{s.body}</p>
</div>
))}
<span className="tl-dot" aria-hidden>
<span className="tl-dot-pulse" />
<span className="tl-dot-core" />
</span>
<div className="tl-card">
<div className="tl-head">
<span className="tl-num">{s.n}</span>
<span className="tl-bar" aria-hidden />
<span className="tl-icon" aria-hidden>
<s.Icon size={14} />
</span>
</div>
<h3 className="tl-title">{s.title}</h3>
<p className="tl-body">{s.body}</p>
</div>
</motion.li>
))}
</ol>
<style jsx>{`
.tl {
position: relative;
padding: 0.5rem 0;
}
.tl-track {
position: absolute;
left: 24px;
top: 16px;
bottom: 16px;
width: 2px;
border-radius: 2px;
background: linear-gradient(180deg,
rgba(222, 224, 240, 0.08) 0%,
rgba(58, 85, 196, 0.12) 50%,
rgba(222, 224, 240, 0.08) 100%);
overflow: hidden;
pointer-events: none;
}
.tl-track-fill {
display: block;
width: 100%;
background: linear-gradient(180deg,
#DEE0F0 0%,
#4a66d8 50%,
#273F94 100%);
box-shadow:
0 0 12px rgba(74, 102, 216, 0.7),
0 0 28px rgba(58, 85, 196, 0.4);
}
.tl-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: clamp(1rem, 1.6vw, 1.4rem);
}
.tl-step {
position: relative;
display: grid;
grid-template-columns: 50px minmax(0, 1fr);
gap: 1rem;
align-items: start;
}
.tl-dot {
position: relative;
width: 50px;
height: 50px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tl-dot-pulse {
position: absolute;
width: 36px;
height: 36px;
border-radius: 999px;
background: var(--tl-acc);
opacity: 0.18;
filter: blur(10px);
animation: tl-pulse 2.4s ease-in-out infinite;
}
.tl-dot-core {
position: relative;
width: 16px;
height: 16px;
border-radius: 999px;
background: radial-gradient(circle at 30% 30%, #FFFFFF, var(--tl-acc) 65%);
box-shadow:
0 0 0 4px rgba(10, 10, 14, 0.95),
0 0 0 5px color-mix(in srgb, var(--tl-acc) 55%, transparent),
0 0 18px color-mix(in srgb, var(--tl-acc) 65%, transparent);
}
@keyframes tl-pulse {
0%, 100% { transform: scale(0.85); opacity: 0.18; }
50% { transform: scale(1.15); opacity: 0.42; }
}
.tl-card {
position: relative;
padding: clamp(1rem, 1.8vw, 1.35rem) clamp(1.1rem, 2vw, 1.5rem);
border-radius: 18px;
border: 1px solid rgba(222, 224, 240, 0.12);
background:
radial-gradient(ellipse 60% 80% at 100% 0%, color-mix(in srgb, var(--tl-acc) 14%, transparent), transparent 60%),
linear-gradient(135deg, rgba(20, 19, 26, 0.7), rgba(8, 8, 12, 0.92));
overflow: hidden;
transition: border-color 0.35s, transform 0.35s, box-shadow 0.35s;
}
.tl-step:hover .tl-card {
border-color: color-mix(in srgb, var(--tl-acc) 45%, transparent);
transform: translateX(2px);
box-shadow:
0 20px 48px rgba(0, 0, 0, 0.55),
0 0 0 1px color-mix(in srgb, var(--tl-acc) 32%, transparent),
0 0 32px color-mix(in srgb, var(--tl-acc) 22%, transparent);
}
.tl-head {
display: flex;
align-items: center;
gap: 0.65rem;
margin-bottom: 0.55rem;
}
.tl-num {
font-size: 0.6rem;
letter-spacing: 0.32em;
color: color-mix(in srgb, var(--tl-acc) 78%, white);
font-weight: 800;
text-transform: uppercase;
}
.tl-bar {
flex: 1;
height: 1px;
background: linear-gradient(90deg, color-mix(in srgb, var(--tl-acc) 50%, transparent), transparent);
}
.tl-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 8px;
color: color-mix(in srgb, var(--tl-acc) 90%, white);
border: 1px solid color-mix(in srgb, var(--tl-acc) 35%, transparent);
background: color-mix(in srgb, var(--tl-acc) 12%, rgba(14, 13, 18, 0.55));
}
.tl-title {
margin: 0 0 0.35rem;
font-size: 1.18rem;
font-weight: 700;
color: #FFFFFF;
letter-spacing: -0.01em;
}
.tl-body {
margin: 0;
color: #DEE0F0;
font-size: 0.92rem;
line-height: 1.55;
}
@media (min-width: 900px) {
.tl { padding: 0.5rem 0; }
.tl-track { left: 32px; }
.tl-step { grid-template-columns: 64px minmax(0, 1fr); gap: 1.25rem; }
.tl-dot { width: 64px; height: 64px; }
}
@media (prefers-reduced-motion: reduce) {
.tl-dot-pulse { animation: none !important; }
}
`}</style>
</div>
);
}

View File

@ -1,101 +1,652 @@
'use client';
type Service = { title: string; body: string; icon: string };
import { motion } from 'framer-motion';
import { Boxes, Cog, ClipboardCheck, GraduationCap, Wrench, ShieldCheck, Activity, Layers } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { CTAButton } from '@/components/ui/CTAButton';
const SERVICES: Service[] = [
{
title: 'Autonomous Robotics & Intelligent Automation',
body: 'End-to-end deployment of autonomous robotic systems for industrial, commercial, and public sector operations.',
icon: 'M12 2a8 8 0 0 1 8 8v3l2 4-2 1v3a3 3 0 0 1-3 3h-2v-3h-2v3H9v-3H7v3H5a3 3 0 0 1-3-3v-3l-2-1 2-4v-3a8 8 0 0 1 8-8Z',
},
{
title: 'Autonomous Site Inspection & Patrolling',
body: 'Quadruped patrol robots for facility, infrastructure, and security inspection across UAE sites.',
icon: 'M3 11l3-3h4l2-2 2 2h4l3 3v3l-2 1v3h-3v-3h-3l-1 2-1-2H8v3H5v-3l-2-1v-3Z',
},
{
title: 'Smart Service & Delivery Robots',
body: 'Hospitality, retail, and back-of-house delivery robots configured for UAE venues.',
icon: 'M3 6h11v9H3V6Zm14 3h3l2 3v3h-2a2 2 0 1 1-4 0h-1V9h2ZM6 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z',
},
{
title: 'Collaborative & Flexible Robotics Integration',
body: 'Integration of robotic systems into existing workflows collaborative, flexible, business-friendly.',
icon: 'M12 2v6m0 8v6M4.93 4.93l4.24 4.24m5.66 5.66 4.24 4.24M2 12h6m8 0h6M4.93 19.07l4.24-4.24m5.66-5.66 4.24-4.24',
},
{
title: 'Custom Robot Motion Programming',
body: 'Tailored motion sequences and choreography for events, demos, and brand activations.',
icon: 'M3 12h4l3-7 4 14 3-7h4',
},
{
title: 'Cloud-Based Robot Program Repository',
body: 'Centralized program repository for storing, versioning, and deploying robot behaviors.',
icon: 'M7 18a4 4 0 0 1-1.4-7.8 5 5 0 0 1 9.6-1.6A4.5 4.5 0 0 1 21 13.5 4.5 4.5 0 0 1 16.5 18H7Z',
},
{
title: 'Algorithm Customization & Optimization',
body: 'Tuning of perception, planning, and control algorithms for venue-specific performance.',
icon: 'M4 6h16M4 12h10m-6 6h12',
},
{
title: 'Remote System Monitoring & Diagnostics',
body: 'Live monitoring, alerts, and remote diagnostics keep your robot fleet running.',
icon: 'M2 12h4l3-9 4 18 3-9h6',
},
{
title: 'Hardware-Software Co-Design Consultation',
body: 'Co-design across hardware, sensors, software, and integration for custom robotics programs.',
icon: 'M4 4h7v7H4Zm9 0h7v7h-7Zm-9 9h7v7H4Zm9 0h7v7h-7Z',
},
{
title: 'Robotics Software Development Kit',
body: 'SDK access for partners and developers building on top of our deployed robots.',
icon: 'M8 3 3 8l5 5m8-10 5 5-5 5M14 3l-4 18',
},
{
title: 'Data Analytics Dashboard',
body: 'Centralized dashboards turning robot telemetry into actionable business insight.',
icon: 'M3 21V8h4v13H3Zm7 0V3h4v18h-4Zm7 0v-9h4v9h-4Z',
},
const ease = [0.16, 1, 0.3, 1] as const;
/* Abstract Dubai-centered coverage network */
type Node = { id: string; label: string; angle: number; radius: number };
const HQ = { x: 50, y: 50 };
const NODES: Node[] = [
{ id: 'abu-dhabi', label: 'Abu Dhabi', angle: 210, radius: 36 },
{ id: 'sharjah', label: 'Sharjah', angle: 30, radius: 24 },
{ id: 'ajman', label: 'Ajman', angle: 350, radius: 30 },
{ id: 'rak', label: 'Ras Al Khaimah', angle: 320, radius: 40 },
{ id: 'fujairah', label: 'Fujairah', angle: 80, radius: 38 },
];
const ACCENTS = ['#DEE0F0', '#273F94', '#8891C7'];
const nodePos = (n: Node) => {
const rad = (n.angle * Math.PI) / 180;
return { x: HQ.x + Math.cos(rad) * n.radius, y: HQ.y + Math.sin(rad) * n.radius };
};
type Proof = { value: string; label: string; detail: string; Icon: LucideIcon };
const PROOFS: Proof[] = [
{ value: '11+', label: 'Robot models', detail: 'Unitree + Pudu portfolio', Icon: Layers },
{ value: 'UAE-wide', label: 'Coverage', detail: 'Deployment from Dubai HQ', Icon: ShieldCheck },
{ value: '1 day', label: 'Response SLA', detail: 'Sales & demo · 7 days a week', Icon: Activity },
];
type Step = { n: string; label: string; Icon: LucideIcon };
const STEPS: Step[] = [
{ n: '01', label: 'Procure', Icon: Boxes },
{ n: '02', label: 'Configure', Icon: Cog },
{ n: '03', label: 'Deploy', Icon: ClipboardCheck },
{ n: '04', label: 'Train', Icon: GraduationCap },
{ n: '05', label: 'Support', Icon: Wrench },
];
export function ServicesGrid() {
return (
<div
style={{
display: 'grid',
gap: '1rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
}}
>
{SERVICES.map((s, i) => {
const accent = ACCENTS[i % ACCENTS.length];
return (
<div key={s.title} className="card" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '0.875rem', minHeight: 200 }}>
<div
style={{
width: 44,
height: 44,
borderRadius: 12,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `${accent}1A`,
border: `1px solid ${accent}55`,
}}
<section className="net" aria-label="UAE robotics deployment network">
<div className="net-bg" aria-hidden />
<motion.div
className="net-glow"
aria-hidden
animate={{ opacity: [0.45, 0.75, 0.45], scale: [1, 1.04, 1] }}
transition={{ duration: 10, repeat: Infinity, ease: 'easeInOut' }}
/>
<div className="net-vignette" aria-hidden />
<header className="net-intro">
<span className="net-eyebrow">
<span className="net-eyebrow-dot" />
UAE Deployment
</span>
<h3 className="net-title">
<span className="text-gradient">Robotics deployment across the UAE.</span>
</h3>
<p className="net-desc">
From Dubai showroom demo to live venue deployment, our team handles procurement, configuration, installation, staff training, and ongoing support.
</p>
</header>
<div className="net-body">
{/* LEFT — network visual */}
<motion.figure
className="net-vis"
initial={{ opacity: 0, y: 18 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-12%' }}
transition={{ duration: 0.75, ease }}
aria-label="Dubai HQ deployment network reaching every emirate"
>
<span className="net-vis-tag">Dubai HQ · Coverage network</span>
<svg
className="net-vis-svg"
viewBox="0 0 400 400"
role="img"
aria-hidden="true"
preserveAspectRatio="xMidYMid meet"
>
<defs>
<radialGradient id="net-haze" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="rgba(74, 102, 216, 0.55)" />
<stop offset="65%" stopColor="rgba(58, 85, 196, 0.16)" />
<stop offset="100%" stopColor="rgba(58, 85, 196, 0)" />
</radialGradient>
<radialGradient id="net-hq" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="55%" stopColor="#DEE0F0" />
<stop offset="100%" stopColor="#4a66d8" />
</radialGradient>
<linearGradient id="net-route" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="rgba(222, 224, 240, 0.85)" />
<stop offset="100%" stopColor="rgba(74, 102, 216, 0.55)" />
</linearGradient>
<filter id="net-blur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" />
</filter>
</defs>
{/* central haze */}
<circle cx="200" cy="200" r="170" fill="url(#net-haze)" />
{/* coverage rings */}
{[60, 100, 140, 175].map((r, i) => (
<circle
key={`ring-${r}`}
cx="200"
cy="200"
r={r}
fill="none"
stroke="rgba(222, 224, 240, 0.12)"
strokeWidth="1"
strokeDasharray={i === 3 ? '4 6' : '2 5'}
opacity={1 - i * 0.12}
/>
))}
{/* animated pulse ring */}
<circle cx="200" cy="200" r="40" fill="none" stroke="rgba(74, 102, 216, 0.7)" strokeWidth="1.4">
<animate attributeName="r" values="40;160;40" dur="4s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.65;0;0.65" dur="4s" repeatCount="indefinite" />
</circle>
<circle cx="200" cy="200" r="40" fill="none" stroke="rgba(222, 224, 240, 0.45)" strokeWidth="1">
<animate attributeName="r" values="40;130;40" dur="4s" begin="1.2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.5;0;0.5" dur="4s" begin="1.2s" repeatCount="indefinite" />
</circle>
{/* axis cross */}
<line x1="40" y1="200" x2="360" y2="200" stroke="rgba(222,224,240,0.08)" strokeDasharray="1 6" />
<line x1="200" y1="40" x2="200" y2="360" stroke="rgba(222,224,240,0.08)" strokeDasharray="1 6" />
{/* routes */}
{NODES.map((n) => {
const p = nodePos(n);
const x2 = (p.x / 100) * 400;
const y2 = (p.y / 100) * 400;
return (
<g key={`route-${n.id}`}>
<line
x1="200"
y1="200"
x2={x2}
y2={y2}
stroke="url(#net-route)"
strokeWidth="1.2"
strokeDasharray="3 5"
opacity="0.7"
/>
<circle r="3" fill="#DEE0F0" opacity="0.95">
<animateMotion
dur="3.2s"
repeatCount="indefinite"
path={`M200,200 L${x2},${y2}`}
/>
<animate attributeName="opacity" values="0;1;0" dur="3.2s" repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* HQ glow */}
<circle cx="200" cy="200" r="28" fill="rgba(74, 102, 216, 0.32)" filter="url(#net-blur)" />
{/* HQ core */}
<circle cx="200" cy="200" r="11" fill="url(#net-hq)" />
<circle cx="200" cy="200" r="14" fill="none" stroke="rgba(222, 224, 240, 0.55)" strokeWidth="1" />
<text
x="200"
y="235"
textAnchor="middle"
fill="#FFFFFF"
fontFamily="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
fontSize="9.5"
fontWeight="800"
letterSpacing="0.22em"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d={s.icon} />
</svg>
</div>
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 600, lineHeight: 1.3 }}>{s.title}</h3>
<p style={{ margin: 0, color: '#DEE0F0', lineHeight: 1.6, fontSize: '0.88rem' }}>{s.body}</p>
DUBAI · HQ
</text>
{/* peripheral nodes */}
{NODES.map((n) => {
const p = nodePos(n);
const x = (p.x / 100) * 400;
const y = (p.y / 100) * 400;
const leftSide = p.x < 50;
return (
<g key={n.id} transform={`translate(${x} ${y})`}>
<circle r="9" fill="rgba(74, 102, 216, 0.18)" />
<circle r="4" fill="#DEE0F0" />
<circle r="4" fill="none" stroke="rgba(222, 224, 240, 0.5)" strokeWidth="1">
<animate attributeName="r" values="4;12;4" dur="3.6s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.9;0;0.9" dur="3.6s" repeatCount="indefinite" />
</circle>
<text
x={leftSide ? -12 : 12}
y="3"
textAnchor={leftSide ? 'end' : 'start'}
fill="#B5BDDB"
fontFamily="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
fontSize="8.5"
fontWeight="700"
letterSpacing="0.18em"
>
{n.label.toUpperCase()}
</text>
</g>
);
})}
</svg>
<div className="net-vis-legend">
<span className="net-legend-item">
<span className="net-legend-dot net-legend-hq" />
Dubai HQ · Showroom
</span>
<span className="net-legend-item">
<span className="net-legend-dot" />
Active service regions
</span>
</div>
);
})}
</div>
</motion.figure>
{/* RIGHT — proof cards */}
<div className="net-proofs">
{PROOFS.map((p, i) => (
<motion.article
key={p.label}
className="net-proof"
initial={{ opacity: 0, x: 18 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: '-10%' }}
transition={{ duration: 0.55, ease, delay: 0.08 * i }}
>
<span className="net-proof-icon" aria-hidden>
<p.Icon size={16} />
</span>
<div className="net-proof-stack">
<span className="net-proof-value">{p.value}</span>
<span className="net-proof-label">{p.label}</span>
<span className="net-proof-detail">{p.detail}</span>
</div>
</motion.article>
))}
</div>
</div>
{/* TIMELINE */}
<div className="net-timeline" aria-label="Deployment steps">
<div className="net-timeline-line" aria-hidden />
{STEPS.map((s, i) => (
<motion.div
key={s.n}
className="net-step"
initial={{ opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-5%' }}
transition={{ duration: 0.5, ease, delay: 0.08 * i }}
>
<span className="net-step-dot" aria-hidden>
<span className="net-step-dot-core" />
</span>
<span className="net-step-num">{s.n}</span>
<div className="net-step-text">
<span className="net-step-icon" aria-hidden>
<s.Icon size={13} />
</span>
<span className="net-step-label">{s.label}</span>
</div>
</motion.div>
))}
</div>
<div className="net-cta-row">
<CTAButton href="/book-demo/" variant="primary" size="lg" arrow="up-right">
Book a deployment call
</CTAButton>
<CTAButton href="/robots/" variant="secondary" size="lg" arrow="right">
Browse robots
</CTAButton>
</div>
<style jsx>{`
.net {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: 26px;
border: 1px solid rgba(222, 224, 240, 0.12);
background:
radial-gradient(circle at 88% 4%, rgba(58, 85, 196, 0.16), transparent 55%),
linear-gradient(180deg, #0a0a0d 0%, #050507 100%);
padding: clamp(1.4rem, 3vw, 2.25rem) clamp(1.25rem, 3vw, 2.25rem);
display: grid;
gap: clamp(1.1rem, 2.2vw, 1.6rem);
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(222, 224, 240, 0.05);
}
.net-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 18% 22%, rgba(58, 85, 196, 0.18), transparent 55%),
radial-gradient(circle at 82% 80%, rgba(136, 145, 199, 0.1), transparent 55%);
background-image:
linear-gradient(rgba(199, 207, 230, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(199, 207, 230, 0.035) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 48px 48px, 48px 48px;
z-index: 0;
pointer-events: none;
}
.net-glow {
position: absolute;
top: -10%;
left: -10%;
width: 55%;
height: 65%;
background: radial-gradient(ellipse 60% 60% at 50% 50%, rgba(58, 85, 196, 0.42), transparent 70%);
filter: blur(55px);
z-index: 0;
pointer-events: none;
}
.net-vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 100% 70% at 50% 115%, rgba(0, 0, 0, 0.4), transparent 55%);
z-index: 0;
pointer-events: none;
}
/* INTRO */
.net-intro {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 0.7rem;
max-width: 760px;
}
.net-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.55rem;
font-size: 0.6rem;
letter-spacing: 0.32em;
text-transform: uppercase;
font-weight: 800;
color: #8891C7;
}
.net-eyebrow-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #DEE0F0;
box-shadow: 0 0 14px rgba(222, 224, 240, 0.85);
animation: net-pulse 2.2s ease-in-out infinite;
}
@keyframes net-pulse {
0%, 100% { opacity: 0.45; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.12); }
}
.net-title {
margin: 0;
font-size: clamp(1.7rem, 4.2vw, 2.6rem);
font-weight: 300;
line-height: 1.05;
letter-spacing: -0.04em;
color: #FFFFFF;
}
.net-title :global(.text-gradient) { font-weight: 600; }
.net-desc {
margin: 0;
color: #DEE0F0;
font-size: clamp(0.95rem, 1.55vw, 1.05rem);
line-height: 1.6;
}
/* BODY */
.net-body {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: clamp(1rem, 1.8vw, 1.5rem);
}
/* NETWORK VISUAL */
.net-vis {
position: relative;
margin: 0;
border-radius: 22px;
border: 1px solid rgba(222, 224, 240, 0.1);
background:
radial-gradient(ellipse 80% 80% at 50% 50%, rgba(58, 85, 196, 0.12), transparent 70%),
linear-gradient(180deg, rgba(14, 13, 18, 0.7), rgba(6, 6, 10, 0.95));
padding: 1.1rem;
overflow: hidden;
aspect-ratio: 1 / 1;
max-height: 480px;
}
.net-vis-tag {
position: absolute;
top: 1rem;
left: 1.1rem;
z-index: 2;
font-size: 0.56rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: #8891C7;
font-weight: 800;
}
.net-vis-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.net-vis-legend {
position: absolute;
left: 1rem;
right: 1rem;
bottom: 0.9rem;
z-index: 2;
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.58rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #8891C7;
font-weight: 700;
}
.net-legend-item { display: inline-flex; align-items: center; gap: 0.4rem; }
.net-legend-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #A6B2D8;
box-shadow: 0 0 8px rgba(166, 178, 216, 0.55);
}
.net-legend-hq {
background: #DEE0F0;
box-shadow: 0 0 12px rgba(222, 224, 240, 0.9), 0 0 0 3px rgba(74, 102, 216, 0.32);
}
/* PROOF CARDS */
.net-proofs {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
align-content: start;
}
.net-proof {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 1rem;
padding: 1.1rem 1.2rem;
border-radius: 16px;
border: 1px solid rgba(222, 224, 240, 0.12);
background:
radial-gradient(ellipse 80% 100% at 0% 50%, rgba(58, 85, 196, 0.14), transparent 60%),
linear-gradient(135deg, rgba(20, 19, 26, 0.65), rgba(8, 8, 12, 0.9));
transition: border-color 0.35s, transform 0.35s, box-shadow 0.35s;
}
.net-proof:hover {
border-color: rgba(74, 102, 216, 0.45);
transform: translateY(-2px);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(74, 102, 216, 0.28), 0 0 28px rgba(58, 85, 196, 0.22);
}
.net-proof-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 12px;
border: 1px solid rgba(74, 102, 216, 0.42);
background: rgba(58, 85, 196, 0.16);
color: #DEE0F0;
}
.net-proof-stack {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.net-proof-value {
font-size: 1.55rem;
font-weight: 700;
color: #FFFFFF;
letter-spacing: -0.02em;
line-height: 1.05;
}
.net-proof-label {
font-size: 0.62rem;
letter-spacing: 0.26em;
text-transform: uppercase;
color: #DEE0F0;
font-weight: 700;
}
.net-proof-detail {
font-size: 0.78rem;
color: #8891C7;
line-height: 1.45;
margin-top: 0.18rem;
}
/* TIMELINE */
.net-timeline {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
padding: 1.2rem 1.1rem 1.1rem;
border-radius: 20px;
border: 1px solid rgba(222, 224, 240, 0.1);
background: linear-gradient(135deg, rgba(20, 19, 26, 0.55), rgba(8, 8, 12, 0.78));
backdrop-filter: blur(10px);
}
.net-timeline-line { display: none; }
.net-step {
position: relative;
display: grid;
grid-template-columns: 22px 32px minmax(0, 1fr);
align-items: center;
gap: 0.8rem;
padding: 0.55rem 0.6rem;
border-radius: 12px;
transition: background 0.3s;
}
.net-step:hover { background: rgba(74, 102, 216, 0.08); }
.net-step-dot {
position: relative;
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.net-step-dot-core {
width: 10px;
height: 10px;
border-radius: 999px;
background: radial-gradient(circle at 30% 30%, #FFFFFF, #4a66d8 65%);
box-shadow:
0 0 0 3px rgba(10, 10, 14, 0.95),
0 0 0 4px rgba(74, 102, 216, 0.45),
0 0 14px rgba(74, 102, 216, 0.6);
}
.net-step-num {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.62rem;
letter-spacing: 0.26em;
color: #6f78a2;
font-weight: 800;
}
.net-step-text {
display: flex;
align-items: center;
gap: 0.55rem;
min-width: 0;
}
.net-step-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 8px;
border: 1px solid rgba(74, 102, 216, 0.4);
background: rgba(58, 85, 196, 0.14);
color: #DEE0F0;
}
.net-step-label {
font-size: 1rem;
font-weight: 700;
color: #FFFFFF;
letter-spacing: -0.005em;
}
/* CTAs */
.net-cta-row {
position: relative;
z-index: 2;
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
/* DESKTOP */
@media (min-width: 900px) {
.net { padding: clamp(1.8rem, 3vw, 2.5rem); gap: clamp(1.25rem, 2.2vw, 1.8rem); }
.net-body {
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
gap: clamp(1.25rem, 2.4vw, 1.85rem);
align-items: stretch;
}
.net-vis { max-height: 520px; }
.net-timeline {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0;
padding: 1.1rem 0.5rem;
position: relative;
}
.net-timeline-line {
display: block;
position: absolute;
left: 6%;
right: 6%;
top: 50%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(74, 102, 216, 0.45), rgba(222, 224, 240, 0.35), rgba(74, 102, 216, 0.45), transparent);
z-index: 0;
}
.net-step {
grid-template-columns: 22px minmax(0, 1fr);
grid-template-rows: auto auto;
align-items: center;
column-gap: 0.55rem;
row-gap: 0.3rem;
padding: 0.55rem 1rem;
background: transparent !important;
z-index: 1;
}
.net-step::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(20, 19, 26, 0.55), rgba(8, 8, 12, 0.78));
border-radius: 12px;
z-index: -1;
}
.net-step-num { grid-column: 2; grid-row: 1; }
.net-step-dot { grid-column: 1; grid-row: 1 / span 2; }
.net-step-text { grid-column: 2; grid-row: 2; }
}
@media (max-width: 520px) {
.net-cta-row :global(.cta-btn) { width: 100%; justify-content: space-between; }
}
@media (prefers-reduced-motion: reduce) {
.net-glow { animation: none !important; }
.net-eyebrow-dot { animation: none !important; }
}
`}</style>
</section>
);
}

View File

@ -23,7 +23,7 @@ const SHOWROOM_ROBOTS = [
{
name: 'Pudu BellaBot',
slug: 'pudu-bellabot',
image: '/images/robots/pudu-bellabot.svg',
image: '/images/robots/pudu-bellabot.png',
category: 'Delivery',
accent: '#8891C7',
},

View File

@ -0,0 +1,130 @@
'use client';
import Link from 'next/link';
import { forwardRef } from 'react';
import type { ReactNode, MouseEventHandler } from 'react';
import { ArrowRight, ArrowUpRight } from 'lucide-react';
export type CTAVariant = 'primary' | 'secondary' | 'ghost' | 'link';
export type CTASize = 'sm' | 'md' | 'lg';
export type CTAArrow = 'up-right' | 'right' | 'none';
type BaseProps = {
variant?: CTAVariant;
size?: CTASize;
arrow?: CTAArrow;
full?: boolean;
children: ReactNode;
className?: string;
ariaLabel?: string;
};
type AnchorProps = BaseProps & {
href: string;
external?: boolean;
download?: boolean | string;
type?: never;
onClick?: MouseEventHandler<HTMLAnchorElement>;
disabled?: never;
};
type ButtonProps = BaseProps & {
href?: undefined;
type?: 'button' | 'submit' | 'reset';
onClick?: MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
};
export type CTAButtonProps = AnchorProps | ButtonProps;
const arrowIcon = (arrow: CTAArrow, size: CTASize) => {
if (arrow === 'none') return null;
const px = size === 'sm' ? 12 : size === 'lg' ? 15 : 13;
return (
<span className="cta-arrow" aria-hidden="true">
{arrow === 'up-right' ? <ArrowUpRight size={px} /> : <ArrowRight size={px} />}
</span>
);
};
const classFor = (variant: CTAVariant, size: CTASize, full: boolean, extra?: string) =>
[
'cta-btn',
`cta-${variant}`,
`cta-${size}`,
full ? 'cta-full' : '',
extra ?? '',
]
.filter(Boolean)
.join(' ');
export const CTAButton = forwardRef<HTMLAnchorElement | HTMLButtonElement, CTAButtonProps>(
function CTAButton(props, ref) {
const {
variant = 'primary',
size = 'md',
arrow = 'up-right',
full = false,
children,
className,
ariaLabel,
} = props;
const cls = classFor(variant, size, full, className);
const inner = (
<>
<span className="cta-label">{children}</span>
{arrowIcon(arrow, size)}
</>
);
if ('href' in props && props.href) {
const isExternal = props.external || /^https?:\/\//.test(props.href);
if (isExternal) {
return (
<a
ref={ref as React.Ref<HTMLAnchorElement>}
href={props.href}
target="_blank"
rel="noopener noreferrer"
className={cls}
data-arrow={arrow}
aria-label={ariaLabel}
onClick={props.onClick}
>
{inner}
</a>
);
}
return (
<Link
ref={ref as React.Ref<HTMLAnchorElement>}
href={props.href}
className={cls}
data-arrow={arrow}
aria-label={ariaLabel}
onClick={props.onClick}
>
{inner}
</Link>
);
}
const btnProps = props as ButtonProps;
return (
<button
ref={ref as React.Ref<HTMLButtonElement>}
type={btnProps.type ?? 'button'}
onClick={btnProps.onClick}
disabled={btnProps.disabled}
className={cls}
data-arrow={arrow}
aria-label={ariaLabel}
>
{inner}
</button>
);
},
);
CTAButton.displayName = 'CTAButton';

View File

@ -309,10 +309,10 @@ export const ROBOTS: Robot[] = [
SPEC_CONSULT,
SPEC_PLACEHOLDER,
],
image: '/images/robots/pudu-bellabot.svg',
imageType: 'placeholder',
image: '/images/robots/pudu-bellabot.png',
imageType: 'photo',
accent: GOLD_BRONZE,
officialUrl: 'https://www.pudurobotics.com/en',
officialUrl: 'https://www.pudurobotics.com/en/products/bellabot',
},
{
id: 'pudu-kettybot',
@ -339,10 +339,10 @@ export const ROBOTS: Robot[] = [
SPEC_CONSULT,
SPEC_PLACEHOLDER,
],
image: '/images/robots/pudu-kettybot.svg',
imageType: 'placeholder',
image: '/images/robots/pudu-kettybot.png',
imageType: 'photo',
accent: GOLD_BRAND,
officialUrl: 'https://www.pudurobotics.com/en',
officialUrl: 'https://www.pudurobotics.com/en/products/kettybot_pro',
},
{
id: 'pudu-cc1',
@ -369,10 +369,10 @@ export const ROBOTS: Robot[] = [
SPEC_CONSULT,
SPEC_PLACEHOLDER,
],
image: '/images/robots/pudu-cc1.svg',
imageType: 'placeholder',
image: '/images/robots/pudu-cc1.png',
imageType: 'photo',
accent: GOLD_BRONZE,
officialUrl: 'https://www.pudurobotics.com/en',
officialUrl: 'https://www.pudurobotics.com/en/products/puduCC1',
},
{
id: 'pudu-d-series',
@ -395,10 +395,10 @@ export const ROBOTS: Robot[] = [
],
useCases: ['Warehouses', 'Hospital logistics', 'Hotel back-of-house', 'Industrial kitchens'],
specs: [SPEC_CONSULT, SPEC_PLACEHOLDER],
image: '/images/robots/pudu-pudubot.svg',
imageType: 'placeholder',
image: '/images/robots/pudu-pudubot2.png',
imageType: 'photo',
accent: GOLD_CHAMPAGNE,
officialUrl: 'https://www.pudurobotics.com/en',
officialUrl: 'https://www.pudurobotics.com/en/products/pudubot2',
},
];