diff --git a/apps/sim/app/(landing)/components/cta/components/cta-chat.tsx b/apps/sim/app/(landing)/components/cta/components/cta-chat.tsx
deleted file mode 100644
index 21f62a5a1ef..00000000000
--- a/apps/sim/app/(landing)/components/cta/components/cta-chat.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { LandingPreviewChatInput } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input'
-import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/hooks/use-landing-submit'
-import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
-
-/**
- * Pre-footer CTA chat input - the page's final conversion surface. A real,
- * interactive copy of the Mothership chat input: the visitor types their first
- * prompt, and {@link useLandingSubmit} stashes it in browser storage and routes
- * to `/signup` so the message survives the auth hop and lands them in Sim. The
- * placeholder cycles the same "Ask Sim to …" examples as the product's home
- * empty state, so the CTA reads as the front door to the workspace.
- */
-export function CtaChat() {
- const [value, setValue] = useState('')
- const placeholder = useAnimatedPlaceholder()
- const submit = useLandingSubmit()
-
- return (
-
- submit(value)}
- placeholder={placeholder}
- shadow
- />
-
- )
-}
diff --git a/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx b/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx
index 9fb215f17cc..fb3ccaff2ca 100644
--- a/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx
+++ b/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx
@@ -913,11 +913,16 @@ export function HeroVisual() {
return () => cancelAnimationFrame(raf)
}, [loaderPainting, paintFrame])
+ const phaseRef = useRef(phase)
useEffect(() => {
- const onResize = () => positionCursor(phase, true)
+ phaseRef.current = phase
+ }, [phase])
+
+ useEffect(() => {
+ const onResize = () => positionCursor(phaseRef.current, true)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
- }, [phase, positionCursor])
+ }, [positionCursor])
useEffect(
() => () => {
diff --git a/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx b/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx
index 204d0cc50ad..58d82704c2a 100644
--- a/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx
+++ b/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx
@@ -1,18 +1,30 @@
'use client'
+import { useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
+/** Dimension-stable placeholder sized to the preview's exact footprint (zero CLS). */
+const PLACEHOLDER_CLASS = 'aspect-[1116/615] w-full rounded bg-[var(--surface-1)]'
+
+/**
+ * Load the preview chunk a little before it scrolls into view so it's ready by
+ * the time the user reaches it, without paying for it on initial load.
+ */
+const PRELOAD_ROOT_MARGIN = '400px'
+
/**
* Client mount for the {@link LandingPreview} - the heavy, animated workspace
* island (framer-motion + reactflow). Isolated here so the sections that show it
* stay Server Components: only this leaf is `'use client'`.
*
* Loaded with `ssr: false` so the framer-motion/reactflow bundle never ships in
- * the server-rendered HTML, and behind a dimension-stable placeholder sized to
- * the preview's exact `aspect-[1116/615]` footprint so there is zero layout
- * shift while it streams in. The placeholder fills with the canvas surface
- * (`--surface-1`) so there is no flash as the island mounts.
+ * the server-rendered HTML, and **gated on viewport proximity**: the chunk only
+ * downloads once an {@link IntersectionObserver} reports the mount is near the
+ * viewport, so the below-the-fold previews don't pull the heavy bundle into the
+ * initial homepage load. A dimension-stable placeholder (the preview's exact
+ * `aspect-[1116/615]` footprint, filled with the canvas surface) holds the space
+ * before and during load, so there is zero layout shift or flash.
*/
const LandingPreview = dynamic(
() =>
@@ -21,7 +33,7 @@ const LandingPreview = dynamic(
),
{
ssr: false,
- loading: () => ,
+ loading: () => ,
}
)
@@ -35,5 +47,36 @@ interface LandingPreviewMountProps {
}
export function LandingPreviewMount({ autoplay, view, workflowId }: LandingPreviewMountProps) {
- return
+ const ref = useRef(null)
+ const [inView, setInView] = useState(false)
+
+ useEffect(() => {
+ if (inView) return
+ // Graceful degradation: without IntersectionObserver support, load eagerly
+ // rather than leave the preview stuck on its placeholder.
+ if (typeof IntersectionObserver === 'undefined') {
+ setInView(true)
+ return
+ }
+ const el = ref.current
+ if (!el) return
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) setInView(true)
+ },
+ { rootMargin: PRELOAD_ROOT_MARGIN }
+ )
+ observer.observe(el)
+ return () => observer.disconnect()
+ }, [inView])
+
+ return (
+
+ {inView ? (
+
+ ) : (
+
+ )}
+
+ )
}
diff --git a/apps/sim/app/(landing)/integrations/components/integration-card.tsx b/apps/sim/app/(landing)/integrations/components/integration-card.tsx
index 646e47417b3..3749dd00a94 100644
--- a/apps/sim/app/(landing)/integrations/components/integration-card.tsx
+++ b/apps/sim/app/(landing)/integrations/components/integration-card.tsx
@@ -1,4 +1,5 @@
import type { ComponentType, SVGProps } from 'react'
+import { memo } from 'react'
import Link from 'next/link'
import type { Integration } from '@/lib/integrations'
import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow'
@@ -46,7 +47,10 @@ export function IntegrationCard({ integration, IconComponent }: IntegrationItemP
* Integration list row - matches blog remaining post pattern.
* Each row followed by an h-px divider.
*/
-export function IntegrationRow({ integration, IconComponent }: IntegrationItemProps) {
+export const IntegrationRow = memo(function IntegrationRow({
+ integration,
+ IconComponent,
+}: IntegrationItemProps) {
const { slug, name, description, bgColor } = integration
return (
@@ -80,4 +84,4 @@ export function IntegrationRow({ integration, IconComponent }: IntegrationItemPr
>
)
-}
+})
diff --git a/apps/sim/app/(landing)/integrations/components/integration-grid.tsx b/apps/sim/app/(landing)/integrations/components/integration-grid.tsx
index a78827c500f..a226fec75cc 100644
--- a/apps/sim/app/(landing)/integrations/components/integration-grid.tsx
+++ b/apps/sim/app/(landing)/integrations/components/integration-grid.tsx
@@ -1,5 +1,6 @@
'use client'
+import { useMemo } from 'react'
import { ChipInput, Search } from '@sim/emcn'
import { debounce, useQueryStates } from 'nuqs'
import { blockTypeToIconMap, formatIntegrationType, type Integration } from '@/lib/integrations'
@@ -28,29 +29,42 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
)
const activeCategory = category || null
- const counts = new Map()
- for (const i of integrations) {
- if (i.integrationType) {
- counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
+ // Category facets and a per-integration lowercased search index, derived once
+ // from the (stable) integration list instead of rebuilt on every keystroke.
+ // The index keeps each searchable field as its own entry so matching stays
+ // identical to a per-field `includes` (no cross-field boundary matches).
+ const { availableCategories, searchIndex } = useMemo(() => {
+ const counts = new Map()
+ const searchIndex = new Map()
+ for (const i of integrations) {
+ if (i.integrationType) {
+ counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
+ }
+ searchIndex.set(i.type, [
+ i.name.toLowerCase(),
+ i.description.toLowerCase(),
+ ...i.operations.flatMap((op) => [op.name.toLowerCase(), op.description.toLowerCase()]),
+ ...i.triggers.map((t) => t.name.toLowerCase()),
+ ])
}
- }
- const availableCategories = Array.from(counts.entries())
- .sort((a, b) => b[1] - a[1])
- .map(([key]) => key)
+ return {
+ availableCategories: Array.from(counts.entries())
+ .sort((a, b) => b[1] - a[1])
+ .map(([key]) => key),
+ searchIndex,
+ }
+ }, [integrations])
const q = query.trim().toLowerCase()
- const filtered = integrations.filter((i) => {
- if (activeCategory && i.integrationType !== activeCategory) return false
- if (!q) return true
- return (
- i.name.toLowerCase().includes(q) ||
- i.description.toLowerCase().includes(q) ||
- i.operations.some(
- (op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
- ) ||
- i.triggers.some((t) => t.name.toLowerCase().includes(q))
- )
- })
+ const filtered = useMemo(
+ () =>
+ integrations.filter((i) => {
+ if (activeCategory && i.integrationType !== activeCategory) return false
+ if (!q) return true
+ return searchIndex.get(i.type)?.some((field) => field.includes(q)) ?? false
+ }),
+ [integrations, searchIndex, q, activeCategory]
+ )
return (
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff
deleted file mode 100644
index 704a2e2f63f..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff2
deleted file mode 100644
index 4c66183c587..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff2 and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff
deleted file mode 100644
index 567c2d8350a..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff2
deleted file mode 100644
index 99d28b0ed7c..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff2 and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff
deleted file mode 100644
index 067216a9944..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff2
deleted file mode 100644
index 8a09772bf27..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff2 and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff
deleted file mode 100644
index e6ed31d2d50..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff2
deleted file mode 100644
index 7ce737db982..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff2 and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff
deleted file mode 100644
index db82e64468d..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff2
deleted file mode 100644
index 2b98eb29b2b..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff2 and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff
deleted file mode 100644
index a8c35b1f237..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff2
deleted file mode 100644
index 073a47af351..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff2 and /dev/null differ
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSansUprightsVF.woff b/apps/sim/app/_styles/fonts/season/SeasonSansUprightsVF.woff
deleted file mode 100644
index bead4f5df89..00000000000
Binary files a/apps/sim/app/_styles/fonts/season/SeasonSansUprightsVF.woff and /dev/null differ
diff --git a/apps/sim/public/landing/sim-mothership.webp b/apps/sim/public/landing/sim-mothership.webp
deleted file mode 100644
index 140d5f2ef0c..00000000000
Binary files a/apps/sim/public/landing/sim-mothership.webp and /dev/null differ
diff --git a/apps/sim/public/static/mothership.gif b/apps/sim/public/static/mothership.gif
deleted file mode 100644
index 7d35f17c572..00000000000
Binary files a/apps/sim/public/static/mothership.gif and /dev/null differ