diff --git a/package.json b/package.json
index e103df0..f0bc4ec 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"preview": "astro preview",
"astro": "astro",
"build:icon-data": "node scripts/build-icon-data.mjs",
- "check": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-catalog.mjs && node --disable-warning=ExperimentalWarning scripts/check-business-hours.mjs && node scripts/check-news-ticker.mjs && node --disable-warning=ExperimentalWarning scripts/check-flip-box.mjs && node scripts/check-social-grid.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-four.mjs && node --disable-warning=ExperimentalWarning scripts/check-video-player.mjs && node scripts/check-cookie-consent.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-icon.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-animated-background.mjs && node --disable-warning=ExperimentalWarning scripts/check-modal-and-bar.mjs && node --disable-warning=ExperimentalWarning scripts/check-reveal-cards.mjs && node --disable-warning=ExperimentalWarning scripts/check-time-and-toggles.mjs && node --disable-warning=ExperimentalWarning scripts/check-media-and-links.mjs && node --disable-warning=ExperimentalWarning scripts/check-structure.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-feedback.mjs && node --disable-warning=ExperimentalWarning scripts/check-controls.mjs && node scripts/check-small-pieces.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-categories.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-mega-menu.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-styles.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-type.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-palettes.mjs && node --disable-warning=ExperimentalWarning scripts/check-shop.mjs"
+ "check": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-catalog.mjs && node --disable-warning=ExperimentalWarning scripts/check-business-hours.mjs && node scripts/check-news-ticker.mjs && node --disable-warning=ExperimentalWarning scripts/check-flip-box.mjs && node scripts/check-social-grid.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-four.mjs && node --disable-warning=ExperimentalWarning scripts/check-video-player.mjs && node scripts/check-cookie-consent.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-icon.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-animated-background.mjs && node --disable-warning=ExperimentalWarning scripts/check-modal-and-bar.mjs && node --disable-warning=ExperimentalWarning scripts/check-reveal-cards.mjs && node --disable-warning=ExperimentalWarning scripts/check-time-and-toggles.mjs && node --disable-warning=ExperimentalWarning scripts/check-media-and-links.mjs && node --disable-warning=ExperimentalWarning scripts/check-structure.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-feedback.mjs && node --disable-warning=ExperimentalWarning scripts/check-controls.mjs && node scripts/check-small-pieces.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-categories.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-mega-menu.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-styles.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-type.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-palettes.mjs && node --disable-warning=ExperimentalWarning scripts/check-shop.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/check-image-effects.mjs"
},
"dependencies": {
"astro": "^7.2.1"
diff --git a/public/demo/scene-city.svg b/public/demo/scene-city.svg
new file mode 100644
index 0000000..2681fcd
--- /dev/null
+++ b/public/demo/scene-city.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/demo/scene-dunes.svg b/public/demo/scene-dunes.svg
new file mode 100644
index 0000000..f043f44
--- /dev/null
+++ b/public/demo/scene-dunes.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/demo/scene-forest.svg b/public/demo/scene-forest.svg
new file mode 100644
index 0000000..da44ac8
--- /dev/null
+++ b/public/demo/scene-forest.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/demo/scene-harbor.svg b/public/demo/scene-harbor.svg
new file mode 100644
index 0000000..885b9cf
--- /dev/null
+++ b/public/demo/scene-harbor.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/demo/scene-lavender.svg b/public/demo/scene-lavender.svg
new file mode 100644
index 0000000..9b6c185
--- /dev/null
+++ b/public/demo/scene-lavender.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/demo/scene-peaks.svg b/public/demo/scene-peaks.svg
new file mode 100644
index 0000000..4b584de
--- /dev/null
+++ b/public/demo/scene-peaks.svg
@@ -0,0 +1,4 @@
+
diff --git a/scripts/check-categories.mjs b/scripts/check-categories.mjs
index 04858ff..ceda33d 100644
--- a/scripts/check-categories.mjs
+++ b/scripts/check-categories.mjs
@@ -69,6 +69,7 @@ const GOLDEN = {
'card-slider': 'media',
'testimonial-carousel': 'media',
icon: 'media',
+ 'image-effects': 'media',
countdown: 'conversion',
'announcement-bar': 'conversion',
diff --git a/scripts/check-image-effects.mjs b/scripts/check-image-effects.mjs
new file mode 100644
index 0000000..d16ad2f
--- /dev/null
+++ b/scripts/check-image-effects.mjs
@@ -0,0 +1,205 @@
+#!/usr/bin/env node
+/**
+ * npm run check (after check-catalog.mjs, which builds dist/) — pins image-effects.
+ *
+ * Source (src/library/image-effects/ImageEffects.astro)
+ * 1. The `` block names exactly the effects the catalogue's `effect` prop
+ * lists, in the same order, so the docs never offer an effect the file refuses or hide one
+ * it has.
+ * 2. The stylesheet: every transition, animation and will-change sits inside
+ * `@media (prefers-reduced-motion: no-preference)`; so do zoom's `scale` and shine's sweep.
+ * No `:hover` in it stands without a keyboard twin (`:focus-visible` or `:focus-within`) in
+ * the same selector, except tilt (the script does it) and caption's figure hover, which
+ * carries `:focus-within`.
+ * 3. The runtime: each moving block (``, ``, ``,
+ * ``) reads `still.matches` (reduced motion, tracked live), and the tile
+ * animation uses Slider Revolution's numbers: 1000 ms, 45 ms apart, power3.out.
+ *
+ * Built pages (the demo page and the gallery index)
+ * 4. The stylesheet and the runtime are on the page once; every scripted root (pixelate,
+ * tilt, tiles, gallery-open) is followed straight away by its mount line; a CSS-only root
+ * is not.
+ * 5. Every
in an element has an alt attribute.
+ * 6. tiles, as a visitor without JavaScript gets it: a named carousel, slides as groups named
+ * "n of total", the first one current, the controls `hidden` with three named buttons.
+ * On the index (`quiet`) it neither autoplays nor flies in on load.
+ *
+ * Mutation tests: the built-page checks run again on broken copies of the demo page; each must
+ * be caught, or the check itself is broken.
+ *
+ * Exits 1 with one line per failure.
+ */
+import { existsSync, readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const root = join(dirname(fileURLToPath(import.meta.url)), '..');
+const SRC = 'src/library/image-effects/ImageEffects.astro';
+const failures = [];
+const fail = (m) => failures.push(m);
+const src = readFileSync(join(root, SRC), 'utf8');
+const { catalog } = await import(pathToFileURL(join(root, 'src/data/catalog.ts')).href);
+const entry = catalog.find((e) => e.id === 'image-effects');
+
+// ------------------------------------------------------------------ 1. effect list
+const listBlock = src.match(/\/\/ ([\s\S]*?)\/\/ <\/image-effects-list>/)?.[1] ?? '';
+const quoted = (s) => [...s.matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
+const fileEffects = quoted(listBlock);
+const docEffects = quoted(entry?.props.find((p) => p.name === 'effect')?.type ?? '');
+if (!fileEffects.length) fail(`${SRC}: no block.`);
+if (fileEffects.join() !== docEffects.join()) {
+ fail(`${SRC} offers [${fileEffects.join(', ')}] but the catalogue's \`effect\` prop lists [${docEffects.join(', ')}]; keep them the same, in order.`);
+}
+
+// ------------------------------------------------------------------ 2. stylesheet
+const css = (src.match(/