Compare commits
2 Commits
fc190343eb
...
d639415d89
| Author | SHA1 | Date | |
|---|---|---|---|
| d639415d89 | |||
| f7bab1fe70 |
BIN
public/images/card 1.png
Normal file
BIN
public/images/card 1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/images/card 2.png
Normal file
BIN
public/images/card 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/images/card 3.png
Normal file
BIN
public/images/card 3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/images/card4.png
Normal file
BIN
public/images/card4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/images/card5.png
Normal file
BIN
public/images/card5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/images/card6.png
Normal file
BIN
public/images/card6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
BIN
public/videos/fleet.mp4
Normal file
BIN
public/videos/fleet.mp4
Normal file
Binary file not shown.
@@ -153,16 +153,21 @@ body {
|
||||
#masthead .elementor-element.elementor-element-0b7bf6f > .elementor-widget-container {
|
||||
padding: 0 42px !important;
|
||||
border-radius: 22px !important;
|
||||
height: 58px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
#masthead .elementor-element.elementor-element-0b7bf6f .header-menu-container .main-menu > li > a {
|
||||
padding-top: 15px !important;
|
||||
padding-bottom: 16px !important;
|
||||
padding-top: 6px !important;
|
||||
padding-bottom: 6px !important;
|
||||
padding-left: 14px !important;
|
||||
padding-right: 14px !important;
|
||||
font-size: 15px !important;
|
||||
line-height: 1.2 !important;
|
||||
white-space: nowrap !important;
|
||||
min-height: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#masthead .header-menu-container .main-menu {
|
||||
@@ -966,18 +971,30 @@ body {
|
||||
|
||||
/* ============================================================
|
||||
SHARED PAGE CONTENT CONTAINER
|
||||
Mirrors the site's e-con-boxed > e-con-inner system: 1480px
|
||||
max-width centred with fluid horizontal padding identical to the
|
||||
Industry Solutions / OurTeam override above. Apply this class to
|
||||
any section's inner container div to guarantee that content edges
|
||||
line up with every other page section (Home, About, Solutions…).
|
||||
Matches the Hero/Header/Footer's real rendered OUTER edge, not the
|
||||
Footer's narrower inner text column. The Hero card
|
||||
(.owl-stage-outer, via .wings-hero-page.elementor-element-741f56c)
|
||||
and the Footer's outer band (.elementor-element-b29b8fc) both sit a
|
||||
fixed, uncapped 20px in from the viewport edge at every desktop
|
||||
width (verified 1280px-2400px — no max-width ever kicks in). Apply
|
||||
this class to any Wings-page section's content wrapper to guarantee
|
||||
its cards/timeline/grid hug those same edges instead of sitting
|
||||
narrower and more centered.
|
||||
============================================================ */
|
||||
.dm-section-container {
|
||||
max-width: 1480px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.dm-section-container {
|
||||
padding-left: var(--dm-mobile-gutter, 10px);
|
||||
padding-right: var(--dm-mobile-gutter, 10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Doormile Wings hero frame: the generic vendor CSS assigns 32px padding to
|
||||
@@ -1197,3 +1214,29 @@ body {
|
||||
margin-top: var(--space-section) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Global Desktop Hero Formatting (Matches live site)
|
||||
.miletruth-hero-container is deliberately excluded: unlike the other
|
||||
hero wrappers it IS the visual card itself (background-image + a
|
||||
flat bottom edge that the stats bar overlaps by -25px), not a
|
||||
wrapper around a separate inner card, so it carries its own
|
||||
correctly-tuned margin-top/padding/border-radius in MileTruthHero.tsx.
|
||||
Including it here clobbered that with the wrapper values, collapsing
|
||||
its top gap to 8px and its internal padding to 20px. */
|
||||
@media (min-width: 1025px) {
|
||||
/* Give every hero wrapper a consistent gap from the top and sides */
|
||||
.elementor-element.elementor-element-741f56c,
|
||||
.custom-standard-hero-container {
|
||||
padding: 20px 24px !important;
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
/* Ensure all inner dark hero cards have elegant rounded corners */
|
||||
.elementor-element.elementor-element-741f56c .owl-carousel.owl-theme .content-item,
|
||||
.elementor-element.elementor-element-741f56c .content-item,
|
||||
.custom-standard-hero-card,
|
||||
.miletruth-hero .slide-content {
|
||||
border-radius: 24px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,11 +797,13 @@ export default function Header() {
|
||||
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
|
||||
/* Fixed 20px safe area on both sides — matches the Hero
|
||||
card and Footer's outer band exactly (both use a flat
|
||||
20px inset, uncapped, at every desktop width), so the
|
||||
header aligns with the rest of the page instead of
|
||||
drifting wider on large screens. Symmetric, so the
|
||||
centred nav does not shift. */
|
||||
padding-inline: clamp(24px, 3vw, 48px) !important;
|
||||
padding-inline: 20px !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;
|
||||
@@ -833,9 +835,13 @@ export default function Header() {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* navbar-right: Contact button, aligned to the right edge. */
|
||||
/* navbar-right: Contact button, aligned to the right edge.
|
||||
The vendor kit's own --margin-right:3% (site.css) would add
|
||||
extra drift off the grid's 20px safe area — zero it so the
|
||||
button's true right edge lands flush with that 20px inset. */
|
||||
#masthead .elementor-element.elementor-element-f961133 {
|
||||
justify-self: end !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function MileTruthHero() {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-radius: 25px 25px 0 0;
|
||||
margin-top: 20px;
|
||||
margin-top: 28px;
|
||||
padding: 100px 0;
|
||||
}
|
||||
.miletruth-hero-container::before {
|
||||
|
||||
62
src/components/sections/TheDoormileWay.module.css
Normal file
62
src/components/sections/TheDoormileWay.module.css
Normal file
@@ -0,0 +1,62 @@
|
||||
/* "The Doormile Way" — eyebrow + hairline divider + a huge display/graphic
|
||||
headline (transparent fill, thin red stroke), composed as a poster-scale
|
||||
typographic element rather than a plain large heading. Font, scale, and
|
||||
line length are deliberately tuned to the reference composition, not
|
||||
derived from a generic fluid-fill formula. */
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kicker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 18px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2.5px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(23, 20, 15, 0.6);
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 0 0 40px 0;
|
||||
border: none;
|
||||
background-color: rgba(23, 20, 15, 0.16);
|
||||
}
|
||||
|
||||
.headingWrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* !important beats the sitewide `.elementor-kit-5 h2` rule
|
||||
(public/css/site.css:48-56, specificity 0-1-1 > this single class). */
|
||||
.heading {
|
||||
margin: 0;
|
||||
max-width: 1220px;
|
||||
font-family: var(--font-syne), "Syne", sans-serif !important;
|
||||
font-weight: 800 !important;
|
||||
font-style: normal !important;
|
||||
letter-spacing: -0.03em !important;
|
||||
line-height: 0.86 !important;
|
||||
text-transform: none !important;
|
||||
color: transparent !important;
|
||||
-webkit-text-stroke: 2px #c01227;
|
||||
font-size: clamp(72px, 14vw, 240px) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.heading {
|
||||
-webkit-text-stroke-width: 1.5px;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { ScrollReveal, StaggerChildren } from "@/animations/Reveal";
|
||||
import styles from "./TheDoormileWay.module.css";
|
||||
|
||||
export default function TheDoormileWay() {
|
||||
return (
|
||||
@@ -10,29 +11,15 @@ export default function TheDoormileWay() {
|
||||
<div className="elementor-element elementor-element-88745f4 e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-parent" data-id="88745f4" data-element_type="container" data-e-type="container">
|
||||
<div className="e-con-inner">
|
||||
<div className="elementor-element elementor-element-343b363 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="343b363" data-element_type="container" data-e-type="container">
|
||||
<ScrollReveal delay={0.05} duration={0.7} yOffset={20}>
|
||||
<div className="elementor-element elementor-element-7afb238 elementor-widget elementor-widget-logico_heading" data-id="7afb238" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title">/ Doormile Approach /</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.15} duration={0.85} yOffset={30}>
|
||||
<div className="elementor-element elementor-element-1cc335a elementor-widget elementor-widget-logico_heading" data-id="1cc335a" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div
|
||||
className="logico-title"
|
||||
style={{
|
||||
WebkitTextStroke: "4px #c01227",
|
||||
color: "#fff",
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
The Doormile Way
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
<div className={styles.header}>
|
||||
<ScrollReveal delay={0.05} duration={0.7} yOffset={20} className={styles.kicker}>
|
||||
<p className={styles.eyebrow}>/ Doormile Approach /</p>
|
||||
<hr className={styles.divider} />
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.15} duration={0.85} yOffset={30} className={styles.headingWrap}>
|
||||
<h2 className={styles.heading}>The Doormile Way</h2>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-4c3e632 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="4c3e632" data-element_type="container" data-e-type="container">
|
||||
<ScrollReveal delay={0.2} duration={0.8} yOffset={25}>
|
||||
<div className="elementor-element elementor-element-5ba6bbf e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5ba6bbf" data-element_type="container" data-e-type="container">
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function WingsFeatureGrid() {
|
||||
__html: `
|
||||
.wfg-section {
|
||||
background: #ffffff;
|
||||
padding: var(--space-section-lg, 80px) clamp(20px, 5vw, 40px);
|
||||
padding: var(--space-section-lg, 80px) 0;
|
||||
}
|
||||
|
||||
.wfg-header {
|
||||
@@ -82,9 +82,12 @@ export default function WingsFeatureGrid() {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wfg-grid {
|
||||
max-width: 1280px;
|
||||
.wfg-inner {
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wfg-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
@@ -139,26 +142,35 @@ export default function WingsFeatureGrid() {
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.wfg-grid { grid-template-columns: 1fr; }
|
||||
/* var(--space-section-lg) floors at a fixed 52px on any phone
|
||||
width (clamp() has no further breakpoint-based step-down), which
|
||||
reads as noticeably heavier than the section rhythm near the
|
||||
footer. Trimmed to a real mobile value here instead. */
|
||||
.wfg-section { padding-top: 44px; padding-bottom: 44px; }
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<section className="wfg-section" id="wings-features">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wfg-header">
|
||||
<div className="wfg-eyebrow">/ Why Businesses Choose Doormile Wings /</div>
|
||||
<h2 className="wfg-title">Built to be depended on</h2>
|
||||
</ScrollReveal>
|
||||
<div className="dm-section-container">
|
||||
<div className="wfg-inner">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wfg-header">
|
||||
<div className="wfg-eyebrow">/ Why Businesses Choose Doormile Wings /</div>
|
||||
<h2 className="wfg-title">Built to be depended on</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<StaggerChildren stagger={0.1} duration={0.7} yOffset={30} className="wfg-grid">
|
||||
{FEATURES.map((f) => (
|
||||
<div key={f.title} className="wfg-card">
|
||||
<div className="wfg-icon" aria-hidden="true">{f.icon}</div>
|
||||
<h3 className="wfg-card-title">{f.title}</h3>
|
||||
<p className="wfg-card-desc">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</StaggerChildren>
|
||||
<StaggerChildren stagger={0.1} duration={0.7} yOffset={30} className="wfg-grid">
|
||||
{FEATURES.map((f) => (
|
||||
<div key={f.title} className="wfg-card">
|
||||
<div className="wfg-icon" aria-hidden="true">{f.icon}</div>
|
||||
<h3 className="wfg-card-title">{f.title}</h3>
|
||||
<p className="wfg-card-desc">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</StaggerChildren>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,15 @@ export default function WingsFinalCTA() {
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
/* dm-section-container replaces the vendor .we-container class here
|
||||
(which is otherwise reused verbatim on the About page CTA), so this
|
||||
section's edges line up with the Footer's shared container instead
|
||||
of that widget's own 1320px width. It carries no vertical margin,
|
||||
so restore the same top spacing .we-container provided. */
|
||||
.wfcta-container {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.wfcta-box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -46,12 +55,28 @@ export default function WingsFinalCTA() {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wfcta-container {
|
||||
margin-top: 40px !important;
|
||||
margin-bottom: 40px !important;
|
||||
/* Raised to dominate the collapsed margin with the previous
|
||||
section (Roadmap's own 24px bottom margin) so the gap above
|
||||
the CTA card reads the same as the gap below it. */
|
||||
margin-top: 14px !important;
|
||||
/* There's ~110px of unexplained extra space rendered between this
|
||||
container and the Footer that follows on mobile (verified with
|
||||
Playwright — no element/padding/margin in the DOM accounts for
|
||||
it, so it isn't coming from either section's own box). Pulling
|
||||
the CTA up with a negative bottom margin closes most of that
|
||||
gap directly instead of chasing the underlying cause, without
|
||||
touching the shared Footer component (used on every page) or
|
||||
either card's internal padding. Tuned so the resulting visible
|
||||
gap below the card matches the ~44px gap above it. */
|
||||
margin-bottom: -96px !important;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
.wfcta-box {
|
||||
padding: 48px 20px !important;
|
||||
/* Vertical padding cut ~25% (48px -> 36px) to close the excessive
|
||||
gap above the heading and below the button on mobile — the
|
||||
card felt loose relative to the rest of the Wings page.
|
||||
Horizontal padding/border-radius/width untouched. */
|
||||
padding: 36px 20px !important;
|
||||
border-radius: 22px !important;
|
||||
}
|
||||
.wfcta-title {
|
||||
@@ -68,7 +93,7 @@ export default function WingsFinalCTA() {
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div className="we-container wfcta-container" id="wings-final-cta">
|
||||
<div className="dm-section-container wfcta-container" id="wings-final-cta">
|
||||
<div className="we-cta wfcta-box">
|
||||
<div className="we-cta-inner">
|
||||
<h2 className="we-cta-title wfcta-title">
|
||||
|
||||
@@ -49,6 +49,20 @@ const FEATURES: EVFeature[] = [
|
||||
export default function WingsFleetStrategy() {
|
||||
return (
|
||||
<div id="wings-fleet">
|
||||
{/* EVSection is shared across other pages (Workflow1/Workflow2 etc.),
|
||||
so its own .evnd-gap-bottom rule isn't touched — this override is
|
||||
scoped to #wings-fleet only, and reduces just the blank margin
|
||||
immediately before Infrastructure by ~42% (24px -> 14px) on
|
||||
mobile, matching the site's tighter mobile rhythm elsewhere. */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@media (max-width: 767px) {
|
||||
#wings-fleet .evnd-gap-bottom { margin-bottom: 14px !important; }
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<EVSection
|
||||
bannerImage="/images/pahse1.png"
|
||||
cardNumber="Phase 1"
|
||||
@@ -61,29 +75,29 @@ export default function WingsFleetStrategy() {
|
||||
metrics={METRICS}
|
||||
cardsHeading="FLEET SPECIFICATIONS"
|
||||
cardsTheme={{ accent: "#c8102e", accent2: "#ff6a3d", glow: "rgba(200,16,46,0.22)" }}
|
||||
gapTop
|
||||
gapBottom
|
||||
badges={[
|
||||
{ value: "3", label: "Phase 1 Aircraft" },
|
||||
{ value: "25", label: "Phase 2 Vision" },
|
||||
]}
|
||||
mediaSlot={
|
||||
<div
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: 420,
|
||||
background: "linear-gradient(150deg, #17171b 0%, #0b0b0d 100%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
background: "#17171b"
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Placeholder — swap for sourced ATR72/Q400 photography or a
|
||||
designed illustration once available. */}
|
||||
<svg width="88" height="88" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.15)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M22 12h-6l-4-9-2 1 3 8H5l-2-3-1.5.6L3 14l1.5 4.4L6 18l-2-3h8l3 8 2-1-3-8h6z" />
|
||||
</svg>
|
||||
</div>
|
||||
<source src="/videos/fleet.mp4" type="video/mp4" />
|
||||
</video>
|
||||
}
|
||||
ariaLabel="Doormile Wings fleet strategy"
|
||||
/>
|
||||
|
||||
@@ -36,23 +36,29 @@ export default function WingsHero() {
|
||||
return;
|
||||
}
|
||||
|
||||
gsap.fromTo(
|
||||
heading,
|
||||
{ y: 55, opacity: 0, scale: 0.95 },
|
||||
{ y: 0, opacity: 1, scale: 1, duration: 1.1, ease: "power4.out" }
|
||||
);
|
||||
if (heading) {
|
||||
gsap.fromTo(
|
||||
heading,
|
||||
{ y: 55, opacity: 0, scale: 0.95 },
|
||||
{ y: 0, opacity: 1, scale: 1, duration: 1.1, ease: "power4.out" }
|
||||
);
|
||||
}
|
||||
|
||||
gsap.fromTo(
|
||||
text,
|
||||
{ y: 30, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.95, ease: "power3.out", delay: 0.25 }
|
||||
);
|
||||
if (text) {
|
||||
gsap.fromTo(
|
||||
text,
|
||||
{ y: 30, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.95, ease: "power3.out", delay: 0.25 }
|
||||
);
|
||||
}
|
||||
|
||||
gsap.fromTo(
|
||||
actions,
|
||||
{ y: 24, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.85, ease: "power3.out", delay: 0.38 }
|
||||
);
|
||||
if (actions) {
|
||||
gsap.fromTo(
|
||||
actions,
|
||||
{ y: 24, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.85, ease: "power3.out", delay: 0.38 }
|
||||
);
|
||||
}
|
||||
}, [activeSlide]);
|
||||
|
||||
const handleDotClick = (index: number) => {
|
||||
@@ -72,14 +78,14 @@ export default function WingsHero() {
|
||||
__html: `
|
||||
.wings-hero-bg.elementor-repeater-item-3264830,
|
||||
.wings-hero-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0.16)), url('/images/wings-heroslider3.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.38), rgba(0, 0, 0, 0.06)), url('/images/wings-heroslider3.png') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
|
||||
.wings-hero-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0.16)), url('/images/wings-heroslide2.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.38), rgba(0, 0, 0, 0.06)), url('/images/wings-heroslide2.png') !important;
|
||||
}
|
||||
|
||||
#doormile-wings,
|
||||
@@ -125,7 +131,7 @@ export default function WingsHero() {
|
||||
align-items: center !important;
|
||||
text-align: center !important;
|
||||
width: 100% !important;
|
||||
max-width: 1040px !important;
|
||||
max-width: 1000px !important;
|
||||
margin: 0 auto !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
@@ -157,7 +163,7 @@ export default function WingsHero() {
|
||||
|
||||
.wings-hero-page .text-content {
|
||||
text-align: center !important;
|
||||
max-width: 760px !important;
|
||||
max-width: 680px !important;
|
||||
width: 100% !important;
|
||||
margin: 0 auto !important;
|
||||
padding-left: 15px !important;
|
||||
@@ -222,12 +228,15 @@ export default function WingsHero() {
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.wings-hero-page.elementor-element-741f56c {
|
||||
padding: 20px !important;
|
||||
padding: 20px 24px !important;
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.wings-hero-page .owl-carousel.owl-theme .content-item {
|
||||
height: 800px !important;
|
||||
min-height: 800px !important;
|
||||
border-radius: 24px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,19 +254,19 @@ export default function WingsHero() {
|
||||
}
|
||||
|
||||
.wings-hero-page .owl-stage-outer {
|
||||
height: 620px !important;
|
||||
height: 600px !important;
|
||||
}
|
||||
|
||||
.wings-hero-page .owl-carousel.owl-theme .content-item {
|
||||
width: 100% !important;
|
||||
min-height: 620px !important;
|
||||
height: 620px !important;
|
||||
min-height: 600px !important;
|
||||
height: 600px !important;
|
||||
border-radius: 25px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.wings-hero-page .slide-content {
|
||||
min-height: 620px !important;
|
||||
min-height: 600px !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
@@ -284,13 +293,13 @@ export default function WingsHero() {
|
||||
}
|
||||
|
||||
.wings-hero-page .owl-stage-outer {
|
||||
height: 560px !important;
|
||||
height: 520px !important;
|
||||
}
|
||||
|
||||
.wings-hero-page .owl-carousel.owl-theme .content-item,
|
||||
.wings-hero-page .slide-content {
|
||||
min-height: 560px !important;
|
||||
height: 560px !important;
|
||||
min-height: 520px !important;
|
||||
height: 520px !important;
|
||||
border-radius: 22px !important;
|
||||
}
|
||||
|
||||
@@ -303,7 +312,7 @@ export default function WingsHero() {
|
||||
|
||||
.wings-hero-page .content-slider-item-heading {
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(24px, 7.2vw, 32px) !important;
|
||||
font-size: clamp(20px, 7vw, 32px) !important;
|
||||
line-height: 1.12 !important;
|
||||
}
|
||||
|
||||
|
||||
292
src/components/sections/WingsHowItWorks.module.css
Normal file
292
src/components/sections/WingsHowItWorks.module.css
Normal file
@@ -0,0 +1,292 @@
|
||||
/* Premium editorial "How It Works" — bright warm-white surface, huge uppercase
|
||||
display headline, top-right slide counter, and a five-column process row
|
||||
divided by hairlines with oversized outlined numerals pinned to the top and
|
||||
copy pinned to the bottom, echoing a tall, breathing editorial layout. */
|
||||
|
||||
.section {
|
||||
background: linear-gradient(180deg, #fbfaf7 0%, #f5f2ec 100%);
|
||||
padding: clamp(56px, 7vw, 96px) 0 clamp(80px, 8vw, 120px);
|
||||
margin-bottom: 40px;
|
||||
margin-right: 24px;
|
||||
margin-left: 24px;
|
||||
border-radius: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sectionShell {
|
||||
width: 100%;
|
||||
max-width: 1420px;
|
||||
margin: 0 auto;
|
||||
padding: 0 5vw;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sectionShell {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Header: eyebrow + giant display headline, counter top-right ---------- */
|
||||
|
||||
.headerRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
margin-bottom: clamp(48px, 6vw, 88px);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: left;
|
||||
max-width: 780px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: lowercase;
|
||||
color: rgba(20, 18, 15, 0.5);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: clamp(32px, 4.6vw, 68px) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: -0.01em !important;
|
||||
line-height: 1.02 !important;
|
||||
text-transform: uppercase !important;
|
||||
color: #17140f !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.progressCount {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: #17140f;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progressTrack {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: rgba(23, 20, 15, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: #c8102e;
|
||||
transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* ---------- Process row: hairline-divided columns, numeral up top, copy at the bottom ---------- */
|
||||
|
||||
.flow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.step {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: clamp(200px, 18vw, 260px);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
padding: 0 32px;
|
||||
border-left: 1px solid rgba(23, 20, 15, 0.14);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step:first-child {
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.step:focus-visible {
|
||||
outline: 2px solid rgba(200, 16, 46, 0.55);
|
||||
outline-offset: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stepTop {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stepNum {
|
||||
font-size: clamp(44px, 5.1vw, 92px);
|
||||
font-weight: 800;
|
||||
line-height: 0.85;
|
||||
letter-spacing: -0.02em;
|
||||
color: transparent;
|
||||
-webkit-text-stroke: 1.5px rgba(23, 20, 15, 0.22);
|
||||
transition: -webkit-text-stroke-color 0.4s ease, filter 0.4s ease;
|
||||
}
|
||||
|
||||
.stepActive .stepNum {
|
||||
-webkit-text-stroke-color: #c8102e;
|
||||
filter: drop-shadow(0 0 14px rgba(200, 16, 46, 0.18));
|
||||
}
|
||||
|
||||
.stepTag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(23, 20, 15, 0.4);
|
||||
transition: color 0.4s ease;
|
||||
}
|
||||
|
||||
.stepIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stepActive .stepTag {
|
||||
color: #c8102e;
|
||||
}
|
||||
|
||||
.stepBottom {
|
||||
align-self: end;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.stepTitle {
|
||||
font-size: 19px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3 !important;
|
||||
text-transform: none !important;
|
||||
color: #17140f !important;
|
||||
margin: 0 0 10px 0 !important;
|
||||
}
|
||||
|
||||
.stepDesc {
|
||||
font-size: 13.5px;
|
||||
line-height: 1.65;
|
||||
color: rgba(23, 20, 15, 0.55);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.headerRow {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 40px 28px;
|
||||
}
|
||||
|
||||
.step {
|
||||
flex: none;
|
||||
border-left: none;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stepBottom {
|
||||
max-width: none;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.step:nth-child(n + 3) {
|
||||
border-top: 1px solid rgba(23, 20, 15, 0.14);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.flow > .step:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
/* The clamp() on .section floors at a fixed 56px/80px on any phone width
|
||||
(no further breakpoint-based step-down), noticeably heavier than the
|
||||
rhythm every other Wings section settles into on mobile — normalized to
|
||||
the same 44px/44px here.
|
||||
|
||||
.section's own margin-left/right also stays fixed at 24px below 1024px
|
||||
(set at the top of this file, for the card's inset from the viewport
|
||||
edge). Stacked with .sectionShell's padding that put the card's content
|
||||
34px from the screen edge on phones — noticeably narrower than the
|
||||
Footer's real 10px edge gutter (--dm-mobile-gutter, see globals.css's
|
||||
.dm-section-container). Moving the full 10px gutter onto .section's
|
||||
margin reproduces that same 10px screen-to-card edge, so the card sits
|
||||
flush with its neighbors. .sectionShell then gets its own small 10px
|
||||
breathing-room padding (independent of the outer gutter) so the eyebrow,
|
||||
heading, progress indicator, and step content aren't flush against the
|
||||
card's inner edge. */
|
||||
.section {
|
||||
padding-top: 44px;
|
||||
padding-bottom: 44px;
|
||||
margin-left: var(--dm-mobile-gutter, 10px);
|
||||
margin-right: var(--dm-mobile-gutter, 10px);
|
||||
}
|
||||
|
||||
.sectionShell {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step {
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
.step:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.step:not(:first-child) {
|
||||
border-top: 1px solid rgba(23, 20, 15, 0.14);
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ScrollReveal } from "@/animations/Reveal";
|
||||
import { ScrollReveal, StaggerChildren } from "@/animations/Reveal";
|
||||
import styles from "./WingsHowItWorks.module.css";
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
label: "Local Pickup",
|
||||
desc: "Fast, on-demand first-mile logistics dispatched by electric vehicle.",
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 13h13l3 4h2v3H3z" />
|
||||
<circle cx="7" cy="20" r="1.6" />
|
||||
<circle cx="16" cy="20" r="1.6" />
|
||||
@@ -20,7 +21,7 @@ const STEPS = [
|
||||
label: "Airport Clearance",
|
||||
desc: "Automated screening, grouping, and rapid cross-docking at the hub.",
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
|
||||
<path d="m9 12 2 2 4-4" />
|
||||
</svg>
|
||||
@@ -30,7 +31,7 @@ const STEPS = [
|
||||
label: "Air Transit",
|
||||
desc: "Optimized cargo flights bypass surface congestion on our regional fleet.",
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M22 12h-6l-4-9-2 1 3 8H5l-2-3-1.5.6L3 14l1.5 4.4L6 18l-2-3h8l3 8 2-1-3-8h6z" />
|
||||
</svg>
|
||||
),
|
||||
@@ -39,7 +40,7 @@ const STEPS = [
|
||||
label: "Destination Hub",
|
||||
desc: "Immediate de-consolidation and screening upon arrival.",
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="4" y="10" width="16" height="11" rx="1" />
|
||||
<path d="M9 21v-6h6v6M4 10 12 3l8 7" />
|
||||
</svg>
|
||||
@@ -49,7 +50,7 @@ const STEPS = [
|
||||
label: "EV Last-Mile",
|
||||
desc: "Zero-emission electric vehicles complete the final delivery.",
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 13h13l3 4h2v3H3z" />
|
||||
<circle cx="7" cy="20" r="1.6" />
|
||||
<circle cx="16" cy="20" r="1.6" />
|
||||
@@ -61,246 +62,63 @@ const STEPS = [
|
||||
|
||||
export default function WingsHowItWorks() {
|
||||
const [active, setActive] = useState(0);
|
||||
const fillPct = ((active + 1) / STEPS.length) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.whw-section {
|
||||
background: #09090b;
|
||||
padding: var(--space-section-lg, 80px) clamp(20px, 5vw, 40px);
|
||||
}
|
||||
<section className={styles.section} id="wings-how-it-works">
|
||||
<div className={styles.sectionShell}>
|
||||
<div className={styles.headerRow}>
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className={styles.header}>
|
||||
<div className={styles.eyebrow}>/ How It Works /</div>
|
||||
<h2 className={styles.title}>
|
||||
Pickup to
|
||||
<br />
|
||||
delivery, one
|
||||
<br />
|
||||
connected flow
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
.whw-header {
|
||||
margin: 0 auto 56px auto;
|
||||
max-width: 720px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.whw-eyebrow {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: #c8102e;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.whw-title {
|
||||
font-size: clamp(28px, 3.2vw, 46px) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: -0.03em !important;
|
||||
line-height: 1.15 !important;
|
||||
text-transform: none !important;
|
||||
color: #ffffff !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.whw-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.whw-track-wrap {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 2px;
|
||||
margin: 0 auto 44px auto;
|
||||
}
|
||||
|
||||
.whw-track-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: #c8102e;
|
||||
box-shadow: 0 0 10px #c8102e;
|
||||
border-radius: 2px;
|
||||
transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.whw-nodes {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.whw-node {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #18181b;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
|
||||
}
|
||||
|
||||
.whw-node.is-active {
|
||||
background: #c8102e;
|
||||
border-color: #ffffff;
|
||||
box-shadow: 0 0 10px #c8102e;
|
||||
}
|
||||
|
||||
.whw-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.whw-step {
|
||||
min-width: 0;
|
||||
padding: 22px 18px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.35s ease, background-color 0.35s ease, transform 0.35s ease;
|
||||
}
|
||||
|
||||
.whw-step.is-active {
|
||||
border-color: rgba(200, 16, 46, 0.5);
|
||||
background: rgba(200, 16, 46, 0.06);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.whw-step-num {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.whw-step.is-active .whw-step-num {
|
||||
color: #c8102e;
|
||||
}
|
||||
|
||||
.whw-step-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.whw-step.is-active .whw-step-icon {
|
||||
background: rgba(200, 16, 46, 0.15);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.whw-step-label {
|
||||
font-size: 15px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3 !important;
|
||||
text-transform: none !important;
|
||||
color: #ffffff !important;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.whw-step-desc {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.whw-track-wrap { display: none; }
|
||||
|
||||
.whw-steps {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.whw-steps::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.whw-step {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.whw-step::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -27px;
|
||||
top: 24px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #18181b;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.whw-step.is-active::before {
|
||||
background: #c8102e;
|
||||
border-color: #ffffff;
|
||||
box-shadow: 0 0 10px rgba(200, 16, 46, 0.7);
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<section className="whw-section" id="wings-how-it-works">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="whw-header">
|
||||
<div className="whw-eyebrow">/ How It Works /</div>
|
||||
<h2 className="whw-title">Pickup to delivery, one connected flow</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<ScrollReveal delay={0.1} duration={0.8} yOffset={30} className="whw-inner">
|
||||
<div className="whw-track-wrap" aria-hidden="true">
|
||||
<div className="whw-track-fill" style={{ width: `${fillPct}%` }} />
|
||||
<div className="whw-nodes">
|
||||
{STEPS.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`whw-node${i <= active ? " is-active" : ""}`}
|
||||
style={{ left: `${((i + 0.5) / STEPS.length) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.progress} aria-hidden="true">
|
||||
<span className={styles.progressCount}>
|
||||
{String(active + 1).padStart(2, "0")} / {String(STEPS.length).padStart(2, "0")}
|
||||
</span>
|
||||
<span className={styles.progressTrack}>
|
||||
<span
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${((active + 1) / STEPS.length) * 100}%` }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="whw-steps">
|
||||
{STEPS.map((step, i) => (
|
||||
<div
|
||||
key={step.label}
|
||||
className={`whw-step${i === active ? " is-active" : ""}`}
|
||||
onMouseEnter={() => setActive(i)}
|
||||
onClick={() => setActive(i)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") setActive(i);
|
||||
}}
|
||||
>
|
||||
<div className="whw-step-num">0{i + 1}</div>
|
||||
<div className="whw-step-icon" aria-hidden="true">{step.icon}</div>
|
||||
<h3 className="whw-step-label">{step.label}</h3>
|
||||
<p className="whw-step-desc">{step.desc}</p>
|
||||
<StaggerChildren stagger={0.06} duration={0.6} yOffset={24} className={styles.flow}>
|
||||
{STEPS.map((step, i) => (
|
||||
<div
|
||||
key={step.label}
|
||||
className={`${styles.step} ${i === active ? styles.stepActive : ""}`}
|
||||
onMouseEnter={() => setActive(i)}
|
||||
onClick={() => setActive(i)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") setActive(i);
|
||||
}}
|
||||
>
|
||||
<div className={styles.stepTop}>
|
||||
<span className={styles.stepNum}>{String(i + 1).padStart(2, "0")}</span>
|
||||
<span className={styles.stepTag}>
|
||||
<span className={styles.stepIcon} aria-hidden="true">{step.icon}</span>
|
||||
Step
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</section>
|
||||
</>
|
||||
<div className={styles.stepBottom}>
|
||||
<h3 className={styles.stepTitle}>{step.label}</h3>
|
||||
<p className={styles.stepDesc}>{step.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerChildren>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,31 +5,37 @@ const INDUSTRIES = [
|
||||
title: "Quick Commerce",
|
||||
challenge: "Sub-3-hour delivery windows for dark stores and micro-warehouses.",
|
||||
solution: "Dedicated same-day air capacity linking metros to fulfillment hubs.",
|
||||
image: "/images/card 1.png",
|
||||
},
|
||||
{
|
||||
title: "Express B2B Cargo",
|
||||
challenge: "Time-critical industrial parts and components stuck on slow trunk routes.",
|
||||
solution: "Rapid airport-to-airport transport between major economic nodes.",
|
||||
image: "/images/card 2.png",
|
||||
},
|
||||
{
|
||||
title: "Healthcare & Pharma",
|
||||
challenge: "Temperature-sensitive, compliance-critical shipments with zero margin for error.",
|
||||
solution: "Active and passive cold-chain packaging with full chain-of-custody tracking.",
|
||||
image: "/images/card 3.png",
|
||||
},
|
||||
{
|
||||
title: "Cold Chain Logistics",
|
||||
challenge: "Perishables and fresh goods that can't survive slow surface transit.",
|
||||
solution: "Precision temperature tracking matched to airline-speed transit.",
|
||||
image: "/images/card4.png",
|
||||
},
|
||||
{
|
||||
title: "High-Value Cargo",
|
||||
challenge: "Zero tolerance for loss, tampering, or delay on high-value shipments.",
|
||||
solution: "Maximum-security handling with real-time electronic custody tracking.",
|
||||
image: "/images/card5.png",
|
||||
},
|
||||
{
|
||||
title: "E-Commerce Logistics",
|
||||
challenge: "Next-day and same-day fulfillment expectations across tier-1 and tier-2 cities.",
|
||||
solution: "Direct sorting-hub-to-airport clearance network for faster regional delivery.",
|
||||
image: "/images/card6.png",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -41,7 +47,7 @@ export default function WingsIndustries() {
|
||||
__html: `
|
||||
.wind-section {
|
||||
background: #fafafa;
|
||||
padding: var(--space-section-lg, 80px) clamp(20px, 5vw, 40px);
|
||||
padding: var(--space-section-lg, 80px) 0 20px 0; /* Reduced bottom padding to minimize gap */
|
||||
}
|
||||
|
||||
.wind-header {
|
||||
@@ -69,9 +75,12 @@ export default function WingsIndustries() {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wind-grid {
|
||||
max-width: 1280px;
|
||||
.wind-inner {
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wind-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
@@ -172,40 +181,57 @@ export default function WingsIndustries() {
|
||||
@media (max-width: 600px) {
|
||||
.wind-grid { grid-template-columns: 1fr; }
|
||||
.wind-card { aspect-ratio: 3 / 4; }
|
||||
/* var(--space-section-lg) floors at a fixed 52px on any phone
|
||||
width; bottom is already trimmed to 20px, so only the top needs
|
||||
a real mobile reduction to match the section rhythm near the
|
||||
footer. */
|
||||
.wind-section { padding-top: 44px; }
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<section className="wind-section" id="wings-industries">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wind-header">
|
||||
<div className="wind-eyebrow">/ Industries We Serve /</div>
|
||||
<h2 className="wind-title">Built for the shipments that can’t wait</h2>
|
||||
</ScrollReveal>
|
||||
<div className="dm-section-container">
|
||||
<div className="wind-inner">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wind-header">
|
||||
<div className="wind-eyebrow">/ Industries We Serve /</div>
|
||||
<h2 className="wind-title">Built for the shipments that can’t wait</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<ScrollReveal delay={0.1} duration={0.8} yOffset={30} className="wind-grid">
|
||||
{INDUSTRIES.map((ind) => (
|
||||
<div key={ind.title} className="wind-card">
|
||||
<div className="wind-card-wash" aria-hidden="true" />
|
||||
<ScrollReveal delay={0.1} duration={0.8} yOffset={30} className="wind-grid">
|
||||
{INDUSTRIES.map((ind) => (
|
||||
<div
|
||||
key={ind.title}
|
||||
className="wind-card"
|
||||
style={ind.image ? {
|
||||
backgroundImage: `linear-gradient(to bottom, rgba(0,0,0,0.2), rgba(0,0,0,0.8)), url('${ind.image}')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : undefined}
|
||||
>
|
||||
<div className="wind-card-wash" aria-hidden="true" />
|
||||
|
||||
<div className="wind-card-default">
|
||||
<h3 className="wind-card-default-title">{ind.title}</h3>
|
||||
</div>
|
||||
<div className="wind-card-default">
|
||||
<h3 className="wind-card-default-title">{ind.title}</h3>
|
||||
</div>
|
||||
|
||||
<div className="wind-card-hover">
|
||||
<h3 className="wind-card-hover-title">{ind.title}</h3>
|
||||
<div>
|
||||
<p className="wind-card-label">Challenge</p>
|
||||
<p className="wind-card-text">{ind.challenge}</p>
|
||||
<div className="wind-card-hover">
|
||||
<h3 className="wind-card-hover-title">{ind.title}</h3>
|
||||
<div>
|
||||
<p className="wind-card-label">Challenge</p>
|
||||
<p className="wind-card-text">{ind.challenge}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="wind-card-label">Doormile Wings</p>
|
||||
<p className="wind-card-text">{ind.solution}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="wind-card-label">Doormile Wings</p>
|
||||
<p className="wind-card-text">{ind.solution}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
247
src/components/sections/WingsInfrastructure.module.css
Normal file
247
src/components/sections/WingsInfrastructure.module.css
Normal file
@@ -0,0 +1,247 @@
|
||||
.section {
|
||||
background: #09090b;
|
||||
padding: var(--space-section-lg, 100px) 0;
|
||||
margin-bottom: 30px;
|
||||
margin-right: 24px;
|
||||
margin-left: 24px;
|
||||
border-radius: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Ambient depth: calm, near-static light (stable mood) instead of the more
|
||||
energetic red glow used on How It Works, plus a matching vignette. */
|
||||
.section::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18%;
|
||||
left: 50%;
|
||||
width: 58%;
|
||||
aspect-ratio: 1;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, rgba(200, 16, 46, 0.05) 40%, rgba(0, 0, 0, 0) 68%);
|
||||
transform: translateX(-50%);
|
||||
filter: blur(8px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
animation: infraGlowDrift 14s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.section::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(120% 90% at 50% 0%, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes infraGlowDrift {
|
||||
0% {
|
||||
transform: translateX(-52%) translateY(0);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-48%) translateY(10px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.section::before {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Single page-alignment wrapper. Numbers verified against the Footer's real
|
||||
* rendered content container (elementor-element-e183a77) with Playwright at
|
||||
* 320-2560px: that row sits flush at a 20px gutter up to a 1420px cap, then
|
||||
* centers beyond it, dropping to a 10px gutter under ~1024px. max-width:1420
|
||||
* + padding:20px reproduces the same edge as a 20px-gutter + inner
|
||||
* max-width:1380px pair in one box (no nested max-width needed). Kept
|
||||
* byte-identical to WingsHowItWorks.module.css's .sectionShell.
|
||||
*/
|
||||
.sectionShell {
|
||||
width: 100%;
|
||||
max-width: 1420px;
|
||||
margin: 0 auto;
|
||||
padding: 0 5vw;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sectionShell {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto 56px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: clamp(28px, 3.2vw, 46px) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: -0.03em !important;
|
||||
line-height: 1.15 !important;
|
||||
text-transform: none !important;
|
||||
color: #ffffff !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Capability row — editorial columns divided by hairline rules, matching
|
||||
How It Works' language but unnumbered (these aren't sequential steps). */
|
||||
.grid {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 0 30px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.09);
|
||||
transition: border-color 0.4s ease;
|
||||
}
|
||||
|
||||
.card:first-child {
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
margin-bottom: 26px;
|
||||
transition: border-color 0.4s ease, color 0.4s ease, background-color 0.4s ease, box-shadow 0.4s ease;
|
||||
}
|
||||
|
||||
.card:hover .icon {
|
||||
border-color: rgba(200, 16, 46, 0.55);
|
||||
color: #ffffff;
|
||||
background: rgba(200, 16, 46, 0.12);
|
||||
box-shadow: 0 0 18px rgba(200, 16, 46, 0.28);
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 17px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3 !important;
|
||||
text-transform: none !important;
|
||||
color: rgba(255, 255, 255, 0.82) !important;
|
||||
margin: 0 0 10px 0;
|
||||
transition: color 0.4s ease;
|
||||
}
|
||||
|
||||
.card:hover .cardTitle {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.cardDesc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
margin: 0;
|
||||
transition: color 0.4s ease;
|
||||
}
|
||||
|
||||
.card:hover .cardDesc {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.section {
|
||||
padding: var(--space-section, 64px) 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 34px 28px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-left: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card:nth-child(n + 3) {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
||||
padding-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
/* var(--space-section) floors at a fixed 40px on any phone width (no
|
||||
further breakpoint-based step-down), and .sectionShell's horizontal
|
||||
padding otherwise stays fixed at 24px below 1024px — both read as
|
||||
inconsistent with the rhythm every other Wings section settles into on
|
||||
mobile. Normalized to match: 44px/44px vertical, and the shared
|
||||
--dm-mobile-gutter (10px) horizontal.
|
||||
|
||||
.section's own margin-left/right also stays fixed at 24px below 1024px
|
||||
(set at the top of this file, for the card's inset from the viewport
|
||||
edge). Stacked with .sectionShell's padding that put the card's content
|
||||
34px from the screen edge on phones — noticeably narrower than the
|
||||
Footer's real 10px edge gutter (--dm-mobile-gutter, see globals.css's
|
||||
.dm-section-container). Moving the full 10px gutter onto .section's
|
||||
margin reproduces that same 10px screen-to-card edge, so the card sits
|
||||
flush with its neighbors. .sectionShell then gets its own small 10px
|
||||
breathing-room padding (independent of the outer gutter) so the eyebrow,
|
||||
heading, icons, card titles/descriptions, and dividers (which span
|
||||
.card's full width) aren't flush against the card's inner edge. */
|
||||
.section {
|
||||
padding-top: 44px;
|
||||
padding-bottom: 44px;
|
||||
margin-left: var(--dm-mobile-gutter, 10px);
|
||||
margin-right: var(--dm-mobile-gutter, 10px);
|
||||
}
|
||||
|
||||
.sectionShell {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
.card:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.card:not(:first-child) {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
.card:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ScrollReveal, StaggerChildren } from "@/animations/Reveal";
|
||||
import styles from "./WingsInfrastructure.module.css";
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
@@ -44,117 +45,23 @@ const CARDS = [
|
||||
|
||||
export default function WingsInfrastructure() {
|
||||
return (
|
||||
<>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.wia-section {
|
||||
background: #09090b;
|
||||
padding: var(--space-section, 64px) clamp(20px, 5vw, 40px);
|
||||
}
|
||||
|
||||
.wia-header {
|
||||
margin: 0 auto 44px auto;
|
||||
max-width: 720px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wia-eyebrow {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.wia-title {
|
||||
font-size: clamp(26px, 3vw, 40px) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: -0.03em !important;
|
||||
line-height: 1.2 !important;
|
||||
text-transform: none !important;
|
||||
color: #ffffff !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wia-grid {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wia-card {
|
||||
min-width: 0;
|
||||
padding: 28px 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transition: border-color 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.wia-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.wia-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.wia-card-title {
|
||||
font-size: 15.5px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3 !important;
|
||||
text-transform: none !important;
|
||||
color: #ffffff !important;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.wia-card-desc {
|
||||
font-size: 13.5px;
|
||||
line-height: 1.55;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.wia-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.wia-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<section className="wia-section" id="wings-infrastructure">
|
||||
<ScrollReveal delay={0.05} duration={0.7} yOffset={20} className="wia-header">
|
||||
<div className="wia-eyebrow">/ Infrastructure /</div>
|
||||
<h2 className="wia-title">A network built to move, not to sit idle</h2>
|
||||
<section className={styles.section} id="wings-infrastructure">
|
||||
<div className={styles.sectionShell}>
|
||||
<ScrollReveal delay={0.05} duration={0.7} yOffset={20} className={styles.header}>
|
||||
<div className={styles.eyebrow}>/ Infrastructure /</div>
|
||||
<h2 className={styles.title}>A network built to move, not to sit idle</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<StaggerChildren stagger={0.08} duration={0.6} yOffset={20} className="wia-grid">
|
||||
<StaggerChildren stagger={0.08} duration={0.6} yOffset={20} className={styles.grid}>
|
||||
{CARDS.map((c) => (
|
||||
<div key={c.title} className="wia-card">
|
||||
<div className="wia-icon" aria-hidden="true">{c.icon}</div>
|
||||
<h3 className="wia-card-title">{c.title}</h3>
|
||||
<p className="wia-card-desc">{c.desc}</p>
|
||||
<div key={c.title} className={styles.card}>
|
||||
<div className={styles.icon} aria-hidden="true">{c.icon}</div>
|
||||
<h3 className={styles.cardTitle}>{c.title}</h3>
|
||||
<p className={styles.cardDesc}>{c.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</StaggerChildren>
|
||||
</section>
|
||||
</>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export default function WingsMileTruth() {
|
||||
.wmt-section {
|
||||
position: relative;
|
||||
width: calc(100% - 40px);
|
||||
margin: 0 auto;
|
||||
margin: clamp(24px, 4vw, 40px) auto;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
@@ -195,7 +195,7 @@ export default function WingsMileTruth() {
|
||||
.wmt-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1200px;
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -347,6 +347,11 @@ export default function WingsMileTruth() {
|
||||
@media (max-width: 600px) {
|
||||
.wmt-grid { grid-template-columns: 1fr; }
|
||||
.wmt-callout { flex-direction: column; align-items: flex-start; }
|
||||
/* The vertical clamp() floors at a fixed 64px on any phone width;
|
||||
trimmed to a real mobile value to match the section rhythm near
|
||||
the footer. Horizontal padding is left untouched (its own
|
||||
clamp already resolves small on phones). */
|
||||
.wmt-section { padding-top: 48px; padding-bottom: 48px; }
|
||||
}
|
||||
`,
|
||||
}}
|
||||
@@ -357,7 +362,7 @@ export default function WingsMileTruth() {
|
||||
<div className="wmt-glow tl" aria-hidden="true" />
|
||||
<div className="wmt-glow br" aria-hidden="true" />
|
||||
|
||||
<div className="wmt-inner">
|
||||
<div className="wmt-inner dm-section-container">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wmt-header">
|
||||
<div className="wmt-eyebrow">/ MileTruth™ AI /</div>
|
||||
<h2 className="wmt-title">The intelligence layer behind every delivery</h2>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function WingsRoadmap() {
|
||||
.wrm-section {
|
||||
position: relative;
|
||||
width: calc(100% - 40px);
|
||||
margin: 0 auto;
|
||||
margin: clamp(56px, 7vw, 96px) auto clamp(24px, 4vw, 40px);
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
@@ -87,7 +87,7 @@ export default function WingsRoadmap() {
|
||||
}
|
||||
|
||||
.wrm-inner {
|
||||
max-width: 1200px;
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -203,6 +203,16 @@ export default function WingsRoadmap() {
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Both the outer margin-top and the internal vertical padding
|
||||
clamp()s floor at fixed, sizeable values (56px + 64px = ~120px
|
||||
combined) on any phone width — noticeably heavier than the
|
||||
section rhythm near the footer. Horizontal padding is left
|
||||
untouched (its own clamp already resolves small on phones). */
|
||||
.wrm-section {
|
||||
margin-top: 40px;
|
||||
padding-top: 48px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
.wrm-track-wrap { display: none; }
|
||||
.wrm-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -244,43 +254,45 @@ export default function WingsRoadmap() {
|
||||
/>
|
||||
|
||||
<section className="wrm-section" id="wings-roadmap">
|
||||
<div className="wrm-header">
|
||||
<div className="wrm-eyebrow">/ Future Roadmap /</div>
|
||||
<h2 className="wrm-title">A ten-year path to national scale</h2>
|
||||
</div>
|
||||
|
||||
<div className="wrm-inner">
|
||||
<div className="wrm-track-wrap" aria-hidden="true">
|
||||
<div className="wrm-track-fill" style={{ width: `${fillPct}%` }} />
|
||||
<div className="wrm-nodes">
|
||||
{ROADMAP_DATA.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`wrm-node${i <= active ? " is-active" : ""}`}
|
||||
style={{ left: `${((i + 0.5) / ROADMAP_DATA.length) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="dm-section-container">
|
||||
<div className="wrm-header">
|
||||
<div className="wrm-eyebrow">/ Future Roadmap /</div>
|
||||
<h2 className="wrm-title">A ten-year path to national scale</h2>
|
||||
</div>
|
||||
|
||||
<div className="wrm-grid">
|
||||
{ROADMAP_DATA.map((item, i) => (
|
||||
<div
|
||||
key={item.year}
|
||||
className={`wrm-card${item.glow ? " wrm-glow" : ""}${i === active ? " is-active" : ""}`}
|
||||
onMouseEnter={() => setActive(i)}
|
||||
onClick={() => setActive(i)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") setActive(i);
|
||||
}}
|
||||
>
|
||||
<div className="wrm-card-year">{item.year}</div>
|
||||
<span className={`wrm-phase-pill ${item.phaseClass}`}>{item.phase}</span>
|
||||
<p className="wrm-card-desc">{item.desc}</p>
|
||||
<div className="wrm-inner">
|
||||
<div className="wrm-track-wrap" aria-hidden="true">
|
||||
<div className="wrm-track-fill" style={{ width: `${fillPct}%` }} />
|
||||
<div className="wrm-nodes">
|
||||
{ROADMAP_DATA.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`wrm-node${i <= active ? " is-active" : ""}`}
|
||||
style={{ left: `${((i + 0.5) / ROADMAP_DATA.length) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="wrm-grid">
|
||||
{ROADMAP_DATA.map((item, i) => (
|
||||
<div
|
||||
key={item.year}
|
||||
className={`wrm-card${item.glow ? " wrm-glow" : ""}${i === active ? " is-active" : ""}`}
|
||||
onMouseEnter={() => setActive(i)}
|
||||
onClick={() => setActive(i)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") setActive(i);
|
||||
}}
|
||||
>
|
||||
<div className="wrm-card-year">{item.year}</div>
|
||||
<span className={`wrm-phase-pill ${item.phaseClass}`}>{item.phase}</span>
|
||||
<p className="wrm-card-desc">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function WingsSpeedAdvantage() {
|
||||
__html: `
|
||||
.wsa-section {
|
||||
background: #fafafa;
|
||||
padding: var(--space-section-lg, 80px) clamp(20px, 5vw, 40px);
|
||||
padding: var(--space-section-lg, 80px) 0;
|
||||
}
|
||||
|
||||
.wsa-header {
|
||||
@@ -71,10 +71,13 @@ export default function WingsSpeedAdvantage() {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wsa-inner {
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wsa-face-off {
|
||||
position: relative;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
@@ -159,39 +162,53 @@ export default function WingsSpeedAdvantage() {
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.wsa-face-off { grid-template-columns: 1fr; gap: 40px; }
|
||||
.wsa-vs { position: static; transform: none; margin: -20px auto; }
|
||||
/* Effective visible gap above/below the VS circle is (grid gap +
|
||||
this margin), i.e. 40px - 28px = 12px on each side — an equal,
|
||||
~40% reduction from the previous 20px/20px so the circle reads
|
||||
as bridging the two cards instead of floating in a large empty
|
||||
gap. margin: auto keeps it horizontally centered; the shorthand
|
||||
applies identically to top and bottom so both stay equal. */
|
||||
.wsa-vs { position: static; transform: none; margin: -28px auto; }
|
||||
/* var(--space-section-lg) floors at a fixed 52px on any phone
|
||||
width; trimmed to a real mobile value to match the section
|
||||
rhythm near the footer. */
|
||||
.wsa-section { padding-top: 44px; padding-bottom: 44px; }
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<section className="wsa-section" ref={sectionRef} id="wings-speed">
|
||||
<div className="dm-section-container">
|
||||
<div className="wsa-header" ref={headerRef}>
|
||||
<div className="wsa-eyebrow">/ The Speed Advantage /</div>
|
||||
<h2 className="wsa-title">Same-day, not some-day</h2>
|
||||
</div>
|
||||
|
||||
<div className="wsa-face-off">
|
||||
<div className="wsa-panel wsa-panel-surface" ref={leftRef}>
|
||||
<div className="wsa-panel-label">Surface Logistics</div>
|
||||
<div className="wsa-value">
|
||||
48–<CountUp end={96} duration={1.6} triggerOnce />
|
||||
<div className="wsa-inner">
|
||||
<div className="wsa-face-off">
|
||||
<div className="wsa-panel wsa-panel-surface" ref={leftRef}>
|
||||
<div className="wsa-panel-label">Surface Logistics</div>
|
||||
<div className="wsa-value">
|
||||
48–<CountUp end={96} duration={1.6} triggerOnce />
|
||||
</div>
|
||||
<div className="wsa-unit">Hours</div>
|
||||
<p className="wsa-panel-sub">Delhi–Mumbai standard surface transit time.</p>
|
||||
</div>
|
||||
<div className="wsa-unit">Hours</div>
|
||||
<p className="wsa-panel-sub">Delhi–Mumbai standard surface transit time.</p>
|
||||
</div>
|
||||
|
||||
<div className="wsa-vs" ref={vsRef}>VS</div>
|
||||
<div className="wsa-vs" ref={vsRef}>VS</div>
|
||||
|
||||
<div className="wsa-panel wsa-panel-wings" ref={rightRef}>
|
||||
<div className="wsa-panel-label">Doormile Wings</div>
|
||||
<div className="wsa-value">
|
||||
Max <CountUp end={3} duration={1.4} triggerOnce />
|
||||
<div className="wsa-panel wsa-panel-wings" ref={rightRef}>
|
||||
<div className="wsa-panel-label">Doormile Wings</div>
|
||||
<div className="wsa-value">
|
||||
Max <CountUp end={3} duration={1.4} triggerOnce />
|
||||
</div>
|
||||
<div className="wsa-unit">Hours, Door-to-Door</div>
|
||||
<p className="wsa-panel-sub">Same-day delivery between major regional capitals.</p>
|
||||
</div>
|
||||
<div className="wsa-unit">Hours, Door-to-Door</div>
|
||||
<p className="wsa-panel-sub">Same-day delivery between major regional capitals.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { MotionPathPlugin } from "gsap/MotionPathPlugin";
|
||||
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
|
||||
import { ScrollReveal, StaggerChildren } from "@/animations/Reveal";
|
||||
|
||||
let hasDrawSVG = false;
|
||||
if (typeof window !== "undefined") {
|
||||
gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);
|
||||
try {
|
||||
// DrawSVGPlugin ships free with gsap 3.13+. Registration (not the import
|
||||
// itself) is guarded so the draw step can fall back to plain
|
||||
// stroke-dasharray if it's ever unavailable, without touching timing.
|
||||
gsap.registerPlugin(DrawSVGPlugin);
|
||||
hasDrawSVG = true;
|
||||
} catch {
|
||||
hasDrawSVG = false;
|
||||
}
|
||||
}
|
||||
|
||||
const MOBILE_BQ = 600;
|
||||
const AIRCRAFT_ROTATION_OFFSET = 90; // glyph's rest pose points "up"
|
||||
const WAVE_AMPLITUDE_MIN = 20;
|
||||
const WAVE_AMPLITUDE_MAX = 38;
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
title: "Same-Day Air Logistics",
|
||||
@@ -48,7 +74,415 @@ const FEATURES = [
|
||||
},
|
||||
];
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function buildPathD(points: Point[]): string {
|
||||
if (points.length < 2) return "";
|
||||
let d = `M ${points[0].x} ${points[0].y}`;
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i];
|
||||
const p1 = points[i + 1];
|
||||
const dx = (p1.x - p0.x) / 2;
|
||||
d += ` C ${p0.x + dx} ${p0.y}, ${p1.x - dx} ${p1.y}, ${p1.x} ${p1.y}`;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
// Inserts one raised "crest" point between every consecutive pair of card
|
||||
// anchors, turning the mostly-straight spline into an alternating
|
||||
// trough-crest-trough wave: each card position stays a trough (so the pulse
|
||||
// still arrives level with its icon, unchanged), while the gap between cards
|
||||
// arcs upward. buildPathD's per-segment horizontal-tangent control points
|
||||
// already keep every trough/crest transition smooth (no sharp bends) without
|
||||
// needing a fancier spline — only the input point list changes.
|
||||
//
|
||||
// Each crest's height and horizontal position are nudged by a small,
|
||||
// index-derived (not random) offset, so consecutive humps are never
|
||||
// identical — reading as a real, slightly irregular air route rather than a
|
||||
// generated sine wave — while staying perfectly stable across re-measures
|
||||
// (resize, etc.) since the offsets depend only on segment index, not chance.
|
||||
// Each offset sums two sine terms at unrelated frequencies/phases rather
|
||||
// than one, so the sequence of crest heights doesn't itself read as a
|
||||
// single obvious repeating rhythm.
|
||||
function buildWavePoints(points: Point[], amplitude: number): Point[] {
|
||||
if (points.length < 2 || amplitude <= 0) return points;
|
||||
const waved: Point[] = [points[0]];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i];
|
||||
const p1 = points[i + 1];
|
||||
const ampFactor =
|
||||
1 + 0.28 * Math.sin(i * 2.9 + 0.4) + 0.15 * Math.sin(i * 5.3 + 2.1);
|
||||
const xJitter =
|
||||
(p1.x - p0.x) * (0.08 * Math.sin(i * 1.7 + 1.3) + 0.06 * Math.sin(i * 3.6 + 0.2));
|
||||
waved.push({
|
||||
x: (p0.x + p1.x) / 2 + xJitter,
|
||||
y: Math.min(p0.y, p1.y) - amplitude * ampFactor,
|
||||
});
|
||||
waved.push(p1);
|
||||
}
|
||||
return waved;
|
||||
}
|
||||
|
||||
export default function WingsWhyGrid() {
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const pathRef = useRef<SVGPathElement>(null);
|
||||
// A second copy of the same "d", drawn brighter/thicker/blurred. A moving
|
||||
// dasharray window (kept in sync with the pulse's own progress, see
|
||||
// onUpdate below) makes a soft, feathered segment of the route glow right
|
||||
// under the pulse and fade back to the resting line just behind it — the
|
||||
// route being "energized" as the pulse passes, not just a static line.
|
||||
const illumRef = useRef<SVGPathElement>(null);
|
||||
// The traveler is the primary visual: a bright optical pulse riding the
|
||||
// route. It's the outer group MotionPathPlugin translates. The aircraft is
|
||||
// nested inside it as a small, secondary glyph — never its own focal point.
|
||||
const travelerRef = useRef<SVGGElement>(null);
|
||||
// Only this inner group rotates to face the direction of travel (the
|
||||
// halo). Kept separate from travelerRef with an explicit 0,0 transform
|
||||
// origin so rotation never pivots around a fill-box-derived point.
|
||||
const directionalRef = useRef<SVGGElement>(null);
|
||||
// The aircraft's own rotation wrapper, painted as a sibling *after* the
|
||||
// core circle (see JSX) so its silhouette stays readable on top of the
|
||||
// bright core instead of being buried under it. Mirrors directionalRef's
|
||||
// rotation exactly (set alongside it in onUpdate) so both halves of the
|
||||
// traveler always face the same way despite being separate DOM branches.
|
||||
const aircraftDirectionalRef = useRef<SVGGElement>(null);
|
||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const iconRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const wrap = wrapRef.current;
|
||||
const svg = svgRef.current;
|
||||
const path = pathRef.current;
|
||||
const illum = illumRef.current;
|
||||
const traveler = travelerRef.current;
|
||||
const directional = directionalRef.current;
|
||||
const aircraftDirectional = aircraftDirectionalRef.current;
|
||||
if (!wrap || !svg || !path || !illum || !traveler || !directional || !aircraftDirectional) return;
|
||||
|
||||
// Declared here (not inside the gsap.context callback below) so the
|
||||
// cleanup function — which must run on every unmount, including ones
|
||||
// gsap.context() itself doesn't know about, like these plain rAF/
|
||||
// ResizeObserver handles — can always reach them.
|
||||
let resizeRaf = 0;
|
||||
let initialRaf = 0;
|
||||
let rafIn = 0;
|
||||
let ro: ResizeObserver | undefined;
|
||||
let loadHandler: (() => void) | undefined;
|
||||
// Also assigned (not re-declared) inside the gsap.context callback below,
|
||||
// for the same reason: cleanup needs to reach them even though
|
||||
// ctx.revert() already kills anything the context tracked, since the
|
||||
// ScrollTrigger is created synchronously (tracked) but the flight
|
||||
// timeline is only created later, asynchronously, once onEnter actually
|
||||
// fires (not reliably tracked by the context in that case).
|
||||
let st: ScrollTrigger | null = null;
|
||||
let playedTimeline: gsap.core.Timeline | null = null;
|
||||
|
||||
// Everything GSAP-related (ScrollTrigger, timelines, gsap.set calls) is
|
||||
// created inside this context so a single ctx.revert() on unmount kills
|
||||
// all of it — this is what makes the whole setup safe to re-run on every
|
||||
// mount (initial load, client-side route navigation, back/forward, hot
|
||||
// reload) instead of only working after a hard refresh, where a prior
|
||||
// mount's leftover ScrollTrigger/timeline could otherwise linger or hold
|
||||
// a stale measurement.
|
||||
const ctx = gsap.context(() => {
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
let played = false;
|
||||
|
||||
const isMobileLayout = () => window.matchMedia(`(max-width: ${MOBILE_BQ}px)`).matches;
|
||||
|
||||
const measure = (): { points: Point[]; isMobile: boolean } => {
|
||||
const wrapRect = wrap.getBoundingClientRect();
|
||||
const isMobile = isMobileLayout();
|
||||
|
||||
const points = cardRefs.current
|
||||
.map((card, i) => {
|
||||
const icon = iconRefs.current[i];
|
||||
if (!card || !icon) return null;
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const iconRect = icon.getBoundingClientRect();
|
||||
if (isMobile) {
|
||||
return {
|
||||
x: 12,
|
||||
y: iconRect.top + iconRect.height / 2 - wrapRect.top,
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: iconRect.left + iconRect.width / 2 - wrapRect.left,
|
||||
y: cardRect.top - wrapRect.top - 14,
|
||||
};
|
||||
})
|
||||
.filter((p): p is Point => p !== null);
|
||||
|
||||
svg.setAttribute("width", String(wrapRect.width));
|
||||
svg.setAttribute("height", String(wrapRect.height));
|
||||
svg.setAttribute("viewBox", `0 0 ${wrapRect.width} ${wrapRect.height}`);
|
||||
|
||||
// Wave only applies to the horizontal (non-mobile) layout — mobile
|
||||
// keeps its existing straight vertical line unchanged. Amplitude
|
||||
// scales with the section's width so it stays "gentle" (never a
|
||||
// straight line, never overly aggressive) at any breakpoint.
|
||||
const amplitude = isMobile
|
||||
? 0
|
||||
: Math.max(WAVE_AMPLITUDE_MIN, Math.min(WAVE_AMPLITUDE_MAX, wrapRect.width / 20));
|
||||
const routePoints = isMobile ? points : buildWavePoints(points, amplitude);
|
||||
const d = buildPathD(routePoints);
|
||||
path.setAttribute("d", d);
|
||||
illum.setAttribute("d", d);
|
||||
|
||||
return { points, isMobile };
|
||||
};
|
||||
|
||||
// Keep the (static, or already-settled) line in sync with layout reflow.
|
||||
// Only ever updates geometry — never restarts or replays the animation.
|
||||
ro = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(measure);
|
||||
});
|
||||
ro.observe(wrap);
|
||||
|
||||
// Initial synchronous measurement so `path` has real geometry before any
|
||||
// gsap.set/timeline touches it (ResizeObserver's first callback is async).
|
||||
// Followed by one deferred re-measure: StaggerChildren's own mount effect
|
||||
// (a sibling/child effect) can still have cards translated by their
|
||||
// pre-reveal yOffset at this exact tick, so the very first synchronous
|
||||
// read can be transiently stale — a post-paint rAF corrects it.
|
||||
measure();
|
||||
initialRaf = requestAnimationFrame(() => {
|
||||
measure();
|
||||
// Recalculates every ScrollTrigger's start/end position against the
|
||||
// now-settled layout. Without this, a trigger created a moment before
|
||||
// web fonts/images finish shifting the page can end up with a stale
|
||||
// "top 85%" position that a normal scroll never actually crosses —
|
||||
// the classic reason a GSAP ScrollTrigger works after a hard refresh
|
||||
// (cache-warm, layout already stable) but not after a fresh client-side
|
||||
// navigation.
|
||||
ScrollTrigger.refresh();
|
||||
});
|
||||
|
||||
// Extra safety net: images loading in after this point can still shift
|
||||
// layout. One more refresh once the whole page (including images) has
|
||||
// finished loading catches that — a no-op if `load` already fired.
|
||||
if (document.readyState !== "complete") {
|
||||
loadHandler = () => ScrollTrigger.refresh();
|
||||
window.addEventListener("load", loadHandler, { once: true });
|
||||
}
|
||||
|
||||
const activateCard = (i: number) => {
|
||||
const card = cardRefs.current[i];
|
||||
const icon = iconRefs.current[i];
|
||||
if (!card) return;
|
||||
// Arrival: border gently strengthens, ambient glow grows, icon
|
||||
// brightens, card lifts a touch (CSS-driven, eased over ~0.5s — see
|
||||
// .wwg-card / .wwg-card-glow transition durations below — so it grows
|
||||
// and settles rather than snapping on).
|
||||
card.classList.add("wwg-card-flown", "wwg-card-pulse");
|
||||
// Icon's own gentle scale pulse starts a beat after the arrival
|
||||
// begins, so the sequence reads as "arrives, then responds" rather
|
||||
// than everything happening at once.
|
||||
if (icon) {
|
||||
gsap.timeline({ delay: 0.28 })
|
||||
.to(icon, { scale: 1.02, duration: 0.32, ease: "expo.out" })
|
||||
.to(icon, { scale: 1, duration: 0.32, ease: "sine.inOut" });
|
||||
}
|
||||
// Hold the arrival state, then ease into the subtler, persistent
|
||||
// "flown" resting state.
|
||||
gsap.delayedCall(0.95, () => card.classList.remove("wwg-card-pulse"));
|
||||
};
|
||||
|
||||
const runSequence = () => {
|
||||
if (played) return;
|
||||
played = true;
|
||||
|
||||
const { points, isMobile } = measure();
|
||||
if (points.length < 2) return;
|
||||
|
||||
const restOpacity = isMobile ? 0.15 : 0.18;
|
||||
const total = path.getTotalLength();
|
||||
// The illuminated window trailing the pulse: roughly a fifth of the
|
||||
// route's length, so it reads as a genuine glowing trail rather than
|
||||
// a small blip or the entire line at once.
|
||||
const trailLen = total * 0.22;
|
||||
const illumDashGap = total + 200;
|
||||
illum.setAttribute("stroke-dasharray", `${trailLen} ${illumDashGap}`);
|
||||
const fractions = points.map((pt) => {
|
||||
let best = 0;
|
||||
let bestDist = Infinity;
|
||||
const step = Math.max(2, total / 400);
|
||||
for (let l = 0; l <= total; l += step) {
|
||||
const p = path.getPointAtLength(l);
|
||||
const dist = (p.x - pt.x) ** 2 + (p.y - pt.y) ** 2;
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = l;
|
||||
}
|
||||
}
|
||||
return best / total;
|
||||
});
|
||||
const fired = fractions.map(() => false);
|
||||
|
||||
let currentAngle = 0;
|
||||
let angleInit = false;
|
||||
|
||||
const tl = gsap.timeline();
|
||||
playedTimeline = tl;
|
||||
|
||||
// The energy pulse's own journey — calm and deliberate, ~4.8-5.5s so
|
||||
// it glides rather than rushes from the first card to the last.
|
||||
const FLIGHT_DURATION = 5.2;
|
||||
// The route draws slightly faster than the pulse travels, both
|
||||
// starting together at t=0 — so the drawn extent always leads the
|
||||
// pulse's position (never the other way round) and the whole route
|
||||
// is guaranteed fully illuminated well before the pulse's own journey
|
||||
// ends, while both still read as one continuous, synchronized motion.
|
||||
const DRAW_DURATION = FLIGHT_DURATION * 0.9;
|
||||
|
||||
if (hasDrawSVG) {
|
||||
tl.to(path, { drawSVG: "100%", duration: DRAW_DURATION, ease: "power2.inOut" }, 0);
|
||||
} else {
|
||||
gsap.set(path, { strokeDasharray: total, strokeDashoffset: total });
|
||||
tl.to(path, { strokeDashoffset: 0, duration: DRAW_DURATION, ease: "power2.inOut" }, 0);
|
||||
}
|
||||
// The base route ramps straight to its final resting opacity as it
|
||||
// draws — never to an elevated "traveling" brightness — so there is
|
||||
// only ever one illuminated layer on screen: the bright illum trail
|
||||
// riding with the pulse (below). Without this, the whole drawn route
|
||||
// would sit at a visibly elevated opacity for the entire flight,
|
||||
// reading as a second, competing "lit" route behind the pulse.
|
||||
tl.to(path, { opacity: restOpacity, duration: 0.9, ease: "sine.out" }, 0);
|
||||
|
||||
// GSAP computes its own rotation pivot for SVG elements from the
|
||||
// target's bounding box (stamped as a data-svg-origin attribute) and
|
||||
// ignores plain CSS transform-origin for that purpose — so without
|
||||
// this, both directional groups were actually rotating around their
|
||||
// bbox corner instead of (0,0), visibly orbiting off the path's
|
||||
// centerline as they turned. svgOrigin is GSAP's own equivalent,
|
||||
// stated in the element's literal SVG coordinates (not bbox-relative
|
||||
// percentages), and only needs setting once — the per-frame
|
||||
// rotation-only gsap.set calls below then reuse it automatically.
|
||||
gsap.set([directional, aircraftDirectional], { svgOrigin: "0 0" });
|
||||
|
||||
// A soft fade-in rather than an instant pop, so the pulse's arrival
|
||||
// reads as one continuous gesture from the very first frame.
|
||||
tl.to(traveler, { opacity: 1, duration: 0.3, ease: "sine.out" }, 0);
|
||||
tl.to(illum, { opacity: 1, duration: 0.4, ease: "sine.out" }, 0);
|
||||
tl.to(traveler, {
|
||||
// alignOrigin locks the traveler's own center (not a default
|
||||
// corner/bbox reference) to the path, matching the explicit
|
||||
// transform-origin: 50% 50% above — keeps the pulse/aircraft
|
||||
// precisely on the route's centerline with no vertical drift.
|
||||
motionPath: { path, autoRotate: false, alignOrigin: [0.5, 0.5] },
|
||||
duration: FLIGHT_DURATION,
|
||||
ease: "power2.inOut",
|
||||
onUpdate: function () {
|
||||
const p = this.progress();
|
||||
const l = p * total;
|
||||
const eps = Math.max(1, total * 0.01);
|
||||
const a = path.getPointAtLength(Math.max(0, l - eps));
|
||||
const b = path.getPointAtLength(Math.min(total, l + eps));
|
||||
const target = (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI + AIRCRAFT_ROTATION_OFFSET;
|
||||
|
||||
if (!angleInit) {
|
||||
currentAngle = target;
|
||||
angleInit = true;
|
||||
} else {
|
||||
let delta = target - currentAngle;
|
||||
// shortest angular path so the lerp never spins the long way round
|
||||
delta = ((delta + 180) % 360 + 360) % 360 - 180;
|
||||
currentAngle += delta * 0.12;
|
||||
}
|
||||
// Only the two directional groups (trail + halo, and the aircraft)
|
||||
// rotate — the core stays a perfectly round point of light on the
|
||||
// path. Both are set to the same angle every frame so they always
|
||||
// face the same way despite being separate DOM branches (see
|
||||
// aircraftDirectionalRef for why they're split).
|
||||
gsap.set(directional, { rotation: currentAngle });
|
||||
gsap.set(aircraftDirectional, { rotation: currentAngle });
|
||||
|
||||
// Slide the illuminated window's trailing edge to l - trailLen and
|
||||
// its leading edge to l (i.e. right under the pulse) by shifting
|
||||
// the dash pattern's phase — the CSS blur on .wwg-illum-trail turns
|
||||
// those two hard edges into a soft glow that fades into the resting
|
||||
// route, rather than a sharp on/off cut.
|
||||
illum.setAttribute("stroke-dashoffset", String(trailLen - l));
|
||||
|
||||
fractions.forEach((f, i) => {
|
||||
if (!fired[i] && p >= f) {
|
||||
fired[i] = true;
|
||||
activateCard(i);
|
||||
}
|
||||
});
|
||||
},
|
||||
}, 0);
|
||||
|
||||
// Only once the pulse's own journey is complete does it fade out —
|
||||
// the route itself is already at its resting opacity by this point
|
||||
// (see above), so nothing further happens to it here.
|
||||
tl.to(traveler, { opacity: 0, duration: 0.6, ease: "sine.out" }, FLIGHT_DURATION);
|
||||
tl.to(illum, { opacity: 0, duration: 0.6, ease: "sine.out" }, FLIGHT_DURATION);
|
||||
};
|
||||
|
||||
if (reduced) {
|
||||
const { isMobile } = measure();
|
||||
gsap.set(path, { opacity: isMobile ? 0.15 : 0.18 });
|
||||
if (hasDrawSVG) {
|
||||
gsap.set(path, { drawSVG: "100%" });
|
||||
} else {
|
||||
gsap.set(path, { strokeDasharray: "none", strokeDashoffset: 0 });
|
||||
}
|
||||
gsap.set(traveler, { opacity: 0 });
|
||||
gsap.set(illum, { opacity: 0 });
|
||||
} else {
|
||||
gsap.set(path, { opacity: 0 });
|
||||
if (hasDrawSVG) {
|
||||
gsap.set(path, { drawSVG: "0%" });
|
||||
}
|
||||
gsap.set(traveler, { opacity: 0 });
|
||||
gsap.set(illum, { opacity: 0 });
|
||||
|
||||
// Starts as soon as the section is mostly in view (matches the cards'
|
||||
// own "top 85%" stagger-reveal trigger) rather than waiting for it to
|
||||
// be ~centered — the ~6s draw+flight sequence needs that extra lead
|
||||
// time to actually be seen during a normal scroll pass, not just
|
||||
// triggered and then scrolled straight past.
|
||||
st = ScrollTrigger.create({
|
||||
trigger: wrap,
|
||||
start: "top 85%",
|
||||
once: true,
|
||||
onEnter: runSequence,
|
||||
});
|
||||
|
||||
rafIn = requestAnimationFrame(() => {
|
||||
const r = wrap.getBoundingClientRect();
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight;
|
||||
if (r.top < vh * 0.85 && r.bottom > 0) {
|
||||
runSequence();
|
||||
st?.kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, wrap);
|
||||
|
||||
return () => {
|
||||
ro?.disconnect();
|
||||
cancelAnimationFrame(resizeRaf);
|
||||
cancelAnimationFrame(initialRaf);
|
||||
cancelAnimationFrame(rafIn);
|
||||
if (loadHandler) window.removeEventListener("load", loadHandler);
|
||||
st?.kill();
|
||||
playedTimeline?.kill();
|
||||
// Reverts/kills everything gsap.context tracked (ScrollTrigger.create,
|
||||
// gsap.set calls, etc.) as a safety net beyond the explicit kills
|
||||
// above, which exist specifically because the flight timeline is
|
||||
// created asynchronously (inside onEnter) and so isn't guaranteed to
|
||||
// be tracked by the context itself.
|
||||
ctx.revert();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style
|
||||
@@ -56,7 +490,7 @@ export default function WingsWhyGrid() {
|
||||
__html: `
|
||||
.wwg-section {
|
||||
background: #ffffff;
|
||||
padding: var(--space-section-lg, 80px) clamp(20px, 5vw, 40px);
|
||||
padding: var(--space-section-lg, 80px) 0;
|
||||
}
|
||||
|
||||
.wwg-header {
|
||||
@@ -84,21 +518,129 @@ export default function WingsWhyGrid() {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wwg-grid {
|
||||
max-width: 1280px;
|
||||
.wwg-inner {
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wwg-grid-wrap {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.wwg-flightpath {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.wwg-flightpath-line {
|
||||
fill: none;
|
||||
stroke: #c8102e;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* A brighter, blurred copy of the same route, painted *behind* the
|
||||
crisp foreground line (see JSX order). Its dasharray is driven
|
||||
per-frame (see onUpdate) to keep a soft window of it trailing
|
||||
directly beneath the pulse; because it sits behind rather than on
|
||||
top, its blur reads as a glow radiating from under the route
|
||||
instead of visually thickening the route's own edges. Stroke
|
||||
width intentionally matches the base route so it never peeks out
|
||||
wider than the crisp line drawn on top of it. */
|
||||
.wwg-illum-trail {
|
||||
fill: none;
|
||||
stroke: #ff3d5a;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
filter: blur(5px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* The traveler is the primary visual: a single centered optical
|
||||
pulse riding the route, with a small secondary aircraft glyph
|
||||
carried inside it. Position (translate) lives on .wwg-traveler;
|
||||
only the inner .wwg-traveler-directional group rotates to face
|
||||
the direction of travel — the round pulse core stays outside it
|
||||
so it never rotates, always reading as a clean point of light. */
|
||||
.wwg-traveler {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
/* Explicit center alignment (paired with alignOrigin: [0.5, 0.5]
|
||||
on the motionPath tween) so the traveler's own center — not a
|
||||
corner/default reference point — is what MotionPathPlugin locks
|
||||
to the route, keeping it precisely on the path's centerline
|
||||
with no vertical drift. */
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.wwg-traveler-directional {
|
||||
transform-origin: 0px 0px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Large, radially-symmetric ambient glow — the true hero of the
|
||||
traveler. Centered on the pulse with no directional offset, so
|
||||
there is never a brighter patch leading ahead of the aircraft.
|
||||
Non-directional, so it lives outside the rotating
|
||||
.wwg-traveler-directional group. */
|
||||
.wwg-pulse-bloom {
|
||||
fill: #c8102e;
|
||||
opacity: 0.2;
|
||||
filter: blur(18px);
|
||||
}
|
||||
|
||||
.wwg-pulse-halo {
|
||||
fill: #c8102e;
|
||||
opacity: 0.52;
|
||||
filter: blur(7px);
|
||||
}
|
||||
|
||||
.wwg-pulse-core {
|
||||
fill: #ffffff;
|
||||
filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.98)) drop-shadow(0 0 9px rgba(200, 16, 46, 0.85))
|
||||
drop-shadow(0 0 15px rgba(200, 16, 46, 0.5));
|
||||
}
|
||||
|
||||
/* Deliberately no drop-shadow of its own — the bloom/halo/trail
|
||||
above supply all the glow. Sized to be clearly recognizable as an
|
||||
aircraft without becoming the focal point: the pulse is still the
|
||||
brightest, largest element on screen at every moment. */
|
||||
.wwg-aircraft {
|
||||
color: #c8102e;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.wwg-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.wwg-card {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding: 30px 24px;
|
||||
border: 1px solid rgba(17, 17, 17, 0.08);
|
||||
border-radius: 18px;
|
||||
background: #fafafa;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
|
||||
/* Staggered delays so activation reads as a cascade — glow moves
|
||||
first (see .wwg-card-glow below, no delay), then border/shape,
|
||||
then the background tint settles last — rather than every
|
||||
property snapping at once. The "expo-out" curve (rather than
|
||||
plain ease) is what gives it a soft, premium settle instead of a
|
||||
mechanical linear-ish snap. */
|
||||
transition:
|
||||
border-color 0.75s cubic-bezier(0.16, 1, 0.3, 1) 0.08s,
|
||||
box-shadow 0.75s cubic-bezier(0.16, 1, 0.3, 1) 0.08s,
|
||||
transform 0.75s cubic-bezier(0.16, 1, 0.3, 1) 0.08s,
|
||||
background-color 0.75s cubic-bezier(0.16, 1, 0.3, 1) 0.22s;
|
||||
}
|
||||
|
||||
.wwg-card:hover {
|
||||
@@ -107,6 +649,37 @@ export default function WingsWhyGrid() {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.wwg-card-glow {
|
||||
position: absolute;
|
||||
inset: -12px;
|
||||
border-radius: 22px;
|
||||
background: radial-gradient(circle, rgba(200, 16, 46, 0.45) 0%, rgba(200, 16, 46, 0) 70%);
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
filter: blur(6px);
|
||||
transition: opacity 0.75s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.wwg-card-flown {
|
||||
border-color: rgba(200, 16, 46, 0.25);
|
||||
background-color: #fdf7f8;
|
||||
}
|
||||
|
||||
.wwg-card-flown .wwg-card-glow {
|
||||
opacity: 0.32;
|
||||
}
|
||||
|
||||
.wwg-card-pulse {
|
||||
border-color: rgba(200, 16, 46, 0.5);
|
||||
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.07);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.wwg-card-pulse .wwg-card-glow {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.wwg-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
@@ -117,6 +690,18 @@ export default function WingsWhyGrid() {
|
||||
background: rgba(200, 16, 46, 0.06);
|
||||
color: #c8102e;
|
||||
margin-bottom: 20px;
|
||||
transition:
|
||||
background-color 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.16s,
|
||||
filter 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.16s;
|
||||
}
|
||||
|
||||
.wwg-card-flown .wwg-icon {
|
||||
background: rgba(200, 16, 46, 0.1);
|
||||
}
|
||||
|
||||
.wwg-card-pulse .wwg-icon {
|
||||
background: rgba(200, 16, 46, 0.14);
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.wwg-card-title {
|
||||
@@ -140,27 +725,101 @@ export default function WingsWhyGrid() {
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.wwg-grid { grid-template-columns: 1fr; }
|
||||
.wwg-grid { grid-template-columns: 1fr; padding-left: 28px; }
|
||||
/* var(--space-section-lg) floors at a fixed 52px on any phone
|
||||
width; trimmed to a real mobile value to match the section
|
||||
rhythm near the footer. Matches this file's own MOBILE_BQ
|
||||
breakpoint used by the flight-path animation logic. */
|
||||
.wwg-section { padding-top: 44px; padding-bottom: 44px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wwg-card, .wwg-card-glow, .wwg-icon {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<section className="wwg-section" id="wings-why">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wwg-header">
|
||||
<div className="wwg-eyebrow">/ Why Doormile Wings /</div>
|
||||
<h2 className="wwg-title">Built for the speed of the Gen-Z economy</h2>
|
||||
</ScrollReveal>
|
||||
<div className="dm-section-container">
|
||||
<div className="wwg-inner">
|
||||
<ScrollReveal delay={0.05} duration={0.75} yOffset={25} className="wwg-header">
|
||||
<div className="wwg-eyebrow">/ Why Doormile Wings /</div>
|
||||
<h2 className="wwg-title">Built for the speed of the Gen-Z economy</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<StaggerChildren stagger={0.1} duration={0.7} yOffset={30} className="wwg-grid">
|
||||
{FEATURES.map((f) => (
|
||||
<div key={f.title} className="wwg-card">
|
||||
<div className="wwg-icon" aria-hidden="true">{f.icon}</div>
|
||||
<h3 className="wwg-card-title">{f.title}</h3>
|
||||
<p className="wwg-card-desc">{f.desc}</p>
|
||||
<div className="wwg-grid-wrap" ref={wrapRef}>
|
||||
<svg ref={svgRef} className="wwg-flightpath" aria-hidden="true" focusable="false">
|
||||
{/* Rendered first (behind) so its blur/extra width reads as a
|
||||
soft glow sitting under the route rather than visually
|
||||
thickening it — the crisp .wwg-flightpath-line painted
|
||||
after it always defines the route's actual edges. */}
|
||||
<path ref={illumRef} className="wwg-illum-trail" d="" />
|
||||
<path ref={pathRef} className="wwg-flightpath-line" d="" />
|
||||
<g ref={travelerRef} className="wwg-traveler">
|
||||
{/* Large feathered bloom: the true hero of the traveler, an
|
||||
ambient glow with no direction of its own. */}
|
||||
<circle className="wwg-pulse-bloom" cx="0" cy="0" r="26" />
|
||||
<g ref={directionalRef} className="wwg-traveler-directional">
|
||||
<ellipse className="wwg-pulse-halo" cx="0" cy="0" rx="11" ry="6" />
|
||||
</g>
|
||||
<circle className="wwg-pulse-core" cx="0" cy="0" r="2.6" />
|
||||
{/* Painted after (on top of) the core so the aircraft's
|
||||
silhouette stays readable instead of being buried under
|
||||
the bright core dot. Rotation mirrors directionalRef
|
||||
exactly (see onUpdate) despite living in its own group. */}
|
||||
<g ref={aircraftDirectionalRef} className="wwg-traveler-directional">
|
||||
{/* The stock glyph's original 24x24 viewBox had ~3.5
|
||||
units of dead padding on every side around the actual
|
||||
ink (true silhouette bbox was only 17x20) — scaling
|
||||
from the nominal 24-unit box would have kept baking
|
||||
that padding in and rendering smaller than intended.
|
||||
These coordinates instead start from the silhouette's
|
||||
own tight bbox (no padding), centered on (0,0), then
|
||||
scaled 1.3x — giving a true rendered size of ~22x26px,
|
||||
clearing 22px on both axes. Deliberately no
|
||||
scale()/translate() transform: baked directly into
|
||||
the path data so it renders at full native
|
||||
resolution rather than through a transform stacked on
|
||||
top of this group's own rotation transform. */}
|
||||
<path
|
||||
className="wwg-aircraft"
|
||||
d="M0,-13 C-0.78,-13 -1.3,-12.22 -1.3,-11.31 L-1.3,-3.9 L-11.05,1.3 L-11.05,3.9 L-1.3,0.91 L-1.3,9.1 L-4.55,11.44 L-4.55,13 L0,11.7 L4.55,13 L4.55,11.44 L1.3,9.1 L1.3,0.91 L11.05,3.9 L11.05,1.3 L1.3,-3.9 L1.3,-11.31 C1.3,-12.22 0.78,-13 0,-13 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<StaggerChildren stagger={0.1} duration={0.7} yOffset={30} className="wwg-grid">
|
||||
{FEATURES.map((f, i) => (
|
||||
<div
|
||||
key={f.title}
|
||||
className="wwg-card"
|
||||
ref={(el) => {
|
||||
cardRefs.current[i] = el;
|
||||
}}
|
||||
>
|
||||
<span className="wwg-card-glow" aria-hidden="true" />
|
||||
<div
|
||||
className="wwg-icon"
|
||||
aria-hidden="true"
|
||||
ref={(el) => {
|
||||
iconRefs.current[i] = el;
|
||||
}}
|
||||
>
|
||||
{f.icon}
|
||||
</div>
|
||||
<h3 className="wwg-card-title">{f.title}</h3>
|
||||
<p className="wwg-card-desc">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</StaggerChildren>
|
||||
</div>
|
||||
))}
|
||||
</StaggerChildren>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user