changeset 273:e02e2036ef84 default tip

add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Sat, 08 Aug 2026 02:08:08 -0700
parents 41a49c29a28f
children
files mrjunejune/PWA_SETUP.md mrjunejune/main.c mrjunejune/src/jrpg/index.html mrjunejune/src/jrpg/jrpg.css mrjunejune/src/jrpg/jrpg.js mrjunejune/src/public/component-sandbox.css mrjunejune/src/public/component-sandbox.html mrjunejune/src/public/composer-lab.html mrjunejune/src/public/mjj-composer.js mrjunejune/src/public/mjj-modal.css mrjunejune/src/public/mjj-modal.js mrjunejune/src/public/pwa-register.js mrjunejune/src/sw.js mrjunejune/test/snapshots/jrpg.snapshot mrjunejune/test/theme_and_webp_test.js
diffstat 15 files changed, 2067 insertions(+), 610 deletions(-) [+]
line wrap: on
line diff
--- a/mrjunejune/PWA_SETUP.md	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/PWA_SETUP.md	Sat Aug 08 02:08:08 2026 -0700
@@ -58,7 +58,7 @@
 ✅ **Offline Support** - Caches pages, CSS, JS, fonts, images
 ✅ **App Shortcuts** - Quick access to Blog and Notes
 ✅ **Install Prompt** - Automatic install button
-✅ **Auto-updates** - Service worker updates on reload
+✅ **Auto-updates** - New service workers activate and reload clients automatically
 ✅ **Fast Loading** - Cached resources load instantly
 
 ## Customization
@@ -72,7 +72,13 @@
 Edit `sw.js` to change:
 - `CACHE_VERSION` - Increment to force cache refresh
 - `STATIC_CACHE` - Files to cache immediately
-- Caching strategy (currently: cache-first with network fallback)
+- Caching strategy:
+  - HTML, CSS, JS, JSON, and WASM are network-first with offline cache fallback.
+  - Fonts and images are stale-while-revalidate.
+  - Component sandbox assets always bypass service-worker caching.
+
+Unversioned application code must not use cache-first. That can preserve stale
+production CSS or JavaScript indefinitely when a deployment reuses the same URL.
 
 ## Testing on Mobile
 
--- a/mrjunejune/main.c	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/main.c	Sat Aug 08 02:08:08 2026 -0700
@@ -1214,12 +1214,145 @@
   return resp;
 }
 
+typedef struct {
+  const char *name;
+  const char *title;
+  const char *copy;
+  const char *works;
+} Jrpg_Initial_Panel;
+
+static boolean Mjj_Replace_All(
+    char *buffer,
+    size_t capacity,
+    const char *needle,
+    const char *replacement)
+{
+  if (!buffer || !needle || !replacement || needle[0] == '\0')
+    return FALSE;
+
+  size_t needle_length = strlen(needle);
+  size_t replacement_length = strlen(replacement);
+  char *match = strstr(buffer, needle);
+  while (match)
+  {
+    size_t current_length = strlen(buffer);
+    size_t offset = (size_t)(match - buffer);
+    size_t new_length =
+        current_length - needle_length + replacement_length;
+    if (new_length >= capacity)
+      return FALSE;
+
+    memmove(
+        match + replacement_length,
+        match + needle_length,
+        current_length - offset - needle_length + 1);
+    memcpy(match, replacement, replacement_length);
+    match = strstr(match + replacement_length, needle);
+  }
+  return TRUE;
+}
+
+static const Jrpg_Initial_Panel *Jrpg_Resolve_Initial_Panel(
+    Seobeo_Request_Entry *req)
+{
+  static const Jrpg_Initial_Panel panels[] = {
+    {
+      "resume",
+      "Resume",
+      "Member of Technical Staff and engineering leader with 10+ years "
+      "building AI agent platforms and production systems across Microsoft, "
+      "Meta, Google, and growth-stage companies.",
+      "<li><a href=\"/resume\" data-resume-modal>"
+      "<span>Full resume</span><small>Career dossier</small></a></li>"
+      "<li><a href=\"https://www.microsoft.com/en-us/microsoft-copilot/blog/"
+      "2026/02/26/copilot-tasks-from-answers-to-actions/\">"
+      "<span>Copilot Tasks</span><small>Agentic execution</small></a></li>"
+    },
+    {
+      "tools",
+      "Tools",
+      "Useful browser tools backed by first-party C, WASM, media, and "
+      "document-processing systems.",
+      "<li><a href=\"/tools/markdown_to_html\" data-latest-tool "
+      "data-tool-url=\"/tools/markdown_to_html\">"
+      "<span>Markdown</span><small>Writing</small></a></li>"
+      "<li><a href=\"/tools/file_converter\" data-latest-tool "
+      "data-tool-url=\"/tools/file_converter\">"
+      "<span>Converter</span><small>Media</small></a></li>"
+      "<li><a href=\"/tools/hls_player\" data-latest-tool "
+      "data-tool-url=\"/tools/hls_player\">"
+      "<span>HLS Player</span><small>Streaming</small></a></li>"
+    },
+    {
+      "blog",
+      "Blogs",
+      "Technical writing about networking, rendering, performance, developer "
+      "tooling, and experiments.",
+      "<li><a href=\"/blog\" data-blog-archive>"
+      "<span>All posts</span><small>Archive</small></a></li>"
+    },
+    {
+      "conversations",
+      "Resume",
+      "",
+      ""
+    },
+  };
+
+  const char *requested = NULL;
+  void *panel_kv = Dowa_HashMap_Get_Ptr(req, "query_panel");
+  if (panel_kv)
+    requested = ((Seobeo_Request_Entry *)panel_kv)->value;
+
+  for (size_t i = 0; i < sizeof(panels) / sizeof(panels[0]); ++i)
+  {
+    if (requested && strcmp(requested, panels[i].name) == 0)
+      return &panels[i];
+  }
+  return &panels[0];
+}
+
 Seobeo_Request_Entry *GetJrpg(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
   char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
   if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/jrpg/index.html", arena))
     return html_render_error(arena, "Internal Server Error");
+
+  const Jrpg_Initial_Panel *panel = Jrpg_Resolve_Initial_Panel(req);
+  boolean conversations =
+      strcmp(panel->name, "conversations") == 0 ? TRUE : FALSE;
+  const char *preview_selection = conversations ? "resume" : panel->name;
+  struct {
+    const char *token;
+    const char *value;
+  } replacements[] = {
+    {"__MJJ_PANEL__", panel->name},
+    {"__MJJ_PREVIEW_SELECTION__", preview_selection},
+    {"__MJJ_PREVIEW_TITLE__", panel->title},
+    {"__MJJ_PREVIEW_COPY__", panel->copy},
+    {"__MJJ_PREVIEW_WORKS__", panel->works},
+    {"__MJJ_PREVIEW_HIDDEN__", conversations ? "hidden" : ""},
+    {"__MJJ_ARCHIVE_HIDDEN__", conversations ? "" : "hidden"},
+    {"__MJJ_RESUME_PRESSED__",
+      strcmp(panel->name, "resume") == 0 ? "true" : "false"},
+    {"__MJJ_TOOLS_PRESSED__",
+      strcmp(panel->name, "tools") == 0 ? "true" : "false"},
+    {"__MJJ_BLOG_PRESSED__",
+      strcmp(panel->name, "blog") == 0 ? "true" : "false"},
+    {"__MJJ_CONVERSATIONS_PRESSED__", conversations ? "true" : "false"},
+  };
+  for (size_t i = 0;
+       i < sizeof(replacements) / sizeof(replacements[0]);
+       ++i)
+  {
+    if (!Mjj_Replace_All(
+          final_body,
+          HTML_PAGE_CAP,
+          replacements[i].token,
+          replacements[i].value))
+      return html_render_error(arena, "Internal Server Error");
+  }
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena);
   return resp;
--- a/mrjunejune/src/jrpg/index.html	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/jrpg/index.html	Sat Aug 08 02:08:08 2026 -0700
@@ -7,13 +7,18 @@
     <link rel="preload" href="/public/jrpg/background-frame-mobile.webp" as="image" media="(max-width: 52rem) and (orientation: portrait)">
     <link rel="preload" href="/public/jrpg/bar-ink.webp" as="image">
     <link rel="stylesheet" href="/public/mjj-composer.css">
+    <link rel="stylesheet" href="/public/mjj-modal.css">
     <link rel="stylesheet" href="/jrpg/jrpg.css">
     <script type="module" src="/public/mjj-composer.js"></script>
+    <script type="module" src="/public/mjj-modal.js"></script>
     <script type="module" src="/jrpg/jrpg.js"></script>
   </head>
   <body class="jrpg-page">
     <main class="jrpg-main">
-      <mjj-jrpg-shell class="jrpg-shell">
+      <mjj-jrpg-shell
+        class="jrpg-shell"
+        data-initial-panel="__MJJ_PANEL__"
+      >
         <div class="jrpg-workspace">
           <div class="jrpg-brand">
             <header>
@@ -49,61 +54,67 @@
               </a>
             </zen-button>
           </nav>
-          <zen-dialog
+          <mjj-window-modal
             class="jrpg-mobile-destination-owner"
             data-mobile-destination-owner
           >
-            <zen-button class="jrpg-mobile-menu-btn" appearance="plain" size="xs">
-              <button
-                type="button"
-                data-zen-trigger
-                data-mobile-menu-toggle
-                aria-label="Open destination menu"
-              >
-                <zen-icon name="menu"></zen-icon>
-              </button>
-            </zen-button>
-            <dialog
-              class="jrpg-mobile-destination-dialog"
-              data-mobile-destination-dialog
-              aria-labelledby="jrpg-mobile-destination-title"
-            >
-              <header class="jrpg-mobile-destination-header">
-                <zen-button
-                  appearance="plain"
-                  size="sm"
-                  data-mobile-destination-back-owner
-                  hidden
+            <zen-dialog>
+              <zen-button class="jrpg-mobile-menu-btn" appearance="plain" size="xs">
+                <button
+                  type="button"
+                  data-zen-trigger
+                  data-mobile-menu-toggle
+                  aria-label="Open destination menu"
                 >
-                  <button
-                    type="button"
-                    data-mobile-destination-back
-                    aria-label="Back to destination menu"
+                  <zen-icon name="menu"></zen-icon>
+                </button>
+              </zen-button>
+              <dialog
+                class="jrpg-mobile-destination-dialog"
+                data-mobile-destination-dialog
+              >
+                <header class="jrpg-mobile-destination-header" data-modal-header>
+                  <zen-button
+                    appearance="plain"
+                    size="sm"
+                    data-mobile-destination-back-owner
+                    hidden
                   >
-                    <zen-icon name="chevron-left"></zen-icon>
-                    Menu
-                  </button>
-                </zen-button>
-                <h2
-                  id="jrpg-mobile-destination-title"
-                  data-mobile-destination-title
-                >Menu</h2>
-                <zen-button appearance="plain" size="sm">
-                  <button
-                    type="button"
-                    data-zen-close
-                    aria-label="Close destination menu"
-                  >
-                    <zen-icon name="close"></zen-icon>
-                  </button>
-                </zen-button>
-              </header>
-              <div
-                class="jrpg-mobile-destination-content"
-                data-mobile-destination-content
-              ></div>
-            </dialog>
-          </zen-dialog>
+                    <button
+                      type="button"
+                      data-mobile-destination-back
+                      aria-label="Back to destination menu"
+                    >
+                      <zen-icon name="chevron-left"></zen-icon>
+                      Menu
+                    </button>
+                  </zen-button>
+                  <h2
+                    id="jrpg-mobile-destination-title"
+                    data-mobile-destination-title
+                    data-modal-title
+                  >Menu</h2>
+                  <zen-button appearance="plain" size="sm">
+                    <button
+                      type="button"
+                      data-zen-close
+                      aria-label="Close destination menu"
+                    >
+                      <zen-icon name="close"></zen-icon>
+                    </button>
+                  </zen-button>
+                </header>
+                <zen-scroll-area
+                  class="jrpg-mobile-destination-content"
+                  data-mobile-destination-content
+                  data-modal-body
+                  data-cyber-scroll
+                  tabindex="0"
+                  aria-label="Destination content"
+                ></zen-scroll-area>
+              </dialog>
+            </zen-dialog>
+          </mjj-window-modal>
           <footer class="jrpg-frame-telemetry" aria-label="System telemetry">
             <span aria-label="Temperature unavailable">N/A</span>
             <span data-frame-account aria-live="polite"><a href="/login?next=/jrpg">LOGIN</a></span>
@@ -147,52 +158,62 @@
           </section>
 
           <section class="jrpg-utility" aria-label="Selected destination">
-            <mjj-jrpg-preview data-selection="resume">
+            <mjj-jrpg-preview data-selection="__MJJ_PREVIEW_SELECTION__">
               <article class="jrpg-preview-panel" aria-live="polite">
-                <div class="jrpg-preview-content" data-preview-content>
+                <div
+                  class="jrpg-preview-content"
+                  data-preview-content
+                  __MJJ_PREVIEW_HIDDEN__
+                >
                   <zen-heading size="xl">
-                    <h2 data-preview-title>Resume</h2>
+                    <h2 data-preview-title>__MJJ_PREVIEW_TITLE__</h2>
                   </zen-heading>
                   <p data-preview-copy>
-                    Member of Technical Staff and engineering leader with 10+ years building AI agent platforms and production systems.
+                    __MJJ_PREVIEW_COPY__
                   </p>
-                  <ul class="jrpg-work-showcase" data-work-showcase>
-                    <li>
-                      <a href="https://www.microsoft.com/en-us/microsoft-copilot/blog/2026/02/26/copilot-tasks-from-answers-to-actions/">
-                        <span>Copilot Tasks</span>
-                        <small>Agentic execution</small>
-                      </a>
-                    </li>
-                    <li>
-                      <a href="https://www.cio.com/article/3977098/microsoft-doubles-down-on-multi-model-ai-as-it-builds-a-copilot-super-app.html">
-                        <span>Copilot SuperApp</span>
-                        <small>AI platform</small>
-                      </a>
-                    </li>
-                  </ul>
-                  <zen-dialog class="jrpg-preview-dialog">
-                    <zen-button appearance="plain" size="xs" data-dialog-trigger-owner hidden>
-                      <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
-                    </zen-button>
-                    <dialog class="jrpg-detail-dialog" aria-labelledby="jrpg-dialog-title">
-                      <div class="jrpg-dialog-heading">
-                        <p>DESTINATION DATA</p>
+                  <ul class="jrpg-work-showcase" data-work-showcase>__MJJ_PREVIEW_WORKS__</ul>
+                  <mjj-content-modal data-detail-modal-owner>
+                    <zen-dialog class="jrpg-preview-dialog">
+                      <zen-button appearance="plain" size="xs" data-dialog-trigger-owner hidden>
+                        <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
+                      </zen-button>
+                      <dialog class="jrpg-detail-dialog">
+                        <header class="jrpg-dialog-heading" data-modal-header>
+                          <div data-modal-heading>
+                            <p data-modal-kicker>DESTINATION DATA</p>
+                            <h2
+                              id="jrpg-dialog-title"
+                              data-dialog-title
+                              data-modal-title
+                            >Resume</h2>
+                          </div>
                         <div class="jrpg-dialog-heading-actions">
-                          <zen-button appearance="plain" size="md">
-                            <a href="/resume" data-preview-link>
-                              Open page
-                              <zen-icon name="external"></zen-icon>
+                          <zen-button
+                            appearance="plain"
+                            size="sm"
+                            data-resume-download
+                          >
+                            <a href="/public/resume.pdf">
+                              Download PDF
+                              <zen-icon name="download"></zen-icon>
                             </a>
                           </zen-button>
-                          <zen-button appearance="plain" size="md">
+                          <zen-button appearance="plain" size="sm">
                             <button type="button" data-zen-close aria-label="Close destination details">
                               <zen-icon name="close"></zen-icon>
                             </button>
                           </zen-button>
