Files
doormile_react/src/components/strategy/StrategySection.tsx
2026-07-20 13:33:30 +05:30

501 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { STAGES, N } from "./theme";
const StrategyCanvas = dynamic(() => import("./StrategyCanvas"), { ssr: false });
/** Center of each stage's scroll window (0…1). */
const CENTER = (i: number) => i / (N - 1);
/** Persistent top rail: the 5 stages, current one highlighted. */
function StageRail({ active }: { active: number }) {
return (
<div className="dm-st-rail" aria-hidden>
{STAGES.map((s, i) => {
const state = i < active ? "done" : i === active ? "current" : "todo";
return (
<React.Fragment key={s.n}>
{i > 0 && <span className={`dm-st-rail__line is-${i <= active ? "on" : "off"}`} />}
<div className={`dm-st-rail__step is-${state}`} style={{ ["--c" as string]: s.theme }}>
<span className="dm-st-rail__num">{i < active ? "✓" : s.n}</span>
<span className="dm-st-rail__title">{s.kicker}</span>
</div>
</React.Fragment>
);
})}
</div>
);
}
/** A cross-fading glass content card pinned to one side, themed per stage. */
function StageCard({
i,
scroll,
side,
active,
children,
}: {
i: number;
scroll: MotionValue<number>;
side: "left" | "right";
active: number;
children: React.ReactNode;
}) {
const c = CENTER(i);
// Window kept < the 0.25 per-stage span (5 stages) so two adjacent stage cards
// never overlap — one stage's card is fully out before the next fades in.
const opacity = useTransform(scroll, [c - 0.1, c - 0.05, c + 0.05, c + 0.1], [0, 1, 1, 0]);
const y = useTransform(scroll, [c - 0.1, c - 0.05], [34, 0]);
const s = STAGES[i];
// Only mount the card while its stage is active.
if (active !== i) return null;
return (
<motion.div
className={`dm-st-card-story is-${side}`}
style={{ opacity, y, ["--c" as string]: s.theme }}
>
<div className="dm-st-card-story__head">
<span className="dm-st-pillar__num">{s.n}</span>
<span className="dm-st-pillar__kicker">{s.kicker}</span>
</div>
{children}
</motion.div>
);
}
/**
* "Strategy" — a premium, Apple-keynote-style 3D scroll-storytelling section.
* A single GSAP ScrollTrigger maps scroll to a normalized progress that drives
* the R3F camera through five floating glass stages while DOM glass cards
* cross-fade in lockstep. Pins via a self-managed fixed element (the site's
* fixed header + an ancestor `overflow:hidden` break CSS sticky / GSAP pin).
*/
export default function StrategySection({ connected = false }: { connected?: boolean } = {}) {
const containerRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
const scroll = useMotionValue(0);
const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before");
const [active, setActive] = useState(0);
const [mountScene, setMountScene] = useState(false);
const [sceneReady, setSceneReady] = useState(false);
const [showLoader, setShowLoader] = useState(true);
const [sceneActive, setSceneActive] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mqMobile = window.matchMedia("(max-width: 767px)");
const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)");
const sync = () => { setIsMobile(mqMobile.matches); setReduced(mqReduce.matches); };
sync();
mqMobile.addEventListener("change", sync);
mqReduce.addEventListener("change", sync);
return () => { mqMobile.removeEventListener("change", sync); mqReduce.removeEventListener("change", sync); };
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const mountIo = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setMountScene(true);
setSceneActive(true);
mountIo.disconnect();
}
},
{ rootMargin: "70% 0px" },
);
const activeIo = new IntersectionObserver(
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
{ rootMargin: "10% 0px" },
);
mountIo.observe(el);
activeIo.observe(el);
return () => { mountIo.disconnect(); activeIo.disconnect(); };
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
gsap.registerPlugin(ScrollTrigger);
let lastPin: "before" | "pinned" | "after" = "before";
let lastActive = 0;
const st = ScrollTrigger.create({
trigger: el,
start: "top top",
end: "bottom bottom",
// Match Workflow 1's responsiveness (0.4) for consistent pacing across all
// three workflows — 0.5 made the dolly feel slower / disconnected here.
scrub: 0.4,
invalidateOnRefresh: true,
onUpdate: (self) => {
const p = self.progress;
progressRef.current = p;
scroll.set(p);
const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned";
if (ns !== lastPin) { lastPin = ns; setPinState(ns); }
const na = Math.round(p * (N - 1));
if (na !== lastActive) { lastActive = na; setActive(na); }
},
});
const refresh = setTimeout(() => ScrollTrigger.refresh(), 120);
return () => { clearTimeout(refresh); st.kill(); };
}, [scroll]);
// Top-level safety fallback: if canvas does not report ready within 4 seconds of mounting, force it.
useEffect(() => {
if (!mountScene) return;
const timer = setTimeout(() => {
setSceneReady((ready) => {
if (!ready) {
console.warn("StrategySection: Scene readiness fallback triggered.");
return true;
}
return ready;
});
}, 4000);
return () => clearTimeout(timer);
}, [mountScene]);
// Reliable loader cleanup: force showLoader(false) 300ms after sceneReady becomes true
useEffect(() => {
if (sceneReady) {
const timer = setTimeout(() => {
setShowLoader(false);
}, 300);
return () => clearTimeout(timer);
}
}, [sceneReady]);
// Intro hint fades out as the journey begins.
const introOpacity = useTransform(scroll, [0, 0.03, 0.06], [1, 1, 0]);
// Persistent header fades in after the intro.
const headerOpacity = useTransform(scroll, [0.02, 0.07], [0, 1]);
return (
<section
ref={containerRef}
className={`dm-st is-${pinState}${connected ? " is-connected" : ""}`}
aria-label="Strategy — Happier Fleet Drivers. Higher Fulfillment."
>
<div className="dm-st-sticky">
<div className="dm-st-card">
{mountScene && (
<div className={`dm-st-canvas${sceneReady ? " is-ready" : ""}`} aria-hidden={!sceneReady}>
<StrategyCanvas
progress={progressRef}
reduced={reduced}
isMobile={isMobile}
active={sceneActive && pinState === "pinned"}
stage={active}
ready={sceneReady}
onReady={() => setSceneReady(true)}
/>
</div>
)}
{showLoader && (
<div
className={`dm-st-loader${sceneReady ? " is-hiding" : ""}`}
role="status"
aria-live="polite"
aria-label="Loading MileTruth Strategy Engine"
>
<span className="dm-st-loader__ring" />
<span className="dm-st-loader__text">Loading strategy engine...</span>
</div>
)}
{/* Overlay mounts only once the section is pinned/activated — its content
can never be seen during the approach ("before"), where the sticky sits
at the top of the tall section near the previous workflow's seam. */}
{pinState !== "before" && (
<div className={`dm-st-ui${sceneReady ? " is-ready" : ""}`} aria-hidden={!sceneReady}>
{/* Persistent header */}
<motion.div className="dm-st-top" style={{ opacity: headerOpacity }}>
<div className="dm-st-eyebrow"><span className="dm-st-dot" /> MileTruth Strategy Engine</div>
<StageRail active={active} />
</motion.div>
{/* Intro hint */}
<motion.div className="dm-st-scrollhint" style={{ opacity: introOpacity }}>
<span>Scroll to follow the strategy</span>
<span className="dm-st-arrow"></span>
</motion.div>
{/* Side cards are now light text anchors — the 3D world carries the
detail. Each: short title + one lead line + a couple of key chips. */}
{/* STAGE 01 — INPUT (green) */}
<StageCard i={0} scroll={scroll} side="left" active={active}>
<h3 className="dm-st-pillar__title">Orders &amp; fleet drivers enter the system</h3>
<p className="dm-st-anchor__lead">Orders are uploaded and matched against the available fleet, ready for assignment.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">59 Orders</span>
<span className="dm-st-anchor__chip">4 Drivers</span>
<span className="dm-st-anchor__chip">Fleet ready</span>
</div>
</StageCard>
{/* STAGE 02 — PARALLEL EXECUTION (purple) */}
<StageCard i={1} scroll={scroll} side="right" active={active}>
<h3 className="dm-st-pillar__title">Six strategies, evaluated in parallel</h3>
<p className="dm-st-anchor__lead">The AI runs every routing strategy at the same time legacy baselines and MileTruth&apos;s unified engine.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">EV Aware</span>
<span className="dm-st-anchor__chip">Multi Trip</span>
<span className="dm-st-anchor__chip">+4 more</span>
</div>
</StageCard>
{/* STAGE 03 — SMART OPTIMIZATION (blue) */}
<StageCard i={2} scroll={scroll} side="left" active={active}>
<h3 className="dm-st-pillar__title">Routes optimized &amp; validated</h3>
<p className="dm-st-anchor__lead">Every route is solved for distance, then checked against battery range and service SLAs.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">Optimize</span>
<span className="dm-st-anchor__chip">Battery</span>
<span className="dm-st-anchor__chip">SLA</span>
</div>
</StageCard>
{/* STAGE 04 — PERFORMANCE GRADING (orange) */}
<StageCard i={3} scroll={scroll} side="right" active={active}>
<h3 className="dm-st-pillar__title">Every strategy is scored</h3>
<p className="dm-st-anchor__lead">Each strategy is graded live on fulfillment, SLA compliance, efficiency and battery feasibility.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip">Grade A</span>
<span className="dm-st-anchor__chip">88% Fulfillment</span>
<span className="dm-st-anchor__chip">95% SLA</span>
</div>
</StageCard>
{/* STAGE 05 — STRATEGY COMPARISON (red, hero) */}
<StageCard i={4} scroll={scroll} side="right" active={active}>
<h3 className="dm-st-pillar__title">Happier fleet drivers. Higher fulfillment.</h3>
<p className="dm-st-anchor__lead">EV Aware wins the best fulfillment with feasible, battery-safe routes for every fleet driver.</p>
<div className="dm-st-anchor__chips">
<span className="dm-st-anchor__chip dm-st-anchor__chip--win">🏆 EV Aware</span>
<span className="dm-st-anchor__chip">88% Score</span>
<span className="dm-st-anchor__chip">52/59 Fulfilled</span>
</div>
</StageCard>
</div>
)}
</div>
</div>
<style>{styles}</style>
</section>
);
}
const styles = `
/* Scroll length tuned for pacing: ~100vh per stage (was 144vh) so the 5 stages
complete in noticeably less scrolling and the workflow feels tighter / faster.
Stage cross-fade windows are progress-based (0…1), so they stay aligned. */
.dm-st { position: relative; height: 500vh; background: transparent; }
.dm-st-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden;
will-change: transform; transform: translateZ(0); backface-visibility: hidden; }
.dm-st.is-pinned .dm-st-sticky { position: fixed; top: 0; left: 0; }
.dm-st.is-after .dm-st-sticky { position: absolute; top: auto; bottom: 0; }
.dm-st-card {
position: absolute !important; inset: 20px !important;
border-radius: 28px !important; overflow: hidden !important;
background: radial-gradient(120% 100% at 50% 0%, #ffffff 0%, #eef1f6 60%, #e6eaf2 100%) !important;
border: 1px solid rgba(15,23,42,0.08) !important;
box-shadow: 0 30px 90px -34px rgba(15,23,42,0.4) !important;
box-sizing: border-box !important;
}
@media (max-width: 767px) { .dm-st-card { inset: 10px !important; border-radius: 20px !important; } }
/* Connected mode (inside Workflow 3): flatten the card's bottom so the Strategy
content card below butts directly against it — same seam as Workflow 1 & 2. */
.dm-st.is-connected .dm-st-card {
top: 20px !important; left: 20px !important; right: 20px !important; bottom: 0 !important;
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
/* Flush against the Strategy card below — drop the heavy downward shadow so it
doesn't cast a dark band onto that card's top edge (the two read as one container). */
box-shadow: none !important;
}
@media (max-width: 767px) {
.dm-st.is-connected .dm-st-card {
top: 10px !important; left: 10px !important; right: 10px !important; bottom: 0 !important;
border-radius: 20px 20px 0 0 !important;
}
}
.dm-st-canvas { position: absolute; inset: 0; z-index: 1; opacity: 0; visibility: hidden;
transition: opacity 0.25s cubic-bezier(0.22,1,0.36,1), visibility 0s linear 0.25s; }
.dm-st-canvas.is-ready { opacity: 1; visibility: visible; transition-delay: 0s; }
.dm-st-canvas canvas { display: block; }
.dm-st-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none;
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; color: #0f172a;
opacity: 0; visibility: hidden;
transition: opacity 0.25s cubic-bezier(0.22,1,0.36,1), visibility 0s linear 0.25s; }
.dm-st-ui.is-ready { opacity: 1; visibility: visible; transition-delay: 0s; }
.dm-st-loader { position: absolute; top: 24px; left: 50%; transform: translateX(-50%); z-index: 6;
display: flex; align-items: center; gap: 8px;
background: rgba(15,23,42,0.85); border: 1px solid rgba(255,255,255,0.08);
padding: 8px 14px; border-radius: 999px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
opacity: 1; pointer-events: none;
animation: dmStLoaderFadeIn 0.2s ease both;
transition: opacity 0.25s ease; }
.dm-st-loader.is-hiding { opacity: 0; pointer-events: none; }
.dm-st-loader__ring { width: 14px; height: 14px; border-radius: 50%;
border: 2px solid rgba(255,255,255,0.2); border-top-color: #00E5FF;
animation: dm-hiw-spin 0.8s linear infinite; box-sizing: border-box; }
.dm-st-loader__text { font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
color: #ffffff; font-size: 13.5px; font-weight: 500; opacity: 0.75; white-space: nowrap; }
@keyframes dmStLoaderFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes dm-hiw-spin { to { transform: rotate(360deg); } }
/* ---- Persistent header: title + 5-stage rail ---- */
.dm-st-top { position: absolute; top: clamp(96px, 13vh, 128px); left: 0; right: 0; z-index: 5;
display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 0 16px; }
.dm-st-eyebrow { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1.35;
letter-spacing: 0.28em; text-transform: uppercase; color: #475569; padding: 9px 18px; border-radius: 999px;
background: rgba(255,255,255,0.72); border: 1px solid rgba(15,23,42,0.08); backdrop-filter: blur(10px); white-space: nowrap; }
.dm-st-dot { width: 6px; height: 6px; border-radius: 50%; background: #6366f1; box-shadow: 0 0 10px #6366f1; }
.dm-st-rail { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; max-width: 980px; }
.dm-st-rail__step { display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px; border-radius: 999px;
background: rgba(255,255,255,0.7); border: 1px solid rgba(15,23,42,0.08); backdrop-filter: blur(8px);
transition: all 0.45s cubic-bezier(0.22,1,0.36,1); }
.dm-st-rail__num { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 800; color: #64748b; background: rgba(15,23,42,0.06); }
.dm-st-rail__title { font-size: 11px; font-weight: 600; letter-spacing: 0.02em; color: #64748b; white-space: nowrap; }
.dm-st-rail__step.is-current { background: color-mix(in srgb, var(--c) 16%, white); border-color: var(--c);
box-shadow: 0 0 22px -6px var(--c); }
.dm-st-rail__step.is-current .dm-st-rail__num { background: var(--c); color: #fff; }
.dm-st-rail__step.is-current .dm-st-rail__title { color: #0f172a; }
.dm-st-rail__step.is-done .dm-st-rail__num { background: #22C55E; color: #fff; }
.dm-st-rail__step.is-done .dm-st-rail__title { color: #334155; }
.dm-st-rail__line { width: 14px; height: 1px; background: rgba(15,23,42,0.14); margin: 0 3px; transition: background 0.45s ease; }
.dm-st-rail__line.is-on { background: var(--c, #22C55E); }
.dm-st-scrollhint { position: absolute; bottom: clamp(26px, 6vh, 60px); left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 12px; letter-spacing: 0.12em;
color: #64748b; text-transform: uppercase; text-align: center; }
.dm-st-arrow { font-size: 18px; animation: dmStBob 1.8s ease-in-out infinite; }
@keyframes dmStBob { 0%,100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(6px); opacity: 1; } }
/* ---- Per-stage glass content card ---- */
.dm-st-card-story { position: absolute; bottom: clamp(24px, 6vh, 64px); width: min(484px, 88vw);
pointer-events: auto; will-change: opacity, transform; padding: 20px 22px; border-radius: 20px;
background: rgba(255,255,255,0.94); border: 1px solid rgba(15,23,42,0.08);
/* backdrop blur removed — card cross-fades/translates per scroll-stage; blur was the
heaviest per-frame cost on this section. Near-opaque white keeps the glass look. */
border-top: 3px solid var(--c);
box-shadow: 0 28px 70px -34px rgba(15,23,42,0.5); }
.dm-st-card-story.is-left { left: clamp(18px, 5vw, 72px); }
.dm-st-card-story.is-right { right: clamp(18px, 5vw, 72px); }
.dm-st-card-story__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.dm-st-pillar__num { font-size: 12px; font-weight: 800; letter-spacing: 0.1em; color: #fff;
background: var(--c); border-radius: 7px; padding: 3px 8px; }
.dm-st-pillar__kicker { font-size: clamp(11px, 1.1vw, 13px); font-weight: 700; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--c); }
.dm-st .dm-st-pillar__title { margin: 0 0 14px !important; padding: 0 !important; color: #0f172a !important;
font-weight: 700 !important; text-transform: none !important; letter-spacing: -0.015em !important;
font-size: clamp(18px, 2vw, 26px) !important; line-height: 1.16 !important; }
.dm-st .dm-st-pillar__title--hero { font-size: clamp(22px, 2.6vw, 34px) !important;
background: linear-gradient(90deg, #C01227, #E2354A) !important; -webkit-background-clip: text !important;
background-clip: text !important; -webkit-text-fill-color: transparent !important; }
.dm-st-foot { margin: 12px 0 0; font-size: clamp(12px, 1.1vw, 13.5px); line-height: 1.5; color: #475569;
display: flex; align-items: center; gap: 8px; }
.dm-st-livedot { width: 8px; height: 8px; border-radius: 50%; background: var(--c); box-shadow: 0 0 0 0 var(--c);
animation: dmStPulse 1.8s ease-out infinite; }
@keyframes dmStPulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--c) 55%, transparent); } 100% { box-shadow: 0 0 0 9px transparent; } }
/* In-scene 3D labels (drei <Html>) — crisp glass chips floating in the WebGL scene */
.dm-st3d-file, .dm-st3d-count, .dm-st3d-ai, .dm-st3d-chip {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
pointer-events: none; user-select: none; white-space: nowrap; transition: opacity 0.2s linear; }
.dm-st3d-file, .dm-st3d-ai {
display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #0f172a;
background: rgba(255,255,255,0.88); border: 1px solid rgba(34,197,94,0.4); border-radius: 999px;
padding: 6px 13px; box-shadow: 0 8px 22px -12px rgba(34,197,94,0.7); backdrop-filter: blur(8px); }
.dm-st3d-count { font-size: 15px; font-weight: 800; color: #0f172a; background: rgba(255,255,255,0.9);
border: 1px solid rgba(34,197,94,0.45); border-radius: 12px; padding: 6px 14px;
box-shadow: 0 10px 26px -12px rgba(34,197,94,0.8); backdrop-filter: blur(8px); }
.dm-st3d-count span { color: #16a34a; font-size: 19px; }
.dm-st3d-chip { display: inline-flex; align-items: center; gap: 8px; background: rgba(255,255,255,0.92);
border: 1px solid rgba(34,197,94,0.4); border-radius: 12px; padding: 6px 11px;
box-shadow: 0 10px 26px -14px rgba(15,23,42,0.7); backdrop-filter: blur(8px); }
.dm-st3d-chip__ico { font-size: 17px; }
.dm-st3d-chip__txt { display: flex; flex-direction: column; line-height: 1.15; }
.dm-st3d-chip__txt b { font-size: 12.5px; font-weight: 800; color: #0f172a; }
.dm-st3d-chip__txt { font-size: 10.5px; color: #475569; }
/* Generic themed 3D chips (stages 0205) — colour comes from --tc per element */
.dm-st3d-tag, .dm-st3d-score {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
pointer-events: none; user-select: none; white-space: nowrap; transition: opacity 0.2s linear; }
.dm-st3d-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 700; color: #0f172a;
background: rgba(255,255,255,0.9); border: 1px solid color-mix(in srgb, var(--tc, #8B5CF6) 55%, white);
border-radius: 999px; padding: 5px 11px; box-shadow: 0 8px 20px -12px var(--tc, #8B5CF6); backdrop-filter: blur(8px); }
.dm-st3d-tag b { font-weight: 800; color: var(--tc, #0f172a); }
.dm-st3d-tag.is-u { background: color-mix(in srgb, var(--tc) 14%, white); border-color: var(--tc); }
.dm-st3d-tag.is-muted { opacity: 0.82; border-style: dashed; }
.dm-st3d-tag.is-win { border-color: var(--tc); box-shadow: 0 10px 26px -10px var(--tc); }
.dm-st3d-score { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #0f172a;
background: rgba(255,255,255,0.92); border: 1px solid color-mix(in srgb, var(--tc, #0f172a) 45%, white);
border-radius: 12px; padding: 6px 13px; box-shadow: 0 10px 26px -12px var(--tc, #0f172a); backdrop-filter: blur(8px); }
.dm-st3d-score b { font-size: 16px; font-weight: 800; color: var(--tc, #0f172a); }
.dm-st3d-score.is-win { border-color: var(--tc); }
/* Light side-card anchor — the 3D world now carries the detail */
.dm-st-anchor__lead { margin: 0 0 14px; font-size: clamp(13px, 1.2vw, 15px); line-height: 1.55; color: #475569; }
.dm-st-anchor__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.dm-st-anchor__chip { font-size: 12px; font-weight: 700; color: #334155; padding: 6px 12px; border-radius: 999px;
background: color-mix(in srgb, var(--c) 9%, white); border: 1px solid color-mix(in srgb, var(--c) 30%, white); }
.dm-st-anchor__chip--win { color: #fff; background: linear-gradient(90deg, #C01227, #E2354A); border-color: transparent; }
/* In-world Command Center KPI card + Winner card (drei <Html>, faded by proximity) */
.dm-st3d-kpi, .dm-st3d-winner3d {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
pointer-events: none; user-select: none; transition: opacity 0.2s linear; }
.dm-st3d-kpi { display: flex; flex-direction: column; gap: 5px; width: 132px; padding: 9px 12px; border-radius: 12px;
background: rgba(255,255,255,0.95); border: 1px solid color-mix(in srgb, var(--tc, #F59E0B) 40%, white);
box-shadow: 0 10px 26px -14px var(--tc, #F59E0B); backdrop-filter: blur(8px); }
.dm-st3d-kpi__n { font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
.dm-st3d-kpi__v { font-size: 24px; font-weight: 800; color: #0f172a; line-height: 1; }
.dm-st3d-kpi__v i { font-size: 14px; font-weight: 700; color: var(--tc, #F59E0B); font-style: normal; margin-left: 1px; }
.dm-st3d-kpi__bar { height: 6px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; }
.dm-st3d-kpi__bar i { display: block; height: 100%; border-radius: 999px; background: var(--tc, #F59E0B); }
.dm-st3d-winner3d { display: flex; flex-direction: column; gap: 3px; width: 184px; padding: 13px 15px; border-radius: 14px;
background: rgba(255,255,255,0.96); border: 1px solid rgba(192,18,39,0.4); box-shadow: 0 16px 40px -16px rgba(192,18,39,0.6); backdrop-filter: blur(8px); }
.dm-st3d-winner3d__top { font-size: 10.5px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #C01227; }
.dm-st3d-winner3d__name { font-size: 26px; font-weight: 800; color: #0f172a; letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 4px; }
.dm-st3d-winner3d__row { font-size: 12px; color: #475569; }
.dm-st3d-winner3d__row b { color: #C01227; font-weight: 800; margin-right: 4px; }
@media (max-width: 1000px) {
.dm-st-rail__title { display: none; }
.dm-st-rail__step { padding: 5px 7px; }
.dm-st-rail__line { width: 9px; }
}
@media (max-width: 767px) {
.dm-st { height: 420vh; }
/* Full-width, bottom-anchored story card. Bound its height to the viewport and
let it scroll internally so a tall stage card (Command Center / Winner) can
never be clipped off the top of a short phone screen — the active workflow
state always stays fully visible. */
.dm-st-card-story { left: 0 !important; right: 0 !important; margin: 0 auto; width: calc(100% - 28px);
bottom: clamp(18px, 4vh, 40px); padding: 15px 16px;
max-height: 52vh; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
}
@media (prefers-reduced-motion: reduce) {
.dm-st-arrow { animation: none !important; }
}
`;