diff --git a/CLAUDE.md b/CLAUDE.md
index 26e0f7f3..330bec4a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1670,8 +1670,8 @@ Structure (mirrors the consumer-test's plumbing):
   working data** (the copied model `current-model.gguf` + cache) on every **cold start**
   (`onCreate`) — a privacy guarantee independent of the OS calling `onDestroy`; `MainActivity` also
   wipes best-effort on finish. Only the **opt-in saved session** persists. **`MainActivity.kt`** —
-  Compose UI with a **two-row app bar** (row 1 = horizontally-scrollable model name on its own line,
-  row 2 = all action icons) + SAF picker + flag menu + Save/Load + the ⚙️ Settings sheet
+  Compose UI with a **two-row app bar** (row 1 = horizontally-scrollable model name + a ❌ unload
+  button when a model is loaded, row 2 = all action icons) + SAF picker + flag menu + Save/Load + the ⚙️ Settings sheet
   (sampling knobs + a **Model** section: CPU threads / context length applied via **Reload model**) +
   the 🧾 log strip/viewer + Stop/Copy/Regenerate/Clear chat actions (+ **long-press any bubble to
   copy**) + **prompt shortcut chips** + a **device-readiness card** (free RAM / storage / battery via
diff --git a/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/ChatViewModel.kt b/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/ChatViewModel.kt
index 9d3b09c7..d79a9afd 100644
--- a/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/ChatViewModel.kt
+++ b/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/ChatViewModel.kt
@@ -341,6 +341,34 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
         log("Chat cleared")
     }
 
+    /**
+     * Unloads the current model: cancels any generation, frees the native memory, and deletes the
+     * copied working model file (privacy + storage). The chat history is kept (still saveable /
+     * copyable), and the UI returns to the "no model loaded" state so a new model can be picked.
+     * No-op if no model is loaded.
+     */
+    fun unloadModel() {
+        if (model == null && _uiState.value.modelState == ModelState.NONE) {
+            return
+        }
+        generation?.cancel()
+        generation = null
+        val toClose = model
+        model = null
+        modelPath = null
+        chatTemplate = null
+        _uiState.update {
+            it.copy(modelState = ModelState.NONE, modelName = null, generating = false, error = null)
+        }
+        log("Model unloaded")
+        viewModelScope.launch(Dispatchers.IO) {
+            toClose?.close()
+            // Drop the copied working model (frees storage; nothing lingers). A model loaded by an
+            // absolute path (test / session restore) has no copy here, so this is then a no-op.
+            File(getApplication<Application>().filesDir, MODEL_COPY_NAME).delete()
+        }
+    }
+
     /** Saves the current conversation (and its model path) to private local storage. */
     fun saveSession() {
         val snapshot = _uiState.value
diff --git a/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/MainActivity.kt b/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/MainActivity.kt
index fdb66621..f990abcb 100644
--- a/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/MainActivity.kt
+++ b/android-llmservice/app/src/main/kotlin/net/ladenthin/android/llmservice/MainActivity.kt
@@ -164,17 +164,24 @@ private fun ChatScreen(viewModel: ChatViewModel) {
             // readable / draggable), and all the action icons sit on a second line below it.
             Surface(shadowElevation = 3.dp) {
                 Column(modifier = Modifier.fillMaxWidth().statusBarsPadding()) {
-                    // Row 1: model name, horizontally scrollable (drag a long name into view; no
-                    // auto-marquee wobble).
-                    Text(
-                        text = state.modelName ?: stringResource(R.string.app_name),
-                        style = MaterialTheme.typography.titleLarge,
-                        maxLines = 1,
-                        softWrap = false,
-                        modifier = Modifier.fillMaxWidth()
-                            .horizontalScroll(rememberScrollState())
-                            .padding(horizontal = 16.dp, vertical = 10.dp),
-                    )
+                    // Row 1: model name (horizontally scrollable — drag a long name into view; no
+                    // auto-marquee wobble) + a ❌ button to unload the model when one is loaded.
+                    Row(verticalAlignment = Alignment.CenterVertically) {
+                        Text(
+                            text = state.modelName ?: stringResource(R.string.app_name),
+                            style = MaterialTheme.typography.titleLarge,
+                            maxLines = 1,
+                            softWrap = false,
+                            modifier = Modifier.weight(1f)
+                                .horizontalScroll(rememberScrollState())
+                                .padding(horizontal = 16.dp, vertical = 10.dp),
+                        )
+                        if (state.modelState == ChatViewModel.ModelState.READY) {
+                            IconButton(onClick = { viewModel.unloadModel() }) {
+                                Text("❌", modifier = Modifier.testTag("unloadButton"))
+                            }
+                        }
+                    }
                     // Row 2: all action icons on their own line.
                     Row(
                         modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
diff --git a/android-llmservice/requirements.md b/android-llmservice/requirements.md
index da3fe808..c3e4a1ec 100644
--- a/android-llmservice/requirements.md
+++ b/android-llmservice/requirements.md
@@ -60,6 +60,7 @@ by hand only (no automated test); `build` = enforced at build/resource-compile t
 | R3.5 | Loading a new model **closes** the previously loaded one (native memory is not GC-managed). | `ChatViewModel.openModel` | manual |
 | R3.6 | Recommended real model for coherent replies: an instruct GGUF (default **Gemma 3 4B Instruct**); base/completion models still run. | `README.md` | manual |
 | R3.7 | The model-picker screen shows a **device-readiness card**: free/total **RAM** (`ActivityManager.MemoryInfo`), free **storage** (`StatFs`), and **battery** level + charging (`BatteryManager`) — so the user can gauge whether a model fits before loading. Read once at composition. | `DeviceInfo`; `MainActivity.DeviceCard` | manual |
+| R3.8 | **Unload model:** a ❌ button next to the model name (shown only when a model is loaded) frees the native model from memory, deletes the copied working model file, and returns to the "no model loaded" state. The **chat history is kept** (still saveable/copyable); a generation in flight is cancelled first. | `MainActivity` (`unloadButton`); `ChatViewModel.unloadModel` | manual |
 
 ## R4 — Chat & streaming
 
@@ -127,7 +128,7 @@ by hand only (no automated test); `build` = enforced at build/resource-compile t
 
 | ID | Requirement | Source | Verified by |
 |---|---|---|---|
-| R10.1 | The app bar is **two rows**: row 1 is the **model-name title on its own full-width line**, **horizontally scrollable** (draggable) so a long name can be read in full (single line, no auto-marquee); row 2 holds **all action icons** (save / load / clear / settings / language) on their own line below it. | `MainActivity` top bar | manual |
+| R10.1 | The app bar is **two rows**: row 1 is the **model-name title** (own full-width line, **horizontally scrollable** / draggable, single line, no auto-marquee) plus a **❌ unload button** at its right when a model is loaded (R3.8); row 2 holds **all action icons** (save / load / clear / settings / language) on their own line below it. | `MainActivity` top bar | manual |
 | R10.2 | The release `signingConfig` reads an **upload keystore** from env vars / `-P` props, and **falls back to debug signing** when none is set (so forks/PRs/local builds stay green). **Upgrade caveat:** the debug fallback uses an ephemeral per-runner key, so debug-signed CI APKs are **not** mutually upgradeable (a signer change is rejected → uninstall required); stable in-place upgrades need the upload-key secrets. | `app/build.gradle.kts` | build |
 | R10.3 | The build produces a release **`.aab`** (for Play) and an installable release **`.apk`** (sideload); the Maven Central GPG key cannot sign these — Android needs a Java keystore upload key. | `app/build.gradle.kts`; `README.md` | build |
 | R10.4 | `versionCode` is **monotonic** (derived from `GITHUB_RUN_NUMBER` in CI, strictly increasing per run; `-PappVersionCode` overrides; local hand builds use `1`), so successive release APKs advertise a higher version and Android accepts the in-place upgrade (given a stable signer, R10.2). | `app/build.gradle.kts` | build |
