feat(web): restore tall hero + carousel slide + viewport-fixed scroll cue
All checks were successful
Deploy to Production / deploy (push) Successful in 1m0s

Three coordinated tweaks to the landing-page above-the-fold:

1. **Hero padding restored to py-14/sm:py-20/md:py-28** (was py-12/14/16).
   Compressing it for the scroll-cue position fight made the hero feel
   cramped and gave the ParticleHero background less room to breathe.
   With the cue moved out (see #3), there's no reason to shrink the hero.

2. **Step rotator switches to carousel-style horizontal slide.** The
   AnimatePresence transition was a fade+y-shift cross-fade — clean but
   sequential. Now the leaving card slides left out (x:-220) while the
   entering card slides right in (x:220→0), both coexisting in the same
   3D-space and inheriting the same mouse-tilt. The container gets
   `min-h-[240px]` so the absolutely-positioned cards have layout to
   anchor to (claude_desktop_config.json is the tallest at 7 lines).
   Reduced-motion still gets the opacity-only cross-fade — sliding
   content sideways is exactly the kind of motion that preference is
   meant to suppress.

3. **`<ScrollCue>` extracted into its own client component**, fixed-
   positioned at viewport bottom (bottom-5) with a frosted pill style.
   Fades to opacity:0 once `window.scrollY > 80`, so it doesn't shadow
   the rest of the page. Lives next to `<section>` in page.tsx rather
   than inside the hero — that way it anchors to the loadscreen's
   natural bottom edge whether the hero is short or tall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi
2026-05-27 12:11:42 +02:00
parent e4e437c44c
commit 0cf9c66b6b
3 changed files with 73 additions and 19 deletions

View File

@@ -109,23 +109,38 @@ export function HeroStepRotator() {
return (
<div className="flex flex-col items-center gap-5">
{/* Container is relative + has a min-height so the absolutely-
positioned cards inside (during the slide) overlap cleanly
without collapsing the layout. min-h is sized for the tallest
card (claude_desktop_config.json at 7 lines). */}
<div
ref={containerRef}
className="relative w-full max-w-md"
className="relative w-full max-w-md min-h-[240px]"
style={{ perspective: 1200 }}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
onMouseMove={onMove}
>
<AnimatePresence mode="wait" initial={false}>
<AnimatePresence initial={false}>
<motion.div
key={step}
initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 12 }}
animate={reduced ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}
exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.97, y: -8 }}
transition={{ duration: reduced ? 0.15 : 0.5, ease: [0.16, 1, 0.3, 1] }}
style={{ rotateX, rotateY, transformStyle: 'preserve-3d' }}
className="relative overflow-hidden rounded-lg border border-[--color-border-strong] bg-[--color-bg-elevated] shadow-2xl shadow-black/50"
// Carousel: current card slides left-out, next slides right-in.
// Both cards coexist briefly in the same 3D-space and inherit
// the same tilt — reads as a single tile that's swapping its
// contents, not two discrete tiles. Reduced-motion collapses
// the slide to a plain opacity cross-fade.
initial={reduced ? { opacity: 0 } : { x: 220, opacity: 0 }}
animate={reduced ? { opacity: 1 } : { x: 0, opacity: 1 }}
exit={reduced ? { opacity: 0 } : { x: -220, opacity: 0 }}
transition={{ duration: reduced ? 0.15 : 0.55, ease: [0.16, 1, 0.3, 1] }}
style={{
rotateX,
rotateY,
position: 'absolute',
inset: 0,
transformStyle: 'preserve-3d',
}}
className="overflow-hidden rounded-lg border border-[--color-border-strong] bg-[--color-bg-elevated] shadow-2xl shadow-black/50"
>
{/* Cursor-following glow — sits behind the content, additive. */}
<motion.div

View File

@@ -0,0 +1,43 @@
'use client';
import { ChevronDown } from 'lucide-react';
import { useEffect, useState } from 'react';
/**
* Scroll cue — a fixed pill anchored to the bottom of the viewport that
* points the visitor down to the flow video below the hero. Fades out
* once the user has scrolled past the loadscreen so it doesn't follow
* them around the page.
*
* Lives at z-30 so it sits above the hero content but below modals.
* The frosted pill (backdrop-blur + border) reads clearly against the
* particle background without stealing focus from the H1.
*/
export function ScrollCue({ targetId }: { targetId: string }) {
const [visible, setVisible] = useState(true);
useEffect(() => {
function onScroll() {
setVisible(window.scrollY < 80);
}
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<a
href={`#${targetId}`}
aria-label="See the flow in action"
className={`fixed inset-x-0 bottom-5 z-30 mx-auto flex w-fit items-center gap-1.5 rounded-full border border-[--color-border] px-3 py-1.5 text-[10.5px] uppercase tracking-[0.18em] text-[--color-fg-subtle] backdrop-blur transition-opacity duration-500 hover:text-[--color-fg] ${
visible ? 'opacity-100' : 'pointer-events-none opacity-0'
}`}
style={{
backgroundColor: 'color-mix(in oklab, var(--color-bg-elevated) 75%, transparent)',
}}
>
<span>see it run</span>
<ChevronDown size={12} className="animate-bounce" />
</a>
);
}