diff --git a/CLAUDE.md b/CLAUDE.md
index 7590083..2437050 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,6 +80,10 @@ without network access. NuGet restore still uses nuget.org.
- `IncludeTailwindOutput` adds the generated file to `Content` before static web assets are resolved. Without that, a
clean-clone publish would ship `app.css` without its `.gz`/`.br` copies and outside `service-worker-assets.js`.
The deploy workflow checks for `css/app.css.gz`.
+- Right after Tailwind writes `app.css`, `tools/css/motion.py` (run by `TailwindCompile` and by `tailwind.sh build` and
+ `watch`) deletes the blocks daisyUI emits for `prefers-reduced-motion: reduce` and unwraps its `no-preference` ones,
+ so its transitions and animations play for everyone. It fails the build on a query it cannot rewrite. It needs
+ `python3`.
- `bash tools/tailwind.sh watch` rebuilds the CSS live next to `dotnet watch`.
- Tailwind scans every file `.gitignore` doesn't exclude (minus `tools/` and `wwwroot/vendor/`), and only
generates the classes it finds written out **whole**. Never build a class name by concatenation or interpolation (`"btn-" + size`, `$"btn-{size}"`). Write
@@ -355,7 +359,9 @@ Never redeclare the base's members in a component.
paints any `[data-blurhash]`: a picture's until it loads, again when its source changes, and for good behind the
sensitive-media button), a picture's focal point (`object-position`), and an event's online link, participants and
status. A picture, video or sound that fails to load says "Media unavailable" with "Open original"
- (`data-media-unavailable`), and a gifv plays only on a tap under `prefers-reduced-motion`.
+ (`data-media-unavailable`), and a gifv plays by itself, muted and looping, for every reader. Blazor writes `muted`
+ only as an attribute, which leaves the video unmuted and Chromium refuses to autoplay it before the reader's first
+ tap, so `main.js` sets the `muted` property of every video carrying the attribute on its `loadstart`.
- **About** (`Pages/About.razor` at `/about`, no sign-in, `data-about`; linked from NavMenu as `data-nav-about` and from
Login as `data-about-offer`): the server's name (`data-about-title`), short description and thumbnail, its long
description (`data-about-description`, `/api/v1/instance/extended_description`), contact (`data-about-contact`), rules
@@ -612,11 +618,11 @@ Never redeclare the base's members in a component.
quick choices (`Quick`), the server's emoji by category (none first) and common unicode ones by group
(`Helpers/UnicodeEmojis.cs`, single emoji only, searched by their Mastodon/GitHub shortcode and a few words). Each
choice is `data-emoji-choice` (the shortcode, or the character) and goes to `OnPick` as `:shortcode:` or the
- character; it closes the picker first. A GIF shows its still picture under `prefers-reduced-motion`. The card's React
- button (`data-react`) and the announcements banner's (`data-announcement-react`) react with it, after their quick
- reactions; the composer's emoji button writes it where the caret is, replacing a selection, else at the end, a
- shortcode with a space on each side where a letter would touch it (`AppStatusService.CaretOf` / `PlaceCaret`,
- `main.js`'s `caretOf` and `placeCaret`).
+ character; it closes the picker first. An animated emoji moves in the picker for every reader (its `url`, never its
+ `static_url`). The card's React button (`data-react`) and the announcements banner's (`data-announcement-react`)
+ react with it, after their quick reactions; the composer's emoji button writes it where the caret is, replacing a
+ selection, else at the end, a shortcode with a space on each side where a letter would touch it
+ (`AppStatusService.CaretOf` / `PlaceCaret`, `main.js`'s `caretOf` and `placeCaret`).
- **Suggestions:** in the composer, `:` and two letters or more at the end of the words offer the matching emoji
(shortcodes starting with it first, eight at most) among the mention and hashtag suggestions, as
`data-emoji-suggestion="{shortcode}"`; one picked is written `:shortcode: `.
@@ -765,8 +771,8 @@ Never redeclare the base's members in a component.
- **The globe** (`wwwroot/js/globe.js`, an ES module imported by `GlobePanel`) runs on globe.gl, vendored in
`wwwroot/vendor/globe/` with its textures and `countries.json` (`SOURCES.md` has versions, licences and hashes; the
service worker never precaches it). It looks at the spherical mean of the located posts in view, pulses and draws an
- arc to a hovered post, honours `prefers-reduced-motion`, and draws only while something moves (idle, it costs no GPU
- time; `wake()` before anything that animates).
+ arc to a hovered post, moves the same way for every reader (camera flights, the Sun, pulses, arcs, auto-rotation),
+ and draws only while something moves (idle, it costs no GPU time; `wake()` before anything that animates).
- **Map.** Each pixel computes its own map coordinates from its point on the sphere (`sharpen`), so the poles don't
pinch; the maps wrap horizontally and get 8× anisotropic filtering (`keepSharp`). The day texture is NASA's, its
seam repaired by `tools/globe/prepare-day-texture.py` (dev-only, needs Pillow).
@@ -978,6 +984,9 @@ The nginx vhost and headers are in `deploy/nginx/` (it sends `X-Robots-Tag: noin
## Conventions
+- **Motion plays for everyone** (owner, 9 November 2026): nothing reads `prefers-reduced-motion` or any equivalent
+ (`matchMedia`, `motion-safe:`/`motion-reduce:`, a flag fed from it), so animations, GIFs and gifvs, smooth scrolling
+ and the globe behave the same for every reader.
- Indent with tabs.
- `Nullable` is disabled and `ImplicitUsings` is enabled.
- Global Razor usings are in `_Imports.razor`.
diff --git a/Components/Content.razor b/Components/Content.razor
index cee0dd0..9a387db 100644
--- a/Components/Content.razor
+++ b/Components/Content.razor
@@ -907,8 +907,7 @@
unavailableMedia.Add(media.Id)" />
,
- // under reduced motion a gifv waits for a tap (its controls)
- "gifv" => @