diff --git a/src/components/marketing/ProductMotif.tsx b/src/components/marketing/ProductMotif.tsx index da65d9e..d9fe18e 100644 --- a/src/components/marketing/ProductMotif.tsx +++ b/src/components/marketing/ProductMotif.tsx @@ -43,7 +43,8 @@ const workflowLanePath = (offset: number, rightX = 262, leftX = 58) => { ].join(" "); }; -// Card motifs draw one 320×400 cluster stretched over the 4/5 plate. Heroes +// Card motifs draw one 320×400 cluster on a fixed 4/5 canvas. Compact cards +// crop that canvas instead of squeezing its shapes and animation paths. Heroes // are wide and short, so slicing that same viewBox magnifies the artwork ~4× // and the center cut-out mask swallows it — the shapes stop reading as the // card imagery. Instead heroes repeat the cluster across a 1600×400 field so @@ -485,7 +486,13 @@ export const ProductMotif = ({ productId, surface }: ProductMotifProps) => { return (
); }; diff --git a/src/components/marketing/sections/LatestPostCallout.module.css b/src/components/marketing/sections/LatestPostCallout.module.css new file mode 100644 index 0000000..4e49b90 --- /dev/null +++ b/src/components/marketing/sections/LatestPostCallout.module.css @@ -0,0 +1,86 @@ +.story { + --post-width: min(72rem, calc(100vw - 2rem)); + position: relative; + z-index: 20; + padding: 3rem 1rem 4rem; +} + +.track { + position: relative; + width: 100%; +} + +.card { + position: relative; + display: block; + width: 100%; + max-width: 72rem; + margin-inline: auto; + overflow: hidden; + border-radius: 24px; +} + +.card::before { + content: ""; + position: absolute; + z-index: 1; + inset: 0; + border-top: 1px solid rgb(255 255 255 / 60%); + border-radius: inherit; + pointer-events: none; +} + +.pill { + display: none; +} + +.content { + position: relative; + width: var(--post-width); +} + +/* A 48px sticky carrier keeps the bottom edge stable as the card grows up. + Its track ends at the callout's resting position, so it releases into the + page naturally, without fixed positioning, reparenting or scroll locking. */ +.story[data-enhanced] { + margin-top: -80px; + padding-top: 0; +} + +.story[data-enhanced] .track { + height: calc(var(--post-height, 320px) + 144px); +} + +.story[data-enhanced] .sticky { + position: sticky; + top: calc(var(--post-viewport-height, 100svh) - 80px); + height: 48px; +} + +.story[data-enhanced] .card { + position: absolute; + inset-inline: 0; + bottom: 0; +} + +.story[data-enhanced] .pill { + position: absolute; + inset: 0 0 auto; + display: flex; + height: 46px; + align-items: center; +} + +@media (min-width: 48rem) { + .story { + --post-width: min(72rem, calc(100vw - 6rem)); + padding-inline: 3rem; + } +} + +@media (min-width: 1681px) { + .story { + --post-width: min(72rem, calc(100vw - 7rem)); + padding-inline: 3.5rem; + } +} diff --git a/src/components/marketing/sections/LatestPostCallout.tsx b/src/components/marketing/sections/LatestPostCallout.tsx new file mode 100644 index 0000000..9877128 --- /dev/null +++ b/src/components/marketing/sections/LatestPostCallout.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { motion, useMotionValue, useScroll, useTransform } from "framer-motion"; +import { ArrowRight } from "lucide-react"; +import { canonicalizeInternalHref } from "@/lib/internalHref"; +import { EYEBROW_CLASS, SECTION_H2_CLASS } from "../typography"; +import styles from "./LatestPostCallout.module.css"; + +export interface LatestPost { + title: string; + description: string; + href: string; + image: { src: string; width: number; height: number; alt: string } | null; +} + +export function LatestPostCallout({ post }: { post: LatestPost }) { + const [reducedMotion, setReducedMotion] = useState(false); + const storyRef = useRefLatest update
++ {post.description} +
+ ) : null} + + Read update
-
+