-                        </div>
-                      </div>
-                      <h2 id="jrpg-dialog-title" data-dialog-title>Resume</h2>
-                      <p data-dialog-copy>Experience, projects, and the systems I have helped build.</p>
+                          </div>
+                        </header>
+                        <zen-scroll-area
+                          class="jrpg-detail-body"
+                          data-modal-body
+                          data-cyber-scroll
+                          tabindex="0"
+                          aria-label="Destination details"
+                        >
+                          <div class="jrpg-detail-content" data-modal-article>
+                            <p data-dialog-copy>Experience, projects, and the systems I have helped build.</p>
                       <section
                         class="jrpg-resume-dossier"
                         data-resume-dossier
@@ -229,18 +250,16 @@
                           <div data-tool-content></div>
                         </article>
                       </section>
-                      <div class="jrpg-dialog-actions" data-dialog-actions>
-                        <zen-button appearance="plain" size="lg" data-resume-download>
-                          <a href="/public/resume.pdf">
-                            Download PDF
-                            <zen-icon name="download"></zen-icon>
-                          </a>
-                        </zen-button>
-                      </div>
-                    </dialog>
-                  </zen-dialog>
+                          </div>
+                        </zen-scroll-area>
+                      </dialog>
+                    </zen-dialog>
+                  </mjj-content-modal>
                 </div>
-                <mjj-conversation-archive hidden aria-label="Conversations">
+                <mjj-conversation-archive
+                  __MJJ_ARCHIVE_HIDDEN__
+                  aria-label="Conversations"
+                >
               <div class="jrpg-archive-heading">
                 <h2>Archive</h2>
                 <zen-button appearance="plain" size="xs">
@@ -263,52 +282,83 @@
                 <button type="button" data-archive-load-more>Load more</button>
               </zen-button>
               <p class="jrpg-archive-status" data-archive-status aria-live="polite">Loading conversations...</p>
-              <dialog
-                class="jrpg-archive-dialog"
-                data-archive-delete-dialog
-                aria-labelledby="archive-delete-title"
-                aria-modal="true"
-              >
-                <p id="archive-delete-title" class="jrpg-archive-dialog-title">Delete this conversation?</p>
-                <p data-archive-delete-name class="jrpg-archive-dialog-subtitle"></p>
-                <div class="jrpg-archive-dialog-actions">
-                  <zen-button appearance="plain" size="md">
-                    <button type="button" data-archive-delete-cancel>Cancel</button>
-                  </zen-button>
-                  <zen-button appearance="plain" size="md">
-                    <button type="button" data-archive-delete-confirm>Delete</button>
+              <mjj-content-modal data-archive-delete-owner>
+                <zen-dialog>
+                  <zen-button appearance="plain" size="xs" hidden>
+                    <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
                   </zen-button>
-                </div>
-              </dialog>
-              <dialog
-                class="jrpg-archive-dialog"
-                data-archive-rename-dialog
-                aria-labelledby="archive-rename-title"
-                aria-modal="true"
-              >
-                <p id="archive-rename-title" class="jrpg-archive-dialog-title">Rename conversation</p>
-                <form data-archive-rename-form novalidate>
-                  <zen-field appearance="plain" size="md">
-                    <label for="archive-rename-input">New title</label>
-                    <input
-                      id="archive-rename-input"
-                      data-archive-rename-input
-                      type="text"
-                      maxlength="200"
-                      required
-                      autocomplete="off"
+                  <dialog
+                    class="jrpg-archive-dialog"
+                    data-archive-delete-dialog
+                  >
+                    <header data-modal-header>
+                    <h2
+                      id="archive-delete-title"
+                      class="jrpg-archive-dialog-title"
+                      data-modal-title
+                    >Delete this conversation?</h2>
+                    </header>
+                    <div data-modal-body>
+                    <div data-modal-article>
+                      <p data-archive-delete-name class="jrpg-archive-dialog-subtitle"></p>
+                    </div>
+                    </div>
+                    <footer class="jrpg-archive-dialog-actions" data-modal-footer>
+                    <zen-button appearance="plain" size="md">
+                      <button type="button" data-archive-delete-cancel>Cancel</button>
+                    </zen-button>
+                    <zen-button appearance="plain" size="md">
+                      <button type="button" data-archive-delete-confirm>Delete</button>
+                    </zen-button>
+                    </footer>
+                  </dialog>
+                </zen-dialog>
+              </mjj-content-modal>
+              <mjj-window-modal data-archive-rename-owner>
+                <zen-dialog>
+                  <zen-button appearance="plain" size="xs" hidden>
+                    <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
+                  </zen-button>
+                  <dialog
+                    class="jrpg-archive-dialog"
+                    data-archive-rename-dialog
+                  >
+                    <header data-modal-header>
+                    <h2
+                      id="archive-rename-title"
+                      class="jrpg-archive-dialog-title"
+                      data-modal-title
+                    >Rename conversation</h2>
+                    </header>
+                    <form
+                    id="archive-rename-form"
+                    data-modal-body
+                    data-archive-rename-form
+                    novalidate
                     >
-                  </zen-field>
-                  <div class="jrpg-archive-dialog-actions">
+                    <zen-field appearance="plain" size="md">
+                      <label for="archive-rename-input">New title</label>
+                      <input
+                        id="archive-rename-input"
+                        data-archive-rename-input
+                        type="text"
+                        maxlength="200"
+                        required
+                        autocomplete="off"
+                      >
+                    </zen-field>
+                    </form>
+                    <footer class="jrpg-archive-dialog-actions" data-modal-footer>
                     <zen-button appearance="plain" size="md">
                       <button type="button" data-archive-rename-cancel>Cancel</button>
                     </zen-button>
                     <zen-button appearance="plain" size="md">
-                      <button type="submit">Save</button>
+                      <button type="submit" form="archive-rename-form">Save</button>
                     </zen-button>
-                  </div>
-                </form>
-              </dialog>
+                    </footer>
+                  </dialog>
+                </zen-dialog>
+              </mjj-window-modal>
                 </mjj-conversation-archive>
               </article>
             </mjj-jrpg-preview>
@@ -372,7 +422,7 @@
               <ul>
                 <li>
                   <zen-button appearance="plain" size="sm">
-                    <button type="button" data-preview="resume" aria-pressed="true">
+                    <button type="button" data-preview="resume" aria-pressed="__MJJ_RESUME_PRESSED__">
                       <zen-icon name="chevron-right"></zen-icon>
                       Resume
                     </button>
@@ -380,7 +430,7 @@
                 </li>
                 <li>
                   <zen-button appearance="plain" size="sm">
-                    <button type="button" data-preview="tools" aria-pressed="false">
+                    <button type="button" data-preview="tools" aria-pressed="__MJJ_TOOLS_PRESSED__">
                       <zen-icon name="chevron-right"></zen-icon>
                       Tools
                     </button>
@@ -388,7 +438,7 @@
                 </li>
                 <li>
                   <zen-button appearance="plain" size="sm">
-                    <button type="button" data-preview="blog" aria-pressed="false">
+                    <button type="button" data-preview="blog" aria-pressed="__MJJ_BLOG_PRESSED__">
                       <zen-icon name="chevron-right"></zen-icon>
                       Blogs
                     </button>
@@ -396,7 +446,7 @@
                 </li>
                 <li>
                   <zen-button appearance="plain" size="sm">
-                    <button type="button" data-preview="conversations" aria-pressed="false">
+                    <button type="button" data-preview="conversations" aria-pressed="__MJJ_CONVERSATIONS_PRESSED__">
                       <zen-icon name="chevron-right"></zen-icon>
                       Conversations
                     </button>
@@ -406,26 +456,30 @@
             </nav>
           </mjj-jrpg-menu>
 
-          <zen-dialog class="jrpg-login-dialog-owner" data-login-dialog-owner>
-            <zen-button appearance="plain" size="md" hidden>
-              <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
-            </zen-button>
-            <dialog
-              class="jrpg-login-dialog"
-              data-login-dialog
-              aria-labelledby="jrpg-login-title"
-              aria-modal="true"
-            >
-              <div class="jrpg-login-dialog-header">
-                <h2 id="jrpg-login-title">SIGN IN</h2>
-                <zen-button appearance="plain" size="md">
-                  <button type="button" data-zen-close aria-label="Close sign-in dialog">
-                    <zen-icon name="close"></zen-icon>
-                  </button>
-                </zen-button>
-              </div>
-              <div role="alert" data-login-error aria-live="assertive" hidden></div>
-              <form data-login-form novalidate>
+          <mjj-window-modal class="jrpg-login-dialog-owner" data-login-dialog-owner>
+            <zen-dialog>
+              <zen-button appearance="plain" size="md" hidden>
+                <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
+              </zen-button>
+              <dialog
+                class="jrpg-login-dialog"
+                data-login-dialog
+              >
+                <header class="jrpg-login-dialog-header" data-modal-header>
+                  <h2 id="jrpg-login-title" data-modal-title>SIGN IN</h2>
+                  <zen-button appearance="plain" size="md">
+                    <button type="button" data-zen-close aria-label="Close sign-in dialog">
+                      <zen-icon name="close"></zen-icon>
+                    </button>
+                  </zen-button>
+                </header>
+                <form
+                  id="jrpg-login-form"
+                  data-login-form
+                  data-modal-body
+                  novalidate
+                >
+                  <div role="alert" data-login-error aria-live="assertive" hidden></div>
                 <zen-field appearance="plain" size="md">
                   <label for="jrpg-login-username">Username</label>
                   <input
@@ -451,17 +505,22 @@
                     maxlength="1024"
                   >
                 </zen-field>
-                <div class="jrpg-login-dialog-actions">
+                </form>
+                <footer class="jrpg-login-dialog-actions" data-modal-footer>
                   <zen-button appearance="plain" size="md">
                     <button type="button" data-login-cancel data-zen-close>Cancel</button>
                   </zen-button>
                   <zen-button appearance="plain" size="md">
-                    <button type="submit" data-login-submit>Sign in</button>
+                    <button
+                      type="submit"
+                      form="jrpg-login-form"
+                      data-login-submit
+                    >Sign in</button>
                   </zen-button>
-                </div>
-              </form>
-            </dialog>
-          </zen-dialog>
+                </footer>
+              </dialog>
+            </zen-dialog>
+          </mjj-window-modal>
         </div>
       </mjj-jrpg-shell>
     </main>
--- a/mrjunejune/src/jrpg/jrpg.css	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/jrpg/jrpg.css	Sat Aug 08 02:08:08 2026 -0700
@@ -41,7 +41,15 @@
   --zenbu-sys-font-family-ui: "Pixel Mplus";
   --zenbu-sys-font-family-content: "Pixel Mplus";
   --zenbu-sys-font-family-code: "Pixel Mplus";
+  --zenbu-sys-type-caption-family: "Pixel Mplus";
   --zenbu-sys-type-label-family: "Pixel Mplus";
+  --zenbu-sys-type-body-family: "Pixel Mplus";
+  --zenbu-sys-type-body-large-family: "Pixel Mplus";
+  --zenbu-sys-type-subtitle-family: "Pixel Mplus";
+  --zenbu-sys-type-title-family: "Pixel Mplus";
+  --zenbu-sys-type-page-title-family: "Pixel Mplus";
+  --zenbu-sys-type-reading-family: "Pixel Mplus";
+  --zenbu-sys-type-code-family: "Pixel Mplus";
   min-width: 20rem;
   min-height: 100dvh;
   overflow: hidden;
@@ -741,38 +749,6 @@
   box-sizing: border-box;
   width: min(82vw, 46rem);
   max-width: calc(100% - var(--zenbu-sys-space-content));
-  padding: var(--zenbu-sys-padding-xl);
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
-  border-radius: 0;
-  background: var(--mjj-jrpg-surface-raised);
-  color: var(--mjj-jrpg-text);
-  box-shadow:
-    inset 0 0 0 var(--zenbu-sys-stroke-width)
-    var(--mjj-jrpg-accent),
-    var(--zenbu-sys-space-control)
-    var(--zenbu-sys-space-control)
-    0
-    var(--mjj-jrpg-accent);
-}
-
-.jrpg-preview-dialog dialog::backdrop {
-  background: color-mix(in srgb, var(--mjj-jrpg-canvas) 84%, transparent);
-}
-
-.jrpg-preview-dialog dialog[open] {
-  animation: mjj-jrpg-dialog-open var(--zenbu-sys-motion-duration-layout)
-    var(--zenbu-sys-motion-ease-enter);
-}
-
-@keyframes mjj-jrpg-dialog-open {
-  from {
-    clip-path: inset(48% 25%);
-    transform: scaleX(0.35);
-  }
-  to {
-    clip-path: inset(0);
-    transform: scaleX(1);
-  }
 }
 
 .jrpg-dialog-heading {
@@ -780,8 +756,6 @@
   align-items: center;
   justify-content: space-between;
   gap: var(--zenbu-sys-space-group);
-  padding-bottom: var(--zenbu-sys-padding-md);
-  border-bottom: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
 }
 
 .jrpg-dialog-heading p,
@@ -800,8 +774,13 @@
 }
 
 .jrpg-dialog-heading zen-button > :where(button, a) {
+  box-sizing: border-box;
   width: auto;
   min-width: var(--zenbu-control-height);
+  height: var(--zenbu-control-height);
+  padding:
+    var(--zenbu-control-padding-block)
+    var(--zenbu-control-padding-inline);
   border-color: var(--mjj-jrpg-accent);
   background: var(--mjj-jrpg-surface);
   color: var(--mjj-jrpg-text);
@@ -812,40 +791,32 @@
   padding: 0;
 }
 
-.jrpg-preview-dialog dialog h2 {
-  margin-top: var(--zenbu-sys-space-content);
-}
-
-.jrpg-preview-dialog dialog > p {
-  margin-bottom: var(--zenbu-sys-space-content);
-}
-
 .jrpg-preview-dialog dialog.jrpg-detail-dialog {
-  width: min(94vw, 72rem);
+  width: min(92vw, 60rem);
   max-height: calc(100dvh - var(--zenbu-sys-space-container));
   overflow: hidden;
 }
 
 .jrpg-preview-dialog dialog.jrpg-detail-dialog[open] {
   display: grid;
-  grid-template-rows: auto auto minmax(0, 1fr) auto;
+  grid-template-rows: auto minmax(0, 1fr) auto;
   gap: var(--zenbu-sys-space-group);
 }
 
+mjj-window-modal .jrpg-detail-content {
+  box-sizing: border-box;
+  height: 100%;
+  min-height: 0;
+  padding: var(--zenbu-sys-padding-lg);
+}
+
+mjj-content-modal .jrpg-detail-content {
+  padding: var(--zenbu-sys-padding-lg);
+}
+
 .jrpg-preview-dialog dialog.jrpg-detail-dialog[data-detail-mode="tools"] {
-  width: calc(100vw - var(--zenbu-sys-space-content));
-  max-width: none;
-  height: calc(100dvh - var(--zenbu-sys-space-content));
-  max-height: none;
-  padding: 0;
-  border: 0;
-  background: transparent;
-  box-shadow: none;
-}
-
-.jrpg-preview-dialog dialog.jrpg-detail-dialog[data-detail-mode="tools"][open] {
-  grid-template-rows: auto minmax(0, 1fr);
-  gap: var(--zenbu-sys-space-control);
+  width: min(90vw, 56rem);
+  height: min(78dvh, 44rem);
 }
 
 .jrpg-detail-dialog[data-detail-mode="tools"] > h2 {
@@ -854,19 +825,14 @@
 
 .jrpg-detail-dialog[data-detail-mode="tools"] .jrpg-dialog-heading {
   padding: var(--zenbu-sys-padding-sm);
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
-  background: var(--mjj-jrpg-surface-raised);
-  box-shadow:
-    inset 0 0 0 var(--zenbu-sys-stroke-width)
-    var(--mjj-jrpg-accent);
 }
 
 .jrpg-resume-dossier {
   min-height: 0;
-  padding: var(--zenbu-sys-padding-lg);
+  padding: 0;
   overflow: auto;
-  border: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
-  background: var(--mjj-jrpg-surface-sunken);
+  border: 0;
+  background: transparent;
   scrollbar-color: var(--mjj-jrpg-frame) transparent;
 }
 
@@ -901,9 +867,10 @@
 .jrpg-resume-dossier .info {
   display: grid;
   gap: var(--zenbu-sys-space-control);
-  padding: var(--zenbu-sys-padding-lg);
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-primary);
-  background: var(--mjj-jrpg-panel);
+  padding: 0 0 var(--zenbu-sys-padding-lg);
+  border: 0;
+  border-bottom: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
+  background: transparent;
 }
 
 .jrpg-resume-dossier .info > p {
@@ -972,6 +939,10 @@
   text-underline-offset: var(--zenbu-sys-space-icon-label);
 }
 
