diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md
index 126bfbf94..72acaa06b 100644
--- a/LOTUS_FEATURES.md
+++ b/LOTUS_FEATURES.md
@@ -405,9 +405,32 @@ A local sound plays when another participant joins or leaves a call you're in.
Files: `src/app/utils/callSounds.ts`, `src/app/hooks/useCallJoinLeaveSounds.ts`
-### Noise Suppression Toggle
+### Noise Suppression (3-Tier, incl. on-device ML) (P5-30)
-A `noiseSuppression` URL parameter is passed to the Element Call widget URL, allowing the noise suppression feature to be toggled from within Lotus settings.
+A three-way mic noise-suppression control in **Settings → General → Calls**:
+
+| Tier | What it does |
+|---|---|
+| **Off** | No suppression (`noiseSuppression=false` to Element Call). |
+| **Browser-native** | Element Call's built-in WebRTC suppressor (`noiseSuppression=true`). Default. |
+| **ML (beta)** | On-device RNNoise — Krisp-style removal of fans, keyboards, dogs, etc. |
+
+**Why a shim, not a fork:** Element Call captures the mic *inside* its iframe and publishes to LiveKit; the host can't reach that track. LiveKit's Krisp filter is LiveKit-Cloud-only (we self-host the SFU), and EC's own RNNoise work (PR #3892) is unmerged. So the **ML tier** is delivered by injecting a same-origin pre-init script into the vendored EC `index.html` that monkeypatches `getUserMedia` and routes the captured mic through an RNNoise `AudioWorklet` (`@sapphi-red/web-noise-suppressor`) before LiveKit ever sees it — the same post-capture pipeline #3892 uses, executed from the realm we already control. Works on the self-hosted LiveKit SFU, survives EC version bumps, no EC fork/AGPL/rebase burden.
+
+**How it's wired:**
+
+- `callNoiseSuppression` setting is `'off' | 'browser' | 'ml'` (legacy boolean migrates: `true`→`browser`, `false`→`off`)
+- `CallEmbed.getWidget()` maps the tier to the `noiseSuppression` URL param and appends `lotusDenoise=ml` for the ML tier (browser-native suppressor is disabled in ML mode so RNNoise owns suppression)
+- The `lotusDenoise` vite plugin copies the RNNoise worklet + wasm into `public/element-call/denoise/`, copies the shim, and injects `