diff --git a/apps/web/docs/accessibility.md b/apps/web/docs/accessibility.md
new file mode 100644
index 0000000..c5e95fd
--- /dev/null
+++ b/apps/web/docs/accessibility.md
@@ -0,0 +1,231 @@
+# Frontend accessibility guide
+
+The accessibility standard `apps/web` targets, and the patterns required to meet it:
+keyboard navigation through the conversation list and message composer, focus management
+in modals and the safety-number panel, screen-reader announcement of incoming messages,
+and colour contrast.
+
+This document describes both the pattern to follow and, honestly, where the current code
+already meets it and where it does not yet. Where a gap is called out, treat it as
+something to fix when you touch that area, not as the intended design.
+
+---
+
+## Target standard
+
+**WCAG 2.1 Level AA.** This is the conventional baseline for a web application handling
+real user-to-user communication, and is the standard assumed throughout this document —
+there is no stricter internal bar and no formal deviation from it.
+
+**How it is checked today: manually, not automatically.** There is no `eslint-plugin-jsx-a11y`,
+no automated axe/Lighthouse run in CI, and no accessibility test suite in this repo as of
+this writing. Conformance currently depends entirely on the patterns below being followed
+by hand and reviewed in PRs. If you are adding accessibility tooling, wiring an automated
+check (axe-core in CI, or `eslint-plugin-jsx-a11y` at minimum) closes a real gap rather
+than adding redundant coverage — until then, the [pre-merge checklist](#pre-merge-checklist-for-a-new-interactive-component)
+below is the actual enforcement mechanism.
+
+---
+
+## Keyboard navigation
+
+### Conversation list
+
+`components/conversations/ConversationListSidebar.tsx` renders each conversation as a
+plain ``:
+
+```tsx
+
+ {/* avatar, title, preview */}
+
+```
+
+There is no custom keyboard handling — no roving `tabindex`, no arrow-key list navigation.
+Keyboard support comes entirely from using a real `` (an anchor): it is naturally
+focusable, appears in Tab order, and activates on Enter, all for free. **This is the
+required pattern for list items in this app** — a clickable row must be a real
+link/button, never a `
`, precisely so keyboard support does not have to be
+hand-built. Arrow-key roving-tabindex navigation (a full ARIA `listbox`/`menu` pattern) is
+not implemented and is not required — sequential Tab order through the list is the
+supported navigation model.
+
+### Message composer
+
+The composer (`app/app/conversations/[id]/page.tsx`) supports Enter-to-send /
+Shift+Enter-for-newline on the message input:
+
+```tsx
+ setSendText(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ void handleSendEncrypted();
+ }
+ }}
+/>
+```
+
+When adding a keyboard shortcut like this, always `preventDefault()` only on the branch
+that consumes the key (here, plain Enter) and let every other key (including Shift+Enter)
+fall through untouched — do not swallow keys you are not handling.
+
+The composer's send and attach-file buttons are real `