+.jrpg-resume-dossier a[href="/public/resume.pdf"] {
+  display: none;
+}
+
 .jrpg-dialog-actions {
   display: grid;
   grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1127,56 +1098,18 @@
 }
 
 .jrpg-tool-panes {
-  position: relative;
   box-sizing: border-box;
   display: grid;
-  grid-template-columns:
-    minmax(0, 1fr)
-    calc(var(--zenbu-sys-space-container) * 2)
-    minmax(0, 1fr);
+  grid-template-columns: minmax(0, 1fr);
+  grid-template-rows: repeat(2, minmax(18rem, auto));
   gap: var(--zenbu-sys-space-control);
-  height: 100%;
+  height: auto;
   min-height: 0;
   padding: var(--zenbu-sys-padding-sm);
 }
 
 .jrpg-tool-panes::before {
-  grid-column: 2;
-  grid-row: 1;
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
-  background-image:
-    radial-gradient(
-      circle at 50% var(--zenbu-sys-space-content),
-      var(--mjj-jrpg-highlight) 0 var(--zenbu-sys-stroke-width-emphasis),
-      var(--mjj-jrpg-frame) var(--zenbu-sys-stroke-width-emphasis)
-        calc(var(--zenbu-sys-stroke-width-emphasis) * 2),
-      transparent calc(var(--zenbu-sys-stroke-width-emphasis) * 2)
-    ),
-    radial-gradient(
-      circle at 50% calc(100% - var(--zenbu-sys-space-content)),
-      var(--mjj-jrpg-highlight) 0 var(--zenbu-sys-stroke-width-emphasis),
-      var(--mjj-jrpg-frame) var(--zenbu-sys-stroke-width-emphasis)
-        calc(var(--zenbu-sys-stroke-width-emphasis) * 2),
-      transparent calc(var(--zenbu-sys-stroke-width-emphasis) * 2)
-    ),
-    linear-gradient(
-      90deg,
-      var(--mjj-jrpg-surface-sunken),
-      var(--mjj-jrpg-surface-raised) 45%,
-      var(--mjj-jrpg-frame) 50%,
-      var(--mjj-jrpg-surface-raised) 55%,
-      var(--mjj-jrpg-surface-sunken)
-    ),
-    var(--mjj-jrpg-art);
-  background-position: center;
-  background-size: auto, auto, auto, cover;
-  box-shadow:
-    inset 0 0 0 var(--zenbu-sys-stroke-width)
-    var(--mjj-jrpg-info),
-    0 0 var(--zenbu-sys-space-related)
-    color-mix(in srgb, var(--mjj-jrpg-primary) 65%, transparent);
-  content: "";
-  image-rendering: pixelated;
+  display: none;
 }
 
 .jrpg-tool-window:first-of-type {
@@ -1185,8 +1118,8 @@
 }
 
 .jrpg-tool-window:last-of-type {
-  grid-column: 3;
-  grid-row: 1;
+  grid-column: 1;
+  grid-row: 2;
 }
 
 .jrpg-tool-pane {
@@ -1981,26 +1914,10 @@
   box-sizing: border-box;
   min-width: min(22rem, calc(100vw - var(--zenbu-sys-space-content)));
   max-width: min(26rem, calc(100vw - var(--zenbu-sys-space-content)));
-  padding: var(--zenbu-sys-padding-lg);
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
-  border-radius: 0;
-  background: var(--mjj-jrpg-surface-raised);
-  color: var(--mjj-jrpg-text);
-  box-shadow:
-    inset 0 0 0 var(--zenbu-sys-stroke-width) var(--mjj-jrpg-accent),
-    var(--zenbu-sys-space-control) var(--zenbu-sys-space-control) 0 var(--mjj-jrpg-accent);
 }
 
