diff --git a/astro.config.ts b/astro.config.ts index b6558b6..25c276a 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -84,6 +84,7 @@ export default defineConfig({ include: [ 'mermaid', 'framer-motion', + '@rivet-gg/icons', 'clsx', 'sonner', '@radix-ui/react-slot', diff --git a/src/components/ScrollObserver.tsx b/src/components/ScrollObserver.tsx index 3929b1f..d063498 100644 --- a/src/components/ScrollObserver.tsx +++ b/src/components/ScrollObserver.tsx @@ -6,6 +6,33 @@ const REVEAL_SELECTOR = '[data-site-reveal], [data-site-reveal-group], [data-sit const VISIBLE_ATTRIBUTE = 'data-site-reveal-visible'; const OBSERVER_READY_ATTRIBUTE = 'data-site-motion-observer-ready'; +const findHydratingIsland = (element: HTMLElement) => { + let ancestor = element.parentElement; + + while (ancestor) { + if (ancestor.matches('astro-island[ssr]')) { + let boundary = element.parentElement; + let isSlottedContent = false; + + while (boundary && boundary !== ancestor) { + if (boundary.tagName === 'ASTRO-SLOT') { + isSlottedContent = true; + break; + } + boundary = boundary.parentElement; + } + + // Astro preserves slotted server HTML when the wrapping island hydrates, + // so reveal attributes on that content cannot be discarded. + if (!isSlottedContent) return ancestor as HTMLElement; + } + + ancestor = ancestor.parentElement; + } + + return null; +}; + // Astro pages keep their content server-rendered. This observer gives those // blocks the same one-time fade-rise used by marketing/motion.tsx without // turning every section into a separate hydrated React island. @@ -38,7 +65,7 @@ export function ScrollObserver() { // An island can replace its server-rendered children while it hydrates. // Wait for that boundary before mutating reveal state so the visible // attribute cannot be discarded by a nested island's hydration pass. - const hydratingIsland = element.closest('astro-island[ssr]'); + const hydratingIsland = findHydratingIsland(element); if (hydratingIsland) { if (!pendingHydrationListeners.has(hydratingIsland)) { const handleHydration = () => { diff --git a/src/components/marketing/ProductMotif.tsx b/src/components/marketing/ProductMotif.tsx new file mode 100644 index 0000000..9fe376b --- /dev/null +++ b/src/components/marketing/ProductMotif.tsx @@ -0,0 +1,392 @@ +import type { CSSProperties } from "react"; +import { productLogos } from "@/sitemap/productLogos"; + +export type ProductMotifId = + "actors" | "agentos" | "workflows" | "dynamic-apps"; + +export interface ProductMotifProps { + productId: ProductMotifId; + surface: "card" | "hero"; +} + +const workflowSteps = [ + { x: 164, y: 84, width: 108, height: 16 }, + { x: 44, y: 132, width: 132, height: 16 }, + { x: 144, y: 180, width: 128, height: 16 }, + { x: 60, y: 228, width: 132, height: 16 }, +] as const; + +const heroWorkflowStepY = [20, 110, 250, 340] as const; + +const dynamicAppCircles = [ + { cx: 16, cy: 36, direction: "forward", tone: "bright", delay: 0 }, + { cx: 96, cy: 108, direction: "backward", tone: "soft", delay: 0 }, + { cx: 176, cy: 36, direction: "forward", tone: "soft", delay: 120 }, + { cx: 256, cy: 108, direction: "backward", tone: "bright", delay: 120 }, + { cx: 16, cy: 180, direction: "forward", tone: "soft", delay: 180 }, + { cx: 96, cy: 252, direction: "backward", tone: "bright", delay: 180 }, + { cx: 176, cy: 180, direction: "forward", tone: "bright", delay: 300 }, + { cx: 256, cy: 252, direction: "backward", tone: "soft", delay: 300 }, +] as const; + +const easeInOutSine = (progress: number) => + (1 - Math.cos(Math.PI * progress)) / 2; + +const agentOSRings = Array.from({ length: 9 }, (_, index) => ({ + scale: 0.06 + easeInOutSine(index / 9) * 1.1, + delay: index * -800, + tone: index % 3 === 0 ? "bright" : "soft", +})); + +const actorLogoMarks = [ + { + left: -19, + top: -13.5, + tilt: -5, + rock: 3.5, + swayX: 6, + bob: 4, + duration: 7.6, + delay: -1.4, + opacity: 0.18, + }, + { + left: 29, + top: -11.5, + tilt: 3, + rock: 4, + swayX: -5, + bob: 3, + duration: 8.4, + delay: -3.2, + opacity: 0.24, + }, + { + left: 75, + top: -13, + tilt: -2, + rock: 3, + swayX: -6, + bob: 5, + duration: 6.9, + delay: -4.8, + opacity: 0.18, + }, + { + left: 4, + top: 13.5, + tilt: 5, + rock: 3.5, + swayX: 7, + bob: 3, + duration: 8.8, + delay: -2.5, + opacity: 0.2, + }, + { + left: 52, + top: 11.5, + tilt: -4, + rock: 4.5, + swayX: -7, + bob: 4, + duration: 7.2, + delay: -5.3, + opacity: 0.26, + }, + { + left: -17, + top: 36.5, + tilt: 4, + rock: 3, + swayX: 5, + bob: 5, + duration: 8.1, + delay: -3.7, + opacity: 0.17, + }, + { + left: 30, + top: 38.5, + tilt: -5, + rock: 3.5, + swayX: -6, + bob: 3, + duration: 6.6, + delay: -1.9, + opacity: 0.22, + }, + { + left: 76, + top: 36.5, + tilt: 3, + rock: 4, + swayX: -5, + bob: 4, + duration: 8.6, + delay: -6.1, + opacity: 0.18, + }, + { + left: 6, + top: 63.5, + tilt: -3, + rock: 4.5, + swayX: 7, + bob: 5, + duration: 7.4, + delay: -4.4, + opacity: 0.2, + }, + { + left: 50, + top: 61, + tilt: 5, + rock: 3, + swayX: -6, + bob: 3, + duration: 8.3, + delay: -2.8, + opacity: 0.24, + }, +] as const; + +const cardMaskClass = + "[-webkit-mask-image:linear-gradient(to_bottom,black_55%,transparent_84%)] [mask-image:linear-gradient(to_bottom,black_55%,transparent_84%)]"; +const actorsCardMaskClass = + "[-webkit-mask-image:linear-gradient(to_bottom,black_58%,transparent_86%)] [mask-image:linear-gradient(to_bottom,black_58%,transparent_86%)]"; + +const containerClassName = ( + productId: ProductMotifId, + surface: ProductMotifProps["surface"], +) => + [ + "product-motif pointer-events-none absolute overflow-hidden", + `product-motif--${surface}`, + `product-motif--${productId}`, + productId === "actors" ? "actors-backdrop" : "", + surface === "card" + ? `inset-0 ${productId === "actors" ? actorsCardMaskClass : cardMaskClass}` + : "inset-x-0 top-0", + ] + .filter(Boolean) + .join(" "); + +// Rails stop at each node instead of relying on a product-colored cutout. This +// keeps the card unchanged while allowing the same geometry to sit over the +// hero's depth wash without painting flat rectangles into the paper field. +const WorkflowMotif = ({ surface }: Pick) => { + const renderedSteps = [ + ...workflowSteps.map((step) => ({ ...step, variant: "default" as const })), + ...(surface === "hero" + ? workflowSteps.map((step, index) => ({ + ...step, + y: heroWorkflowStepY[index], + variant: "mobile" as const, + })) + : []), + ]; + + return ( + + {renderedSteps.map((positionedStep, index) => { + const sequenceIndex = index % workflowSteps.length; + const centerY = positionedStep.y + positionedStep.height / 2; + + return ( + + + + + + + + ); + })} + + ); +}; + +const DynamicAppsMotif = ({ surface }: Pick) => ( + + {dynamicAppCircles.map((circle) => ( + + ))} + +); + +const AgentOSMotif = ({ surface }: Pick) => ( + + {agentOSRings.map((ring, index) => ( + + ))} + +); + +// This is the exact outer rounded square from the Actors product mark. Drawing +// only that path removes the old accent-colored center cover and makes the +// outline reusable over both product blue and the porcelain hero. +const ActorsMarkOutline = () => ( + + + +); + +const ActorsMotif = ({ surface }: Pick) => ( +
+ {[0, 1].map((cycle) => ( +
+ {actorLogoMarks.map((mark, index) => ( + 0 ? -2 : 2)}px`, + "--actors-logo-float-back-x": `${mark.swayX * -0.55}px`, + "--actors-logo-float-y": `${mark.bob * -1}px`, + "--actors-logo-contact-y": `${mark.bob * -1 + 2}px`, + "--actors-logo-float-duration": `${mark.duration}s`, + "--actors-logo-float-delay": `${mark.delay}s`, + } as CSSProperties + } + > + {surface === "card" ? ( + <> + + + + ) : ( + + )} + + ))} +
+ ))} +
+); + +export const ProductMotif = ({ productId, surface }: ProductMotifProps) => { + const motif = + productId === "actors" ? ( + + ) : productId === "agentos" ? ( + + ) : productId === "workflows" ? ( + + ) : ( + + ); + + return ( + + ); +}; diff --git a/src/components/marketing/agentos/AgentOSPage.tsx b/src/components/marketing/agentos/AgentOSPage.tsx index 04534f8..9417c4f 100644 --- a/src/components/marketing/agentos/AgentOSPage.tsx +++ b/src/components/marketing/agentos/AgentOSPage.tsx @@ -45,6 +45,7 @@ import { import { DeploymentOptions } from '@/components/marketing/DeploymentOptions'; import { CopyInstallCommand } from '@/components/marketing/CopyInstallCommand'; import { ClosingCtaPanel } from '@/components/marketing/ClosingCtaPanel'; +import { ProductMotif } from '@/components/marketing/ProductMotif'; import { SectionRule } from '@/components/marketing/SectionRule'; import { CARD_TITLE_CLASS, @@ -617,7 +618,7 @@ const Hero = () => { name: string; } | null>(null); const reduceMotion = useReducedMotion() ?? false; - const introMotion = (step: number, y = 20) => + const introMotion = (step: number) => reduceMotion ? { initial: false as const, @@ -625,7 +626,9 @@ const Hero = () => { transition: { duration: 0 }, } : { - initial: { opacity: 0, y }, + // Keep the server-rendered hero visible while this large island + // hydrates. Other product heroes use the layout-level reveal fallback. + initial: false as const, animate: { opacity: 1, y: 0 }, transition: { duration: 0.5, delay: HERO_INTRO_STAGGER * step }, }; @@ -697,7 +700,8 @@ const Hero = () => { // Keep the hero height tied to the viewport, as on the main landing page, // while preserving enough room below the foundation tiles.
-
+ +
{/* Centered single-column hero */}
{/* Title */} @@ -724,7 +728,7 @@ const Hero = () => { {/* Benchmark highlights — proof for "faster, lighter, cheaper", linked to the benchmarks below */} - + {heroStats.map((stat) => ( @@ -742,7 +746,7 @@ const Hero = () => {
-
+
diff --git a/src/components/marketing/sections/StackSection.tsx b/src/components/marketing/sections/StackSection.tsx index 1a51b1c..cc2a937 100644 --- a/src/components/marketing/sections/StackSection.tsx +++ b/src/components/marketing/sections/StackSection.tsx @@ -1,9 +1,9 @@ import { Icon } from "@rivet-gg/icons"; -import type { CSSProperties } from "react"; import { visibleProducts, type Product } from "@/sitemap/products"; import { productLogos } from "@/sitemap/productLogos"; import { productAccent, wordmarkMaskStyle } from "@/lib/product-accent"; import { BODY_CLASS, CARD_TITLE_BASE_CLASS, SectionHeading } from "../typography"; +import { ProductMotif, type ProductMotifId } from "../ProductMotif"; import { SITE_SECTION_CLASS, SITE_STANDARD_RAIL_CLASS } from "../layout"; import { canonicalizeInternalHref } from "@/lib/internalHref"; @@ -18,263 +18,11 @@ const stack = visibleProducts.map((product) => ({ highlight: product.id === "actors", })); -const workflowSteps = [ - { x: 164, y: 84, width: 108, height: 16 }, - { x: 44, y: 132, width: 132, height: 16 }, - { x: 144, y: 180, width: 128, height: 16 }, - { x: 60, y: 228, width: 132, height: 16 }, -] as const; -const workflowRails = workflowSteps.map((step) => step.y + step.height / 2); - -const dynamicAppCircles = [ - { cx: 16, cy: 36, direction: "forward", tone: "bright", delay: 0 }, - { cx: 96, cy: 108, direction: "backward", tone: "soft", delay: 0 }, - { cx: 176, cy: 36, direction: "forward", tone: "soft", delay: 120 }, - { cx: 256, cy: 108, direction: "backward", tone: "bright", delay: 120 }, - { cx: 16, cy: 180, direction: "forward", tone: "soft", delay: 180 }, - { cx: 96, cy: 252, direction: "backward", tone: "bright", delay: 180 }, - { cx: 176, cy: 180, direction: "forward", tone: "bright", delay: 300 }, - { cx: 256, cy: 252, direction: "backward", tone: "soft", delay: 300 }, -] as const; - -const easeInOutSine = (progress: number) => (1 - Math.cos(Math.PI * progress)) / 2; - -const agentOSRings = Array.from({ length: 9 }, (_, index) => ({ - scale: 0.06 + easeInOutSine(index / 9) * 1.1, - delay: index * -800, - tone: index % 3 === 0 ? "bright" : "soft", -})); - -const actorLogoMarks = [ - { - left: -19, top: -13.5, tilt: -5, rock: 3.5, - swayX: 6, bob: 4, duration: 7.6, delay: -1.4, opacity: 0.18, - }, - { - left: 29, top: -11.5, tilt: 3, rock: 4, - swayX: -5, bob: 3, duration: 8.4, delay: -3.2, opacity: 0.24, - }, - { - left: 75, top: -13, tilt: -2, rock: 3, - swayX: -6, bob: 5, duration: 6.9, delay: -4.8, opacity: 0.18, - }, - { - left: 4, top: 13.5, tilt: 5, rock: 3.5, - swayX: 7, bob: 3, duration: 8.8, delay: -2.5, opacity: 0.2, - }, - { - left: 52, top: 11.5, tilt: -4, rock: 4.5, - swayX: -7, bob: 4, duration: 7.2, delay: -5.3, opacity: 0.26, - }, - { - left: -17, top: 36.5, tilt: 4, rock: 3, - swayX: 5, bob: 5, duration: 8.1, delay: -3.7, opacity: 0.17, - }, - { - left: 30, top: 38.5, tilt: -5, rock: 3.5, - swayX: -6, bob: 3, duration: 6.6, delay: -1.9, opacity: 0.22, - }, - { - left: 76, top: 36.5, tilt: 3, rock: 4, - swayX: -5, bob: 4, duration: 8.6, delay: -6.1, opacity: 0.18, - }, - { - left: 6, top: 63.5, tilt: -3, rock: 4.5, - swayX: 7, bob: 5, duration: 7.4, delay: -4.4, opacity: 0.2, - }, - { - left: 50, top: 61, tilt: 5, rock: 3, - swayX: -6, bob: 3, duration: 8.3, delay: -2.8, opacity: 0.24, - }, -] as const; - -// A quiet workflow field for the Workflows card. The rails stay visible at -// rest; hover and keyboard focus trace each task from input to output. -const WorkflowBackdrop = () => ( - -); - -// Four diagonal pairs exchange positions, then return to their own origin. -// The offset circles echo the Dynamic Apps reference without adding a new -// foreground element or competing with the card copy. -const DynamicAppsBackdrop = () => ( - -); - -// Rings share one lower-right origin. Their resting scales follow the same -// sine-eased phases as their negative delays, so hover begins without a jump. -const AgentOSBackdrop = () => ( - -); - -// Reuse the real Actors mark so its outline stays exact. A plate-colored -// center cover removes the inner glyphs, while two identical fields make the -// upward hover loop seamless as one replaces the other. -const ActorsBackdrop = () => ( - -); +const hasProductMotif = (productId: string): productId is ProductMotifId => + productId === "actors" || + productId === "agentos" || + productId === "workflows" || + productId === "dynamic-apps"; // The mark at plate scale, rendered cream directly on the accent field. const StackProductMark = ({ product }: { product: Product }) => { @@ -325,17 +73,9 @@ export const StackSection = () => (
- {product.id === "workflows" ? ( - - ) : product.id === "dynamic-apps" ? ( - - ) : product.id === "agentos" ? ( - - ) : product.id === "actors" ? ( - - ) : ( - null - )} + {hasProductMotif(product.id) ? ( + + ) : null} {product.verb} diff --git a/src/components/v2/Header.tsx b/src/components/v2/Header.tsx index 697cb26..ad8327e 100644 --- a/src/components/v2/Header.tsx +++ b/src/components/v2/Header.tsx @@ -82,7 +82,7 @@ function ProductsDropdown({ const lightDropdownRef = useRef(null); const lightTriggerRef = useRef(null); const focusFromPointerRef = useRef(false); - const openAtTriggerPointerDownRef = useRef(null); + const pinnedOpenRef = useRef(false); const lightDropdownId = useId(); // The product verticals, in registry order. Each item opens the product's @@ -113,6 +113,7 @@ function ProductsDropdown({ ) { return; } + pinnedOpenRef.current = false; setIsOpen(false); }, 150); }; @@ -129,6 +130,7 @@ function ProductsDropdown({ const handleOpenChange = (open: boolean) => { if (!open) { cancelClose(); + pinnedOpenRef.current = false; setIsOpen(false); } }; @@ -152,6 +154,7 @@ function ProductsDropdown({ clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } + pinnedOpenRef.current = false; setIsOpen(false); }; @@ -168,15 +171,10 @@ function ProductsDropdown({ className={cn("group/products px-2.5 py-2", align === "start" && "relative")} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - onPointerDownCapture={(event) => { + onPointerDownCapture={() => { // Pointer activation focuses the button before `click`. Let `click` // perform the toggle so touch users do not immediately reopen it. focusFromPointerRef.current = true; - openAtTriggerPointerDownRef.current = - event.target instanceof Node && - lightTriggerRef.current?.contains(event.target) - ? isOpen - : null; queueMicrotask(() => { focusFromPointerRef.current = false; }); @@ -194,6 +192,7 @@ function ProductsDropdown({ return; } cancelClose(); + pinnedOpenRef.current = false; setIsOpen(false); }} onKeyDown={(event) => { @@ -202,6 +201,7 @@ function ProductsDropdown({ event.stopPropagation(); cancelClose(); lightTriggerRef.current?.focus(); + pinnedOpenRef.current = false; setIsOpen(false); }} > @@ -223,11 +223,8 @@ function ProductsDropdown({ onMouseEnter={handleMouseEnter} onClick={() => { cancelClose(); - const openAtPointerDown = openAtTriggerPointerDownRef.current; - openAtTriggerPointerDownRef.current = null; - setIsOpen((open) => - openAtPointerDown === null ? !open : !openAtPointerDown, - ); + pinnedOpenRef.current = !pinnedOpenRef.current; + setIsOpen(pinnedOpenRef.current); }} > Products diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 75b414e..08cb2cf 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -192,12 +192,121 @@ const footerOpenings = await fetchOpeningsCount(AbortSignal.timeout(4000)); {pageFamily === 'site' && ( )} diff --git a/src/pages/actors.astro b/src/pages/actors.astro index fa505e8..e284953 100644 --- a/src/pages/actors.astro +++ b/src/pages/actors.astro @@ -4,6 +4,7 @@ // per-tenant databases. There is no separate agent solution page. import MarketingLayout from '@/layouts/MarketingLayout.astro'; import { ProductLockup } from '@/components/marketing/ProductLockup'; +import { ProductMotif } from '@/components/marketing/ProductMotif'; import { BuiltInFeatures } from '@/components/marketing/sections/BuiltInFeatures'; import { ProblemSection } from '@/components/marketing/sections/ProblemSection'; import { BenchmarksSection } from '@/components/marketing/sections/BenchmarksSection'; @@ -97,7 +98,8 @@ const compositionLayers = [
-
+ +
diff --git a/src/pages/dynamic-apps.astro b/src/pages/dynamic-apps.astro index 13b3f55..2e563eb 100644 --- a/src/pages/dynamic-apps.astro +++ b/src/pages/dynamic-apps.astro @@ -6,6 +6,7 @@ import MarketingLayout from '@/layouts/MarketingLayout.astro'; import FaqJsonLd from '@/components/faq/FaqJsonLd.astro'; import { FaqList } from '@/components/faq/FaqSection'; import { ProductLockup } from '@/components/marketing/ProductLockup'; +import { ProductMotif } from '@/components/marketing/ProductMotif'; import { ClosingCtaPanel } from '@/components/marketing/ClosingCtaPanel'; import { SectionRule } from '@/components/marketing/SectionRule'; import { DeploymentOptions } from '@/components/marketing/DeploymentOptions'; @@ -240,7 +241,8 @@ const compositionLayers = [
-
+ +
diff --git a/src/pages/workflows.astro b/src/pages/workflows.astro index 590b8da..725c404 100644 --- a/src/pages/workflows.astro +++ b/src/pages/workflows.astro @@ -3,6 +3,7 @@ import MarketingLayout from '@/layouts/MarketingLayout.astro'; import FaqJsonLd from '@/components/faq/FaqJsonLd.astro'; import { FaqList } from '@/components/faq/FaqSection'; import { ProductLockup } from '@/components/marketing/ProductLockup'; +import { ProductMotif } from '@/components/marketing/ProductMotif'; import { ClosingCtaPanel } from '@/components/marketing/ClosingCtaPanel'; import { SectionRule } from '@/components/marketing/SectionRule'; import { DeploymentOptions } from '@/components/marketing/DeploymentOptions'; @@ -177,7 +178,8 @@ const compositionLayers = [
-
+ +
diff --git a/src/styles/main.css b/src/styles/main.css index 20c03ea..5db84cd 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -597,6 +597,120 @@ padding-bottom: 4px; } +/* The four product motifs share geometry across the homepage cards and their + corresponding product heroes. Cards keep the cream-on-accent treatment; + heroes use quiet neutral ink over the porcelain depth wash. */ +.product-motif--card { + --motif-line-soft: rgba(244, 241, 231, 0.2); + --motif-line-strong: rgba(244, 241, 231, 0.34); + --motif-node-stroke: rgba(244, 241, 231, 0.22); +} + +.product-motif--card.product-motif--workflows { + --motif-line-soft: rgba(244, 241, 231, 0.18); +} + +.product-motif--hero { + --motif-line-soft: rgba(27, 25, 22, 0.075); + --motif-line-strong: rgba(27, 25, 22, 0.115); + --motif-node-stroke: rgba(27, 25, 22, 0.095); + height: min(42rem, 100%); + z-index: 0; + -webkit-mask-image: linear-gradient( + to bottom, + transparent 5%, + rgba(0, 0, 0, 0.38) 30%, + black 56%, + rgba(0, 0, 0, 0.5) 78%, + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + transparent 5%, + rgba(0, 0, 0, 0.38) 30%, + black 56%, + rgba(0, 0, 0, 0.5) 78%, + transparent 100% + ); +} + +.product-motif--hero:is(.product-motif--workflows, .product-motif--dynamic-apps) { + --motif-line-soft: rgba(27, 25, 22, 0.09); + --motif-line-strong: rgba(27, 25, 22, 0.135); + --motif-node-stroke: rgba(27, 25, 22, 0.11); +} + +/* Cut the motif out of the message field itself. Masking the artwork instead + of painting paper over it preserves the existing depth wash while keeping + every lockup, headline, description, and action on an uninterrupted field. */ +.product-motif--hero > * { + -webkit-mask-image: radial-gradient( + ellipse 72% 52% at 50% 50%, + transparent 0%, + transparent 46%, + rgba(0, 0, 0, 0.08) 50%, + rgba(0, 0, 0, 0.55) 58%, + black 68% + ); + mask-image: radial-gradient( + ellipse 72% 52% at 50% 50%, + transparent 0%, + transparent 46%, + rgba(0, 0, 0, 0.08) 50%, + rgba(0, 0, 0, 0.55) 58%, + black 68% + ); +} + +.product-motif--hero svg { + display: block; + height: 100%; + width: 100%; +} + +@media (max-width: 639px) { + .product-motif--hero { + height: min(42rem, 100%); + -webkit-mask-image: linear-gradient( + to bottom, + transparent 2%, + rgba(0, 0, 0, 0.65) 14%, + black 28%, + black 66%, + rgba(0, 0, 0, 0.55) 82%, + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + transparent 2%, + rgba(0, 0, 0, 0.65) 14%, + black 28%, + black 66%, + rgba(0, 0, 0, 0.55) 82%, + transparent 100% + ); + } + + .product-motif--hero > * { + -webkit-mask-image: radial-gradient( + ellipse 110% 64% at 50% 51%, + transparent 0%, + transparent 50%, + rgba(0, 0, 0, 0.12) 58%, + rgba(0, 0, 0, 0.56) 66%, + black 74% + ); + mask-image: radial-gradient( + ellipse 110% 64% at 50% 51%, + transparent 0%, + transparent 50%, + rgba(0, 0, 0, 0.12) 58%, + rgba(0, 0, 0, 0.56) 66%, + black 74% + ); + } +} + /* Two stacked copies of the field create a seamless upward loop. */ .actors-logo-track { position: absolute; @@ -621,19 +735,34 @@ } /* Staggered centers keep a hairline gap between the larger marks. */ -.actors-card .actors-floating-logo { +.actors-floating-logo { position: absolute; top: var(--actors-logo-top); left: var(--actors-logo-left); - width: 44%; aspect-ratio: 1; transform: rotate(var(--actors-logo-tilt)); will-change: transform; +} + +.product-motif--card .actors-floating-logo { + width: 44%; animation: actors-logo-float var(--actors-logo-float-duration) ease-in-out infinite; animation-delay: var(--actors-logo-float-delay); animation-play-state: paused; } +.product-motif--hero .actors-logo-track { + height: 180%; + animation: none; + will-change: auto; +} + +.product-motif--hero .actors-floating-logo { + width: clamp(12rem, 28vw, 26rem); + animation: none; + will-change: auto; +} + .actors-card:hover .actors-floating-logo { animation-play-state: running; } @@ -651,13 +780,25 @@ opacity: var(--actors-logo-opacity); } -/* The cover follows the logo's outer contour, removing both the glyphs and - the heavy inner stroke so the remaining line matches the other cards. */ +/* The card uses the complete product mark; this accent-token cover removes + the inner glyphs while preserving the exact outer-logo silhouette. */ .actors-logo-center-cover { position: absolute; inset: 12%; border-radius: 30%; - background: #2c5a7a; +} + +.actors-logo-outline-path { + fill: none; +} + +.product-motif--hero .actors-logo--outline { + opacity: 1; +} + +.product-motif--hero .actors-logo-outline-path { + stroke: var(--motif-line-strong); + stroke-width: 1.25; } @keyframes actors-logo-rise { @@ -696,17 +837,13 @@ horizontal execution lanes. Each trace enters a task, the task resolves, then the trace continues before the sequence loops while the card is active. */ .workflow-rail { - stroke: rgba(244, 241, 231, 0.18); + stroke: var(--motif-line-soft); stroke-width: 1; } -.workflow-node-cutout { - fill: #6a4c93; -} - .workflow-node { fill: var(--workflow-node-fill); - stroke: rgba(244, 241, 231, 0.22); + stroke: var(--motif-node-stroke); stroke-width: 1; transform-box: fill-box; transform-origin: center; @@ -736,6 +873,29 @@ --workflow-node-active-fill: rgba(255, 255, 255, 0.46); } +.product-motif--hero .workflow-step { + --workflow-node-fill: rgba(27, 25, 22, 0.055); + --workflow-node-active-fill: rgba(27, 25, 22, 0.055); +} + +.product-motif--hero .workflow-node { + transition: none; +} + +.workflow-step--mobile { + display: none; +} + +@media (max-width: 639px) { + .product-motif--hero .workflow-step--default { + display: none; + } + + .product-motif--hero .workflow-step--mobile { + display: initial; + } +} + .workflow-trace { fill: none; stroke: rgba(244, 241, 231, 0.88); @@ -814,11 +974,11 @@ } .dynamic-app-circle--bright { - stroke: rgba(244, 241, 231, 0.34); + stroke: var(--motif-line-strong); } .dynamic-app-circle--soft { - stroke: rgba(244, 241, 231, 0.2); + stroke: var(--motif-line-soft); } .dynamic-apps-card:is(:hover, :focus-visible) .dynamic-app-circle--forward { @@ -899,11 +1059,16 @@ } .agentos-ring--bright { - stroke: rgba(244, 241, 231, 0.34); + stroke: var(--motif-line-strong); } .agentos-ring--soft { - stroke: rgba(244, 241, 231, 0.2); + stroke: var(--motif-line-soft); +} + +.product-motif--hero .agentos-ring { + animation: none; + opacity: 1; } .agentos-card:is(:hover, :focus-visible) .agentos-ring {