diff --git a/.gitignore b/.gitignore
index 13c74945b..6f39e04e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ devAssets
.DS_Store
.ideapackage-lock.json
+public/decorations/
diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md
index e44870473..126bfbf94 100644
--- a/LOTUS_FEATURES.md
+++ b/LOTUS_FEATURES.md
@@ -11,20 +11,21 @@ Last updated: June 2026.
2. [LotusGuild Terminal Design System (TDS) v1.2](#lotusguild-terminal-design-system-tds-v12)
3. [Animated Chat Backgrounds (P5-4)](#animated-chat-backgrounds-p5-4)
4. [Seasonal Theme Overlays (P5-12)](#seasonal-theme-overlays-p5-12)
-5. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
-6. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
-7. [Voice / Video Call Improvements](#voice--video-call-improvements)
-8. [Per-Message Read Receipts](#per-message-read-receipts)
-9. [Delivery Status Indicators](#delivery-status-indicators)
-10. [Messaging Enhancements](#messaging-enhancements)
-11. [Presence](#presence)
-12. [UX & Composer](#ux--composer)
-13. [Room Customization](#room-customization)
-14. [Moderation](#moderation)
-15. [Notifications](#notifications)
-16. [Server Integration](#server-integration)
-17. [Infrastructure](#infrastructure)
-18. [Key Custom Files](#key-custom-files)
+5. [Avatar Decorations (P5-13/P5-14)](#avatar-decorations-p5-13p5-14)
+6. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
+7. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
+8. [Voice / Video Call Improvements](#voice--video-call-improvements)
+9. [Per-Message Read Receipts](#per-message-read-receipts)
+10. [Delivery Status Indicators](#delivery-status-indicators)
+11. [Messaging Enhancements](#messaging-enhancements)
+12. [Presence](#presence)
+13. [UX & Composer](#ux--composer)
+14. [Room Customization](#room-customization)
+15. [Moderation](#moderation)
+16. [Notifications](#notifications)
+17. [Server Integration](#server-integration)
+18. [Infrastructure](#infrastructure)
+19. [Key Custom Files](#key-custom-files)
---
@@ -200,6 +201,75 @@ Decorative CSS-only overlays that activate automatically on holidays and events.
---
+## Avatar Decorations (P5-13/P5-14)
+
+Animated APNG overlay frames that float around user avatars, inspired by Discord's Avatar Decorations feature. Each decoration extends 8px beyond the avatar border on all sides, with a transparent center hole that reveals the avatar beneath. Other Lotus Chat users see your selected decoration in real time — stored in the Matrix profile via MSC4133.
+
+### Decoration Library
+
+99 hand-curated, original-IP decorations (no licensed character artwork) organized into 9 categories:
+
+| Category | Count | Highlights |
+|---|---|---|
+| Gaming | 13 | Slither 'n Snack, Joystick, Space Invaders, Gaming Headsets |
+| Cyber | 9 | Cybernetic, Glitch, Digital Sunrise, Futuristic UI (3 colors) |
+| Space | 8 | Black Hole, Constellations, Solar Orbit, Aurora |
+| Fantasy | 22 | Kitsune, Phoenix, Glowing Runes, D&D dice, Crystal Balls |
+| Elements | 7 | Fire, Water, Air, Earth, Lightning, Ki Energy |
+| Japanese | 6 | Kabuto, Oni Mask, Sakura Warrior, Straw Hat |
+| Nature | 12 | **Lotus Flower**, Koi Pond, Sakura, Fall Leaves, Fireflies |
+| Spooky | 13 | Candlelight, Witch Hat, Ghosts, Jack-o'-Lantern |
+| Cozy | 11 | Cozy Cat, Fox Hat (3 colors), Cat Ears, Frog Hat |
+
+All decoration files are 256×256 APNGs. They animate natively in all modern browsers via `
` elements.
+
+### Architecture
+
+**Profile storage — MSC4133:**
+Decoration preference is stored in the public Matrix profile field `io.lotus.avatar_decoration` (a slug string, e.g. `lotus_flower`). Any Lotus Chat user viewing your profile sees your current decoration.
+
+**CDN:**
+Files are self-hosted on the Lotus Nextcloud instance. Direct access: `https://drive.lotusguild.org/public.php/dav/files/{token}/cinny-decorations/{slug}.png`. `
` elements load cross-origin freely — no CORS headers needed.
+
+**Module-level cache with in-flight deduplication:**
+`useAvatarDecoration(userId)` fetches the profile field once per user per session. A `Map` cache prevents redundant requests; a second `pending` waiters map ensures multiple components requesting the same userId simultaneously share one HTTP request rather than firing duplicates.
+
+**Wrapping pattern:**
+`AvatarDecoration` renders a `position: relative; display: inline-flex` wrapper div. The decoration `
` is `position: absolute` with `top/left/right/bottom: -8px`, extending equally on all sides while the `z-index: 10` keeps it above the avatar. `onError` hides the image if the CDN file is absent. This wrapper sits outside `PresenceRingAvatar` so the presence ring and decoration layer are fully independent.
+
+### Placement — Where Decorations Render
+
+| Location | File |
+|---|---|
+| Message timeline | `src/app/features/room/message/Message.tsx` |
+| Members drawer | `src/app/features/room/MembersDrawer.tsx` |
+| `@mention` autocomplete | `src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx` |
+| Inbox / notifications | `src/app/pages/client/inbox/Notifications.tsx` |
+
+### Settings — Decoration Picker
+
+**Settings → Account → Avatar Decoration** shows a scrollable grid of all decorations, grouped by category. Each cell is a 52×52px button with a live preview of the APNG. The currently selected decoration gets a 2px cyan border. "No Decoration" clears the field. Changes are saved only when the "Save" button is clicked (visible only when a change is pending). After save, `invalidateDecorationCache(userId)` forces other components to re-fetch.
+
+### Catalog Sync Script
+
+After deleting decoration files from the Nextcloud share, run:
+
+```bash
+npm run sync:decorations
+```
+
+The script (`scripts/syncDecorations.mjs`) sends HTTP HEAD requests to the CDN URL for every slug in `avatarDecorations.ts` and automatically removes entries for files that returned 404. Empty categories are pruned automatically. Review with `git diff`.
+
+### Files
+
+- `src/app/features/lotus/avatarDecorations.ts` — full catalog (`DECORATION_CATEGORIES`, `ALL_DECORATIONS`, `decorationUrl()`, `DECORATION_CDN`)
+- `src/app/hooks/useAvatarDecoration.ts` — profile fetch, module-level cache, `invalidateDecorationCache()`
+- `src/app/components/avatar-decoration/AvatarDecoration.tsx` — wrapper component with APNG overlay
+- `src/app/features/settings/account/ProfileDecoration.tsx` — settings UI (picker grid, save button)
+- `scripts/syncDecorations.mjs` — CDN sync script to prune deleted decorations from the catalog
+
+---
+
## Glassmorphism Sidebar (P5-3)
An optional frosted-glass sidebar style toggled in **Settings → Appearance**.
@@ -969,3 +1039,8 @@ The `encUrlPreview` setting defaults to `true` rather than `false`. A security a
| `src/app/hooks/useExtendedProfile.ts` | MSC4133 extended profile fields (`m.pronouns`, `m.tz`) read/write |
| `src/app/hooks/useLocalTime.ts` | Derives current local time from `m.tz` profile field, updates every 60s |
| `src/app/components/url-preview/UrlPreviewCard.tsx` | 13 domain-specific URL preview layouts plus generic fallback with favicon |
+| `src/app/features/lotus/avatarDecorations.ts` | Avatar decoration catalog, CDN URL, `decorationUrl()` helper |
+| `src/app/hooks/useAvatarDecoration.ts` | Profile field fetch with module-level cache and in-flight deduplication |
+| `src/app/components/avatar-decoration/AvatarDecoration.tsx` | APNG overlay wrapper rendered around avatars in timeline, members drawer, autocomplete |
+| `src/app/features/settings/account/ProfileDecoration.tsx` | Settings decoration picker — scrollable grid, category headers, save button |
+| `scripts/syncDecorations.mjs` | CDN HEAD-check sync script: removes catalog entries for deleted Nextcloud files |
diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md
index 92a18416d..f6d730b61 100644
--- a/LOTUS_TODO.md
+++ b/LOTUS_TODO.md
@@ -260,7 +260,7 @@ Themes:
---
-### [ ] P5-12 · Seasonal / Event Themes
+### [x] P5-12 · Seasonal / Event Themes
**What:** Automatic + manually toggleable seasonal overlays with CSS particle effects and accent color variants:
@@ -274,7 +274,7 @@ Themes:
---
-### [ ] P5-13 · Avatar Frame / Border Decorations
+### [x] P5-13 · Avatar Frame / Border Decorations
**What:** Decorative CSS rings/frames rendered around user avatars. Built-in options: TDS Glow (animated orange pulsing), Cyberpunk (rotating gradient), Minimal (thin ring), Gold (supporter cosmetic). Stored in Matrix account data `io.lotus.avatar_frame`. Only visible in Lotus Chat.
**[AUDIT REQUIRED]** Verify folds Avatar component allows overlay decoration without breaking child-type constraints (see previous white-circle avatar bug).
@@ -282,7 +282,7 @@ Themes:
---
-### [ ] P5-14 · Animated Avatar Overlay Decorations (Discord-style)
+### [x] P5-14 · Animated Avatar Overlay Decorations (Discord-style)
**What:** Animated WebM/GIF overlays that float around avatars (transparent center showing avatar). Curated built-in set OR user-uploaded mxc:// overlay. Stored in account data. Only Lotus Chat users see them.
**[AUDIT REQUIRED]** See #P5-13 audit. Also decide: curated set only vs user-uploadable.
@@ -355,7 +355,7 @@ Themes:
---
-### [ ] P5-34 · User-to-User Private Notes
+### [x] P5-34 · User-to-User Private Notes
**What:** A private "Notes" field on user profiles visible only to you. Syncs across all your devices.
**Matrix Tech:** Store in `io.lotus.user_notes` account data. Must be keyed by `userId`.
diff --git a/README.md b/README.md
index c6fa3a11f..40156c90b 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina
- 20+ static chat background patterns
- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies (with improved per-layer looping, phosphor-flicker rain, fluid aurora sweep, and organic firefly bioluminescence)
- 11 seasonal & holiday theme overlays — Halloween, Christmas, New Year, Autumn, Valentine's Day, St. Patrick's Day, Earth Day, Lunar New Year, April Fools', Deep Space, and Retro Arcade; auto-selected by date with a manual override in Settings → Appearance
+- Avatar decorations — 99 animated APNG overlays (Gaming, Cyber, Space, Fantasy, Nature, Spooky, Cozy, and more) that frame your avatar across the timeline, members list, and @mention autocomplete; visible to all Lotus Chat users; select in Settings → Account → Avatar Decoration
- Toggle to pause background animations
- Glassmorphism sidebar — frosted glass effect that lets the background show through
- Night Light / blue light filter with an adjustable intensity slider
diff --git a/package.json b/package.json
index 7896c320f..e2c1af6cf 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
"typecheck": "tsc --noEmit",
"prepare": "husky",
"commit": "git-cz",
- "postinstall": "node scripts/patch-folds.mjs"
+ "postinstall": "node scripts/patch-folds.mjs",
+ "sync:decorations": "node scripts/syncDecorations.mjs"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": "eslint",
diff --git a/scripts/syncDecorations.mjs b/scripts/syncDecorations.mjs
new file mode 100644
index 000000000..78009e0ea
--- /dev/null
+++ b/scripts/syncDecorations.mjs
@@ -0,0 +1,89 @@
+#!/usr/bin/env node
+/**
+ * Syncs avatarDecorations.ts with what's actually available on the Nextcloud CDN.
+ *
+ * Usage:
+ * npm run sync:decorations
+ *
+ * Workflow after deleting files from Nextcloud:
+ * 1. Delete decoration files from your Nextcloud share.
+ * 2. Run: npm run sync:decorations
+ * 3. It probes each catalog slug via HTTP HEAD and removes entries
+ * whose files returned 404. Empty categories are dropped automatically.
+ * 4. Commit the updated avatarDecorations.ts.
+ */
+
+import { readFileSync, writeFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const root = join(__dirname, '..');
+const catalogPath = join(root, 'src', 'app', 'features', 'lotus', 'avatarDecorations.ts');
+
+const CDN =
+ 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations';
+
+// Extract all slugs from the catalog file
+const catalog = readFileSync(catalogPath, 'utf8');
+const slugMatches = [...catalog.matchAll(/slug: '([^']+)'/g)].map((m) => m[1]);
+
+if (slugMatches.length === 0) {
+ console.error('No slugs found in catalog — check the file path.');
+ process.exit(1);
+}
+
+console.log(`Checking ${slugMatches.length} decorations against ${CDN} …`);
+console.log('(This makes one HEAD request per decoration)\n');
+
+// Probe all slugs in parallel batches of 16
+async function headCheck(slug) {
+ try {
+ const res = await fetch(`${CDN}/${slug}.png`, { method: 'HEAD' });
+ return { slug, ok: res.ok, status: res.status };
+ } catch {
+ return { slug, ok: false, status: 0 };
+ }
+}
+
+const BATCH = 16;
+const results = [];
+for (let i = 0; i < slugMatches.length; i += BATCH) {
+ const batch = slugMatches.slice(i, i + BATCH);
+ const batchResults = await Promise.all(batch.map(headCheck));
+ results.push(...batchResults);
+}
+
+const missing = results.filter((r) => !r.ok);
+const found = results.filter((r) => r.ok);
+
+if (missing.length === 0) {
+ console.log(`All ${found.length} decorations are available — catalog is up to date.`);
+ process.exit(0);
+}
+
+console.log(`Found: ${found.length} Missing: ${missing.length}\n`);
+missing.forEach((r) =>
+ console.log(` Removing (HTTP ${r.status}): ${r.slug}`),
+);
+
+const missingSet = new Set(missing.map((r) => r.slug));
+
+// Remove individual entries for missing slugs
+let updated = catalog.replace(
+ /^[ \t]*\{ slug: '([^']+)', name: .+\},?\r?\n/gm,
+ (match, slug) => (missingSet.has(slug) ? '' : match),
+);
+
+// Drop category blocks that now have an empty decorations array
+updated = updated.replace(
+ / \{\n id: '[^']+',\n label: '[^']+',\n decorations: \[\n?[ \t]*\],?\n \},?\n/g,
+ '',
+);
+
+// Clean up stray blank lines
+updated = updated.replace(/\n{3,}/g, '\n\n');
+
+writeFileSync(catalogPath, updated, 'utf8');
+console.log(`\nDone. Removed ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} from the catalog.`);
+console.log('Review with: git diff src/app/features/lotus/avatarDecorations.ts');
diff --git a/src/app/components/avatar-decoration/AvatarDecoration.tsx b/src/app/components/avatar-decoration/AvatarDecoration.tsx
index 9ffcd0c10..ea32df1ed 100644
--- a/src/app/components/avatar-decoration/AvatarDecoration.tsx
+++ b/src/app/components/avatar-decoration/AvatarDecoration.tsx
@@ -46,6 +46,9 @@ export function AvatarDecoration({ userId, children }: AvatarDecorationProps) {
aria-hidden="true"
loading="lazy"
decoding="async"
+ onError={(e) => {
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
+ }}
/>
);
diff --git a/src/app/features/lotus/avatarDecorations.ts b/src/app/features/lotus/avatarDecorations.ts
index 8007a413b..59be0198c 100644
--- a/src/app/features/lotus/avatarDecorations.ts
+++ b/src/app/features/lotus/avatarDecorations.ts
@@ -1,4 +1,5 @@
-export const DECORATION_CDN = 'https://img.avatardecoration.com/decorations';
+export const DECORATION_CDN =
+ 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations';
export type AvatarDecoration = {
slug: string;