@@ -105,17 +105,30 @@ const providers = [
105105 </div >
106106 </div >
107107
108- <!-- Pipeline steps -->
108+ <!-- Pipeline rail -->
109109 <div class =" mt-16" >
110110 <div class =" text-xs uppercase tracking-[0.18em] text-[var(--color-muted)] mb-6" >{ s .pipelineLabel } </div >
111- <ol class =" grid gap-4 md:grid-cols-2 lg:grid-cols-3 pipeline-steps" >
112- { s .pipeline .map ((step ) => (
113- <li class = " pipeline-step relative rounded-xl border border-white/10 bg-[var(--color-surface)]/60 p-5 transition-all duration-500" >
114- <div class = " flex items-baseline gap-3 mb-2" >
115- <span class = " step-num font-mono text-[10px] text-[var(--color-dim)] transition-colors duration-500" >{ step .step } </span >
116- <span class = " font-semibold tracking-tight" >{ step .label } </span >
117- </div >
118- <p class = " text-sm text-[var(--color-muted)] leading-relaxed" set :html = { step .body .replace (/ `([^ `] + )`/ g , ' <code class="font-mono text-[12px] text-[var(--color-brand-1)] bg-white/5 rounded px-1 py-0.5">$1</code>' )} ></p >
111+
112+ <div class =" pipeline-rail relative h-px bg-white/10 overflow-hidden mb-6" >
113+ <div class =" pipeline-rail-fill absolute inset-y-0 left-0 bg-gradient-to-r from-[var(--color-brand-1)] via-[var(--color-brand-2)] to-[var(--color-accent)] transition-[width] duration-500 ease-[cubic-bezier(.22,.9,.3,1)]" style =" width: 16.66%" ></div >
114+ </div >
115+
116+ <ol class =" pipeline-steps grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6" role =" tablist" >
117+ { s .pipeline .map ((step , i ) => (
118+ <li >
119+ <button
120+ type = " button"
121+ class = " pipeline-step group w-full text-left py-4 pr-4 sm:pr-5 border-r border-white/10 last:border-r-0 transition-colors"
122+ data-pipeline-step = { i }
123+ aria-label = { ` Pipeline step ${step .step }: ${step .label } ` }
124+ >
125+ <div class = " flex items-center justify-between mb-3" >
126+ <span class = " pipeline-step-num font-mono text-[11px] tracking-[0.12em] text-[var(--color-dim)] transition-colors" >{ step .step } </span >
127+ <span class = " pipeline-step-tick w-2 h-2 rounded-full bg-white/15 transition-all" aria-hidden = " true" ></span >
128+ </div >
129+ <h4 class = " pipeline-step-title font-medium text-sm text-white mb-1.5 tracking-tight" >{ step .label } </h4 >
130+ <p class = " text-xs text-[var(--color-muted)] leading-[1.55]" set :html = { step .body .replace (/ `([^ `] + )`/ g , ' <code class="font-mono text-[11px] text-[var(--color-brand-1)] bg-white/5 rounded px-1 py-0.5">$1</code>' )} ></p >
131+ </button >
119132 </li >
120133 ))}
121134 </ol >
@@ -125,42 +138,29 @@ const providers = [
125138
126139<style >
127140 .pipeline-step {
128- transform: translateY(8px);
129141 opacity: 0.55;
142+ cursor: pointer;
143+ background: transparent;
144+ border: 0;
145+ border-right: 1px solid rgba(255, 255, 255, 0.1);
146+ font-family: inherit;
147+ transition: opacity 300ms ease;
130148 }
131- .pipeline-step.is-active {
132- transform: translateY(0);
133- opacity: 1;
134- border-color: rgba(167, 139, 250, 0.45);
135- box-shadow:
136- 0 0 0 1px rgba(34, 211, 238, 0.18),
137- 0 12px 40px -12px rgba(167, 139, 250, 0.35);
138- }
139- .pipeline-step.is-active .step-num {
140- color: var(--color-brand-1);
141- }
142- .pipeline-step::before {
143- content: "";
144- position: absolute;
145- inset: 0;
146- border-radius: inherit;
147- pointer-events: none;
148- background: linear-gradient(135deg, rgba(34, 211, 238, 0.08), rgba(167, 139, 250, 0.04));
149- opacity: 0;
150- transition: opacity 500ms ease;
151- }
152- .pipeline-step.is-active::before {
153- opacity: 1;
149+ .pipeline-step:hover { opacity: 0.85; }
150+ .pipeline-step.is-active,
151+ .pipeline-step.is-done { opacity: 1; }
152+ .pipeline-step.is-active .pipeline-step-num { color: var(--color-brand-1); }
153+ .pipeline-step.is-active .pipeline-step-tick {
154+ background: var(--color-brand-1);
155+ box-shadow: 0 0 14px var(--color-brand-1);
154156 }
157+ .pipeline-step.is-done .pipeline-step-tick { background: var(--color-brand-2); }
158+
155159 @media (prefers-reduced-motion: reduce) {
156- .pipeline-step {
157- transform: none;
158- opacity: 1;
159- }
160+ .pipeline-rail-fill,
161+ .pipeline-step { transition: none !important; }
160162 .arch-flow,
161- .arch-glow {
162- animation: none !important;
163- }
163+ .arch-glow { animation: none !important; }
164164 }
165165
166166 @keyframes archDash {
@@ -186,18 +186,62 @@ const providers = [
186186</style >
187187
188188<script >
189- const steps = document.querySelectorAll(".pipeline-step");
190- if (steps.length && "IntersectionObserver" in window) {
191- const obs = new IntersectionObserver(
192- (entries) => {
193- for (const entry of entries) {
194- entry.target.classList.toggle("is-active", entry.isIntersecting);
195- }
196- },
197- { rootMargin: "-25% 0px -25% 0px", threshold: 0 },
189+ // Auto-advance the pipeline as the section scrolls into view; pause when out of
190+ // view. Users can click any step to jump there — the timer resumes afterwards.
191+ const section = document.getElementById("how");
192+ const steps = Array.from(
193+ document.querySelectorAll<HTMLButtonElement>("[data-pipeline-step]")
194+ );
195+ const rail = document.querySelector<HTMLElement>(".pipeline-rail-fill");
196+
197+ if (section && steps.length && rail) {
198+ const total = steps.length;
199+ let active = 0;
200+ let visible = false;
201+ let timer: number | undefined;
202+
203+ const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
204+
205+ const render = () => {
206+ steps.forEach((el, i) => {
207+ el.classList.toggle("is-active", i === active);
208+ el.classList.toggle("is-done", i < active);
209+ });
210+ rail.style.width = `${((active + 1) / total) * 100}%`;
211+ };
212+
213+ const schedule = () => {
214+ if (timer) window.clearTimeout(timer);
215+ if (!visible || prefersReduced) return;
216+ timer = window.setTimeout(() => {
217+ active = (active + 1) % total;
218+ render();
219+ schedule();
220+ }, 2400);
221+ };
222+
223+ steps.forEach((el, i) =>
224+ el.addEventListener("click", () => {
225+ active = i;
226+ render();
227+ schedule();
228+ })
198229 );
199- steps.forEach((s) => obs.observe(s));
200- } else {
201- steps.forEach((s) => s.classList.add("is-active"));
230+
231+ if ("IntersectionObserver" in window) {
232+ const obs = new IntersectionObserver(
233+ (entries) => {
234+ visible = entries[0]?.isIntersecting ?? false;
235+ schedule();
236+ },
237+ { threshold: 0.2 }
238+ );
239+ obs.observe(section);
240+ } else {
241+ visible = true;
242+ schedule();
243+ }
244+
245+ render();
202246 }
203247</script >
0 commit comments