diff --git a/docs/superpowers/specs/2026-03-15-user-avatar-design.md b/docs/superpowers/specs/2026-03-15-user-avatar-design.md
new file mode 100644
index 0000000..01ddf09
--- /dev/null
+++ b/docs/superpowers/specs/2026-03-15-user-avatar-design.md
@@ -0,0 +1,112 @@
+# User Avatar — Design Spec
+
+## Goal
+
+Allow users to upload a profile avatar image (with client-side circular crop) that replaces initials everywhere in the app.
+
+## Backend
+
+### Entity Changes
+
+**User** — add nullable field:
+- `avatarFileName: ?string` (length 255) — UUID-based filename stored on disk
+
+### Storage
+
+- Directory: `var/uploads/avatars/`
+- Parameter in `services.yaml`: `avatar_upload_dir`
+
+### Endpoints
+
+All under `/api/users/{id}/avatar`:
+
+| Method | Description | Auth |
+|--------|-------------|------|
+| `POST` | Upload avatar (multipart file) | Owner or ROLE_ADMIN |
+| `GET` | Serve avatar image (inline) | ROLE_USER or ROLE_CLIENT |
+| `DELETE` | Remove avatar | Owner or ROLE_ADMIN |
+
+**POST** accepts a single `file` field. Validates: image MIME (jpeg, png, webp, gif), max 5 MB. Stores with UUID filename, updates `avatarFileName`. Deletes previous file if exists.
+
+**GET** returns the image with proper `Content-Type`. Returns 404 if no avatar.
+
+**DELETE** removes file from disk, sets `avatarFileName` to null.
+
+These are custom Symfony controllers (not API Platform resources) under `/api/` with `priority: 1`.
+
+### Serialization
+
+Add a virtual `avatarUrl` field to User serialization (group `user:read`):
+- If `avatarFileName` is set: `/api/users/{id}/avatar`
+- If null: `null`
+
+This way the frontend knows if an avatar exists from any user payload.
+
+### Migration
+
+- Add `avatar_file_name` column (VARCHAR 255, nullable) to `user` table.
+
+## Frontend
+
+### New Components
+
+**`UserAvatar.vue`** (`frontend/components/user/UserAvatar.vue`):
+- Props: `user: { id: number, username: string, avatarUrl?: string | null }`, `size: 'xs' | 'sm' | 'md' | 'lg'`
+- Sizes: xs=20px, sm=24px, md=32px, lg=48px
+- If `avatarUrl`: `
` rounded-full, object-cover
+- Else: initials badge (current bg-primary-500 style), 2 first chars of username uppercased
+- Handles `@error` on img to fallback to initials (broken image)
+
+**`AvatarCropper.vue`** (`frontend/components/user/AvatarCropper.vue`):
+- Uses `vue-advanced-cropper` with `CircleStencil`
+- Props: `imageFile: File`
+- Emits: `crop(blob: Blob)`, `cancel`
+- Fixed output size: 256x256px
+- Modal overlay with crop area + confirm/cancel buttons
+
+### New Page
+
+**`/profile`** (`frontend/pages/profile.vue`):
+- Shows current avatar (large) with "Change" button
+- File input triggers AvatarCropper modal
+- On confirm: POST blob to `/api/users/{id}/avatar`
+- On success: refresh auth store user data
+- "Remove avatar" button if avatar exists
+- Accessible from "Mon profil" button in AppTopNav dropdown
+
+### New Service
+
+**`frontend/services/avatar.ts`**:
+- `upload(userId: number, file: Blob): Promise` — POST multipart
+- `remove(userId: number): Promise` — DELETE
+- `getUrl(userId: number): string` — returns URL path
+
+### DTO Update
+
+**`UserData`** — add: `avatarUrl?: string | null`
+
+### Replacement Points
+
+Replace initials/icon with `` in:
+
+| File | Current display | Size |
+|------|----------------|------|
+| `TaskCard.vue:48-53` | Initials badge (h-5 w-5) | xs |
+| `archives.vue:50-55` | Initials badge (h-5 w-5) | xs |
+| `AppTopNav.vue:13` | `mdi:account-circle-outline` icon | md |
+| `AdminClientTicketTab.vue` | Username text for submitter | sm |
+| `ClientTicketDetailModal.vue` | submittedBy display | sm |
+
+### Auth Store
+
+After avatar upload/delete, re-fetch current user data so `avatarUrl` updates everywhere reactively.
+
+## Dependencies
+
+- `vue-advanced-cropper` — npm install in frontend/
+
+## Out of Scope
+
+- Server-side image processing/resize
+- Multiple image formats conversion
+- Avatar for clients (entities), only users