diff --git a/services/site/DESIGN.md b/services/site/DESIGN.md
index 4b40e922431..0ef1040cb6b 100644
--- a/services/site/DESIGN.md
+++ b/services/site/DESIGN.md
@@ -115,7 +115,7 @@ Motion decisions follow Emil Kowalski's skills ([emilkowalski/skills](https://gi
- Show real product footage and screenshots only.
- Ship no placeholders; hide a media slot until its real content exists, as on the changelog. → [Timeline](/dev/components#changelog-timeline)
- Put heavy media (video, audio, zip, glb, wasm, anything over 256KB) on the CDN, not in git; SVGs, favicons, Open Graph images and small posters stay in git.
-- Give every video a poster; card videos rest on a still and play on hover. → [Product carousel](/dev/components#carousel)
+- Give every video a poster; card videos rest on a short loop, started from its still once the page has loaded, and play the demo on hover. → [Product carousel](/dev/components#carousel)
- Caption videos that have speech.
## Accessibility
diff --git a/services/site/MEDIA.md b/services/site/MEDIA.md
index 8eb584b73de..f195681c58e 100644
--- a/services/site/MEDIA.md
+++ b/services/site/MEDIA.md
@@ -9,7 +9,7 @@ The site's heavy media (product videos, arcade sounds, 3D models and download zi
- The build (`script/build.ts`) drops every `mediaFile` from the Worker's assets, so a local copy in `public/` never ships.
- In development, `vite dev` proxies `/files` to `https://dev.opencode.ai`, so pages play the dev bucket's copies.
- Images (`png`, `jpg`, `webp`, `avif`, `svg`), posters, fonts and the 3D decoders (`arcade3d/draco`, `arcade3d/basis`) stay in git and in the static assets.
-- Only the H.264 originals are served. The AV1 and HEVC copies `media-manifest.json` once described were never uploaded, so `videoSources()` offers the original alone.
+- `videoSources()` offers a video's AV1 and HEVC copies (`media/....mp4`, listed in `src/component/site/media-manifest.json`) before the H.264 original. The manifest lists only copies that are in the bucket; a video without copies plays its original.
| Folder | Files |
| --- | --- |
diff --git a/services/site/media-manifest.txt b/services/site/media-manifest.txt
index 403e4f69196..77de2ed5684 100644
--- a/services/site/media-manifest.txt
+++ b/services/site/media-manifest.txt
@@ -102,6 +102,8 @@ b3b147cdb22ec2117d539c18490c7103adaa088a13600852b5a5363f667ee051 186628 /brand
8998172a4ff22a6b292da18c87a21998728bc86f94b3828e536298ca6b00da54 149085 /brand/badges/opencode-works-with.zip
140f2c17fa7235f2199a3db22f1eda3eccbfb8ce98da39ee7b1e3e9eb9387846 6801928 /font/film/opencode-font-film-v3-1080p.mp4
c0d7932b3c2f5d27368e2039e580c169ac94802b8317260e5e679551c41e8461 6300093 /font/film/opencode-font-film-v3-4x5.mp4
+a8d0f5bc1395a756affd9cff1cd0db46e97fba0c1d72508d0e665c62a8a92c2d 146992 /media/card-loops/mobile-orb-75.720.av1.a8d0f5bc.mp4
+a82f1012a18962731b12ee4b07da137a8a89165b3aef838705c43d75d186ef90 336142 /media/card-loops/mobile-orb-75.720.hevc.a82f1012.mp4
927d3f29a862b2af9853d2ba951f29e4ff381e0c5f1b08c7d8b34cd62b7128e2 2818872 /opencode-brand-assets.zip
0f1d6dfc747f2d88650b2073bb04cd41380241adfa4de3e2f070bdc4bb367579 272426 /press/anomaly/anomaly-logo.zip
9cba0d1d2108c8af4e22cc54cafcd8532101d6bbd2809ac18dd26ff1aaf615b3 5999115 /press-media/zips/opencode-desktop-media.zip
diff --git a/services/site/script/check-client-bundle.ts b/services/site/script/check-client-bundle.ts
index ba9771928a4..f7fc1277aba 100644
--- a/services/site/script/check-client-bundle.ts
+++ b/services/site/script/check-client-bundle.ts
@@ -66,23 +66,23 @@ console.log(
const budgets: Record = {
"[...404].tsx": 104.1,
"brand/index.tsx": 104.5,
- "changelog/[tag].tsx": 97.8,
- "changelog/index.tsx": 97,
- "desktop.tsx": 126.5,
+ "changelog/[tag].tsx": 99.2,
+ "changelog/index.tsx": 98.4,
+ "desktop.tsx": 127.9,
"devices.tsx": 56.8,
- "product/browse.tsx": 102.8,
- "font/index.tsx": 118.7,
- "ide.tsx": 126.5,
- "index.tsx": 113.4,
+ "product/browse.tsx": 104.2,
+ "font/index.tsx": 120.1,
+ "ide.tsx": 127.9,
+ "index.tsx": 114.9,
"legal/index.tsx": 148.3,
"legal/privacy-policy/index.tsx": 137.8,
"legal/terms-of-service/index.tsx": 133.2,
- "mini.tsx": 126.5,
- "mobile.tsx": 126.5,
- "press/index.tsx": 111.8,
- "product/index.tsx": 111.3,
- "sdk.tsx": 126.5,
- "terminal.tsx": 126.5,
+ "mini.tsx": 127.9,
+ "mobile.tsx": 127.9,
+ "press/index.tsx": 113.2,
+ "product/index.tsx": 112.7,
+ "sdk.tsx": 127.9,
+ "terminal.tsx": 127.9,
"video-editor/index.tsx": 55.2,
}
type Chunk = { file: string; imports?: string[] }
diff --git a/services/site/src/component/site/media-manifest.json b/services/site/src/component/site/media-manifest.json
index 752fe62eafb..dc3cb7cb477 100644
--- a/services/site/src/component/site/media-manifest.json
+++ b/services/site/src/component/site/media-manifest.json
@@ -4,30 +4,6 @@
[1920, "av1", "f4aac9d0", 8],
[1920, "hevc", "9a8fdd1e", 120]
],
- "/product-videos/card-hover/angled-continue/desktop.mp4": [
- [1080, "av1", "3d944be5", 8, "hi"],
- [720, "av1", "8c2f11ae", 4],
- [1080, "hevc", "06dafc49", 120, "hi"],
- [720, "hevc", "dd4ef775", 93]
- ],
- "/product-videos/card-hover/angled-continue/mobile.mp4": [
- [1080, "av1", "1d02d18f", 8, "hi"],
- [720, "av1", "fc122802", 4],
- [1080, "hevc", "029f5c19", 120, "hi"],
- [720, "hevc", "c94ebb22", 93]
- ],
- "/product-videos/card-hover/angled-continue/sdk.mp4": [
- [1080, "av1", "2eef5975", 8, "hi"],
- [720, "av1", "35b2a488", 4],
- [1080, "hevc", "d7a76c34", 120, "hi"],
- [720, "hevc", "270b3d5f", 93]
- ],
- "/product-videos/card-hover/angled-continue/terminal.mp4": [
- [1080, "av1", "de1a465e", 8, "hi"],
- [720, "av1", "32caf68d", 4],
- [1080, "hevc", "1fb02df6", 120, "hi"],
- [720, "hevc", "383908d2", 93]
- ],
"/product-videos/card-hover/angled/desktop.mp4": [
[1080, "av1", "000848bb", 8, "hi"],
[720, "av1", "7a8ced27", 4],
@@ -52,54 +28,6 @@
[1080, "hevc", "5802fcd0", 120, "hi"],
[720, "hevc", "33b9daa2", 93]
],
- "/product-videos/card-hover/cinematic-continue/desktop.mp4": [
- [1080, "av1", "79e8094e", 8, "hi"],
- [720, "av1", "50aaddc3", 4],
- [1080, "hevc", "efc2e19a", 120, "hi"],
- [720, "hevc", "7b98ff3e", 93]
- ],
- "/product-videos/card-hover/cinematic-continue/mobile.mp4": [
- [1080, "av1", "735cceb4", 8, "hi"],
- [720, "av1", "97fa967a", 4],
- [1080, "hevc", "d0d339af", 120, "hi"],
- [720, "hevc", "ed5ead76", 93]
- ],
- "/product-videos/card-hover/cinematic-continue/sdk.mp4": [
- [1080, "av1", "5b45cb59", 8, "hi"],
- [720, "av1", "e2cc8efd", 4],
- [1080, "hevc", "fb1bc058", 120, "hi"],
- [720, "hevc", "e0752306", 93]
- ],
- "/product-videos/card-hover/cinematic-continue/terminal.mp4": [
- [1080, "av1", "c6c59bed", 8, "hi"],
- [720, "av1", "2f9535de", 4],
- [1080, "hevc", "6995f361", 120, "hi"],
- [720, "hevc", "80276e87", 93]
- ],
- "/product-videos/card-hover/cinematic/desktop.mp4": [
- [1080, "av1", "829c3da8", 8, "hi"],
- [720, "av1", "598a8997", 4],
- [1080, "hevc", "176438ec", 120, "hi"],
- [720, "hevc", "70baa9dc", 93]
- ],
- "/product-videos/card-hover/cinematic/mobile.mp4": [
- [1080, "av1", "0c67eb98", 8, "hi"],
- [720, "av1", "b5fa7ce2", 4],
- [1080, "hevc", "5abf1f1e", 120, "hi"],
- [720, "hevc", "d9b47452", 93]
- ],
- "/product-videos/card-hover/cinematic/sdk.mp4": [
- [1080, "av1", "0de9145c", 8, "hi"],
- [720, "av1", "847247e6", 4],
- [1080, "hevc", "1c34707c", 120, "hi"],
- [720, "hevc", "0a03196c", 93]
- ],
- "/product-videos/card-hover/cinematic/terminal.mp4": [
- [1080, "av1", "9776cf70", 8, "hi"],
- [720, "av1", "76a5c8b0", 4],
- [1080, "hevc", "fa24ac2a", 120, "hi"],
- [720, "hevc", "24c9487e", 93]
- ],
"/product-videos/card-loops/desktop.ja.mp4": [
[1064, "av1", "8d53f4c4", 8, "hi"],
[720, "av1", "03051f4d", 4],
@@ -117,11 +45,9 @@
[1064, "hevc", "de5e01b6", 120, "hi"],
[720, "hevc", "2adfbc82", 93]
],
- "/product-videos/card-loops/mobile.mp4": [
- [1064, "av1", "969038f3", 8, "hi"],
- [720, "av1", "0ad54196", 4],
- [1064, "hevc", "b522b660", 120, "hi"],
- [720, "hevc", "13e6540a", 93]
+ "/product-videos/card-loops/mobile-orb-75.mp4": [
+ [720, "av1", "a8d0f5bc", 4],
+ [720, "hevc", "a82f1012", 93]
],
"/product-videos/card-loops/sdk.mp4": [
[1064, "av1", "57dfbb5d", 8, "hi"],
@@ -135,30 +61,6 @@
[1064, "hevc", "cb91d991", 120, "hi"],
[720, "hevc", "e8322dc4", 93]
],
- "/product-videos/features/desktop-1.mp4": [
- [1200, "av1", "ff4a4260", 5, "hi"],
- [800, "av1", "ef555136", 4],
- [1200, "hevc", "9cdc5d28", 93, "hi"],
- [800, "hevc", "d0e2cfef", 90]
- ],
- "/product-videos/features/desktop-2.mp4": [
- [1200, "av1", "b92cab97", 5, "hi"],
- [800, "av1", "34d0517c", 4],
- [1200, "hevc", "d598ea33", 93, "hi"],
- [800, "hevc", "96b8b510", 90]
- ],
- "/product-videos/features/desktop-3.mp4": [
- [1200, "av1", "10f94e71", 5, "hi"],
- [800, "av1", "47a5a8a9", 4],
- [1200, "hevc", "aa172509", 93, "hi"],
- [800, "hevc", "ba0c50ee", 90]
- ],
- "/product-videos/features/desktop-4.mp4": [
- [1200, "av1", "6cfaeefa", 5, "hi"],
- [800, "av1", "0316f39c", 4],
- [1200, "hevc", "5719cc56", 93, "hi"],
- [800, "hevc", "1fcebe5c", 90]
- ],
"/product-videos/features/focused/desktop-1.mp4": [
[1200, "av1", "a4464782", 5, "hi"],
[800, "av1", "ff6b7fc8", 4],
@@ -231,70 +133,6 @@
[1200, "hevc", "ddc881b8", 93, "hi"],
[800, "hevc", "1bfc3877", 90]
],
- "/product-videos/features/mobile-1.mp4": [
- [1200, "av1", "dc15a609", 5, "hi"],
- [800, "av1", "95c72454", 4],
- [1200, "hevc", "8013fea3", 93, "hi"],
- [800, "hevc", "be07c96c", 90]
- ],
- "/product-videos/features/mobile-2.mp4": [
- [1200, "av1", "3a868173", 5, "hi"],
- [800, "av1", "b333f8f4", 4],
- [1200, "hevc", "d1cc1d25", 93, "hi"],
- [800, "hevc", "27840268", 90]
- ],
- "/product-videos/features/mobile-3.mp4": [
- [1200, "av1", "0924e4e2", 5, "hi"],
- [800, "av1", "a5a118d2", 4],
- [1200, "hevc", "596b8211", 93, "hi"],
- [800, "hevc", "b819f9f5", 90]
- ],
- "/product-videos/features/mobile-4.mp4": [
- [1200, "av1", "3eb473b1", 5, "hi"],
- [800, "av1", "dea200a1", 4],
- [1200, "hevc", "b066c55e", 93, "hi"],
- [800, "hevc", "10402b50", 90]
- ],
- "/product-videos/features/sdk-1.mp4": [
- [1200, "av1", "eead75dc", 5, "hi"],
- [800, "av1", "8d1247ee", 4],
- [1200, "hevc", "11e6218a", 93, "hi"],
- [800, "hevc", "c62e3038", 90]
- ],
- "/product-videos/features/sdk-2.mp4": [
- [1200, "av1", "178c6b50", 5, "hi"],
- [800, "av1", "d52a969f", 4],
- [1200, "hevc", "810895c4", 93, "hi"],
- [800, "hevc", "2d8678dc", 90]
- ],
- "/product-videos/features/sdk-3.mp4": [
- [1200, "av1", "fe427d04", 5, "hi"],
- [800, "av1", "22015ab8", 4],
- [1200, "hevc", "fa20dfe7", 93, "hi"],
- [800, "hevc", "3f1c498c", 90]
- ],
- "/product-videos/features/terminal-1.mp4": [
- [1200, "av1", "8e233258", 5, "hi"],
- [800, "av1", "4fa5af14", 4],
- [1200, "hevc", "17334020", 93, "hi"],
- [800, "hevc", "d614132e", 90]
- ],
- "/product-videos/features/terminal-2.mp4": [
- [1200, "av1", "65d38182", 5, "hi"],
- [800, "av1", "f0cbdd1a", 4],
- [1200, "hevc", "561a6203", 93, "hi"],
- [800, "hevc", "a617561a", 90]
- ],
- "/product-videos/features/terminal-3.mp4": [
- [1200, "av1", "9e447bd5", 5, "hi"],
- [800, "av1", "0a311ac9", 4],
- [1200, "hevc", "8615f916", 93, "hi"],
- [800, "hevc", "f43c3475", 90]
- ],
- "/product-videos/features/terminal-4.mp4": [
- [800, "av1", "114cdf52", 4],
- [800, "hevc", "4123e308", 90]
- ],
"/product-videos/heroes/desktop-landscape-dark.mp4": [
[1280, "av1", "87488462", 5, "phone"],
[1920, "av1", "476dd57a", 8],
diff --git a/services/site/src/component/site/media-sources.json b/services/site/src/component/site/media-sources.json
index ee098d73ced..322fc7c0986 100644
--- a/services/site/src/component/site/media-sources.json
+++ b/services/site/src/component/site/media-sources.json
@@ -77,6 +77,7 @@
"/product-videos/card-loops/mobile-orb-100.webp": "ea6453e6b91748ba",
"/product-videos/card-loops/mobile-orb-25.webp": "41f7dc05c0f80eea",
"/product-videos/card-loops/mobile-orb-50.webp": "20859953b02bfb65",
+ "/product-videos/card-loops/mobile-orb-75.mp4": "41351d6aaa74c958",
"/product-videos/card-loops/mobile-orb-75.webp": "7facb79fd8e4b423",
"/product-videos/card-loops/mobile.mp4": "8c1efd91fe0f25ca",
"/product-videos/card-loops/mobile.webp": "0854c8b479cb9fb6",
diff --git a/services/site/src/component/site/media.tsx b/services/site/src/component/site/media.tsx
index f3967caf99d..4a91ad3575c 100644
--- a/services/site/src/component/site/media.tsx
+++ b/services/site/src/component/site/media.tsx
@@ -7,6 +7,10 @@ import { mediaUrl } from "~/lib/media"
* for where they are drawn and content-hashed so they cache for a year. media-manifest.json lists only
* copies that met the quality bar (VMAF ≥ 95 and SSIM ≥ 0.98 for video, SSIMULACRA2 ≥ 85 for
* stills) against the original, which stays in place as the last fallback. Bundled at build time.
+ * The Mobile orb loop's copies are the exception: every dot of its grid changes brightness in every
+ * frame, so no copy much smaller than the original scores VMAF 95. Its 720-wide AV1 (147 kB) and HEVC
+ * (336 kB) copies, against the 578 kB original, score SSIM 0.99 and were matched by eye at the
+ * card's size.
*/
// Copy names follow one pattern, so the manifest holds only what varies (media-opt/manifest.py):
// /media/....mp4 and /media/...,
@@ -19,26 +23,39 @@ type Manifest = {
}
type MediaKey = "hi" | "phone"
-// Only the stills' half: the pages offer the original videos alone (videoSources), so the video
-// copies' rows stay out of the bundle.
-const images: Manifest["image"] =
- Object.values(import.meta.glob("./media-manifest.json", { eager: true, import: "image" }))[0] ?? {}
+const manifest: Manifest = Object.values(
+ import.meta.glob("./media-manifest.json", { eager: true, import: "default" }),
+)[0] ?? { video: {}, image: {} }
+
+// The 1080-wide card and 1200-wide clip files go to screens of 2.5 device pixels per CSS pixel or
+// more (most phones); the 1280-wide hero files to phone-width windows.
+const queries: Record = {
+ hi: "(min-resolution: 2.5dppx)",
+ phone: "(max-width: 40rem)",
+}
const base = (src: string) => `/media/${src.replace(/^\/(product-videos\/)?/, "").replace(/\.\w+$/, "")}`
export type VideoSource = { src: string; type?: string; media?: string }
/**
- * A video's source: the original H.264 file in the files bucket. The manifest's AV1 and HEVC copies
- * were never uploaded, so they are not offered.
+ * A video's sources in the order the browser should try them, all in the files bucket: the AV1
+ * copies, the HEVC ones (Safari without an AV1 decoder), then the original H.264 file.
*/
export function videoSources(src: string): VideoSource[] {
- return [{ src: mediaUrl(src), type: "video/mp4" }]
+ return [
+ ...(manifest.video[src] ?? []).map(([width, codec, hash, level, key]) => ({
+ src: mediaUrl(`${base(src)}.${width}.${codec}.${hash}.mp4`),
+ type: `video/mp4; codecs="${codec === "av1" ? `av01.0.${String(level).padStart(2, "0")}M.08` : `hvc1.1.6.L${level}.B0`}"`,
+ media: key ? queries[key] : undefined,
+ })),
+ { src: mediaUrl(src), type: "video/mp4" },
+ ]
}
/** A still's AVIF and WebP srcsets (if it has copies), and the file for `src`: the smallest WebP. */
export function imageSources(src: string) {
- const entry = images[src]
+ const entry = manifest.image[src]
const file = (format: "avif" | "webp", [width, hash]: [number, string]) => `${base(src)}.${width}.${hash}.${format}`
const set = (format: "avif" | "webp") =>
entry?.[format]?.map((item) => `${file(format, item)} ${item[0]}w`).join(", ")
diff --git a/services/site/src/component/site/products.tsx b/services/site/src/component/site/products.tsx
index 5711f7d51c1..82d87774873 100644
--- a/services/site/src/component/site/products.tsx
+++ b/services/site/src/component/site/products.tsx
@@ -582,9 +582,9 @@ export function carouselCss() {
* ("hover · reverse") or the still fades back over it ("hover · fade"). "autoplay" plays all the
* time. Reduced motion keeps the still.
*
- * With `loop`, the card rests on that short idle loop instead of a still: it plays while the card
- * is on screen, crossfades to the product video on hover and back on leave. Reduced motion shows
- * its first frame (`still`).
+ * With `loop`, the card rests on that short idle loop instead of a still: it shows its first frame
+ * (`still`) until the page has loaded, then plays while the card is on screen, crossfades to the
+ * product video on hover and back on leave. Reduced motion and Save-Data keep the first frame.
*/
export function CardVideo(props: {
src: string
@@ -652,10 +652,17 @@ export function CardVideo(props: {
}
createEffect(speed)
- // The idle loop plays only while the card is on screen and resting.
+ // Reduced motion and Save-Data keep a card on its still: no resting loop and no sizzle.
+ const cover = () =>
+ prefersReducedMotion() ||
+ (navigator as typeof window.navigator & { connection?: { saveData?: boolean } }).connection?.saveData === true
+
+ // The idle loop plays only while the card is on screen and resting, and only once the page has
+ // loaded (ready), so it never competes with the stills: it starts from the frame the still shows.
+ let ready = false
const rest = () => {
if (!idle) return
- if (visible && !active && mode() !== "autoplay" && !prefersReducedMotion()) {
+ if (ready && visible && !active && mode() !== "autoplay" && !cover()) {
void idle.play().catch(() => {})
return
}
@@ -774,8 +781,6 @@ export function CardVideo(props: {
// Leaving pauses the sizzle where it is and coming back resumes it, unless the card was away
// for over 30s: then it starts again from the cover. Reduced motion and Save-Data keep the cover.
setSizzle(window.matchMedia("(hover: none), (pointer: coarse)").matches || isMobile(detectPlatform()))
- const connection = (navigator as typeof window.navigator & { connection?: { saveData?: boolean } }).connection
- const cover = () => prefersReducedMotion() || connection?.saveData === true
let wait: ReturnType | undefined
let away = 0
// Back to the cover with the sizzle rewound, without a fade (the card is off screen).
@@ -821,7 +826,13 @@ export function CardVideo(props: {
{ rootMargin: "100% 50%" },
)
if (media) ahead.observe(media)
+ const start = () => {
+ ready = true
+ rest()
+ }
+ afterLoad(start)
onCleanup(() => {
+ window.removeEventListener("load", start)
view.disconnect()
ahead.disconnect()
clearTimeout(wait)
@@ -839,13 +850,16 @@ export function CardVideo(props: {
diff --git a/services/site/src/routes/index.css b/services/site/src/routes/index.css
index b92142a50e0..2cd8267563f 100644
--- a/services/site/src/routes/index.css
+++ b/services/site/src/routes/index.css
@@ -1796,6 +1796,7 @@
width: 1.25rem;
height: 1.25rem;
background: currentColor;
+ mask-image: var(--logo);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
@@ -1806,11 +1807,18 @@
height: calc(var(--height) * var(--logo-size));
translate: 0 calc(var(--shift) * var(--logo-size));
background: currentColor;
+ mask-image: var(--logo);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
}
+ /* Until the grid is near the screen (data-near, ToolGrid), its logos stay empty masks, so their
+ SVGs don't load with the page. */
+ &:not([data-near]) :is([data-slot="icon"], [data-slot="wordmark"]) {
+ mask-image: linear-gradient(transparent 0 0);
+ }
+
/* The typed name stays as the tile's accessible text. */
[data-slot="name"] {
position: absolute;
diff --git a/services/site/src/routes/index.tsx b/services/site/src/routes/index.tsx
index f645a56500a..215354ef72c 100644
--- a/services/site/src/routes/index.tsx
+++ b/services/site/src/routes/index.tsx
@@ -1351,6 +1351,20 @@ function ToolGrid(props: {
return wash.armed() && style ? { animation: style, vars: revealMotion(style, siteSettings.logoTuning) } : undefined
}
createEffect(on([animation, () => siteSettings.toolsStagger], wash.replay, { defer: true }))
+ // The tiles' logos (SVG masks, index.css) load once the grid is within a screen of the window, so
+ // a page doesn't fetch them with its first screen.
+ onMount(() => {
+ const near = new IntersectionObserver(
+ (entries) => {
+ if (!entries.some((entry) => entry.isIntersecting)) return
+ grid?.setAttribute("data-near", "")
+ near.disconnect()
+ },
+ { rootMargin: "100% 0px" },
+ )
+ if (grid) near.observe(grid)
+ onCleanup(() => near.disconnect())
+ })
return (
{props.iconTiles ? null : tool.name}
>
@@ -1404,8 +1418,7 @@ function ToolGrid(props: {
data-slot="wordmark"
aria-hidden="true"
style={{
- "mask-image": `url(${tool.wordmark})`,
- "-webkit-mask-image": `url(${tool.wordmark})`,
+ "--logo": `url(${tool.wordmark})`,
...(tool.metrics ? balanceLogo(tool.metrics) : {}),
}}
/>