diff --git a/DESIGN.md b/DESIGN.md
index 0a1e5b5adf6..376e563dc8d 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -168,3 +168,11 @@ tokenize motion. Principles — reasoning only; values live in `ui/src/index.css
in addition to each animation's own component-level guard.
Agent Chat and regular task chats keep pending questions as compact “Unanswered question” entries at their original position in history. Dismissing the form or sending a newer user message clears it from the composer without resolving it. Questions never contribute to composer pending counts or navigation. Opening the history entry restores the original form and its draft; submitting later uses the same durable question response path. Actual permission reviews retain their permission checks.
+
+## Offline recovery
+
+The service worker recovery page uses a centered character, a dark surface, and
+a visible Reload page action. Keep the character inline and embed the marked
+`offline-recovery` section from `ui/src/index.css` at build time so the page needs
+no network requests or cached assets. Preserve the supplied character palette
+through the offline tokens.
diff --git a/ui/public/sw.js b/ui/public/sw.js
index 7f443c6a001..da8437c2f8c 100644
--- a/ui/public/sw.js
+++ b/ui/public/sw.js
@@ -5,6 +5,7 @@
// reloads parked tabs onto the fresh bundle. Left as the literal placeholder in
// dev, where HMR (not the worker) drives refreshes.
const BUILD_ID = "__PAPERCLIP_BUILD_ID__";
+const OFFLINE_STYLES = "__PAPERCLIP_OFFLINE_STYLES__";
// Separate this allowlisted cache from older workers that cached arbitrary URLs.
const CACHE_NAME = `paperclip-public-assets-${BUILD_ID}`;
const privateRequests = new Set();
@@ -15,9 +16,61 @@ function offlineNavigationResponse() {
return new Response(`
-Paperclip is offline
-
Paperclip is offline
+Paperclip is offline
+
+
+
+
+
+
Paperclip is offline
+
Check your connection, then reload this page to try again.