-.jrpg-archive-dialog::backdrop {
-  background: color-mix(in srgb, var(--mjj-jrpg-canvas) 84%, transparent);
-}
-
-.jrpg-archive-dialog[open] {
-  display: grid;
-  grid-template-rows: auto auto auto;
-  gap: var(--zenbu-sys-space-group);
-  animation: mjj-jrpg-dialog-open var(--zenbu-sys-motion-duration-layout)
-    var(--zenbu-sys-motion-ease-enter);
+mjj-window-modal [data-archive-rename-dialog] {
+  height: auto;
 }
 
 .jrpg-archive-dialog-title {
@@ -2276,7 +2193,7 @@
   }
 
   /* Hamburger button: transparent overlay over baked art */
-  .jrpg-mobile-destination-owner > .jrpg-mobile-menu-btn {
+  .jrpg-mobile-destination-owner > zen-dialog > .jrpg-mobile-menu-btn {
     display: grid;
     place-items: center;
     position: absolute;
@@ -2381,10 +2298,13 @@
     cursor: pointer;
   }
 
-  .jrpg-mobile-destination-content {
+  mjj-window-modal .jrpg-mobile-destination-content {
     min-width: 0;
     min-height: 0;
-    overflow: hidden;
+    padding: 0;
+    overflow-x: auto;
+    overflow-y: scroll;
+    scrollbar-gutter: stable;
   }
 
   .jrpg-mobile-destination-content > :is(mjj-jrpg-menu, .jrpg-utility) {
@@ -2538,27 +2458,7 @@
 .jrpg-login-dialog {
   box-sizing: border-box;
   width: min(24rem, calc(100vw - var(--zenbu-sys-space-content)));
-  padding: var(--zenbu-sys-padding-xl);
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
-  border-radius: 0;
-  background: var(--mjj-jrpg-surface-raised);
-  color: var(--mjj-jrpg-text);
-  box-shadow:
-    inset 0 0 0 var(--zenbu-sys-stroke-width) var(--mjj-jrpg-accent),
-    var(--zenbu-sys-space-control) var(--zenbu-sys-space-control) 0
-      var(--mjj-jrpg-accent);
-}
-
-.jrpg-login-dialog::backdrop {
-  background: color-mix(in srgb, var(--mjj-jrpg-canvas) 84%, transparent);
-}
-
-.jrpg-login-dialog[open] {
-  display: grid;
-  grid-template-rows: auto auto minmax(0, 1fr);
-  gap: var(--zenbu-sys-space-group);
-  animation: mjj-jrpg-dialog-open var(--zenbu-sys-motion-duration-layout)
-    var(--zenbu-sys-motion-ease-enter);
+  height: auto;
 }
 
 .jrpg-login-dialog-header {
@@ -2566,8 +2466,6 @@
   align-items: center;
   justify-content: space-between;
   gap: var(--zenbu-sys-space-group);
-  padding-bottom: var(--zenbu-sys-padding-md);
-  border-bottom: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
 }
 
 .jrpg-login-dialog-header h2 {
--- a/mrjunejune/src/jrpg/jrpg.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/jrpg/jrpg.js	Sat Aug 08 02:08:08 2026 -0700
@@ -543,6 +543,7 @@
 
 class MjjJrpgPreview extends HTMLElement {
   connectedCallback() {
+    this._modalOwner = this.querySelector("[data-detail-modal-owner]");
     this._dialog = this.querySelector("dialog");
     this._onDialogClose = () => {
       this._toolRequest = (this._toolRequest || 0) + 1;
@@ -570,30 +571,32 @@
       const latestBlog = event.target.closest("[data-latest-blog]");
       if (latestBlog && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
+        this.show("blog");
+        this._modalOwner?.open();
         void this.loadBlogDetail(latestBlog.dataset.blogUrl);
         return;
       }
       const blogArchive = event.target.closest("[data-blog-archive]");
       if (blogArchive) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
         this.show("blog");
+        this._modalOwner?.open();
         void this.loadBlogs();
         return;
       }
       const resumeModal = event.target.closest("[data-resume-modal]");
       if (resumeModal) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
         this.show("resume");
+        this._modalOwner?.open();
         void this.loadResume();
         return;
       }
       const latestTool = event.target.closest("[data-latest-tool]");
       if (latestTool && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
+        this.show("tools");
+        this._modalOwner?.open();
         void this.loadToolDetail(latestTool.dataset.toolUrl);
       }
     };
@@ -608,6 +611,18 @@
     this.cleanupTool();
   }
 
+  _setDetailModalType(useWindow) {
+    const targetName = useWindow ? "mjj-window-modal" : "mjj-content-modal";
+    if (!this._modalOwner || this._modalOwner.localName === targetName) return;
+    const replacement = document.createElement(targetName);
+    for (const attribute of [...this._modalOwner.attributes]) {
+      replacement.setAttribute(attribute.name, attribute.value);
+    }
+    replacement.append(...this._modalOwner.childNodes);
+    this._modalOwner.replaceWith(replacement);
+    this._modalOwner = replacement;
+  }
+
   show(selection) {
     this.cleanupTool();
     const preview = PREVIEWS[selection] || PREVIEWS.resume;
@@ -617,9 +632,6 @@
     this.querySelector("[data-dialog-title]").textContent = preview.title;
     this.querySelector("[data-dialog-copy]").textContent = preview.copy;
     this.renderShowcase(preview.works);
-    const link = this.querySelector("[data-preview-link]");
-    link.href = preview.url;
-    link.setAttribute("aria-label", `Open ${preview.title}`);
     const resumeDossier = this.querySelector("[data-resume-dossier]");
     const resumeDownload = this.querySelector("[data-resume-download]");
     const blogBrowser = this.querySelector("[data-blog-browser]");
@@ -627,10 +639,10 @@
     const isResume = selection === "resume";
     const isBlog = selection === "blog";
     const isTools = selection === "tools";
+    this._setDetailModalType(isTools);
     this._dialog.dataset.detailMode = isTools ? "tools" : "content";
     resumeDossier.hidden = !isResume;
     resumeDownload.hidden = !isResume;
-    this.querySelector("[data-dialog-actions]").hidden = !isResume;
     blogBrowser.hidden = !isBlog;
     toolBrowser.hidden = !isTools;
     this.querySelector("[data-dialog-copy]").hidden =
@@ -711,6 +723,12 @@
       const resume = documentCopy.querySelector("main");
       if (!resume) throw new Error("Resume content is unavailable");
       sanitizeFetchedMain(resume);
+      for (const duplicateDownload of resume.querySelectorAll(
+        'a[href="/public/resume.pdf"]',
+      )) {
+        duplicateDownload.closest("zen-button")?.remove();
+        duplicateDownload.remove();
+      }
       content.replaceChildren(...resume.childNodes);
       status.hidden = true;
       this._resumeLoaded = true;
@@ -823,9 +841,6 @@
     status.textContent = `Loading ${blog.label}...`;
     content.replaceChildren();
     this.querySelector("[data-dialog-title]").textContent = blog.label;
-    const fullPage = this.querySelector("[data-preview-link]");
-    fullPage.href = blog.url;
-    fullPage.setAttribute("aria-label", `Open ${blog.label}`);
     for (const button of this.querySelectorAll("[data-blog-entry]")) {
       button.setAttribute(
         "aria-pressed",
@@ -928,9 +943,6 @@
     this.cleanupTool();
     content.replaceChildren();
     this.querySelector("[data-dialog-title]").textContent = tool.label;
-    const fullPage = this.querySelector("[data-preview-link]");
-    fullPage.href = tool.url;
-    fullPage.setAttribute("aria-label", `Open ${tool.label}`);
     if (tool.url === "/tools/markdown_to_html") {
       await this.renderMarkdownTool(content, status, request);
       return;
@@ -1426,10 +1438,12 @@
     this._loadMoreButton = this.querySelector("[data-archive-load-more]");
     this._newButton = this.querySelector("[data-archive-new]");
     this._closeButton = this.querySelector("[data-archive-close]");
+    this._deleteModal = this.querySelector("[data-archive-delete-owner]");
     this._deleteDialog = this.querySelector("[data-archive-delete-dialog]");
     this._deleteNameEl = this.querySelector("[data-archive-delete-name]");
     this._deleteCancelButton = this.querySelector("[data-archive-delete-cancel]");
     this._deleteConfirmButton = this.querySelector("[data-archive-delete-confirm]");
+    this._renameModal = this.querySelector("[data-archive-rename-owner]");
     this._renameDialog = this.querySelector("[data-archive-rename-dialog]");
     this._renameInput = this.querySelector("[data-archive-rename-input]");
     this._renameForm = this.querySelector("[data-archive-rename-form]");
@@ -1452,7 +1466,7 @@
     });
 
     this._deleteCancelButton?.addEventListener("click", () => {
-      this._deleteDialog?.close();
+      this._deleteModal?.close();
       const t = this._pendingDeleteTrigger;
       this._pendingDeleteId = null;
       this._pendingDeleteTrigger = null;
@@ -1462,7 +1476,7 @@
     this._deleteConfirmButton?.addEventListener("click", () => {
       const id = this._pendingDeleteId;
       const trigger = this._pendingDeleteTrigger;
-      this._deleteDialog?.close();
+      this._deleteModal?.close();
       this._pendingDeleteId = null;
       this._pendingDeleteTrigger = null;
       if (id) {
@@ -1481,7 +1495,7 @@
     });
 
     this._renameCancelButton?.addEventListener("click", () => {
-      this._renameDialog?.close();
+      this._renameModal?.close();
       const t = this._pendingRenameTrigger;
       this._pendingRenameId = null;
       this._pendingRenameTrigger = null;
@@ -1494,7 +1508,7 @@
       const trigger = this._pendingRenameTrigger;
       const newTitle = this._renameInput?.value.trim();
       if (!id || !newTitle) return;
-      this._renameDialog?.close();
+      this._renameModal?.close();
       this._pendingRenameId = null;
       this._pendingRenameTrigger = null;
       this.dispatchEvent(new CustomEvent("mjj-archive-rename", {
@@ -1529,7 +1543,7 @@
           this._pendingRenameId = item._convId;
           this._pendingRenameTrigger = renameButton;
           if (this._renameInput) this._renameInput.value = item._convTitle || "";
-          this._renameDialog?.showModal();
+          this._renameModal?.open();
           requestAnimationFrame(() => { this._renameInput?.select(); });
         }
         return;
@@ -1541,7 +1555,7 @@
           this._pendingDeleteId = item._convId;
           this._pendingDeleteTrigger = deleteButton;
           if (this._deleteNameEl) this._deleteNameEl.textContent = item._convTitle || "";
-          this._deleteDialog?.showModal();
+          this._deleteModal?.open();
           requestAnimationFrame(() => { this._deleteConfirmButton?.focus(); });
         }
         return;
@@ -2186,7 +2200,10 @@
 
   let characterTimer = 0;
   let currentConversationId = null;
-  let currentPanel = "resume";
+  const serverInitialPanel = VALID_PANELS.includes(shell?.dataset.initialPanel)
+    ? shell.dataset.initialPanel
+    : "resume";
+  let currentPanel = serverInitialPanel;
   let archiveCursor = null;
   let activeController = null;
   let archiveLoadInFlight = false;  /* serialize pagination requests */
@@ -2799,7 +2816,7 @@
         normalUrl,
       );
     }
-    currentPanel = validPanel || "resume";
+    currentPanel = validPanel || serverInitialPanel;
   }
 
   /* ---- Load archive; only an explicit URL restores a conversation ---- */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/component-sandbox.css	Sat Aug 08 02:08:08 2026 -0700
@@ -0,0 +1,203 @@
+@font-face {
+  font-family: "Pixel Mplus";
+  src: url("/public/fonts/pixel-mplus-12-regular.ttf") format("truetype");
+  font-display: block;
+}
+
+:root {
+  --zenbu-sys-font-family-ui: "Pixel Mplus";
+  --zenbu-sys-font-family-content: "Pixel Mplus";
+  --zenbu-sys-font-family-code: "Pixel Mplus";
+  --zenbu-sys-type-caption-family: "Pixel Mplus";
+  --zenbu-sys-type-label-family: "Pixel Mplus";
+  --zenbu-sys-type-body-family: "Pixel Mplus";
+  --zenbu-sys-type-body-large-family: "Pixel Mplus";
+  --zenbu-sys-type-subtitle-family: "Pixel Mplus";
+  --zenbu-sys-type-title-family: "Pixel Mplus";
+  --zenbu-sys-type-page-title-family: "Pixel Mplus";
+  --zenbu-sys-type-reading-family: "Pixel Mplus";
+  --zenbu-sys-type-code-family: "Pixel Mplus";
+}
+
+body {
+  box-sizing: border-box;
+  min-height: 100dvh;
+  margin: 0;
+  padding: var(--zenbu-sys-padding-xl);
+  background: var(--zenbu-sys-color-surface-page);
+  color: var(--zenbu-sys-color-text-primary);
+  font-family: var(--zenbu-sys-font-family-ui);
+}
+
+body * {
+  font-family: inherit;
+}
+
+zen-icon svg {
+  shape-rendering: crispEdges;
+  stroke-linecap: square;
+  stroke-linejoin: miter;
+}
+
+main {
+  display: grid;
+  gap: var(--zenbu-sys-space-container);
+  width: min(100%, 64rem);
+  margin-inline: auto;
+}
+
+.sandbox-heading {
+  display: grid;
+  gap: var(--zenbu-sys-space-control);
+}
+
+.sandbox-heading h1 {
+  color: var(--zenbu-sys-color-action-primary-background);
+  text-shadow:
+    var(--zenbu-sys-space-icon-label)
+    var(--zenbu-sys-space-icon-label)
+    0
+    var(--zenbu-sys-color-danger-foreground);
+}
+
+.sandbox-heading :where(h1, p),
+.sandbox-card :where(h2, p),
+.sandbox-pane :where(h3, p) {
+  margin: 0;
+}
+
+.sandbox-heading p,
+.sandbox-card > p,
+.sandbox-muted {
+  color: var(--zenbu-sys-color-text-secondary);
+}
+
+.sandbox-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: var(--zenbu-sys-space-group);
+}
+
+.sandbox-layers {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: var(--zenbu-sys-space-control);
+  margin: 0;
+}
+
+.sandbox-layers > div {
+  display: grid;
+  gap: var(--zenbu-sys-space-icon-label);
+  padding: var(--zenbu-sys-padding-md);
+  border: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-border-default);
+  border-radius: var(--zenbu-sys-radius-control);
+  background: var(--zenbu-sys-color-surface-sunken);
+}
+
+.sandbox-layers > div:nth-child(1) dt {
+  color: var(--zenbu-sys-color-warning-foreground);
+}
+
+.sandbox-layers > div:nth-child(2) dt {
+  color: var(--zenbu-sys-color-info-foreground);
+}
+
+.sandbox-layers > div:nth-child(3) dt {
+  color: var(--zenbu-sys-color-action-primary-background);
+}
+
+.sandbox-layers dt {
+  color: var(--zenbu-sys-color-text-primary);
+  font-weight: var(--zenbu-sys-font-weight-strong);
+}
+
+.sandbox-layers dd {
+  margin: 0;
+  color: var(--zenbu-sys-color-text-secondary);
+  font-size: var(--zenbu-sys-font-size-sm);
+}
+
+.sandbox-rule {
+  padding: var(--zenbu-sys-padding-md);
+  border-inline-start: var(--zenbu-sys-stroke-width-emphasis) solid
+    var(--zenbu-sys-color-action-primary-border);
+  background: var(--zenbu-sys-color-surface-sunken);
+  color: var(--zenbu-sys-color-text-secondary);
+}
+
+.sandbox-card {
+  display: grid;
+  align-content: start;
+  gap: var(--zenbu-sys-space-group);
+  padding: var(--zenbu-sys-padding-xl);
+  border: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-border-default);
+  border-radius: var(--zenbu-sys-radius-container);
+  background: var(--zenbu-sys-color-surface-raised);
+  box-shadow:
+    var(--zenbu-sys-space-control)
+    var(--zenbu-sys-space-control)
+    0
+    var(--zenbu-sys-color-danger-foreground);
+}
+
+.sandbox-card > h2 {
+  color: var(--zenbu-sys-color-action-primary-background);
+}
+
+.sandbox-list {
+  display: grid;
+  gap: var(--zenbu-sys-space-control);
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.sandbox-list li {
+  padding-bottom: var(--zenbu-sys-padding-sm);
+  border-bottom: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-border-subtle);
+}
+
+.sandbox-audit {
+  display: grid;
+  gap: var(--zenbu-sys-space-control);
+  margin: var(--zenbu-sys-space-group) 0 0;
+  padding: 0;
+  list-style: none;
+}
+
+.sandbox-audit li {
+  display: grid;
+  grid-template-columns: auto minmax(0, 1fr);
+  gap: var(--zenbu-sys-space-control);
+  padding-block: var(--zenbu-sys-padding-sm);
+  border-top: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-border-subtle);
+  color: var(--zenbu-sys-color-text-secondary);
+}
+
+.sandbox-audit strong {
+  color: var(--zenbu-sys-color-warning-foreground);
+}
+
+.sandbox-pane {
+  display: grid;
+  align-content: start;
+  gap: var(--zenbu-sys-space-control);
+}
+
+@media (max-width: 42rem) {
+  body {
+    padding: var(--zenbu-sys-padding-lg);
+  }
+
+  .sandbox-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .sandbox-layers {
+    grid-template-columns: minmax(0, 1fr);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/component-sandbox.html	Sat Aug 08 02:08:08 2026 -0700
@@ -0,0 +1,263 @@
+<!doctype html>
+<html lang="en" data-zen-theme="cyberpunk">
+  <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AI Component Sandbox | MrJuneJune</title>
+    <link rel="stylesheet" href="/public/design-system/styles/tokens.css">
+    <link rel="stylesheet" href="/public/design-system/styles/themes.css">
+    <link rel="stylesheet" href="/public/design-system/styles/components.css">
+    <script type="module" src="/public/design-system/components/index.js"></script>
+    <script type="module">
+      const version = Date.now();
+      const styles = [
+        "/public/mjj-modal.css",
+        "/public/component-sandbox.css",
+      ];
+      await Promise.all(styles.map(href => new Promise((resolve, reject) => {
+        const link = document.createElement("link");
+        link.rel = "stylesheet";
+        link.href = `${href}?sandbox=${version}`;
+        link.addEventListener("load", resolve, { once: true });
+        link.addEventListener("error", reject, { once: true });
+        document.head.append(link);
+      })));
+      await import(`/public/mjj-modal.js?sandbox=${version}`);
+      void navigator.serviceWorker?.getRegistration()
+        .then(registration => registration?.update());
+    </script>
+  </head>
+  <body>
+    <main>
+      <header class="sandbox-heading">
+        <p class="sandbox-muted">Isolated browser fixture</p>
+        <h1>AI Component Sandbox</h1>
+        <p>
+          Edit one component family, refresh this page, and inspect desktop or
+          mobile behavior without application chrome.
+        </p>
+        <dl class="sandbox-layers" aria-label="Component dependency layers">
+          <div>
+            <dt>Layer 0 · Foundation</dt>
+            <dd>API mapping, i18n, and semantic themes.</dd>
+          </div>
+          <div>
+            <dt>Layer 1 · Primitives</dt>
+            <dd>Zenbu icons, fields, buttons, and dialog behavior.</dd>
+          </div>
+          <div>
+            <dt>Layer 2 · Composites</dt>
+            <dd>MrJuneJune composers and modal families.</dd>
+          </div>
+        </dl>
+        <p class="sandbox-rule">
+          Dependency rule: Layer 2 may consume Layers 1 and 0. Application
+          routes, persistence, and business state stay above this sandbox.
+        </p>
+      </header>
+
+      <section class="sandbox-grid" aria-label="Modal component variants">
+        <article class="sandbox-card">
+          <h2>Content modal</h2>
+          <p>
+            Editorial reading surface for blogs, notes, release details, and
+            long-form content.
+          </p>
+          <mjj-content-modal data-component-layer="2">
+            <zen-dialog>
+              <zen-button size="md">
+                <button type="button" data-zen-trigger>
+                  Open article
+                  <zen-icon name="external"></zen-icon>
+                </button>
+              </zen-button>
+              <dialog>
+                <header data-modal-header>
+                  <div data-modal-heading>
+                    <p data-modal-kicker>Engineering note · 8 min read</p>
+                    <h2 data-modal-title>Designing durable agent interfaces</h2>
+                  </div>
+                  <div data-modal-header-actions>
+                    <zen-button variant="quiet" size="sm">
+                      <button type="button" data-zen-close aria-label="Close article">
+                        <zen-icon name="close"></zen-icon>
+                      </button>
+                    </zen-button>
+                  </div>
+                </header>
+                <zen-scroll-area
+                  data-modal-body
+                  data-cyber-scroll
+                  tabindex="0"
+                  aria-label="Article content"
+                >
+                  <article data-modal-article>
+                    <p data-modal-description>
+                      A content modal should feel like a focused reading room:
+                      enough structure to orient the reader, but no window chrome
+                      competing with the article.
+                    </p>
+                    <h2>Start with a strict shell</h2>
+                    <p>
+                      Header, body, and footer are stable regions. The article
+                      owns its semantic headings, paragraphs, lists, and code.
+                      The modal only supplies rhythm, containment, scrolling,
+                      and depth.
+                    </p>
+                    <blockquote>
+                      Content should remain ordinary HTML even when the
+                      presentation becomes cinematic.
+                    </blockquote>
+                    <h3>Keep the body readable</h3>
+                    <p>
+                      The reading column stays bounded while the dialog itself
+                      can grow. This avoids very long lines on desktop and keeps
+                      padding deliberate on small screens.
+                    </p>
+                    <h3>Use a visible scroll channel</h3>
+                    <p>
+                      Cyan marks the active rail, violet separates the track,
+                      and magenta appears on hover. Keyboard users can focus the
+                      region and scroll without moving the modal chrome.
+                    </p>
+                    <p>
+                      The header and footer stay fixed while this article moves.
+                      That stable frame keeps context visible during longer blog
+                      posts, documentation, and generated explanations.
+                    </p>
+                    <p>
+                      Layer 2 only chooses layout and presentation. The Layer 1
+                      scroll primitive still owns the native overflow behavior.
+                    </p>
+                    <pre><code>&lt;mjj-content-modal&gt;
+  &lt;header data-modal-header&gt;...&lt;/header&gt;
+  &lt;zen-scroll-area data-modal-body data-cyber-scroll&gt;
+    ...
+  &lt;/zen-scroll-area&gt;
+  &lt;footer data-modal-footer&gt;...&lt;/footer&gt;
+&lt;/mjj-content-modal&gt;</code></pre>
+                  </article>
+                </zen-scroll-area>
+                <footer data-modal-footer>
+                  <p>Updated August 7, 2026</p>
+                  <div data-modal-footer-actions>
+                    <zen-button variant="quiet" size="md">
+                      <button type="button" data-zen-close>Done reading</button>
+                    </zen-button>
+                  </div>
+                </footer>
+              </dialog>
+            </zen-dialog>
+          </mjj-content-modal>
+        </article>
+
+        <article class="sandbox-card">
+          <h2>Window modal</h2>
+          <p>
+            Structured workspace for tools and multi-region interfaces with
+            fixed chrome and a bounded scrolling body.
+          </p>
+          <mjj-window-modal data-component-layer="2">
+            <zen-dialog>
+              <zen-button size="md">
+                <button type="button" data-zen-trigger>
+                  Open workspace
+                  <zen-icon name="external"></zen-icon>
+                </button>
+              </zen-button>
+              <dialog>
+                <header data-modal-header>
+                  <div data-modal-heading>
+                    <p data-modal-kicker>Workspace · Read only</p>
+                    <h2 data-modal-title>Component inspector</h2>
+                  </div>
+                  <div data-modal-header-actions>
+                    <zen-button variant="quiet" size="sm">
+                      <button type="button" aria-label="Component settings">
+                        <zen-icon name="settings"></zen-icon>
+                      </button>
+                    </zen-button>
+                    <zen-button variant="quiet" size="sm">
+                      <button type="button" data-zen-close aria-label="Close workspace">
+                        <zen-icon name="close"></zen-icon>
+                      </button>
+                    </zen-button>
+                  </div>
+                </header>
+                <zen-scroll-area
+                  data-modal-body
+                  data-modal-layout="split"
+                  data-cyber-scroll
+                  tabindex="0"
+                  aria-label="Workspace content"
+                >
+                  <zen-scroll-area
+                    data-modal-pane
+                    data-cyber-scroll
+                    tabindex="0"
+                    aria-label="Inspector regions"
+                  >
+                    <aside class="sandbox-pane">
+                      <h3>Regions</h3>
+                      <ul class="sandbox-list">
+                        <li>Header</li>
+                        <li>Body</li>
+                        <li>Footer</li>
+                      </ul>
+                    </aside>
+                  </zen-scroll-area>
+                  <zen-scroll-area
+                    data-modal-pane="primary"
+                    data-cyber-scroll
+                    tabindex="0"
+                    aria-label="Inspector body content"
+                  >
+                    <article class="sandbox-pane">
+                      <h3>Body content</h3>
+                      <p data-modal-description>
+                        The window body can host split panes, editors, previews,
+                        or tools without changing the surrounding contract.
+                      </p>
+                      <zen-field size="md">
+                        <label for="sandbox-component-name">Component name</label>
+                        <input
+                          id="sandbox-component-name"
+                          value="mjj-window-modal"
+                          readonly
+                        >
+                        <small>The active light-DOM fixture.</small>
+                      </zen-field>
+                      <ol class="sandbox-audit">
+                        <li><strong>01</strong> Header region validated</li>
+                        <li><strong>02</strong> Title association generated</li>
+                        <li><strong>03</strong> Body region bounded</li>
+                        <li><strong>04</strong> Scroll primitive connected</li>
+                        <li><strong>05</strong> Footer actions discovered</li>
+                        <li><strong>06</strong> Pixel typography inherited</li>
+                        <li><strong>07</strong> Cyberpunk theme resolved</li>
+                        <li><strong>08</strong> Keyboard scrolling enabled</li>
+                        <li><strong>09</strong> Responsive split verified</li>
+                        <li><strong>10</strong> Layer contract ready</li>
+                      </ol>
+                    </article>
+                  </zen-scroll-area>
+                </zen-scroll-area>
+                <footer data-modal-footer>
+                  <p>3 required regions · contract valid</p>
+                  <div data-modal-footer-actions>
+                    <zen-button variant="quiet" size="md">
+                      <button type="button" data-zen-close>Cancel</button>
+                    </zen-button>
+                    <zen-button size="md">
+                      <button type="button" data-zen-close>Apply</button>
+                    </zen-button>
+                  </div>
+                </footer>
+              </dialog>
+            </zen-dialog>
+          </mjj-window-modal>
+        </article>
+      </section>
+    </main>
+  </body>
+</html>
--- a/mrjunejune/src/public/composer-lab.html	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/public/composer-lab.html	Sat Aug 08 02:08:08 2026 -0700
@@ -7,9 +7,21 @@
     <link rel="stylesheet" href="/public/design-system/styles/tokens.css">
     <link rel="stylesheet" href="/public/design-system/styles/themes.css">
     <link rel="stylesheet" href="/public/design-system/styles/components.css">
-    <link rel="stylesheet" href="/public/mjj-composer.css">
     <script type="module" src="/public/design-system/components/index.js"></script>
-    <script type="module" src="/public/mjj-composer.js"></script>
+    <script type="module">
+      const version = Date.now();
+      const link = document.createElement("link");
+      link.rel = "stylesheet";
+      link.href = `/public/mjj-composer.css?sandbox=${version}`;
+      await new Promise((resolve, reject) => {
+        link.addEventListener("load", resolve, { once: true });
+        link.addEventListener("error", reject, { once: true });
+        document.head.append(link);
+      });
+      await import(`/public/mjj-composer.js?sandbox=${version}`);
+      void navigator.serviceWorker?.getRegistration()
+        .then(registration => registration?.update());
+    </script>
     <style>
       body {
         box-sizing: border-box;
--- a/mrjunejune/src/public/mjj-composer.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/public/mjj-composer.js	Sat Aug 08 02:08:08 2026 -0700
@@ -1,3 +1,10 @@
+/**
+ * Layer 2 composer composite.
+ *
+ * Dependencies flow downward only: Zenbu Layer 1 primitives and Layer 0
+ * themes/i18n/API mapping. Application routes and state remain outside.
+ */
+
 /**
  * Coordinates a native composer form built from Zenbu controls.
  *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/mjj-modal.css	Sat Aug 08 02:08:08 2026 -0700
@@ -0,0 +1,360 @@
+:where(mjj-content-modal, mjj-window-modal) {
+  display: contents;
+}
+
+:where(mjj-content-modal, mjj-window-modal) dialog {
+  --mjj-modal-border: var(--zenbu-sys-color-border-strong);
+  --mjj-modal-shadow:
+    inset 0 0 0 var(--zenbu-sys-stroke-width)
+      var(--zenbu-sys-color-action-primary-border),
+    var(--zenbu-sys-space-control)
+      var(--zenbu-sys-space-control)
+      0
+      var(--zenbu-sys-color-danger-border),
+    var(--zenbu-sys-shadow-dialog);
+  box-sizing: border-box;
+  width: min(
+    calc(100vw - var(--zenbu-sys-space-container)),
+    var(--mjj-modal-width)
+  );
+  max-width: none;
+  max-height: calc(100dvh - var(--zenbu-sys-space-container));
+  margin: auto;
+  padding: 0;
+  overflow: hidden;
+  border: var(--zenbu-sys-stroke-width) solid var(--mjj-modal-border);
+  border-radius: 0;
+  background: var(--zenbu-sys-color-surface-raised);
+  color: var(--zenbu-sys-color-text-primary);
+  box-shadow: var(--mjj-modal-shadow);
+}
+
+:where(mjj-content-modal, mjj-window-modal) dialog::backdrop {
+  background: var(--zenbu-sys-color-scrim);
+  backdrop-filter: blur(var(--zenbu-sys-stroke-width-emphasis));
+}
+
+:where(mjj-content-modal, mjj-window-modal) dialog[open] {
+  display: grid;
+  grid-template-rows: auto minmax(0, 1fr) auto;
+  animation:
+    mjj-modal-enter
+    var(--zenbu-sys-motion-duration-layout)
+    var(--zenbu-sys-motion-ease-enter);
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-header] {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto;
+  align-items: center;
+  gap: var(--zenbu-sys-space-group);
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-heading] {
+  display: grid;
+  gap: var(--zenbu-sys-space-icon-label);
+  min-width: 0;
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-kicker] {
+  margin: 0;
+  color: var(--zenbu-sys-color-warning-foreground);
+  font-size: var(--zenbu-sys-font-size-xs);
+  font-weight: var(--zenbu-sys-font-weight-strong);
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-title] {
+  margin: 0;
+  color: var(--zenbu-sys-color-action-primary-background);
+  font-family: var(--zenbu-sys-type-title-family);
+  font-size: var(--zenbu-sys-type-title-size);
+  font-weight: var(--zenbu-sys-type-title-weight);
+  line-height: var(--zenbu-sys-type-title-line-height);
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-header-actions],
+:where(mjj-content-modal, mjj-window-modal) [data-modal-footer-actions] {
+  display: flex;
+  align-items: center;
+  gap: var(--zenbu-sys-space-control);
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-header-actions] {
+  justify-content: flex-end;
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-footer] {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: var(--zenbu-sys-space-group);
+}
+
+:where(mjj-content-modal, mjj-window-modal) [data-modal-footer] > p {
+  margin: 0;
+  color: var(--zenbu-sys-color-text-secondary);
+  font-size: var(--zenbu-sys-font-size-sm);
+}
+
+:where(mjj-content-modal, mjj-window-modal) zen-scroll-area[data-cyber-scroll] {
+  box-sizing: border-box;
+  display: block;
+  width: 100%;
+  height: 100%;
+  max-height: none;
+  min-width: 0;
+  min-height: 0;
+  overflow-x: auto;
+  overflow-y: scroll;
+  border: 0;
+  border-radius: 0;
+  background: var(--zenbu-sys-color-surface-default);
+  scrollbar-color:
+    var(--zenbu-sys-color-action-primary-background)
+    var(--zenbu-sys-color-surface-sunken);
+  scrollbar-width: thin;
+  scrollbar-gutter: stable;
+}
+
+:where(mjj-content-modal, mjj-window-modal)
+  zen-scroll-area[data-cyber-scroll]::-webkit-scrollbar {
+  width: var(--zenbu-sys-space-control);
+  height: var(--zenbu-sys-space-control);
+}
+
+:where(mjj-content-modal, mjj-window-modal)
+  zen-scroll-area[data-cyber-scroll]::-webkit-scrollbar-track {
+  border-inline-start: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-info-border);
+  background: var(--zenbu-sys-color-surface-sunken);
+}
+
+:where(mjj-content-modal, mjj-window-modal)
+  zen-scroll-area[data-cyber-scroll]::-webkit-scrollbar-thumb {
+  border: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-surface-sunken);
+  border-radius: 0;
+  background: var(--zenbu-sys-color-action-primary-background);
+}
+
+:where(mjj-content-modal, mjj-window-modal)
+  zen-scroll-area[data-cyber-scroll]::-webkit-scrollbar-thumb:hover {
+  background: var(--zenbu-sys-color-danger-foreground);
+}
+
+mjj-content-modal {
+  --mjj-modal-width: 54rem;
+}
+
+mjj-content-modal dialog {
+  --mjj-modal-border: var(--zenbu-sys-color-action-primary-border);
+}
+
+mjj-content-modal [data-modal-header] {
+  padding:
+    var(--zenbu-sys-padding-xl)
+    var(--zenbu-sys-padding-xl)
+    var(--zenbu-sys-padding-lg);
+  border-bottom: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-danger-border);
+  background: var(--zenbu-sys-color-surface-raised);
+}
+
+mjj-content-modal [data-modal-body] {
+  min-height: 0;
+  padding: 0;
+  overflow: auto;
+  background: var(--zenbu-sys-color-surface-default);
+  color: var(--zenbu-sys-color-text-primary);
+  font-family: var(--zenbu-sys-type-reading-family);
+  font-size: var(--zenbu-sys-type-reading-size);
+  line-height: var(--zenbu-sys-type-reading-line-height);
+}
+
+mjj-content-modal [data-modal-article] {
+  display: grid;
+  gap: var(--zenbu-sys-space-content);
+  padding:
+    var(--zenbu-sys-padding-xl)
+    max(
+      var(--zenbu-sys-padding-xl),
+      calc((100% - 42rem) / 2)
+    );
+}
+
+mjj-content-modal [data-modal-article] > :where(
+  h2,
+  h3,
+  p,
+  blockquote,
+  pre,
+  ul,
+  ol
+) {
+  margin-block: 0;
+}
+
+mjj-content-modal [data-modal-body] :where(h2, h3) {
+  color: var(--zenbu-sys-color-action-primary-background);
+  font-family: var(--zenbu-sys-font-family-ui);
+}
+
+mjj-content-modal [data-modal-body] [data-modal-description] {
+  color: var(--zenbu-sys-color-text-secondary);
+  font-size: var(--zenbu-sys-type-body-large-size);
+  line-height: var(--zenbu-sys-type-body-large-line-height);
+}
+
+mjj-content-modal [data-modal-body] blockquote {
+  padding:
+    var(--zenbu-sys-padding-md)
+    var(--zenbu-sys-padding-lg);
+  border-inline-start: var(--zenbu-sys-stroke-width-emphasis) solid
+    var(--zenbu-sys-color-danger-border);
+  background: var(--zenbu-sys-color-surface-sunken);
+  color: var(--zenbu-sys-color-text-secondary);
+}
+
+mjj-content-modal [data-modal-body] pre {
+  padding: var(--zenbu-sys-padding-lg);
+  overflow: auto;
+  border: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-info-border);
+  border-radius: 0;
+  background: var(--zenbu-sys-color-surface-sunken);
+  font-family: var(--zenbu-sys-type-code-family);
+  font-size: var(--zenbu-sys-type-code-size);
+  line-height: var(--zenbu-sys-type-code-line-height);
+}
+
+mjj-content-modal [data-modal-footer] {
+  padding:
+    var(--zenbu-sys-padding-md)
+    var(--zenbu-sys-padding-xl);
+  border-top: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-danger-border);
+  background: var(--zenbu-sys-color-surface-raised);
+}
+
+mjj-window-modal {
+  --mjj-modal-width: 72rem;
+}
+
+mjj-window-modal dialog {
+  --mjj-modal-border: var(--zenbu-sys-color-info-border);
+  height: min(82dvh, 48rem);
+  border-radius: 0;
+}
+
+mjj-window-modal [data-modal-header] {
+  padding:
+    var(--zenbu-sys-padding-md)
+    var(--zenbu-sys-padding-lg);
+  border-bottom: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-action-primary-border);
+  background: var(--zenbu-sys-color-surface-sunken);
+  box-shadow: var(--zenbu-sys-shadow-subtle);
+}
+
+mjj-window-modal [data-modal-body] {
+  min-width: 0;
+  min-height: 0;
+  padding: var(--zenbu-sys-padding-lg);
+  overflow: auto;
+  background: var(--zenbu-sys-color-surface-default);
+}
+
+mjj-window-modal [data-modal-body][data-modal-layout="split"] {
+  display: grid;
+  grid-template-columns: minmax(12rem, 0.32fr) minmax(0, 1fr);
+  gap: var(--zenbu-sys-space-group);
+}
+
+mjj-window-modal [data-modal-pane] {
+  min-width: 0;
+  min-height: 0;
+  padding: var(--zenbu-sys-padding-lg);
+  overflow: auto;
+  border: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-info-border);
+  border-radius: 0;
+  background: var(--zenbu-sys-color-surface-sunken);
+}
+
+mjj-window-modal [data-modal-pane="primary"] {
+  border-color: var(--zenbu-sys-color-action-primary-border);
+  background: var(--zenbu-sys-color-surface-raised);
+  box-shadow:
+    var(--zenbu-sys-space-icon-label)
+    var(--zenbu-sys-space-icon-label)
+    0
+    var(--zenbu-sys-color-danger-border);
+}
+
+mjj-window-modal [data-modal-footer] {
+  padding:
+    var(--zenbu-sys-padding-sm)
+    var(--zenbu-sys-padding-lg);
+  border-top: var(--zenbu-sys-stroke-width) solid
+    var(--zenbu-sys-color-action-primary-border);
+  background: var(--zenbu-sys-color-surface-sunken);
+}
+
+@keyframes mjj-modal-enter {
+  0% {
+    clip-path: inset(49% 45%);
+    opacity: 0.72;
+    transform: scaleX(0.12);
+  }
+  45% {
+    clip-path: inset(49% 0);
+    opacity: 1;
+    transform: scaleX(1);
+  }
+  100% {
+    clip-path: inset(0);
+    opacity: 1;
+    transform: scaleX(1);
+  }
+}
+
+@media (max-width: 42rem) {
+  :where(mjj-content-modal, mjj-window-modal) dialog {
+    width: calc(100vw - var(--zenbu-sys-space-group));
+    max-height: calc(100dvh - var(--zenbu-sys-space-group));
+  }
+
+  mjj-content-modal [data-modal-header],
+  mjj-content-modal [data-modal-footer] {
+    padding-inline: var(--zenbu-sys-padding-lg);
+  }
+
+  mjj-content-modal [data-modal-body] {
+    padding: 0;
+  }
+
+  mjj-content-modal [data-modal-article] {
+    padding: var(--zenbu-sys-padding-lg);
+  }
+
+  mjj-window-modal dialog {
+    height: calc(100dvh - var(--zenbu-sys-space-group));
+  }
+
+  mjj-window-modal [data-modal-body][data-modal-layout="split"] {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  :where(mjj-content-modal, mjj-window-modal) [data-modal-footer] {
+    align-items: stretch;
+    flex-direction: column;
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  :where(mjj-content-modal, mjj-window-modal) dialog[open] {
+    animation: none;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/public/mjj-modal.js	Sat Aug 08 02:08:08 2026 -0700
@@ -0,0 +1,130 @@
+/**
+ * Layer 2 modal composites.
+ *
+ * Dependencies flow downward only: Zenbu Layer 1 primitives and Layer 0
+ * themes/i18n/API mapping. Application routes and state remain outside.
+ */
+
+let nextModalId = 1;
+
+/**
+ * Validates and labels a site modal composed with zen-dialog.
+ *
+ * @extends {HTMLElement}
+ */
+class MjjModal extends HTMLElement {
+  connectedCallback() {
+    this._observer ||= new MutationObserver(() => this.sync());
+    this._observer.observe(this, { childList: true, subtree: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+  }
+
+  /**
+   * Applies the strict modal region contract.
+   */
+  sync() {
+    const owner = this.querySelector(":scope > zen-dialog");
+    const dialog = owner?.querySelector(":scope > dialog");
+    const header = dialog?.querySelector(":scope > [data-modal-header]");
+    const body = dialog?.querySelector(":scope > [data-modal-body]");
+    const footer = dialog?.querySelector(":scope > [data-modal-footer]");
+    const title = header?.querySelector("[data-modal-title]");
+    const regions = dialog
+      ? [...dialog.children].filter(element =>
+          element.matches(
+            "[data-modal-header], [data-modal-body], [data-modal-footer]",
+          ))
+      : [];
+    const regionOrder = regions.map(element => {
+      if (element.hasAttribute("data-modal-header")) return "header";
+      if (element.hasAttribute("data-modal-body")) return "body";
+      return "footer";
+    });
+    const expectedOrder = footer
+      ? ["header", "body", "footer"]
+      : ["header", "body"];
+    const errors = [];
+
+    if (!owner) errors.push("requires one direct zen-dialog");
+    if (!dialog) errors.push("requires one direct native dialog");
+    if (!header) errors.push("requires a direct data-modal-header region");
+    if (!body) errors.push("requires a direct data-modal-body region");
+    if (!title) errors.push("requires data-modal-title inside the header");
+    if (regionOrder.join(",") !== expectedOrder.join(",")) {
+      errors.push(`regions must be ordered ${expectedOrder.join(", ")}`);
+    }
+
+    if (errors.length) {
+      this.dataset.invalid = "";
+      this.removeAttribute("data-ready");
+      const message = `${this.localName}: ${errors.join("; ")}`;
+      if (this._lastError !== message) {
+        this._lastError = message;
+        console.error(message);
+        this.dispatchEvent(new CustomEvent("mjj-modal-error", {
+          bubbles: true,
+          detail: { errors: [...errors] },
+        }));
+      }
+      return;
+    }
+
+    delete this.dataset.invalid;
+    this.dataset.ready = "";
+    this.dataset.componentLayer = "2";
+    this._lastError = null;
+    if (!title.id) title.id = `mjj-modal-title-${nextModalId++}`;
+    dialog.setAttribute("aria-labelledby", title.id);
+    dialog.setAttribute("aria-modal", "true");
+    const description = body.querySelector("[data-modal-description]");
+    if (description) {
+      if (!description.id) {
+        description.id = `mjj-modal-description-${nextModalId++}`;
+      }
+      dialog.setAttribute("aria-describedby", description.id);
+    } else {
+      dialog.removeAttribute("aria-describedby");
+    }
+  }
+
+  /**
+   * Opens the composed Zenbu dialog.
+   */
+  open() {
+    this.querySelector(":scope > zen-dialog")?.open();
+  }
+
+  /**
+   * Closes the composed Zenbu dialog.
+   *
+   * @param {string=} returnValue
+   */
+  close(returnValue) {
+    this.querySelector(":scope > zen-dialog")?.close(returnValue);
+  }
+}
+
+/**
+ * Presents long-form editorial content with a reading-focused body.
+ *
+ * @extends {MjjModal}
+ */
+export class MjjContentModal extends MjjModal {}
+
+/**
+ * Presents application content in a strict window-style frame.
+ *
+ * @extends {MjjModal}
+ */
+export class MjjWindowModal extends MjjModal {}
+
+if (!customElements.get("mjj-content-modal")) {
+  customElements.define("mjj-content-modal", MjjContentModal);
+}
+if (!customElements.get("mjj-window-modal")) {
+  customElements.define("mjj-window-modal", MjjWindowModal);
+}
--- a/mrjunejune/src/public/pwa-register.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/public/pwa-register.js	Sat Aug 08 02:08:08 2026 -0700
@@ -5,6 +5,7 @@
       .register('/sw.js', { scope: '/' })
       .then((registration) => {
         // console.log('[PWA] Service Worker registered:', registration.scope);
+        void registration.update();
 
         // Check for updates periodically
         setInterval(() => {
@@ -16,11 +17,7 @@
           const newWorker = registration.installing;
           newWorker.addEventListener('statechange', () => {
             if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
-              // New content available, show update notification
-              if (confirm('New version available! Reload to update?')) {
-                newWorker.postMessage({ type: 'SKIP_WAITING' });
-                window.location.reload();
-              }
+              newWorker.postMessage({ type: 'SKIP_WAITING' });
             }
           });
         });
--- a/mrjunejune/src/sw.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/sw.js	Sat Aug 08 02:08:08 2026 -0700
@@ -1,6 +1,12 @@
 // Root-scoped Service Worker for MrJuneJune PWA
-const CACHE_VERSION = 'v35-role-greeting';
+const CACHE_VERSION = 'v37-network-first';
 const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`;
+const DEVELOPMENT_SANDBOX_PATHS = [
+  '/public/component-sandbox',
+  '/public/composer-lab',
+  '/public/mjj-composer.',
+  '/public/mjj-modal.',
+];
 
 // Files to cache immediately on install
 const STATIC_CACHE = [
@@ -73,7 +79,46 @@
   );
 });
 
-// Fetch event - serve from cache, fallback to network
+function isCacheable(response) {
+  return response &&
+    response.status === 200 &&
+    response.type !== 'error';
+}
+
+async function updateCache(request, response) {
+  if (!isCacheable(response)) return;
+  const cache = await caches.open(CACHE_NAME);
+  await cache.put(request, response.clone());
+}
+
+async function networkFirst(request, offlineFallback = false) {
+  try {
+    const response = await fetch(request, { cache: 'no-cache' });
+    await updateCache(request, response);
+    return response;
+  } catch (error) {
+    const cached = await caches.match(request);
+    if (cached) return cached;
+    if (offlineFallback) {
+      const offline = await caches.match('/offline.html');
+      if (offline) return offline;
+    }
+    throw error;
+  }
+}
+
+async function staleWhileRevalidate(event, request) {
+  const cached = await caches.match(request);
+  const update = fetch(request, { cache: 'no-cache' }).then(async response => {
+    await updateCache(request, response);
+    return response;
+  });
+  if (!cached) return update;
+  event.waitUntil(update.catch(() => undefined));
+  return cached;
+}
+
+// Fetch event - current code first, cached media first, offline fallback.
 self.addEventListener('fetch', (event) => {
   const { request } = event;
   const url = new URL(request.url);
@@ -88,6 +133,10 @@
     return;
   }
 
+  if (url.origin !== self.location.origin) {
+    return;
+  }
+
   // Skip API calls and media uploads (always go to network)
   if (url.pathname.startsWith('/api/')) {
     return;
@@ -98,60 +147,32 @@
     return;
   }
 
+  // Component workbenches must reflect every ibazel rebuild on any hostname.
+  if (DEVELOPMENT_SANDBOX_PATHS.some(path =>
+    url.pathname.startsWith(path))) {
+    return;
+  }
+
   // The HLS tool and media must always reflect the current player version.
   if (url.pathname.startsWith('/tools/hls_player') ||
       url.pathname.startsWith('/public/hls-sample/')) {
     return;
   }
 
-  event.respondWith(
-    caches.match(request).then((cachedResponse) => {
-      if (cachedResponse) {
-        console.log('[SW] Serving from cache:', url.pathname);
-        return cachedResponse;
-      }
-
-      // Not in cache, fetch from network
-      return fetch(request).then((networkResponse) => {
-        // Only cache successful responses
-        if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {
-          return networkResponse;
-        }
+  const acceptsHtml = request.mode === 'navigate' ||
+    request.headers.get('Accept')?.includes('text/html');
+  const currentCode = /\.(?:css|js|json|wasm)$/i.test(url.pathname);
+  const revalidatedMedia =
+    /\.(?:svg|jpe?g|webp|woff2?|ttf|otf|pdf)$/i.test(url.pathname);
 
-        // Cache specific file types
-        const shouldCache =
-          url.pathname.endsWith('.css') ||
-          url.pathname.endsWith('.js') ||
-          url.pathname.endsWith('.svg') ||
-          url.pathname.endsWith('.jpg') ||
-          url.pathname.endsWith('.webp') ||
-          url.pathname.endsWith('.woff') ||
-          url.pathname.endsWith('.woff2') ||
-          url.pathname.endsWith('.ttf') ||
-          url.pathname.endsWith('.otf') ||
-          url.pathname.startsWith('/blog/') ||
-          url.pathname.startsWith('/notes/') ||
-          url.pathname === '/';
+  if (acceptsHtml || currentCode) {
+    event.respondWith(networkFirst(request, acceptsHtml));
+    return;
+  }
 
-        if (shouldCache) {
-          const responseToCache = networkResponse.clone();
-          caches.open(CACHE_NAME).then((cache) => {
-            console.log('[SW] Caching new resource:', url.pathname);
-            cache.put(request, responseToCache);
-          });
-        }
-
-        return networkResponse;
-      }).catch((error) => {
-        console.log('[SW] Fetch failed:', error);
-
-        // Return offline page for HTML requests
-        if (request.headers.get('Accept').includes('text/html')) {
-          return caches.match('/offline.html');
-        }
-      });
-    })
-  );
+  if (revalidatedMedia) {
+    event.respondWith(staleWhileRevalidate(event, request));
+  }
 });
 
 // Handle messages from the client
--- a/mrjunejune/test/snapshots/jrpg.snapshot	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/test/snapshots/jrpg.snapshot	Sat Aug 08 02:08:08 2026 -0700
@@ -36,13 +36,18 @@
     <link rel="preload" href="/public/jrpg/background-frame-mobile.webp" as="image" media="(max-width: 52rem) and (orientation: portrait)">
     <link rel="preload" href="/public/jrpg/bar-ink.webp" as="image">
     <link rel="stylesheet" href="/public/mjj-composer.css">
+    <link rel="stylesheet" href="/public/mjj-modal.css">
     <link rel="stylesheet" href="/jrpg/jrpg.css">
     <script type="module" src="/public/mjj-composer.js"></script>
+    <script type="module" src="/public/mjj-modal.js"></script>
     <script type="module" src="/jrpg/jrpg.js"></script>
   </head>
   <body class="jrpg-page">
     <main class="jrpg-main">
-      <mjj-jrpg-shell class="jrpg-shell">
+      <mjj-jrpg-shell
+        class="jrpg-shell"
+        data-initial-panel="resume"
+      >
         <div class="jrpg-workspace">
           <div class="jrpg-brand">
             <header>
@@ -78,61 +83,67 @@
               </a>
             </zen-button>
           </nav>
-          <zen-dialog
+          <mjj-window-modal
             class="jrpg-mobile-destination-owner"
             data-mobile-destination-owner
           >
-            <zen-button class="jrpg-mobile-menu-btn" appearance="plain" size="xs">
-              <button
-                type="button"
-                data-zen-trigger
-                data-mobile-menu-toggle
-                aria-label="Open destination menu"
-              >
-                <zen-icon name="menu"></zen-icon>
-              </button>
-            </zen-button>
-            <dialog
-              class="jrpg-mobile-destination-dialog"
-              data-mobile-destination-dialog
-              aria-labelledby="jrpg-mobile-destination-title"
-            >
-              <header class="jrpg-mobile-destination-header">
-                <zen-button
-                  appearance="plain"
-                  size="sm"
-                  data-mobile-destination-back-owner
-                  hidden
+            <zen-dialog>
+              <zen-button class="jrpg-mobile-menu-btn" appearance="plain" size="xs">
+                <button
+                  type="button"
+                  data-zen-trigger
+                  data-mobile-menu-toggle
+                  aria-label="Open destination menu"
                 >
-                  <button
-                    type="button"
-                    data-mobile-destination-back
-                    aria-label="Back to destination menu"
+                  <zen-icon name="menu"></zen-icon>
+                </button>
+              </zen-button>
+              <dialog
+                class="jrpg-mobile-destination-dialog"
+                data-mobile-destination-dialog
+              >
+                <header class="jrpg-mobile-destination-header" data-modal-header>
+                  <zen-button
+                    appearance="plain"
+                    size="sm"
+                    data-mobile-destination-back-owner
+                    hidden
                   >
-                    <zen-icon name="chevron-left"></zen-icon>
-                    Menu
-                  </button>
-                </zen-button>
-                <h2
-                  id="jrpg-mobile-destination-title"
-                  data-mobile-destination-title
-                >Menu</h2>
-                <zen-button appearance="plain" size="sm">
-                  <button
-                    type="button"
-                    data-zen-close
-                    aria-label="Close destination menu"
-                  >
-                    <zen-icon name="close"></zen-icon>
-                  </button>
-                </zen-button>
-              </header>
-              <div
-                class="jrpg-mobile-destination-content"
-                data-mobile-destination-content
-              ></div>
-            </dialog>
-          </zen-dialog>
+                    <button
+                      type="button"
+                      data-mobile-destination-back
+                      aria-label="Back to destination menu"
+                    >
+                      <zen-icon name="chevron-left"></zen-icon>
+                      Menu
+                    </button>
+                  </zen-button>
+                  <h2
+                    id="jrpg-mobile-destination-title"
+                    data-mobile-destination-title
+                    data-modal-title
+                  >Menu</h2>
+                  <zen-button appearance="plain" size="sm">
+                    <button
+                      type="button"
+                      data-zen-close
+                      aria-label="Close destination menu"
+                    >
+                      <zen-icon name="close"></zen-icon>
+                    </button>
+                  </zen-button>
+                </header>
+                <zen-scroll-area
+                  class="jrpg-mobile-destination-content"
+                  data-mobile-destination-content
+                  data-modal-body
+                  data-cyber-scroll
+                  tabindex="0"
+                  aria-label="Destination content"
+                ></zen-scroll-area>
+              </dialog>
+            </zen-dialog>
+          </mjj-window-modal>
           <footer class="jrpg-frame-telemetry" aria-label="System telemetry">
             <span aria-label="Temperature unavailable">N/A</span>
             <span data-frame-account aria-live="polite"><a href="/login?next=/jrpg">LOGIN</a></span>
@@ -178,50 +189,60 @@
           <section class="jrpg-utility" aria-label="Selected destination">
             <mjj-jrpg-preview data-selection="resume">
               <article class="jrpg-preview-panel" aria-live="polite">
-                <div class="jrpg-preview-content" data-preview-content>
+                <div
+                  class="jrpg-preview-content"
+                  data-preview-content
+                  
+                >
                   <zen-heading size="xl">
                     <h2 data-preview-title>Resume</h2>
                   </zen-heading>
                   <p data-preview-copy>
-                    Member of Technical Staff and engineering leader with 10+ years building AI agent platforms and production systems.
+                    Member of Technical Staff and engineering leader with 10+ years building AI agent platforms and production systems across Microsoft, Meta, Google, and growth-stage companies.
                   </p>
-                  <ul class="jrpg-work-showcase" data-work-showcase>
-                    <li>
-                      <a href="https://www.microsoft.com/en-us/microsoft-copilot/blog/2026/02/26/copilot-tasks-from-answers-to-actions/">
-                        <span>Copilot Tasks</span>
-                        <small>Agentic execution</small>
-                      </a>
-                    </li>
-                    <li>
-                      <a href="https://www.cio.com/article/3977098/microsoft-doubles-down-on-multi-model-ai-as-it-builds-a-copilot-super-app.html">
-                        <span>Copilot SuperApp</span>
-                        <small>AI platform</small>
-                      </a>
-                    </li>
-                  </ul>
-                  <zen-dialog class="jrpg-preview-dialog">
-                    <zen-button appearance="plain" size="xs" data-dialog-trigger-owner hidden>
-                      <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
-                    </zen-button>
-                    <dialog class="jrpg-detail-dialog" aria-labelledby="jrpg-dialog-title">
-                      <div class="jrpg-dialog-heading">
-                        <p>DESTINATION DATA</p>
+                  <ul class="jrpg-work-showcase" data-work-showcase><li><a href="/resume" data-resume-modal><span>Full resume</span><small>Career dossier</small></a></li><li><a href="https://www.microsoft.com/en-us/microsoft-copilot/blog/2026/02/26/copilot-tasks-from-answers-to-actions/"><span>Copilot Tasks</span><small>Agentic execution</small></a></li></ul>
+                  <mjj-content-modal data-detail-modal-owner>
+                    <zen-dialog class="jrpg-preview-dialog">
+                      <zen-button appearance="plain" size="xs" data-dialog-trigger-owner hidden>
+                        <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
+                      </zen-button>
+                      <dialog class="jrpg-detail-dialog">
+                        <header class="jrpg-dialog-heading" data-modal-header>
+                          <div data-modal-heading>
+                            <p data-modal-kicker>DESTINATION DATA</p>
+                            <h2
+                              id="jrpg-dialog-title"
+                              data-dialog-title
+                              data-modal-title
+                            >Resume</h2>
+                          </div>
                         <div class="jrpg-dialog-heading-actions">
-                          <zen-button appearance="plain" size="md">
-                            <a href="/resume" data-preview-link>
-                              Open page
-                              <zen-icon name="external"></zen-icon>
+                          <zen-button
+                            appearance="plain"
+                            size="sm"
+                            data-resume-download
+                          >
+                            <a href="/public/resume.pdf">
+                              Download PDF
+                              <zen-icon name="download"></zen-icon>
                             </a>
                           </zen-button>
-                          <zen-button appearance="plain" size="md">
+                          <zen-button appearance="plain" size="sm">
                             <button type="button" data-zen-close aria-label="Close destination details">
                               <zen-icon name="close"></zen-icon>
                             </button>
                           </zen-button>
-                        </div>
-                      </div>
-                      <h2 id="jrpg-dialog-title" data-dialog-title>Resume</h2>
-                      <p data-dialog-copy>Experience, projects, and the systems I have helped build.</p>
+                          </div>
+                        </header>
+                        <zen-scroll-area
+                          class="jrpg-detail-body"
+                          data-modal-body
+                          data-cyber-scroll
+                          tabindex="0"
+                          aria-label="Destination details"
+                        >
+                          <div class="jrpg-detail-content" data-modal-article>
+                            <p data-dialog-copy>Experience, projects, and the systems I have helped build.</p>
                       <section
                         class="jrpg-resume-dossier"
                         data-resume-dossier
@@ -258,18 +279,16 @@
                           <div data-tool-content></div>
                         </article>
                       </section>
-                      <div class="jrpg-dialog-actions" data-dialog-actions>
-                        <zen-button appearance="plain" size="lg" data-resume-download>
-                          <a href="/public/resume.pdf">
-                            Download PDF
-                            <zen-icon name="download"></zen-icon>
-                          </a>
-                        </zen-button>
-                      </div>
-                    </dialog>
-                  </zen-dialog>
+                          </div>
+                        </zen-scroll-area>
+                      </dialog>
+                    </zen-dialog>
+                  </mjj-content-modal>
                 </div>
-                <mjj-conversation-archive hidden aria-label="Conversations">
+                <mjj-conversation-archive
+                  hidden
+                  aria-label="Conversations"
+                >
               <div class="jrpg-archive-heading">
                 <h2>Archive</h2>
                 <zen-button appearance="plain" size="xs">
@@ -292,52 +311,83 @@
                 <button type="button" data-archive-load-more>Load more</button>
               </zen-button>
               <p class="jrpg-archive-status" data-archive-status aria-live="polite">Loading conversations...</p>
-              <dialog
-                class="jrpg-archive-dialog"
-                data-archive-delete-dialog
-                aria-labelledby="archive-delete-title"
-                aria-modal="true"
-              >
-                <p id="archive-delete-title" class="jrpg-archive-dialog-title">Delete this conversation?</p>
-                <p data-archive-delete-name class="jrpg-archive-dialog-subtitle"></p>
-                <div class="jrpg-archive-dialog-actions">
-                  <zen-button appearance="plain" size="md">
-                    <button type="button" data-archive-delete-cancel>Cancel</button>
-                  </zen-button>
-                  <zen-button appearance="plain" size="md">
-                    <button type="button" data-archive-delete-confirm>Delete</button>
+              <mjj-content-modal data-archive-delete-owner>
+                <zen-dialog>
+                  <zen-button appearance="plain" size="xs" hidden>
+                    <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
                   </zen-button>
-                </div>
-              </dialog>
-              <dialog
-                class="jrpg-archive-dialog"
-                data-archive-rename-dialog
-                aria-labelledby="archive-rename-title"
-                aria-modal="true"
-              >
-                <p id="archive-rename-title" class="jrpg-archive-dialog-title">Rename conversation</p>
-                <form data-archive-rename-form novalidate>
-                  <zen-field appearance="plain" size="md">
-                    <label for="archive-rename-input">New title</label>
-                    <input
-                      id="archive-rename-input"
-                      data-archive-rename-input
-                      type="text"
-                      maxlength="200"
-                      required
-                      autocomplete="off"
+                  <dialog
+                    class="jrpg-archive-dialog"
+                    data-archive-delete-dialog
+                  >
+                    <header data-modal-header>
+                    <h2
+                      id="archive-delete-title"
+                      class="jrpg-archive-dialog-title"
+                      data-modal-title
+                    >Delete this conversation?</h2>
+                    </header>
+                    <div data-modal-body>
+                    <div data-modal-article>
+                      <p data-archive-delete-name class="jrpg-archive-dialog-subtitle"></p>
+                    </div>
+                    </div>
+                    <footer class="jrpg-archive-dialog-actions" data-modal-footer>
+                    <zen-button appearance="plain" size="md">
+                      <button type="button" data-archive-delete-cancel>Cancel</button>
+                    </zen-button>
+                    <zen-button appearance="plain" size="md">
+                      <button type="button" data-archive-delete-confirm>Delete</button>
+                    </zen-button>
+                    </footer>
+                  </dialog>
+                </zen-dialog>
+              </mjj-content-modal>
+              <mjj-window-modal data-archive-rename-owner>
+                <zen-dialog>
+                  <zen-button appearance="plain" size="xs" hidden>
+                    <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
+                  </zen-button>
+                  <dialog
+                    class="jrpg-archive-dialog"
+                    data-archive-rename-dialog
+                  >
+                    <header data-modal-header>
+                    <h2
+                      id="archive-rename-title"
+                      class="jrpg-archive-dialog-title"
+                      data-modal-title
+                    >Rename conversation</h2>
+                    </header>
+                    <form
+                    id="archive-rename-form"
+                    data-modal-body
+                    data-archive-rename-form
+                    novalidate
                     >
-                  </zen-field>
-                  <div class="jrpg-archive-dialog-actions">
+                    <zen-field appearance="plain" size="md">
+                      <label for="archive-rename-input">New title</label>
+                      <input
+                        id="archive-rename-input"
+                        data-archive-rename-input
+                        type="text"
+                        maxlength="200"
+                        required
+                        autocomplete="off"
+                      >
+                    </zen-field>
+                    </form>
+                    <footer class="jrpg-archive-dialog-actions" data-modal-footer>
                     <zen-button appearance="plain" size="md">
                       <button type="button" data-archive-rename-cancel>Cancel</button>
                     </zen-button>
                     <zen-button appearance="plain" size="md">
-                      <button type="submit">Save</button>
+                      <button type="submit" form="archive-rename-form">Save</button>
                     </zen-button>
-                  </div>
-                </form>
-              </dialog>
+                    </footer>
+                  </dialog>
+                </zen-dialog>
+              </mjj-window-modal>
                 </mjj-conversation-archive>
               </article>
             </mjj-jrpg-preview>
@@ -435,26 +485,30 @@
             </nav>
           </mjj-jrpg-menu>
 
-          <zen-dialog class="jrpg-login-dialog-owner" data-login-dialog-owner>
-            <zen-button appearance="plain" size="md" hidden>
-              <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
-            </zen-button>
-            <dialog
-              class="jrpg-login-dialog"
-              data-login-dialog
-              aria-labelledby="jrpg-login-title"
-              aria-modal="true"
-            >
-              <div class="jrpg-login-dialog-header">
-                <h2 id="jrpg-login-title">SIGN IN</h2>
-                <zen-button appearance="plain" size="md">
-                  <button type="button" data-zen-close aria-label="Close sign-in dialog">
-                    <zen-icon name="close"></zen-icon>
-                  </button>
-                </zen-button>
-              </div>
-              <div role="alert" data-login-error aria-live="assertive" hidden></div>
-              <form data-login-form novalidate>
+          <mjj-window-modal class="jrpg-login-dialog-owner" data-login-dialog-owner>
+            <zen-dialog>
+              <zen-button appearance="plain" size="md" hidden>
+                <button type="button" data-zen-trigger tabindex="-1" aria-hidden="true"></button>
+              </zen-button>
+              <dialog
+                class="jrpg-login-dialog"
+                data-login-dialog
+              >
+                <header class="jrpg-login-dialog-header" data-modal-header>
+                  <h2 id="jrpg-login-title" data-modal-title>SIGN IN</h2>
+                  <zen-button appearance="plain" size="md">
+                    <button type="button" data-zen-close aria-label="Close sign-in dialog">
+                      <zen-icon name="close"></zen-icon>
+                    </button>
+                  </zen-button>
+                </header>
+                <form
+                  id="jrpg-login-form"
+                  data-login-form
+                  data-modal-body
+                  novalidate
+                >
+                  <div role="alert" data-login-error aria-live="assertive" hidden></div>
                 <zen-field appearance="plain" size="md">
                   <label for="jrpg-login-username">Username</label>
                   <input
@@ -480,17 +534,22 @@
                     maxlength="1024"
                   >
                 </zen-field>
-                <div class="jrpg-login-dialog-actions">
+                </form>
+                <footer class="jrpg-login-dialog-actions" data-modal-footer>
                   <zen-button appearance="plain" size="md">
                     <button type="button" data-login-cancel data-zen-close>Cancel</button>
                   </zen-button>
                   <zen-button appearance="plain" size="md">
-                    <button type="submit" data-login-submit>Sign in</button>
+                    <button
+                      type="submit"
+                      form="jrpg-login-form"
+                      data-login-submit
+                    >Sign in</button>
                   </zen-button>
-                </div>
-              </form>
-            </dialog>
-          </zen-dialog>
+                </footer>
+              </dialog>
+            </zen-dialog>
+          </mjj-window-modal>
         </div>
       </mjj-jrpg-shell>
     </main>
--- a/mrjunejune/test/theme_and_webp_test.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/test/theme_and_webp_test.js	Sat Aug 08 02:08:08 2026 -0700
@@ -491,6 +491,173 @@
   await context.close();
 }
 
+async function testModalSandbox(browser) {
+  const context = await browser.newContext({
+    serviceWorkers: 'block',
+    viewport: { width: 1280, height: 900 },
+  });
+  const page = await context.newPage();
+  const errors = [];
+  page.on('pageerror', error => errors.push(error.message));
+  page.on('console', message => {
+    if (message.type() === 'error') errors.push(message.text());
+  });
+  await page.goto(`${baseUrl}/public/component-sandbox.html`, {
+    waitUntil: 'networkidle',
+  });
+  await page.waitForFunction(() =>
+    customElements.get('mjj-content-modal') &&
+    customElements.get('mjj-window-modal') &&
+    [...document.querySelectorAll('mjj-content-modal, mjj-window-modal')]
+      .every(modal => modal.hasAttribute('data-ready'))
+  );
+  await page.evaluate(() => document.fonts.ready);
+  const modalCss = await (
+    await fetch(`${baseUrl}/public/mjj-modal.css`, { cache: 'no-store' })
+  ).text();
+  assert.match(modalCss, /clip-path:\s*inset\(49% 45%\)/);
+  assert.match(modalCss, /clip-path:\s*inset\(49% 0\)/);
+  assert.match(modalCss, /scrollbar-gutter:\s*stable/);
+
+  assert.equal(
+    await page.locator('mjj-content-modal[data-invalid], mjj-window-modal[data-invalid]')
+      .count(),
+    0,
+  );
+
+  await page.getByRole('button', { name: 'Open article' }).click();
+  const contentDialog = page.locator('mjj-content-modal dialog');
+  await contentDialog.waitFor({ state: 'visible' });
+  await contentDialog.evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
+  const contentMetrics = await contentDialog.evaluate(dialog => {
+    const body = dialog.querySelector(':scope > [data-modal-body]');
+    const children = [...dialog.children].map(child => {
+      if (child.hasAttribute('data-modal-header')) return 'header';
+      if (child.hasAttribute('data-modal-body')) return 'body';
+      if (child.hasAttribute('data-modal-footer')) return 'footer';
+      return child.localName;
+    });
+    const style = getComputedStyle(dialog);
+    const bodyStyle = getComputedStyle(body);
+    return {
+      bodyScrollable: ['auto', 'scroll'].includes(bodyStyle.overflowY),
+      borderRadius: style.borderRadius,
+      children,
+      fontFamily: style.fontFamily,
+      labelled: dialog.getAttribute('aria-labelledby') ===
+        dialog.querySelector('[data-modal-title]').id,
+      overflowing: body.scrollHeight > body.clientHeight,
+      shadow: style.boxShadow,
+      scrollPrimitive: body.localName,
+      scrollbarColor: bodyStyle.scrollbarColor,
+      scrollbarGutter: bodyStyle.scrollbarGutter,
+      width: dialog.getBoundingClientRect().width,
+    };
+  });
+  assert.deepEqual(contentMetrics.children, ['header', 'body', 'footer']);
+  assert.equal(contentMetrics.bodyScrollable, true);
+  assert.equal(contentMetrics.borderRadius, '0px');
+  assert.equal(contentMetrics.labelled, true);
+  assert.equal(contentMetrics.fontFamily, '"Pixel Mplus"');
+  assert.equal(contentMetrics.overflowing, true);
+  assert.equal(contentMetrics.scrollPrimitive, 'zen-scroll-area');
+  assert.notEqual(contentMetrics.scrollbarColor, 'auto');
+  assert.match(contentMetrics.scrollbarGutter, /stable/);
+  assert.notEqual(contentMetrics.shadow, 'none');
+  assert.ok(contentMetrics.width <= 864);
+  await page.getByRole('button', { name: 'Close article' }).click();
+  await contentDialog.waitFor({ state: 'hidden' });
+
+  await page.getByRole('button', { name: 'Open workspace' }).click();
+  const windowDialog = page.locator('mjj-window-modal dialog');
+  await windowDialog.waitFor({ state: 'visible' });
+  await windowDialog.evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
+  const windowMetrics = await windowDialog.evaluate(dialog => {
+    const body = dialog.querySelector(':scope > [data-modal-body]');
+    const bodyStyle = getComputedStyle(body);
+    const primary = dialog.querySelector('[data-modal-pane="primary"]');
+    return {
+      bodyColumns: bodyStyle.gridTemplateColumns,
+      bodyScrollable: ['auto', 'scroll'].includes(bodyStyle.overflowY),
+      borderRadius: getComputedStyle(dialog).borderRadius,
+      hasStrictRegions:
+        Boolean(dialog.querySelector(':scope > [data-modal-header]')) &&
+        Boolean(body) &&
+        Boolean(dialog.querySelector(':scope > [data-modal-footer]')),
+      height: dialog.getBoundingClientRect().height,
+      primaryShadow: getComputedStyle(primary).boxShadow,
+      primaryOverflowing: primary.scrollHeight > primary.clientHeight,
+      primaryScrollColor: getComputedStyle(primary).scrollbarColor,
+      scrollPrimitive: body.localName,
+      scrollPrimitives:
+        dialog.querySelectorAll('zen-scroll-area[data-cyber-scroll]').length,
+      scrollbarColor: bodyStyle.scrollbarColor,
+      scrollbarGutter: bodyStyle.scrollbarGutter,
+      shadow: getComputedStyle(dialog).boxShadow,
+    };
+  });
+  assert.equal(windowMetrics.bodyScrollable, true);
+  assert.equal(windowMetrics.borderRadius, '0px');
+  assert.equal(windowMetrics.hasStrictRegions, true);
+  assert.equal(windowMetrics.scrollPrimitive, 'zen-scroll-area');
+  assert.notEqual(windowMetrics.scrollbarColor, 'auto');
+  assert.match(windowMetrics.scrollbarGutter, /stable/);
+  assert.match(windowMetrics.bodyColumns, /\d/);
+  assert.ok(windowMetrics.height <= 768);
+  assert.notEqual(windowMetrics.primaryShadow, 'none');
+  assert.equal(windowMetrics.primaryOverflowing, true);
+  assert.notEqual(windowMetrics.primaryScrollColor, 'auto');
+  assert.equal(windowMetrics.scrollPrimitives, 3);
+  assert.notEqual(windowMetrics.shadow, 'none');
+  await page.getByRole('button', { name: 'Close workspace' }).click();
+  await windowDialog.waitFor({ state: 'hidden' });
+  assert.deepEqual(errors, []);
+  await context.close();
+
+  const mobileContext = await browser.newContext({
+    serviceWorkers: 'block',
+    viewport: { width: 390, height: 844 },
+  });
+  const mobilePage = await mobileContext.newPage();
+  await mobilePage.goto(`${baseUrl}/public/component-sandbox.html`, {
+    waitUntil: 'networkidle',
+  });
+  await mobilePage.evaluate(() => document.fonts.ready);
+  await mobilePage.getByRole('button', { name: 'Open workspace' }).click();
+  const mobileDialog = mobilePage.locator('mjj-window-modal dialog');
+  await mobileDialog.waitFor({ state: 'visible' });
+  await mobileDialog.evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
+  const mobileMetrics = await mobileDialog.evaluate(dialog => {
+    const body = dialog.querySelector('[data-modal-body]');
+    const dialogBox = dialog.getBoundingClientRect();
+    return {
+      bodyColumns: getComputedStyle(body).gridTemplateColumns,
+      fits:
+        dialogBox.left >= 0 &&
+        dialogBox.top >= 0 &&
+        dialogBox.right <= innerWidth &&
+        dialogBox.bottom <= innerHeight,
+      pageFits: document.documentElement.scrollWidth <= innerWidth,
+    };
+  });
+  assert.equal(mobileMetrics.fits, true);
+  assert.equal(mobileMetrics.pageFits, true);
+  assert.doesNotMatch(mobileMetrics.bodyColumns, /\s/);
+  assert.equal(
+    await mobilePage.locator('mjj-window-modal').getAttribute(
+      'data-component-layer',
+    ),
+    '2',
+  );
+  await mobileContext.close();
+}
+
 async function testJrpgPage(browser) {
   const context = await browser.newContext({
     colorScheme: 'light',
@@ -547,6 +714,30 @@
     }
   });
 
+  for (const expected of [
+    { panel: 'resume', title: 'Resume' },
+    { panel: 'tools', title: 'Tools' },
+    { panel: 'blog', title: 'Blogs' },
+  ]) {
+    const html = await (
+      await fetch(`${baseUrl}/jrpg?panel=${expected.panel}`)
+    ).text();
+    assert.match(
+      html,
+      new RegExp(`data-initial-panel="${expected.panel}"`),
+    );
+    assert.match(html, new RegExp(`data-preview-title>${expected.title}<`));
+    assert.doesNotMatch(html, /__MJJ_[A-Z_]+__/);
+  }
+  const conversationHtml = await (
+    await fetch(`${baseUrl}/jrpg?panel=conversations`)
+  ).text();
+  assert.match(conversationHtml, /data-initial-panel="conversations"/);
+  assert.match(
+    conversationHtml,
+    /<mjj-conversation-archive\s+aria-label="Conversations"/,
+  );
+
   await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
   await page.waitForFunction(() =>
     customElements.get('mjj-composer') &&
@@ -554,6 +745,26 @@
     customElements.get('mjj-jrpg-chat') &&
     customElements.get('mjj-jrpg-preview')
   );
+  await page.waitForFunction(() =>
+    [...document.querySelectorAll('.jrpg-workspace dialog')].every(dialog => {
+      const owner = dialog.closest('mjj-content-modal, mjj-window-modal');
+      return owner?.hasAttribute('data-ready');
+    })
+  );
+  assert.deepEqual(
+    await page.evaluate(() =>
+      [...document.querySelectorAll('.jrpg-workspace dialog')]
+        .map(dialog =>
+          dialog.closest('mjj-content-modal, mjj-window-modal')?.localName)),
+    [
+      'mjj-window-modal',
+      'mjj-content-modal',
+      'mjj-content-modal',
+      'mjj-window-modal',
+      'mjj-window-modal',
+    ],
+    'every JRPG dialog has a Layer 2 owner',
+  );
   await page.evaluate(() => document.fonts.ready);
   const desktop = await page.evaluate(() => {
     const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
@@ -1067,6 +1278,36 @@
     await page.locator('button[data-preview="conversations"]').getAttribute('aria-pressed'),
     'true',
   );
+  await page.evaluate(() => {
+    document.querySelector('mjj-conversation-archive')?.addConversation({
+      id: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
+      title: 'Modal migration fixture',
+      turn_count: 2,
+    });
+  });
+  await page.getByRole('button', {
+    name: 'Rename "Modal migration fixture"',
+  }).click();
+  await page.locator('[data-archive-rename-dialog]').waitFor({ state: 'visible' });
+  assert.equal(
+    await page.locator('[data-archive-rename-dialog]').evaluate(dialog =>
+      dialog.closest('mjj-window-modal')?.localName),
+    'mjj-window-modal',
+  );
+  await page.locator('[data-archive-rename-cancel]').click();
+  await page.locator('[data-archive-rename-dialog]').waitFor({ state: 'hidden' });
+
+  await page.getByRole('button', {
+    name: 'Delete "Modal migration fixture"',
+  }).click();
+  await page.locator('[data-archive-delete-dialog]').waitFor({ state: 'visible' });
+  assert.equal(
+    await page.locator('[data-archive-delete-dialog]').evaluate(dialog =>
+      dialog.closest('mjj-content-modal')?.localName),
+    'mjj-content-modal',
+  );
+  await page.locator('[data-archive-delete-cancel]').click();
+  await page.locator('[data-archive-delete-dialog]').waitFor({ state: 'hidden' });
 
   // Archive: close via close button, navigates back to resume panel
   await page.locator('[data-archive-close]').click();
@@ -1128,10 +1369,7 @@
     await page.locator('[data-preview-title]').textContent(),
     'Tools',
   );
-  assert.equal(
-    await page.locator('[data-preview-link]').getAttribute('href'),
-    '/tools',
-  );
+  assert.equal(await page.locator('[data-preview-link]').count(), 0);
   assert.equal(
     await page.getByRole('button', { name: 'Tools', exact: true })
       .getAttribute('aria-pressed'),
@@ -1151,13 +1389,15 @@
   await page.locator('[data-latest-tool]').first().click();
   await page.locator('[data-functional-tool="markdown"]').waitFor();
   assert.equal(
+    await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
+      dialog.closest('mjj-content-modal, mjj-window-modal')?.localName),
+    'mjj-window-modal',
+    'tool details use the window modal',
+  );
+  assert.equal(
     await page.locator('[data-dialog-title]').textContent(),
     'MarkDown to HTML',
   );
-  assert.equal(
-    await page.locator('[data-preview-link]').getAttribute('href'),
-    '/tools/markdown_to_html',
-  );
   await page.locator('[data-markdown-source]').fill('# Modal Markdown');
   await page.waitForFunction(() =>
     document.querySelector('[data-markdown-output] h1')?.textContent ===
@@ -1173,27 +1413,18 @@
       .map(window => window.getBoundingClientRect());
     const windowShadows = [...document.querySelectorAll('.jrpg-tool-window')]
       .map(window => getComputedStyle(window).boxShadow);
-    const rail = getComputedStyle(
-      document.querySelector('.jrpg-tool-panes'),
-      '::before',
-    );
     return {
       dialogHeight: dialogElement.offsetHeight,
       dialogWidth: dialogElement.offsetWidth,
-      fillsViewport: dialogElement.offsetWidth > innerWidth * 0.9 &&
-        dialogElement.offsetHeight > innerHeight * 0.9,
+      compact: dialogElement.offsetWidth <= innerWidth * 0.9 &&
+        dialogElement.offsetHeight <= innerHeight * 0.8,
       viewportHeight: innerHeight,
       viewportWidth: innerWidth,
-      sideBySide: windows.length === 2 &&
-        windows[0].right < windows[1].left &&
-        Math.abs(windows[0].top - windows[1].top) < 1,
-      fullPageInHeader: Boolean(
-        document.querySelector('[data-preview-link]')
-          .closest('.jrpg-dialog-heading-actions'),
-      ),
-      footerHidden: document.querySelector('[data-dialog-actions]').hidden,
-      metalRail: rail.content !== 'none' &&
-        rail.backgroundImage.includes('bar-ink.webp'),
+      stacked: windows.length === 2 &&
+        windows[0].bottom < windows[1].top &&
+        Math.abs(windows[0].left - windows[1].left) < 1,
+      noOpenPageAction: !document.querySelector('[data-preview-link]'),
+      noFooter: !document.querySelector('[data-dialog-actions]'),
       pixelIcons: document.querySelectorAll('.jrpg-tool-window zen-icon').length >= 2 &&
         [...document.querySelectorAll('.jrpg-tool-window zen-icon svg')]
           .every(icon =>
@@ -1201,21 +1432,19 @@
           ),
       separateGlows: windowShadows.length === 2 &&
         windowShadows.every(shadow => shadow !== 'none'),
-      wideWindows: windows.every(window => window.width > dialog.width * 0.35),
+      wideWindows: windows.every(window => window.width > dialog.width * 0.8),
     };
   });
-  assert.equal(
-    toolWorkspace.fillsViewport,
-    true,
-    JSON.stringify(toolWorkspace),
-  );
-  assert.equal(toolWorkspace.sideBySide, true);
-  assert.equal(toolWorkspace.fullPageInHeader, true);
-  assert.equal(toolWorkspace.footerHidden, true);
-  assert.equal(toolWorkspace.metalRail, true);
+  assert.equal(toolWorkspace.compact, true, JSON.stringify(toolWorkspace));
+  assert.equal(toolWorkspace.stacked, true);
+  assert.equal(toolWorkspace.noOpenPageAction, true);
+  assert.equal(toolWorkspace.noFooter, true);
   assert.equal(toolWorkspace.pixelIcons, true);
   assert.equal(toolWorkspace.separateGlows, true);
   assert.equal(toolWorkspace.wideWindows, true);
+  await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
   await page.getByRole('button', {
     name: 'Close destination details',
   }).click();
@@ -1283,10 +1512,6 @@
 
   await page.locator('[data-latest-tool]').nth(2).click();
   await page.locator('[data-functional-tool="hls"]').waitFor();
-  assert.equal(
-    await page.locator('[data-preview-link]').getAttribute('href'),
-    '/tools/hls_player',
-  );
   await page.locator('[data-hls-sample]').click();
   await page.waitForFunction(() =>
     ['ready', 'error'].includes(
@@ -1336,10 +1561,6 @@
     await page.locator('[data-tool-content]').textContent(),
     /private browser-based writing workspace/,
   );
-  assert.equal(
-    await page.locator('[data-preview-link]').getAttribute('href'),
-    '/notes',
-  );
   await page.getByRole('button', {
     name: 'Close destination details',
   }).click();
@@ -1347,6 +1568,12 @@
 
   await page.getByRole('button', { name: 'Resume', exact: true }).click();
   await page.locator('[data-resume-modal]').click();
+  assert.equal(
+    await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
+      dialog.closest('mjj-content-modal, mjj-window-modal')?.localName),
+    'mjj-content-modal',
+    'resume details use the content modal',
+  );
   await page.locator('[data-resume-content] .sub-header').first().waitFor();
   const resumeDossier = await page.locator('[data-resume-dossier]').evaluate(
     dossier => ({
@@ -1358,6 +1585,46 @@
   assert.match(resumeDossier.text, /Copilot Tasks/);
   assert.match(resumeDossier.text, /Microsoft/);
   assert.match(resumeDossier.text, /Meta/);
+  const resumeModalGeometry = await page.locator('.jrpg-detail-dialog').evaluate(
+    dialog => {
+      const body = dialog.querySelector('[data-modal-body]').getBoundingClientRect();
+      const dossier = dialog.querySelector('[data-resume-dossier]')
+        .getBoundingClientRect();
+      const buttons = [...dialog.querySelectorAll(
+        '[data-modal-header] zen-button > :is(button, a)',
+      )].map(control => control.getBoundingClientRect());
+      return {
+        bodyFilled:
+          dossier.width / body.width > 0.9 &&
+          Math.abs(dossier.left - body.left) < 32,
+        equalHeaderControls:
+          buttons.length === 2 &&
+          Math.abs(buttons[0].height - buttons[1].height) < 1,
+        headerControlHeights: buttons.map(button => button.height),
+        footerAbsent: !dialog.querySelector('[data-modal-footer]'),
+        headerPdfLinks: dialog.querySelectorAll(
+          '[data-modal-header] a[href="/public/resume.pdf"]',
+        ).length,
+        visibleBodyPdfLinks: [...dialog.querySelectorAll(
+          '[data-modal-body] a[href="/public/resume.pdf"]',
+        )].filter(link => getComputedStyle(link).display !== 'none').length,
+        pdfLinks: dialog.querySelectorAll('a[href="/public/resume.pdf"]').length,
+      };
+    },
+  );
+  assert.equal(resumeModalGeometry.bodyFilled, true);
+  assert.equal(
+    resumeModalGeometry.equalHeaderControls,
+    true,
+    JSON.stringify(resumeModalGeometry),
+  );
+  assert.equal(resumeModalGeometry.footerAbsent, true);
+  assert.equal(resumeModalGeometry.headerPdfLinks, 1);
+  assert.equal(resumeModalGeometry.visibleBodyPdfLinks, 0);
+  assert.equal(resumeModalGeometry.pdfLinks, 1);
+  await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
   assert.equal(
     await page.locator('[data-resume-content] a[target="_blank"]').count() > 0,
     true,
@@ -1386,15 +1653,21 @@
     'WebSocket Demystified',
   );
   assert.equal(
-    await page.locator('[data-preview-link]').getAttribute('href'),
-    '/blog/websocket-demystified',
-  );
-  assert.equal(
     await page.locator('[data-blog-content]').evaluate(
       article => getComputedStyle(article).fontFamily,
     ),
     '"Pixel Mplus"',
   );
+  const blogWidth = await page.locator('.jrpg-detail-dialog').evaluate(dialog => {
+    const body = dialog.querySelector('[data-modal-body]').getBoundingClientRect();
+    const browser = dialog.querySelector('[data-blog-browser]')
+      .getBoundingClientRect();
+    return browser.width / body.width;
+  });
+  assert.ok(blogWidth > 0.9);
+  await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
   await page.getByRole('button', {
     name: 'Close destination details',
   }).click();
@@ -1410,10 +1683,6 @@
     document.querySelector('[data-blog-content] h1')?.textContent ===
       'Creating Network Library in C'
   );
-  assert.equal(
-    await page.locator('[data-preview-link]').getAttribute('href'),
-    '/blog/my-seobeo-journey',
-  );
   await page.getByRole('button', {
     name: 'Close destination details',
   }).click();
@@ -1689,6 +1958,7 @@
       ).matches,
       menuInModal: menuElement.parentElement === destinationContent,
       menuHidden: menu.width === 0 && menu.height === 0,
+      modalOwner: destinationContent.closest('mjj-window-modal')?.localName,
       mobileArtIsCorrect,
       noOverflow: document.documentElement.scrollWidth <= innerWidth,
       sendButtonCompact:
@@ -1720,6 +1990,10 @@
       utilityHidden: utility.width === 0 && utility.height === 0,
       utilityInModal: document.querySelector('.jrpg-utility').parentElement ===
         destinationContent,
+      usesCyberScroll:
+        destinationContent.localName === 'zen-scroll-area' &&
+        destinationContent.hasAttribute('data-cyber-scroll') &&
+        getComputedStyle(destinationContent).scrollbarColor !== 'auto',
       usesDesignSystemControls:
         !composerField.hasAttribute('appearance') &&
         !sendOwner.hasAttribute('appearance'),
@@ -1762,6 +2036,8 @@
   );
   assert.equal(mobile.utilityHidden, true, 'destination view stays out of the frame');
   assert.equal(mobile.utilityInModal, true, 'destination view is hosted by the mobile modal');
+  assert.equal(mobile.modalOwner, 'mjj-window-modal');
+  assert.equal(mobile.usesCyberScroll, true, 'JRPG modal uses the shared cyber scroll area');
   assert.equal(mobile.usesDesignSystemControls, true, 'composer uses styled Zenbu controls');
   assert.equal(mobile.workspaceFills, true, 'workspace fills full viewport');
   assert.equal(mobile.topLeftClear, true, 'hidden dialog triggers do not cover frame art');
@@ -1771,6 +2047,9 @@
   await mobilePage.locator('[data-mobile-destination-dialog]').waitFor({
     state: 'visible',
   });
+  await mobilePage.locator('[data-mobile-destination-dialog]').evaluate(dialog =>
+    Promise.all(dialog.getAnimations().map(animation => animation.finished))
+  );
   assert.equal(
     await mobilePage.locator('[data-mobile-menu-toggle]')
       .getAttribute('aria-expanded'),
@@ -2810,7 +3089,7 @@
     for (const source of [home, dogGame, manifest]) {
       assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
     }
-    assert.match(serviceWorker, /v35-role-greeting/);
+    assert.match(serviceWorker, /v37-network-first/);
     assert.match(
       await (
         await fetch(`${baseUrl}/public/pwa-register.js`)
@@ -2820,6 +3099,18 @@
     assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')"));
     assert.ok(serviceWorker.includes("url.hostname === 'localhost'"));
     assert.ok(serviceWorker.includes("url.hostname === '127.0.0.1'"));
+    assert.ok(serviceWorker.includes("DEVELOPMENT_SANDBOX_PATHS"));
+    assert.ok(serviceWorker.includes("'/public/component-sandbox'"));
+    assert.ok(serviceWorker.includes("'/public/mjj-modal.'"));
+    assert.ok(serviceWorker.includes('networkFirst(request, acceptsHtml)'));
+    assert.ok(serviceWorker.includes('staleWhileRevalidate(event, request)'));
+    assert.ok(serviceWorker.includes("fetch(request, { cache: 'no-cache' })"));
+    assert.doesNotMatch(
+      await (
+        await fetch(`${baseUrl}/public/pwa-register.js`)
+      ).text(),
+      /confirm\(/,
+    );
     assert.ok(!serviceWorker.includes("startsWith('/jrpg')"));
 
     for (const asset of [
@@ -2877,6 +3168,7 @@
       await testDynamicButtonOwnership(browser);
       await testResumePrint(browser);
       await testComposerLab(browser);
+      await testModalSandbox(browser);
     }
     if (runsSuite('jrpg')) await testJrpgPage(browser);
     if (runsSuite('routing')) await testConversationRouting(browser);